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",