From ee33dc729efa062d569e0f88a79d967c6f6642eb Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 16:17:07 -0700 Subject: [PATCH 01/12] 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 4bce7f2914b6318fbb111b953e82c5db7d7e76a0 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 16:38:22 -0700 Subject: [PATCH 02/12] 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 cbecb6b05a0b493ef67fcd8061020d7d92f698fa Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 16:50:42 -0700 Subject: [PATCH 03/12] 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 35bb431e1d73e23fa856808485ffdc59e81383a1 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:04:25 -0700 Subject: [PATCH 04/12] 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 071166c84780b776cf898102ac2ee6a6a8fb547f Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:07:07 -0700 Subject: [PATCH 05/12] 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 2d437716181e99ed11830b19374d54cfd190e887 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:12:20 -0700 Subject: [PATCH 06/12] 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 1f7cdd57479ff26b35f4f526a324b4fd7c35c54b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:16:53 -0700 Subject: [PATCH 07/12] 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 64cbe8577b8d04fa33298a894436309c99d48cff Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:19:50 -0700 Subject: [PATCH 08/12] 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 a8b5feb851fada32a467b5c2a9d19a7aa24a7505 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:27:05 -0700 Subject: [PATCH 09/12] 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 7dc199ce71a7b416e0ce8b350753bb2c30b1ef08 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:34:51 -0700 Subject: [PATCH 10/12] 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 102a6dec38e5bfc9600117caebe11cac26442c6f Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 17:46:00 -0700 Subject: [PATCH 11/12] 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 6852eaac9770c6e9950f8ebc23e766dbd0ef14f6 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Apr 2025 18:21:56 -0700 Subject: [PATCH 12/12] 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) {