Repository navigation
Description
Bug Description
Hey, hope you're doing well! It's me again from #1147. Thanks so much for the quick fix in #1156, that was really fast and it works great on Discord, Slack and Google Chat.
While trying a few more adapters I think I noticed a small sibling of that issue: on X and Twitch, a block quote with more than one paragraph comes out with the paragraphs glued together, like First.Second.
For context, XFormatConverter and TwitchFormatConverter turn the markdown a bot posts into plain text for those platforms. Their nodeToText has no branch for blockquote, so the quote falls through to BaseFormatConverter.defaultNodeToText, which joins the child blocks with an empty string.
XFormatConverter.nodeToText on main has no blockquote branch, so line 74 passes the quote to BaseFormatConverter.defaultNodeToText, which joins the children with "" at line 857.
flowchart LR
A["> First.<br>><br>> Second."] --> B["X / Twitch nodeToText"]
B -- "no blockquote branch" --> C["defaultNodeToText<br>children.join('')"]
C --> D["First.Second."]
Steps to Reproduce
- Create
new XFormatConverter()(ornew TwitchFormatConverter()). - Call
fromMarkdown("> First.\n>\n> Second."). - The quote has two paragraphs, so the parser gives a
blockquotenode with twoparagraphchildren. nodeToTexthas noblockquotebranch, so it callsdefaultNodeToText, which runschildren.map(nodeConverter).join("").- The output is
First.Second.on both X and Twitch.
The same happens with a list inside a quote: "> Intro:\n>\n> - one\n> - two" gives Intro:• one\n• two on X and Intro:• one • two on Twitch.
Expected Behavior
The paragraphs stay apart, the same way they do without a quote ("First.\n\nSecond." gives First.\n\nSecond. on X and First. Second. on Twitch).
- X:
First.\n\nSecond.(or with a>prefix on each line, if you'd like X to show the quote) - Twitch:
First. Second., since Twitch flattens everything to one line
Actual Behavior
- X:
First.Second.andIntro:• one\n• two - Twitch:
First.Second.andIntro:• one • two
Code Sample
import { XFormatConverter } from "@chat-adapter/x";
import { TwitchFormatConverter } from "@chat-adapter/twitch";
const md = "> First.\n>\n> Second.";
console.log(JSON.stringify(new XFormatConverter().fromMarkdown(md)));
// "First.Second."
console.log(JSON.stringify(new TwitchFormatConverter().fromMarkdown(md)));
// "First.Second."Chat SDK Version
4.45.0 (main at 990411e)
Platform Adapter
Other (X, Twitch)
Additional Context
The X converter, where blockquote reaches the fallback:
chat/packages/adapter-x/src/markdown.ts
Lines 49 to 75 in 990411e
| protected nodeToText(node: Content): string { | |
| if (isTextNode(node)) { | |
| return getNodeValue(node); | |
| } | |
| if (isLinkNode(node)) { | |
| const label = getNodeChildren(node) | |
| .map((child) => this.nodeToText(child)) | |
| .join(""); | |
| return label && label !== node.url ? `${label} (${node.url})` : node.url; | |
| } | |
| if (isCodeNode(node)) { | |
| return getNodeValue(node); | |
| } | |
| if (isListNode(node)) { | |
| return this.renderList(node, 0, (child) => this.nodeToText(child), "•"); | |
| } | |
| if (isTableNode(node)) { | |
| return tableToAscii(node); | |
| } | |
| if (node.type === "break") { | |
| return "\n"; | |
| } | |
| if (node.type === "thematicBreak") { | |
| return "---"; | |
| } | |
| return this.defaultNodeToText(node, (child) => this.nodeToText(child)); | |
| } |
The Twitch converter, same spot:
chat/packages/adapter-twitch/src/markdown.ts
Lines 50 to 80 in 990411e
| protected nodeToText(node: Content): string { | |
| if (isTextNode(node)) { | |
| return getNodeValue(node); | |
| } | |
| if (isLinkNode(node)) { | |
| const label = getNodeChildren(node) | |
| .map((child) => this.nodeToText(child)) | |
| .join(""); | |
| return label && label !== node.url ? `${label} (${node.url})` : node.url; | |
| } | |
| if (isCodeNode(node)) { | |
| return getNodeValue(node); | |
| } | |
| if (isListNode(node)) { | |
| return this.renderList(node, 0, (child) => this.nodeToText(child), "•"); | |
| } | |
| if (isTableNode(node)) { | |
| const [headerRow, ...bodyRows] = getNodeChildren(node); | |
| return tableToTwitchText( | |
| headerRow ? this.rowToCells(headerRow) : [], | |
| bodyRows.map((row) => this.rowToCells(row)) | |
| ); | |
| } | |
| if (node.type === "break") { | |
| return "\n"; | |
| } | |
| if (node.type === "thematicBreak") { | |
| return ""; | |
| } | |
| return this.defaultNodeToText(node, (child) => this.nodeToText(child)); | |
| } |
The fallback that joins child blocks with "":
chat/packages/chat/src/markdown.ts
Lines 851 to 860 in 990411e
| protected defaultNodeToText( | |
| node: Content, | |
| nodeConverter: (node: Content) => string | |
| ): string { | |
| const children = getNodeChildren(node); | |
| if (children.length > 0) { | |
| return children.map(nodeConverter).join(""); | |
| } | |
| return getNodeValue(node); | |
| } |
From what I could see, Gmail, Messenger, Twilio, WhatsApp and Instagram go through stringify or toPlainText, so I think only X and Twitch are affected.
I wasn't sure which fix you'd prefer, so I wanted to ask first:
- For X, would you rather reuse
renderBlockquotefrom fix: quote every line of Discord, Slack, and Google Chat block quotes #1156 with a"> "prefix, or just join the quote's paragraphs with a blank line like normal text? - For Twitch, would a plain space join feel right, since
toSingleLineflattens it anyway?
Happy to put up a PR for whichever you like. Thanks again for all your work on this!

Activity