diff --git a/lib/model/content.dart b/lib/model/content.dart
index 28486f634d..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,15 +379,20 @@ 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({
- required this.styles,
- required this.text,
- required this.nodes,
+ this.styles = const KatexSpanStyles(),
+ this.text,
+ this.nodes,
super.debugHtmlNode,
}) : assert((text != null) ^ (nodes != null));
@@ -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,
diff --git a/lib/model/katex.dart b/lib/model/katex.dart
index 6eab8473c7..d7d09d5ea2 100644
--- a/lib/model/katex.dart
+++ b/lib/model/katex.dart
@@ -341,13 +341,10 @@ 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/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
' - '' - '' - '
', - [ - 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```', - '' - '' - '' - '
', - [ - 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```', - '' - '' - '' - '
', - [ - 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```', - '' - '' - '' - '
', [ - 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```', - '' - '' - '' - '
', [ - 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```', - '' - '' - '' - '
', [ - 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```', - '' - '' - '' - '
', [ - 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```', - '' - '' - '' - '
', [ - 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```', - '' - '' - '' - '
', [ - 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```', - '' - '' - '' - '
', [ - 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```', - '' - '' - '' - '
', [ - 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 +1773,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..3b0916d3c0 --- /dev/null +++ b/test/model/katex_test.dart @@ -0,0 +1,611 @@ +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'; + +/// An example of KaTeX Zulip content for test cases. +/// +/// For guidance on writing examples, see comments on [ContentExample]. +class KatexExample extends ContentExample { + KatexExample.inline(String description, String texSource, String html, + List' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.488), // .reset-size6.size11 + text: '1'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 2.074), // .reset-size6.size10 + text: '2'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.728), // .reset-size6.size9 + text: '3'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.44), // .reset-size6.size8 + text: '4'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.2), // .reset-size6.size7 + text: '5'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 1.0), // .reset-size6.size6 + text: '6'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.9), // .reset-size6.size5 + text: '7'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.8), // .reset-size6.size4 + text: '8'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + text: '9'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.5), // .reset-size6.size1 + text: '0'), + ]), + ]); + + static final mathBlockKatexNestedSizing = KatexExample.block( + 'math block; KaTeX nested sizing', + r'\tiny {1 \Huge 2}', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 1.6034, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.5), // reset-size6 size1 + nodes: [ + KatexSpanNode(text: '1'), + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 4.976), // reset-size1 size11 + text: '2'), + ]), + ]), + ]); + + static final mathBlockKatexDelimSizing = KatexExample.block( + 'math block; KaTeX delimiter sizing', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2147135 + r'⟨ \big( \Big[ \bigg⌈ \Bigg⌊', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 3, verticalAlignEm: -1.25), + KatexSpanNode(text: '⟨'), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size1'), + text: '('), + ]), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size2'), + text: '['), + ]), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size3'), + text: '⌈'), + ]), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Size4'), + text: '⌊'), + ]), + ]), + ]); + + static final mathBlockKatexSpace = KatexExample.block( + 'math block; KaTeX space', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2214883 + '1:2', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(text: '1'), + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.2778), + nodes: []), + KatexSpanNode(text: ':'), + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.2778), + nodes: []), + ]), + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(text: '2'), + ]), + ]); + + 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'", + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.8019, verticalAlignEm: null), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'a'), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -3.113 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + nodes: [ + KatexSpanNode(styles: KatexSpanStyles(fontSizeEm: 0.7), nodes: [ + KatexSpanNode(nodes: [ + KatexSpanNode(text: '′'), + ]), + ]), + ])), + ]), + ]), + ]), + ]), + ]); + + 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 + 'x_n', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.5806, verticalAlignEm: -0.15), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'x'), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.55 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginLeftEm: 0, marginRightEm: 0.05), + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'n'), + ]), + ])), + ]), + ]), + ]), + ]), + ]); + + 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 + '_u^o', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.9614, verticalAlignEm: -0.247), + KatexSpanNode(nodes: [ + KatexSpanNode(nodes: []), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.453 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'u'), + ]), + ])), + KatexVlistRowNode( + verticalOffsetEm: -3.113 + 2.7, + node: KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'o'), + ]), + ])), + ]), + ]), + ]), + ]), + ]); + + 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 + r'a\raisebox{0.25em}{$b$}c', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.9444, verticalAlignEm: null), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'a'), + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -3.25 + 3, + node: KatexSpanNode(nodes: [ + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'b'), + ]), + ])), + ]), + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'c'), + ]), + ]); + + static final mathBlockKatexNegativeMargin = KatexExample.block( + 'math block, KaTeX negative margin', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2223563 + r'1 \! 2', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.6444, verticalAlignEm: null), + KatexSpanNode(text: '1'), + KatexSpanNode(nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexSpanNode(text: '2'), + ]), + ]), + ]); + + static final mathBlockKatexLogo = KatexExample.block( + 'math block, KaTeX logo', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/2141902 + r'\KaTeX', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.8988, verticalAlignEm: -0.2155), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'K'), + KatexSpanNode(nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.17, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.905 + 2.7, + node: KatexSpanNode(nodes: [ + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main', fontSizeEm: 0.7), // .reset-size6.size3 + text: 'A'), + ]), + ])), + ]), + KatexSpanNode(nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.15, nodes: [ + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'T'), + KatexSpanNode(nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.1667, nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.7845 + 3, + node: KatexSpanNode(nodes: [ + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'E'), + ]), + ])), + ]), + KatexSpanNode(nodes: []), + KatexNegativeMarginNode(leftOffsetEm: -0.125, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Main'), + text: 'X'), + ]), + ]), + ]), + ]), + ]), + ]), + ]), + ]); + + 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 + 'X_n', + '' + '' + '' + '
', [ + KatexSpanNode(nodes: [ + KatexStrutNode(heightEm: 0.8333, verticalAlignEm: -0.15), + KatexSpanNode(nodes: [ + KatexSpanNode( + styles: KatexSpanStyles( + marginRightEm: 0.07847, + fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'X'), + KatexSpanNode( + styles: KatexSpanStyles(textAlign: KatexSpanTextAlign.left), + nodes: [ + KatexVlistNode(rows: [ + KatexVlistRowNode( + verticalOffsetEm: -2.55 + 2.7, + node: KatexSpanNode(nodes: [ + KatexNegativeMarginNode(leftOffsetEm: -0.0785, nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(marginRightEm: 0.05), + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontSizeEm: 0.7), // .reset-size6.size3 + nodes: [ + KatexSpanNode( + styles: KatexSpanStyles(fontFamily: 'KaTeX_Math', fontStyle: KatexSpanFontStyle.italic), + text: 'n'), + ]), + ]), + ]), + ])), + ]), + ]), + ]), + ]), + ]); +} + +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 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. + + // 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*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*\);', + ).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..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'; @@ -107,6 +107,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