fix(editor): stop escaping a lone ~ so Go-template prose round-trips byte-exact

faithfulEscape backslash-escaped every ~, but in GFM only ~~ is strikethrough —
a lone ~ is literal. Escaping it injected a stray \ into prompt prose (e.g.
~10%/~30% in generator.tmpl + refiner.tmpl), and since the editor's getMarkdown
output is the Go text/template stored and sent to the LLM, the \ shipped to the
model. Escape ~ only in runs of 2+ (a balanced ~~strike~~ is handled by the
Strike mark upstream and never reaches this path); backtick/backslash unchanged.

Pinned by byte-identity tests for lone tildes (the corpus test asserts only
word-multiset + convergence, so \~ slipped through).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-06-30 20:09:58 +07:00
co-authored by Claude Opus 4.8
parent 7a96d9dbd4
commit 443fcc90ee
2 changed files with 19 additions and 2 deletions
@@ -24,8 +24,12 @@ const createFaithfulMarked = () => {
// ``` ` * _ [ ] ~ ``` — both corrupt our content (tags become entities, `[1-1000]`/`*.php`/`snake_case`
// gain stray backslashes). Text serialization is hard-coded in the manager (no per-extension hook), so we
// retune that one method: drop the entity-encoding entirely, and backslash-escape only the chars that
// would otherwise re-parse as inline syntax (`` ` ``, `~`, `\`).
const faithfulEscape = (text: string): string => text.replace(/([\\`~])/g, '\\$1');
// would otherwise re-parse as inline syntax: `` ` `` and `\` always, and `~` only when doubled — a lone
// `~` is literal in GFM, so escaping it would inject a stray `\` into prose like `~10%`.
const faithfulEscape = (text: string): string =>
text.replace(/[\\`]|~+/g, (match) =>
match[0] === '~' ? (match.length > 1 ? match.replace(/~/g, '\\~') : match) : `\\${match}`,
);
type ManagerWithEncode = {
codeTypes: Set<string>;
@@ -63,6 +63,19 @@ describe('selective escape — no stray backslashes on literal punctuation', ()
);
});
describe('lone tilde stays literal — no stray backslash injected (M1)', () => {
it.each(['~10% for environment setup', 'approximately ~5 minutes', 'a ~ b ~ c'])(
'keeps %s byte-identical',
(s) => {
expect(roundTrip(s)).toBe(s);
},
);
it('still round-trips ~~strikethrough~~ (double tilde unaffected)', () => {
expect(roundTrip('use ~~deprecated~~ here')).toContain('~~deprecated~~');
});
});
describe('inline marks round-trip', () => {
it.each(['**bold**', '*italic*', '`code span`', '~~strike~~', '[a link](https://example.com)', '**bold `code` end**'])(
'preserves %s',