From 07525f2c4f062e80f8e3be8d2b85885f7beef67b Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 16:04:42 +0100 Subject: [PATCH 1/7] Fix SCSSParsingError on unicode characters in SCSS selectors (#14065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scss-parser@1.0.6 tokenizer uses ASCII-only regex for identifiers, rejecting valid non-ASCII CSS characters (e.g., #présentation). Encode non-ASCII characters as ASCII codepoint placeholders before parsing since the parser is only used for variable analysis, not CSS generation. Fixes #14065 Co-Authored-By: Claude Opus 4.6 --- src/core/sass/analyzer/parse.ts | 8 ++++++++ .../docs/smoke-all/2026/02/20/custom-14065.scss | 5 +++++ tests/docs/smoke-all/2026/02/20/issue-14065.qmd | 17 +++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 tests/docs/smoke-all/2026/02/20/custom-14065.scss create mode 100644 tests/docs/smoke-all/2026/02/20/issue-14065.qmd diff --git a/src/core/sass/analyzer/parse.ts b/src/core/sass/analyzer/parse.ts index 5e9794a34a8..90c5e634f74 100644 --- a/src/core/sass/analyzer/parse.ts +++ b/src/core/sass/analyzer/parse.ts @@ -41,6 +41,14 @@ export const makeParserModule = ( "$1: $2", ); + // scss-parser's tokenizer only handles ASCII identifier characters. + // Encode non-ASCII characters as ASCII codepoint placeholders since the + // parser is only used for variable analysis, not CSS generation. + 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/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.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. From d081eec8d03a1c7de7c94d8574ffbe4264818230 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 16:15:48 +0100 Subject: [PATCH 2/7] Add gitignore added by quarto from rendering test file --- tests/docs/smoke-all/2025/03/31/issue-12338/.gitignore | 2 ++ 1 file changed, 2 insertions(+) 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 From 800a3d4b0980d00b7ff28b0cfd2c2f2217a8d35e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 16:27:04 +0100 Subject: [PATCH 3/7] Add to changelog --- news/changelog-1.9.md | 1 + 1 file changed, 1 insertion(+) 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` From c37cf9284ddb87bc932abd40e411056a7f41bd22 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 17:07:59 +0100 Subject: [PATCH 4/7] Decode Unicode variable names in CSS vars export block The SCSS analyzer encodes non-ASCII characters as _u_ for parsing, but cssVarsBlock emitted these encoded names into the CSS vars block. Dart Sass then failed because the encoded names don't match the original SCSS variable names. Decode before emitting so names round-trip correctly. Co-Authored-By: Claude Opus 4.6 --- src/core/sass/add-css-vars.ts | 11 ++++++++++- .../smoke-all/2026/02/20/custom-14065-var.scss | 7 +++++++ .../docs/smoke-all/2026/02/20/issue-14065-var.qmd | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/docs/smoke-all/2026/02/20/custom-14065-var.scss create mode 100644 tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd diff --git a/src/core/sass/add-css-vars.ts b/src/core/sass/add-css-vars.ts index 3edcd99be8b..4dcdbe9ada0 100644 --- a/src/core/sass/add-css-vars.ts +++ b/src/core/sass/add-css-vars.ts @@ -16,6 +16,14 @@ 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. +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 +46,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/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/issue-14065-var.qmd b/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd new file mode 100644 index 00000000000..3408fae10cb --- /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', 'background-color'] +--- + +Content with a unicode-named color variable in SCSS. From 82fc88ecdd05d2e31ef896fe57236772fa5a4373 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 17:16:50 +0100 Subject: [PATCH 5/7] Strengthen Unicode variable test assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assert that the CSS vars export block contains the decoded property name (--quarto-scss-export-présentation-bg), not just that the SCSS compiled successfully. Co-Authored-By: Claude Opus 4.6 --- tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3408fae10cb..65c276f78b0 100644 --- a/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd +++ b/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd @@ -9,7 +9,7 @@ _quarto: tests: html: ensureCssRegexMatches: - - ['#test-unicode-var', 'background-color'] + - ['#test-unicode-var', '--quarto-scss-export-pr.*sentation-bg'] --- Content with a unicode-named color variable in SCSS. From 6a9bc23ed2b58d747b7e20286f10a40544ab72c8 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 17:24:36 +0100 Subject: [PATCH 6/7] Use literal Unicode in test assertion for decode validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wildcard regex pr.*sentation matched both decoded and encoded forms, making the test pass regardless of whether decodeScssName worked. Use the literal character présentation-bg instead. Co-Authored-By: Claude Opus 4.6 --- tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 65c276f78b0..585b37660b8 100644 --- a/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd +++ b/tests/docs/smoke-all/2026/02/20/issue-14065-var.qmd @@ -9,7 +9,7 @@ _quarto: tests: html: ensureCssRegexMatches: - - ['#test-unicode-var', '--quarto-scss-export-pr.*sentation-bg'] + - ['#test-unicode-var', '--quarto-scss-export-présentation-bg'] --- Content with a unicode-named color variable in SCSS. From 1dc70f883c878b2b12fe76dec0929eb2fa14a3ec Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 18:34:39 +0100 Subject: [PATCH 7/7] Add CSS/SCSS spec references for non-ASCII identifier validity --- src/core/sass/add-css-vars.ts | 2 ++ src/core/sass/analyzer/parse.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core/sass/add-css-vars.ts b/src/core/sass/add-css-vars.ts index 4dcdbe9ada0..7d762bf0862 100644 --- a/src/core/sass/add-css-vars.ts +++ b/src/core/sass/add-css-vars.ts @@ -19,6 +19,8 @@ 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)) diff --git a/src/core/sass/analyzer/parse.ts b/src/core/sass/analyzer/parse.ts index 90c5e634f74..a2f73206c80 100644 --- a/src/core/sass/analyzer/parse.ts +++ b/src/core/sass/analyzer/parse.ts @@ -42,8 +42,15 @@ export const makeParserModule = ( ); // scss-parser's tokenizer only handles ASCII identifier characters. - // Encode non-ASCII characters as ASCII codepoint placeholders since the - // parser is only used for variable analysis, not CSS generation. + // 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)}_`,