This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new be874da37 fix(ai): preserve fenced code during Markdown normalization 
(#6449)
be874da37 is described below

commit be874da37e101af1db604631cfedfe6c131eb5eb
Author: Loyal-Young <[email protected]>
AuthorDate: Thu Oct 15 20:27:47 2026 +0800

    fix(ai): preserve fenced code during Markdown normalization (#6449)
    
    * fix(ai): preserve fenced code during Markdown normalization
    
    * fix(ai): drop the lookbehind assertion from Markdown line splitting
    
    esbuild cannot lower a lookbehind assertion, so targeting the Vite default
    build set turned `/(?<=\n)/` into `new RegExp("(?<=\\n)")`, which still 
throws
    on Safari versions older than 16.4. Splitting on the newline and rejoining
    with it produces byte-identical output, including for CRLF input where the
    `\r` stays at the end of the preceding line.
    
    ---------
    
    Co-authored-by: lizhimins <[email protected]>
---
 .../ai/render/__tests__/markdownFences.test.ts     | 53 ++++++++++++++++++++++
 web/src/pages/ai/render/markdown.ts                | 29 ++++++++++--
 2 files changed, 79 insertions(+), 3 deletions(-)

diff --git a/web/src/pages/ai/render/__tests__/markdownFences.test.ts 
b/web/src/pages/ai/render/__tests__/markdownFences.test.ts
new file mode 100644
index 000000000..38d396af2
--- /dev/null
+++ b/web/src/pages/ai/render/__tests__/markdownFences.test.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it } from 'vitest';
+import { normalizeAiMarkdown } from '../markdown';
+
+describe('Markdown fenced code preservation', () => {
+  it.each(['```', '~~~', '````', '~~~~'])('preservesCodeBodyTest: %s', (fence) 
=> {
+    const input = 
`${fence}diff\n-removed\n+added\n#comment\n**literal**\n${fence}\n`;
+    expect(normalizeAiMarkdown(input)).toBe(input);
+  });
+
+  it.each(['\n', '\r\n'])('preservesUnclosedStreamingFenceTest: %j', (eol) => {
+    const input = ['```sh', '#!/bin/sh', '-1', '+2'].join(eol);
+    expect(normalizeAiMarkdown(input)).toBe(input);
+  });
+
+  it('shortOrDifferentFenceCannotEndCodeTest', () => {
+    const input = '````text\n```\n#inside\n~~~\n+inside\n````\n##结论';
+    expect(normalizeAiMarkdown(input)).toBe(input.replace('##结论', '## 结论'));
+  });
+
+  it('fenceWithTrailingTextCannotEndCodeTest', () => {
+    const input = '```text\n```not-a-close\n-unchanged\n```';
+    expect(normalizeAiMarkdown(input)).toBe(input);
+  });
+
+  it('allowsIndentedOpeningAndLongerClosingFenceTest', () => {
+    const input = '  ~~~text\n#inside\n  ~~~~  \n-项目';
+    expect(normalizeAiMarkdown(input)).toBe(input.replace('-项目', '- 项目'));
+  });
+
+  it('repairsProseAndMalformedOpeningWhilePreservingCodeTest', () => {
+    const input = '##结论\n-项目\n```bashprintf ok\n#comment\n```\n+下一项';
+    expect(normalizeAiMarkdown(input)).toBe(
+      '## 结论\n- 项目\n```bash\nprintf ok\n#comment\n```\n+ 下一项',
+    );
+  });
+});
diff --git a/web/src/pages/ai/render/markdown.ts 
b/web/src/pages/ai/render/markdown.ts
index 65cd4f517..d509bfb39 100644
--- a/web/src/pages/ai/render/markdown.ts
+++ b/web/src/pages/ai/render/markdown.ts
@@ -28,8 +28,31 @@
  * every assistant text block, live or replayed.
  */
 export function normalizeAiMarkdown(content: string): string {
+  let fence: { marker: string; length: number } | null = null;
+  // Split on the newline itself rather than a lookbehind (`/(?<=\n)/`): 
esbuild cannot downlevel
+  // lookbehind assertions, and the Vite default build target still covers 
Safari versions that
+  // throw a SyntaxError while merely parsing such a literal.
   return content
-    .replace(/^(#{1,6})(?=\S)/gm, '$1 ')
-    .replace(/^([-+*])(?=\S)/gm, '$1 ')
-    .replace(/^```(bash|sh|shell|json|ya?ml|sql|text)(?=\S)/gim, '```$1\n');
+    .split('\n')
+    .map((line) => {
+      const marker = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)/.exec(line);
+      if (fence) {
+        if (
+          marker &&
+          marker[1][0] === fence.marker &&
+          marker[1].length >= fence.length &&
+          !marker[2].trim()
+        ) {
+          fence = null;
+        }
+        return line;
+      }
+      // A backtick fence cannot contain backticks in its info string. Tilde 
fences can.
+      if (marker && (marker[1][0] === '~' || !marker[2].includes('`'))) {
+        fence = { marker: marker[1][0], length: marker[1].length };
+        return line.replace(/^```(bash|sh|shell|json|ya?ml|sql|text)(?=\S)/i, 
'```$1\n');
+      }
+      return line.replace(/^(#{1,6})(?=\S)/, '$1 ').replace(/^([-+*])(?=\S)/, 
'$1 ');
+    })
+    .join('\n');
 }

Reply via email to