Skip to content

Fix Windows-only theme CSS fallback and metadata href separators#374

Merged
cscheid merged 6 commits into
mainfrom
bugfix/bd-22rtwdur-windows-smoke-all-suite
Jul 3, 2026
Merged

Fix Windows-only theme CSS fallback and metadata href separators#374
cscheid merged 6 commits into
mainfrom
bugfix/bd-22rtwdur-windows-smoke-all-suite

Conversation

@cderv

@cderv cderv commented Jul 3, 2026

Copy link
Copy Markdown
Member

On a Windows checkout, cargo nextest run --workspace -E 'binary(integration) & test(smoke_all)' fails on 21 of 90 smoke fixtures: every theme (custom, project, metadata, cascade, bootswatch) fixture missing its custom colors and marker classes, one dir-metadata-paths fixture with a backslash leaking into an HTML href, and Python syntax highlighting missing .hl-keyword. CI runs ubuntu + macos only, so none of this showed up there.

Root Cause

Two independent defects, both from path/string handling that behaves differently on Windows:

  1. resources/scss/**/*.scss are checked out with CRLF line endings (git autocrlf) and baked into the binary via include_dir!. parse_layer's boundary-marker regex is $-anchored; in multiline mode $ matches before \n but leaves a trailing \r on CRLF-terminated markers like /*-- scss:defaults --*/, so the regex misses every marker. Layer parsing then reports NoBoundaryMarkers, theme compilation errors, and the render silently falls back to base CSS — dropping every theme variable, custom-scss layer, and highlight class.

  2. grass resolves @import/@use by PathBuf::join-ing each load path with the import specifier. Embedded resources use forward-slash virtual load paths, but on Windows PathBuf::join inserts a native backslash at the join boundary, while the embedded-resource lookup keys are forward-slash (include_dir normalizes at build time). The corrupted key never matched, so embedded @imports inside Bootstrap also failed.

  3. Separately, adjust_paths_to_document_dir (used for !path metadata like css:) stringified the rebased path with to_string_lossy, leaking native backslashes into HTML href attributes, and used Path::is_relative as its "already absolute" guard — which is false for POSIX-absolute paths like /usr/share/base.css on Windows, wrongly rebasing them.

Fix

  • Normalize CRLF to LF once at parse_layer entry — SCSS→CSS keeps no byte-offset map, so this is a safe single chokepoint (crates/quarto-sass/src/layer.rs).
  • Normalize embedded-resource lookup keys to forward slashes at the single lookup boundary, reusing the existing quarto_util::to_forward_slashes helper rather than adding a new one (crates/quarto-sass/src/resources.rs).
  • Emit !path-adjusted values with quarto_util::to_forward_slashes, and guard on is_rooted (has_root) instead of Path::is_relative (crates/quarto-core/src/project/mod.rs).

Root-cause and design notes: claude-notes/designs/embedded-resource-virtual-path-contract.md.

Test Plan

  • cargo nextest run -p quarto-sass — 206/206
  • cargo nextest run -p quarto-core -E 'test(directory_metadata)' — 16/16
  • cargo nextest run --workspace -E 'binary(integration) & test(smoke_all)' — passes (was 21 failures)

On a Windows checkout the SCSS sources baked in via include_dir! carry
CRLF line endings (git autocrlf). parse_layer's boundary detection uses a
$-anchored regex against the raw content; in multiline mode $ matches
before the \n but leaves the trailing \r on the marker line, so
CRLF-terminated markers like /*-- scss:defaults --*/ fail to match. The
gate then reports no boundary markers, theme/bootstrap/highlight layer
compilation errors out, and the render silently falls back to base CSS —
so on Windows no theme variables, custom-scss markers, or highlight
classes reach the output.

Normalize CRLF to LF once at parse_layer entry, a single line-ending
agnostic chokepoint. SCSS-to-CSS compilation keeps no byte-offset map, so
normalizing here is correct (contrast the document-content preserve
policy, where byte offsets must survive). The parse loop already strips
\r via str::lines(); the only failure point was the boundary-detection
gate, but normalizing at entry also guards any future line-anchored scan
and keeps a clean LF stream flowing into grass.

The CRLF path is covered by an in-source unit test that builds \r\n
input inline, so it runs on Linux CI rather than only on a Windows
checkout.
@posit-snyk-bot

posit-snyk-bot commented Jul 3, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

cderv added 5 commits July 3, 2026 17:32
Records the design for fixing Windows `@import` resolution in embedded SCSS:
the two path domains (real OS paths vs the forward-slash virtual
/__quarto_resources__ namespace), the grass PathBuf::join root cause with
source anchors, the native-vs-WASM contract comparison, and the chosen fix
(a single canonicalization boundary in EmbeddedResources). Also captures
the deferred WASM pool-unification and Windows-CI items and the decisions
taken with the design's multi-agent + grass-source research.
The canonicalization boundary is already owned by quarto-util
(to_forward_slashes), byte-identical to the proposed helper and already
tested. Reuse it instead of adding a new function, and note the two
existing private copies to consolidate. Records the quarto-util dependency
addition to quarto-sass and the WASM-build verification step.
grass resolves `@import`/`@use` by PathBuf::join-ing each load path with the
import specifier. Our embedded resources use virtual, forward-slash load
paths (/__quarto_resources__/...), but on Windows PathBuf::join inserts a
native backslash at the join boundary, and EmbeddedResources' lookup keys
are forward-slash (include_dir normalizes at build time). The corrupted
key (e.g. "\vendor/_rfs.scss") never matched, so every embedded `@import`
inside Bootstrap failed, theme compilation errored, and the render fell
back to base CSS on Windows-from-source builds.

Normalize to forward slashes at the single embedded-lookup boundary
(EmbeddedResources::strip_prefix and the key collectors) using the
existing quarto_util::to_forward_slashes helper rather than a new
function. RuntimeFs is left untouched so its std::fs fallback keeps
genuine native paths intact for real on-disk files.

Design and root-cause analysis: claude-notes/designs/embedded-resource-virtual-path-contract.md
adjust_paths_to_document_dir rebases !path metadata values (css,
resources, bibliography, ...) relative to the document directory. Two
Windows-only defects lived in that one function:

- The rebased value is stringified with to_string_lossy, so on Windows
  pathdiff's native separators leaked into the value. Since it is used
  verbatim in HTML hrefs (a css: !path <link>), the emitted attribute
  became href="..\..\shared\styles.css" instead of forward slashes.
  Normalize at the emission site with quarto_util::to_forward_slashes,
  the same helper the SCSS and JSON-writer separator fixes reuse.

- The relative-path guard used Path::is_relative, which on Windows is
  false only for drive-prefixed paths; a POSIX-absolute value like
  /usr/share/base.css is not is_absolute there, so it was wrongly
  rebased into ../../../usr/share/base.css. Guard on is_rooted
  (has_root) instead, matching the helper's documented purpose.

The suite's dir-metadata-paths fixture and the existing
directory_metadata_tests (previously red on Windows, green elsewhere)
now pass on all platforms.
@cderv cderv force-pushed the bugfix/bd-22rtwdur-windows-smoke-all-suite branch from 845bc84 to 4ca6ca4 Compare July 3, 2026 15:36
@cscheid cscheid merged commit 1954462 into main Jul 3, 2026
8 checks passed
@cscheid cscheid deleted the bugfix/bd-22rtwdur-windows-smoke-all-suite branch July 3, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants