| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-05-03 03:08:18 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-05-03 19:34:35 UTC |
| parent | ad1e8065219392929567a144ff924a9084bb42c4 |
| borogove/XEP0393.hx | +28 | -5 |
| test/TestChatMessage.hx | +20 | -3 |
| test/TestChatMessageBuilder.hx | +2 | -2 |
| test/TestHtml.hx | +1 | -1 |
| test/TestXEP0393.hx | +100 | -31 |
diff --git a/borogove/XEP0393.hx b/borogove/XEP0393.hx index 61ec127..3a7f742 100644 --- a/borogove/XEP0393.hx +++ b/borogove/XEP0393.hx @@ -113,7 +113,7 @@ class XEP0393 { endsWithNewline = true; } - if (xhtml.name == "p") { + if (xhtml.name == "p" && xhtml.attr.get("class") != "tight") { s.add("\n"); } @@ -231,11 +231,26 @@ class XEP0393 { return parsePreformatted(styled); } else { var end = 0; + final nodes = []; + var tight = false; final styledLength = styled.length; - while (end < styledLength && styled.charAt(end) != "\n") end++; - final lineEnd = end; - if (end < styledLength && styled.charAt(end) == "\n") end++; - return { block: new Stanza("div").addChildNodes(parseSpans(styled.substr(0, lineEnd))), rest: styled.substr(end) }; + while (end < styledLength) { + var lineEnd = end; + while (lineEnd < styledLength && styled.charAt(lineEnd) != "\n") lineEnd++; + if (nodes.length > 0) nodes.push(Element(new Stanza("br"))); + for (span in parseSpans(styled.substr(end, lineEnd - end))) nodes.push(span); + end = lineEnd + 1; + if (styled.charAt(end) == "\n") { + end++; + break; + } + if (styled.charAt(end) == ">" || styled.substr(end, 3) == "```") { + tight = true; + break; + } + } + + return { block: new Stanza("p", tight ? { "class": "tight" } : {}).addChildNodes(nodes), rest: styled.substr(end) }; } } @@ -262,6 +277,10 @@ class XEP0393 { } } + if (styled.charAt(end) == "\n") { + end++; + } + return { block: new Stanza("blockquote").addChildren(parse(lines.join(""))), rest: styled.substr(end) }; } @@ -293,6 +312,10 @@ class XEP0393 { } } + if (styled.charAt(end) == "\n") { + end++; + } + final block = new Stanza("pre"); if (lang != "") { block.tag("code", {"class": 'language-$lang'}).text(lines.join("")); diff --git a/test/TestChatMessage.hx b/test/TestChatMessage.hx index cbb922a..e08794d 100644 --- a/test/TestChatMessage.hx +++ b/test/TestChatMessage.hx @@ -57,7 +57,7 @@ class TestChatMessage extends utest.Test { final msg = Message.fromStanza(stanza, JID.parse("bob@example.com")); switch (msg.parsed) { case ChatMessageStanza(m): - Assert.equals("<div>line 1</div><div><strong>line 2</strong></div>", m.body().toString()); + Assert.equals("<p>line 1<br /><strong>line 2</strong></p>", m.body().toString()); Assert.equals("line 1\n*line 2*", m.body().toPlainText()); default: Assert.fail("Expected ChatMessageStanza"); @@ -100,6 +100,23 @@ class TestChatMessage extends utest.Test { } } + public function testStyledBodyWithPreTightAfterPlain() { + final stanza = new Stanza("message"); + stanza.attr.set("id", "test-id-1"); + stanza.attr.set("from", "alice@example.com"); + stanza.attr.set("to", "bob@example.com"); + stanza.attr.set("type", "chat"); + stanza.addChild(new Stanza("body").text("hello\n```\nlet hello;")); + + final msg = Message.fromStanza(stanza, JID.parse("bob@example.com")); + switch (msg.parsed) { + case ChatMessageStanza(m): + Assert.equals("<p class=\"tight\">hello</p><pre>let hello;\n</pre>", m.body().toString()); + Assert.equals("hello\n```\nlet hello;\n```", m.body().toPlainText()); + default: + Assert.fail("Expected ChatMessageStanza"); + } + } public function testStyledBodyWithLinkBeaks() { final stanza = new Stanza("message"); @@ -112,7 +129,7 @@ class TestChatMessage extends utest.Test { final msg = Message.fromStanza(stanza, JID.parse("bob@example.com")); switch (msg.parsed) { case ChatMessageStanza(m): - Assert.equals("<div>Hey <<a href=\"https://example.com\">https://example.com</a>></div>", m.body().toString()); + Assert.equals("<p>Hey <<a href=\"https://example.com\">https://example.com</a>></p>", m.body().toString()); Assert.equals("Hey <https://example.com>", m.body().toPlainText()); default: Assert.fail("Expected ChatMessageStanza"); @@ -130,7 +147,7 @@ class TestChatMessage extends utest.Test { final msg = Message.fromStanza(stanza, JID.parse("bob@example.com")); switch (msg.parsed) { case ChatMessageStanza(m): - Assert.equals("<div>Hey <a href=\"https://example.com\">example.com</a></div>", m.body().toString()); + Assert.equals("<p>Hey <a href=\"https://example.com\">example.com</a></p>", m.body().toString()); Assert.equals("Hey example.com", m.body().toPlainText()); default: Assert.fail("Expected ChatMessageStanza"); diff --git a/test/TestChatMessageBuilder.hx b/test/TestChatMessageBuilder.hx index 4dd814f..17a3777 100644 --- a/test/TestChatMessageBuilder.hx +++ b/test/TestChatMessageBuilder.hx @@ -22,7 +22,7 @@ class TestChatMessageBuilder extends utest.Test { final msg = new ChatMessageBuilder(); msg.setBody(Html.fromString("<blockquote>Hello<br>you</blockquote><img alt=':boop:'><br><b>hi</b> <em>hi</em> <s>hey</s> <tt>up</tt><pre>hello<br>you")); Assert.equals( - "> Hello\n> you\n:boop:\n*hi* _hi_ ~hey~ `up`\n```\nhello\nyou\n```", + "> Hello\n> you\n\n:boop:\n*hi* _hi_ ~hey~ `up`\n```\nhello\nyou\n```", msg.text ); } @@ -58,7 +58,7 @@ class TestChatMessageBuilder extends utest.Test { final msg = new ChatMessageBuilder(); msg.setBody(Html.fromString("<blockquote>Hello<br>you</blockquote><img alt=':boop:'><br><b>hi</b> <em>hi</em> <s>hey</s> <tt>up</tt><p>a</p><p>b</p><pre>hello<br>you")); Assert.equals( - "> Hello\n> you\n:boop:\n*hi* _hi_ ~hey~ `up`\na\nb\n```\nhello\nyou\n```", + "> Hello\n> you\n\n:boop:\n*hi* _hi_ ~hey~ `up`\na\n\nb\n\n```\nhello\nyou\n```", msg.text ); } diff --git a/test/TestHtml.hx b/test/TestHtml.hx index 9047cfa..837ee4c 100644 --- a/test/TestHtml.hx +++ b/test/TestHtml.hx @@ -47,7 +47,7 @@ class TestHtml extends utest.Test { final participant = new Participant("hatter", null, "", false, [], msg.from, null); Assert.equals( - "<div class=\"action\"><div>hatter says hello</div></div>", + "<div class=\"action\"><p>hatter says hello</p></div>", msg.build().body(participant).toString() ); } diff --git a/test/TestXEP0393.hx b/test/TestXEP0393.hx index fd0e853..0920216 100644 --- a/test/TestXEP0393.hx +++ b/test/TestXEP0393.hx @@ -13,7 +13,7 @@ class TestXEP0393 extends utest.Test { public function testSpansDoNotEscapeBlocks() { Assert.equals( - "<div>There are three blocks in this body, one per line,</div><div>but there is no *formatting</div><div>as spans* may not escape blocks.</div>", + "<p>There are three blocks in this body, one per line,<br/>but there is no *formatting<br/>as spans* may not escape blocks.</p>", toHtml("There are three blocks in this body, one per line, but there is no *formatting as spans* may not escape blocks.") @@ -23,7 +23,7 @@ as spans* may not escape blocks.") public function testPreformattedBlockSimple() { Assert.equals( "<pre>(println \"Hello, world!\") -</pre><div/><div>This should show up as monospace, preformatted text ⤴</div>", +</pre><p>This should show up as monospace, preformatted text ⤴</p>", toHtml("``` (println \"Hello, world!\") ``` @@ -35,7 +35,7 @@ This should show up as monospace, preformatted text ⤴") public function testPreformattedBlock() { Assert.equals( "<pre><code class=\"language-ignored\">(println \"Hello, world!\") -</code></pre><div/><div>This should show up as monospace, preformatted text ⤴</div>", +</code></pre><p>This should show up as monospace, preformatted text ⤴</p>", toHtml("```ignored (println \"Hello, world!\") ``` @@ -47,7 +47,7 @@ This should show up as monospace, preformatted text ⤴") public function testPreformattedBlockUnterminated() { Assert.equals( "<blockquote><pre><code class=\"language-ignored\">(println \"Hello, world!\") -</code></pre></blockquote><div/><div>The entire blockquote is a preformatted text block, but this line</div><div>is plaintext!</div>", +</code></pre></blockquote><p>The entire blockquote is a preformatted text block, but this line<br/>is plaintext!</p>", toHtml("> ```ignored > (println \"Hello, world!\") @@ -58,7 +58,7 @@ is plaintext!") public function testQuotation() { Assert.equals( - "<blockquote><div>That that is, is.</div></blockquote><div/><div>Said the old hermit of Prague.</div>", + "<blockquote><p>That that is, is.</p></blockquote><p>Said the old hermit of Prague.</p>", toHtml("> That that is, is. Said the old hermit of Prague.") @@ -67,7 +67,7 @@ Said the old hermit of Prague.") public function testNestedQuotation() { Assert.equals( - "<blockquote><blockquote><div>That that is, is.</div></blockquote><div>Said the old hermit of Prague.</div></blockquote><div/><div>Who?</div>", + "<blockquote><blockquote><p>That that is, is.</p></blockquote><p>Said the old hermit of Prague.</p></blockquote><p>Who?</p>", toHtml(">> That that is, is. > Said the old hermit of Prague. @@ -75,9 +75,36 @@ Who?") ); } + public function testQuotationAfterPlain() { + Assert.equals( + "<p class=\"tight\">He said:</p><blockquote><p>What is up</p></blockquote>", + toHtml("He said: +> What is up") + ); + } + + public function testCodeAfterPlain() { + Assert.equals( + "<p class=\"tight\">He said:</p><pre>some code +</pre>", + toHtml("He said: +``` +some code") + ); + } + + public function testQuotationAfterPlainPara() { + Assert.equals( + "<p>He said:</p><blockquote><p>What is up</p></blockquote>", + toHtml("He said: + +> What is up") + ); + } + public function testPlainSpan() { Assert.equals( - "<div>plain span</div>", + "<p>plain span</p>", toHtml("plain span") ); } @@ -111,98 +138,98 @@ Who?") public function testStrongSpan() { Assert.equals( - "<div><strong>strong span</strong></div>", + "<p><strong>strong span</strong></p>", toHtml("*strong span*") ); } public function testPlainEmphasisPlain() { Assert.equals( - "<div>plain <em>emphasis</em> plain</div>", + "<p>plain <em>emphasis</em> plain</p>", toHtml("plain _emphasis_ plain") ); } public function testPrePlainStrong() { Assert.equals( - "<div><tt>pre</tt> plain <strong>strong</strong></div>", + "<p><tt>pre</tt> plain <strong>strong</strong></p>", toHtml("`pre` plain *strong*") ); } public function testStrongPlain() { Assert.equals( - "<div><strong>strong</strong>plain*</div>", + "<p><strong>strong</strong>plain*</p>", toHtml("*strong*plain*") ); } public function testPlainStrong() { Assert.equals( - "<div>* plain <strong>strong</strong></div>", + "<p>* plain <strong>strong</strong></p>", toHtml("* plain *strong*") ); } public function testNotStrong1() { Assert.equals( - "<div>not strong*</div>", + "<p>not strong*</p>", toHtml("not strong*") ); } public function testNotStrong2() { Assert.equals( - "<div>*not strong</div>", + "<p>*not strong</p>", toHtml("*not strong") ); } public function testNotStrong3() { Assert.equals( - "<div>*not </div><div> strong</div>", + "<p>*not <br/> strong</p>", toHtml("*not \n strong") ); } public function testNotStrong4() { Assert.equals( - "<div>**</div>", + "<p>**</p>", toHtml("**") ); } public function testNotStrong5() { Assert.equals( - "<div>***</div>", + "<p>***</p>", toHtml("***") ); } public function testNotStrong6() { Assert.equals( - "<div>****</div>", + "<p>****</p>", toHtml("****") ); } public function testStrike() { Assert.equals( - "<div>Everyone <s>dis</s>likes cake.</div>", + "<p>Everyone <s>dis</s>likes cake.</p>", toHtml("Everyone ~dis~likes cake.") ); } public function testThisIsMonospace() { Assert.equals( - "<div>This is <tt>*monospace*</tt></div>", + "<p>This is <tt>*monospace*</tt></p>", toHtml("This is `*monospace*`") ); } public function testThisIsMonospaceAndBold() { Assert.equals( - "<div>This is <strong><tt>monospace and bold</tt></strong></div>", + "<p>This is <strong><tt>monospace and bold</tt></strong></p>", toHtml("This is *`monospace and bold`*") ); } @@ -212,70 +239,112 @@ Who?") public function testAutolink() { Assert.equals( - "<blockquote><div><a href=\"https://example.com\">example.com</a></div></blockquote>", + "<blockquote><p><a href=\"https://example.com\">example.com</a></p></blockquote>", toHtml("> example.com") ); } public function testNoAutolink() { Assert.equals( - "<div><tt>example.com</tt></div>", + "<p><tt>example.com</tt></p>", toHtml("`example.com`") ); } public function testAutolinkXMPP() { Assert.equals( - "<div>hello <a href=\"xmpp:alice@example.com\">xmpp:alice@example.com</a></div>", + "<p>hello <a href=\"xmpp:alice@example.com\">xmpp:alice@example.com</a></p>", toHtml("hello xmpp:alice@example.com") ); } + public function testAutolinkXMPPQueryString() { + Assert.equals( + "<p>hello <a href=\"xmpp:alice@example.com?;a=b\">xmpp:alice@example.com?;a=b</a></p>", + toHtml("hello xmpp:alice@example.com?;a=b") + ); + } + + public function testAutolinkTel() { + Assert.equals( + "<p>hello <a href=\"tel:+15551234567\">tel:+15551234567</a></p>", + toHtml("hello tel:+15551234567") + ); + } + + public function testAutolinkSms() { + Assert.equals( + "<p>hello <a href=\"sms:+15551234567\">sms:+15551234567</a></p>", + toHtml("hello sms:+15551234567") + ); + } + + public function testAutolinkMailto() { + Assert.equals( + "<p>hello <a href=\"mailto:alice@example.com\">mailto:alice@example.com</a></p>", + toHtml("hello mailto:alice@example.com") + ); + } + + public function testAutolinkMailtoQueryString() { + Assert.equals( + "<p>hello <a href=\"mailto:alice@example.com?subject=Hi\">mailto:alice@example.com?subject=Hi</a></p>", + toHtml("hello mailto:alice@example.com?subject=Hi") + ); + } + + public function testAutolinkEmail() { + Assert.equals( + "<p>hello <a href=\"mailto:alice@example.com\">alice@example.com</a></p>", + toHtml("hello alice@example.com") + ); + } + public function testAutolinkAfterEmoji() { Assert.equals( - "<div>📞 icon <a href=\"https://example.com\">example.com</a></div>", + "<p>📞 icon <a href=\"https://example.com\">example.com</a></p>", toHtml("📞 icon example.com") ); } public function testAutolinkNoTrailingSlash() { Assert.equals( - "<div><a href=\"https://example.com/test/\">https://example.com/test/</a> a</div>", + "<p><a href=\"https://example.com/test/\">https://example.com/test/</a> a</p>", toHtml("https://example.com/test/ a") ); } public function testAutolinkBareDomain() { Assert.equals( - "<div><a href=\"https://example.com\">example.com</a></div>", + "<p><a href=\"https://example.com\">example.com</a></p>", toHtml("example.com") ); } public function testAutolinkNoTrailingHash() { Assert.equals( - "<div><a href=\"https://example.com/test#\">https://example.com/test#</a> a</div>", + "<p><a href=\"https://example.com/test#\">https://example.com/test#</a> a</p>", toHtml("https://example.com/test# a") ); } public function testAutolinkTrailingDot() { Assert.equals( - "<div><a href=\"https://example.com/test\">https://example.com/test</a>.</div>", + "<p><a href=\"https://example.com/test\">https://example.com/test</a>.</p>", toHtml("https://example.com/test.") ); } public function testAutolinkTrailingParen() { Assert.equals( - "<div><a href=\"https://example.com/test\">https://example.com/test</a>)</div>", + "<p><a href=\"https://example.com/test\">https://example.com/test</a>)</p>", toHtml("https://example.com/test)") ); } public function testAutolinkBeaks() { Assert.equals( - "<div><<a href=\"https://example.com/test\">https://example.com/test</a>></div>", + "<p><<a href=\"https://example.com/test\">https://example.com/test</a>></p>", toHtml("<https://example.com/test>") ); }