| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2024-10-16 17:53:31 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2024-10-16 17:53:56 UTC |
| parent | 3837fb91d0d3f2f297a4c8106211d2d4051c7217 |
| README.md | +1 | -0 |
| cpp.hxml | +1 | -0 |
| js.hxml | +1 | -0 |
| snikket/ChatMessage.hx | +47 | -1 |
| snikket/Util.hx | +6 | -0 |
| snikket/XEP0393.hx | +73 | -0 |
| test.hxml | +1 | -0 |
| test/TestAll.hx | +4 | -1 |
| test/TestChatMessage.hx | +35 | -0 |
diff --git a/README.md b/README.md index ac28b92..7a53a1c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Working towards simplicity in developing Snikket-compatible apps. haxelib install tink_http haxelib install sha haxelib install thenshim + haxelib install HtmlParser make # JavaScript diff --git a/cpp.hxml b/cpp.hxml index 1b039f6..98ef8d6 100644 --- a/cpp.hxml +++ b/cpp.hxml @@ -4,6 +4,7 @@ --library tink_http --library sha --library thenshim +--library HtmlParser HaxeCBridge snikket.Client diff --git a/js.hxml b/js.hxml index 05d4971..2d892c0 100644 --- a/js.hxml +++ b/js.hxml @@ -4,6 +4,7 @@ --library tink_http --library sha --library thenshim +--library HtmlParser snikket.Client snikket.Push diff --git a/snikket/ChatMessage.hx b/snikket/ChatMessage.hx index e4ec44c..253f918 100644 --- a/snikket/ChatMessage.hx +++ b/snikket/ChatMessage.hx @@ -18,6 +18,8 @@ import snikket.StringUtil; import snikket.XEP0393; import snikket.EmojiUtil; import snikket.Message; +import snikket.Stanza; +import snikket.Util; @:expose @:nullSafety(Strict) @@ -133,7 +135,7 @@ class ChatMessage { **/ @:allow(snikket) public var versions (default, null): Array<ChatMessage> = []; - @:allow(snikket) + @:allow(snikket, test) private var payloads: Array<Stanza> = []; /** @@ -265,6 +267,50 @@ class ChatMessage { return payloads.find((p) -> p.attr.get("xmlns") == "urn:xmpp:styling:0" && p.name == "unstyled") == null ? XEP0393.parse(body).map((s) -> s.toString()).join("") : StringTools.htmlEscape(body); } + /** + Set rich text using an HTML string + Also sets the plain text body appropriately + **/ + public function setHtml(html: String) { + final htmlEl = new Stanza("html", { xmlns: "http://jabber.org/protocol/xhtml-im" }); + final body = new Stanza("body", { xmlns: "http://www.w3.org/1999/xhtml" }); + htmlEl.addChild(body); + final nodes = htmlparser.HtmlParser.run(html, true); + for (node in nodes) { + final el = Util.downcast(node, htmlparser.HtmlNodeElement); + if (el != null && (el.name == "html" || el.name == "body")) { + for (inner in el.nodes) { + body.addDirectChild(htmlToNode(inner)); + } + } else { + body.addDirectChild(htmlToNode(node)); + } + } + final htmlIdx = payloads.findIndex((p) -> p.attr.get("xmlns") == "http://jabber.org/protocol/xhtml-im" && p.name == "html"); + if (htmlIdx >= 0) payloads.splice(htmlIdx, 1); + payloads.push(htmlEl); + text = XEP0393.render(body); + } + + private function htmlToNode(node: htmlparser.HtmlNode) { + final txt = Util.downcast(node, htmlparser.HtmlNodeText); + if (txt != null) { + return CData(new TextNode(txt.toText())); + } + final el = Util.downcast(node, htmlparser.HtmlNodeElement); + if (el != null) { + final s = new Stanza(el.name, {}); + for (attr in el.attributes) { + s.attr.set(attr.name, attr.value); + } + for (child in el.nodes) { + s.addDirectChild(htmlToNode(child)); + } + return Element(s); + } + throw "node was neither text nor element?"; + } + /** The ID of the Chat this message is associated with **/ diff --git a/snikket/Util.hx b/snikket/Util.hx new file mode 100644 index 0000000..9f18718 --- /dev/null +++ b/snikket/Util.hx @@ -0,0 +1,6 @@ +package snikket; + +// Std.downcast doesn't play well with null safety +function downcast<T, S>(value: T, c: Class<S>): Null<S> { + return cast Std.downcast(cast value, cast c); +} diff --git a/snikket/XEP0393.hx b/snikket/XEP0393.hx index ebf9133..a2b923b 100644 --- a/snikket/XEP0393.hx +++ b/snikket/XEP0393.hx @@ -14,6 +14,79 @@ class XEP0393 { return blocks; } + public static function render(xhtml: Stanza) { + if (xhtml.name == "br") { + return "\n"; + } + + if (xhtml.name == "img") { + return xhtml.attr.get("alt") ?? ""; + } + + final s = new StringBuf(); + + if (xhtml.name == "pre") { + s.add("\n```\n"); + } + + if (xhtml.name == "b" || xhtml.name == "strong") { + s.add("*"); + } + + if (xhtml.name == "i" || xhtml.name == "em") { + s.add("_"); + } + + if (xhtml.name == "s" || xhtml.name == "del") { + s.add("~"); + } + + if (xhtml.name == "tt") { + s.add("`"); + } + + for (child in xhtml.children) { + s.add(renderNode(child)); + } + + if (xhtml.name == "b" || xhtml.name == "strong") { + s.add("*"); + } + + if (xhtml.name == "i" || xhtml.name == "em") { + s.add("_"); + } + + if (xhtml.name == "s" || xhtml.name == "del") { + s.add("~"); + } + + if (xhtml.name == "tt") { + s.add("`"); + } + + if (xhtml.name == "blockquote" || xhtml.name == "p" || xhtml.name == "div" || xhtml.name == "pre") { + s.add("\n"); + } + + if (xhtml.name == "pre") { + s.add("```\n"); + } + + if (xhtml.name == "blockquote") { + return ~/^/gm.replace(s.toString(), "> "); + } + + return s.toString(); + } + + public static function renderNode(xhtml: Node) { + return switch (xhtml) { + case Element(c): render(c); + case CData(c): c.content; + }; + } + public static function parseSpans(styled: String) { final spans = []; var start = 0; diff --git a/test.hxml b/test.hxml index 926f792..04efff2 100644 --- a/test.hxml +++ b/test.hxml @@ -1,6 +1,7 @@ --library haxe-strings --library hsluv --library utest +--library HtmlParser --run test.TestAll diff --git a/test/TestAll.hx b/test/TestAll.hx index f8b324b..2f215fa 100644 --- a/test/TestAll.hx +++ b/test/TestAll.hx @@ -5,6 +5,9 @@ import utest.ui.Report; class TestAll { public static function main() { - utest.UTest.run([new TestSessionDescription()]); + utest.UTest.run([ + new TestSessionDescription(), + new TestChatMessage() + ]); } } diff --git a/test/TestChatMessage.hx b/test/TestChatMessage.hx new file mode 100644 index 0000000..904dc09 --- /dev/null +++ b/test/TestChatMessage.hx @@ -0,0 +1,35 @@ +package test; + +import utest.Assert; +import utest.Async; +import snikket.ChatMessage; + +class TestChatMessage extends utest.Test { + public function testConvertHtmlToXHTML() { + final msg = new ChatMessage(); + msg.setHtml("Hello <div><img src='hai'><br>"); + Assert.equals( + "<html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">Hello <div><img src=\"hai\"/><br/></div></body></html>", + msg.payloads[0].toString() + ); + } + + public function testConvertHtmlToText() { + final msg = new ChatMessage(); + msg.setHtml("<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```\n", + msg.text + ); + } + + public function testConvertHtmlToXHTMLIgnoresBody() { + final msg = new ChatMessage(); + msg.setHtml("<body>Hello <div><img src='hai'><br></body>"); + Assert.equals( + "<html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">Hello <div><img src=\"hai\"/><br/></div></body></html>", + msg.payloads[0].toString() + ); + } + +}