From 459ca983ab14ebab133841b6518a078c4b0ffa39 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 14:01:42 -0700 Subject: [PATCH 01/11] katex test [nfc]: Split parsing tests to their own file, like implementation This way we maintain our usual parallel between the files that tests live in and the files with the code the tests are about. There's more we can do to make use of having a specific class for these. But we'll do that in separate commits, so that this massive move commit is as pure of a move as possible. --- test/model/content_test.dart | 655 +------------------------------ test/model/katex_test.dart | 698 +++++++++++++++++++++++++++++++++ test/widgets/content_test.dart | 23 +- 3 files changed, 713 insertions(+), 663 deletions(-) create mode 100644 test/model/katex_test.dart diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 88bd11c66c..bead811079 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -699,647 +699,6 @@ 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 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```', - '

' - '' - '1234567890' - '\\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' - '

', - [ - 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: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 - text: '1', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 - text: '2', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 - text: '3', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 - text: '4', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 - text: '5', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 - text: '6', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 - text: '7', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 - text: '8', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: '9', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 - text: '0', - nodes: null), - ]), - ]), - ]); - - static const mathBlockKatexNestedSizing = ContentExample( - 'math block; KaTeX nested sizing', - '```math\n\\tiny {1 \\Huge 2}\n```', - '

' - '' - '12' - '\\tiny {1 \\Huge 2}' - '

', - [ - MathBlockNode( - texSource: '\\tiny {1 \\Huge 2}', - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: '1', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 - text: '2', - nodes: null), - ]), - ]), - ]), - ]); - - 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```', - '

' - '' - '([' - '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊' - '

', - [ - MathBlockNode( - texSource: '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊', - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), - KatexSpanNode( - styles: KatexSpanStyles(), - text: '⟨', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), - text: '(', - nodes: null), - ]), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), - text: '[', - nodes: null), - ]), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), - text: '⌈', - nodes: null), - ]), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), - text: '⌊', - nodes: null), - ]), - ]), - ]), - ]); - - static const mathBlockKatexSpace = ContentExample( - 'math block; KaTeX space', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2214883 - '```math\n1:2\n```', - '

' - '' - '1:21:2' - '

', [ - MathBlockNode( - texSource: '1:2', - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode( - heightEm: 0.6444, - verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(), - text: '1', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, - nodes: []), - KatexSpanNode( - styles: KatexSpanStyles(), - text: ':', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, - nodes: []), - ]), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode( - heightEm: 0.6444, - verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(), - text: '2', - nodes: null), - ]), - ]), - ]); - - static const mathBlockKatexSuperscript = ContentExample( - 'math block, KaTeX superscript; single vlist-r, single vertical offset row', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176734 - '```math\na\'\n```', - '

' - '' - 'a' - 'a'' - '

', [ - MathBlockNode(texSource: 'a\'', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles( - fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -3.113 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '′', nodes: null), - ]), - ]), - ])), - ]), - ]), - ]), - ]), - ]), - ]); - - static const mathBlockKatexSubscript = ContentExample( - 'math block, KaTeX subscript; two vlist-r, single vertical offset row', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176735 - '```math\nx_n\n```', - '

' - '' - 'xn' - 'x_n' - '

', [ - MathBlockNode(texSource: 'x_n', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles( - fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'x', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.55 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginLeftEm: 0, marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'n', nodes: null), - ]), - ])), - ]), - ]), - ]), - ]), - ]), - ]); - - static const mathBlockKatexSubSuperScript = ContentExample( - 'math block, KaTeX subsup script; two vlist-r, multiple vertical offset rows', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176738 - '```math\n_u^o\n```', - '

' - '' - 'uo' - '_u^o' - '

', [ - MathBlockNode(texSource: "_u^o", nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.453 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'u', nodes: null), - ]), - ])), - KatexVlistRowNode( - verticalOffsetEm: -3.113 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'o', nodes: null), - ]), - ])), - ]), - ]), - ]), - ]), - ]), - ]); - - static const mathBlockKatexRaisebox = ContentExample( - 'math block, KaTeX raisebox; single vlist-r, single vertical offset row', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176739 - '```math\na\\raisebox{0.25em}{\$b\$}c\n```', - '

' - '' - 'abc' - 'a\\raisebox{0.25em}{\$b\$}c' - '

', [ - MathBlockNode(texSource: 'a\\raisebox{0.25em}{\$b\$}c', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', nodes: null), - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -3.25 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'b', nodes: null), - ]), - ])), - ]), - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'c', nodes: null), - ]), - ]), - ]); - - static const mathBlockKatexNegativeMargin = ContentExample( - 'math block, KaTeX negative margin', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563 - '```math\n1 \\! 2\n```', - '

' - '' - '1 ⁣21 \\! 2' - '

', [ - MathBlockNode(texSource: '1 \\! 2', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), text: '1', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '2', nodes: null), - ]), - ]), - ]), - ]); - - static const mathBlockKatexLogo = ContentExample( - 'math block, KaTeX logo', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2141902 - '```math\n\\KaTeX\n```', - '

' - '' - 'KaTeX' - '\\KaTeX' - '

', [ - MathBlockNode(texSource: '\\KaTeX', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'K', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.905 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 - text: 'A', nodes: null), - ]), - ])), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'T', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.7845 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'E', nodes: null), - ]), - ])), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'X', nodes: null), - ]), - ]), - ]), - ]), - ]), - ]), - ]), - ]), - ]); - - static const mathBlockKatexNegativeMarginsOnVlistRow = ContentExample( - 'math block, KaTeX negative margins on a vlist row', - // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2224918 - '```math\nX_n\n```', - '

' - '' - 'XnX_n' - '

', [ - MathBlockNode(texSource: 'X_n', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles( - marginRightEm: 0.07847, - fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'X', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.55 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'n', nodes: null), - ]), - ]), - ]), - ])), - ]), - ]), - ]), - ]), - ]), - ]); - static const imageSingle = ContentExample( 'single image', // https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103 @@ -2421,22 +1780,14 @@ void main() async { testParseExample(ContentExample.codeBlockWithUnknownSpanType); testParseExample(ContentExample.codeBlockFollowedByMultipleLineBreaks); + // The math examples in this file are about how math blocks and spans fit + // into the context of a Zulip message. + // For tests going deeper inside KaTeX content, see katex_test.dart. testParseExample(ContentExample.mathBlock); testParseExample(ContentExample.mathBlocksMultipleInParagraph); testParseExample(ContentExample.mathBlockInQuote); testParseExample(ContentExample.mathBlocksMultipleInQuote); testParseExample(ContentExample.mathBlockBetweenImages); - testParseExample(ContentExample.mathBlockKatexSizing); - testParseExample(ContentExample.mathBlockKatexNestedSizing); - testParseExample(ContentExample.mathBlockKatexDelimSizing); - testParseExample(ContentExample.mathBlockKatexSpace); - testParseExample(ContentExample.mathBlockKatexSuperscript); - testParseExample(ContentExample.mathBlockKatexSubscript); - testParseExample(ContentExample.mathBlockKatexSubSuperScript); - testParseExample(ContentExample.mathBlockKatexRaisebox); - testParseExample(ContentExample.mathBlockKatexNegativeMargin); - testParseExample(ContentExample.mathBlockKatexLogo); - testParseExample(ContentExample.mathBlockKatexNegativeMarginsOnVlistRow); testParseExample(ContentExample.imageSingle); testParseExample(ContentExample.imageSingleNoDimensions); diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart new file mode 100644 index 0000000000..be2ba56d11 --- /dev/null +++ b/test/model/katex_test.dart @@ -0,0 +1,698 @@ +import 'dart:io'; + +import 'package:zulip/model/settings.dart'; +import 'package:checks/checks.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/scaffolding.dart'; +import 'package:zulip/model/content.dart'; +import 'package:zulip/model/katex.dart'; + +import 'binding.dart'; +import 'content_test.dart'; + +/// Holds examples of KaTeX Zulip content for test cases. +/// +/// For guidance on writing examples, see comments on [ContentExample]. +abstract class KatexExample { + // 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 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```', + '

' + '' + '1234567890' + '\\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' + '

', + [ + 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: [ + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 + text: '1', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 + text: '2', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 + text: '3', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 + text: '4', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 + text: '5', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 + text: '6', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 + text: '7', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 + text: '8', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: '9', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 + text: '0', + nodes: null), + ]), + ]), + ]); + + static const mathBlockKatexNestedSizing = ContentExample( + 'math block; KaTeX nested sizing', + '```math\n\\tiny {1 \\Huge 2}\n```', + '

' + '' + '12' + '\\tiny {1 \\Huge 2}' + '

', + [ + MathBlockNode( + texSource: '\\tiny {1 \\Huge 2}', + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 + text: null, + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(), + text: '1', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 + text: '2', + nodes: null), + ]), + ]), + ]), + ]); + + 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```', + '

' + '' + '([' + '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊' + '

', + [ + MathBlockNode( + texSource: '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊', + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), + KatexSpanNode( + styles: KatexSpanStyles(), + text: '⟨', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), + text: '(', + nodes: null), + ]), + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), + text: '[', + nodes: null), + ]), + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), + text: '⌈', + nodes: null), + ]), + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), + text: '⌊', + nodes: null), + ]), + ]), + ]), + ]); + + static const mathBlockKatexSpace = ContentExample( + 'math block; KaTeX space', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2214883 + '```math\n1:2\n```', + '

' + '' + '1:21:2' + '

', [ + MathBlockNode( + texSource: '1:2', + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexStrutNode( + heightEm: 0.6444, + verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(), + text: '1', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.2778), + text: null, + nodes: []), + KatexSpanNode( + styles: KatexSpanStyles(), + text: ':', + nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.2778), + text: null, + nodes: []), + ]), + KatexSpanNode( + styles: KatexSpanStyles(), + text: null, + nodes: [ + KatexStrutNode( + heightEm: 0.6444, + verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(), + text: '2', + nodes: null), + ]), + ]), + ]); + + static const mathBlockKatexSuperscript = ContentExample( + 'math block, KaTeX superscript; single vlist-r, single vertical offset row', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176734 + '```math\na\'\n```', + '

' + '' + 'a' + 'a'' + '

', [ + MathBlockNode(texSource: 'a\'', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'a', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -3.113 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: '′', nodes: null), + ]), + ]), + ])), + ]), + ]), + ]), + ]), + ]), + ]); + + static const mathBlockKatexSubscript = ContentExample( + 'math block, KaTeX subscript; two vlist-r, single vertical offset row', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176735 + '```math\nx_n\n```', + '

' + '' + 'xn' + 'x_n' + '

', [ + MathBlockNode(texSource: 'x_n', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'x', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.55 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginLeftEm: 0, marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'n', nodes: null), + ]), + ])), + ]), + ]), + ]), + ]), + ]), + ]); + + static const mathBlockKatexSubSuperScript = ContentExample( + 'math block, KaTeX subsup script; two vlist-r, multiple vertical offset rows', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176738 + '```math\n_u^o\n```', + '

' + '' + 'uo' + '_u^o' + '

', [ + MathBlockNode(texSource: "_u^o", nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.453 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'u', nodes: null), + ]), + ])), + KatexVlistRowNode( + verticalOffsetEm: -3.113 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'o', nodes: null), + ]), + ])), + ]), + ]), + ]), + ]), + ]), + ]); + + static const mathBlockKatexRaisebox = ContentExample( + 'math block, KaTeX raisebox; single vlist-r, single vertical offset row', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176739 + '```math\na\\raisebox{0.25em}{\$b\$}c\n```', + '

' + '' + 'abc' + 'a\\raisebox{0.25em}{\$b\$}c' + '

', [ + MathBlockNode(texSource: 'a\\raisebox{0.25em}{\$b\$}c', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'a', nodes: null), + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -3.25 + 3, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'b', nodes: null), + ]), + ])), + ]), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'c', nodes: null), + ]), + ]), + ]); + + static const mathBlockKatexNegativeMargin = ContentExample( + 'math block, KaTeX negative margin', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563 + '```math\n1 \\! 2\n```', + '

' + '' + '1 ⁣21 \\! 2' + '

', [ + MathBlockNode(texSource: '1 \\! 2', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), text: '1', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: '2', nodes: null), + ]), + ]), + ]), + ]); + + static const mathBlockKatexLogo = ContentExample( + 'math block, KaTeX logo', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2141902 + '```math\n\\KaTeX\n```', + '

' + '' + 'KaTeX' + '\\KaTeX' + '

', [ + MathBlockNode(texSource: '\\KaTeX', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'K', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.905 + 2.7, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 + text: 'A', nodes: null), + ]), + ])), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'T', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.7845 + 3, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'E', nodes: null), + ]), + ])), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'X', nodes: null), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + static const mathBlockKatexNegativeMarginsOnVlistRow = ContentExample( + 'math block, KaTeX negative margins on a vlist row', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2224918 + '```math\nX_n\n```', + '

' + '' + 'XnX_n' + '

', [ + MathBlockNode(texSource: 'X_n', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + marginRightEm: 0.07847, + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'X', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.55 + 2.7, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'n', nodes: null), + ]), + ]), + ]), + ])), + ]), + ]), + ]), + ]), + ]), + ]); +} + +void main() async { + TestZulipBinding.ensureInitialized(); + + await testBinding.globalStore.settings.setBool( + BoolGlobalSetting.renderKatex, true); + + testParseExample(KatexExample.mathBlockKatexSizing); + testParseExample(KatexExample.mathBlockKatexNestedSizing); + testParseExample(KatexExample.mathBlockKatexDelimSizing); + testParseExample(KatexExample.mathBlockKatexSpace); + testParseExample(KatexExample.mathBlockKatexSuperscript); + testParseExample(KatexExample.mathBlockKatexSubscript); + testParseExample(KatexExample.mathBlockKatexSubSuperScript); + testParseExample(KatexExample.mathBlockKatexRaisebox); + testParseExample(KatexExample.mathBlockKatexNegativeMargin); + testParseExample(KatexExample.mathBlockKatexLogo); + testParseExample(KatexExample.mathBlockKatexNegativeMarginsOnVlistRow); + + test('all KaTeX content examples are tested', () { + // Check that every ContentExample defined above has a corresponding + // actual test case that runs on it. If you've added a new example + // and this test breaks, remember to add a `testParseExample` call for it. + + // This implementation is a bit of a hack; it'd be cleaner to get the + // actual Dart parse tree using package:analyzer. Unfortunately that + // approach takes several seconds just to load the parser library, enough + // to add noticeably to the runtime of our whole test suite. + final thisFilename = Trace.current().frames[0].uri.path; + final source = File(thisFilename).readAsStringSync(); + final declaredExamples = RegExp(multiLine: true, + r'^\s*static\s+(?:const|final)\s+(\w+)\s*=\s*ContentExample\s*(?:\.\s*inline\s*)?\(', + ).allMatches(source).map((m) => m.group(1)); + final testedExamples = RegExp(multiLine: true, + r'^\s*testParseExample\s*\(\s*KatexExample\s*\.\s*(\w+)(?:,\s*skip:\s*true)?\s*\);', + ).allMatches(source).map((m) => m.group(1)); + check(testedExamples).unorderedEquals(declaredExamples); + }, skip: Platform.isWindows, // [intended] purely analyzes source, so + // any one platform is enough; avoid dealing with Windows file paths + ); +} diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index 46ed10079e..86b6e4cc1c 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -23,6 +23,7 @@ import '../example_data.dart' as eg; import '../flutter_checks.dart'; import '../model/binding.dart'; import '../model/content_test.dart'; +import '../model/katex_test.dart'; import '../model/store_checks.dart'; import '../model/test_store.dart'; import '../stdlib_checks.dart'; @@ -579,7 +580,7 @@ void main() { group('characters render at specific offsets with specific size', () { const testCases = <(ContentExample, List<(String, Offset, Size)>, {bool? skip})>[ - (ContentExample.mathBlockKatexSizing, skip: false, [ + (KatexExample.mathBlockKatexSizing, skip: false, [ ('1', Offset(0.00, 2.24), Size(25.59, 61.00)), ('2', Offset(25.59, 10.04), Size(21.33, 51.00)), ('3', Offset(46.91, 16.55), Size(17.77, 43.00)), @@ -591,50 +592,50 @@ void main() { ('9', Offset(119.58, 35.91), Size(7.20, 17.00)), ('0', Offset(126.77, 39.68), Size(5.14, 12.00)), ]), - (ContentExample.mathBlockKatexNestedSizing, skip: false, [ + (KatexExample.mathBlockKatexNestedSizing, skip: false, [ ('1', Offset(0.00, 40.24), Size(5.14, 12.00)), ('2', Offset(5.14, 2.80), Size(25.59, 61.00)), ]), - (ContentExample.mathBlockKatexDelimSizing, skip: false, [ + (KatexExample.mathBlockKatexDelimSizing, skip: false, [ ('(', Offset(8.00, 20.14), Size(9.42, 25.00)), ('[', Offset(17.42, 20.14), Size(9.71, 25.00)), ('⌈', Offset(27.12, 20.14), Size(11.99, 25.00)), ('⌊', Offset(39.11, 20.14), Size(13.14, 25.00)), ]), - (ContentExample.mathBlockKatexSpace, skip: false, [ + (KatexExample.mathBlockKatexSpace, skip: false, [ ('1', Offset(0.00, 2.24), Size(10.28, 25.00)), (':', Offset(16.00, 2.24), Size(5.72, 25.00)), ('2', Offset(27.43, 2.24), Size(10.28, 25.00)), ]), - (ContentExample.mathBlockKatexSuperscript, skip: false, [ + (KatexExample.mathBlockKatexSuperscript, skip: false, [ ('a', Offset(0.00, 5.28), Size(10.88, 25.00)), ('′', Offset(10.88, 1.13), Size(3.96, 17.00)), ]), - (ContentExample.mathBlockKatexSubscript, skip: false, [ + (KatexExample.mathBlockKatexSubscript, skip: false, [ ('x', Offset(0.00, 5.28), Size(11.76, 25.00)), ('n', Offset(11.76, 13.65), Size(8.63, 17.00)), ]), - (ContentExample.mathBlockKatexSubSuperScript, skip: false, [ + (KatexExample.mathBlockKatexSubSuperScript, skip: false, [ ('u', Offset(0.00, 15.65), Size(8.23, 17.00)), ('o', Offset(0.00, 2.07), Size(6.98, 17.00)), ]), - (ContentExample.mathBlockKatexRaisebox, skip: false, [ + (KatexExample.mathBlockKatexRaisebox, skip: false, [ ('a', Offset(0.00, 4.16), Size(10.88, 25.00)), ('b', Offset(10.88, -0.66), Size(8.82, 25.00)), ('c', Offset(19.70, 4.16), Size(8.90, 25.00)), ]), - (ContentExample.mathBlockKatexNegativeMargin, skip: false, [ + (KatexExample.mathBlockKatexNegativeMargin, skip: false, [ ('1', Offset(0.00, 3.12), Size(10.28, 25.00)), ('2', Offset(6.85, 3.36), Size(10.28, 25.00)), ]), - (ContentExample.mathBlockKatexLogo, skip: false, [ + (KatexExample.mathBlockKatexLogo, skip: false, [ ('K', Offset(0.0, 8.64), Size(16.0, 25.0)), ('A', Offset(12.50, 10.85), Size(10.79, 17.0)), ('T', Offset(20.21, 9.36), Size(14.85, 25.0)), ('E', Offset(31.63, 14.52), Size(14.0, 25.0)), ('X', Offset(43.06, 9.85), Size(15.42, 25.0)), ]), - (ContentExample.mathBlockKatexNegativeMarginsOnVlistRow, skip: false, [ + (KatexExample.mathBlockKatexNegativeMarginsOnVlistRow, skip: false, [ ('X', Offset(0.00, 7.04), Size(17.03, 25.00)), ('n', Offset(17.03, 15.90), Size(8.63, 17.00)), ]), From 993bdc23b2e5945e9751f9c11150867f5bcc97f9 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 14:09:57 -0700 Subject: [PATCH 02/11] katex test [nfc]: Make the KaTeX examples instances of KatexExample --- test/model/katex_test.dart | 33 ++++++++++++++++++--------------- test/widgets/content_test.dart | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index be2ba56d11..11d43400b3 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -10,14 +10,17 @@ import 'package:zulip/model/katex.dart'; import 'binding.dart'; import 'content_test.dart'; -/// Holds examples of KaTeX Zulip content for test cases. +/// An example of KaTeX Zulip content for test cases. /// /// For guidance on writing examples, see comments on [ContentExample]. -abstract class KatexExample { +class KatexExample extends ContentExample { + const KatexExample(super.description, super.markdown, super.html, + super.expectedNodes, {super.expectedText}); + // 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 const mathBlockKatexSizing = ContentExample( + static const mathBlockKatexSizing = KatexExample( '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```', @@ -91,7 +94,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexNestedSizing = ContentExample( + static const mathBlockKatexNestedSizing = KatexExample( 'math block; KaTeX nested sizing', '```math\n\\tiny {1 \\Huge 2}\n```', '

' @@ -130,7 +133,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexDelimSizing = ContentExample( + static const mathBlockKatexDelimSizing = KatexExample( '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```', @@ -199,7 +202,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexSpace = ContentExample( + static const mathBlockKatexSpace = KatexExample( 'math block; KaTeX space', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2214883 '```math\n1:2\n```', @@ -258,7 +261,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexSuperscript = ContentExample( + static const mathBlockKatexSuperscript = KatexExample( 'math block, KaTeX superscript; single vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176734 '```math\na\'\n```', @@ -310,7 +313,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexSubscript = ContentExample( + static const mathBlockKatexSubscript = KatexExample( 'math block, KaTeX subscript; two vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176735 '```math\nx_n\n```', @@ -366,7 +369,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexSubSuperScript = ContentExample( + static const mathBlockKatexSubSuperScript = KatexExample( 'math block, KaTeX subsup script; two vlist-r, multiple vertical offset rows', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176738 '```math\n_u^o\n```', @@ -436,7 +439,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexRaisebox = ContentExample( + static const mathBlockKatexRaisebox = KatexExample( 'math block, KaTeX raisebox; single vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176739 '```math\na\\raisebox{0.25em}{\$b\$}c\n```', @@ -480,7 +483,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexNegativeMargin = ContentExample( + static const mathBlockKatexNegativeMargin = KatexExample( 'math block, KaTeX negative margin', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563 '```math\n1 \\! 2\n```', @@ -505,7 +508,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexLogo = ContentExample( + static const mathBlockKatexLogo = KatexExample( 'math block, KaTeX logo', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2141902 '```math\n\\KaTeX\n```', @@ -595,7 +598,7 @@ abstract class KatexExample { ]), ]); - static const mathBlockKatexNegativeMarginsOnVlistRow = ContentExample( + static const mathBlockKatexNegativeMarginsOnVlistRow = KatexExample( 'math block, KaTeX negative margins on a vlist row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2224918 '```math\nX_n\n```', @@ -675,7 +678,7 @@ void main() async { testParseExample(KatexExample.mathBlockKatexNegativeMarginsOnVlistRow); test('all KaTeX content examples are tested', () { - // Check that every ContentExample defined above has a corresponding + // Check that every KatexExample defined above has a corresponding // actual test case that runs on it. If you've added a new example // and this test breaks, remember to add a `testParseExample` call for it. @@ -686,7 +689,7 @@ void main() async { final thisFilename = Trace.current().frames[0].uri.path; final source = File(thisFilename).readAsStringSync(); final declaredExamples = RegExp(multiLine: true, - r'^\s*static\s+(?:const|final)\s+(\w+)\s*=\s*ContentExample\s*(?:\.\s*inline\s*)?\(', + r'^\s*static\s+(?:const|final)\s+(\w+)\s*=\s*KatexExample\s*(?:\.\s*inline\s*)?\(', ).allMatches(source).map((m) => m.group(1)); final testedExamples = RegExp(multiLine: true, r'^\s*testParseExample\s*\(\s*KatexExample\s*\.\s*(\w+)(?:,\s*skip:\s*true)?\s*\);', diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index 86b6e4cc1c..abdcb9d25f 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -579,7 +579,7 @@ void main() { }); group('characters render at specific offsets with specific size', () { - const testCases = <(ContentExample, List<(String, Offset, Size)>, {bool? skip})>[ + const testCases = <(KatexExample, List<(String, Offset, Size)>, {bool? skip})>[ (KatexExample.mathBlockKatexSizing, skip: false, [ ('1', Offset(0.00, 2.24), Size(25.59, 61.00)), ('2', Offset(25.59, 10.04), Size(21.33, 51.00)), From 6f66b06de42233ffe9584fa2b132b5cdc5ef6fa7 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 13:52:10 -0700 Subject: [PATCH 03/11] content test [nfc]: Move some helpers to top level, so importable This will let us split some of this file's tests to a different file and have them still use these helpers. --- test/widgets/content_test.dart | 68 +++++++++++++++++----------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index abdcb9d25f..cae7414083 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -108,6 +108,40 @@ TextStyle? mergedStyleOf(WidgetTester tester, Pattern spanPattern, { /// and reports the target's font size. typedef TargetFontSizeFinder = double Function(InlineSpan rootSpan); +Widget plainContent(String html) { + return Builder(builder: (context) => + DefaultTextStyle( + style: ContentTheme.of(context).textStylePlainParagraph, + child: BlockContentList(nodes: parseContent(html).nodes))); +} + +// TODO(#488) For content that we need to show outside a per-message context +// or a context without a full PerAccountStore, make sure to include tests +// that don't provide such context. +Future prepareContent(WidgetTester tester, Widget child, { + List navObservers = const [], + bool wrapWithPerAccountStoreWidget = false, +}) async { + if (wrapWithPerAccountStoreWidget) { + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + } + + addTearDown(testBinding.reset); + + prepareBoringImageHttpClient(); + + await tester.pumpWidget(TestZulipApp( + accountId: wrapWithPerAccountStoreWidget ? eg.selfAccount.id : null, + navigatorObservers: navObservers, + child: child)); + await tester.pump(); // global store + if (wrapWithPerAccountStoreWidget) { + await tester.pump(); + } + + debugNetworkImageHttpClientProvider = null; +} + void main() { // For testing a new content feature: // @@ -122,45 +156,11 @@ void main() { TestZulipBinding.ensureInitialized(); - Widget plainContent(String html) { - return Builder(builder: (context) => - DefaultTextStyle( - style: ContentTheme.of(context).textStylePlainParagraph, - child: BlockContentList(nodes: parseContent(html).nodes))); - } - Widget messageContent(String html) { return MessageContent(message: eg.streamMessage(content: html), content: parseContent(html)); } - // TODO(#488) For content that we need to show outside a per-message context - // or a context without a full PerAccountStore, make sure to include tests - // that don't provide such context. - Future prepareContent(WidgetTester tester, Widget child, { - List navObservers = const [], - bool wrapWithPerAccountStoreWidget = false, - }) async { - if (wrapWithPerAccountStoreWidget) { - await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - } - - addTearDown(testBinding.reset); - - prepareBoringImageHttpClient(); - - await tester.pumpWidget(TestZulipApp( - accountId: wrapWithPerAccountStoreWidget ? eg.selfAccount.id : null, - navigatorObservers: navObservers, - child: child)); - await tester.pump(); // global store - if (wrapWithPerAccountStoreWidget) { - await tester.pump(); - } - - debugNetworkImageHttpClientProvider = null; - } - /// Test that the given content example renders without throwing an exception. /// /// This requires [ContentExample.expectedText] to be non-null in order to From c5a10af328a2a72742448ccec9d6d665b09ea1e7 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 14:20:28 -0700 Subject: [PATCH 04/11] katex [nfc]: Move KaTeX widgets to their own file This is a pretty self-contained swath of our content widgets -- the only interface the others use from these is KatexWidget. So it works well to split into a separate file, paralleling the model files. --- lib/widgets/content.dart | 227 -------------------------------- lib/widgets/katex.dart | 229 +++++++++++++++++++++++++++++++++ test/widgets/content_test.dart | 147 ++------------------- test/widgets/katex_test.dart | 153 ++++++++++++++++++++++ 4 files changed, 391 insertions(+), 365 deletions(-) create mode 100644 test/widgets/katex_test.dart diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 2263b74f8b..551956966e 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -16,7 +15,6 @@ import '../model/binding.dart'; import '../model/content.dart'; import '../model/emoji.dart'; import '../model/internal_link.dart'; -import '../model/katex.dart'; import '../model/presence.dart'; import 'actions.dart'; import 'code_block.dart'; @@ -834,231 +832,6 @@ class MathBlock extends StatelessWidget { } } -/// Creates a base text style for rendering KaTeX content. -/// -/// This applies the CSS styles defined in .katex class in katex.scss : -/// https://github.com/KaTeX/KaTeX/blob/613c3da8/src/styles/katex.scss#L13-L15 -/// -/// Requires the [style.fontSize] to be non-null. -TextStyle mkBaseKatexTextStyle(TextStyle style) { - return style.copyWith( - fontSize: style.fontSize! * 1.21, - fontFamily: 'KaTeX_Main', - height: 1.2, - fontWeight: FontWeight.normal, - fontStyle: FontStyle.normal, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - decoration: TextDecoration.none, - fontFamilyFallback: const []); -} - -@visibleForTesting -class KatexWidget extends StatelessWidget { - const KatexWidget({ - super.key, - required this.textStyle, - required this.nodes, - }); - - final TextStyle textStyle; - final List nodes; - - @override - Widget build(BuildContext context) { - Widget widget = _KatexNodeList(nodes: nodes); - - return Directionality( - textDirection: TextDirection.ltr, - child: DefaultTextStyle( - style: mkBaseKatexTextStyle(textStyle).copyWith( - color: ContentTheme.of(context).textStylePlainParagraph.color), - child: widget)); - } -} - -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, - // Work around a bug where text inside a WidgetSpan could be scaled - // multiple times incorrectly, if the system font scale is larger - // than 1x. - // See: https://github.com/flutter/flutter/issues/126962 - child: MediaQuery( - data: MediaQueryData(textScaler: TextScaler.noScaling), - child: switch (e) { - KatexSpanNode() => _KatexSpan(e), - KatexStrutNode() => _KatexStrut(e), - KatexVlistNode() => _KatexVlist(e), - KatexNegativeMarginNode() => _KatexNegativeMargin(e), - })); - })))); - } -} - -class _KatexSpan extends StatelessWidget { - const _KatexSpan(this.node); - - final KatexSpanNode node; - - @override - Widget build(BuildContext context) { - var em = DefaultTextStyle.of(context).style.fontSize!; - - Widget widget = const SizedBox.shrink(); - if (node.text != null) { - widget = Text(node.text!); - } else if (node.nodes != null && node.nodes!.isNotEmpty) { - widget = _KatexNodeList(nodes: node.nodes!); - } - - final styles = node.styles; - - // Currently, we expect `top` to be only present with the - // vlist inner row span, and parser handles that explicitly. - assert(styles.topEm == null); - - final fontFamily = styles.fontFamily; - final fontSize = switch (styles.fontSizeEm) { - double fontSizeEm => fontSizeEm * em, - null => null, - }; - if (fontSize != null) em = fontSize; - - final fontWeight = switch (styles.fontWeight) { - KatexSpanFontWeight.bold => FontWeight.bold, - null => null, - }; - var fontStyle = switch (styles.fontStyle) { - KatexSpanFontStyle.normal => FontStyle.normal, - KatexSpanFontStyle.italic => FontStyle.italic, - null => null, - }; - - TextStyle? textStyle; - if (fontFamily != null || - fontSize != null || - fontWeight != null || - fontStyle != null) { - // TODO(upstream) remove this workaround when upstream fixes the broken - // rendering of KaTeX_Math font with italic font style on Android: - // https://github.com/flutter/flutter/issues/167474 - if (defaultTargetPlatform == TargetPlatform.android && - fontFamily == 'KaTeX_Math') { - fontStyle = FontStyle.normal; - } - - textStyle = TextStyle( - fontFamily: fontFamily, - fontSize: fontSize, - fontWeight: fontWeight, - fontStyle: fontStyle, - ); - } - final textAlign = switch (styles.textAlign) { - KatexSpanTextAlign.left => TextAlign.left, - KatexSpanTextAlign.center => TextAlign.center, - KatexSpanTextAlign.right => TextAlign.right, - null => null, - }; - - if (textStyle != null || textAlign != null) { - widget = DefaultTextStyle.merge( - style: textStyle, - textAlign: textAlign, - child: widget); - } - - widget = SizedBox( - height: styles.heightEm != null - ? styles.heightEm! * em - : null, - child: widget); - - final margin = switch ((styles.marginLeftEm, styles.marginRightEm)) { - (null, null) => null, - (null, final marginRightEm?) => - EdgeInsets.only(right: marginRightEm * em), - (final marginLeftEm?, null) => - EdgeInsets.only(left: marginLeftEm * em), - (final marginLeftEm?, final marginRightEm?) => - EdgeInsets.only(left: marginLeftEm * em, right: marginRightEm * em), - }; - - if (margin != null) { - assert(margin.isNonNegative); - widget = Padding(padding: margin, child: widget); - } - - return widget; - } -} - -class _KatexStrut extends StatelessWidget { - const _KatexStrut(this.node); - - final KatexStrutNode node; - - @override - Widget build(BuildContext context) { - final em = DefaultTextStyle.of(context).style.fontSize!; - - final verticalAlignEm = node.verticalAlignEm; - if (verticalAlignEm == null) { - return SizedBox(height: node.heightEm * em); - } - - return SizedBox( - height: node.heightEm * em, - child: Baseline( - baseline: (verticalAlignEm + node.heightEm) * em, - baselineType: TextBaseline.alphabetic, - child: const Text('')), - ); - } -} - -class _KatexVlist extends StatelessWidget { - const _KatexVlist(this.node); - - final KatexVlistNode node; - - @override - Widget build(BuildContext context) { - final em = DefaultTextStyle.of(context).style.fontSize!; - - return Stack(children: List.unmodifiable(node.rows.map((row) { - return Transform.translate( - offset: Offset(0, row.verticalOffsetEm * em), - child: _KatexSpan(row.node)); - }))); - } -} - -class _KatexNegativeMargin extends StatelessWidget { - const _KatexNegativeMargin(this.node); - - final KatexNegativeMarginNode node; - - @override - Widget build(BuildContext context) { - final em = DefaultTextStyle.of(context).style.fontSize!; - - return NegativeLeftOffset( - leftOffset: node.leftOffsetEm * em, - child: _KatexNodeList(nodes: node.nodes)); - } -} - class WebsitePreview extends StatelessWidget { const WebsitePreview({super.key, required this.node}); diff --git a/lib/widgets/katex.dart b/lib/widgets/katex.dart index 9b89270c8b..9d439ffdd3 100644 --- a/lib/widgets/katex.dart +++ b/lib/widgets/katex.dart @@ -4,6 +4,235 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/rendering.dart'; +import '../model/content.dart'; +import '../model/katex.dart'; +import 'content.dart'; + +/// Creates a base text style for rendering KaTeX content. +/// +/// This applies the CSS styles defined in .katex class in katex.scss : +/// https://github.com/KaTeX/KaTeX/blob/613c3da8/src/styles/katex.scss#L13-L15 +/// +/// Requires the [style.fontSize] to be non-null. +TextStyle mkBaseKatexTextStyle(TextStyle style) { + return style.copyWith( + fontSize: style.fontSize! * 1.21, + fontFamily: 'KaTeX_Main', + height: 1.2, + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + textBaseline: TextBaseline.alphabetic, + leadingDistribution: TextLeadingDistribution.even, + decoration: TextDecoration.none, + fontFamilyFallback: const []); +} + +@visibleForTesting +class KatexWidget extends StatelessWidget { + const KatexWidget({ + super.key, + required this.textStyle, + required this.nodes, + }); + + final TextStyle textStyle; + final List nodes; + + @override + Widget build(BuildContext context) { + Widget widget = _KatexNodeList(nodes: nodes); + + return Directionality( + textDirection: TextDirection.ltr, + child: DefaultTextStyle( + style: mkBaseKatexTextStyle(textStyle).copyWith( + color: ContentTheme.of(context).textStylePlainParagraph.color), + child: widget)); + } +} + +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, + // Work around a bug where text inside a WidgetSpan could be scaled + // multiple times incorrectly, if the system font scale is larger + // than 1x. + // See: https://github.com/flutter/flutter/issues/126962 + child: MediaQuery( + data: MediaQueryData(textScaler: TextScaler.noScaling), + child: switch (e) { + KatexSpanNode() => _KatexSpan(e), + KatexStrutNode() => _KatexStrut(e), + KatexVlistNode() => _KatexVlist(e), + KatexNegativeMarginNode() => _KatexNegativeMargin(e), + })); + })))); + } +} + +class _KatexSpan extends StatelessWidget { + const _KatexSpan(this.node); + + final KatexSpanNode node; + + @override + Widget build(BuildContext context) { + var em = DefaultTextStyle.of(context).style.fontSize!; + + Widget widget = const SizedBox.shrink(); + if (node.text != null) { + widget = Text(node.text!); + } else if (node.nodes != null && node.nodes!.isNotEmpty) { + widget = _KatexNodeList(nodes: node.nodes!); + } + + final styles = node.styles; + + // Currently, we expect `top` to be only present with the + // vlist inner row span, and parser handles that explicitly. + assert(styles.topEm == null); + + final fontFamily = styles.fontFamily; + final fontSize = switch (styles.fontSizeEm) { + double fontSizeEm => fontSizeEm * em, + null => null, + }; + if (fontSize != null) em = fontSize; + + final fontWeight = switch (styles.fontWeight) { + KatexSpanFontWeight.bold => FontWeight.bold, + null => null, + }; + var fontStyle = switch (styles.fontStyle) { + KatexSpanFontStyle.normal => FontStyle.normal, + KatexSpanFontStyle.italic => FontStyle.italic, + null => null, + }; + + TextStyle? textStyle; + if (fontFamily != null || + fontSize != null || + fontWeight != null || + fontStyle != null) { + // TODO(upstream) remove this workaround when upstream fixes the broken + // rendering of KaTeX_Math font with italic font style on Android: + // https://github.com/flutter/flutter/issues/167474 + if (defaultTargetPlatform == TargetPlatform.android && + fontFamily == 'KaTeX_Math') { + fontStyle = FontStyle.normal; + } + + textStyle = TextStyle( + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: fontWeight, + fontStyle: fontStyle, + ); + } + final textAlign = switch (styles.textAlign) { + KatexSpanTextAlign.left => TextAlign.left, + KatexSpanTextAlign.center => TextAlign.center, + KatexSpanTextAlign.right => TextAlign.right, + null => null, + }; + + if (textStyle != null || textAlign != null) { + widget = DefaultTextStyle.merge( + style: textStyle, + textAlign: textAlign, + child: widget); + } + + widget = SizedBox( + height: styles.heightEm != null + ? styles.heightEm! * em + : null, + child: widget); + + final margin = switch ((styles.marginLeftEm, styles.marginRightEm)) { + (null, null) => null, + (null, final marginRightEm?) => + EdgeInsets.only(right: marginRightEm * em), + (final marginLeftEm?, null) => + EdgeInsets.only(left: marginLeftEm * em), + (final marginLeftEm?, final marginRightEm?) => + EdgeInsets.only(left: marginLeftEm * em, right: marginRightEm * em), + }; + + if (margin != null) { + assert(margin.isNonNegative); + widget = Padding(padding: margin, child: widget); + } + + return widget; + } +} + +class _KatexStrut extends StatelessWidget { + const _KatexStrut(this.node); + + final KatexStrutNode node; + + @override + Widget build(BuildContext context) { + final em = DefaultTextStyle.of(context).style.fontSize!; + + final verticalAlignEm = node.verticalAlignEm; + if (verticalAlignEm == null) { + return SizedBox(height: node.heightEm * em); + } + + return SizedBox( + height: node.heightEm * em, + child: Baseline( + baseline: (verticalAlignEm + node.heightEm) * em, + baselineType: TextBaseline.alphabetic, + child: const Text('')), + ); + } +} + +class _KatexVlist extends StatelessWidget { + const _KatexVlist(this.node); + + final KatexVlistNode node; + + @override + Widget build(BuildContext context) { + final em = DefaultTextStyle.of(context).style.fontSize!; + + return Stack(children: List.unmodifiable(node.rows.map((row) { + return Transform.translate( + offset: Offset(0, row.verticalOffsetEm * em), + child: _KatexSpan(row.node)); + }))); + } +} + +class _KatexNegativeMargin extends StatelessWidget { + const _KatexNegativeMargin(this.node); + + final KatexNegativeMarginNode node; + + @override + Widget build(BuildContext context) { + final em = DefaultTextStyle.of(context).style.fontSize!; + + return NegativeLeftOffset( + leftOffset: node.leftOffsetEm * em, + child: _KatexNodeList(nodes: node.nodes)); + } +} + class NegativeLeftOffset extends SingleChildRenderObjectWidget { NegativeLeftOffset({super.key, required this.leftOffset, super.child}) : assert(leftOffset.isNegative), diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index cae7414083..e615dc7b81 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -3,7 +3,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_checks/flutter_checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -14,6 +13,7 @@ import 'package:zulip/model/settings.dart'; import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/content.dart'; import 'package:zulip/widgets/icons.dart'; +import 'package:zulip/widgets/katex.dart'; import 'package:zulip/widgets/message_list.dart'; import 'package:zulip/widgets/page.dart'; import 'package:zulip/widgets/store.dart'; @@ -23,7 +23,6 @@ import '../example_data.dart' as eg; import '../flutter_checks.dart'; import '../model/binding.dart'; import '../model/content_test.dart'; -import '../model/katex_test.dart'; import '../model/store_checks.dart'; import '../model/test_store.dart'; import '../stdlib_checks.dart'; @@ -557,6 +556,10 @@ void main() { }); group('MathBlock', () { + // See also katex_test.dart for detailed tests of + // how we render the inside of a math block. + // These tests check how it relates to the enclosing Zulip message. + testContentSmoke(ContentExample.mathBlock); testWidgets('displays KaTeX source; experimental flag disabled', (tester) async { @@ -577,100 +580,6 @@ void main() { await prepareContent(tester, plainContent(ContentExample.mathBlock.html)); tester.widget(find.text('λ', findRichText: true)); }); - - group('characters render at specific offsets with specific size', () { - const testCases = <(KatexExample, List<(String, Offset, Size)>, {bool? skip})>[ - (KatexExample.mathBlockKatexSizing, skip: false, [ - ('1', Offset(0.00, 2.24), Size(25.59, 61.00)), - ('2', Offset(25.59, 10.04), Size(21.33, 51.00)), - ('3', Offset(46.91, 16.55), Size(17.77, 43.00)), - ('4', Offset(64.68, 21.98), Size(14.80, 36.00)), - ('5', Offset(79.48, 26.50), Size(12.34, 30.00)), - ('6', Offset(91.82, 30.26), Size(10.28, 25.00)), - ('7', Offset(102.10, 32.15), Size(9.25, 22.00)), - ('8', Offset(111.35, 34.03), Size(8.23, 20.00)), - ('9', Offset(119.58, 35.91), Size(7.20, 17.00)), - ('0', Offset(126.77, 39.68), Size(5.14, 12.00)), - ]), - (KatexExample.mathBlockKatexNestedSizing, skip: false, [ - ('1', Offset(0.00, 40.24), Size(5.14, 12.00)), - ('2', Offset(5.14, 2.80), Size(25.59, 61.00)), - ]), - (KatexExample.mathBlockKatexDelimSizing, skip: false, [ - ('(', Offset(8.00, 20.14), Size(9.42, 25.00)), - ('[', Offset(17.42, 20.14), Size(9.71, 25.00)), - ('⌈', Offset(27.12, 20.14), Size(11.99, 25.00)), - ('⌊', Offset(39.11, 20.14), Size(13.14, 25.00)), - ]), - (KatexExample.mathBlockKatexSpace, skip: false, [ - ('1', Offset(0.00, 2.24), Size(10.28, 25.00)), - (':', Offset(16.00, 2.24), Size(5.72, 25.00)), - ('2', Offset(27.43, 2.24), Size(10.28, 25.00)), - ]), - (KatexExample.mathBlockKatexSuperscript, skip: false, [ - ('a', Offset(0.00, 5.28), Size(10.88, 25.00)), - ('′', Offset(10.88, 1.13), Size(3.96, 17.00)), - ]), - (KatexExample.mathBlockKatexSubscript, skip: false, [ - ('x', Offset(0.00, 5.28), Size(11.76, 25.00)), - ('n', Offset(11.76, 13.65), Size(8.63, 17.00)), - ]), - (KatexExample.mathBlockKatexSubSuperScript, skip: false, [ - ('u', Offset(0.00, 15.65), Size(8.23, 17.00)), - ('o', Offset(0.00, 2.07), Size(6.98, 17.00)), - ]), - (KatexExample.mathBlockKatexRaisebox, skip: false, [ - ('a', Offset(0.00, 4.16), Size(10.88, 25.00)), - ('b', Offset(10.88, -0.66), Size(8.82, 25.00)), - ('c', Offset(19.70, 4.16), Size(8.90, 25.00)), - ]), - (KatexExample.mathBlockKatexNegativeMargin, skip: false, [ - ('1', Offset(0.00, 3.12), Size(10.28, 25.00)), - ('2', Offset(6.85, 3.36), Size(10.28, 25.00)), - ]), - (KatexExample.mathBlockKatexLogo, skip: false, [ - ('K', Offset(0.0, 8.64), Size(16.0, 25.0)), - ('A', Offset(12.50, 10.85), Size(10.79, 17.0)), - ('T', Offset(20.21, 9.36), Size(14.85, 25.0)), - ('E', Offset(31.63, 14.52), Size(14.0, 25.0)), - ('X', Offset(43.06, 9.85), Size(15.42, 25.0)), - ]), - (KatexExample.mathBlockKatexNegativeMarginsOnVlistRow, skip: false, [ - ('X', Offset(0.00, 7.04), Size(17.03, 25.00)), - ('n', Offset(17.03, 15.90), Size(8.63, 17.00)), - ]), - ]; - - for (final testCase in testCases) { - testWidgets(testCase.$1.description, (tester) async { - await _loadKatexFonts(); - - addTearDown(testBinding.reset); - final globalSettings = testBinding.globalStore.settings; - await globalSettings.setBool(BoolGlobalSetting.renderKatex, true); - check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isTrue(); - - await prepareContent(tester, plainContent(testCase.$1.html)); - - final baseRect = tester.getRect(find.byType(KatexWidget)); - - for (final characterData in testCase.$2) { - final character = characterData.$1; - final expectedTopLeftOffset = characterData.$2; - final expectedSize = characterData.$3; - - final rect = tester.getRect(find.text(character)); - final topLeftOffset = rect.topLeft - baseRect.topLeft; - final size = rect.size; - - check(topLeftOffset) - .within(distance: 0.05, from: expectedTopLeftOffset); - check(size) - .within(distance: 0.05, from: expectedSize); - } - }, skip: testCase.skip); - } - }); }); /// Make a [TargetFontSizeFinder] to pass to [checkFontSizeRatio], @@ -1078,6 +987,10 @@ void main() { }); group('inline math', () { + // See also katex_test.dart for detailed tests of + // how we render the inside of a math span. + // These tests check how it relates to the enclosing Zulip message. + testContentSmoke(ContentExample.mathInline); testWidgets('maintains font-size ratio with surrounding text', (tester) async { @@ -1486,45 +1399,3 @@ void main() { }); }); } - -Future _loadKatexFonts() async { - const fonts = { - 'KaTeX_AMS': ['KaTeX_AMS-Regular.ttf'], - 'KaTeX_Caligraphic': [ - 'KaTeX_Caligraphic-Regular.ttf', - 'KaTeX_Caligraphic-Bold.ttf', - ], - 'KaTeX_Fraktur': [ - 'KaTeX_Fraktur-Regular.ttf', - 'KaTeX_Fraktur-Bold.ttf', - ], - 'KaTeX_Main': [ - 'KaTeX_Main-Regular.ttf', - 'KaTeX_Main-Bold.ttf', - 'KaTeX_Main-Italic.ttf', - 'KaTeX_Main-BoldItalic.ttf', - ], - 'KaTeX_Math': [ - 'KaTeX_Math-Italic.ttf', - 'KaTeX_Math-BoldItalic.ttf', - ], - 'KaTeX_SansSerif': [ - 'KaTeX_SansSerif-Regular.ttf', - 'KaTeX_SansSerif-Bold.ttf', - 'KaTeX_SansSerif-Italic.ttf', - ], - 'KaTeX_Script': ['KaTeX_Script-Regular.ttf'], - 'KaTeX_Size1': ['KaTeX_Size1-Regular.ttf'], - 'KaTeX_Size2': ['KaTeX_Size2-Regular.ttf'], - 'KaTeX_Size3': ['KaTeX_Size3-Regular.ttf'], - 'KaTeX_Size4': ['KaTeX_Size4-Regular.ttf'], - 'KaTeX_Typewriter': ['KaTeX_Typewriter-Regular.ttf'], - }; - for (final MapEntry(key: fontFamily, value: fontFiles) in fonts.entries) { - final fontLoader = FontLoader(fontFamily); - for (final fontFile in fontFiles) { - fontLoader.addFont(rootBundle.load('assets/KaTeX/$fontFile')); - } - await fontLoader.load(); - } -} diff --git a/test/widgets/katex_test.dart b/test/widgets/katex_test.dart new file mode 100644 index 0000000000..eb1c776a30 --- /dev/null +++ b/test/widgets/katex_test.dart @@ -0,0 +1,153 @@ +import 'package:checks/checks.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_checks/flutter_checks.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zulip/model/settings.dart'; +import 'package:zulip/widgets/katex.dart'; + +import '../model/binding.dart'; +import '../model/katex_test.dart'; +import '../model/store_checks.dart'; +import 'content_test.dart'; + +void main() { + TestZulipBinding.ensureInitialized(); + + group('MathBlock', () { + group('characters render at specific offsets with specific size', () { + const testCases = <(KatexExample, List<(String, Offset, Size)>, {bool? skip})>[ + (KatexExample.mathBlockKatexSizing, skip: false, [ + ('1', Offset(0.00, 2.24), Size(25.59, 61.00)), + ('2', Offset(25.59, 10.04), Size(21.33, 51.00)), + ('3', Offset(46.91, 16.55), Size(17.77, 43.00)), + ('4', Offset(64.68, 21.98), Size(14.80, 36.00)), + ('5', Offset(79.48, 26.50), Size(12.34, 30.00)), + ('6', Offset(91.82, 30.26), Size(10.28, 25.00)), + ('7', Offset(102.10, 32.15), Size(9.25, 22.00)), + ('8', Offset(111.35, 34.03), Size(8.23, 20.00)), + ('9', Offset(119.58, 35.91), Size(7.20, 17.00)), + ('0', Offset(126.77, 39.68), Size(5.14, 12.00)), + ]), + (KatexExample.mathBlockKatexNestedSizing, skip: false, [ + ('1', Offset(0.00, 40.24), Size(5.14, 12.00)), + ('2', Offset(5.14, 2.80), Size(25.59, 61.00)), + ]), + (KatexExample.mathBlockKatexDelimSizing, skip: false, [ + ('(', Offset(8.00, 20.14), Size(9.42, 25.00)), + ('[', Offset(17.42, 20.14), Size(9.71, 25.00)), + ('⌈', Offset(27.12, 20.14), Size(11.99, 25.00)), + ('⌊', Offset(39.11, 20.14), Size(13.14, 25.00)), + ]), + (KatexExample.mathBlockKatexSpace, skip: false, [ + ('1', Offset(0.00, 2.24), Size(10.28, 25.00)), + (':', Offset(16.00, 2.24), Size(5.72, 25.00)), + ('2', Offset(27.43, 2.24), Size(10.28, 25.00)), + ]), + (KatexExample.mathBlockKatexSuperscript, skip: false, [ + ('a', Offset(0.00, 5.28), Size(10.88, 25.00)), + ('′', Offset(10.88, 1.13), Size(3.96, 17.00)), + ]), + (KatexExample.mathBlockKatexSubscript, skip: false, [ + ('x', Offset(0.00, 5.28), Size(11.76, 25.00)), + ('n', Offset(11.76, 13.65), Size(8.63, 17.00)), + ]), + (KatexExample.mathBlockKatexSubSuperScript, skip: false, [ + ('u', Offset(0.00, 15.65), Size(8.23, 17.00)), + ('o', Offset(0.00, 2.07), Size(6.98, 17.00)), + ]), + (KatexExample.mathBlockKatexRaisebox, skip: false, [ + ('a', Offset(0.00, 4.16), Size(10.88, 25.00)), + ('b', Offset(10.88, -0.66), Size(8.82, 25.00)), + ('c', Offset(19.70, 4.16), Size(8.90, 25.00)), + ]), + (KatexExample.mathBlockKatexNegativeMargin, skip: false, [ + ('1', Offset(0.00, 3.12), Size(10.28, 25.00)), + ('2', Offset(6.85, 3.36), Size(10.28, 25.00)), + ]), + (KatexExample.mathBlockKatexLogo, skip: false, [ + ('K', Offset(0.0, 8.64), Size(16.0, 25.0)), + ('A', Offset(12.50, 10.85), Size(10.79, 17.0)), + ('T', Offset(20.21, 9.36), Size(14.85, 25.0)), + ('E', Offset(31.63, 14.52), Size(14.0, 25.0)), + ('X', Offset(43.06, 9.85), Size(15.42, 25.0)), + ]), + (KatexExample.mathBlockKatexNegativeMarginsOnVlistRow, skip: false, [ + ('X', Offset(0.00, 7.04), Size(17.03, 25.00)), + ('n', Offset(17.03, 15.90), Size(8.63, 17.00)), + ]), + ]; + + for (final testCase in testCases) { + testWidgets(testCase.$1.description, (tester) async { + await _loadKatexFonts(); + + addTearDown(testBinding.reset); + final globalSettings = testBinding.globalStore.settings; + await globalSettings.setBool(BoolGlobalSetting.renderKatex, true); + check(globalSettings).getBool(BoolGlobalSetting.renderKatex).isTrue(); + + await prepareContent(tester, plainContent(testCase.$1.html)); + + final baseRect = tester.getRect(find.byType(KatexWidget)); + + for (final characterData in testCase.$2) { + final character = characterData.$1; + final expectedTopLeftOffset = characterData.$2; + final expectedSize = characterData.$3; + + final rect = tester.getRect(find.text(character)); + final topLeftOffset = rect.topLeft - baseRect.topLeft; + final size = rect.size; + + check(topLeftOffset) + .within(distance: 0.05, from: expectedTopLeftOffset); + check(size) + .within(distance: 0.05, from: expectedSize); + } + }, skip: testCase.skip); + } + }); + }); +} + +Future _loadKatexFonts() async { + const fonts = { + 'KaTeX_AMS': ['KaTeX_AMS-Regular.ttf'], + 'KaTeX_Caligraphic': [ + 'KaTeX_Caligraphic-Regular.ttf', + 'KaTeX_Caligraphic-Bold.ttf', + ], + 'KaTeX_Fraktur': [ + 'KaTeX_Fraktur-Regular.ttf', + 'KaTeX_Fraktur-Bold.ttf', + ], + 'KaTeX_Main': [ + 'KaTeX_Main-Regular.ttf', + 'KaTeX_Main-Bold.ttf', + 'KaTeX_Main-Italic.ttf', + 'KaTeX_Main-BoldItalic.ttf', + ], + 'KaTeX_Math': [ + 'KaTeX_Math-Italic.ttf', + 'KaTeX_Math-BoldItalic.ttf', + ], + 'KaTeX_SansSerif': [ + 'KaTeX_SansSerif-Regular.ttf', + 'KaTeX_SansSerif-Bold.ttf', + 'KaTeX_SansSerif-Italic.ttf', + ], + 'KaTeX_Script': ['KaTeX_Script-Regular.ttf'], + 'KaTeX_Size1': ['KaTeX_Size1-Regular.ttf'], + 'KaTeX_Size2': ['KaTeX_Size2-Regular.ttf'], + 'KaTeX_Size3': ['KaTeX_Size3-Regular.ttf'], + 'KaTeX_Size4': ['KaTeX_Size4-Regular.ttf'], + 'KaTeX_Typewriter': ['KaTeX_Typewriter-Regular.ttf'], + }; + for (final MapEntry(key: fontFamily, value: fontFiles) in fonts.entries) { + final fontLoader = FontLoader(fontFamily); + for (final fontFile in fontFiles) { + fontLoader.addFont(rootBundle.load('assets/KaTeX/$fontFile')); + } + await fontLoader.load(); + } +} From 7fb722e808119a080e03da6979abecea860f25fd Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 14:51:51 -0700 Subject: [PATCH 05/11] katex test [nfc]: Make some examples more compact --- test/model/katex_test.dart | 316 +++++++++++++++---------------------- 1 file changed, 124 insertions(+), 192 deletions(-) diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 11d43400b3..6f4e175a72 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -40,57 +40,43 @@ class KatexExample extends ContentExample { '7' '8' '9' - '0

', - [ + '0

', [ 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: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 - text: '1', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 - text: '2', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 - text: '3', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 - text: '4', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 - text: '5', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 - text: '6', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 - text: '7', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 - text: '8', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: '9', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 - text: '0', - nodes: null), - ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 + text: '1', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 + text: '2', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 + text: '3', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 + text: '4', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 + text: '5', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 + text: '6', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 + text: '7', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 + text: '8', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: '9', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 + text: '0', nodes: null), + ]), ]), ]); @@ -106,31 +92,21 @@ class KatexExample extends ContentExample { '' '' '1' - '2

', - [ - MathBlockNode( - texSource: '\\tiny {1 \\Huge 2}', - nodes: [ + '2

', [ + MathBlockNode(texSource: '\\tiny {1 \\Huge 2}', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 + text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), + text: '1', nodes: null), KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: '1', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 - text: '2', - nodes: null), - ]), + styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 + text: '2', nodes: null), ]), ]), + ]), ]); static const mathBlockKatexDelimSizing = KatexExample( @@ -148,58 +124,34 @@ class KatexExample extends ContentExample { '(' '[' '' - '

', - [ - MathBlockNode( - texSource: '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊', - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), - KatexSpanNode( - styles: KatexSpanStyles(), - text: '⟨', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), - text: '(', - nodes: null), - ]), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), - text: '[', - nodes: null), - ]), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), - text: '⌈', - nodes: null), - ]), - KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), - text: '⌊', - nodes: null), - ]), - ]), + '

', [ + MathBlockNode(texSource: '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), + KatexSpanNode(styles: KatexSpanStyles(), + text: '⟨', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), + text: '(', nodes: null), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), + text: '[', nodes: null), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), + text: '⌈', nodes: null), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), + text: '⌊', nodes: null), + ]), ]), + ]), ]); static const mathBlockKatexSpace = KatexExample( @@ -219,46 +171,26 @@ class KatexExample extends ContentExample { '' '' '2

', [ - MathBlockNode( - texSource: '1:2', - nodes: [ + MathBlockNode(texSource: '1:2', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), + text: '1', nodes: null), KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode( - heightEm: 0.6444, - verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(), - text: '1', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, - nodes: []), - KatexSpanNode( - styles: KatexSpanStyles(), - text: ':', - nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, - nodes: []), - ]), + styles: KatexSpanStyles(marginRightEm: 0.2778), + text: null, nodes: []), + KatexSpanNode(styles: KatexSpanStyles(), + text: ':', nodes: null), KatexSpanNode( - styles: KatexSpanStyles(), - text: null, - nodes: [ - KatexStrutNode( - heightEm: 0.6444, - verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(), - text: '2', - nodes: null), - ]), + styles: KatexSpanStyles(marginRightEm: 0.2778), + text: null, nodes: []), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), + text: '2', nodes: null), ]), + ]), ]); static const mathBlockKatexSuperscript = KatexExample( @@ -553,46 +485,46 @@ class KatexExample extends ContentExample { text: 'K', nodes: null), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.905 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 - text: 'A', nodes: null), - ]), - ])), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'T', nodes: null), + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.905 + 2.7, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 + text: 'A', nodes: null), + ]), + ])), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'T', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.7845 + 3, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'E', nodes: null), + ]), + ])), + ]), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.7845 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'E', nodes: null), - ]), - ])), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'X', nodes: null), - ]), + KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'X', nodes: null), ]), ]), ]), ]), + ]), ]), ]), ]), From 15c68e0815589fbb60a7ef91c2750b784088db19 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 14:59:51 -0700 Subject: [PATCH 06/11] katex test [nfc]: Simplify a bit with a KatexExample.block constructor This removes some repetitive noisy bits from these examples, as well as a level of indentation. --- test/model/katex_test.dart | 586 +++++++++++++++++------------------ test/widgets/katex_test.dart | 2 +- 2 files changed, 283 insertions(+), 305 deletions(-) diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 6f4e175a72..581719fdba 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -14,16 +14,18 @@ import 'content_test.dart'; /// /// For guidance on writing examples, see comments on [ContentExample]. class KatexExample extends ContentExample { - const KatexExample(super.description, super.markdown, super.html, - super.expectedNodes, {super.expectedText}); + KatexExample.block(String description, String texSource, String html, + List? expectedNodes) + : super(description, '```math\n$texSource\n```', html, + [MathBlockNode(texSource: texSource, nodes: expectedNodes)]); // 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 const mathBlockKatexSizing = KatexExample( + static final mathBlockKatexSizing = KatexExample.block( '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```', + '\\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', '

' '' '1234567890' @@ -41,48 +43,44 @@ class KatexExample extends ContentExample { '8' '9' '0

', [ - 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: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 - text: '1', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 - text: '2', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 - text: '3', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 - text: '4', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 - text: '5', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 - text: '6', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 - text: '7', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 - text: '8', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: '9', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 - text: '0', nodes: null), - ]), - ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 + text: '1', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 + text: '2', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 + text: '3', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 + text: '4', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 + text: '5', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 + text: '6', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 + text: '7', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 + text: '8', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: '9', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 + text: '0', nodes: null), + ]), ]); - static const mathBlockKatexNestedSizing = KatexExample( + static final mathBlockKatexNestedSizing = KatexExample.block( 'math block; KaTeX nested sizing', - '```math\n\\tiny {1 \\Huge 2}\n```', + '\\tiny {1 \\Huge 2}', '

' '' '12' @@ -93,26 +91,24 @@ class KatexExample extends ContentExample { '' '1' '2

', [ - MathBlockNode(texSource: '\\tiny {1 \\Huge 2}', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 - text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), - text: '1', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 - text: '2', nodes: null), - ]), - ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 + text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), + text: '1', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 + text: '2', nodes: null), + ]), ]), ]); - static const mathBlockKatexDelimSizing = KatexExample( + static final mathBlockKatexDelimSizing = KatexExample.block( '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```', + '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊', '

' '' '([' @@ -125,39 +121,37 @@ class KatexExample extends ContentExample { '[' '' '

', [ - MathBlockNode(texSource: '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), + KatexSpanNode(styles: KatexSpanStyles(), + text: '⟨', nodes: null), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), - KatexSpanNode(styles: KatexSpanStyles(), - text: '⟨', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), - text: '(', nodes: null), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), - text: '[', nodes: null), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), - text: '⌈', nodes: null), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), - text: '⌊', nodes: null), - ]), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), + text: '(', nodes: null), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), + text: '[', nodes: null), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), + text: '⌈', nodes: null), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), + text: '⌊', nodes: null), ]), ]), ]); - static const mathBlockKatexSpace = KatexExample( + static final mathBlockKatexSpace = KatexExample.block( 'math block; KaTeX space', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2214883 - '```math\n1:2\n```', + '1:2', '

' '' '1:21:2' @@ -171,32 +165,30 @@ class KatexExample extends ContentExample { '' '' '2

', [ - MathBlockNode(texSource: '1:2', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), - text: '1', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, nodes: []), - KatexSpanNode(styles: KatexSpanStyles(), - text: ':', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, nodes: []), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), - text: '2', nodes: null), - ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), + text: '1', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.2778), + text: null, nodes: []), + KatexSpanNode(styles: KatexSpanStyles(), + text: ':', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.2778), + text: null, nodes: []), + ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), + text: '2', nodes: null), ]), ]); - static const mathBlockKatexSuperscript = KatexExample( + static final mathBlockKatexSuperscript = KatexExample.block( 'math block, KaTeX superscript; single vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176734 - '```math\na\'\n```', + 'a\'', '

' '' 'a' @@ -215,40 +207,38 @@ class KatexExample extends ContentExample { '' '' '

', [ - MathBlockNode(texSource: 'a\'', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles( - fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -3.113 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '′', nodes: null), - ]), + KatexSpanNode( + styles: KatexSpanStyles( + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'a', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -3.113 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: '′', nodes: null), ]), - ])), - ]), + ]), + ])), ]), - ]), + ]), ]), ]), ]); - static const mathBlockKatexSubscript = KatexExample( + static final mathBlockKatexSubscript = KatexExample.block( 'math block, KaTeX subscript; two vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176735 - '```math\nx_n\n```', + 'x_n', '

' '' 'xn' @@ -269,42 +259,40 @@ class KatexExample extends ContentExample { '' '' '

', [ - MathBlockNode(texSource: 'x_n', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles( - fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'x', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.55 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginLeftEm: 0, marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'n', nodes: null), - ]), - ])), - ]), + KatexSpanNode( + styles: KatexSpanStyles( + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'x', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.55 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginLeftEm: 0, marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'n', nodes: null), + ]), + ])), ]), - ]), + ]), ]), ]), ]); - static const mathBlockKatexSubSuperScript = KatexExample( + static final mathBlockKatexSubSuperScript = KatexExample.block( 'math block, KaTeX subsup script; two vlist-r, multiple vertical offset rows', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176738 - '```math\n_u^o\n```', + '_u^o', '

' '' 'uo' @@ -329,52 +317,50 @@ class KatexExample extends ContentExample { '' '' '

', [ - MathBlockNode(texSource: "_u^o", nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.453 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'u', nodes: null), - ]), - ])), - KatexVlistRowNode( - verticalOffsetEm: -3.113 + 2.7, - node: KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'o', nodes: null), - ]), - ])), - ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.453 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'u', nodes: null), + ]), + ])), + KatexVlistRowNode( + verticalOffsetEm: -3.113 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'o', nodes: null), + ]), + ])), ]), - ]), + ]), ]), ]), ]); - static const mathBlockKatexRaisebox = KatexExample( + static final mathBlockKatexRaisebox = KatexExample.block( 'math block, KaTeX raisebox; single vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176739 - '```math\na\\raisebox{0.25em}{\$b\$}c\n```', + 'a\\raisebox{0.25em}{\$b\$}c', '

' '' 'abc' @@ -391,34 +377,32 @@ class KatexExample extends ContentExample { '' 'b' 'c

', [ - MathBlockNode(texSource: 'a\\raisebox{0.25em}{\$b\$}c', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null), - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', nodes: null), - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -3.25 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'b', nodes: null), - ]), - ])), - ]), - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'c', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'a', nodes: null), + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -3.25 + 3, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'b', nodes: null), + ]), + ])), ]), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'c', nodes: null), ]), ]); - static const mathBlockKatexNegativeMargin = KatexExample( + static final mathBlockKatexNegativeMargin = KatexExample.block( 'math block, KaTeX negative margin', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563 - '```math\n1 \\! 2\n```', + '1 \\! 2', '

' '' '1 ⁣21 \\! 2' @@ -428,22 +412,20 @@ class KatexExample extends ContentExample { '1' '' '2

', [ - MathBlockNode(texSource: '1 \\! 2', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), text: '1', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '2', nodes: null), - ]), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(styles: KatexSpanStyles(), text: '1', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: '2', nodes: null), ]), ]), ]); - static const mathBlockKatexLogo = KatexExample( + static final mathBlockKatexLogo = KatexExample.block( 'math block, KaTeX logo', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2141902 - '```math\n\\KaTeX\n```', + '\\KaTeX', '

' '' 'KaTeX' @@ -476,51 +458,49 @@ class KatexExample extends ContentExample { '' '' 'X

', [ - MathBlockNode(texSource: '\\KaTeX', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'K', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'K', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.905 + 2.7, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 + text: 'A', nodes: null), + ]), + ])), + ]), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.905 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 - text: 'A', nodes: null), - ]), - ])), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'T', nodes: null), + KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'T', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.7845 + 3, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'E', nodes: null), + ]), + ])), + ]), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.7845 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'E', nodes: null), - ]), - ])), - ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), - KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'X', nodes: null), - ]), + KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'X', nodes: null), ]), ]), ]), @@ -530,10 +510,10 @@ class KatexExample extends ContentExample { ]), ]); - static const mathBlockKatexNegativeMarginsOnVlistRow = KatexExample( + static final mathBlockKatexNegativeMarginsOnVlistRow = KatexExample.block( 'math block, KaTeX negative margins on a vlist row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2224918 - '```math\nX_n\n```', + 'X_n', '

' '' 'XnX_n' @@ -553,39 +533,37 @@ class KatexExample extends ContentExample { '' '' '

', [ - MathBlockNode(texSource: 'X_n', nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles( - marginRightEm: 0.07847, - fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'X', nodes: null), - KatexSpanNode( - styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ - KatexVlistNode(rows: [ - KatexVlistRowNode( - verticalOffsetEm: -2.55 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ - KatexSpanNode( - styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'n', nodes: null), - ]), - ]), - ]), - ])), - ]), + KatexSpanNode( + styles: KatexSpanStyles( + marginRightEm: 0.07847, + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'X', nodes: null), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + text: null, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.55 + 2.7, + node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: null, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'n', nodes: null), + ]), + ]), + ]), + ])), ]), - ]), + ]), ]), ]), ]); @@ -621,7 +599,7 @@ void main() async { final thisFilename = Trace.current().frames[0].uri.path; final source = File(thisFilename).readAsStringSync(); final declaredExamples = RegExp(multiLine: true, - r'^\s*static\s+(?:const|final)\s+(\w+)\s*=\s*KatexExample\s*(?:\.\s*inline\s*)?\(', + r'^\s*static\s+(?:const|final)\s+(\w+)\s*=\s*KatexExample\s*(?:\.\s*(?:inline|block)\s*)?\(', ).allMatches(source).map((m) => m.group(1)); final testedExamples = RegExp(multiLine: true, r'^\s*testParseExample\s*\(\s*KatexExample\s*\.\s*(\w+)(?:,\s*skip:\s*true)?\s*\);', diff --git a/test/widgets/katex_test.dart b/test/widgets/katex_test.dart index eb1c776a30..325bcf0b6a 100644 --- a/test/widgets/katex_test.dart +++ b/test/widgets/katex_test.dart @@ -15,7 +15,7 @@ void main() { group('MathBlock', () { group('characters render at specific offsets with specific size', () { - const testCases = <(KatexExample, List<(String, Offset, Size)>, {bool? skip})>[ + final testCases = <(KatexExample, List<(String, Offset, Size)>, {bool? skip})>[ (KatexExample.mathBlockKatexSizing, skip: false, [ ('1', Offset(0.00, 2.24), Size(25.59, 61.00)), ('2', Offset(25.59, 10.04), Size(21.33, 51.00)), From 5e1211b1a445f52617e5db660a3c45e5159586ae Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 15:01:27 -0700 Subject: [PATCH 07/11] katex test [nfc]: Use Dart raw strings to avoid double-backslash This is available to us now that these strings don't include the leading "```math" and trailing "```" lines, and the newlines separating them from the main content. --- test/model/katex_test.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 581719fdba..7dc850d09a 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -80,7 +80,7 @@ class KatexExample extends ContentExample { static final mathBlockKatexNestedSizing = KatexExample.block( 'math block; KaTeX nested sizing', - '\\tiny {1 \\Huge 2}', + r'\tiny {1 \Huge 2}', '

' '' '12' @@ -108,7 +108,7 @@ class KatexExample extends ContentExample { static final mathBlockKatexDelimSizing = KatexExample.block( 'math block; KaTeX delimiter sizing', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2147135 - '⟨ \\big( \\Big[ \\bigg⌈ \\Bigg⌊', + r'⟨ \big( \Big[ \bigg⌈ \Bigg⌊', '

' '' '([' @@ -188,7 +188,7 @@ class KatexExample extends ContentExample { static final mathBlockKatexSuperscript = KatexExample.block( 'math block, KaTeX superscript; single vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176734 - 'a\'', + "a'", '

' '' 'a' @@ -360,7 +360,7 @@ class KatexExample extends ContentExample { static final mathBlockKatexRaisebox = KatexExample.block( 'math block, KaTeX raisebox; single vlist-r, single vertical offset row', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2176739 - 'a\\raisebox{0.25em}{\$b\$}c', + r'a\raisebox{0.25em}{$b$}c', '

' '' 'abc' @@ -402,7 +402,7 @@ class KatexExample extends ContentExample { static final mathBlockKatexNegativeMargin = KatexExample.block( 'math block, KaTeX negative margin', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563 - '1 \\! 2', + r'1 \! 2', '

' '' '1 ⁣21 \\! 2' @@ -425,7 +425,7 @@ class KatexExample extends ContentExample { static final mathBlockKatexLogo = KatexExample.block( 'math block, KaTeX logo', // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2141902 - '\\KaTeX', + r'\KaTeX', '

' '' 'KaTeX' From 4b679d44ff73550d6c8ead9a3d460f9e776f8ff7 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 21 Jul 2025 14:28:47 -0700 Subject: [PATCH 08/11] katex test [nfc]: Make a KatexExample.inline similar to .block --- test/model/katex_test.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 7dc850d09a..21ae51058b 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -14,6 +14,11 @@ import 'content_test.dart'; /// /// For guidance on writing examples, see comments on [ContentExample]. class KatexExample extends ContentExample { + KatexExample.inline(String description, String texSource, String html, + List? expectedNodes) + : super.inline(description, '\$\$ $texSource \$\$', html, + MathInlineNode(texSource: texSource, nodes: expectedNodes)); + KatexExample.block(String description, String texSource, String html, List? expectedNodes) : super(description, '```math\n$texSource\n```', html, From 5e303dcd5e6ca6ec90c755c1ac4e67eaf9e413e1 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 15:09:47 -0700 Subject: [PATCH 09/11] katex [nfc]: On KatexSpanNode constructor let text or nodes be omitted We already have an assert enforcing that exactly one of these is passed with a non-null (i.e. non-default) value. So there's no risk of forgetting to specify a detail that should be specified. --- lib/model/content.dart | 4 +- lib/model/katex.dart | 2 - test/model/content_test.dart | 39 +++----- test/model/katex_test.dart | 182 +++++++++++++++++------------------ 4 files changed, 109 insertions(+), 118 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index 28486f634d..94bab3e6ee 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -381,8 +381,8 @@ sealed class KatexNode extends ContentNode { class KatexSpanNode extends KatexNode { const KatexSpanNode({ required this.styles, - required this.text, - required this.nodes, + this.text, + this.nodes, super.debugHtmlNode, }) : assert((text != null) ^ (nodes != null)); diff --git a/lib/model/katex.dart b/lib/model/katex.dart index 6eab8473c7..7e278987f0 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -341,13 +341,11 @@ class _KatexParser { KatexSpanNode child = KatexSpanNode( styles: styles, - text: null, nodes: _parseChildSpans(otherSpans)); if (marginLeftIsNegative) { child = KatexSpanNode( styles: KatexSpanStyles(), - text: null, nodes: [KatexNegativeMarginNode( leftOffsetEm: marginLeftEm!, nodes: [child])]); diff --git a/test/model/content_test.dart b/test/model/content_test.dart index bead811079..24126357b1 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -518,14 +518,13 @@ class ContentExample { ' \\lambda ' '

', MathInlineNode(texSource: r'\lambda', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'λ', - nodes: null), + text: 'λ'), ]), ])); @@ -538,14 +537,13 @@ class ContentExample { '\\lambda' '

', [MathBlockNode(texSource: r'\lambda', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'λ', - nodes: null), + text: 'λ'), ]), ])]); @@ -563,25 +561,23 @@ class ContentExample { 'b' '

', [ MathBlockNode(texSource: 'a', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', - nodes: null), + text: 'a'), ]), ]), MathBlockNode(texSource: 'b', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'b', - nodes: null), + text: 'b'), ]), ]), ]); @@ -602,14 +598,13 @@ class ContentExample { '
\n

\n', [QuotationNode([ MathBlockNode(texSource: r'\lambda', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'λ', - nodes: null), + text: 'λ'), ]), ]), ])]); @@ -631,25 +626,23 @@ class ContentExample { '
\n

\n', [QuotationNode([ MathBlockNode(texSource: 'a', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', - nodes: null), + text: 'a'), ]), ]), MathBlockNode(texSource: 'b', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'b', - nodes: null), + text: 'b'), ]), ]), ])]); @@ -680,13 +673,13 @@ class ContentExample { originalHeight: null), ]), MathBlockNode(texSource: 'a', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', nodes: null), + text: 'a'), ]), ]), ImageNodeList([ diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index 21ae51058b..babfb270f4 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -48,38 +48,38 @@ class KatexExample extends ContentExample { '8' '9' '0

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 - text: '1', nodes: null), + text: '1'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 - text: '2', nodes: null), + text: '2'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 - text: '3', nodes: null), + text: '3'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 - text: '4', nodes: null), + text: '4'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 - text: '5', nodes: null), + text: '5'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 - text: '6', nodes: null), + text: '6'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 - text: '7', nodes: null), + text: '7'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 - text: '8', nodes: null), + text: '8'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: '9', nodes: null), + text: '9'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 - text: '0', nodes: null), + text: '0'), ]), ]); @@ -96,16 +96,16 @@ class KatexExample extends ContentExample { '' '1' '2

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 - text: null, nodes: [ + nodes: [ KatexSpanNode(styles: KatexSpanStyles(), - text: '1', nodes: null), + text: '1'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 - text: '2', nodes: null), + text: '2'), ]), ]), ]); @@ -126,29 +126,29 @@ class KatexExample extends ContentExample { '[' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), KatexSpanNode(styles: KatexSpanStyles(), - text: '⟨', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + text: '⟨'), + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), - text: '(', nodes: null), + text: '('), ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), - text: '[', nodes: null), + text: '['), ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), - text: '⌈', nodes: null), + text: '⌈'), ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), - text: '⌊', nodes: null), + text: '⌊'), ]), ]), ]); @@ -170,23 +170,23 @@ class KatexExample extends ContentExample { '' '' '2

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), KatexSpanNode(styles: KatexSpanStyles(), - text: '1', nodes: null), + text: '1'), KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, nodes: []), + nodes: []), KatexSpanNode(styles: KatexSpanStyles(), - text: ':', nodes: null), + text: ':'), KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.2778), - text: null, nodes: []), + nodes: []), ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), KatexSpanNode(styles: KatexSpanStyles(), - text: '2', nodes: null), + text: '2'), ]), ]); @@ -212,25 +212,25 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', nodes: null), + text: 'a'), KatexSpanNode( styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ + nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -3.113 + 2.7, node: KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '′', nodes: null), + nodes: [ + KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), text: '′'), ]), ]), ])), @@ -264,28 +264,28 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'x', nodes: null), + text: 'x'), KatexSpanNode( styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ + nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.55 + 2.7, node: KatexSpanNode( styles: KatexSpanStyles(marginLeftEm: 0, marginRightEm: 0.05), - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'n', nodes: null), + text: 'n'), ]), ])), ]), @@ -322,38 +322,38 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: []), KatexSpanNode( styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ + nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.453 + 2.7, node: KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'u', nodes: null), + text: 'u'), ]), ])), KatexVlistRowNode( verticalOffsetEm: -3.113 + 2.7, node: KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'o', nodes: null), + text: 'o'), ]), ])), ]), @@ -382,25 +382,25 @@ class KatexExample extends ContentExample { '' 'b' 'c

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'a', nodes: null), + text: 'a'), KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -3.25 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'b', nodes: null), + text: 'b'), ]), ])), ]), KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'c', nodes: null), + text: 'c'), ]), ]); @@ -417,12 +417,12 @@ class KatexExample extends ContentExample { '1' '' '2

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), text: '1', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexSpanNode(styles: KatexSpanStyles(), text: '1'), + KatexSpanNode(styles: KatexSpanStyles(), nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '2', nodes: null), + KatexSpanNode(styles: KatexSpanStyles(), text: '2'), ]), ]), ]); @@ -463,49 +463,49 @@ class KatexExample extends ContentExample { '' '' 'X

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'K', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + text: 'K'), + KatexSpanNode(styles: KatexSpanStyles(), nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.905 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 - text: 'A', nodes: null), + text: 'A'), ]), ])), ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexSpanNode(styles: KatexSpanStyles(), nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'T', nodes: null), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + text: 'T'), + KatexSpanNode(styles: KatexSpanStyles(), nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.7845 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'E', nodes: null), + text: 'E'), ]), ])), ]), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: []), + KatexSpanNode(styles: KatexSpanStyles(), nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), - text: 'X', nodes: null), + text: 'X'), ]), ]), ]), @@ -538,31 +538,31 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexSpanNode( styles: KatexSpanStyles( marginRightEm: 0.07847, fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'X', nodes: null), + text: 'X'), KatexSpanNode( styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), - text: null, nodes: [ + nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.55 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), text: null, nodes: [ + node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.05), - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 - text: null, nodes: [ + nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), - text: 'n', nodes: null), + text: 'n'), ]), ]), ]), From 8a39d0453fdb07424f39185a9205f62eb083050e Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 15:14:49 -0700 Subject: [PATCH 10/11] katex [nfc]: Let KatexSpanNode.styles default to empty We have a lot of calls to the KatexSpanNode constructor in our tests, and a large fraction of them pass an empty `styles`. Let that be the default, reducing some noise. It's a natural default, since it effectively means "do nothing". --- lib/model/content.dart | 2 +- lib/model/katex.dart | 1 - test/model/content_test.dart | 16 +++---- test/model/katex_test.dart | 93 +++++++++++++++++------------------- 4 files changed, 53 insertions(+), 59 deletions(-) diff --git a/lib/model/content.dart b/lib/model/content.dart index 94bab3e6ee..c0a6d3bc9d 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -380,7 +380,7 @@ sealed class KatexNode extends ContentNode { class KatexSpanNode extends KatexNode { const KatexSpanNode({ - required this.styles, + this.styles = const KatexSpanStyles(), this.text, this.nodes, super.debugHtmlNode, diff --git a/lib/model/katex.dart b/lib/model/katex.dart index 7e278987f0..d7d09d5ea2 100644 --- a/lib/model/katex.dart +++ b/lib/model/katex.dart @@ -345,7 +345,6 @@ class _KatexParser { if (marginLeftIsNegative) { child = KatexSpanNode( - styles: KatexSpanStyles(), nodes: [KatexNegativeMarginNode( leftOffsetEm: marginLeftEm!, nodes: [child])]); diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 24126357b1..5eaf5500fa 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -518,7 +518,7 @@ class ContentExample { ' \\lambda ' '

', MathInlineNode(texSource: r'\lambda', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( @@ -537,7 +537,7 @@ class ContentExample { '\\lambda' '

', [MathBlockNode(texSource: r'\lambda', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( @@ -561,7 +561,7 @@ class ContentExample { 'b' '

', [ MathBlockNode(texSource: 'a', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( @@ -571,7 +571,7 @@ class ContentExample { ]), ]), MathBlockNode(texSource: 'b', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( @@ -598,7 +598,7 @@ class ContentExample { '
\n

\n', [QuotationNode([ MathBlockNode(texSource: r'\lambda', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( @@ -626,7 +626,7 @@ class ContentExample { '
\n

\n', [QuotationNode([ MathBlockNode(texSource: 'a', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( @@ -636,7 +636,7 @@ class ContentExample { ]), ]), MathBlockNode(texSource: 'b', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6944, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( @@ -673,7 +673,7 @@ class ContentExample { originalHeight: null), ]), MathBlockNode(texSource: 'a', nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.4306, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles( diff --git a/test/model/katex_test.dart b/test/model/katex_test.dart index babfb270f4..3b0916d3c0 100644 --- a/test/model/katex_test.dart +++ b/test/model/katex_test.dart @@ -48,7 +48,7 @@ class KatexExample extends ContentExample { '8' '9' '0

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 @@ -96,13 +96,12 @@ class KatexExample extends ContentExample { '' '1' '2

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), - text: '1'), + KatexSpanNode(text: '1'), KatexSpanNode( styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 text: '2'), @@ -126,26 +125,25 @@ class KatexExample extends ContentExample { '[' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), - KatexSpanNode(styles: KatexSpanStyles(), - text: '⟨'), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(text: '⟨'), + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), text: '('), ]), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), text: '['), ]), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), text: '⌈'), ]), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), text: '⌊'), @@ -170,23 +168,20 @@ class KatexExample extends ContentExample { '' '' '2

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), - text: '1'), + KatexSpanNode(text: '1'), KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.2778), nodes: []), - KatexSpanNode(styles: KatexSpanStyles(), - text: ':'), + KatexSpanNode(text: ':'), KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.2778), nodes: []), ]), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), - text: '2'), + KatexSpanNode(text: '2'), ]), ]); @@ -212,9 +207,9 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), @@ -229,8 +224,8 @@ class KatexExample extends ContentExample { styles: KatexSpanStyles(marginRightEm: 0.05), nodes: [ KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '′'), + KatexSpanNode(nodes: [ + KatexSpanNode(text: '′'), ]), ]), ])), @@ -264,9 +259,9 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles( fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), @@ -322,10 +317,10 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: []), + KatexSpanNode(nodes: [ + KatexSpanNode(nodes: []), KatexSpanNode( styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), nodes: [ @@ -382,7 +377,7 @@ class KatexExample extends ContentExample { '' 'b' 'c

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null), KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), @@ -390,8 +385,8 @@ class KatexExample extends ContentExample { KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -3.25 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + node: KatexSpanNode(nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), text: 'b'), @@ -417,12 +412,12 @@ class KatexExample extends ContentExample { '1' '' '2

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), - KatexSpanNode(styles: KatexSpanStyles(), text: '1'), - KatexSpanNode(styles: KatexSpanStyles(), nodes: []), + KatexSpanNode(text: '1'), + KatexSpanNode(nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), text: '2'), + KatexSpanNode(text: '2'), ]), ]), ]); @@ -463,45 +458,45 @@ class KatexExample extends ContentExample { '' '' 'X

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), text: 'K'), - KatexSpanNode(styles: KatexSpanStyles(), nodes: []), + KatexSpanNode(nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.905 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + node: KatexSpanNode(nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 text: 'A'), ]), ])), ]), - KatexSpanNode(styles: KatexSpanStyles(), nodes: []), + KatexSpanNode(nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), text: 'T'), - KatexSpanNode(styles: KatexSpanStyles(), nodes: []), + KatexSpanNode(nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.7845 + 3, - node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + node: KatexSpanNode(nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), text: 'E'), ]), ])), ]), - KatexSpanNode(styles: KatexSpanStyles(), nodes: []), + KatexSpanNode(nodes: []), KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ KatexSpanNode( styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), @@ -538,9 +533,9 @@ class KatexExample extends ContentExample { '' '' '

', [ - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), - KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + KatexSpanNode(nodes: [ KatexSpanNode( styles: KatexSpanStyles( marginRightEm: 0.07847, @@ -552,7 +547,7 @@ class KatexExample extends ContentExample { KatexVlistNode(rows: [ KatexVlistRowNode( verticalOffsetEm: -2.55 + 2.7, - node: KatexSpanNode(styles: KatexSpanStyles(), nodes: [ + node: KatexSpanNode(nodes: [ KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ KatexSpanNode( styles: KatexSpanStyles(marginRightEm: 0.05), From 5f85799648832593a1974540a1214dcfde23962c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Sat, 19 Jul 2025 16:34:32 -0700 Subject: [PATCH 11/11] katex [nfc]: Add docs explaining the different content-node classes --- lib/model/content.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/model/content.dart b/lib/model/content.dart index c0a6d3bc9d..78d7af24bc 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -341,6 +341,11 @@ class CodeBlockSpanNode extends ContentNode { } } +/// A complete KaTeX math expression within Zulip content, +/// whether block or inline. +/// +/// The content nodes that are descendants of this node +/// will all be of KaTeX-specific types, such as [KatexNode]. sealed class MathNode extends ContentNode { const MathNode({ super.debugHtmlNode, @@ -374,10 +379,15 @@ sealed class MathNode extends ContentNode { } } +/// A content node that expects a generic KaTeX context from its parent. +/// +/// Each of these will have a [MathNode] as an ancestor. sealed class KatexNode extends ContentNode { const KatexNode({super.debugHtmlNode}); } +/// A generic KaTeX content node, corresponding to any span in KaTeX HTML +/// that we don't otherwise specially handle. class KatexSpanNode extends KatexNode { const KatexSpanNode({ this.styles = const KatexSpanStyles(), @@ -411,6 +421,7 @@ class KatexSpanNode extends KatexNode { } } +/// A KaTeX strut, corresponding to a `span.strut` node in KaTeX HTML. class KatexStrutNode extends KatexNode { const KatexStrutNode({ required this.heightEm, @@ -429,6 +440,12 @@ class KatexStrutNode extends KatexNode { } } +/// A KaTeX "vertical list", corresponding to a `span.vlist-t` in KaTeX HTML. +/// +/// These nodes in KaTeX HTML have a very specific structure. +/// The children of these nodes in our tree correspond in the HTML to +/// certain great-grandchildren (certain `> .vlist-r > .vlist > span`) +/// of the `.vlist-t` node. class KatexVlistNode extends KatexNode { const KatexVlistNode({ required this.rows, @@ -443,6 +460,11 @@ class KatexVlistNode extends KatexNode { } } +/// An element of a KaTeX "vertical list"; a child of a [KatexVlistNode]. +/// +/// These correspond to certain `.vlist-t > .vlist-r > .vlist > span` nodes +/// in KaTeX HTML. The [KatexVlistNode] parent in our tree +/// corresponds to the `.vlist-t` great-grandparent in the HTML. class KatexVlistRowNode extends ContentNode { const KatexVlistRowNode({ required this.verticalOffsetEm, @@ -465,6 +487,11 @@ class KatexVlistRowNode extends ContentNode { } } +/// A KaTeX node corresponding to negative values for `margin-left` +/// or `margin-right` in the inline CSS style of a KaTeX HTML node. +/// +/// The parser synthesizes these as additional nodes, not corresponding +/// directly to any node in the HTML. class KatexNegativeMarginNode extends KatexNode { const KatexNegativeMarginNode({ required this.leftOffsetEm,