|
1 | 1 | const HIDDEN_INLINE_REFERENCE_PATTERN = |
2 | 2 | /`[^`\n]*(?:internal\/tool-results\/|internal\/blocktips\/|components\/integrations\/[^`\n]*README)[^`\n]*`/g |
3 | 3 |
|
4 | | -/** JSON strings own their escaped quotes, backticks, and any quoted tag markers. */ |
5 | | -const JSON_STRING_SOURCE = String.raw`"(?:[^"\\\r\n]|\\[^\r\n])*"` |
| 4 | +/** JSON strings own their escaped quotes, backticks, and quoted tag markers. */ |
| 5 | +const JSON_STRING_SOURCE = '"(?:\\\\(?:["\\\\/bfnrt]|u[0-9a-fA-F]{4})|[^"\\\\\\r\\n])*"' |
6 | 6 |
|
7 | | -/** |
8 | | - * Complete chip tags consume JSON strings atomically. Outside strings, a new |
9 | | - * opener or backtick ends the candidate, so prose mentions cannot join into a |
10 | | - * tag and repeated unclosed openers cannot repeatedly scan the same suffix. |
11 | | - */ |
12 | | -const COMPLETE_TAG_SOURCE = `<(?<chipTag>workspace_resource|source)>\\s*\\{(?:${JSON_STRING_SOURCE}|[^"\`<])*?\\}\\s*</\\k<chipTag>>` |
| 7 | +/** Unquoted openers, backticks, and invalid backslashes bound failed payload scans. */ |
| 8 | +const COMPLETE_TAG_SOURCE = `<(?<chipTag>workspace_resource|source)>\\s*\\{(?:${JSON_STRING_SOURCE}|[^"\`<\\\\])*?\\}\\s*</\\k<chipTag>>` |
13 | 9 |
|
14 | | -const CHIP_OR_CODE_DELIMITER = new RegExp(`${COMPLETE_TAG_SOURCE}|\`|\n`, 'g') |
| 10 | +const INLINE_CHIP_OR_DELIMITER = new RegExp(`${COMPLETE_TAG_SOURCE}|\`+|\\n`, 'g') |
15 | 11 |
|
16 | | -/** |
17 | | - * Pair Markdown delimiters outside chip payloads in one forward pass. A pair |
18 | | - * containing a chip is unwrapped; a lone delimiter is removed only when flush |
19 | | - * against a chip. Neighbouring code spans and multiline fences keep their pairs. |
20 | | - */ |
21 | | -export function sanitizeChatDisplayContent(content: string): string { |
22 | | - const removedDelimiters: number[] = [] |
23 | | - let openingTick = -1 |
24 | | - let containsChip = false |
25 | | - let adjacentToChip = false |
26 | | - let lastChipEnd = -1 |
| 12 | +interface OpenCodeSpan { |
| 13 | + index: number |
| 14 | + containsChip: boolean |
| 15 | + touchesChip: boolean |
| 16 | +} |
27 | 17 |
|
28 | | - for (const match of content.matchAll(CHIP_OR_CODE_DELIMITER)) { |
29 | | - const index = match.index |
30 | | - if (match.groups?.chipTag) { |
31 | | - if (openingTick !== -1) { |
32 | | - containsChip = true |
33 | | - adjacentToChip ||= index === openingTick + 1 |
34 | | - } |
35 | | - lastChipEnd = index + match[0].length |
36 | | - continue |
| 18 | +/** Only matched multi-backtick runs are code; an unmatched run remains ordinary prose. */ |
| 19 | +function unwrapInlineChips(content: string): string { |
| 20 | + const remainingRuns = new Map<number, number>() |
| 21 | + for (const [value] of content.matchAll(INLINE_CHIP_OR_DELIMITER)) { |
| 22 | + if (value.startsWith('`') && value.length > 1) { |
| 23 | + remainingRuns.set(value.length, (remainingRuns.get(value.length) ?? 0) + 1) |
37 | 24 | } |
| 25 | + } |
| 26 | + const removedDelimiters: number[] = [] |
| 27 | + let openSpan: OpenCodeSpan | null = null |
| 28 | + let previousChipEnd = -1 |
| 29 | + let protectedRunLength: number | null = null |
38 | 30 |
|
39 | | - if (match[0] === '\n') { |
40 | | - if (openingTick !== -1 && adjacentToChip) removedDelimiters.push(openingTick) |
41 | | - openingTick = -1 |
42 | | - lastChipEnd = -1 |
43 | | - continue |
44 | | - } |
| 31 | + const finishLine = () => { |
| 32 | + if (openSpan?.touchesChip) removedDelimiters.push(openSpan.index) |
| 33 | + openSpan = null |
| 34 | + } |
45 | 35 |
|
46 | | - if (openingTick === -1) { |
47 | | - openingTick = index |
48 | | - containsChip = false |
49 | | - adjacentToChip = lastChipEnd === index |
| 36 | + for (const token of content.matchAll(INLINE_CHIP_OR_DELIMITER)) { |
| 37 | + const [value] = token |
| 38 | + const index = token.index |
| 39 | + if (value === '\n') { |
| 40 | + finishLine() |
| 41 | + previousChipEnd = -1 |
| 42 | + } else if (value.startsWith('`')) { |
| 43 | + if (value.length > 1) { |
| 44 | + remainingRuns.set(value.length, (remainingRuns.get(value.length) ?? 1) - 1) |
| 45 | + } |
| 46 | + if (protectedRunLength !== null) { |
| 47 | + if (value.length === protectedRunLength) protectedRunLength = null |
| 48 | + continue |
| 49 | + } |
| 50 | + if (value.length > 1) { |
| 51 | + if (!openSpan && remainingRuns.get(value.length)) protectedRunLength = value.length |
| 52 | + continue |
| 53 | + } |
| 54 | + if (openSpan) { |
| 55 | + if (openSpan.containsChip) removedDelimiters.push(openSpan.index, index) |
| 56 | + openSpan = null |
| 57 | + } else { |
| 58 | + openSpan = { index, containsChip: false, touchesChip: previousChipEnd === index } |
| 59 | + } |
50 | 60 | } else { |
51 | | - if (containsChip) removedDelimiters.push(openingTick, index) |
52 | | - openingTick = -1 |
| 61 | + if (openSpan) { |
| 62 | + openSpan.containsChip = true |
| 63 | + openSpan.touchesChip ||= index === openSpan.index + 1 |
| 64 | + } |
| 65 | + previousChipEnd = index + value.length |
53 | 66 | } |
54 | 67 | } |
55 | | - |
56 | | - if (openingTick !== -1 && adjacentToChip) removedDelimiters.push(openingTick) |
| 68 | + finishLine() |
57 | 69 |
|
58 | 70 | const parts: string[] = [] |
59 | | - let start = 0 |
| 71 | + let cursor = 0 |
60 | 72 | for (const index of removedDelimiters) { |
61 | | - parts.push(content.slice(start, index)) |
62 | | - start = index + 1 |
| 73 | + parts.push(content.slice(cursor, index)) |
| 74 | + cursor = index + 1 |
| 75 | + } |
| 76 | + parts.push(content.slice(cursor)) |
| 77 | + return parts.join('') |
| 78 | +} |
| 79 | + |
| 80 | +/** Fenced blocks are literal, including unclosed streaming fences and longer closing runs. */ |
| 81 | +export function sanitizeChatDisplayContent(content: string): string { |
| 82 | + const parts: string[] = [] |
| 83 | + let cursor = 0 |
| 84 | + let fenceStart: number | null = null |
| 85 | + let fenceLength = 0 |
| 86 | + |
| 87 | + for (const line of content.matchAll(/^ {0,3}(`{3,})([^`\n]*)(?:\n|$)/gm)) { |
| 88 | + if (fenceStart === null) { |
| 89 | + fenceStart = line.index |
| 90 | + fenceLength = line[1].length |
| 91 | + } else if (line[1].length >= fenceLength && line[2].trim() === '') { |
| 92 | + const end = line.index + line[0].length |
| 93 | + parts.push( |
| 94 | + unwrapInlineChips(content.slice(cursor, fenceStart)), |
| 95 | + content.slice(fenceStart, end) |
| 96 | + ) |
| 97 | + cursor = end |
| 98 | + fenceStart = null |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + if (fenceStart === null) { |
| 103 | + parts.push(unwrapInlineChips(content.slice(cursor))) |
| 104 | + } else { |
| 105 | + parts.push(unwrapInlineChips(content.slice(cursor, fenceStart)), content.slice(fenceStart)) |
63 | 106 | } |
64 | | - parts.push(content.slice(start)) |
65 | 107 | return parts.join('').replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') |
66 | 108 | } |
0 commit comments