Skip to content

Commit 84ff50f

Browse files
content: Handle multiple math blocks in <p>
Fixes: #1130
1 parent 9aafbd4 commit 84ff50f

File tree

2 files changed

+143
-15
lines changed

2 files changed

+143
-15
lines changed

lib/model/content.dart

+78-15
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,61 @@ class _ZulipContentParser {
14531453
return tableNode ?? UnimplementedBlockContentNode(htmlNode: tableElement);
14541454
}
14551455

1456+
void parseMathBlocks(dom.NodeList nodes, List<BlockContentNode> result) {
1457+
assert(nodes.isNotEmpty);
1458+
assert((() {
1459+
final first = nodes.first;
1460+
return first is dom.Element
1461+
&& first.localName == 'span'
1462+
&& first.className == 'katex-display';
1463+
})());
1464+
1465+
final firstChild = nodes.first as dom.Element;
1466+
final texSource = _parseMath(firstChild, block: true);
1467+
if (texSource != null) {
1468+
result.add(MathBlockNode(texSource: texSource, debugHtmlNode: firstChild));
1469+
} else {
1470+
result.add(UnimplementedBlockContentNode(htmlNode: firstChild));
1471+
}
1472+
1473+
// Skip further checks if there was only a single child.
1474+
if (nodes.length == 1) return;
1475+
1476+
// The case with the `<br>\n` can happen when at the end of a quote;
1477+
// it seems like a glitch in the server's Markdown processing,
1478+
// so hopefully there just aren't any further such glitches.
1479+
bool hasTrailingBreakNewline = false;
1480+
if (nodes
1481+
case [..., dom.Element(localName: 'br'), dom.Text(text: '\n')]
1482+
) {
1483+
hasTrailingBreakNewline = true;
1484+
}
1485+
1486+
final length = hasTrailingBreakNewline
1487+
? nodes.length - 2
1488+
: nodes.length;
1489+
for (int i = 1; i < length; i++) {
1490+
final child = nodes[i];
1491+
1492+
// If there are multiple <span class="katex-display"> nodes in a <p>
1493+
// each node is interleaved by '\n\n'. Whitespaces are ignored in HTML
1494+
// on web but each node has `display: block`, which renders each node
1495+
// on a newline. Since the emitted MathBlockNode are BlockContentNode,
1496+
// we skip these newlines here to replicate the same behavior as on web.
1497+
if (child case dom.Text(text: '\n\n')) continue;
1498+
1499+
if (child case dom.Element(localName: 'span', className: 'katex-display')) {
1500+
final texSource = _parseMath(child, block: true);
1501+
if (texSource != null) {
1502+
result.add(MathBlockNode(texSource: texSource, debugHtmlNode: child));
1503+
continue;
1504+
}
1505+
}
1506+
1507+
result.add(UnimplementedBlockContentNode(htmlNode: child));
1508+
}
1509+
}
1510+
14561511
BlockContentNode parseBlockContent(dom.Node node) {
14571512
final debugHtmlNode = kDebugMode ? node : null;
14581513
if (node is! dom.Element) {
@@ -1471,21 +1526,6 @@ class _ZulipContentParser {
14711526
}
14721527

14731528
if (localName == 'p' && className.isEmpty) {
1474-
// Oddly, the way a math block gets encoded in Zulip HTML is inside a <p>.
1475-
if (element.nodes case [dom.Element(localName: 'span') && var child, ...]) {
1476-
if (child.className == 'katex-display') {
1477-
if (element.nodes case [_]
1478-
|| [_, dom.Element(localName: 'br'),
1479-
dom.Text(text: "\n")]) {
1480-
// This might be too specific; we'll find out when we do #190.
1481-
// The case with the `<br>\n` can happen when at the end of a quote;
1482-
// it seems like a glitch in the server's Markdown processing,
1483-
// so hopefully there just aren't any further such glitches.
1484-
return parseMathBlock(child);
1485-
}
1486-
}
1487-
}
1488-
14891529
final parsed = parseBlockInline(element.nodes);
14901530
return ParagraphNode(debugHtmlNode: debugHtmlNode,
14911531
links: parsed.links,
@@ -1599,6 +1639,18 @@ class _ZulipContentParser {
15991639
for (final node in nodes) {
16001640
if (node is dom.Text && (node.text == '\n')) continue;
16011641

1642+
// Oddly, the way math blocks get encoded in Zulip HTML is inside a <p>.
1643+
// And there can be multiple math blocks inside the paragraph node, so
1644+
// handle it explicitly here.
1645+
if (node case
1646+
dom.Element(localName: 'p', nodes: [
1647+
dom.Element(localName: 'span', className: 'katex-display'), ...])) {
1648+
if (currentParagraph.isNotEmpty) consumeParagraph();
1649+
if (imageNodes.isNotEmpty) consumeImageNodes();
1650+
parseMathBlocks(node.nodes, result);
1651+
continue;
1652+
}
1653+
16021654
if (_isPossibleInlineNode(node)) {
16031655
if (imageNodes.isNotEmpty) {
16041656
consumeImageNodes();
@@ -1642,6 +1694,17 @@ class _ZulipContentParser {
16421694
continue;
16431695
}
16441696

1697+
// Oddly, the way math blocks get encoded in Zulip HTML is inside a <p>.
1698+
// And there can be multiple math blocks inside the paragraph node, so
1699+
// handle it explicitly here.
1700+
if (node case
1701+
dom.Element(localName: 'p', nodes: [
1702+
dom.Element(localName: 'span', className: 'katex-display'), ...])) {
1703+
if (imageNodes.isNotEmpty) consumeImageNodes();
1704+
parseMathBlocks(node.nodes, result);
1705+
continue;
1706+
}
1707+
16451708
final block = parseBlockContent(node);
16461709
if (block is ImageNode) {
16471710
imageNodes.add(block);

test/model/content_test.dart

+65
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,23 @@ class ContentExample {
506506
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span></span></span></span></span></p>',
507507
[MathBlockNode(texSource: r'\lambda')]);
508508

509+
static const mathBlocksMultipleInParagraph = ContentExample(
510+
'math blocks, multiple in paragraph',
511+
'```math\na\n\nb\n```',
512+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/.E2.9C.94.20Rajesh/near/2001490
513+
'<p>'
514+
'<span class="katex-display"><span class="katex">'
515+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>a</mi></mrow>'
516+
'<annotation encoding="application/x-tex">a</annotation></semantics></math></span>'
517+
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span></span>\n\n'
518+
'<span class="katex-display"><span class="katex">'
519+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>b</mi></mrow>'
520+
'<annotation encoding="application/x-tex">b</annotation></semantics></math></span>'
521+
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span></span></p>', [
522+
MathBlockNode(texSource: 'a'),
523+
MathBlockNode(texSource: 'b'),
524+
]);
525+
509526
static const mathBlockInQuote = ContentExample(
510527
'math block in quote',
511528
// There's sometimes a quirky extra `<br>\n` at the end of the `<p>` that
@@ -522,6 +539,51 @@ class ContentExample {
522539
'<br>\n</p>\n</blockquote>',
523540
[QuotationNode([MathBlockNode(texSource: r'\lambda')])]);
524541

542+
static const mathBlocksMultipleInQuote = ContentExample(
543+
'math blocks, multiple in quote',
544+
"````quote\n```math\na\n\nb\n```\n````",
545+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/.E2.9C.94.20Rajesh/near/2029236
546+
'<blockquote>\n<p>'
547+
'<span class="katex-display"><span class="katex">'
548+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>a</mi></mrow>'
549+
'<annotation encoding="application/x-tex">a</annotation></semantics></math></span>'
550+
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span></span>'
551+
'\n\n'
552+
'<span class="katex-display"><span class="katex">'
553+
'<span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>b</mi></mrow>'
554+
'<annotation encoding="application/x-tex">b</annotation></semantics></math></span>'
555+
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span></span></span></span></span>'
556+
'<br>\n</p>\n</blockquote>',
557+
[QuotationNode([
558+
MathBlockNode(texSource: 'a'),
559+
MathBlockNode(texSource: 'b'),
560+
])]);
561+
562+
static const mathBlockBetweenImages = ContentExample(
563+
'math block between images',
564+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Greg/near/2035891
565+
'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',
566+
'<div class="message_inline_image"><a href="https://upload.wikimedia.org/wikipedia/commons/7/78/Verregende_bloem_van_een_Helenium_%27El_Dorado%27._22-07-2023._%28d.j.b%29.jpg"><img src="/external_content/de28eb3abf4b7786de4545023dc42d434a2ea0c2/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f372f37382f566572726567656e64655f626c6f656d5f76616e5f65656e5f48656c656e69756d5f253237456c5f446f7261646f2532372e5f32322d30372d323032332e5f253238642e6a2e622532392e6a7067"></a></div><p><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>a</mi></mrow><annotation encoding="application/x-tex">a</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">a</span></span></span></span></span></p>\n<div class="message_inline_image"><a href="https://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"><img src="/external_content/58b0ef9a06d7bb24faec2b11df2f57f476e6f6bb/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f7468756d622f372f37312f5a616164706c75697a656e5f76616e5f65656e5f436c656d617469735f746578656e7369735f2532375072696e636573735f4469616e612532372e5f31382d30372d323032335f2532386163746d2e2532395f30322e6a70672f3132383070782d5a616164706c75697a656e5f76616e5f65656e5f436c656d617469735f746578656e7369735f2532375072696e636573735f4469616e612532372e5f31382d30372d323032335f2532386163746d2e2532395f30322e6a7067"></a></div>',
567+
[
568+
ImageNodeList([
569+
ImageNode(
570+
srcUrl: '/external_content/de28eb3abf4b7786de4545023dc42d434a2ea0c2/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f372f37382f566572726567656e64655f626c6f656d5f76616e5f65656e5f48656c656e69756d5f253237456c5f446f7261646f2532372e5f32322d30372d323032332e5f253238642e6a2e622532392e6a7067',
571+
thumbnailUrl: null,
572+
loading: false,
573+
originalWidth: null,
574+
originalHeight: null),
575+
]),
576+
MathBlockNode(texSource: 'a'),
577+
ImageNodeList([
578+
ImageNode(
579+
srcUrl: '/external_content/58b0ef9a06d7bb24faec2b11df2f57f476e6f6bb/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f7468756d622f372f37312f5a616164706c75697a656e5f76616e5f65656e5f436c656d617469735f746578656e7369735f2532375072696e636573735f4469616e612532372e5f31382d30372d323032335f2532386163746d2e2532395f30322e6a70672f3132383070782d5a616164706c75697a656e5f76616e5f65656e5f436c656d617469735f746578656e7369735f2532375072696e636573735f4469616e612532372e5f31382d30372d323032335f2532386163746d2e2532395f30322e6a7067',
580+
thumbnailUrl: null,
581+
loading: false,
582+
originalWidth: null,
583+
originalHeight: null),
584+
]),
585+
]);
586+
525587
static const imageSingle = ContentExample(
526588
'single image',
527589
// https://chat.zulip.org/#narrow/stream/7-test-here/topic/Thumbnails/near/1900103
@@ -1470,7 +1532,10 @@ void main() {
14701532
testParseExample(ContentExample.codeBlockFollowedByMultipleLineBreaks);
14711533

14721534
testParseExample(ContentExample.mathBlock);
1535+
testParseExample(ContentExample.mathBlocksMultipleInParagraph);
14731536
testParseExample(ContentExample.mathBlockInQuote);
1537+
testParseExample(ContentExample.mathBlocksMultipleInQuote);
1538+
testParseExample(ContentExample.mathBlockBetweenImages);
14741539

14751540
testParseExample(ContentExample.imageSingle);
14761541
testParseExample(ContentExample.imageSingleNoDimensions);

0 commit comments

Comments
 (0)