From 115f47515c3a8261d0ab3398c5df699d469c5194 Mon Sep 17 00:00:00 2001 From: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:42:57 +0530 Subject: [PATCH] fix(recipe): unescape escaped quotes and backslashes in quoted values The SVS boundary scanners (withoutComments, closingBrace, closingSheet, valueSemicolon) all treat a backslash inside quotes as an escape, but parseValue only stripped the outer quote pair and returned the raw inner text, so \" sequences shipped through verbatim. Unescape the escaped delimiter quote and the escaped backslash when producing the value, matching what the scanners already accept. Fixes #392 Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- packages/recipe/src/parser.ts | 14 +++++++++++++- packages/recipe/test/source-closure.test.ts | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/recipe/src/parser.ts b/packages/recipe/src/parser.ts index 584cfd112..68d6ca14a 100644 --- a/packages/recipe/src/parser.ts +++ b/packages/recipe/src/parser.ts @@ -187,7 +187,19 @@ function parseValue(sourceName: string, text: string, offset: number): Canonical (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { - return value.slice(1, -1); + const quote = value[0] as "\"" | "'"; + let unescaped = ""; + for (let index = 1; index < value.length - 1; index += 1) { + const character = value[index]!; + const next = value[index + 1]; + if (character === "\\" && (next === quote || next === "\\")) { + unescaped += next; + index += 1; + continue; + } + unescaped += character; + } + return unescaped; } return value; } diff --git a/packages/recipe/test/source-closure.test.ts b/packages/recipe/test/source-closure.test.ts index f93800fa5..404c36ce5 100644 --- a/packages/recipe/test/source-closure.test.ts +++ b/packages/recipe/test/source-closure.test.ts @@ -313,6 +313,23 @@ test("quoted Recipe values may contain Prompt punctuation without changing SVS s assert.equal(sheet.recipes[0]?.value.properties.text, "first; second } /* literal */"); }); +test("quoted Recipe values unescape the escaped quote and backslash the scanners honor", () => { + const sheet = parseRecipe( + "escape.svs", + ` + caption.host { + text: "He said \\"hi\\" twice"; + note: "path C:\\\\caps ends here"; + aside: 'It\\'s minimal'; + } +`, + ); + const properties = sheet.recipes[0]?.value.properties; + assert.equal(properties.text, 'He said "hi" twice'); + assert.equal(properties.note, "path C:\\caps ends here"); + assert.equal(properties.aside, "It's minimal"); +}); + test("quoted Recipe values may contain the sheet closing text without ending the document", () => { const sheet = parseRecipe( "literal-close.svs",