|
1 | 1 | const HIDDEN_INLINE_REFERENCE_PATTERN = |
2 | 2 | /`[^`\n]*(?:internal\/tool-results\/|internal\/blocktips\/|components\/integrations\/[^`\n]*README)[^`\n]*`/g |
3 | 3 |
|
4 | | -/** |
5 | | - * A complete inline-chip tag — `<workspace_resource>` or `<source>` — as |
6 | | - * opener, payload, closer. Both are JSON-bodied tags the model places inside a |
7 | | - * sentence, so both attract the same stray backticks. |
8 | | - * |
9 | | - * Two constraints on the payload, both load-bearing: |
10 | | - * |
11 | | - * - **No backtick.** A payload is JSON and carries none, so this is what tells a |
12 | | - * real tag from prose MENTIONING the tag name — a message explaining the |
13 | | - * syntax writes the opener and the closer as two separately backticked spans. |
14 | | - * - **No nested opener**, via the negative lookahead. A cost bound rather than a |
15 | | - * correctness rule: a lazy scan allowed to cross an opener restarts from every |
16 | | - * opener, so a message repeating the tag name is quadratic — on the main |
17 | | - * thread, for every streamed chunk. |
18 | | - * |
19 | | - * Accepted trade: a resource whose title or path itself contains a backtick is |
20 | | - * not matched, so it renders as text rather than a chip. That costs one chip and |
21 | | - * is rare; the failure it replaces corrupts a whole message and is common. |
22 | | - */ |
23 | | -const COMPLETE_TAG_SOURCE = |
24 | | - '<(?<chipTag>workspace_resource|source)>(?:(?!<\\k<chipTag>>)[^`])*?<\\/\\k<chipTag>>' |
25 | | - |
26 | | -/** Non-global so {@link RegExp.test} has no `lastIndex` to carry between calls. */ |
27 | | -const COMPLETE_INLINE_CHIP_TAG = new RegExp(COMPLETE_TAG_SOURCE) |
| 4 | +/** A matching nested opener bounds every payload scan, including quoted strings. */ |
| 5 | +function chipTagPattern(tag: 'workspace_resource' | 'source'): string { |
| 6 | + const noNestedTag = `(?!<${tag}>)` |
| 7 | + /** JSON strings own their backticks; valid escapes cannot consume a nested tag's opener. */ |
| 8 | + const jsonString = `"(?:${noNestedTag}(?:\\\\(?:["\\\\/bfnrt]|u[0-9a-fA-F]{4})|[^"\\\\]))*"` |
| 9 | + return `<${tag}>(?:${noNestedTag}(?:${jsonString}|[^"\`]))*?</${tag}>` |
| 10 | +} |
28 | 11 |
|
29 | | -/** |
30 | | - * One left-to-right pass over the two things that can own a backtick: an inline |
31 | | - * code span, and a tag with a stray backtick pressed against it. |
32 | | - * |
33 | | - * ONE pass is the design. Two separate passes each have to guess which backticks |
34 | | - * belong together, and every previous arrangement of this file got a different |
35 | | - * case wrong — a span two words away, a code fence, then a span sitting flush |
36 | | - * against the tag. Here a span consumes its own delimiters as the scan reaches |
37 | | - * them, so `` `config.json`<tag> `` keeps its pair without a special case. |
38 | | - * |
39 | | - * The trailing backtick is only taken when no further backtick follows on the |
40 | | - * line; otherwise it is not a stray at all but the opener of the next span, and |
41 | | - * `` <tag>`config.json` `` would lose that span's delimiter. A LEADING backtick |
42 | | - * needs no such guard, because a backtick that closes a span is consumed as part |
43 | | - * of that span. Of the two, only the trailing lookahead is pinned by a test — |
44 | | - * swapping the alternatives changes behaviour only for a span that both opens |
45 | | - * flush against a tag and closes elsewhere, which no fixture covers. |
46 | | - */ |
47 | | -const CODE_SPAN_OR_FLANKED_TAG = new RegExp( |
48 | | - `\`[^\`\\n]*\`|\`?(${COMPLETE_TAG_SOURCE})(?:\`(?![^\`\\n]*\`))?`, |
| 12 | +const INLINE_CHIP_OR_DELIMITER = new RegExp( |
| 13 | + `${chipTagPattern('workspace_resource')}|${chipTagPattern('source')}|\`|\\n`, |
49 | 14 | 'g' |
50 | 15 | ) |
51 | 16 |
|
| 17 | +interface OpenCodeSpan { |
| 18 | + index: number |
| 19 | + containsChip: boolean |
| 20 | + touchesChip: boolean |
| 21 | +} |
| 22 | + |
| 23 | +/** Unwraps chip-bearing spans without borrowing delimiters from neighboring code or fences. */ |
52 | 24 | export function sanitizeChatDisplayContent(content: string): string { |
53 | | - return content |
54 | | - .replace(CODE_SPAN_OR_FLANKED_TAG, (match, tag?: string) => { |
55 | | - // A tag with stray backticks against it: keep the tag, drop the strays. |
56 | | - if (tag !== undefined) return tag |
| 25 | + const removedDelimiters: number[] = [] |
| 26 | + let openSpan: OpenCodeSpan | null = null |
| 27 | + let previousChipEnd = -1 |
| 28 | + |
| 29 | + const finishLine = () => { |
| 30 | + if (openSpan?.touchesChip) removedDelimiters.push(openSpan.index) |
| 31 | + openSpan = null |
| 32 | + } |
| 33 | + |
| 34 | + for (const token of content.matchAll(INLINE_CHIP_OR_DELIMITER)) { |
| 35 | + const [value] = token |
| 36 | + const index = token.index |
| 37 | + if (value === '\n') { |
| 38 | + finishLine() |
| 39 | + previousChipEnd = -1 |
| 40 | + } else if (value === '`') { |
| 41 | + if (openSpan) { |
| 42 | + if (openSpan.containsChip) removedDelimiters.push(openSpan.index, index) |
| 43 | + openSpan = null |
| 44 | + } else { |
| 45 | + openSpan = { index, containsChip: false, touchesChip: previousChipEnd === index } |
| 46 | + } |
| 47 | + } else { |
| 48 | + if (openSpan) { |
| 49 | + openSpan.containsChip = true |
| 50 | + openSpan.touchesChip ||= index === openSpan.index + 1 |
| 51 | + } |
| 52 | + previousChipEnd = index + value.length |
| 53 | + } |
| 54 | + } |
| 55 | + finishLine() |
57 | 56 |
|
58 | | - // A code span. Unwrap it only when it genuinely holds a tag — the parser |
59 | | - // lifts the tag out either way, so leaving the delimiters would strand a |
60 | | - // pair of backticks around a hole. Anything else is someone else's span. |
61 | | - const inner = match.slice(1, -1) |
62 | | - return COMPLETE_INLINE_CHIP_TAG.test(inner) ? inner : match |
63 | | - }) |
64 | | - .replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') |
| 57 | + const parts: string[] = [] |
| 58 | + let cursor = 0 |
| 59 | + for (const index of removedDelimiters) { |
| 60 | + parts.push(content.slice(cursor, index)) |
| 61 | + cursor = index + 1 |
| 62 | + } |
| 63 | + parts.push(content.slice(cursor)) |
| 64 | + return parts.join('').replace(HIDDEN_INLINE_REFERENCE_PATTERN, '') |
65 | 65 | } |
0 commit comments