@@ -786,7 +786,7 @@ class MathInlineNode extends InlineContentNode {
786
786
class GlobalTimeNode extends InlineContentNode {
787
787
const GlobalTimeNode ({super .debugHtmlNode, required this .datetime});
788
788
789
- /// Always in UTC, enforced in [_ZulipContentParser .parseInlineContent] .
789
+ /// Always in UTC, enforced in [_ZulipInlineContentParser .parseInlineContent] .
790
790
final DateTime datetime;
791
791
792
792
@override
@@ -806,72 +806,68 @@ class GlobalTimeNode extends InlineContentNode {
806
806
807
807
////////////////////////////////////////////////////////////////
808
808
809
- /// What sort of nodes a [_ZulipContentParser] is currently expecting to find.
810
- enum _ParserContext {
811
- /// The parser is currently looking for block nodes.
812
- block,
809
+ String ? _parseMath (dom. Element element, { required bool block}) {
810
+ final dom. Element katexElement;
811
+ if ( ! block) {
812
+ assert (element.localName == 'span' && element.className == 'katex' );
813
813
814
- /// The parser is currently looking for inline nodes.
815
- inline,
816
- }
817
-
818
- class _ZulipContentParser {
819
- /// The current state of what sort of nodes the parser is looking for.
820
- ///
821
- /// This exists for the sake of debug-mode checks,
822
- /// and should be read or updated only inside an assertion.
823
- _ParserContext _debugParserContext = _ParserContext .block;
824
-
825
- String ? parseMath (dom.Element element, {required bool block}) {
826
- assert (block == (_debugParserContext == _ParserContext .block));
827
-
828
- final dom.Element katexElement;
829
- if (! block) {
830
- assert (element.localName == 'span' && element.className == 'katex' );
814
+ katexElement = element;
815
+ } else {
816
+ assert (element.localName == 'span' && element.className == 'katex-display' );
831
817
832
- katexElement = element;
833
- } else {
834
- assert (element.localName == 'span' && element.className == 'katex-display' );
835
-
836
- if (element.nodes.length != 1 ) return null ;
837
- final child = element.nodes.single;
838
- if (child is ! dom.Element ) return null ;
839
- if (child.localName != 'span' ) return null ;
840
- if (child.className != 'katex' ) return null ;
841
- katexElement = child;
842
- }
843
-
844
- // Expect two children span.katex-mathml, span.katex-html .
845
- // For now we only care about the .katex-mathml .
846
- if (katexElement.nodes.isEmpty) return null ;
847
- final child = katexElement.nodes.first;
818
+ if (element.nodes.length != 1 ) return null ;
819
+ final child = element.nodes.single;
848
820
if (child is ! dom.Element ) return null ;
849
821
if (child.localName != 'span' ) return null ;
850
- if (child.className != 'katex-mathml' ) return null ;
851
-
852
- if (child.nodes.length != 1 ) return null ;
853
- final grandchild = child.nodes.single;
854
- if (grandchild is ! dom.Element ) return null ;
855
- if (grandchild.localName != 'math' ) return null ;
856
- if (grandchild.attributes['display' ] != (block ? 'block' : null )) return null ;
857
- if (grandchild.namespaceUri != 'http://www.w3.org/1998/Math/MathML' ) return null ;
858
-
859
- if (grandchild.nodes.length != 1 ) return null ;
860
- final greatgrand = grandchild.nodes.single;
861
- if (greatgrand is ! dom.Element ) return null ;
862
- if (greatgrand.localName != 'semantics' ) return null ;
863
-
864
- if (greatgrand.nodes.isEmpty) return null ;
865
- final descendant4 = greatgrand.nodes.last;
866
- if (descendant4 is ! dom.Element ) return null ;
867
- if (descendant4.localName != 'annotation' ) return null ;
868
- if (descendant4.attributes['encoding' ] != 'application/x-tex' ) return null ;
869
-
870
- return descendant4.text.trim ();
822
+ if (child.className != 'katex' ) return null ;
823
+ katexElement = child;
824
+ }
825
+
826
+ // Expect two children span.katex-mathml, span.katex-html .
827
+ // For now we only care about the .katex-mathml .
828
+ if (katexElement.nodes.isEmpty) return null ;
829
+ final child = katexElement.nodes.first;
830
+ if (child is ! dom.Element ) return null ;
831
+ if (child.localName != 'span' ) return null ;
832
+ if (child.className != 'katex-mathml' ) return null ;
833
+
834
+ if (child.nodes.length != 1 ) return null ;
835
+ final grandchild = child.nodes.single;
836
+ if (grandchild is ! dom.Element ) return null ;
837
+ if (grandchild.localName != 'math' ) return null ;
838
+ if (grandchild.attributes['display' ] != (block ? 'block' : null )) return null ;
839
+ if (grandchild.namespaceUri != 'http://www.w3.org/1998/Math/MathML' ) return null ;
840
+
841
+ if (grandchild.nodes.length != 1 ) return null ;
842
+ final greatgrand = grandchild.nodes.single;
843
+ if (greatgrand is ! dom.Element ) return null ;
844
+ if (greatgrand.localName != 'semantics' ) return null ;
845
+
846
+ if (greatgrand.nodes.isEmpty) return null ;
847
+ final descendant4 = greatgrand.nodes.last;
848
+ if (descendant4 is ! dom.Element ) return null ;
849
+ if (descendant4.localName != 'annotation' ) return null ;
850
+ if (descendant4.attributes['encoding' ] != 'application/x-tex' ) return null ;
851
+
852
+ return descendant4.text.trim ();
853
+ }
854
+
855
+ /// Parser for the inline-content subtrees within Zulip content HTML.
856
+ ///
857
+ /// The only entry point to this class is [parseBlockInline] .
858
+ ///
859
+ /// After a call to [parseBlockInline] returns, the [_ZulipInlineContentParser]
860
+ /// instance has been reset to its starting state, and can be re-used for
861
+ /// parsing other subtrees.
862
+ class _ZulipInlineContentParser {
863
+ InlineContentNode ? parseInlineMath (dom.Element element) {
864
+ final debugHtmlNode = kDebugMode ? element : null ;
865
+ final texSource = _parseMath (element, block: false );
866
+ if (texSource == null ) return null ;
867
+ return MathInlineNode (texSource: texSource, debugHtmlNode: debugHtmlNode);
871
868
}
872
869
873
870
UserMentionNode ? parseUserMention (dom.Element element) {
874
- assert (_debugParserContext == _ParserContext .inline);
875
871
assert (element.localName == 'span' );
876
872
final debugHtmlNode = kDebugMode ? element : null ;
877
873
@@ -945,7 +941,6 @@ class _ZulipContentParser {
945
941
static final _emojiCodeFromClassNameRegexp = RegExp (r"emoji-([^ ]+)" );
946
942
947
943
InlineContentNode parseInlineContent (dom.Node node) {
948
- assert (_debugParserContext == _ParserContext .inline);
949
944
final debugHtmlNode = kDebugMode ? node : null ;
950
945
InlineContentNode unimplemented () => UnimplementedInlineContentNode (htmlNode: node);
951
946
@@ -1025,36 +1020,49 @@ class _ZulipContentParser {
1025
1020
}
1026
1021
1027
1022
if (localName == 'span' && className == 'katex' ) {
1028
- final texSource = parseMath (element, block: false );
1029
- if (texSource == null ) return unimplemented ();
1030
- return MathInlineNode (texSource: texSource, debugHtmlNode: debugHtmlNode);
1023
+ return parseInlineMath (element) ?? unimplemented ();
1031
1024
}
1032
1025
1033
1026
// TODO more types of node
1034
1027
return unimplemented ();
1035
1028
}
1036
1029
1037
1030
List <InlineContentNode > parseInlineContentList (List <dom.Node > nodes) {
1038
- assert (_debugParserContext == _ParserContext .inline);
1039
1031
return nodes.map (parseInlineContent).toList (growable: false );
1040
1032
}
1041
1033
1034
+ /// Parse the children of a [BlockInlineContainerNode] , making up a
1035
+ /// complete subtree of inline content with no further inline ancestors.
1042
1036
({List <InlineContentNode > nodes, List <LinkNode >? links}) parseBlockInline (List <dom.Node > nodes) {
1043
- assert (_debugParserContext == _ParserContext .block);
1044
- assert (() {
1045
- _debugParserContext = _ParserContext .inline;
1046
- return true ;
1047
- }());
1048
1037
final resultNodes = parseInlineContentList (nodes);
1049
- assert (() {
1050
- _debugParserContext = _ParserContext .block;
1051
- return true ;
1052
- }());
1053
1038
return (nodes: resultNodes, links: _takeLinkNodes ());
1054
1039
}
1040
+ }
1041
+
1042
+ /// Parser for a complete piece of Zulip HTML content, a [ZulipContent] .
1043
+ ///
1044
+ /// The only entry point to this class is [parse] .
1045
+ class _ZulipContentParser {
1046
+ /// The single inline-content parser used and re-used throughout parsing of
1047
+ /// a complete piece of Zulip HTML content.
1048
+ ///
1049
+ /// Because block content can never appear nested inside inline content,
1050
+ /// there's never a need for more than one of these at a time,
1051
+ /// so we can allocate just one up front.
1052
+ final inlineParser = _ZulipInlineContentParser ();
1053
+
1054
+ ({List <InlineContentNode > nodes, List <LinkNode >? links}) parseBlockInline (List <dom.Node > nodes) {
1055
+ return inlineParser.parseBlockInline (nodes);
1056
+ }
1057
+
1058
+ BlockContentNode parseMathBlock (dom.Element element) {
1059
+ final debugHtmlNode = kDebugMode ? element : null ;
1060
+ final texSource = _parseMath (element, block: true );
1061
+ if (texSource == null ) return UnimplementedBlockContentNode (htmlNode: element);
1062
+ return MathBlockNode (texSource: texSource, debugHtmlNode: debugHtmlNode);
1063
+ }
1055
1064
1056
1065
BlockContentNode parseListNode (dom.Element element) {
1057
- assert (_debugParserContext == _ParserContext .block);
1058
1066
ListStyle ? listStyle;
1059
1067
switch (element.localName) {
1060
1068
case 'ol' : listStyle = ListStyle .ordered; break ;
@@ -1077,7 +1085,6 @@ class _ZulipContentParser {
1077
1085
}
1078
1086
1079
1087
BlockContentNode parseSpoilerNode (dom.Element divElement) {
1080
- assert (_debugParserContext == _ParserContext .block);
1081
1088
assert (divElement.localName == 'div'
1082
1089
&& divElement.className == 'spoiler-block' );
1083
1090
@@ -1097,7 +1104,6 @@ class _ZulipContentParser {
1097
1104
}
1098
1105
1099
1106
BlockContentNode parseCodeBlock (dom.Element divElement) {
1100
- assert (_debugParserContext == _ParserContext .block);
1101
1107
final mainElement = () {
1102
1108
assert (divElement.localName == 'div'
1103
1109
&& divElement.className == "codehilite" );
@@ -1180,7 +1186,6 @@ class _ZulipContentParser {
1180
1186
static final _imageDimensionsRegExp = RegExp (r'^(\d+)x(\d+)$' );
1181
1187
1182
1188
BlockContentNode parseImageNode (dom.Element divElement) {
1183
- assert (_debugParserContext == _ParserContext .block);
1184
1189
final elements = () {
1185
1190
assert (divElement.localName == 'div'
1186
1191
&& divElement.className == 'message_inline_image' );
@@ -1272,7 +1277,6 @@ class _ZulipContentParser {
1272
1277
}();
1273
1278
1274
1279
BlockContentNode parseInlineVideoNode (dom.Element divElement) {
1275
- assert (_debugParserContext == _ParserContext .block);
1276
1280
assert (divElement.localName == 'div'
1277
1281
&& _videoClassNameRegexp.hasMatch (divElement.className));
1278
1282
@@ -1305,7 +1309,6 @@ class _ZulipContentParser {
1305
1309
}
1306
1310
1307
1311
BlockContentNode parseEmbedVideoNode (dom.Element divElement) {
1308
- assert (_debugParserContext == _ParserContext .block);
1309
1312
assert (divElement.localName == 'div'
1310
1313
&& _videoClassNameRegexp.hasMatch (divElement.className));
1311
1314
@@ -1344,7 +1347,6 @@ class _ZulipContentParser {
1344
1347
}
1345
1348
1346
1349
BlockContentNode parseTableContent (dom.Element tableElement) {
1347
- assert (_debugParserContext == _ParserContext .block);
1348
1350
assert (tableElement.localName == 'table'
1349
1351
&& tableElement.className.isEmpty);
1350
1352
@@ -1452,7 +1454,6 @@ class _ZulipContentParser {
1452
1454
}
1453
1455
1454
1456
BlockContentNode parseBlockContent (dom.Node node) {
1455
- assert (_debugParserContext == _ParserContext .block);
1456
1457
final debugHtmlNode = kDebugMode ? node : null ;
1457
1458
if (node is ! dom.Element ) {
1458
1459
return UnimplementedBlockContentNode (htmlNode: node);
@@ -1480,9 +1481,7 @@ class _ZulipContentParser {
1480
1481
// The case with the `<br>\n` can happen when at the end of a quote;
1481
1482
// it seems like a glitch in the server's Markdown processing,
1482
1483
// so hopefully there just aren't any further such glitches.
1483
- final texSource = parseMath (child, block: true );
1484
- if (texSource == null ) return UnimplementedBlockContentNode (htmlNode: node);
1485
- return MathBlockNode (texSource: texSource, debugHtmlNode: debugHtmlNode);
1484
+ return parseMathBlock (child);
1486
1485
}
1487
1486
}
1488
1487
}
@@ -1579,10 +1578,15 @@ class _ZulipContentParser {
1579
1578
///
1580
1579
/// See [ParagraphNode] .
1581
1580
List <BlockContentNode > parseImplicitParagraphBlockContentList (dom.NodeList nodes) {
1582
- assert (_debugParserContext == _ParserContext .block);
1583
1581
final List <BlockContentNode > result = [];
1584
- final List <dom. Node > currentParagraph = [];
1582
+
1585
1583
List <ImageNode > imageNodes = [];
1584
+ void consumeImageNodes () {
1585
+ result.add (ImageNodeList (imageNodes));
1586
+ imageNodes = [];
1587
+ }
1588
+
1589
+ final List <dom.Node > currentParagraph = [];
1586
1590
void consumeParagraph () {
1587
1591
final parsed = parseBlockInline (currentParagraph);
1588
1592
result.add (ParagraphNode (
@@ -1597,8 +1601,7 @@ class _ZulipContentParser {
1597
1601
1598
1602
if (_isPossibleInlineNode (node)) {
1599
1603
if (imageNodes.isNotEmpty) {
1600
- result.add (ImageNodeList (imageNodes));
1601
- imageNodes = [];
1604
+ consumeImageNodes ();
1602
1605
// In a context where paragraphs are implicit it should be impossible
1603
1606
// to have more paragraph content after image previews.
1604
1607
result.add (UnimplementedBlockContentNode (htmlNode: node));
@@ -1613,24 +1616,25 @@ class _ZulipContentParser {
1613
1616
imageNodes.add (block);
1614
1617
continue ;
1615
1618
}
1616
- if (imageNodes.isNotEmpty) {
1617
- result.add (ImageNodeList (imageNodes));
1618
- imageNodes = [];
1619
- }
1619
+ if (imageNodes.isNotEmpty) consumeImageNodes ();
1620
1620
result.add (block);
1621
1621
}
1622
1622
if (currentParagraph.isNotEmpty) consumeParagraph ();
1623
- if (imageNodes.isNotEmpty) result.add (ImageNodeList (imageNodes));
1624
-
1623
+ if (imageNodes.isNotEmpty) consumeImageNodes ();
1625
1624
return result;
1626
1625
}
1627
1626
1628
1627
static final _redundantLineBreaksRegexp = RegExp (r'^\n+$' );
1629
1628
1630
1629
List <BlockContentNode > parseBlockContentList (dom.NodeList nodes) {
1631
- assert (_debugParserContext == _ParserContext .block);
1632
1630
final List <BlockContentNode > result = [];
1631
+
1633
1632
List <ImageNode > imageNodes = [];
1633
+ void consumeImageNodes () {
1634
+ result.add (ImageNodeList (imageNodes));
1635
+ imageNodes = [];
1636
+ }
1637
+
1634
1638
for (final node in nodes) {
1635
1639
// We get a bunch of newline Text nodes between paragraphs.
1636
1640
// A browser seems to ignore these; let's do the same.
@@ -1643,13 +1647,10 @@ class _ZulipContentParser {
1643
1647
imageNodes.add (block);
1644
1648
continue ;
1645
1649
}
1646
- if (imageNodes.isNotEmpty) {
1647
- result.add (ImageNodeList (imageNodes));
1648
- imageNodes = [];
1649
- }
1650
+ if (imageNodes.isNotEmpty) consumeImageNodes ();
1650
1651
result.add (block);
1651
1652
}
1652
- if (imageNodes.isNotEmpty) result. add ( ImageNodeList (imageNodes) );
1653
+ if (imageNodes.isNotEmpty) consumeImageNodes ( );
1653
1654
return result;
1654
1655
}
1655
1656
@@ -1660,6 +1661,8 @@ class _ZulipContentParser {
1660
1661
}
1661
1662
}
1662
1663
1664
+ /// Parse a complete piece of Zulip HTML content,
1665
+ /// such as an entire value of [Message.content] .
1663
1666
ZulipContent parseContent (String html) {
1664
1667
return _ZulipContentParser ().parse (html);
1665
1668
}
0 commit comments