git » sdk » commit 335baf4

Improve whitespace for XEP0393 render

author Stephen Paul Weber
2026-04-12 02:20:32 UTC
committer Stephen Paul Weber
2026-04-12 02:20:32 UTC
parent 00f2c43268d9838c869e02b5895ac7eef04af206

Improve whitespace for XEP0393 render

borogove/XEP0393.hx +26 -7
test/TestChatMessage.hx +36 -0
test/TestChatMessageBuilder.hx +9 -0

diff --git a/borogove/XEP0393.hx b/borogove/XEP0393.hx
index b97c7cf..fe2714a 100644
--- a/borogove/XEP0393.hx
+++ b/borogove/XEP0393.hx
@@ -16,7 +16,7 @@ class XEP0393 {
 		return blocks;
 	}
 
-	public static function render(xhtml: Stanza, inPre = false) {
+	public static function render(xhtml: Stanza, inPre = false, followNewline = true) {
 		if (xhtml.name == "br") {
 			return "\n";
 		}
@@ -26,61 +26,80 @@ class XEP0393 {
 		}
 
 		final s = new StringBuf();
+		var endsWithNewline = true;
+
+		if (!followNewline && ["blockquote", "pre", "div", "p"].contains(xhtml.name)) {
+			s.add("\n");
+			endsWithNewline = true;
+		}
 
 		if (xhtml.name == "pre") {
 			final code = xhtml.getChild("code");
 			var lang = "";
-			if (code != null) {
+			if (code != null && xhtml.children.length == 1) {
 				final className = code.attr.get("class") ?? "";
 				if (className.startsWith("language-")) {
 					lang = className.substr(9);
 				}
 			}
-			s.add("\n```" + lang + "\n");
+			s.add("```" + lang + "\n");
+			endsWithNewline = true;
 		}
 
 		if (xhtml.name == "b" || xhtml.name == "strong") {
 			s.add("*");
+			endsWithNewline = false;
 		}
 
 		if (xhtml.name == "i" || xhtml.name == "em") {
 			s.add("_");
+			endsWithNewline = false;
 		}
 
 		if (xhtml.name == "s" || xhtml.name == "del") {
 			s.add("~");
+			endsWithNewline = false;
 		}
 
 		if (!inPre && (xhtml.name == "tt" || xhtml.name == "code")) {
 			s.add("`");
+			endsWithNewline = false;
 		}
 
 		for (child in xhtml.children) {
-			s.add(renderNode(child, xhtml.name == "pre"));
+			final rendered = renderNode(child, xhtml.name == "pre", endsWithNewline);
+			s.add(rendered);
+			endsWithNewline = rendered.endsWith("\n");
 		}
 
 		if (xhtml.name == "b" || xhtml.name == "strong") {
 			s.add("*");
+			endsWithNewline = false;
 		}
 
 		if (xhtml.name == "i" || xhtml.name == "em") {
 			s.add("_");
+			endsWithNewline = false;
 		}
 
 		if (xhtml.name == "s" || xhtml.name == "del") {
 			s.add("~");
+			endsWithNewline = false;
 		}
 
 		if (!inPre && (xhtml.name == "tt" || xhtml.name == "code")) {
 			s.add("`");
+			endsWithNewline = false;
 		}
 
-		if (xhtml.name == "blockquote" || xhtml.name == "p" || xhtml.name == "div" || xhtml.name == "pre") {
+		if (!endsWithNewline && ["blockquote", "pre", "div", "p"].contains(xhtml.name)) {
 			s.add("\n");
+			endsWithNewline = true;
 		}
 
 		if (xhtml.name == "pre") {
 			s.add("```\n");
+			endsWithNewline = true;
 		}
 
 		if (xhtml.name == "blockquote") {
@@ -90,9 +109,9 @@ class XEP0393 {
 		return s.toString();
 	}
 
-	public static function renderNode(xhtml: Node, inPre = false) {
+	public static function renderNode(xhtml: Node, inPre = false, followNewline = true) {
 		return switch (xhtml) {
-			case Element(c): render(c, inPre);
+			case Element(c): render(c, inPre, followNewline);
 			case CData(c): c.content;
 		};
 	}
diff --git a/test/TestChatMessage.hx b/test/TestChatMessage.hx
index bd6238f..cf38366 100644
--- a/test/TestChatMessage.hx
+++ b/test/TestChatMessage.hx
@@ -44,4 +44,40 @@ class TestChatMessage extends utest.Test {
 				Assert.fail("Expected ChatMessageStanza");
 		}
 	}
+
+	public function testStyledBodyWithCodeBlock() {
+		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("```javascript\nlet hello;\n```"));
+
+		final msg = Message.fromStanza(stanza, JID.parse("bob@example.com"));
+		switch (msg.parsed) {
+			case ChatMessageStanza(m):
+				Assert.equals("<pre><code class=\"language-javascript\">let hello;\n</code></pre>", m.body().toString());
+				Assert.equals("```javascript\nlet hello;\n```", m.body().toPlainText());
+			default:
+				Assert.fail("Expected ChatMessageStanza");
+		}
+	}
+
+	public function testStyledBodyWithPreBlock() {
+		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("```\nlet hello;"));
+
+		final msg = Message.fromStanza(stanza, JID.parse("bob@example.com"));
+		switch (msg.parsed) {
+			case ChatMessageStanza(m):
+				Assert.equals("<pre>let hello;\n</pre>", m.body().toString());
+				Assert.equals("```\nlet hello;\n```", m.body().toPlainText());
+			default:
+				Assert.fail("Expected ChatMessageStanza");
+		}
+	}
 }
diff --git a/test/TestChatMessageBuilder.hx b/test/TestChatMessageBuilder.hx
index 9da21d1..36793f9 100644
--- a/test/TestChatMessageBuilder.hx
+++ b/test/TestChatMessageBuilder.hx
@@ -26,6 +26,15 @@ class TestChatMessageBuilder extends utest.Test {
 		);
 	}
 
+	public function testConvertHtmlToTextWithParas() {
+		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```",
+			msg.text
+		);
+	}
+
 	public function testConvertHtmlToXHTMLIgnoresBody() {
 		final msg = new ChatMessageBuilder();
 		msg.setBody(Html.fromString("<body>Hello <div><img src='hai'><br></body>"));