From 5677317bcef6f44b2e97facfa36fbc319b052326 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 19 May 2025 22:04:00 +0530
Subject: [PATCH 1/7] content [nfc]: Remove the `inline` property in _Katex
widget
And inline the behaviour for `inline: false` in MathBlock widget.
---
lib/widgets/content.dart | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 5f0171b5a5..bd4ade091f 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -822,7 +822,13 @@ class MathBlock extends StatelessWidget {
children: [TextSpan(text: node.texSource)])));
}
- return _Katex(inline: false, nodes: nodes);
+ return Center(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: SingleChildScrollViewWithScrollbar(
+ scrollDirection: Axis.horizontal,
+ child: _Katex(
+ nodes: nodes))));
}
}
@@ -835,24 +841,15 @@ const kBaseKatexTextStyle = TextStyle(
class _Katex extends StatelessWidget {
const _Katex({
- required this.inline,
required this.nodes,
});
- final bool inline;
final List nodes;
@override
Widget build(BuildContext context) {
Widget widget = _KatexNodeList(nodes: nodes);
- if (!inline) {
- widget = Center(
- child: SingleChildScrollViewWithScrollbar(
- scrollDirection: Axis.horizontal,
- child: widget));
- }
-
return Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
@@ -1274,7 +1271,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _Katex(inline: true, nodes: nodes));
+ child: _Katex(nodes: nodes));
case GlobalTimeNode():
return WidgetSpan(alignment: PlaceholderAlignment.middle,
From b38301fba0bdccb43ccf592493d10475ccfcb9dd Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 7 Jul 2025 14:22:54 +0530
Subject: [PATCH 2/7] content test: Add offset and size based widget tests for
KaTeX content
And remove font and fontsize based tests, as the newer rect
offset/size based tests are more accurate anyway.
---
lib/widgets/content.dart | 10 +-
test/widgets/content_test.dart | 190 +++++++++++++++++----------------
2 files changed, 106 insertions(+), 94 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index bd4ade091f..edc11d473c 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -827,7 +827,7 @@ class MathBlock extends StatelessWidget {
textDirection: TextDirection.ltr,
child: SingleChildScrollViewWithScrollbar(
scrollDirection: Axis.horizontal,
- child: _Katex(
+ child: KatexWidget(
nodes: nodes))));
}
}
@@ -839,8 +839,10 @@ const kBaseKatexTextStyle = TextStyle(
fontFamily: 'KaTeX_Main',
height: 1.2);
-class _Katex extends StatelessWidget {
- const _Katex({
+@visibleForTesting
+class KatexWidget extends StatelessWidget {
+ const KatexWidget({
+ super.key,
required this.nodes,
});
@@ -1271,7 +1273,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _Katex(nodes: nodes));
+ child: KatexWidget(nodes: nodes));
case GlobalTimeNode():
return WidgetSpan(alignment: PlaceholderAlignment.middle,
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index 9ed263fce9..c87668514c 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_checks/flutter_checks.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -572,98 +573,65 @@ void main() {
tester.widget(find.text('λ', findRichText: true));
});
- void checkKatexText(
- WidgetTester tester,
- String text, {
- required String fontFamily,
- required double fontSize,
- required double fontHeight,
- }) {
- check(mergedStyleOf(tester, text)).isNotNull()
- ..fontFamily.equals(fontFamily)
- ..fontSize.equals(fontSize);
- check(tester.getSize(find.text(text)))
- .height.isCloseTo(fontSize * fontHeight, 0.5);
- }
-
- testWidgets('displays KaTeX content with different sizing', (tester) async {
- addTearDown(testBinding.reset);
- final globalSettings = testBinding.globalStore.settings;
- await globalSettings.setBool(BoolGlobalSetting.renderKatex, true);
- check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isTrue();
-
- final content = ContentExample.mathBlockKatexSizing;
- await prepareContent(tester, plainContent(content.html));
-
- final mathBlockNode = content.expectedNodes.single as MathBlockNode;
- final baseNode = mathBlockNode.nodes!.single as KatexSpanNode;
- final nodes = baseNode.nodes!.skip(1); // Skip .strut node.
- for (var katexNode in nodes) {
- katexNode = katexNode as KatexSpanNode;
- final fontSize = katexNode.styles.fontSizeEm! * kBaseKatexTextStyle.fontSize!;
- checkKatexText(tester, katexNode.text!,
- fontFamily: 'KaTeX_Main',
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ group('characters render at specific offsets with specific size', () {
+ const testCases = <(ContentExample, List<(String, Offset, Size)>, {bool? skip})>[
+ (ContentExample.mathBlockKatexSizing, skip: false, [
+ ('1', Offset(0.00, 2.24), Size(25.59, 61.00)),
+ ('2', Offset(25.59, 9.90), Size(21.33, 51.00)),
+ ('3', Offset(46.91, 16.30), Size(17.77, 43.00)),
+ ('4', Offset(64.68, 21.63), Size(14.80, 36.00)),
+ ('5', Offset(79.48, 26.07), Size(12.34, 30.00)),
+ ('6', Offset(91.82, 29.77), Size(10.28, 25.00)),
+ ('7', Offset(102.10, 31.62), Size(9.25, 22.00)),
+ ('8', Offset(111.35, 33.47), Size(8.23, 20.00)),
+ ('9', Offset(119.58, 35.32), Size(7.20, 17.00)),
+ ('0', Offset(126.77, 39.02), Size(5.14, 12.00)),
+ ]),
+ (ContentExample.mathBlockKatexNestedSizing, skip: false, [
+ ('1', Offset(0.00, 39.58), Size(5.14, 12.00)),
+ ('2', Offset(5.14, 2.80), Size(25.59, 61.00)),
+ ]),
+ // TODO: Re-enable this test after adding support for parsing
+ // `vertical-align` in inline styles. Currently it fails
+ // because `strut` span has `vertical-align`.
+ (ContentExample.mathBlockKatexDelimSizing, skip: true, [
+ ('(', Offset(8.00, 46.36), Size(9.42, 25.00)),
+ ('[', Offset(17.42, 48.36), Size(9.71, 25.00)),
+ ('⌈', Offset(27.12, 49.36), Size(11.99, 25.00)),
+ ('⌊', Offset(39.11, 49.36), Size(13.14, 25.00)),
+ ]),
+ ];
+
+ for (final testCase in testCases) {
+ testWidgets(testCase.$1.description, (tester) async {
+ await _loadKatexFonts();
+
+ addTearDown(testBinding.reset);
+ final globalSettings = testBinding.globalStore.settings;
+ await globalSettings.setBool(BoolGlobalSetting.renderKatex, true);
+ check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isTrue();
+
+ await prepareContent(tester, plainContent(testCase.$1.html));
+
+ final baseRect = tester.getRect(find.byType(KatexWidget));
+
+ for (final characterData in testCase.$2) {
+ final character = characterData.$1;
+ final expectedTopLeftOffset = characterData.$2;
+ final expectedSize = characterData.$3;
+
+ final rect = tester.getRect(find.text(character));
+ final topLeftOffset = rect.topLeft - baseRect.topLeft;
+ final size = rect.size;
+
+ check(topLeftOffset)
+ .within(distance: 0.05, from: expectedTopLeftOffset);
+ check(size)
+ .within(distance: 0.05, from: expectedSize);
+ }
+ }, skip: testCase.skip);
}
});
-
- testWidgets('displays KaTeX content with nested sizing', (tester) async {
- addTearDown(testBinding.reset);
- final globalSettings = testBinding.globalStore.settings;
- await globalSettings.setBool(BoolGlobalSetting.renderKatex, true);
- check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isTrue();
-
- final content = ContentExample.mathBlockKatexNestedSizing;
- await prepareContent(tester, plainContent(content.html));
-
- var fontSize = 0.5 * kBaseKatexTextStyle.fontSize!;
- checkKatexText(tester, '1',
- fontFamily: 'KaTeX_Main',
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
-
- fontSize = 4.976 * fontSize;
- checkKatexText(tester, '2',
- fontFamily: 'KaTeX_Main',
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
- });
-
- testWidgets('displays KaTeX content with different delimiter sizing', (tester) async {
- addTearDown(testBinding.reset);
- final globalSettings = testBinding.globalStore.settings;
- await globalSettings.setBool(BoolGlobalSetting.renderKatex, true);
- check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isTrue();
-
- final content = ContentExample.mathBlockKatexDelimSizing;
- await prepareContent(tester, plainContent(content.html));
-
- final mathBlockNode = content.expectedNodes.single as MathBlockNode;
- final baseNode = mathBlockNode.nodes!.single as KatexSpanNode;
- var nodes = baseNode.nodes!.skip(1); // Skip .strut node.
-
- final fontSize = kBaseKatexTextStyle.fontSize!;
-
- final firstNode = nodes.first as KatexSpanNode;
- checkKatexText(tester, firstNode.text!,
- fontFamily: 'KaTeX_Main',
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
- nodes = nodes.skip(1);
-
- for (var katexNode in nodes) {
- katexNode = katexNode as KatexSpanNode;
- katexNode = katexNode.nodes!.single as KatexSpanNode; // Skip empty .mord parent.
- final fontFamily = katexNode.styles.fontFamily!;
- checkKatexText(tester, katexNode.text!,
- fontFamily: fontFamily,
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
- }
- }, skip: true); // TODO: Re-enable this test after adding support for parsing
- // `vertical-align` in inline styles. Currently it fails
- // because `strut` span has `vertical-align`.
});
/// Make a [TargetFontSizeFinder] to pass to [checkFontSizeRatio],
@@ -1432,3 +1400,45 @@ void main() {
});
});
}
+
+Future _loadKatexFonts() async {
+ const fonts = {
+ 'KaTeX_AMS': ['KaTeX_AMS-Regular.ttf'],
+ 'KaTeX_Caligraphic': [
+ 'KaTeX_Caligraphic-Regular.ttf',
+ 'KaTeX_Caligraphic-Bold.ttf',
+ ],
+ 'KaTeX_Fraktur': [
+ 'KaTeX_Fraktur-Regular.ttf',
+ 'KaTeX_Fraktur-Bold.ttf',
+ ],
+ 'KaTeX_Main': [
+ 'KaTeX_Main-Regular.ttf',
+ 'KaTeX_Main-Bold.ttf',
+ 'KaTeX_Main-Italic.ttf',
+ 'KaTeX_Main-BoldItalic.ttf',
+ ],
+ 'KaTeX_Math': [
+ 'KaTeX_Math-Italic.ttf',
+ 'KaTeX_Math-BoldItalic.ttf',
+ ],
+ 'KaTeX_SansSerif': [
+ 'KaTeX_SansSerif-Regular.ttf',
+ 'KaTeX_SansSerif-Bold.ttf',
+ 'KaTeX_SansSerif-Italic.ttf',
+ ],
+ 'KaTeX_Script': ['KaTeX_Script-Regular.ttf'],
+ 'KaTeX_Size1': ['KaTeX_Size1-Regular.ttf'],
+ 'KaTeX_Size2': ['KaTeX_Size2-Regular.ttf'],
+ 'KaTeX_Size3': ['KaTeX_Size3-Regular.ttf'],
+ 'KaTeX_Size4': ['KaTeX_Size4-Regular.ttf'],
+ 'KaTeX_Typewriter': ['KaTeX_Typewriter-Regular.ttf'],
+ };
+ for (final MapEntry(key: fontFamily, value: fontFiles) in fonts.entries) {
+ final fontLoader = FontLoader(fontFamily);
+ for (final fontFile in fontFiles) {
+ fontLoader.addFont(rootBundle.load('assets/KaTeX/$fontFile'));
+ }
+ await fontLoader.load();
+ }
+}
From bd353ef8bd5f3b5926b76a030d04d0f1f35be444 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 7 Jul 2025 23:45:21 +0530
Subject: [PATCH 3/7] content: Add a workaround for incorrect sizing in
WidgetSpan, for KaTeX
---
lib/widgets/content.dart | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index edc11d473c..543352e0bb 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -873,9 +873,15 @@ class _KatexNodeList extends StatelessWidget {
return WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: switch (e) {
- KatexSpanNode() => _KatexSpan(e),
- });
+ // Work around a bug where text inside a WidgetSpan could be scaled
+ // multiple times incorrectly, if the system font scale is larger
+ // than 1x.
+ // See: https://github.com/flutter/flutter/issues/126962
+ child: MediaQuery(
+ data: MediaQueryData(textScaler: TextScaler.noScaling),
+ child: switch (e) {
+ KatexSpanNode() => _KatexSpan(e),
+ }));
}))));
}
}
From e8e8f410570712e34dd228b09e7b7b0d0eb10780 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Wed, 9 Jul 2025 02:22:19 +0530
Subject: [PATCH 4/7] content: Update base KaTeX text style to be explicit
This fixes a bug about `leadingDistribution` where previously it
was taking the default value of
`TextLeadingDistribution.proportional`, now it uses
`TextLeadingDistribution.even` which seems to be the default
strategy used by CSS, and it doesn't look like `katex.scss`
overrides it.
The vertical offsets being updated in tests are because of this fix.
Another potential bug fix is about `textBaseline` where on some
locale systems the default value for this could be
`TextBaseline.ideographic`, and for KaTeX we always want
`TextBaseline.alphabetic`.
---
lib/widgets/content.dart | 8 +++++++-
test/widgets/content_test.dart | 26 +++++++++++++-------------
2 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 543352e0bb..7f018a5c46 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -837,7 +837,13 @@ class MathBlock extends StatelessWidget {
const kBaseKatexTextStyle = TextStyle(
fontSize: kBaseFontSize * 1.21,
fontFamily: 'KaTeX_Main',
- height: 1.2);
+ height: 1.2,
+ fontWeight: FontWeight.normal,
+ fontStyle: FontStyle.normal,
+ textBaseline: TextBaseline.alphabetic,
+ leadingDistribution: TextLeadingDistribution.even,
+ decoration: TextDecoration.none,
+ fontFamilyFallback: []);
@visibleForTesting
class KatexWidget extends StatelessWidget {
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index c87668514c..d09fc5a989 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -577,18 +577,18 @@ void main() {
const testCases = <(ContentExample, List<(String, Offset, Size)>, {bool? skip})>[
(ContentExample.mathBlockKatexSizing, skip: false, [
('1', Offset(0.00, 2.24), Size(25.59, 61.00)),
- ('2', Offset(25.59, 9.90), Size(21.33, 51.00)),
- ('3', Offset(46.91, 16.30), Size(17.77, 43.00)),
- ('4', Offset(64.68, 21.63), Size(14.80, 36.00)),
- ('5', Offset(79.48, 26.07), Size(12.34, 30.00)),
- ('6', Offset(91.82, 29.77), Size(10.28, 25.00)),
- ('7', Offset(102.10, 31.62), Size(9.25, 22.00)),
- ('8', Offset(111.35, 33.47), Size(8.23, 20.00)),
- ('9', Offset(119.58, 35.32), Size(7.20, 17.00)),
- ('0', Offset(126.77, 39.02), Size(5.14, 12.00)),
+ ('2', Offset(25.59, 10.04), Size(21.33, 51.00)),
+ ('3', Offset(46.91, 16.55), Size(17.77, 43.00)),
+ ('4', Offset(64.68, 21.98), Size(14.80, 36.00)),
+ ('5', Offset(79.48, 26.50), Size(12.34, 30.00)),
+ ('6', Offset(91.82, 30.26), Size(10.28, 25.00)),
+ ('7', Offset(102.10, 32.15), Size(9.25, 22.00)),
+ ('8', Offset(111.35, 34.03), Size(8.23, 20.00)),
+ ('9', Offset(119.58, 35.91), Size(7.20, 17.00)),
+ ('0', Offset(126.77, 39.68), Size(5.14, 12.00)),
]),
(ContentExample.mathBlockKatexNestedSizing, skip: false, [
- ('1', Offset(0.00, 39.58), Size(5.14, 12.00)),
+ ('1', Offset(0.00, 40.24), Size(5.14, 12.00)),
('2', Offset(5.14, 2.80), Size(25.59, 61.00)),
]),
// TODO: Re-enable this test after adding support for parsing
@@ -596,9 +596,9 @@ void main() {
// because `strut` span has `vertical-align`.
(ContentExample.mathBlockKatexDelimSizing, skip: true, [
('(', Offset(8.00, 46.36), Size(9.42, 25.00)),
- ('[', Offset(17.42, 48.36), Size(9.71, 25.00)),
- ('⌈', Offset(27.12, 49.36), Size(11.99, 25.00)),
- ('⌊', Offset(39.11, 49.36), Size(13.14, 25.00)),
+ ('[', Offset(17.42, 46.36), Size(9.71, 25.00)),
+ ('⌈', Offset(27.12, 46.36), Size(11.99, 25.00)),
+ ('⌊', Offset(39.11, 46.36), Size(13.14, 25.00)),
]),
];
From 0ec1641fbbc554abc2962356e1803e06b2da0bca Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 22 Apr 2025 18:01:46 +0530
Subject: [PATCH 5/7] content: Scale inline KaTeX content based on the
surrounding text
This applies the correct font scaling if the KaTeX content is
inside a header.
---
lib/widgets/content.dart | 37 +++++++++++++++++++++-------------
test/widgets/content_test.dart | 26 ++++++++++++++++++++++++
2 files changed, 49 insertions(+), 14 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 7f018a5c46..8a06988a12 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -828,30 +828,39 @@ class MathBlock extends StatelessWidget {
child: SingleChildScrollViewWithScrollbar(
scrollDirection: Axis.horizontal,
child: KatexWidget(
+ textStyle: ContentTheme.of(context).textStylePlainParagraph,
nodes: nodes))));
}
}
-// Base text style from .katex class in katex.scss :
-// https://github.com/KaTeX/KaTeX/blob/613c3da8/src/styles/katex.scss#L13-L15
-const kBaseKatexTextStyle = TextStyle(
- fontSize: kBaseFontSize * 1.21,
- fontFamily: 'KaTeX_Main',
- height: 1.2,
- fontWeight: FontWeight.normal,
- fontStyle: FontStyle.normal,
- textBaseline: TextBaseline.alphabetic,
- leadingDistribution: TextLeadingDistribution.even,
- decoration: TextDecoration.none,
- fontFamilyFallback: []);
+/// Creates a base text style for rendering KaTeX content.
+///
+/// This applies the CSS styles defined in .katex class in katex.scss :
+/// https://github.com/KaTeX/KaTeX/blob/613c3da8/src/styles/katex.scss#L13-L15
+///
+/// Requires the [style.fontSize] to be non-null.
+TextStyle mkBaseKatexTextStyle(TextStyle style) {
+ return style.copyWith(
+ fontSize: style.fontSize! * 1.21,
+ fontFamily: 'KaTeX_Main',
+ height: 1.2,
+ fontWeight: FontWeight.normal,
+ fontStyle: FontStyle.normal,
+ textBaseline: TextBaseline.alphabetic,
+ leadingDistribution: TextLeadingDistribution.even,
+ decoration: TextDecoration.none,
+ fontFamilyFallback: const []);
+}
@visibleForTesting
class KatexWidget extends StatelessWidget {
const KatexWidget({
super.key,
+ required this.textStyle,
required this.nodes,
});
+ final TextStyle textStyle;
final List nodes;
@override
@@ -861,7 +870,7 @@ class KatexWidget extends StatelessWidget {
return Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
- style: kBaseKatexTextStyle.copyWith(
+ style: mkBaseKatexTextStyle(textStyle).copyWith(
color: ContentTheme.of(context).textStylePlainParagraph.color),
child: widget));
}
@@ -1285,7 +1294,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: KatexWidget(nodes: nodes));
+ child: KatexWidget(textStyle: widget.style, nodes: nodes));
case GlobalTimeNode():
return WidgetSpan(alignment: PlaceholderAlignment.middle,
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index d09fc5a989..e0e4180c13 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -1042,6 +1042,32 @@ void main() {
testContentSmoke(ContentExample.mathInline);
testWidgets('maintains font-size ratio with surrounding text', (tester) async {
+ addTearDown(testBinding.reset);
+ final globalSettings = testBinding.globalStore.settings;
+ await globalSettings.setBool(BoolGlobalSetting.renderKatex, true);
+ check(globalSettings.getBool(BoolGlobalSetting.renderKatex)).isTrue();
+
+ const html = ''
+ ''
+ 'λ';
+ await checkFontSizeRatio(tester,
+ targetHtml: html,
+ targetFontSizeFinder: (rootSpan) {
+ late final double result;
+ rootSpan.visitChildren((span) {
+ if (span case WidgetSpan(child: KatexWidget() && var widget)) {
+ result = mergedStyleOf(tester,
+ findAncestor: find.byWidget(widget), r'λ')!.fontSize!;
+ return false;
+ }
+ return true;
+ });
+ return result;
+ });
+ });
+
+ testWidgets('maintains font-size ratio with surrounding text, when showing TeX source', (tester) async {
const html = ''
''
From 85f4ecfcd1e4c564aee3da7135adf751625a6d2f Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 29 May 2025 20:00:22 +0530
Subject: [PATCH 6/7] content: Handle 'strut' span in KaTeX content
In KaTeX HTML it is used to set the baseline of the content in a
span, so handle it separately here.
---
lib/model/content.dart | 18 ++++++++++
lib/model/katex.dart | 64 ++++++++++++++++++++++++++++++++--
lib/widgets/content.dart | 29 +++++++++++++++
test/model/content_test.dart | 36 +++++++------------
test/widgets/content_test.dart | 15 ++++----
5 files changed, 127 insertions(+), 35 deletions(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index e80247325f..78fc961c00 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -411,6 +411,24 @@ class KatexSpanNode extends KatexNode {
}
}
+class KatexStrutNode extends KatexNode {
+ const KatexStrutNode({
+ required this.heightEm,
+ required this.verticalAlignEm,
+ super.debugHtmlNode,
+ });
+
+ final double heightEm;
+ final double? verticalAlignEm;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DoubleProperty('heightEm', heightEm));
+ properties.add(DoubleProperty('verticalAlignEm', verticalAlignEm));
+ }
+}
+
class MathBlockNode extends MathNode implements BlockContentNode {
const MathBlockNode({
super.debugHtmlNode,
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 922546c676..f896bbdc86 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -187,7 +187,34 @@ class _KatexParser {
final debugHtmlNode = kDebugMode ? element : null;
+ if (element.className == 'strut') {
+ if (element.nodes.isNotEmpty) throw _KatexHtmlParseError();
+
+ final styles = _parseSpanInlineStyles(element);
+ if (styles == null) throw _KatexHtmlParseError();
+
+ final heightEm = styles.heightEm;
+ if (heightEm == null) throw _KatexHtmlParseError();
+ final verticalAlignEm = styles.verticalAlignEm;
+
+ // Ensure only `height` and `vertical-align` inline styles are present.
+ if (styles.filter(heightEm: false, verticalAlignEm: false)
+ != const KatexSpanStyles()) {
+ throw _KatexHtmlParseError();
+ }
+
+ return KatexStrutNode(
+ heightEm: heightEm,
+ verticalAlignEm: verticalAlignEm,
+ debugHtmlNode: debugHtmlNode);
+ }
+
final inlineStyles = _parseSpanInlineStyles(element);
+ if (inlineStyles != null) {
+ // We expect `vertical-align` inline style to be only present on a
+ // `strut` span, for which we emit `KatexStrutNode` separately.
+ if (inlineStyles.verticalAlignEm != null) throw _KatexHtmlParseError();
+ }
// Aggregate the CSS styles that apply, in the same order as the CSS
// classes specified for this span, mimicking the behaviour on web.
@@ -214,8 +241,9 @@ class _KatexParser {
case 'strut':
// .strut { ... }
- // Do nothing, it has properties that don't need special handling.
- break;
+ // We expect the 'strut' class to be the only class in a span,
+ // in which case we handle it separately and emit `KatexStrutNode`.
+ throw _KatexHtmlParseError();
case 'textbf':
// .textbf { font-weight: bold; }
@@ -463,6 +491,7 @@ class _KatexParser {
final stylesheet = css_parser.parse('*{$styleStr}');
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
double? heightEm;
+ double? verticalAlignEm;
for (final declaration in rule.declarationGroup.declarations) {
if (declaration case css_visitor.Declaration(
@@ -474,6 +503,10 @@ class _KatexParser {
case 'height':
heightEm = _getEm(expression);
if (heightEm != null) continue;
+
+ case 'vertical-align':
+ verticalAlignEm = _getEm(expression);
+ if (verticalAlignEm != null) continue;
}
// TODO handle more CSS properties
@@ -488,6 +521,7 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
+ verticalAlignEm: verticalAlignEm,
);
} else {
throw _KatexHtmlParseError();
@@ -524,6 +558,7 @@ enum KatexSpanTextAlign {
@immutable
class KatexSpanStyles {
final double? heightEm;
+ final double? verticalAlignEm;
final String? fontFamily;
final double? fontSizeEm;
@@ -533,6 +568,7 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
+ this.verticalAlignEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
@@ -544,6 +580,7 @@ class KatexSpanStyles {
int get hashCode => Object.hash(
'KatexSpanStyles',
heightEm,
+ verticalAlignEm,
fontFamily,
fontSizeEm,
fontWeight,
@@ -555,6 +592,7 @@ class KatexSpanStyles {
bool operator ==(Object other) {
return other is KatexSpanStyles &&
other.heightEm == heightEm &&
+ other.verticalAlignEm == verticalAlignEm &&
other.fontFamily == fontFamily &&
other.fontSizeEm == fontSizeEm &&
other.fontWeight == fontWeight &&
@@ -566,6 +604,7 @@ class KatexSpanStyles {
String toString() {
final args = [];
if (heightEm != null) args.add('heightEm: $heightEm');
+ if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
if (fontFamily != null) args.add('fontFamily: $fontFamily');
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -584,6 +623,7 @@ class KatexSpanStyles {
KatexSpanStyles merge(KatexSpanStyles other) {
return KatexSpanStyles(
heightEm: other.heightEm ?? heightEm,
+ verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
fontFamily: other.fontFamily ?? fontFamily,
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
fontStyle: other.fontStyle ?? fontStyle,
@@ -591,6 +631,26 @@ class KatexSpanStyles {
textAlign: other.textAlign ?? textAlign,
);
}
+
+ KatexSpanStyles filter({
+ bool heightEm = true,
+ bool verticalAlignEm = true,
+ bool fontFamily = true,
+ bool fontSizeEm = true,
+ bool fontWeight = true,
+ bool fontStyle = true,
+ bool textAlign = true,
+ }) {
+ return KatexSpanStyles(
+ heightEm: heightEm ? this.heightEm : null,
+ verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
+ fontFamily: fontFamily ? this.fontFamily : null,
+ fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
+ fontWeight: fontWeight ? this.fontWeight : null,
+ fontStyle: fontStyle ? this.fontStyle : null,
+ textAlign: textAlign ? this.textAlign : null,
+ );
+ }
}
class _KatexHtmlParseError extends Error {
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 8a06988a12..8cdb1e79ec 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -896,6 +896,7 @@ class _KatexNodeList extends StatelessWidget {
data: MediaQueryData(textScaler: TextScaler.noScaling),
child: switch (e) {
KatexSpanNode() => _KatexSpan(e),
+ KatexStrutNode() => _KatexStrut(e),
}));
}))));
}
@@ -918,6 +919,10 @@ class _KatexSpan extends StatelessWidget {
}
final styles = node.styles;
+ // We expect vertical-align to be only present with the
+ // `strut` span, for which parser explicitly emits `KatexStrutNode`.
+ // So, this should always be null for non `strut` spans.
+ assert(styles.verticalAlignEm == null);
final fontFamily = styles.fontFamily;
final fontSize = switch (styles.fontSizeEm) {
@@ -976,6 +981,30 @@ class _KatexSpan extends StatelessWidget {
}
}
+class _KatexStrut extends StatelessWidget {
+ const _KatexStrut(this.node);
+
+ final KatexStrutNode node;
+
+ @override
+ Widget build(BuildContext context) {
+ final em = DefaultTextStyle.of(context).style.fontSize!;
+
+ final verticalAlignEm = node.verticalAlignEm;
+ if (verticalAlignEm == null) {
+ return SizedBox(height: node.heightEm * em);
+ }
+
+ return SizedBox(
+ height: node.heightEm * em,
+ child: Baseline(
+ baseline: (verticalAlignEm + node.heightEm) * em,
+ baselineType: TextBaseline.alphabetic,
+ child: const Text('')),
+ );
+ }
+}
+
class WebsitePreview extends StatelessWidget {
const WebsitePreview({super.key, required this.node});
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index f9b461e17c..652a6f10b8 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -519,7 +519,7 @@ class ContentExample {
'λ
',
MathInlineNode(texSource: r'\lambda', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -539,7 +539,7 @@ class ContentExample {
'λ',
[MathBlockNode(texSource: r'\lambda', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -564,7 +564,7 @@ class ContentExample {
'b', [
MathBlockNode(texSource: 'a', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -575,7 +575,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'b', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -603,7 +603,7 @@ class ContentExample {
[QuotationNode([
MathBlockNode(texSource: r'\lambda', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -632,7 +632,7 @@ class ContentExample {
[QuotationNode([
MathBlockNode(texSource: 'a', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -643,7 +643,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'b', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -681,7 +681,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'a', nodes: [
KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306),text: null, nodes: []),
+ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -731,10 +731,7 @@ class ContentExample {
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexSpanNode(
- styles: KatexSpanStyles(heightEm: 1.6034),
- text: null,
- nodes: []),
+ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11
text: '1',
@@ -800,10 +797,7 @@ class ContentExample {
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexSpanNode(
- styles: KatexSpanStyles(heightEm: 1.6034),
- text: null,
- nodes: []),
+ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null),
KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1
text: null,
@@ -845,10 +839,7 @@ class ContentExample {
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexSpanNode(
- styles: KatexSpanStyles(heightEm: 3.0),
- text: null,
- nodes: []),
+ KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25),
KatexSpanNode(
styles: KatexSpanStyles(),
text: '⟨',
@@ -1981,10 +1972,7 @@ void main() async {
testParseExample(ContentExample.mathBlockBetweenImages);
testParseExample(ContentExample.mathBlockKatexSizing);
testParseExample(ContentExample.mathBlockKatexNestedSizing);
- // TODO: Re-enable this test after adding support for parsing
- // `vertical-align` in inline styles. Currently it fails
- // because `strut` span has `vertical-align`.
- testParseExample(ContentExample.mathBlockKatexDelimSizing, skip: true);
+ testParseExample(ContentExample.mathBlockKatexDelimSizing);
testParseExample(ContentExample.imageSingle);
testParseExample(ContentExample.imageSingleNoDimensions);
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index e0e4180c13..7657a80c3f 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -591,14 +591,11 @@ void main() {
('1', Offset(0.00, 40.24), Size(5.14, 12.00)),
('2', Offset(5.14, 2.80), Size(25.59, 61.00)),
]),
- // TODO: Re-enable this test after adding support for parsing
- // `vertical-align` in inline styles. Currently it fails
- // because `strut` span has `vertical-align`.
- (ContentExample.mathBlockKatexDelimSizing, skip: true, [
- ('(', Offset(8.00, 46.36), Size(9.42, 25.00)),
- ('[', Offset(17.42, 46.36), Size(9.71, 25.00)),
- ('⌈', Offset(27.12, 46.36), Size(11.99, 25.00)),
- ('⌊', Offset(39.11, 46.36), Size(13.14, 25.00)),
+ (ContentExample.mathBlockKatexDelimSizing, skip: false, [
+ ('(', Offset(8.00, 20.14), Size(9.42, 25.00)),
+ ('[', Offset(17.42, 20.14), Size(9.71, 25.00)),
+ ('⌈', Offset(27.12, 20.14), Size(11.99, 25.00)),
+ ('⌊', Offset(39.11, 20.14), Size(13.14, 25.00)),
]),
];
@@ -629,7 +626,7 @@ void main() {
check(size)
.within(distance: 0.05, from: expectedSize);
}
- }, skip: testCase.skip);
+ });
}
});
});
From 1fe817cfce109c8e74339a5ddae10ab552df9e59 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 19 May 2025 21:46:15 +0530
Subject: [PATCH 7/7] content: Handle positive margin-right and margin-left in
KaTeX spans
---
lib/model/katex.dart | 35 ++++++++++++++++++++
lib/widgets/content.dart | 25 ++++++++++++--
test/model/content_test.dart | 60 ++++++++++++++++++++++++++++++++++
test/widgets/content_test.dart | 5 +++
4 files changed, 122 insertions(+), 3 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index f896bbdc86..057f7076bc 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -492,6 +492,8 @@ class _KatexParser {
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
double? heightEm;
double? verticalAlignEm;
+ double? marginRightEm;
+ double? marginLeftEm;
for (final declaration in rule.declarationGroup.declarations) {
if (declaration case css_visitor.Declaration(
@@ -507,6 +509,20 @@ class _KatexParser {
case 'vertical-align':
verticalAlignEm = _getEm(expression);
if (verticalAlignEm != null) continue;
+
+ case 'margin-right':
+ marginRightEm = _getEm(expression);
+ if (marginRightEm != null) {
+ if (marginRightEm < 0) throw _KatexHtmlParseError();
+ continue;
+ }
+
+ case 'margin-left':
+ marginLeftEm = _getEm(expression);
+ if (marginLeftEm != null) {
+ if (marginLeftEm < 0) throw _KatexHtmlParseError();
+ continue;
+ }
}
// TODO handle more CSS properties
@@ -522,6 +538,8 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
verticalAlignEm: verticalAlignEm,
+ marginRightEm: marginRightEm,
+ marginLeftEm: marginLeftEm,
);
} else {
throw _KatexHtmlParseError();
@@ -560,6 +578,9 @@ class KatexSpanStyles {
final double? heightEm;
final double? verticalAlignEm;
+ final double? marginRightEm;
+ final double? marginLeftEm;
+
final String? fontFamily;
final double? fontSizeEm;
final KatexSpanFontWeight? fontWeight;
@@ -569,6 +590,8 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
this.verticalAlignEm,
+ this.marginRightEm,
+ this.marginLeftEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
@@ -581,6 +604,8 @@ class KatexSpanStyles {
'KatexSpanStyles',
heightEm,
verticalAlignEm,
+ marginRightEm,
+ marginLeftEm,
fontFamily,
fontSizeEm,
fontWeight,
@@ -593,6 +618,8 @@ class KatexSpanStyles {
return other is KatexSpanStyles &&
other.heightEm == heightEm &&
other.verticalAlignEm == verticalAlignEm &&
+ other.marginRightEm == marginRightEm &&
+ other.marginLeftEm == marginLeftEm &&
other.fontFamily == fontFamily &&
other.fontSizeEm == fontSizeEm &&
other.fontWeight == fontWeight &&
@@ -605,6 +632,8 @@ class KatexSpanStyles {
final args = [];
if (heightEm != null) args.add('heightEm: $heightEm');
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
+ if (marginRightEm != null) args.add('marginRightEm: $marginRightEm');
+ if (marginLeftEm != null) args.add('marginLeftEm: $marginLeftEm');
if (fontFamily != null) args.add('fontFamily: $fontFamily');
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -624,6 +653,8 @@ class KatexSpanStyles {
return KatexSpanStyles(
heightEm: other.heightEm ?? heightEm,
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
+ marginRightEm: other.marginRightEm ?? marginRightEm,
+ marginLeftEm: other.marginLeftEm ?? marginLeftEm,
fontFamily: other.fontFamily ?? fontFamily,
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
fontStyle: other.fontStyle ?? fontStyle,
@@ -635,6 +666,8 @@ class KatexSpanStyles {
KatexSpanStyles filter({
bool heightEm = true,
bool verticalAlignEm = true,
+ bool marginRightEm = true,
+ bool marginLeftEm = true,
bool fontFamily = true,
bool fontSizeEm = true,
bool fontWeight = true,
@@ -644,6 +677,8 @@ class KatexSpanStyles {
return KatexSpanStyles(
heightEm: heightEm ? this.heightEm : null,
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
+ marginRightEm: marginRightEm ? this.marginRightEm : null,
+ marginLeftEm: marginLeftEm ? this.marginLeftEm : null,
fontFamily: fontFamily ? this.fontFamily : null,
fontSizeEm: fontSizeEm ? this.fontSizeEm : null,
fontWeight: fontWeight ? this.fontWeight : null,
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 8cdb1e79ec..52ab7008b8 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -909,7 +909,7 @@ class _KatexSpan extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final em = DefaultTextStyle.of(context).style.fontSize!;
+ var em = DefaultTextStyle.of(context).style.fontSize!;
Widget widget = const SizedBox.shrink();
if (node.text != null) {
@@ -929,6 +929,8 @@ class _KatexSpan extends StatelessWidget {
double fontSizeEm => fontSizeEm * em,
null => null,
};
+ if (fontSize != null) em = fontSize;
+
final fontWeight = switch (styles.fontWeight) {
KatexSpanFontWeight.bold => FontWeight.bold,
null => null,
@@ -973,11 +975,28 @@ class _KatexSpan extends StatelessWidget {
child: widget);
}
- return SizedBox(
+ widget = SizedBox(
height: styles.heightEm != null
- ? styles.heightEm! * (fontSize ?? em)
+ ? styles.heightEm! * em
: null,
child: widget);
+
+ final margin = switch ((styles.marginLeftEm, styles.marginRightEm)) {
+ (null, null) => null,
+ (null, final marginRightEm?) =>
+ EdgeInsets.only(right: marginRightEm * em),
+ (final marginLeftEm?, null) =>
+ EdgeInsets.only(left: marginLeftEm * em),
+ (final marginLeftEm?, final marginRightEm?) =>
+ EdgeInsets.only(left: marginLeftEm * em, right: marginRightEm * em),
+ };
+
+ if (margin != null) {
+ assert(margin.isNonNegative);
+ widget = Padding(padding: margin, child: widget);
+ }
+
+ return widget;
}
}
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index 652a6f10b8..c19e1c02a5 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -884,6 +884,65 @@ class ContentExample {
]),
]);
+ static const mathBlockKatexSpace = ContentExample(
+ 'math block; KaTeX space',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2214883
+ '```math\n1:2\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ '1'
+ ''
+ ':'
+ ''
+ ''
+ ''
+ '2
', [
+ MathBlockNode(
+ texSource: '1:2',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(
+ heightEm: 0.6444,
+ verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: '1',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.2778),
+ text: null,
+ nodes: []),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: ':',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.2778),
+ text: null,
+ nodes: []),
+ ]),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(
+ heightEm: 0.6444,
+ verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: '2',
+ nodes: null),
+ ]),
+ ]),
+ ]);
+
static const imageSingle = ContentExample(
'single image',
// https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103
@@ -1973,6 +2032,7 @@ void main() async {
testParseExample(ContentExample.mathBlockKatexSizing);
testParseExample(ContentExample.mathBlockKatexNestedSizing);
testParseExample(ContentExample.mathBlockKatexDelimSizing);
+ testParseExample(ContentExample.mathBlockKatexSpace);
testParseExample(ContentExample.imageSingle);
testParseExample(ContentExample.imageSingleNoDimensions);
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index 7657a80c3f..f6366a9215 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -597,6 +597,11 @@ void main() {
('⌈', Offset(27.12, 20.14), Size(11.99, 25.00)),
('⌊', Offset(39.11, 20.14), Size(13.14, 25.00)),
]),
+ (ContentExample.mathBlockKatexSpace, skip: false, [
+ ('1', Offset(0.00, 2.24), Size(10.28, 25.00)),
+ (':', Offset(16.00, 2.24), Size(5.72, 25.00)),
+ ('2', Offset(27.43, 2.24), Size(10.28, 25.00)),
+ ]),
];
for (final testCase in testCases) {