diff --git a/lib/model/content.dart b/lib/model/content.dart index e228163a2e..8a5204973e 100644 --- a/lib/model/content.dart +++ b/lib/model/content.dart @@ -1055,13 +1055,6 @@ class _ZulipContentParser { return inlineParser.parseBlockInline(nodes); } - BlockContentNode parseMathBlock(dom.Element element) { - final debugHtmlNode = kDebugMode ? element : null; - final texSource = _parseMath(element, block: true); - if (texSource == null) return UnimplementedBlockContentNode(htmlNode: element); - return MathBlockNode(texSource: texSource, debugHtmlNode: debugHtmlNode); - } - BlockContentNode parseListNode(dom.Element element) { ListStyle? listStyle; switch (element.localName) { @@ -1453,6 +1446,64 @@ class _ZulipContentParser { return tableNode ?? UnimplementedBlockContentNode(htmlNode: tableElement); } + void parseMathBlocks(dom.NodeList nodes, List result) { + assert(nodes.isNotEmpty); + assert((() { + final first = nodes.first; + return first is dom.Element + && first.localName == 'span' + && first.className == 'katex-display'; + })()); + + final firstChild = nodes.first as dom.Element; + final texSource = _parseMath(firstChild, block: true); + if (texSource != null) { + result.add(MathBlockNode( + texSource: texSource, + debugHtmlNode: kDebugMode ? firstChild : null)); + } else { + result.add(UnimplementedBlockContentNode(htmlNode: firstChild)); + } + + // Skip further checks if there was only a single child. + if (nodes.length == 1) return; + + // The case with the `
\n` can happen when at the end of a quote; + // it seems like a glitch in the server's Markdown processing, + // so hopefully there just aren't any further such glitches. + bool hasTrailingBreakNewline = false; + if (nodes case [..., dom.Element(localName: 'br'), dom.Text(text: '\n')]) { + hasTrailingBreakNewline = true; + } + + final length = hasTrailingBreakNewline + ? nodes.length - 2 + : nodes.length; + for (int i = 1; i < length; i++) { + final child = nodes[i]; + final debugHtmlNode = kDebugMode ? child : null; + + // If there are multiple nodes in a

+ // each node is interleaved by '\n\n'. Whitespaces are ignored in HTML + // on web but each node has `display: block`, which renders each node + // on a new line. Since the emitted MathBlockNode are BlockContentNode, + // we skip these newlines here to replicate the same behavior as on web. + if (child case dom.Text(text: '\n\n')) continue; + + if (child case dom.Element(localName: 'span', className: 'katex-display')) { + final texSource = _parseMath(child, block: true); + if (texSource != null) { + result.add(MathBlockNode( + texSource: texSource, + debugHtmlNode: debugHtmlNode)); + continue; + } + } + + result.add(UnimplementedBlockContentNode(htmlNode: child)); + } + } + BlockContentNode parseBlockContent(dom.Node node) { final debugHtmlNode = kDebugMode ? node : null; if (node is! dom.Element) { @@ -1471,21 +1522,6 @@ class _ZulipContentParser { } if (localName == 'p' && className.isEmpty) { - // Oddly, the way a math block gets encoded in Zulip HTML is inside a

. - if (element.nodes case [dom.Element(localName: 'span') && var child, ...]) { - if (child.className == 'katex-display') { - if (element.nodes case [_] - || [_, dom.Element(localName: 'br'), - dom.Text(text: "\n")]) { - // This might be too specific; we'll find out when we do #190. - // The case with the `
\n` can happen when at the end of a quote; - // it seems like a glitch in the server's Markdown processing, - // so hopefully there just aren't any further such glitches. - return parseMathBlock(child); - } - } - } - final parsed = parseBlockInline(element.nodes); return ParagraphNode(debugHtmlNode: debugHtmlNode, links: parsed.links, @@ -1599,6 +1635,17 @@ class _ZulipContentParser { for (final node in nodes) { if (node is dom.Text && (node.text == '\n')) continue; + // Oddly, the way math blocks get encoded in Zulip HTML is inside a

. + // And there can be multiple math blocks inside the paragraph node, so + // handle it explicitly here. + if (node case dom.Element(localName: 'p', className: '', nodes: [ + dom.Element(localName: 'span', className: 'katex-display'), ...])) { + if (currentParagraph.isNotEmpty) consumeParagraph(); + if (imageNodes.isNotEmpty) consumeImageNodes(); + parseMathBlocks(node.nodes, result); + continue; + } + if (_isPossibleInlineNode(node)) { if (imageNodes.isNotEmpty) { consumeImageNodes(); @@ -1642,6 +1689,16 @@ class _ZulipContentParser { continue; } + // Oddly, the way math blocks get encoded in Zulip HTML is inside a

. + // And there can be multiple math blocks inside the paragraph node, so + // handle it explicitly here. + if (node case dom.Element(localName: 'p', className: '', nodes: [ + dom.Element(localName: 'span', className: 'katex-display'), ...])) { + if (imageNodes.isNotEmpty) consumeImageNodes(); + parseMathBlocks(node.nodes, result); + continue; + } + final block = parseBlockContent(node); if (block is ImageNode) { imageNodes.add(block); diff --git a/test/model/content_test.dart b/test/model/content_test.dart index 361259eb61..117c121660 100644 --- a/test/model/content_test.dart +++ b/test/model/content_test.dart @@ -506,6 +506,23 @@ class ContentExample { '

', [MathBlockNode(texSource: r'\lambda')]); + static const mathBlocksMultipleInParagraph = ContentExample( + 'math blocks, multiple in paragraph', + '```math\na\n\nb\n```', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/.E2.9C.94.20Rajesh/near/2001490 + '

' + '' + 'a' + 'a' + '\n\n' + '' + 'b' + 'b' + '

', [ + MathBlockNode(texSource: 'a'), + MathBlockNode(texSource: 'b'), + ]); + static const mathBlockInQuote = ContentExample( 'math block in quote', // There's sometimes a quirky extra `
\n` at the end of the `

` that @@ -522,6 +539,62 @@ class ContentExample { '
\n

\n', [QuotationNode([MathBlockNode(texSource: r'\lambda')])]); + static const mathBlocksMultipleInQuote = ContentExample( + 'math blocks, multiple in quote', + "````quote\n```math\na\n\nb\n```\n````", + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/.E2.9C.94.20Rajesh/near/2029236 + '
\n

' + '' + 'a' + 'a' + '' + '\n\n' + '' + 'b' + 'b' + '' + '
\n

\n
', + [QuotationNode([ + MathBlockNode(texSource: 'a'), + MathBlockNode(texSource: 'b'), + ])]); + + static const mathBlockBetweenImages = ContentExample( + 'math block between images', + // https://chat.zulip.org/#narrow/channel/7-test-here/topic/Greg/near/2035891 + 'https://upload.wikimedia.org/wikipedia/commons/7/78/Verregende_bloem_van_een_Helenium_%27El_Dorado%27._22-07-2023._%28d.j.b%29.jpg\n```math\na\n```\nhttps://upload.wikimedia.org/wikipedia/commons/thumb/7/71/Zaadpluizen_van_een_Clematis_texensis_%27Princess_Diana%27._18-07-2023_%28actm.%29_02.jpg/1280px-Zaadpluizen_van_een_Clematis_texensis_%27Princess_Diana%27._18-07-2023_%28actm.%29_02.jpg', + '
' + '' + '
' + '

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

\n' + '
' + '' + '
', + [ + ImageNodeList([ + ImageNode( + srcUrl: '/external_content/de28eb3abf4b7786de4545023dc42d434a2ea0c2/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f372f37382f566572726567656e64655f626c6f656d5f76616e5f65656e5f48656c656e69756d5f253237456c5f446f7261646f2532372e5f32322d30372d323032332e5f253238642e6a2e622532392e6a7067', + thumbnailUrl: null, + loading: false, + originalWidth: null, + originalHeight: null), + ]), + MathBlockNode(texSource: 'a'), + ImageNodeList([ + ImageNode( + srcUrl: '/external_content/58b0ef9a06d7bb24faec2b11df2f57f476e6f6bb/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f7468756d622f372f37312f5a616164706c75697a656e5f76616e5f65656e5f436c656d617469735f746578656e7369735f2532375072696e636573735f4469616e612532372e5f31382d30372d323032335f2532386163746d2e2532395f30322e6a70672f3132383070782d5a616164706c75697a656e5f76616e5f65656e5f436c656d617469735f746578656e7369735f2532375072696e636573735f4469616e612532372e5f31382d30372d323032335f2532386163746d2e2532395f30322e6a7067', + thumbnailUrl: null, + loading: false, + originalWidth: null, + originalHeight: null), + ]), + ]); + static const imageSingle = ContentExample( 'single image', // https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103 @@ -1470,7 +1543,10 @@ void main() { testParseExample(ContentExample.codeBlockFollowedByMultipleLineBreaks); testParseExample(ContentExample.mathBlock); + testParseExample(ContentExample.mathBlocksMultipleInParagraph); testParseExample(ContentExample.mathBlockInQuote); + testParseExample(ContentExample.mathBlocksMultipleInQuote); + testParseExample(ContentExample.mathBlockBetweenImages); testParseExample(ContentExample.imageSingle); testParseExample(ContentExample.imageSingleNoDimensions);