fix: preserve markdown formatting around media attachments (#116786)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-07-31 04:20:39 -07:00
committed by GitHub
parent 82231b425b
commit b4e10e8a90
8 changed files with 300 additions and 32 deletions

View File

@@ -205,6 +205,22 @@ describe("parseInlineDirectives", () => {
expect(result.text).toBe(["~~~python", " x = 1", " y = 2", "~~~"].join("\n"));
});
test.each([
[
"a false closing marker",
["```python", "value = 'a b'", "``` not a close", "x = 'c d'", "```"],
],
["an unclosed fence", ["```python", "value = 'a b'", "x = 'c d'"]],
["an indented closing fence", ["```python", "value = 'a b'", " ```"]],
])("preserves canonical code fences with %s after removing directives", (_name, lines) => {
const code = lines.join("\n");
const result = parseInlineDirectives(`[[reply_to_current]]\n[[audio_as_voice]]\n${code}`);
expect(result.hasReplyTag).toBe(true);
expect(result.audioAsVoice).toBe(true);
expect(result.text).toBe(code);
});
test("normalizes plain text without directives using code-fence awareness", () => {
const input = "plain text with extra spaces\n\n```\n code preserved\n```";
const result = parseInlineDirectives(input);

View File

@@ -1,6 +1,7 @@
import { expectDefined } from "@openclaw/normalization-core";
// Directive tag helpers parse inline directive tags from user text.
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { parseFenceSpans } from "../../packages/markdown-core/src/fences.js";
export type InlineDirectiveParseResult = {
text: string;
@@ -47,13 +48,19 @@ function normalizeDirectiveWhitespace(text: string): string {
const blockSentinel = createBlockSentinel(text);
const blockPlaceholderRe = new RegExp(`${blockSentinel}(\\d+)${blockSentinel}`, "g");
const blocks: string[] = [];
const masked = text.replace(
/(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n\1[^\n]*|(?:(?:^|\n)(?: |\t)[^\n]*)+/gm,
(block) => {
blocks.push(block);
return `${blockSentinel}${blocks.length - 1}${blockSentinel}`;
},
);
const fenceSpans = text.includes("```") || text.includes("~~~") ? parseFenceSpans(text) : [];
let masked = "";
let cursor = 0;
// The canonical scanner keeps false closers, indented closers, and open fences intact.
for (const span of fenceSpans) {
blocks.push(text.slice(span.start, span.end));
masked += `${text.slice(cursor, span.start)}${blockSentinel}${blocks.length - 1}${blockSentinel}`;
cursor = span.end;
}
masked = `${masked}${text.slice(cursor)}`.replace(/(?:(?:^|\n)(?: |\t)[^\n]*)+/gm, (block) => {
blocks.push(block);
return `${blockSentinel}${blocks.length - 1}${blockSentinel}`;
});
const normalized = masked
.replace(/\r\n/g, "\n")