From 731cd2e6cd0bc258b1879b3fefbdaf2627a60dda Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:54:17 +0000 Subject: [PATCH] Sync generated provider output --- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- .../scripts/detector/design-system.mjs | 67 ++++++++++++++++++- .../impeccable/scripts/lib/design-parser.mjs | 65 +++++++++++++++++- 30 files changed, 1920 insertions(+), 60 deletions(-) diff --git a/.agents/skills/impeccable/scripts/detector/design-system.mjs b/.agents/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.agents/skills/impeccable/scripts/detector/design-system.mjs +++ b/.agents/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.agents/skills/impeccable/scripts/lib/design-parser.mjs b/.agents/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.agents/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.agents/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.claude/skills/impeccable/scripts/detector/design-system.mjs b/.claude/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.claude/skills/impeccable/scripts/detector/design-system.mjs +++ b/.claude/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.claude/skills/impeccable/scripts/lib/design-parser.mjs b/.claude/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.claude/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.claude/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.cursor/skills/impeccable/scripts/detector/design-system.mjs b/.cursor/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.cursor/skills/impeccable/scripts/detector/design-system.mjs +++ b/.cursor/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.cursor/skills/impeccable/scripts/lib/design-parser.mjs b/.cursor/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.cursor/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.cursor/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.gemini/skills/impeccable/scripts/detector/design-system.mjs b/.gemini/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.gemini/skills/impeccable/scripts/detector/design-system.mjs +++ b/.gemini/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.gemini/skills/impeccable/scripts/lib/design-parser.mjs b/.gemini/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.gemini/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.gemini/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.github/skills/impeccable/scripts/detector/design-system.mjs b/.github/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.github/skills/impeccable/scripts/detector/design-system.mjs +++ b/.github/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.github/skills/impeccable/scripts/lib/design-parser.mjs b/.github/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.github/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.github/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.grok/skills/impeccable/scripts/detector/design-system.mjs b/.grok/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.grok/skills/impeccable/scripts/detector/design-system.mjs +++ b/.grok/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.grok/skills/impeccable/scripts/lib/design-parser.mjs b/.grok/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.grok/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.grok/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.kiro/skills/impeccable/scripts/detector/design-system.mjs b/.kiro/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.kiro/skills/impeccable/scripts/detector/design-system.mjs +++ b/.kiro/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.kiro/skills/impeccable/scripts/lib/design-parser.mjs b/.kiro/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.kiro/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.kiro/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.opencode/skills/impeccable/scripts/detector/design-system.mjs b/.opencode/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.opencode/skills/impeccable/scripts/detector/design-system.mjs +++ b/.opencode/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.opencode/skills/impeccable/scripts/lib/design-parser.mjs b/.opencode/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.opencode/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.opencode/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.pi/skills/impeccable/scripts/detector/design-system.mjs b/.pi/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.pi/skills/impeccable/scripts/detector/design-system.mjs +++ b/.pi/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.pi/skills/impeccable/scripts/lib/design-parser.mjs b/.pi/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.pi/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.pi/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.qoder/skills/impeccable/scripts/detector/design-system.mjs b/.qoder/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.qoder/skills/impeccable/scripts/detector/design-system.mjs +++ b/.qoder/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.qoder/skills/impeccable/scripts/lib/design-parser.mjs b/.qoder/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.qoder/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.qoder/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.rovodev/skills/impeccable/scripts/detector/design-system.mjs b/.rovodev/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.rovodev/skills/impeccable/scripts/detector/design-system.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.rovodev/skills/impeccable/scripts/lib/design-parser.mjs b/.rovodev/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.rovodev/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.trae-cn/skills/impeccable/scripts/detector/design-system.mjs b/.trae-cn/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/design-system.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.trae-cn/skills/impeccable/scripts/lib/design-parser.mjs b/.trae-cn/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.trae/skills/impeccable/scripts/detector/design-system.mjs b/.trae/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.trae/skills/impeccable/scripts/detector/design-system.mjs +++ b/.trae/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.trae/skills/impeccable/scripts/lib/design-parser.mjs b/.trae/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.trae/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.trae/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.vibe/skills/impeccable/scripts/detector/design-system.mjs b/.vibe/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/.vibe/skills/impeccable/scripts/detector/design-system.mjs +++ b/.vibe/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/.vibe/skills/impeccable/scripts/lib/design-parser.mjs b/.vibe/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/.vibe/skills/impeccable/scripts/lib/design-parser.mjs +++ b/.vibe/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/plugin/skills/impeccable/scripts/detector/design-system.mjs b/plugin/skills/impeccable/scripts/detector/design-system.mjs index c4d31eade..b9d9f3f69 100644 --- a/plugin/skills/impeccable/scripts/detector/design-system.mjs +++ b/plugin/skills/impeccable/scripts/detector/design-system.mjs @@ -142,10 +142,73 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// reaches allowedFonts as '\"ibm plex sans' and never matches the same family +// declared in CSS. Scanner instead of a regex: the escape set is small and the +// backslash handling stays readable. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false; diff --git a/plugin/skills/impeccable/scripts/lib/design-parser.mjs b/plugin/skills/impeccable/scripts/lib/design-parser.mjs index 5e2f2c286..f82370bb2 100644 --- a/plugin/skills/impeccable/scripts/lib/design-parser.mjs +++ b/plugin/skills/impeccable/scripts/lib/design-parser.mjs @@ -115,10 +115,71 @@ function stripInlineYamlComment(s) { return s; } +// YAML double-quoted scalars process backslash escapes. Stripping the outer +// quotes without unescaping leaves them in place, so a nested font family like +// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif" +// keeps its literal backslashes and never matches the same family in CSS. +// The full YAML 1.2 double-quote escape set (spec section 5.7). +const YAML_SIMPLE_ESCAPES = { + '0': '\0', + a: '\x07', + b: '\b', + t: '\t', + n: '\n', + v: '\v', + f: '\f', + r: '\r', + e: '\x1b', + ' ': ' ', + '"': '"', + '/': '/', + '\\': '\\', + N: '\u0085', + _: '\u00a0', + L: '\u2028', + P: '\u2029', +}; +const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 }; + +function unescapeYamlDoubleQuoted(body) { + let out = ''; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch !== '\\' || i === body.length - 1) { + out += ch; + continue; + } + const next = body[i + 1]; + if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) { + out += YAML_SIMPLE_ESCAPES[next]; + i++; + continue; + } + // \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay + // literal rather than corrupting the rest of the scalar. + const hexLen = YAML_HEX_ESCAPE_LENGTHS[next]; + if (hexLen) { + const hex = body.slice(i + 2, i + 2 + hexLen); + const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + out += String.fromCodePoint(codePoint); + i += 1 + hexLen; + continue; + } + } + out += ch; + } + return out; +} + function parseScalar(raw) { const s = raw.trim(); - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + return unescapeYamlDoubleQuoted(s.slice(1, -1)); + } + // Single-quoted YAML escapes only the quote itself, by doubling it. + if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) { + return s.slice(1, -1).split("''").join("'"); } if (s === 'true') return true; if (s === 'false') return false;