diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 0f61d281a9c..70b02eedd29 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -50,6 +50,7 @@ All changes included in 1.9: - ([#13825](https://github.com/quarto-dev/quarto-cli/issues/13825)): Fix `column: margin` not working with `renderings: [light, dark]` option. Column classes are now preserved when applying theme classes to cell outputs. - ([#13883](https://github.com/quarto-dev/quarto-cli/issues/13883)): Fix unequal top/bottom spacing in simple untitled callouts. - ([#13900](https://github.com/quarto-dev/quarto-cli/issues/13900)): Warn when `renderings` cell option contains duplicate names. Previously, duplicate names like `[dark, light, dark, light]` would silently use only the last output for each name. +- ([#14065](https://github.com/quarto-dev/quarto-cli/issues/14065)): Fix `SCSSParsingError` when custom SCSS themes contain non-ASCII characters in selectors (e.g., `#présentation`). ### `typst` diff --git a/src/core/sass/add-css-vars.ts b/src/core/sass/add-css-vars.ts index 3edcd99be8b..7d762bf0862 100644 --- a/src/core/sass/add-css-vars.ts +++ b/src/core/sass/add-css-vars.ts @@ -16,6 +16,16 @@ import { getVariableDependencies } from "./analyzer/get-dependencies.ts"; const { getSassAst } = makeParserModule(parse); +// Reverse the _u_ encoding applied in parse.ts so that +// variable names emitted into the CSS vars block match the +// original SCSS source that Dart Sass compiles against. +// Non-ASCII codepoints are valid in CSS custom property names since they +// follow the production (see spec references in parse.ts). +const decodeScssName = (name: string) => + name.replace(/_u([0-9a-f]+)_/g, (_, hex: string) => + String.fromCodePoint(parseInt(hex, 16)) + ); + export class SCSSParsingError extends Error { constructor(message: string) { super(`SCSS Parsing Error: ${message}`); @@ -38,7 +48,8 @@ export const cssVarsBlock = (scssSource: string) => { for (const [dep, _] of deps) { const decl = ast.get(dep); if (decl.valueType === "color") { - output.push(`--quarto-scss-export-${dep}: #{$${dep}};`); + const originalName = decodeScssName(dep); + output.push(`--quarto-scss-export-${originalName}: #{$${originalName}};`); } } output.push("}"); diff --git a/src/core/sass/analyzer/parse.ts b/src/core/sass/analyzer/parse.ts index 5e9794a34a8..a2f73206c80 100644 --- a/src/core/sass/analyzer/parse.ts +++ b/src/core/sass/analyzer/parse.ts @@ -41,6 +41,21 @@ export const makeParserModule = ( "$1: $2", ); + // scss-parser's tokenizer only handles ASCII identifier characters. + // Non-ASCII codepoints are valid in both CSS and SCSS identifiers: + // - CSS Syntax L3 §4.2 defines "ident code point" as including any + // codepoint >= U+0080 (https://www.w3.org/TR/css-syntax-3/#ident-code-point) + // - CSS2 grammar includes `nonascii` in `nmstart`/`nmchar` productions + // (https://www.w3.org/TR/CSS2/grammar.html#scanner) + // - Sass inherits CSS's grammar for identifiers + // (https://github.com/sass/sass/blob/main/spec/syntax.md) + // Dart Sass handles them correctly, so we encode here as ASCII + // placeholders for analysis only, then decode in add-css-vars.ts. + contents = contents.replaceAll( + /[^\x00-\x7F]/g, + (ch) => `_u${ch.codePointAt(0)!.toString(16)}_`, + ); + // This is relatively painful, because unfortunately the error message of scss-parser // is not helpful. diff --git a/tests/docs/smoke-all/2025/03/31/issue-12338/.gitignore b/tests/docs/smoke-all/2025/03/31/issue-12338/.gitignore index 075b2542afb..0e3521a7d0f 100644 --- a/tests/docs/smoke-all/2025/03/31/issue-12338/.gitignore +++ b/tests/docs/smoke-all/2025/03/31/issue-12338/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/2026/02/20/custom-14065-var.scss b/tests/docs/smoke-all/2026/02/20/custom-14065-var.scss new file mode 100644 index 00000000000..eb66ab46056 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/20/custom-14065-var.scss @@ -0,0 +1,7 @@ +/*-- scss:defaults --*/ +$présentation-bg: #ff0000; + +/*-- scss:rules --*/ +#test-unicode-var { + background-color: $présentation-bg; +} diff --git a/tests/docs/smoke-all/2026/02/20/custom-14065.scss b/tests/docs/smoke-all/2026/02/20/custom-14065.scss new file mode 100644 index 00000000000..b420af861ee --- /dev/null +++ b/tests/docs/smoke-all/2026/02/20/custom-14065.scss @@ -0,0 +1,5 @@ +/*-- scss:rules --*/ + +#présentation p { + line-height: 2; +} diff --git a/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd b/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd new file mode 100644 index 00000000000..585b37660b8 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd @@ -0,0 +1,15 @@ +--- +title: "Unicode SCSS Variable Test" +format: + html: + theme: + - default + - custom-14065-var.scss +_quarto: + tests: + html: + ensureCssRegexMatches: + - ['#test-unicode-var', '--quarto-scss-export-présentation-bg'] +--- + +Content with a unicode-named color variable in SCSS. diff --git a/tests/docs/smoke-all/2026/02/20/issue-14065.qmd b/tests/docs/smoke-all/2026/02/20/issue-14065.qmd new file mode 100644 index 00000000000..a41c1e74583 --- /dev/null +++ b/tests/docs/smoke-all/2026/02/20/issue-14065.qmd @@ -0,0 +1,17 @@ +--- +title: "Unicode SCSS Test" +format: + html: + theme: + - default + - custom-14065.scss +_quarto: + tests: + html: + ensureCssRegexMatches: + - ['#présentation p', '--quarto-scss-export-'] +--- + +## Présentation + +Unicode characters in headings become CSS selectors.