From be5eebd3d4bb36b33534e5bf691cc05030f203e8 Mon Sep 17 00:00:00 2001 From: Jakub Rusz Date: Mon, 6 Apr 2026 15:39:39 +0200 Subject: [PATCH] fix(matrix): compact loose list HTML for consistent Element rendering Loose lists (blank lines between items) produce
  • ...

  • via markdown-it, causing Element to render list numbers on separate lines from their content. Fix by setting hidden=true on paragraph tokens inside list items before rendering, mirroring what markdown-it already does for tight lists. Closes #60997. Thanks @gucasbrg. Co-Authored-By: Claude claude-opus-4-6 Signed-off-by: Jakub Rusz --- extensions/matrix/src/matrix/format.test.ts | 49 +++++++++++++++++++++ extensions/matrix/src/matrix/format.ts | 24 +++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/extensions/matrix/src/matrix/format.test.ts b/extensions/matrix/src/matrix/format.test.ts index bad6ecbbd2d..02c2ae9ed02 100644 --- a/extensions/matrix/src/matrix/format.test.ts +++ b/extensions/matrix/src/matrix/format.test.ts @@ -50,6 +50,55 @@ describe("markdownToMatrixHtml", () => { expect(html).toContain(" { + const html = markdownToMatrixHtml("1. first\n\n2. second\n\n3. third"); + expect(html).toContain("
      "); + expect(html).toContain("
    1. "); + expect(html).not.toContain("

      "); + }); + + it("compacts loose unordered lists without paragraph tags", () => { + const html = markdownToMatrixHtml("- one\n\n- two\n\n- three"); + expect(html).toContain("

        "); + expect(html).not.toContain("

        "); + }); + + it("keeps tight lists unchanged", () => { + const html = markdownToMatrixHtml("- one\n- two"); + expect(html).toContain("

          "); + expect(html).not.toContain("

          "); + }); + + it("preserves inline formatting in loose lists", () => { + const html = markdownToMatrixHtml("1. **bold**\n\n2. _italic_"); + expect(html).toContain("bold"); + expect(html).toContain("italic"); + expect(html).not.toContain("

          "); + }); + + it("does not strip paragraph tags outside lists", () => { + const html = markdownToMatrixHtml("Hello\n\nWorld"); + expect(html).toContain("

          Hello

          "); + expect(html).toContain("

          World

          "); + }); + + it("compacts nested sublists without paragraph tags", () => { + const html = markdownToMatrixHtml("1. parent\n\n - child\n\n2. other"); + expect(html).toContain("
            "); + expect(html).toContain("
              "); + expect(html).not.toContain("

              "); + }); + + it("compacts loose lists with mentions via renderMarkdownToMatrixHtmlWithMentions", async () => { + const result = await renderMarkdownToMatrixHtmlWithMentions({ + markdown: "1. hello @alice:example.org\n\n2. bye", + client: createMentionClient(), + }); + expect(result.html).not.toContain("

              "); + expect(result.html).toContain('href="https://matrix.to/#/%40alice%3Aexample.org"'); + expect(result.mentions).toEqual({ user_ids: ["@alice:example.org"] }); + }); + it("renders qualified Matrix user mentions as matrix.to links and m.mentions metadata", async () => { const result = await renderMarkdownToMatrixHtmlWithMentions({ markdown: "hello @alice:example.org", diff --git a/extensions/matrix/src/matrix/format.ts b/extensions/matrix/src/matrix/format.ts index 5d640f13995..b134a4526dc 100644 --- a/extensions/matrix/src/matrix/format.ts +++ b/extensions/matrix/src/matrix/format.ts @@ -309,9 +309,28 @@ function mutateInlineTokensWithMentions(params: { return { children: nextChildren, roomMentioned }; } +// Compact loose lists by hiding paragraph tokens inside list items, +// mirroring what markdown-it already does for tight lists. Without this +// Element renders

              margins inside

            • , splitting numbers from content. +function compactLooseListTokens(tokens: MarkdownToken[]): void { + let insideListItem = 0; + for (const token of tokens) { + if (token.type === "list_item_open") { + insideListItem++; + } else if (token.type === "list_item_close") { + insideListItem--; + } else if (insideListItem > 0) { + if (token.type === "paragraph_open" || token.type === "paragraph_close") { + token.hidden = true; + } + } + } +} + export function markdownToMatrixHtml(markdown: string): string { - const rendered = md.render(markdown ?? ""); - return rendered.trimEnd(); + const tokens = md.parse(markdown ?? "", {}); + compactLooseListTokens(tokens); + return md.renderer.render(tokens, md.options, {}).trimEnd(); } async function resolveMarkdownMentionState(params: { @@ -366,6 +385,7 @@ export async function renderMarkdownToMatrixHtmlWithMentions(params: { client: MatrixClient; }): Promise<{ html?: string; mentions: MatrixMentions }> { const state = await resolveMarkdownMentionState(params); + compactLooseListTokens(state.tokens); const html = md.renderer.render(state.tokens, md.options, {}).trimEnd(); return { html: html || undefined,