From 22371f8156a9a60ab8c36c4233186f6d06f5a21b Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 18 Sep 2026 18:26:15 -0400 Subject: [PATCH] fix(composer): no LaTeX conversion inside a typed markdown fence or backtick span (#184 O3) In markdown mode each paragraph line is serialised before parseBlockMD joins them, so $x$ inside a ``` fence became data-mx-maths markup inside the resulting
 (rendered as math in a code block).
Track fence state across lines and skip math for fenced lines and for
backtick code spans. Unit tests added; verified in the browser.

Also: scripts/dev-homeserver.sh enables MSC4140 delayed events so
scheduled messages work locally.

Co-Authored-By: Claude Opus 5 
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
---
 scripts/dev-homeserver.sh                |  4 ++++
 src/app/components/editor/output.test.ts | 24 ++++++++++++++++++++++++
 src/app/components/editor/output.ts      | 20 ++++++++++++++++++++
 3 files changed, 48 insertions(+)

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>');