diff --git a/scripts/dev-homeserver.sh b/scripts/dev-homeserver.sh index 31903b319..ed9cbf80b 100755 --- a/scripts/dev-homeserver.sh +++ b/scripts/dev-homeserver.sh @@ -43,6 +43,10 @@ rc_joins: { local: { per_second: 1000, burst_count: 10000 }, remote: { per_secon rc_presence: { per_user: { per_second: 1000, burst_count: 10000 } } max_upload_size: 50M suppress_key_server_warning: true +# scheduled messages (MSC4140 delayed events) +max_event_delay_duration: 24h +experimental_features: + msc4140_enabled: true """ open(p, "w").write(s) PY diff --git a/src/app/components/editor/output.test.ts b/src/app/components/editor/output.test.ts index 0b1138244..3f9c2ca87 100644 --- a/src/app/components/editor/output.test.ts +++ b/src/app/components/editor/output.test.ts @@ -65,3 +65,27 @@ test('no math conversion inside a code block', () => { test('a message with no math is unchanged', () => { assert.equal(toMatrixCustomHTML(txt('just hello'), OPTS), 'just hello'); }); + +// Gitea #184 O3 — markdown mode: fences and backtick spans typed as plain +// paragraphs are literal too (the block markdown parser only sees the fence +// after the lines are joined, so math must be skipped while serialising them). +const MD_OPTS = { ...OPTS, allowInlineMarkdown: true, allowBlockMarkdown: true }; + +test('markdown: no math conversion inside a typed ``` fence', () => { + const paragraphs = [ + el(BlockType.Paragraph, [txt('```')]), + el(BlockType.Paragraph, [txt('$x$ literal')]), + el(BlockType.Paragraph, [txt('```')]), + el(BlockType.Paragraph, [txt('after $y$')]), + ]; + const out = toMatrixCustomHTML(paragraphs, MD_OPTS); + assert.ok(out.includes(' { + const out = toMatrixCustomHTML(el(BlockType.Paragraph, [txt('use `$x$` and $y$')]), MD_OPTS); + assert.ok(/]*>\$x\$<\/code>/.test(out), 'backtick span stays literal'); + assert.ok(out.includes('data-mx-maths="y"')); +}); diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index 1b98bbd58..07ba8c514 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -36,6 +36,18 @@ const textToCustomHtml = (node: Text, opts: OutputOptions): string => { // applied inside inline code. Non-math text recurses with allowMath off so it // still gets the normal marks + inline-markdown treatment. if (opts.allowMath && !node.code) { + // Markdown inline code spans (`…`) are literal too: apply math only to the + // text between them (Gitea #184 O3 — `$x$` inside backticks stayed math). + if (opts.allowInlineMarkdown && /`[^`]*`/.test(node.text)) { + return node.text + .split(/(`+[^`]*`+)/) + .map((part) => + part.startsWith('`') + ? textToCustomHtml({ ...node, text: part }, { ...opts, allowMath: false }) + : textToCustomHtml({ ...node, text: part }, opts), + ) + .join(''); + } const segments = splitMathSegments(node.text); if (segments.some((seg) => seg.type !== 'text')) { return segments @@ -128,12 +140,20 @@ export const toMatrixCustomHTML = ( opts: OutputOptions, ): string => { let markdownLines = ''; + // Inside a markdown ``` fence every line is literal: no `$…$` math conversion + // (the fence is only recognised by parseBlockMD after the lines are joined, + // which is too late — Gitea #184 O3). + let inFence = false; const parseNode = (n: Descendant, index: number, targetNodes: Descendant[]) => { if (opts.allowBlockMarkdown && 'type' in n && n.type === BlockType.Paragraph) { + const isFenceLine = /^\s*```/.test(toPlainText(n, false)); + const literal = inFence || isFenceLine; + if (isFenceLine) inFence = !inFence; const line = toMatrixCustomHTML(n, { ...opts, allowInlineMarkdown: false, allowBlockMarkdown: false, + allowMath: opts.allowMath && !literal, }) .replace(/$/, '\n') .replace(/^(\\*)>/, '$1>');