From b42f784e9832e94cb8d48e61b4fa8812b1cff3df Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 24 Apr 2025 11:37:46 +0530
Subject: [PATCH 01/16] content test [nfc]: Use const for math block tests
---
test/model/content_test.dart | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index 5ab60c8e7e..6a0bc6ebe7 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -529,7 +529,7 @@ class ContentExample {
]),
]));
- static final mathBlock = ContentExample(
+ static const mathBlock = ContentExample(
'math block',
"```math\n\\lambda\n```",
expectedText: r'\lambda',
@@ -549,7 +549,7 @@ class ContentExample {
]),
])]);
- static final mathBlocksMultipleInParagraph = ContentExample(
+ static const mathBlocksMultipleInParagraph = ContentExample(
'math blocks, multiple in paragraph',
'```math\na\n\nb\n```',
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/.E2.9C.94.20Rajesh/near/2001490
@@ -586,7 +586,7 @@ class ContentExample {
]),
]);
- static final mathBlockInQuote = ContentExample(
+ static const mathBlockInQuote = ContentExample(
'math block in quote',
// There's sometimes a quirky extra `
\n` at the end of the `` that
// encloses the math block. In particular this happens when the math block
@@ -614,7 +614,7 @@ class ContentExample {
]),
])]);
- static final mathBlocksMultipleInQuote = ContentExample(
+ static const mathBlocksMultipleInQuote = ContentExample(
'math blocks, multiple in quote',
"````quote\n```math\na\n\nb\n```\n````",
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/.E2.9C.94.20Rajesh/near/2029236
@@ -654,7 +654,7 @@ class ContentExample {
]),
])]);
- static final mathBlockBetweenImages = ContentExample(
+ static const mathBlockBetweenImages = ContentExample(
'math block between images',
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Greg/near/2035891
'https://upload.wikimedia.org/wikipedia/commons/7/78/Verregende_bloem_van_een_Helenium_%27El_Dorado%27._22-07-2023._%28d.j.b%29.jpg\n```math\na\n```\nhttps://upload.wikimedia.org/wikipedia/commons/thumb/7/71/Zaadpluizen_van_een_Clematis_texensis_%27Princess_Diana%27._18-07-2023_%28actm.%29_02.jpg/1280px-Zaadpluizen_van_een_Clematis_texensis_%27Princess_Diana%27._18-07-2023_%28actm.%29_02.jpg',
@@ -702,7 +702,7 @@ class ContentExample {
// The font sizes can be compared using the katex.css generated
// from katex.scss :
// https://unpkg.com/katex@0.16.21/dist/katex.css
- static final mathBlockKatexSizing = ContentExample(
+ static const mathBlockKatexSizing = ContentExample(
'math block; KaTeX different sizing',
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2155476
'```math\n\\Huge 1\n\\huge 2\n\\LARGE 3\n\\Large 4\n\\large 5\n\\normalsize 6\n\\small 7\n\\footnotesize 8\n\\scriptsize 9\n\\tiny 0\n```',
@@ -779,7 +779,7 @@ class ContentExample {
]),
]);
- static final mathBlockKatexNestedSizing = ContentExample(
+ static const mathBlockKatexNestedSizing = ContentExample(
'math block; KaTeX nested sizing',
'```math\n\\tiny {1 \\Huge 2}\n```',
'
'
@@ -821,7 +821,7 @@ class ContentExample {
]),
]);
- static final mathBlockKatexDelimSizing = ContentExample(
+ static const mathBlockKatexDelimSizing = ContentExample(
'math block; KaTeX delimiter sizing',
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2147135
'```math\n⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊\n```',
From 423160883c1800f5913e6c512097f0dd408310be Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Thu, 29 May 2025 11:00:43 -0700
Subject: [PATCH 02/16] content test [nfc]: Enable skips in testParseExample
and testParse
---
test/model/content_test.dart | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index 6a0bc6ebe7..e8c22298ca 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -1642,15 +1642,18 @@ UnimplementedInlineContentNode inlineUnimplemented(String html) {
return UnimplementedInlineContentNode(htmlNode: fragment.nodes.single);
}
-void testParse(String name, String html, List nodes) {
+void testParse(String name, String html, List nodes, {
+ Object? skip,
+}) {
test(name, () {
check(parseContent(html))
.equalsNode(ZulipContent(nodes: nodes));
- });
+ }, skip: skip);
}
-void testParseExample(ContentExample example) {
- testParse('parse ${example.description}', example.html, example.expectedNodes);
+void testParseExample(ContentExample example, {Object? skip}) {
+ testParse('parse ${example.description}', example.html, example.expectedNodes,
+ skip: skip);
}
void main() async {
@@ -2034,7 +2037,7 @@ void main() async {
r'^\s*static\s+(?:const|final)\s+(\w+)\s*=\s*ContentExample\s*(?:\.\s*inline\s*)?\(',
).allMatches(source).map((m) => m.group(1));
final testedExamples = RegExp(multiLine: true,
- r'^\s*testParseExample\s*\(\s*ContentExample\s*\.\s*(\w+)\);',
+ r'^\s*testParseExample\s*\(\s*ContentExample\s*\.\s*(\w+)(?:,\s*skip:\s*true)?\s*\);',
).allMatches(source).map((m) => m.group(1));
check(testedExamples).unorderedEquals(declaredExamples);
}, skip: Platform.isWindows, // [intended] purely analyzes source, so
From 04a3d2580a79c90cdd5b34ec9cdc2027347f2fc6 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 8 May 2025 21:04:10 +0530
Subject: [PATCH 03/16] content [nfc]: Inline _logError in
_KatexParser._parseSpan
This will prevent string interpolation being evaluated during
release build. Especially useful in later commit where it becomes
more expensive.
---
lib/model/katex.dart | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 709f91b4b2..7c4b2deb1b 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -109,11 +109,6 @@ class _KatexParser {
bool get hasError => _hasError;
bool _hasError = false;
- void _logError(String message) {
- assert(debugLog(message));
- _hasError = true;
- }
-
List parseKatexHtml(dom.Element element) {
assert(element.localName == 'span');
assert(element.className == 'katex-html');
@@ -334,7 +329,8 @@ class _KatexParser {
break;
default:
- _logError('KaTeX: Unsupported CSS class: $spanClass');
+ assert(debugLog('KaTeX: Unsupported CSS class: $spanClass'));
+ _hasError = true;
}
}
final styles = KatexSpanStyles(
From e2022de7d8980d2739dd871db475272696ea0a03 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 8 May 2025 21:34:09 +0530
Subject: [PATCH 04/16] content [nfc]: Refactor _KatexParser._parseChildSpans
to take list of nodes
---
lib/model/katex.dart | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 7c4b2deb1b..f6375f907e 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -112,11 +112,11 @@ class _KatexParser {
List parseKatexHtml(dom.Element element) {
assert(element.localName == 'span');
assert(element.className == 'katex-html');
- return _parseChildSpans(element);
+ return _parseChildSpans(element.nodes);
}
- List _parseChildSpans(dom.Element element) {
- return List.unmodifiable(element.nodes.map((node) {
+ List _parseChildSpans(List nodes) {
+ return List.unmodifiable(nodes.map((node) {
if (node case dom.Element(localName: 'span')) {
return _parseSpan(node);
} else {
@@ -346,7 +346,7 @@ class _KatexParser {
if (element.nodes case [dom.Text(:final data)]) {
text = data;
} else {
- spans = _parseChildSpans(element);
+ spans = _parseChildSpans(element.nodes);
}
if (text == null && spans == null) throw KatexHtmlParseError();
From 82c6b014495564b525d86ab0c0fa801de485240b Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 27 May 2025 23:02:56 +0530
Subject: [PATCH 05/16] content: Populate `debugHtmlNode` for KatexNode
---
lib/model/katex.dart | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index f6375f907e..4c7ddf7b64 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -131,6 +131,8 @@ class _KatexParser {
KatexNode _parseSpan(dom.Element element) {
// TODO maybe check if the sequence of ancestors matter for spans.
+ final debugHtmlNode = kDebugMode ? element : null;
+
// Aggregate the CSS styles that apply, in the same order as the CSS
// classes specified for this span, mimicking the behaviour on web.
//
@@ -353,7 +355,8 @@ class _KatexParser {
return KatexNode(
styles: styles,
text: text,
- nodes: spans);
+ nodes: spans,
+ debugHtmlNode: debugHtmlNode);
}
}
From b6da144ae1173e696de835701cd38f1266165867 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 24 Apr 2025 13:21:18 +0530
Subject: [PATCH 06/16] content [nfc]: Reintroduce KatexNode as a base sealed
class
And rename previous type to KatexSpanNode, also while making it a
subtype of KatexNode.
---
lib/model/content.dart | 8 ++-
lib/model/katex.dart | 2 +-
lib/widgets/content.dart | 6 +-
test/model/content_test.dart | 104 ++++++++++++++++-----------------
test/widgets/content_test.dart | 12 ++--
5 files changed, 70 insertions(+), 62 deletions(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 59f7b41aad..768031ae9a 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -369,8 +369,12 @@ abstract class MathNode extends ContentNode {
}
}
-class KatexNode extends ContentNode {
- const KatexNode({
+sealed class KatexNode extends ContentNode {
+ const KatexNode({super.debugHtmlNode});
+}
+
+class KatexSpanNode extends KatexNode {
+ const KatexSpanNode({
required this.styles,
required this.text,
required this.nodes,
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 4c7ddf7b64..1fe747f210 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -352,7 +352,7 @@ class _KatexParser {
}
if (text == null && spans == null) throw KatexHtmlParseError();
- return KatexNode(
+ return KatexSpanNode(
styles: styles,
text: text,
nodes: spans,
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 44411434fd..dd4919edb7 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -870,7 +870,9 @@ class _KatexNodeList extends StatelessWidget {
return WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _KatexSpan(e));
+ child: switch (e) {
+ KatexSpanNode() => _KatexSpan(e),
+ });
}))));
}
}
@@ -878,7 +880,7 @@ class _KatexNodeList extends StatelessWidget {
class _KatexSpan extends StatelessWidget {
const _KatexSpan(this.node);
- final KatexNode node;
+ final KatexSpanNode node;
@override
Widget build(BuildContext context) {
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index e8c22298ca..baea1a8109 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -518,9 +518,9 @@ class ContentExample {
' \\lambda '
'λ
',
MathInlineNode(texSource: r'\lambda', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -538,9 +538,9 @@ class ContentExample {
'\\lambda'
'λ
',
[MathBlockNode(texSource: r'\lambda', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -563,9 +563,9 @@ class ContentExample {
'b'
'b', [
MathBlockNode(texSource: 'a', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -574,9 +574,9 @@ class ContentExample {
]),
]),
MathBlockNode(texSource: 'b', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -602,9 +602,9 @@ class ContentExample {
'
\n\n',
[QuotationNode([
MathBlockNode(texSource: r'\lambda', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -631,9 +631,9 @@ class ContentExample {
'
\n\n',
[QuotationNode([
MathBlockNode(texSource: 'a', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -642,9 +642,9 @@ class ContentExample {
]),
]),
MathBlockNode(texSource: 'b', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -680,9 +680,9 @@ class ContentExample {
originalHeight: null),
]),
MathBlockNode(texSource: 'a', nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(),text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(),text: null, nodes: []),
+ KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
fontStyle: KatexSpanFontStyle.italic),
@@ -727,51 +727,51 @@ class ContentExample {
MathBlockNode(
texSource: "\\Huge 1\n\\huge 2\n\\LARGE 3\n\\Large 4\n\\large 5\n\\normalsize 6\n\\small 7\n\\footnotesize 8\n\\scriptsize 9\n\\tiny 0",
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: []),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11
text: '1',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10
text: '2',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9
text: '3',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8
text: '4',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7
text: '5',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6
text: '6',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5
text: '7',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4
text: '8',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3
text: '9',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1
text: '0',
nodes: null),
@@ -796,23 +796,23 @@ class ContentExample {
MathBlockNode(
texSource: '\\tiny {1 \\Huge 2}',
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: []),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: '1',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11
text: '2',
nodes: null),
@@ -841,50 +841,50 @@ class ContentExample {
MathBlockNode(
texSource: '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊',
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: []),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: '⟨',
nodes: null),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'),
text: '(',
nodes: null),
]),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'),
text: '[',
nodes: null),
]),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'),
text: '⌈',
nodes: null),
]),
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'),
text: '⌊',
nodes: null),
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index b5150a54ee..7e1309478f 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -596,9 +596,10 @@ void main() {
await prepareContent(tester, plainContent(content.html));
final mathBlockNode = content.expectedNodes.single as MathBlockNode;
- final baseNode = mathBlockNode.nodes!.single;
+ final baseNode = mathBlockNode.nodes!.single as KatexSpanNode;
final nodes = baseNode.nodes!.skip(1); // Skip .strut node.
- for (final katexNode in nodes) {
+ for (var katexNode in nodes) {
+ katexNode = katexNode as KatexSpanNode;
final fontSize = katexNode.styles.fontSizeEm! * kBaseKatexTextStyle.fontSize!;
checkKatexText(tester, katexNode.text!,
fontFamily: 'KaTeX_Main',
@@ -639,12 +640,12 @@ void main() {
await prepareContent(tester, plainContent(content.html));
final mathBlockNode = content.expectedNodes.single as MathBlockNode;
- final baseNode = mathBlockNode.nodes!.single;
+ final baseNode = mathBlockNode.nodes!.single as KatexSpanNode;
var nodes = baseNode.nodes!.skip(1); // Skip .strut node.
final fontSize = kBaseKatexTextStyle.fontSize!;
- final firstNode = nodes.first;
+ final firstNode = nodes.first as KatexSpanNode;
checkKatexText(tester, firstNode.text!,
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
@@ -652,7 +653,8 @@ void main() {
nodes = nodes.skip(1);
for (var katexNode in nodes) {
- katexNode = katexNode.nodes!.single; // Skip empty .mord parent.
+ 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,
From c8f7d0bbcdc203890f76a792850a1d2bcd52c71a Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 19 May 2025 21:36:45 +0530
Subject: [PATCH 07/16] content: Ignore more KaTeX classes that don't have CSS
definition
---
lib/model/katex.dart | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 1fe747f210..dd69cc181a 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -326,6 +326,12 @@ class _KatexParser {
case 'mord':
case 'mopen':
+ case 'mtight':
+ case 'text':
+ case 'mrel':
+ case 'mop':
+ case 'mclose':
+ case 'minner':
// Ignore these classes because they don't have a CSS definition
// in katex.scss, but we encounter them in the generated HTML.
break;
From 51fb5f58427b4056fa06f10c03fa8a7ced04931e Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 19 May 2025 21:42:55 +0530
Subject: [PATCH 08/16] content: Handle 'mspace' and 'msupsub' KaTeX CSS
classes
---
lib/model/katex.dart | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index dd69cc181a..8d9a646f08 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -272,6 +272,21 @@ class _KatexParser {
fontStyle = KatexSpanFontStyle.normal;
// TODO handle skipped class declarations between .mainrm and
+ // .mspace .
+
+ case 'mspace':
+ // .mspace { ... }
+ // Do nothing, it has properties that don't need special handling.
+ break;
+
+ // TODO handle skipped class declarations between .mspace and
+ // .msupsub .
+
+ case 'msupsub':
+ // .msupsub { text-align: left; }
+ textAlign = KatexSpanTextAlign.left;
+
+ // TODO handle skipped class declarations between .msupsub and
// .sizing .
case 'sizing':
From db0c1d83e296b2cf9043d15a97b0ad3e63df19ed Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 19 May 2025 22:04:00 +0530
Subject: [PATCH 09/16] content [nfc]: Remove the `inline` property in _Katex
widget
And inline the behaviour for `inline: false` in MathBlock widget.
---
lib/widgets/content.dart | 17 ++++++-----------
1 file changed, 6 insertions(+), 11 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index dd4919edb7..ec8e940c1f 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -818,7 +818,11 @@ class MathBlock extends StatelessWidget {
children: [TextSpan(text: node.texSource)])));
}
- return _Katex(inline: false, nodes: nodes);
+ return Center(
+ child: SingleChildScrollViewWithScrollbar(
+ scrollDirection: Axis.horizontal,
+ child: _Katex(
+ nodes: nodes)));
}
}
@@ -831,24 +835,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(
@@ -1265,7 +1260,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 0bf40b8452b4b787e5914b1db5ab36ccc7a8fc13 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 1 Apr 2025 18:32:25 +0530
Subject: [PATCH 10/16] content: Support parsing and handling inline styles for
KaTeX content
---
lib/model/katex.dart | 74 +++++++++++++++++++++++++++++++++-
lib/widgets/content.dart | 8 +++-
test/model/content_test.dart | 27 +++++++------
test/widgets/content_test.dart | 4 +-
4 files changed, 98 insertions(+), 15 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 8d9a646f08..190cd9941f 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -1,3 +1,5 @@
+import 'package:csslib/parser.dart' as css_parser;
+import 'package:csslib/visitor.dart' as css_visitor;
import 'package:flutter/foundation.dart';
import 'package:html/dom.dart' as dom;
@@ -133,6 +135,8 @@ class _KatexParser {
final debugHtmlNode = kDebugMode ? element : null;
+ final inlineStyles = _parseSpanInlineStyles(element);
+
// Aggregate the CSS styles that apply, in the same order as the CSS
// classes specified for this span, mimicking the behaviour on web.
//
@@ -374,11 +378,62 @@ class _KatexParser {
if (text == null && spans == null) throw KatexHtmlParseError();
return KatexSpanNode(
- styles: styles,
+ styles: inlineStyles != null
+ ? styles.merge(inlineStyles)
+ : styles,
text: text,
nodes: spans,
debugHtmlNode: debugHtmlNode);
}
+
+ KatexSpanStyles? _parseSpanInlineStyles(dom.Element element) {
+ if (element.attributes case {'style': final styleStr}) {
+ // `package:csslib` doesn't seem to have a way to parse inline styles:
+ // https://github.com/dart-lang/tools/issues/1173
+ // So, workaround that by wrapping it in a universal declaration.
+ final stylesheet = css_parser.parse('*{$styleStr}');
+ if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
+ double? heightEm;
+
+ for (final declaration in rule.declarationGroup.declarations) {
+ if (declaration case css_visitor.Declaration(
+ :final property,
+ expression: css_visitor.Expressions(
+ expressions: [css_visitor.Expression() && final expression]),
+ )) {
+ switch (property) {
+ case 'height':
+ heightEm = _getEm(expression);
+ if (heightEm != null) continue;
+ }
+
+ // TODO handle more CSS properties
+ assert(debugLog('KaTeX: Unsupported CSS expression:'
+ ' ${expression.toDebugString()}'));
+ _hasError = true;
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+
+ return KatexSpanStyles(
+ heightEm: heightEm,
+ );
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+ return null;
+ }
+
+ /// Returns the CSS `em` unit value if the given [expression] is actually an
+ /// `em` unit expression, else returns null.
+ double? _getEm(css_visitor.Expression expression) {
+ if (expression is css_visitor.EmTerm && expression.value is num) {
+ return (expression.value as num).toDouble();
+ }
+ return null;
+ }
}
enum KatexSpanFontWeight {
@@ -398,6 +453,8 @@ enum KatexSpanTextAlign {
@immutable
class KatexSpanStyles {
+ final double? heightEm;
+
final String? fontFamily;
final double? fontSizeEm;
final KatexSpanFontWeight? fontWeight;
@@ -405,6 +462,7 @@ class KatexSpanStyles {
final KatexSpanTextAlign? textAlign;
const KatexSpanStyles({
+ this.heightEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
@@ -415,6 +473,7 @@ class KatexSpanStyles {
@override
int get hashCode => Object.hash(
'KatexSpanStyles',
+ heightEm,
fontFamily,
fontSizeEm,
fontWeight,
@@ -425,6 +484,7 @@ class KatexSpanStyles {
@override
bool operator ==(Object other) {
return other is KatexSpanStyles &&
+ other.heightEm == heightEm &&
other.fontFamily == fontFamily &&
other.fontSizeEm == fontSizeEm &&
other.fontWeight == fontWeight &&
@@ -435,6 +495,7 @@ class KatexSpanStyles {
@override
String toString() {
final args = [];
+ if (heightEm != null) args.add('heightEm: $heightEm');
if (fontFamily != null) args.add('fontFamily: $fontFamily');
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
if (fontWeight != null) args.add('fontWeight: $fontWeight');
@@ -442,6 +503,17 @@ class KatexSpanStyles {
if (textAlign != null) args.add('textAlign: $textAlign');
return '${objectRuntimeType(this, 'KatexSpanStyles')}(${args.join(', ')})';
}
+
+ KatexSpanStyles merge(KatexSpanStyles other) {
+ return KatexSpanStyles(
+ heightEm: other.heightEm ?? heightEm,
+ fontFamily: other.fontFamily ?? fontFamily,
+ fontSizeEm: other.fontSizeEm ?? fontSizeEm,
+ fontStyle: other.fontStyle ?? fontStyle,
+ fontWeight: other.fontWeight ?? fontWeight,
+ textAlign: other.textAlign ?? textAlign,
+ );
+ }
}
class KatexHtmlParseError extends Error {
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index ec8e940c1f..6bd604f7c9 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -938,7 +938,13 @@ class _KatexSpan extends StatelessWidget {
textAlign: textAlign,
child: widget);
}
- return widget;
+
+ return SizedBox(
+ height: styles.heightEm != null
+ ? styles.heightEm! * em
+ : null,
+ child: widget,
+ );
}
}
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index baea1a8109..c90ac54b33 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(), text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
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(), text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
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(), text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
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(), text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
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(), text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
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(), text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
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(), text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
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(),text: null, nodes: []),
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306),text: null, nodes: []),
KatexSpanNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -732,7 +732,7 @@ class ContentExample {
text: null,
nodes: [
KatexSpanNode(
- styles: KatexSpanStyles(),
+ styles: KatexSpanStyles(heightEm: 1.6034),
text: null,
nodes: []),
KatexSpanNode(
@@ -801,7 +801,7 @@ class ContentExample {
text: null,
nodes: [
KatexSpanNode(
- styles: KatexSpanStyles(),
+ styles: KatexSpanStyles(heightEm: 1.6034),
text: null,
nodes: []),
KatexSpanNode(
@@ -846,7 +846,7 @@ class ContentExample {
text: null,
nodes: [
KatexSpanNode(
- styles: KatexSpanStyles(),
+ styles: KatexSpanStyles(heightEm: 3.0),
text: null,
nodes: []),
KatexSpanNode(
@@ -1963,7 +1963,10 @@ void main() async {
testParseExample(ContentExample.mathBlockBetweenImages);
testParseExample(ContentExample.mathBlockKatexSizing);
testParseExample(ContentExample.mathBlockKatexNestedSizing);
- testParseExample(ContentExample.mathBlockKatexDelimSizing);
+ // 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.imageSingle);
testParseExample(ContentExample.imageSingleNoDimensions);
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index 7e1309478f..754410fddc 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -661,7 +661,9 @@ void main() {
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],
From 04348639a01e927d953ab2ac0f7373547c56efda Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 22 Apr 2025 18:01:46 +0530
Subject: [PATCH 11/16] 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 | 38 ++++++++++++++++++++++------------
test/widgets/content_test.dart | 30 ++++++++++++++++++---------
2 files changed, 45 insertions(+), 23 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 6bd604f7c9..308d307b8c 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -822,22 +822,33 @@ class MathBlock extends StatelessWidget {
child: SingleChildScrollViewWithScrollbar(
scrollDirection: Axis.horizontal,
child: _Katex(
+ 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);
+/// 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);
+}
class _Katex extends StatelessWidget {
const _Katex({
+ required this.textStyle,
required this.nodes,
});
+ final TextStyle textStyle;
final List nodes;
@override
@@ -846,9 +857,8 @@ class _Katex extends StatelessWidget {
return Directionality(
textDirection: TextDirection.ltr,
- child: DefaultTextStyle(
- style: kBaseKatexTextStyle.copyWith(
- color: ContentTheme.of(context).textStylePlainParagraph.color),
+ child: DefaultTextStyle.merge(
+ style: mkBaseKatexTextStyle(textStyle),
child: widget));
}
}
@@ -865,9 +875,11 @@ class _KatexNodeList extends StatelessWidget {
return WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: switch (e) {
- KatexSpanNode() => _KatexSpan(e),
- });
+ child: MediaQuery(
+ data: MediaQueryData(textScaler: TextScaler.noScaling),
+ child: switch (e) {
+ KatexSpanNode() => _KatexSpan(e),
+ }));
}))));
}
}
@@ -1266,7 +1278,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _Katex(nodes: nodes));
+ child: _Katex(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 754410fddc..2f64aad97a 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -595,16 +595,20 @@ void main() {
final content = ContentExample.mathBlockKatexSizing;
await prepareContent(tester, plainContent(content.html));
+ final context = tester.element(find.byType(MathBlock));
+ final baseTextStyle =
+ mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
+
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!;
+ final fontSize = katexNode.styles.fontSizeEm! * baseTextStyle.fontSize!;
checkKatexText(tester, katexNode.text!,
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontHeight: baseTextStyle.height!);
}
});
@@ -617,17 +621,21 @@ void main() {
final content = ContentExample.mathBlockKatexNestedSizing;
await prepareContent(tester, plainContent(content.html));
- var fontSize = 0.5 * kBaseKatexTextStyle.fontSize!;
+ final context = tester.element(find.byType(MathBlock));
+ final baseTextStyle =
+ mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
+
+ var fontSize = 0.5 * baseTextStyle.fontSize!;
checkKatexText(tester, '1',
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontHeight: baseTextStyle.height!);
fontSize = 4.976 * fontSize;
checkKatexText(tester, '2',
fontFamily: 'KaTeX_Main',
fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontHeight: baseTextStyle.height!);
});
testWidgets('displays KaTeX content with different delimiter sizing', (tester) async {
@@ -643,13 +651,15 @@ void main() {
final baseNode = mathBlockNode.nodes!.single as KatexSpanNode;
var nodes = baseNode.nodes!.skip(1); // Skip .strut node.
- final fontSize = kBaseKatexTextStyle.fontSize!;
+ final context = tester.element(find.byType(MathBlock));
+ final baseTextStyle =
+ mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
final firstNode = nodes.first as KatexSpanNode;
checkKatexText(tester, firstNode.text!,
fontFamily: 'KaTeX_Main',
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontSize: baseTextStyle.fontSize!,
+ fontHeight: baseTextStyle.height!);
nodes = nodes.skip(1);
for (var katexNode in nodes) {
@@ -658,8 +668,8 @@ void main() {
final fontFamily = katexNode.styles.fontFamily!;
checkKatexText(tester, katexNode.text!,
fontFamily: fontFamily,
- fontSize: fontSize,
- fontHeight: kBaseKatexTextStyle.height!);
+ fontSize: baseTextStyle.fontSize!,
+ fontHeight: baseTextStyle.height!);
}
}, skip: true); // TODO: Re-enable this test after adding support for parsing
// `vertical-align` in inline styles. Currently it fails
From d81008f1b2def30e25d8a2d688016ef4d5739c79 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 29 May 2025 20:00:22 +0530
Subject: [PATCH 12/16] 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 | 65 ++++++++++++++++++++++++++++++++--
lib/widgets/content.dart | 25 +++++++++++++
test/model/content_test.dart | 36 +++++++------------
test/widgets/content_test.dart | 4 +--
5 files changed, 119 insertions(+), 29 deletions(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 768031ae9a..84dfa70bea 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -406,6 +406,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 190cd9941f..3a9430718e 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -133,9 +133,37 @@ class _KatexParser {
KatexNode _parseSpan(dom.Element element) {
// TODO maybe check if the sequence of ancestors matter for spans.
+ if (element.className.startsWith('strut')) {
+ if (element.className == 'strut' && element.nodes.isEmpty) {
+ 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) !=
+ KatexSpanStyles()) {
+ throw KatexHtmlParseError();
+ }
+
+ return KatexStrutNode(
+ heightEm: heightEm,
+ verticalAlignEm: verticalAlignEm);
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+
final debugHtmlNode = kDebugMode ? element : null;
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.
@@ -162,8 +190,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; }
@@ -394,6 +423,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(
@@ -405,6 +435,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
@@ -418,6 +452,7 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
+ verticalAlignEm: verticalAlignEm,
);
} else {
throw KatexHtmlParseError();
@@ -454,6 +489,7 @@ enum KatexSpanTextAlign {
@immutable
class KatexSpanStyles {
final double? heightEm;
+ final double? verticalAlignEm;
final String? fontFamily;
final double? fontSizeEm;
@@ -463,6 +499,7 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
+ this.verticalAlignEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
@@ -474,6 +511,7 @@ class KatexSpanStyles {
int get hashCode => Object.hash(
'KatexSpanStyles',
heightEm,
+ verticalAlignEm,
fontFamily,
fontSizeEm,
fontWeight,
@@ -485,6 +523,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 &&
@@ -496,6 +535,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');
@@ -507,6 +547,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,
@@ -514,6 +555,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 308d307b8c..451f889338 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -879,6 +879,7 @@ class _KatexNodeList extends StatelessWidget {
data: MediaQueryData(textScaler: TextScaler.noScaling),
child: switch (e) {
KatexSpanNode() => _KatexSpan(e),
+ KatexStrutNode() => _KatexStrut(e),
}));
}))));
}
@@ -960,6 +961,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 c90ac54b33..d3820a91b3 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: '⟨',
@@ -1963,10 +1954,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 2f64aad97a..333cb175a0 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -671,9 +671,7 @@ void main() {
fontSize: baseTextStyle.fontSize!,
fontHeight: baseTextStyle.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],
From 532b3244f031d0624b6b1ab33209009d4d9779f8 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 19 May 2025 21:46:15 +0530
Subject: [PATCH 13/16] content: Handle positive margin-right and margin-left
in KaTeX spans
---
lib/model/katex.dart | 35 +++++++++++++++++++++++++++++++++++
lib/widgets/content.dart | 25 ++++++++++++++++++++++++-
2 files changed, 59 insertions(+), 1 deletion(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 3a9430718e..e9cee9b7ab 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -424,6 +424,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(
@@ -439,6 +441,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
@@ -453,6 +469,8 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
verticalAlignEm: verticalAlignEm,
+ marginRightEm: marginRightEm,
+ marginLeftEm: marginLeftEm,
);
} else {
throw KatexHtmlParseError();
@@ -491,6 +509,9 @@ class KatexSpanStyles {
final double? heightEm;
final double? verticalAlignEm;
+ final double? marginRightEm;
+ final double? marginLeftEm;
+
final String? fontFamily;
final double? fontSizeEm;
final KatexSpanFontWeight? fontWeight;
@@ -500,6 +521,8 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
this.verticalAlignEm,
+ this.marginRightEm,
+ this.marginLeftEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
@@ -512,6 +535,8 @@ class KatexSpanStyles {
'KatexSpanStyles',
heightEm,
verticalAlignEm,
+ marginRightEm,
+ marginLeftEm,
fontFamily,
fontSizeEm,
fontWeight,
@@ -524,6 +549,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 &&
@@ -536,6 +563,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');
@@ -548,6 +577,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,
@@ -559,6 +590,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,
@@ -568,6 +601,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 451f889338..cb4e349406 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -952,7 +952,30 @@ class _KatexSpan extends StatelessWidget {
child: widget);
}
- return SizedBox(
+ final marginRight = switch (styles.marginRightEm) {
+ double marginRightEm => marginRightEm * em,
+ null => null,
+ };
+ final marginLeft = switch (styles.marginLeftEm) {
+ double marginLeftEm => marginLeftEm * em,
+ null => null,
+ };
+
+ EdgeInsets? margin;
+ if (marginRight != null || marginLeft != null) {
+ margin = EdgeInsets.zero;
+ if (marginRight != null) {
+ assert(marginRight >= 0);
+ margin += EdgeInsets.only(right: marginRight);
+ }
+ if (marginLeft != null) {
+ assert(marginLeft >= 0);
+ margin += EdgeInsets.only(left: marginLeft);
+ }
+ }
+
+ return Container(
+ margin: margin,
height: styles.heightEm != null
? styles.heightEm! * em
: null,
From fef8a60edec183e7be3353c4e059be434da657a7 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Mon, 19 May 2025 21:54:24 +0530
Subject: [PATCH 14/16] content: Handle 'top' property in KaTeX span inline
style
---
lib/model/katex.dart | 15 +++++++++++++++
lib/widgets/content.dart | 3 +++
2 files changed, 18 insertions(+)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index e9cee9b7ab..df32949c3a 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -424,6 +424,7 @@ class _KatexParser {
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
double? heightEm;
double? verticalAlignEm;
+ double? topEm;
double? marginRightEm;
double? marginLeftEm;
@@ -442,6 +443,10 @@ class _KatexParser {
verticalAlignEm = _getEm(expression);
if (verticalAlignEm != null) continue;
+ case 'top':
+ topEm = _getEm(expression);
+ if (topEm != null) continue;
+
case 'margin-right':
marginRightEm = _getEm(expression);
if (marginRightEm != null) {
@@ -468,6 +473,7 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
+ topEm: topEm,
verticalAlignEm: verticalAlignEm,
marginRightEm: marginRightEm,
marginLeftEm: marginLeftEm,
@@ -509,6 +515,8 @@ class KatexSpanStyles {
final double? heightEm;
final double? verticalAlignEm;
+ final double? topEm;
+
final double? marginRightEm;
final double? marginLeftEm;
@@ -521,6 +529,7 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
this.verticalAlignEm,
+ this.topEm,
this.marginRightEm,
this.marginLeftEm,
this.fontFamily,
@@ -535,6 +544,7 @@ class KatexSpanStyles {
'KatexSpanStyles',
heightEm,
verticalAlignEm,
+ topEm,
marginRightEm,
marginLeftEm,
fontFamily,
@@ -549,6 +559,7 @@ class KatexSpanStyles {
return other is KatexSpanStyles &&
other.heightEm == heightEm &&
other.verticalAlignEm == verticalAlignEm &&
+ other.topEm == topEm &&
other.marginRightEm == marginRightEm &&
other.marginLeftEm == marginLeftEm &&
other.fontFamily == fontFamily &&
@@ -563,6 +574,7 @@ class KatexSpanStyles {
final args = [];
if (heightEm != null) args.add('heightEm: $heightEm');
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
+ if (topEm != null) args.add('topEm: $topEm');
if (marginRightEm != null) args.add('marginRightEm: $marginRightEm');
if (marginLeftEm != null) args.add('marginLeftEm: $marginLeftEm');
if (fontFamily != null) args.add('fontFamily: $fontFamily');
@@ -577,6 +589,7 @@ class KatexSpanStyles {
return KatexSpanStyles(
heightEm: other.heightEm ?? heightEm,
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
+ topEm: other.topEm ?? topEm,
marginRightEm: other.marginRightEm ?? marginRightEm,
marginLeftEm: other.marginLeftEm ?? marginLeftEm,
fontFamily: other.fontFamily ?? fontFamily,
@@ -590,6 +603,7 @@ class KatexSpanStyles {
KatexSpanStyles filter({
bool heightEm = true,
bool verticalAlignEm = true,
+ bool topEm = true,
bool marginRightEm = true,
bool marginLeftEm = true,
bool fontFamily = true,
@@ -601,6 +615,7 @@ class KatexSpanStyles {
return KatexSpanStyles(
heightEm: heightEm ? this.heightEm : null,
verticalAlignEm: verticalAlignEm ? this.verticalAlignEm : null,
+ topEm: topEm ? this.topEm : null,
marginRightEm: marginRightEm ? this.marginRightEm : null,
marginLeftEm: marginLeftEm ? this.marginLeftEm : null,
fontFamily: fontFamily ? this.fontFamily : null,
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index cb4e349406..df3ca510ab 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -979,6 +979,9 @@ class _KatexSpan extends StatelessWidget {
height: styles.heightEm != null
? styles.heightEm! * em
: null,
+ transform: styles.topEm != null
+ ? Matrix4.translationValues(0, styles.topEm! * em, 0)
+ : null,
child: widget,
);
}
From 2c5a28cd9b906315d7289975d03e8b4fb3d58126 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 1 Apr 2025 21:29:15 +0530
Subject: [PATCH 15/16] content: Handle vertical offset spans in KaTeX content
Implement handling most common types of `vlist` spans.
---
lib/model/content.dart | 31 +++
lib/model/katex.dart | 85 +++++++-
lib/widgets/content.dart | 27 ++-
test/model/content_test.dart | 386 +++++++++++++++++++++++++++++++++
test/widgets/content_test.dart | 99 +++++++++
5 files changed, 623 insertions(+), 5 deletions(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 84dfa70bea..2d256a80db 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -424,6 +424,37 @@ class KatexStrutNode extends KatexNode {
}
}
+class KatexVlistNode extends KatexNode {
+ const KatexVlistNode({
+ required this.rows,
+ super.debugHtmlNode,
+ });
+
+ final List rows;
+
+ @override
+ List debugDescribeChildren() {
+ return rows.map((row) => row.toDiagnosticsNode()).toList();
+ }
+}
+
+class KatexVlistRowNode extends ContentNode {
+ const KatexVlistRowNode({
+ required this.verticalOffsetEm,
+ required this.node,
+ super.debugHtmlNode,
+ });
+
+ final double verticalOffsetEm;
+ final KatexSpanNode node;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DoubleProperty('verticalOffsetEm', verticalOffsetEm));
+ }
+}
+
class MathBlockNode extends MathNode implements BlockContentNode {
const MathBlockNode({
super.debugHtmlNode,
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index df32949c3a..7022fb5a19 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -156,6 +156,87 @@ class _KatexParser {
}
}
+ if (element.className.startsWith('vlist')) {
+ if (element case dom.Element(
+ localName: 'span',
+ className: 'vlist-t' || 'vlist-t vlist-t2',
+ nodes: [...],
+ ) && final vlistT) {
+ if (vlistT.attributes.containsKey('style')) throw KatexHtmlParseError();
+
+ final hasTwoVlistR = vlistT.className == 'vlist-t vlist-t2';
+ if (!hasTwoVlistR && vlistT.nodes.length != 1) throw KatexHtmlParseError();
+
+ if (hasTwoVlistR) {
+ if (vlistT.nodes case [
+ _,
+ dom.Element(localName: 'span', className: 'vlist-r', nodes: [
+ dom.Element(localName: 'span', className: 'vlist', nodes: [
+ dom.Element(localName: 'span', className: '', nodes: []),
+ ]),
+ ]),
+ ]) {
+ // Do nothing.
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+
+ if (vlistT.nodes.first
+ case dom.Element(localName: 'span', className: 'vlist-r') &&
+ final vlistR) {
+ if (vlistR.attributes.containsKey('style')) throw KatexHtmlParseError();
+
+ if (vlistR.nodes.first
+ case dom.Element(localName: 'span', className: 'vlist') &&
+ final vlist) {
+ final rows = [];
+
+ for (final innerSpan in vlist.nodes) {
+ if (innerSpan case dom.Element(
+ localName: 'span',
+ className: '',
+ nodes: [
+ dom.Element(localName: 'span', className: 'pstrut') &&
+ final pstrutSpan,
+ ...final otherSpans,
+ ],
+ )) {
+ var styles = _parseSpanInlineStyles(innerSpan)!;
+ final topEm = styles.topEm ?? 0;
+
+ styles = styles.filter(topEm: false);
+
+ final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
+ final pstrutHeight = pstrutStyles.heightEm ?? 0;
+
+ rows.add(KatexVlistRowNode(
+ verticalOffsetEm: topEm + pstrutHeight,
+ debugHtmlNode: kDebugMode ? innerSpan : null,
+ node: KatexSpanNode(
+ styles: styles,
+ text: null,
+ nodes: _parseChildSpans(otherSpans))));
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+
+ return KatexVlistNode(
+ rows: rows,
+ debugHtmlNode: kDebugMode ? vlistT : null,
+ );
+ } else {
+ throw KatexHtmlParseError();
+ }
+ } else {
+ throw KatexHtmlParseError();
+ }
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+
final debugHtmlNode = kDebugMode ? element : null;
final inlineStyles = _parseSpanInlineStyles(element);
@@ -173,7 +254,9 @@ class _KatexParser {
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
// A copy of class definition (where possible) is accompanied in a comment
// with each case statement to keep track of updates.
- final spanClasses = List.unmodifiable(element.className.split(' '));
+ final spanClasses = element.className != ''
+ ? List.unmodifiable(element.className.split(' '))
+ : const [];
String? fontFamily;
double? fontSizeEm;
KatexSpanFontWeight? fontWeight;
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index df3ca510ab..0b7c01f1a7 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -821,7 +821,7 @@ class MathBlock extends StatelessWidget {
return Center(
child: SingleChildScrollViewWithScrollbar(
scrollDirection: Axis.horizontal,
- child: _Katex(
+ child: Katex(
textStyle: ContentTheme.of(context).textStylePlainParagraph,
nodes: nodes)));
}
@@ -842,8 +842,9 @@ TextStyle mkBaseKatexTextStyle(TextStyle style) {
fontStyle: FontStyle.normal);
}
-class _Katex extends StatelessWidget {
- const _Katex({
+class Katex extends StatelessWidget {
+ const Katex({
+ super.key,
required this.textStyle,
required this.nodes,
});
@@ -880,6 +881,7 @@ class _KatexNodeList extends StatelessWidget {
child: switch (e) {
KatexSpanNode() => _KatexSpan(e),
KatexStrutNode() => _KatexStrut(e),
+ KatexVlistNode() => _KatexVlist(e),
}));
}))));
}
@@ -1011,6 +1013,23 @@ class _KatexStrut extends StatelessWidget {
}
}
+class _KatexVlist extends StatelessWidget {
+ const _KatexVlist(this.node);
+
+ final KatexVlistNode node;
+
+ @override
+ Widget build(BuildContext context) {
+ final em = DefaultTextStyle.of(context).style.fontSize!;
+
+ return Stack(children: List.unmodifiable(node.rows.map((row) {
+ return Transform.translate(
+ offset: Offset(0, row.verticalOffsetEm * em),
+ child: _KatexSpan(row.node));
+ })));
+ }
+}
+
class WebsitePreview extends StatelessWidget {
const WebsitePreview({super.key, required this.node});
@@ -1329,7 +1348,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _Katex(textStyle: widget.style, nodes: nodes));
+ child: Katex(textStyle: widget.style, nodes: nodes));
case GlobalTimeNode():
return WidgetSpan(alignment: PlaceholderAlignment.middle,
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index d3820a91b3..3d497bfdef 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -884,6 +884,387 @@ class ContentExample {
]),
]);
+ static const mathBlockKatexVertical1 = ContentExample(
+ 'math block katex vertical 1',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176734
+ '```math\na\'\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'a'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ '′
',
+ [
+ MathBlockNode(
+ texSource: 'a\'',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'a',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.113 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: '′',
+ nodes: null),
+ ]),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical2 = ContentExample(
+ 'math block katex vertical 2',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176735
+ '```math\nx_n\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'x'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'n'
+ ''
+ ''
+ '
',
+ [
+ MathBlockNode(
+ texSource: 'x_n',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'x',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -2.55 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(
+ marginLeftEm: 0,
+ marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'n',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical3 = ContentExample(
+ 'math block katex vertical 3',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176737
+ '```math\ne^x\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'e'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'x
',
+ [
+ MathBlockNode(
+ texSource: 'e^x',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.7144, verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'e',
+ nodes: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.113 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'x',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical4 = ContentExample(
+ 'math block katex vertical 4',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176738
+ '```math\n_u^o\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'u'
+ ''
+ ''
+ ''
+ 'o'
+ ''
+ ''
+ '
',
+ [
+ MathBlockNode(
+ texSource: "_u^o",
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247),
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: []),
+ KatexSpanNode(
+ styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left),
+ text: null,
+ nodes: [
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -2.453 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'u',
+ nodes: null),
+ ]),
+ ])),
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.113 + 2.7,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(marginRightEm: 0.05),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(fontSizeEm: 0.7),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'o',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ ]);
+
+ static const mathBlockKatexVertical5 = ContentExample(
+ 'math block katex vertical 5',
+ // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176739
+ '```math\na\\raisebox{0.25em}{\$b\$}c\n```',
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'a'
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'b'
+ 'c
',
+ [
+ MathBlockNode(
+ texSource: 'a\\raisebox{0.25em}{\$b\$}c',
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null),
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'a',
+ nodes: null),
+ KatexVlistNode(
+ rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: -3.25 + 3,
+ node: KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(),
+ text: null,
+ nodes: [
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'b',
+ nodes: null),
+ ]),
+ ])),
+ ]),
+ KatexSpanNode(
+ styles: KatexSpanStyles(
+ fontFamily: 'KaTeX_Math',
+ fontStyle: KatexSpanFontStyle.italic),
+ text: 'c',
+ nodes: null),
+ ]),
+ ]),
+ ]);
+
static const imageSingle = ContentExample(
'single image',
// https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103
@@ -1955,6 +2336,11 @@ void main() async {
testParseExample(ContentExample.mathBlockKatexSizing);
testParseExample(ContentExample.mathBlockKatexNestedSizing);
testParseExample(ContentExample.mathBlockKatexDelimSizing);
+ testParseExample(ContentExample.mathBlockKatexVertical1);
+ testParseExample(ContentExample.mathBlockKatexVertical2);
+ testParseExample(ContentExample.mathBlockKatexVertical3);
+ testParseExample(ContentExample.mathBlockKatexVertical4);
+ testParseExample(ContentExample.mathBlockKatexVertical5);
testParseExample(ContentExample.imageSingle);
testParseExample(ContentExample.imageSingleNoDimensions);
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index 333cb175a0..a98fbdddaf 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';
@@ -672,6 +673,59 @@ void main() {
fontHeight: baseTextStyle.height!);
}
});
+
+ group('characters render at specific offsets with specific size: ', () {
+ final testCases = [
+ (ContentExample.mathBlockKatexVertical1, [
+ ('a', Offset(0.0, 5.28), Size(10.88, 25.0)),
+ ('′', Offset(10.88, 1.13), Size(3.96, 17.0)),
+ ]),
+ (ContentExample.mathBlockKatexVertical2, [
+ ('x', Offset(0.0, 5.28), Size(11.76, 25.0)),
+ ('n', Offset(11.76, 13.65), Size(8.63, 17.0)),
+ ]),
+ (ContentExample.mathBlockKatexVertical3, [
+ ('e', Offset(0.0, 5.28), Size(9.58, 25.0)),
+ ('x', Offset(9.58, 2.07), Size(8.23, 17.0)),
+ ]),
+ (ContentExample.mathBlockKatexVertical4, [
+ ('u', Offset(0.0, 15.65), Size(8.23, 17.0)),
+ ('o', Offset(0.0, 2.07), Size(6.98, 17.0)),
+ ]),
+ (ContentExample.mathBlockKatexVertical5, [
+ ('a', Offset(0.0, 4.16), Size(10.88, 25.0)),
+ ('b', Offset(10.88, -0.66), Size(8.82, 25.0)),
+ ('c', Offset(19.70, 4.16), Size(8.90, 25.0)),
+ ]),
+ ];
+
+ 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(Katex));
+
+ for (final characterData in testCase.$2) {
+ final character = characterData.$1;
+ final topLeftOffset = characterData.$2;
+ final size = characterData.$3;
+
+ final rect = tester.getRect(find.text(character));
+ check(rect.topLeft - baseRect.topLeft)
+ .within(distance: 0.02, from: topLeftOffset);
+ check(rect.size)
+ .within(distance: 0.02, from: size);
+ }
+ });
+ }
+ });
});
/// Make a [TargetFontSizeFinder] to pass to [checkFontSizeRatio],
@@ -1420,3 +1474,48 @@ 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 entry in fonts.entries) {
+ final fontFamily = entry.key;
+ final fontFiles = entry.value;
+
+ final fontLoader = FontLoader(fontFamily);
+ for (final fontFile in fontFiles) {
+ fontLoader.addFont(rootBundle.load('assets/KaTeX/$fontFile'));
+ }
+ await fontLoader.load();
+ }
+}
From 7d76da76b4b21e6e6c2425126a40fa7fcd533764 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Fri, 20 Jun 2025 21:04:39 +0530
Subject: [PATCH 16/16] content: Error message for unexpected CSS class in
vlist inner span
---
lib/model/katex.dart | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 7022fb5a19..e74a13ce49 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -195,13 +195,17 @@ class _KatexParser {
for (final innerSpan in vlist.nodes) {
if (innerSpan case dom.Element(
localName: 'span',
- className: '',
nodes: [
dom.Element(localName: 'span', className: 'pstrut') &&
final pstrutSpan,
...final otherSpans,
],
)) {
+ if (innerSpan.className != '') {
+ throw KatexHtmlParseError('unexpected CSS class for '
+ 'vlist inner span: ${innerSpan.className}');
+ }
+
var styles = _parseSpanInlineStyles(innerSpan)!;
final topEm = styles.topEm ?? 0;