From 16ce3e18af73b61306f3a1718ff980bd782d20c8 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 16:17:07 -0700
Subject: [PATCH 01/18] katex [nfc]: Factor out MathNode from its
block-vs-inline variants
---
lib/model/content.dart | 32 +++++++++++++-------------------
1 file changed, 13 insertions(+), 19 deletions(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 72c8240133..59f7b41aad 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -341,8 +341,8 @@ class CodeBlockSpanNode extends ContentNode {
}
}
-class MathBlockNode extends BlockContentNode {
- const MathBlockNode({
+abstract class MathNode extends ContentNode {
+ const MathNode({
super.debugHtmlNode,
required this.texSource,
required this.nodes,
@@ -402,6 +402,14 @@ class KatexNode extends ContentNode {
}
}
+class MathBlockNode extends MathNode implements BlockContentNode {
+ const MathBlockNode({
+ super.debugHtmlNode,
+ required super.texSource,
+ required super.nodes,
+ });
+}
+
class ImageNodeList extends BlockContentNode {
const ImageNodeList(this.images, {super.debugHtmlNode});
@@ -863,26 +871,12 @@ class ImageEmojiNode extends EmojiNode {
}
}
-class MathInlineNode extends InlineContentNode {
+class MathInlineNode extends MathNode implements InlineContentNode {
const MathInlineNode({
super.debugHtmlNode,
- required this.texSource,
- required this.nodes,
+ required super.texSource,
+ required super.nodes,
});
-
- final String texSource;
- final List? nodes;
-
- @override
- void debugFillProperties(DiagnosticPropertiesBuilder properties) {
- super.debugFillProperties(properties);
- properties.add(StringProperty('texSource', texSource));
- }
-
- @override
- List debugDescribeChildren() {
- return nodes?.map((node) => node.toDiagnosticsNode()).toList() ?? const [];
- }
}
class GlobalTimeNode extends InlineContentNode {
From 3ac00373bef886f17ab71a69b9b8b7928d33beb9 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 16:38:22 -0700
Subject: [PATCH 02/18] katex [nfc]: Increment class index at top of loop
This way there's nothing that needs to happen at the bottom of the
loop, if any of the cases matched.
---
lib/model/katex.dart | 24 +++++++++++-------------
1 file changed, 11 insertions(+), 13 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index fc71025b59..21a8963931 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -149,9 +149,9 @@ class _KatexParser {
var styles = KatexSpanStyles();
var index = 0;
while (index < spanClasses.length) {
+ final spanClass = spanClasses[index++];
var classFound = false;
- final spanClass = spanClasses[index];
switch (spanClass) {
case 'base':
// .base { ... }
@@ -306,9 +306,9 @@ class _KatexParser {
case 'fontsize-ensurer':
// .sizing,
// .fontsize-ensurer { ... }
- if (index + 2 < spanClasses.length) {
- final resetSizeClass = spanClasses[index + 1];
- final sizeClass = spanClasses[index + 2];
+ if (index + 1 < spanClasses.length) {
+ final resetSizeClass = spanClasses[index];
+ final sizeClass = spanClasses[index + 1];
final resetSizeClassSuffix = _resetSizeClassRegExp.firstMatch(resetSizeClass)?.group(1);
final sizeClassSuffix = _sizeClassRegExp.firstMatch(sizeClass)?.group(1);
@@ -322,7 +322,7 @@ class _KatexParser {
// These indexes start at 1.
if (resetSizeIdx <= sizes.length && sizeIdx <= sizes.length) {
styles.fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
- index += 3;
+ index += 2;
continue;
}
}
@@ -332,8 +332,8 @@ class _KatexParser {
case 'delimsizing':
// .delimsizing { ... }
- if (index + 1 < spanClasses.length) {
- final nextClass = spanClasses[index + 1];
+ if (index < spanClasses.length) {
+ final nextClass = spanClasses[index];
switch (nextClass) {
case 'size1':
styles.fontFamily = 'KaTeX_Size1';
@@ -351,7 +351,7 @@ class _KatexParser {
if (styles.fontFamily == null) throw KatexHtmlParseError();
- index += 2;
+ index += 1;
continue;
}
@@ -361,8 +361,8 @@ class _KatexParser {
case 'op-symbol':
// .op-symbol { ... }
- if (index + 1 < spanClasses.length) {
- final nextClass = spanClasses[index + 1];
+ if (index < spanClasses.length) {
+ final nextClass = spanClasses[index];
switch (nextClass) {
case 'small-op':
styles.fontFamily = 'KaTeX_Size1';
@@ -371,7 +371,7 @@ class _KatexParser {
}
if (styles.fontFamily == null) throw KatexHtmlParseError();
- index += 2;
+ index += 1;
continue;
}
@@ -389,8 +389,6 @@ class _KatexParser {
}
if (!classFound) _logError('KaTeX: Unsupported CSS class: $spanClass');
-
- index++;
}
String? text;
From b003242d7b6ee1d00c6d5d8ae4b790d709cea2fc Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 16:50:42 -0700
Subject: [PATCH 03/18] katex [nfc]: Join the CSS-class switch statements into
one
Conveniently, the two redundant rules say the exact same thing
when they apply. So the first one has no effect, and we can
ignore it.
---
lib/model/katex.dart | 22 +++++++---------------
1 file changed, 7 insertions(+), 15 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 21a8963931..716b6d5274 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -141,7 +141,7 @@ class _KatexParser {
// Aggregate the CSS styles that apply, in the same order as the CSS
// classes specified for this span, mimicking the behaviour on web.
//
- // Each case in the switch blocks below is a separate CSS class definition
+ // Each case in the switch block below is a separate CSS class definition
// in the same order as in katex.scss :
// https://github.com/KaTeX/KaTeX/blob/2fe1941b/src/styles/katex.scss
// A copy of class definition (where possible) is accompanied in a comment
@@ -178,10 +178,10 @@ class _KatexParser {
styles.fontFamily = 'KaTeX_Main';
classFound = true;
- case 'textsf':
- // .textsf { font-family: KaTeX_SansSerif; }
- styles.fontFamily = 'KaTeX_SansSerif';
- classFound = true;
+ // case 'textsf':
+ // // .textsf { font-family: KaTeX_SansSerif; }
+ // This CSS rule has no effect, because the other `.textsf` rule below
+ // has the exact same list of declarations. Handle it there instead.
case 'texttt':
// .texttt { font-family: KaTeX_Typewriter; }
@@ -261,13 +261,7 @@ class _KatexParser {
// .textscr { font-family: KaTeX_Script; }
styles.fontFamily = 'KaTeX_Script';
classFound = true;
- }
- // We can't add the case for the next class (.mathsf, .textsf) in the
- // above switch block, because there is already a case for .textsf above.
- // So start a new block, to keep the order of the cases here same as the
- // CSS class definitions in katex.scss .
- switch (spanClass) {
case 'mathsf':
case 'textsf':
// .mathsf,
@@ -378,13 +372,11 @@ class _KatexParser {
throw KatexHtmlParseError();
// TODO handle more classes from katex.scss
- }
- // Ignore these classes because they don't have a CSS definition
- // in katex.scss, but we encounter them in the generated HTML.
- switch (spanClass) {
case 'mord':
case 'mopen':
+ // Ignore these classes because they don't have a CSS definition
+ // in katex.scss, but we encounter them in the generated HTML.
classFound = true;
}
From b8186a79ceb3367a626fb0bc2f8cc4b2b519ee4a Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:04:25 -0700
Subject: [PATCH 04/18] katex [nfc]: Replace classFound local with a default
case
Before this change, all cases of this switch statement either
continue the loop, or throw, or set classFound to true.
The error therefore gets logged just if none of the cases matched.
So we can express the same behavior with a default case.
---
lib/model/katex.dart | 33 ++++++---------------------------
1 file changed, 6 insertions(+), 27 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 716b6d5274..993739ad47 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -150,33 +150,28 @@ class _KatexParser {
var index = 0;
while (index < spanClasses.length) {
final spanClass = spanClasses[index++];
- var classFound = false;
-
switch (spanClass) {
case 'base':
// .base { ... }
// Do nothing, it has properties that don't need special handling.
- classFound = true;
+ break;
case 'strut':
// .strut { ... }
// Do nothing, it has properties that don't need special handling.
- classFound = true;
+ break;
case 'textbf':
// .textbf { font-weight: bold; }
styles.fontWeight = KatexSpanFontWeight.bold;
- classFound = true;
case 'textit':
// .textit { font-style: italic; }
styles.fontStyle = KatexSpanFontStyle.italic;
- classFound = true;
case 'textrm':
// .textrm { font-family: KaTeX_Main; }
styles.fontFamily = 'KaTeX_Main';
- classFound = true;
// case 'textsf':
// // .textsf { font-family: KaTeX_SansSerif; }
@@ -186,61 +181,51 @@ class _KatexParser {
case 'texttt':
// .texttt { font-family: KaTeX_Typewriter; }
styles.fontFamily = 'KaTeX_Typewriter';
- classFound = true;
case 'mathnormal':
// .mathnormal { font-family: KaTeX_Math; font-style: italic; }
styles.fontFamily = 'KaTeX_Math';
styles.fontStyle = KatexSpanFontStyle.italic;
- classFound = true;
case 'mathit':
// .mathit { font-family: KaTeX_Main; font-style: italic; }
styles.fontFamily = 'KaTeX_Main';
styles.fontStyle = KatexSpanFontStyle.italic;
- classFound = true;
case 'mathrm':
// .mathrm { font-style: normal; }
styles.fontStyle = KatexSpanFontStyle.normal;
- classFound = true;
case 'mathbf':
// .mathbf { font-family: KaTeX_Main; font-weight: bold; }
styles.fontFamily = 'KaTeX_Main';
styles.fontWeight = KatexSpanFontWeight.bold;
- classFound = true;
case 'boldsymbol':
// .boldsymbol { font-family: KaTeX_Math; font-weight: bold; font-style: italic; }
styles.fontFamily = 'KaTeX_Math';
styles.fontWeight = KatexSpanFontWeight.bold;
styles.fontStyle = KatexSpanFontStyle.italic;
- classFound = true;
case 'amsrm':
// .amsrm { font-family: KaTeX_AMS; }
styles.fontFamily = 'KaTeX_AMS';
- classFound = true;
case 'mathbb':
case 'textbb':
// .mathbb,
// .textbb { font-family: KaTeX_AMS; }
styles.fontFamily = 'KaTeX_AMS';
- classFound = true;
case 'mathcal':
// .mathcal { font-family: KaTeX_Caligraphic; }
styles.fontFamily = 'KaTeX_Caligraphic';
- classFound = true;
case 'mathfrak':
case 'textfrak':
// .mathfrak,
// .textfrak { font-family: KaTeX_Fraktur; }
styles.fontFamily = 'KaTeX_Fraktur';
- classFound = true;
case 'mathboldfrak':
case 'textboldfrak':
@@ -248,26 +233,22 @@ class _KatexParser {
// .textboldfrak { font-family: KaTeX_Fraktur; font-weight: bold; }
styles.fontFamily = 'KaTeX_Fraktur';
styles.fontWeight = KatexSpanFontWeight.bold;
- classFound = true;
case 'mathtt':
// .mathtt { font-family: KaTeX_Typewriter; }
styles.fontFamily = 'KaTeX_Typewriter';
- classFound = true;
case 'mathscr':
case 'textscr':
// .mathscr,
// .textscr { font-family: KaTeX_Script; }
styles.fontFamily = 'KaTeX_Script';
- classFound = true;
case 'mathsf':
case 'textsf':
// .mathsf,
// .textsf { font-family: KaTeX_SansSerif; }
styles.fontFamily = 'KaTeX_SansSerif';
- classFound = true;
case 'mathboldsf':
case 'textboldsf':
@@ -275,7 +256,6 @@ class _KatexParser {
// .textboldsf { font-family: KaTeX_SansSerif; font-weight: bold; }
styles.fontFamily = 'KaTeX_SansSerif';
styles.fontWeight = KatexSpanFontWeight.bold;
- classFound = true;
case 'mathsfit':
case 'mathitsf':
@@ -285,13 +265,11 @@ class _KatexParser {
// .textitsf { font-family: KaTeX_SansSerif; font-style: italic; }
styles.fontFamily = 'KaTeX_SansSerif';
styles.fontStyle = KatexSpanFontStyle.italic;
- classFound = true;
case 'mainrm':
// .mainrm { font-family: KaTeX_Main; font-style: normal; }
styles.fontFamily = 'KaTeX_Main';
styles.fontStyle = KatexSpanFontStyle.normal;
- classFound = true;
// TODO handle skipped class declarations between .mainrm and
// .sizing .
@@ -377,10 +355,11 @@ class _KatexParser {
case 'mopen':
// Ignore these classes because they don't have a CSS definition
// in katex.scss, but we encounter them in the generated HTML.
- classFound = true;
- }
+ break;
- if (!classFound) _logError('KaTeX: Unsupported CSS class: $spanClass');
+ default:
+ _logError('KaTeX: Unsupported CSS class: $spanClass');
+ }
}
String? text;
From 9854bb8b26d8fa5d706e7cc30407c8cff8327f07 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:07:07 -0700
Subject: [PATCH 05/18] katex [nfc]: Increment class index immediately on
dereference
This makes the reasoning about these index values more local.
---
lib/model/katex.dart | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 993739ad47..3db298b27a 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -279,8 +279,8 @@ class _KatexParser {
// .sizing,
// .fontsize-ensurer { ... }
if (index + 1 < spanClasses.length) {
- final resetSizeClass = spanClasses[index];
- final sizeClass = spanClasses[index + 1];
+ final resetSizeClass = spanClasses[index++];
+ final sizeClass = spanClasses[index++];
final resetSizeClassSuffix = _resetSizeClassRegExp.firstMatch(resetSizeClass)?.group(1);
final sizeClassSuffix = _sizeClassRegExp.firstMatch(sizeClass)?.group(1);
@@ -294,7 +294,6 @@ class _KatexParser {
// These indexes start at 1.
if (resetSizeIdx <= sizes.length && sizeIdx <= sizes.length) {
styles.fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
- index += 2;
continue;
}
}
@@ -305,7 +304,7 @@ class _KatexParser {
case 'delimsizing':
// .delimsizing { ... }
if (index < spanClasses.length) {
- final nextClass = spanClasses[index];
+ final nextClass = spanClasses[index++];
switch (nextClass) {
case 'size1':
styles.fontFamily = 'KaTeX_Size1';
@@ -323,7 +322,6 @@ class _KatexParser {
if (styles.fontFamily == null) throw KatexHtmlParseError();
- index += 1;
continue;
}
@@ -334,7 +332,7 @@ class _KatexParser {
case 'op-symbol':
// .op-symbol { ... }
if (index < spanClasses.length) {
- final nextClass = spanClasses[index];
+ final nextClass = spanClasses[index++];
switch (nextClass) {
case 'small-op':
styles.fontFamily = 'KaTeX_Size1';
@@ -343,7 +341,6 @@ class _KatexParser {
}
if (styles.fontFamily == null) throw KatexHtmlParseError();
- index += 1;
continue;
}
From 31e0bf06e68c9ec3ddf810dc5635d8b2b3dadd36 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:12:20 -0700
Subject: [PATCH 06/18] katex [nfc]: Handle error immediately on spanClasses
overrun
Like an early return, this (a) brings the consequence of the error
immediately next to the condition defining it, and (b) lets the normal
happy case continue vertically down without adding indentation.
---
lib/model/katex.dart | 85 +++++++++++++++++++-------------------------
1 file changed, 37 insertions(+), 48 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 3db298b27a..dcf358bfa0 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -278,24 +278,23 @@ class _KatexParser {
case 'fontsize-ensurer':
// .sizing,
// .fontsize-ensurer { ... }
- if (index + 1 < spanClasses.length) {
- final resetSizeClass = spanClasses[index++];
- final sizeClass = spanClasses[index++];
+ if (index + 2 > spanClasses.length) throw KatexHtmlParseError();
+ final resetSizeClass = spanClasses[index++];
+ final sizeClass = spanClasses[index++];
- final resetSizeClassSuffix = _resetSizeClassRegExp.firstMatch(resetSizeClass)?.group(1);
- final sizeClassSuffix = _sizeClassRegExp.firstMatch(sizeClass)?.group(1);
+ final resetSizeClassSuffix = _resetSizeClassRegExp.firstMatch(resetSizeClass)?.group(1);
+ final sizeClassSuffix = _sizeClassRegExp.firstMatch(sizeClass)?.group(1);
- if (resetSizeClassSuffix != null && sizeClassSuffix != null) {
- const sizes = [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2, 1.44, 1.728, 2.074, 2.488];
+ if (resetSizeClassSuffix != null && sizeClassSuffix != null) {
+ const sizes = [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2, 1.44, 1.728, 2.074, 2.488];
- final resetSizeIdx = int.parse(resetSizeClassSuffix, radix: 10);
- final sizeIdx = int.parse(sizeClassSuffix, radix: 10);
+ final resetSizeIdx = int.parse(resetSizeClassSuffix, radix: 10);
+ final sizeIdx = int.parse(sizeClassSuffix, radix: 10);
- // These indexes start at 1.
- if (resetSizeIdx <= sizes.length && sizeIdx <= sizes.length) {
- styles.fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
- continue;
- }
+ // These indexes start at 1.
+ if (resetSizeIdx <= sizes.length && sizeIdx <= sizes.length) {
+ styles.fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
+ continue;
}
}
@@ -303,48 +302,38 @@ class _KatexParser {
case 'delimsizing':
// .delimsizing { ... }
- if (index < spanClasses.length) {
- final nextClass = spanClasses[index++];
- switch (nextClass) {
- case 'size1':
- styles.fontFamily = 'KaTeX_Size1';
- case 'size2':
- styles.fontFamily = 'KaTeX_Size2';
- case 'size3':
- styles.fontFamily = 'KaTeX_Size3';
- case 'size4':
- styles.fontFamily = 'KaTeX_Size4';
-
- case 'mult':
- // TODO handle nested spans with `.delim-size{1,4}` class.
- break;
- }
-
- if (styles.fontFamily == null) throw KatexHtmlParseError();
-
- continue;
+ if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
+ final nextClass = spanClasses[index++];
+ switch (nextClass) {
+ case 'size1':
+ styles.fontFamily = 'KaTeX_Size1';
+ case 'size2':
+ styles.fontFamily = 'KaTeX_Size2';
+ case 'size3':
+ styles.fontFamily = 'KaTeX_Size3';
+ case 'size4':
+ styles.fontFamily = 'KaTeX_Size4';
+
+ case 'mult':
+ // TODO handle nested spans with `.delim-size{1,4}` class.
+ break;
}
- throw KatexHtmlParseError();
+ if (styles.fontFamily == null) throw KatexHtmlParseError();
// TODO handle .nulldelimiter and .delimcenter .
case 'op-symbol':
// .op-symbol { ... }
- if (index < spanClasses.length) {
- final nextClass = spanClasses[index++];
- switch (nextClass) {
- case 'small-op':
- styles.fontFamily = 'KaTeX_Size1';
- case 'large-op':
- styles.fontFamily = 'KaTeX_Size2';
- }
- if (styles.fontFamily == null) throw KatexHtmlParseError();
-
- continue;
+ if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
+ final nextClass = spanClasses[index++];
+ switch (nextClass) {
+ case 'small-op':
+ styles.fontFamily = 'KaTeX_Size1';
+ case 'large-op':
+ styles.fontFamily = 'KaTeX_Size2';
}
-
- throw KatexHtmlParseError();
+ if (styles.fontFamily == null) throw KatexHtmlParseError();
// TODO handle more classes from katex.scss
From 7751c0b3c3bf11a8d99b0b8d35afb822941cfc27 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:16:53 -0700
Subject: [PATCH 07/18] katex [nfc]: More early throws
Same motivation as in the parent commit.
---
lib/model/katex.dart | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index dcf358bfa0..1a01f29f9e 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -283,22 +283,19 @@ class _KatexParser {
final sizeClass = spanClasses[index++];
final resetSizeClassSuffix = _resetSizeClassRegExp.firstMatch(resetSizeClass)?.group(1);
+ if (resetSizeClassSuffix == null) throw KatexHtmlParseError();
final sizeClassSuffix = _sizeClassRegExp.firstMatch(sizeClass)?.group(1);
+ if (sizeClassSuffix == null) throw KatexHtmlParseError();
- if (resetSizeClassSuffix != null && sizeClassSuffix != null) {
- const sizes = [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2, 1.44, 1.728, 2.074, 2.488];
+ const sizes = [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2, 1.44, 1.728, 2.074, 2.488];
- final resetSizeIdx = int.parse(resetSizeClassSuffix, radix: 10);
- final sizeIdx = int.parse(sizeClassSuffix, radix: 10);
+ final resetSizeIdx = int.parse(resetSizeClassSuffix, radix: 10);
+ final sizeIdx = int.parse(sizeClassSuffix, radix: 10);
- // These indexes start at 1.
- if (resetSizeIdx <= sizes.length && sizeIdx <= sizes.length) {
- styles.fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
- continue;
- }
- }
-
- throw KatexHtmlParseError();
+ // These indexes start at 1.
+ if (resetSizeIdx > sizes.length) throw KatexHtmlParseError();
+ if (sizeIdx > sizes.length) throw KatexHtmlParseError();
+ styles.fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
case 'delimsizing':
// .delimsizing { ... }
From 403b1575394d9672963169ca3e2aa574f13eadd3 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:19:50 -0700
Subject: [PATCH 08/18] katex [nfc]: Small further simplifications in CSS-class
logic
---
lib/model/katex.dart | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 1a01f29f9e..cac29c6814 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -300,8 +300,7 @@ class _KatexParser {
case 'delimsizing':
// .delimsizing { ... }
if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
- final nextClass = spanClasses[index++];
- switch (nextClass) {
+ switch (spanClasses[index++]) {
case 'size1':
styles.fontFamily = 'KaTeX_Size1';
case 'size2':
@@ -313,24 +312,25 @@ class _KatexParser {
case 'mult':
// TODO handle nested spans with `.delim-size{1,4}` class.
- break;
- }
+ throw KatexHtmlParseError();
- if (styles.fontFamily == null) throw KatexHtmlParseError();
+ default:
+ throw KatexHtmlParseError();
+ }
// TODO handle .nulldelimiter and .delimcenter .
case 'op-symbol':
// .op-symbol { ... }
if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
- final nextClass = spanClasses[index++];
- switch (nextClass) {
+ switch (spanClasses[index++]) {
case 'small-op':
styles.fontFamily = 'KaTeX_Size1';
case 'large-op':
styles.fontFamily = 'KaTeX_Size2';
+ default:
+ throw KatexHtmlParseError();
}
- if (styles.fontFamily == null) throw KatexHtmlParseError();
// TODO handle more classes from katex.scss
From d76120a2f1d66d6dcdf2fe886c8004c72fbc422a Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:27:05 -0700
Subject: [PATCH 09/18] katex [nfc]: Make KatexSpanStyles immutable
Once the parsing is done, we want these to remain unchanged,
just like the other objects in the parse tree.
So, like ContentNode and its subclasses, make the class immutable.
The parser needs to mutate its own draft of what styles to apply
to a given span; but it can do that with its own local variables
corresponding to the fields, and construct a styles object at the
end of the loop.
---
lib/model/katex.dart | 101 ++++++++++++++++++++++++-------------------
1 file changed, 56 insertions(+), 45 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index cac29c6814..0a41cdc9b7 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -136,8 +136,6 @@ class _KatexParser {
KatexNode _parseSpan(dom.Element element) {
// TODO maybe check if the sequence of ancestors matter for spans.
- final spanClasses = List.unmodifiable(element.className.split(' '));
-
// Aggregate the CSS styles that apply, in the same order as the CSS
// classes specified for this span, mimicking the behaviour on web.
//
@@ -146,7 +144,12 @@ 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.
- var styles = KatexSpanStyles();
+ final spanClasses = List.unmodifiable(element.className.split(' '));
+ String? fontFamily;
+ double? fontSizeEm;
+ KatexSpanFontWeight? fontWeight;
+ KatexSpanFontStyle? fontStyle;
+ KatexSpanTextAlign? textAlign;
var index = 0;
while (index < spanClasses.length) {
final spanClass = spanClasses[index++];
@@ -163,15 +166,15 @@ class _KatexParser {
case 'textbf':
// .textbf { font-weight: bold; }
- styles.fontWeight = KatexSpanFontWeight.bold;
+ fontWeight = KatexSpanFontWeight.bold;
case 'textit':
// .textit { font-style: italic; }
- styles.fontStyle = KatexSpanFontStyle.italic;
+ fontStyle = KatexSpanFontStyle.italic;
case 'textrm':
// .textrm { font-family: KaTeX_Main; }
- styles.fontFamily = 'KaTeX_Main';
+ fontFamily = 'KaTeX_Main';
// case 'textsf':
// // .textsf { font-family: KaTeX_SansSerif; }
@@ -180,82 +183,82 @@ class _KatexParser {
case 'texttt':
// .texttt { font-family: KaTeX_Typewriter; }
- styles.fontFamily = 'KaTeX_Typewriter';
+ fontFamily = 'KaTeX_Typewriter';
case 'mathnormal':
// .mathnormal { font-family: KaTeX_Math; font-style: italic; }
- styles.fontFamily = 'KaTeX_Math';
- styles.fontStyle = KatexSpanFontStyle.italic;
+ fontFamily = 'KaTeX_Math';
+ fontStyle = KatexSpanFontStyle.italic;
case 'mathit':
// .mathit { font-family: KaTeX_Main; font-style: italic; }
- styles.fontFamily = 'KaTeX_Main';
- styles.fontStyle = KatexSpanFontStyle.italic;
+ fontFamily = 'KaTeX_Main';
+ fontStyle = KatexSpanFontStyle.italic;
case 'mathrm':
// .mathrm { font-style: normal; }
- styles.fontStyle = KatexSpanFontStyle.normal;
+ fontStyle = KatexSpanFontStyle.normal;
case 'mathbf':
// .mathbf { font-family: KaTeX_Main; font-weight: bold; }
- styles.fontFamily = 'KaTeX_Main';
- styles.fontWeight = KatexSpanFontWeight.bold;
+ fontFamily = 'KaTeX_Main';
+ fontWeight = KatexSpanFontWeight.bold;
case 'boldsymbol':
// .boldsymbol { font-family: KaTeX_Math; font-weight: bold; font-style: italic; }
- styles.fontFamily = 'KaTeX_Math';
- styles.fontWeight = KatexSpanFontWeight.bold;
- styles.fontStyle = KatexSpanFontStyle.italic;
+ fontFamily = 'KaTeX_Math';
+ fontWeight = KatexSpanFontWeight.bold;
+ fontStyle = KatexSpanFontStyle.italic;
case 'amsrm':
// .amsrm { font-family: KaTeX_AMS; }
- styles.fontFamily = 'KaTeX_AMS';
+ fontFamily = 'KaTeX_AMS';
case 'mathbb':
case 'textbb':
// .mathbb,
// .textbb { font-family: KaTeX_AMS; }
- styles.fontFamily = 'KaTeX_AMS';
+ fontFamily = 'KaTeX_AMS';
case 'mathcal':
// .mathcal { font-family: KaTeX_Caligraphic; }
- styles.fontFamily = 'KaTeX_Caligraphic';
+ fontFamily = 'KaTeX_Caligraphic';
case 'mathfrak':
case 'textfrak':
// .mathfrak,
// .textfrak { font-family: KaTeX_Fraktur; }
- styles.fontFamily = 'KaTeX_Fraktur';
+ fontFamily = 'KaTeX_Fraktur';
case 'mathboldfrak':
case 'textboldfrak':
// .mathboldfrak,
// .textboldfrak { font-family: KaTeX_Fraktur; font-weight: bold; }
- styles.fontFamily = 'KaTeX_Fraktur';
- styles.fontWeight = KatexSpanFontWeight.bold;
+ fontFamily = 'KaTeX_Fraktur';
+ fontWeight = KatexSpanFontWeight.bold;
case 'mathtt':
// .mathtt { font-family: KaTeX_Typewriter; }
- styles.fontFamily = 'KaTeX_Typewriter';
+ fontFamily = 'KaTeX_Typewriter';
case 'mathscr':
case 'textscr':
// .mathscr,
// .textscr { font-family: KaTeX_Script; }
- styles.fontFamily = 'KaTeX_Script';
+ fontFamily = 'KaTeX_Script';
case 'mathsf':
case 'textsf':
// .mathsf,
// .textsf { font-family: KaTeX_SansSerif; }
- styles.fontFamily = 'KaTeX_SansSerif';
+ fontFamily = 'KaTeX_SansSerif';
case 'mathboldsf':
case 'textboldsf':
// .mathboldsf,
// .textboldsf { font-family: KaTeX_SansSerif; font-weight: bold; }
- styles.fontFamily = 'KaTeX_SansSerif';
- styles.fontWeight = KatexSpanFontWeight.bold;
+ fontFamily = 'KaTeX_SansSerif';
+ fontWeight = KatexSpanFontWeight.bold;
case 'mathsfit':
case 'mathitsf':
@@ -263,13 +266,13 @@ class _KatexParser {
// .mathsfit,
// .mathitsf,
// .textitsf { font-family: KaTeX_SansSerif; font-style: italic; }
- styles.fontFamily = 'KaTeX_SansSerif';
- styles.fontStyle = KatexSpanFontStyle.italic;
+ fontFamily = 'KaTeX_SansSerif';
+ fontStyle = KatexSpanFontStyle.italic;
case 'mainrm':
// .mainrm { font-family: KaTeX_Main; font-style: normal; }
- styles.fontFamily = 'KaTeX_Main';
- styles.fontStyle = KatexSpanFontStyle.normal;
+ fontFamily = 'KaTeX_Main';
+ fontStyle = KatexSpanFontStyle.normal;
// TODO handle skipped class declarations between .mainrm and
// .sizing .
@@ -295,20 +298,20 @@ class _KatexParser {
// These indexes start at 1.
if (resetSizeIdx > sizes.length) throw KatexHtmlParseError();
if (sizeIdx > sizes.length) throw KatexHtmlParseError();
- styles.fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
+ fontSizeEm = sizes[sizeIdx - 1] / sizes[resetSizeIdx - 1];
case 'delimsizing':
// .delimsizing { ... }
if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
switch (spanClasses[index++]) {
case 'size1':
- styles.fontFamily = 'KaTeX_Size1';
+ fontFamily = 'KaTeX_Size1';
case 'size2':
- styles.fontFamily = 'KaTeX_Size2';
+ fontFamily = 'KaTeX_Size2';
case 'size3':
- styles.fontFamily = 'KaTeX_Size3';
+ fontFamily = 'KaTeX_Size3';
case 'size4':
- styles.fontFamily = 'KaTeX_Size4';
+ fontFamily = 'KaTeX_Size4';
case 'mult':
// TODO handle nested spans with `.delim-size{1,4}` class.
@@ -325,9 +328,9 @@ class _KatexParser {
if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
switch (spanClasses[index++]) {
case 'small-op':
- styles.fontFamily = 'KaTeX_Size1';
+ fontFamily = 'KaTeX_Size1';
case 'large-op':
- styles.fontFamily = 'KaTeX_Size2';
+ fontFamily = 'KaTeX_Size2';
default:
throw KatexHtmlParseError();
}
@@ -344,6 +347,13 @@ class _KatexParser {
_logError('KaTeX: Unsupported CSS class: $spanClass');
}
}
+ final styles = KatexSpanStyles(
+ fontFamily: fontFamily,
+ fontSizeEm: fontSizeEm,
+ fontWeight: fontWeight,
+ fontStyle: fontStyle,
+ textAlign: textAlign,
+ );
String? text;
List? spans;
@@ -376,14 +386,15 @@ enum KatexSpanTextAlign {
right,
}
+@immutable
class KatexSpanStyles {
- String? fontFamily;
- double? fontSizeEm;
- KatexSpanFontWeight? fontWeight;
- KatexSpanFontStyle? fontStyle;
- KatexSpanTextAlign? textAlign;
+ final String? fontFamily;
+ final double? fontSizeEm;
+ final KatexSpanFontWeight? fontWeight;
+ final KatexSpanFontStyle? fontStyle;
+ final KatexSpanTextAlign? textAlign;
- KatexSpanStyles({
+ const KatexSpanStyles({
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
From 31942c3dc4bb81c00d82c0256c19644018b2c586 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:34:51 -0700
Subject: [PATCH 10/18] katex [nfc]: Compact a bit using switch-expressions
---
lib/model/katex.dart | 38 ++++++++++++++------------------------
1 file changed, 14 insertions(+), 24 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 0a41cdc9b7..709f91b4b2 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -303,37 +303,27 @@ class _KatexParser {
case 'delimsizing':
// .delimsizing { ... }
if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
- switch (spanClasses[index++]) {
- case 'size1':
- fontFamily = 'KaTeX_Size1';
- case 'size2':
- fontFamily = 'KaTeX_Size2';
- case 'size3':
- fontFamily = 'KaTeX_Size3';
- case 'size4':
- fontFamily = 'KaTeX_Size4';
-
- case 'mult':
+ fontFamily = switch (spanClasses[index++]) {
+ 'size1' => 'KaTeX_Size1',
+ 'size2' => 'KaTeX_Size2',
+ 'size3' => 'KaTeX_Size3',
+ 'size4' => 'KaTeX_Size4',
+ 'mult' =>
// TODO handle nested spans with `.delim-size{1,4}` class.
- throw KatexHtmlParseError();
-
- default:
- throw KatexHtmlParseError();
- }
+ throw KatexHtmlParseError(),
+ _ => throw KatexHtmlParseError(),
+ };
// TODO handle .nulldelimiter and .delimcenter .
case 'op-symbol':
// .op-symbol { ... }
if (index + 1 > spanClasses.length) throw KatexHtmlParseError();
- switch (spanClasses[index++]) {
- case 'small-op':
- fontFamily = 'KaTeX_Size1';
- case 'large-op':
- fontFamily = 'KaTeX_Size2';
- default:
- throw KatexHtmlParseError();
- }
+ fontFamily = switch (spanClasses[index++]) {
+ 'small-op' => 'KaTeX_Size1',
+ 'large-op' => 'KaTeX_Size2',
+ _ => throw KatexHtmlParseError(),
+ };
// TODO handle more classes from katex.scss
From 6bb726ac92f01ea5a33b4e414b1fb141710218f3 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 17:46:00 -0700
Subject: [PATCH 11/18] katex [nfc]: Factor out _KatexNodeList widget
This deduplicates the logic for the particular way that a list of
KaTeX nodes get combined into a single widget.
---
lib/widgets/content.dart | 33 +++++++++++++++++++--------------
1 file changed, 19 insertions(+), 14 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 4bf6f8adae..80559a4cf7 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -838,13 +838,7 @@ class _Katex extends StatelessWidget {
@override
Widget build(BuildContext context) {
- Widget widget = Text.rich(TextSpan(
- children: List.unmodifiable(nodes.map((e) {
- return WidgetSpan(
- alignment: PlaceholderAlignment.baseline,
- baseline: TextBaseline.alphabetic,
- child: _KatexSpan(e));
- }))));
+ Widget widget = _KatexNodeList(nodes: nodes);
if (!inline) {
widget = Center(
@@ -862,6 +856,23 @@ class _Katex extends StatelessWidget {
}
}
+class _KatexNodeList extends StatelessWidget {
+ const _KatexNodeList({required this.nodes});
+
+ final List nodes;
+
+ @override
+ Widget build(BuildContext context) {
+ return Text.rich(TextSpan(
+ children: List.unmodifiable(nodes.map((e) {
+ return WidgetSpan(
+ alignment: PlaceholderAlignment.baseline,
+ baseline: TextBaseline.alphabetic,
+ child: _KatexSpan(e));
+ }))));
+ }
+}
+
class _KatexSpan extends StatelessWidget {
const _KatexSpan(this.span);
@@ -875,13 +886,7 @@ class _KatexSpan extends StatelessWidget {
if (span.text != null) {
widget = Text(span.text!);
} else if (span.nodes != null && span.nodes!.isNotEmpty) {
- widget = Text.rich(TextSpan(
- children: List.unmodifiable(span.nodes!.map((e) {
- return WidgetSpan(
- alignment: PlaceholderAlignment.baseline,
- baseline: TextBaseline.alphabetic,
- child: _KatexSpan(e));
- }))));
+ widget = _KatexNodeList(nodes: span.nodes!);
}
final styles = span.styles;
From 0eb1410aad3e4a7090f9ca27d3b7a1fefba0be2a Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Mon, 21 Apr 2025 18:21:56 -0700
Subject: [PATCH 12/18] katex [nfc]: Rename _KatexSpan field to "node"
This makes it more uniform with our other content widgets.
---
lib/widgets/content.dart | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 80559a4cf7..0305edde91 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -874,22 +874,22 @@ class _KatexNodeList extends StatelessWidget {
}
class _KatexSpan extends StatelessWidget {
- const _KatexSpan(this.span);
+ const _KatexSpan(this.node);
- final KatexNode span;
+ final KatexNode node;
@override
Widget build(BuildContext context) {
final em = DefaultTextStyle.of(context).style.fontSize!;
Widget widget = const SizedBox.shrink();
- if (span.text != null) {
- widget = Text(span.text!);
- } else if (span.nodes != null && span.nodes!.isNotEmpty) {
- widget = _KatexNodeList(nodes: span.nodes!);
+ if (node.text != null) {
+ widget = Text(node.text!);
+ } else if (node.nodes != null && node.nodes!.isNotEmpty) {
+ widget = _KatexNodeList(nodes: node.nodes!);
}
- final styles = span.styles;
+ final styles = node.styles;
final fontFamily = styles.fontFamily;
final fontSize = switch (styles.fontSizeEm) {
From d3b21fd76c8c0fe78cdc89aa74bb73c82b35d890 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 24 Apr 2025 11:37:46 +0530
Subject: [PATCH 13/18] 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 4b51709de8c5be491b79312be9fe7991fae08307 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 1 Apr 2025 18:32:25 +0530
Subject: [PATCH 14/18] content: Support parsing and handling inline styles for
KaTeX content
---
lib/model/katex.dart | 83 +++++++++++++++++++++++++++++++++++-
lib/widgets/content.dart | 15 ++++++-
test/model/content_test.dart | 24 ++++++-----
3 files changed, 109 insertions(+), 13 deletions(-)
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 709f91b4b2..a35fe677ee 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;
@@ -354,11 +356,67 @@ class _KatexParser {
}
if (text == null && spans == null) throw KatexHtmlParseError();
+ final inlineStyles = _parseSpanInlineStyles(element);
+
return KatexNode(
- styles: styles,
+ styles: inlineStyles != null
+ ? styles.merge(inlineStyles)
+ : styles,
text: text,
nodes: spans);
}
+
+ 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;
+ double? verticalAlignEm;
+
+ 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;
+
+ case 'vertical-align':
+ verticalAlignEm = _getEm(expression);
+ if (verticalAlignEm != null) continue;
+ }
+
+ // TODO handle more CSS properties
+ _logError('KaTeX: Unsupported CSS property: $property of '
+ 'type ${expression.runtimeType}');
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+
+ return KatexSpanStyles(
+ heightEm: heightEm,
+ verticalAlignEm: verticalAlignEm,
+ );
+ } else {
+ throw KatexHtmlParseError();
+ }
+ }
+ return 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 {
@@ -378,6 +436,9 @@ enum KatexSpanTextAlign {
@immutable
class KatexSpanStyles {
+ final double? heightEm;
+ final double? verticalAlignEm;
+
final String? fontFamily;
final double? fontSizeEm;
final KatexSpanFontWeight? fontWeight;
@@ -385,6 +446,8 @@ class KatexSpanStyles {
final KatexSpanTextAlign? textAlign;
const KatexSpanStyles({
+ this.heightEm,
+ this.verticalAlignEm,
this.fontFamily,
this.fontSizeEm,
this.fontWeight,
@@ -395,6 +458,8 @@ class KatexSpanStyles {
@override
int get hashCode => Object.hash(
'KatexSpanStyles',
+ heightEm,
+ verticalAlignEm,
fontFamily,
fontSizeEm,
fontWeight,
@@ -405,6 +470,8 @@ class KatexSpanStyles {
@override
bool operator ==(Object other) {
return other is KatexSpanStyles &&
+ other.heightEm == heightEm &&
+ other.verticalAlignEm == verticalAlignEm &&
other.fontFamily == fontFamily &&
other.fontSizeEm == fontSizeEm &&
other.fontWeight == fontWeight &&
@@ -415,6 +482,8 @@ class KatexSpanStyles {
@override
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');
@@ -422,6 +491,18 @@ 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,
+ verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
+ 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 0305edde91..d26716b6ae 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -939,7 +939,20 @@ class _KatexSpan extends StatelessWidget {
textAlign: textAlign,
child: widget);
}
- return widget;
+
+ if (styles.verticalAlignEm != null) {
+ widget = Baseline(
+ baseline: styles.verticalAlignEm! * em,
+ baselineType: TextBaseline.alphabetic,
+ child: 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 6a0bc6ebe7..f8c1fc7c6d 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -519,7 +519,7 @@ class ContentExample {
'λ
',
MathInlineNode(texSource: r'\lambda', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -539,7 +539,7 @@ class ContentExample {
'λ
',
[MathBlockNode(texSource: r'\lambda', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -564,7 +564,7 @@ class ContentExample {
'b', [
MathBlockNode(texSource: 'a', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -575,7 +575,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'b', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -603,7 +603,7 @@ class ContentExample {
[QuotationNode([
MathBlockNode(texSource: r'\lambda', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -632,7 +632,7 @@ class ContentExample {
[QuotationNode([
MathBlockNode(texSource: 'a', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.4306), text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -643,7 +643,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'b', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(), text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.6944), text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -681,7 +681,7 @@ class ContentExample {
]),
MathBlockNode(texSource: 'a', nodes: [
KatexNode(styles: KatexSpanStyles(), text: null, nodes: [
- KatexNode(styles: KatexSpanStyles(),text: null, nodes: []),
+ KatexNode(styles: KatexSpanStyles(heightEm: 0.4306),text: null, nodes: []),
KatexNode(
styles: KatexSpanStyles(
fontFamily: 'KaTeX_Math',
@@ -732,7 +732,7 @@ class ContentExample {
text: null,
nodes: [
KatexNode(
- styles: KatexSpanStyles(),
+ styles: KatexSpanStyles(heightEm: 1.6034),
text: null,
nodes: []),
KatexNode(
@@ -801,7 +801,7 @@ class ContentExample {
text: null,
nodes: [
KatexNode(
- styles: KatexSpanStyles(),
+ styles: KatexSpanStyles(heightEm: 1.6034),
text: null,
nodes: []),
KatexNode(
@@ -846,7 +846,9 @@ class ContentExample {
text: null,
nodes: [
KatexNode(
- styles: KatexSpanStyles(),
+ styles: KatexSpanStyles(
+ heightEm: 3.0,
+ verticalAlignEm: -1.25),
text: null,
nodes: []),
KatexNode(
From f794730ba1c895aa97087ee300f8346567faddbe Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 22 Apr 2025 18:01:46 +0530
Subject: [PATCH 15/18] 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 | 25 +++++++++++++++++--------
test/widgets/content_test.dart | 30 ++++++++++++++++++++----------
2 files changed, 37 insertions(+), 18 deletions(-)
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index d26716b6ae..fdfe5a9748 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -816,24 +816,34 @@ class MathBlock extends StatelessWidget {
children: [TextSpan(text: node.texSource)])));
}
- return _Katex(inline: false, nodes: nodes);
+ return _Katex(
+ inline: false,
+ 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);
+TextStyle mkBaseKatexTextStyle(TextStyle style) {
+ assert(style.fontSize != null);
+ 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.inline,
+ required this.textStyle,
required this.nodes,
});
final bool inline;
+ final TextStyle textStyle;
final List nodes;
@override
@@ -850,8 +860,7 @@ class _Katex extends StatelessWidget {
return Directionality(
textDirection: TextDirection.ltr,
child: DefaultTextStyle(
- style: kBaseKatexTextStyle.copyWith(
- color: ContentTheme.of(context).textStylePlainParagraph.color),
+ style: mkBaseKatexTextStyle(textStyle),
child: widget));
}
}
@@ -1274,7 +1283,7 @@ class _InlineContentBuilder {
: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _Katex(inline: true, nodes: nodes));
+ child: _Katex(inline: true, 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 a788225aac..84c2b15ad2 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -595,15 +595,19 @@ 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;
final nodes = baseNode.nodes!.skip(1); // Skip .strut node.
for (final katexNode in nodes) {
- 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!);
}
});
@@ -616,17 +620,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 {
@@ -642,13 +650,15 @@ void main() {
final baseNode = mathBlockNode.nodes!.single;
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;
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) {
@@ -656,8 +666,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!);
}
});
});
From 5309a50c8dbffaa629da6f18be2bcec7edc766eb Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Thu, 24 Apr 2025 13:21:18 +0530
Subject: [PATCH 16/18] 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 a35fe677ee..42b65d68cf 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -358,7 +358,7 @@ class _KatexParser {
final inlineStyles = _parseSpanInlineStyles(element);
- return KatexNode(
+ return KatexSpanNode(
styles: inlineStyles != null
? styles.merge(inlineStyles)
: styles,
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index fdfe5a9748..3b0fc6eab9 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -877,7 +877,9 @@ class _KatexNodeList extends StatelessWidget {
return WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
- child: _KatexSpan(e));
+ child: switch (e) {
+ KatexSpanNode() => _KatexSpan(e),
+ });
}))));
}
}
@@ -885,7 +887,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 f8c1fc7c6d..315baa5e4c 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(heightEm: 0.6944), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), 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(heightEm: 0.6944), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), 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(heightEm: 0.4306), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), 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(heightEm: 0.6944), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), 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(heightEm: 0.6944), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), 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(heightEm: 0.4306), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306), 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(heightEm: 0.6944), text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.6944), 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(heightEm: 0.4306),text: null, nodes: []),
- KatexNode(
+ KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [
+ KatexSpanNode(styles: KatexSpanStyles(heightEm: 0.4306),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(heightEm: 1.6034),
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(heightEm: 1.6034),
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,52 +841,52 @@ class ContentExample {
MathBlockNode(
texSource: '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊',
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(),
text: null,
nodes: [
- KatexNode(
+ KatexSpanNode(
styles: KatexSpanStyles(
heightEm: 3.0,
verticalAlignEm: -1.25),
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 84c2b15ad2..d5445fb931 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -600,9 +600,10 @@ void main() {
mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
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! * baseTextStyle.fontSize!;
checkKatexText(tester, katexNode.text!,
fontFamily: 'KaTeX_Main',
@@ -647,14 +648,14 @@ 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 context = tester.element(find.byType(MathBlock));
final baseTextStyle =
mkBaseKatexTextStyle(ContentTheme.of(context).textStylePlainParagraph);
- final firstNode = nodes.first;
+ final firstNode = nodes.first as KatexSpanNode;
checkKatexText(tester, firstNode.text!,
fontFamily: 'KaTeX_Main',
fontSize: baseTextStyle.fontSize!,
@@ -662,7 +663,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 a21ec64f924a88f57d764c572582b21a6bb4b2b7 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 1 Apr 2025 21:29:15 +0530
Subject: [PATCH 17/18] content: Handle vertical offset spans in KaTeX content
Implement handling most common types of `vlist` spans.
---
lib/model/content.dart | 35 ++++++++++++
lib/model/katex.dart | 117 ++++++++++++++++++++++++++++++++++++++-
lib/widgets/content.dart | 27 +++++++++
3 files changed, 178 insertions(+), 1 deletion(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 768031ae9a..8796373f9b 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -406,6 +406,41 @@ class KatexSpanNode 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,
+ this.nodes = const [],
+ });
+
+ final double verticalOffsetEm;
+ final List nodes;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(StringProperty('verticalOffsetEm', '$verticalOffsetEm'));
+ }
+
+ @override
+ List debugDescribeChildren() {
+ return nodes.map((node) => node.toDiagnosticsNode()).toList();
+ }
+}
+
class MathBlockNode extends MathNode implements BlockContentNode {
const MathBlockNode({
super.debugHtmlNode,
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 42b65d68cf..a207ead3c5 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -138,6 +138,110 @@ class _KatexParser {
KatexNode _parseSpan(dom.Element element) {
// TODO maybe check if the sequence of ancestors matter for spans.
+ final spanClasses = List.unmodifiable(element.className.split(' '));
+
+ if (element case dom.Element(localName: 'span', :final className)
+ when className.startsWith('vlist')) {
+ switch (element) {
+ case dom.Element(
+ localName: 'span',
+ className: 'vlist-t',
+ attributes: final attributesVlistT,
+ nodes: [
+ dom.Element(
+ localName: 'span',
+ className: 'vlist-r',
+ attributes: final attributesVlistR,
+ nodes: [
+ dom.Element(
+ localName: 'span',
+ className: 'vlist',
+ nodes: [
+ dom.Element(
+ localName: 'span',
+ className: '',
+ nodes: [
+ dom.Element(localName: 'span', className: 'pstrut')
+ && final pstrutSpan,
+ ...,
+ ]) && final innerSpan,
+ ]),
+ ]),
+ ])
+ when !attributesVlistT.containsKey('style') &&
+ !attributesVlistR.containsKey('style'):
+ // TODO vlist element should only have `height` style, which we ignore.
+
+ var styles = _parseSpanInlineStyles(innerSpan)!;
+ final topEm = styles.topEm ?? 0;
+
+ final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
+ final pstrutHeight = pstrutStyles.heightEm ?? 0;
+
+ // TODO handle negative right-margin inline style on row nodes.
+ return KatexVlistNode(rows: [
+ KatexVlistRowNode(
+ verticalOffsetEm: topEm + pstrutHeight,
+ nodes: _parseChildSpans(innerSpan)),
+ ]);
+
+ case dom.Element(
+ localName: 'span',
+ className: 'vlist-t vlist-t2',
+ attributes: final attributesVlistT,
+ nodes: [
+ dom.Element(
+ localName: 'span',
+ className: 'vlist-r',
+ attributes: final attributesVlistR,
+ nodes: [
+ dom.Element(
+ localName: 'span',
+ className: 'vlist',
+ nodes: [...]) && final vlist1,
+ dom.Element(localName: 'span', className: 'vlist-s'),
+ ]),
+ dom.Element(localName: 'span', className: 'vlist-r', nodes: [
+ dom.Element(localName: 'span', className: 'vlist', nodes: [
+ dom.Element(localName: 'span', className: '', nodes: []),
+ ])
+ ]),
+ ])
+ when !attributesVlistT.containsKey('style') &&
+ !attributesVlistR.containsKey('style'):
+ // TODO Ensure both should only have a `height` style.
+
+ final rows = [];
+
+ for (final innerSpan in vlist1.nodes) {
+ if (innerSpan case dom.Element(
+ localName: 'span',
+ className: '',
+ nodes: [
+ dom.Element(localName: 'span', className: 'pstrut') &&
+ final pstrutSpan,
+ ...,
+ ])) {
+ final styles = _parseSpanInlineStyles(innerSpan)!;
+ final topEm = styles.topEm ?? 0;
+
+ final pstrutStyles = _parseSpanInlineStyles(pstrutSpan)!;
+ final pstrutHeight = pstrutStyles.heightEm ?? 0;
+
+ // TODO handle negative right-margin inline style on row nodes.
+ rows.add(KatexVlistRowNode(
+ verticalOffsetEm: topEm + pstrutHeight,
+ nodes: _parseChildSpans(innerSpan)));
+ }
+ }
+
+ return KatexVlistNode(rows: rows);
+
+ default:
+ 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.
//
@@ -146,7 +250,6 @@ 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(' '));
String? fontFamily;
double? fontSizeEm;
KatexSpanFontWeight? fontWeight;
@@ -374,6 +477,7 @@ class _KatexParser {
final stylesheet = css_parser.parse('*{$styleStr}');
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
double? heightEm;
+ double? topEm;
double? verticalAlignEm;
for (final declaration in rule.declarationGroup.declarations) {
@@ -387,6 +491,10 @@ class _KatexParser {
heightEm = _getEm(expression);
if (heightEm != null) continue;
+ case 'top':
+ topEm = _getEm(expression);
+ if (topEm != null) continue;
+
case 'vertical-align':
verticalAlignEm = _getEm(expression);
if (verticalAlignEm != null) continue;
@@ -402,6 +510,7 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
+ topEm: topEm,
verticalAlignEm: verticalAlignEm,
);
} else {
@@ -437,6 +546,7 @@ enum KatexSpanTextAlign {
@immutable
class KatexSpanStyles {
final double? heightEm;
+ final double? topEm;
final double? verticalAlignEm;
final String? fontFamily;
@@ -447,6 +557,7 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
+ this.topEm,
this.verticalAlignEm,
this.fontFamily,
this.fontSizeEm,
@@ -459,6 +570,7 @@ class KatexSpanStyles {
int get hashCode => Object.hash(
'KatexSpanStyles',
heightEm,
+ topEm,
verticalAlignEm,
fontFamily,
fontSizeEm,
@@ -471,6 +583,7 @@ class KatexSpanStyles {
bool operator ==(Object other) {
return other is KatexSpanStyles &&
other.heightEm == heightEm &&
+ other.topEm == topEm &&
other.verticalAlignEm == verticalAlignEm &&
other.fontFamily == fontFamily &&
other.fontSizeEm == fontSizeEm &&
@@ -483,6 +596,7 @@ class KatexSpanStyles {
String toString() {
final args = [];
if (heightEm != null) args.add('heightEm: $heightEm');
+ if (topEm != null) args.add('topEm: $topEm');
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
if (fontFamily != null) args.add('fontFamily: $fontFamily');
if (fontSizeEm != null) args.add('fontSizeEm: $fontSizeEm');
@@ -495,6 +609,7 @@ class KatexSpanStyles {
KatexSpanStyles merge(KatexSpanStyles other) {
return KatexSpanStyles(
heightEm: other.heightEm ?? heightEm,
+ topEm: other.topEm ?? topEm,
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
fontFamily: other.fontFamily ?? fontFamily,
fontSizeEm: other.fontSizeEm ?? fontSizeEm,
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 3b0fc6eab9..9141d777e7 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -879,6 +879,7 @@ class _KatexNodeList extends StatelessWidget {
baseline: TextBaseline.alphabetic,
child: switch (e) {
KatexSpanNode() => _KatexSpan(e),
+ KatexVlistNode() => _KatexVlist(e),
});
}))));
}
@@ -967,6 +968,32 @@ class _KatexSpan 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: RichText(text: TextSpan(
+ children: List.unmodifiable(row.nodes.map((e) {
+ return WidgetSpan(
+ alignment: PlaceholderAlignment.baseline,
+ baseline: TextBaseline.alphabetic,
+ child: switch (e) {
+ KatexSpanNode() => _KatexSpan(e),
+ KatexVlistNode() => _KatexVlist(e),
+ });
+ })))));
+ })));
+ }
+}
+
class WebsitePreview extends StatelessWidget {
const WebsitePreview({super.key, required this.node});
From 16cb28ffd464dfd224b50a6eac5f86a2f374ebf3 Mon Sep 17 00:00:00 2001
From: Rajesh Malviya
Date: Tue, 1 Apr 2025 20:06:41 +0530
Subject: [PATCH 18/18] content: Support negative right-margin on KaTeX spans
Negative margin spans on web render to the offset being applied
to the specific span and all the adjacent spans, so mimic the
same behaviour here.
---
lib/model/content.dart | 22 ++++++++++++++++++++++
lib/model/katex.dart | 38 +++++++++++++++++++++++++++++++++-----
lib/widgets/content.dart | 32 +++++++++++++++++++++++++++++++-
3 files changed, 86 insertions(+), 6 deletions(-)
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 8796373f9b..fbd56b006e 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -441,6 +441,28 @@ class KatexVlistRowNode extends ContentNode {
}
}
+class KatexNegativeMarginNode extends KatexNode {
+ const KatexNegativeMarginNode({
+ required this.marginRightEm,
+ required this.nodes,
+ super.debugHtmlNode,
+ });
+
+ final double marginRightEm;
+ final List nodes;
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(StringProperty('marginRightEm', '$marginRightEm'));
+ }
+
+ @override
+ List debugDescribeChildren() {
+ return nodes.map((node) => node.toDiagnosticsNode()).toList();
+ }
+}
+
class MathBlockNode extends MathNode implements BlockContentNode {
const MathBlockNode({
super.debugHtmlNode,
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index a207ead3c5..4c71ba16b5 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -123,13 +123,29 @@ class _KatexParser {
}
List _parseChildSpans(dom.Element element) {
- return List.unmodifiable(element.nodes.map((node) {
- if (node case dom.Element(localName: 'span')) {
- return _parseSpan(node);
- } else {
+ var resultSpans = [];
+ for (final node in element.nodes.reversed) {
+ if (node is! dom.Element || node.localName != 'span') {
throw KatexHtmlParseError();
}
- }));
+
+ final span = _parseSpan(node);
+ resultSpans.add(span);
+
+ if (span is KatexSpanNode) {
+ final marginRightEm = span.styles.marginRightEm;
+ if (marginRightEm != null && marginRightEm.isNegative) {
+ final previousSpansReversed =
+ resultSpans.reversed.toList(growable: false);
+ resultSpans = [];
+ resultSpans.add(KatexNegativeMarginNode(
+ marginRightEm: marginRightEm,
+ nodes: previousSpansReversed));
+ }
+ }
+ }
+
+ return resultSpans.reversed.toList(growable: false);
}
static final _resetSizeClassRegExp = RegExp(r'^reset-size(\d\d?)$');
@@ -477,6 +493,7 @@ class _KatexParser {
final stylesheet = css_parser.parse('*{$styleStr}');
if (stylesheet.topLevels case [css_visitor.RuleSet() && final rule]) {
double? heightEm;
+ double? marginRightEm;
double? topEm;
double? verticalAlignEm;
@@ -491,6 +508,10 @@ class _KatexParser {
heightEm = _getEm(expression);
if (heightEm != null) continue;
+ case 'margin-right':
+ marginRightEm = _getEm(expression);
+ if (marginRightEm != null) continue;
+
case 'top':
topEm = _getEm(expression);
if (topEm != null) continue;
@@ -510,6 +531,7 @@ class _KatexParser {
return KatexSpanStyles(
heightEm: heightEm,
+ marginRightEm: marginRightEm,
topEm: topEm,
verticalAlignEm: verticalAlignEm,
);
@@ -546,6 +568,7 @@ enum KatexSpanTextAlign {
@immutable
class KatexSpanStyles {
final double? heightEm;
+ final double? marginRightEm;
final double? topEm;
final double? verticalAlignEm;
@@ -557,6 +580,7 @@ class KatexSpanStyles {
const KatexSpanStyles({
this.heightEm,
+ this.marginRightEm,
this.topEm,
this.verticalAlignEm,
this.fontFamily,
@@ -570,6 +594,7 @@ class KatexSpanStyles {
int get hashCode => Object.hash(
'KatexSpanStyles',
heightEm,
+ marginRightEm,
topEm,
verticalAlignEm,
fontFamily,
@@ -583,6 +608,7 @@ class KatexSpanStyles {
bool operator ==(Object other) {
return other is KatexSpanStyles &&
other.heightEm == heightEm &&
+ other.marginRightEm == marginRightEm &&
other.topEm == topEm &&
other.verticalAlignEm == verticalAlignEm &&
other.fontFamily == fontFamily &&
@@ -596,6 +622,7 @@ class KatexSpanStyles {
String toString() {
final args = [];
if (heightEm != null) args.add('heightEm: $heightEm');
+ if (marginRightEm != null) args.add('marginRightEm: $marginRightEm');
if (topEm != null) args.add('topEm: $topEm');
if (verticalAlignEm != null) args.add('verticalAlignEm: $verticalAlignEm');
if (fontFamily != null) args.add('fontFamily: $fontFamily');
@@ -609,6 +636,7 @@ class KatexSpanStyles {
KatexSpanStyles merge(KatexSpanStyles other) {
return KatexSpanStyles(
heightEm: other.heightEm ?? heightEm,
+ marginRightEm: other.marginRightEm ?? marginRightEm,
topEm: other.topEm ?? topEm,
verticalAlignEm: other.verticalAlignEm ?? verticalAlignEm,
fontFamily: other.fontFamily ?? fontFamily,
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 9141d777e7..b7035dac7f 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -880,6 +880,7 @@ class _KatexNodeList extends StatelessWidget {
child: switch (e) {
KatexSpanNode() => _KatexSpan(e),
KatexVlistNode() => _KatexVlist(e),
+ KatexNegativeMarginNode() => _KatexNegativeMargin(e),
});
}))));
}
@@ -959,7 +960,10 @@ class _KatexSpan extends StatelessWidget {
child: widget);
}
- return SizedBox(
+ return Container(
+ margin: styles.marginRightEm != null && !styles.marginRightEm!.isNegative
+ ? EdgeInsets.only(right: styles.marginRightEm! * em)
+ : null,
height: styles.heightEm != null
? styles.heightEm! * em
: null,
@@ -988,12 +992,38 @@ class _KatexVlist extends StatelessWidget {
child: switch (e) {
KatexSpanNode() => _KatexSpan(e),
KatexVlistNode() => _KatexVlist(e),
+ KatexNegativeMarginNode() => _KatexNegativeMargin(e),
});
})))));
})));
}
}
+class _KatexNegativeMargin extends StatelessWidget {
+ const _KatexNegativeMargin(this.node);
+
+ final KatexNegativeMarginNode node;
+
+ @override
+ Widget build(BuildContext context) {
+ final em = DefaultTextStyle.of(context).style.fontSize!;
+
+ return Transform.translate(
+ offset: Offset(node.marginRightEm * em, 0),
+ child: Text.rich(TextSpan(
+ children: List.unmodifiable(node.nodes.map((e) {
+ return WidgetSpan(
+ alignment: PlaceholderAlignment.baseline,
+ baseline: TextBaseline.alphabetic,
+ child: switch (e) {
+ KatexSpanNode() => _KatexSpan(e),
+ KatexVlistNode() => _KatexVlist(e),
+ KatexNegativeMarginNode() => _KatexNegativeMargin(e),
+ });
+ })))));
+ }
+}
+
class WebsitePreview extends StatelessWidget {
const WebsitePreview({super.key, required this.node});