Fixed idempotency issue. Block comment is a normal fill item in values.
28 KiB
Coding agent guides for crates/oxc_formatter_css
Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the CSS/SCSS/Less-specific rules and translations.
Overview
Prettier compatible CSS/SCSS/Less formatter (oxfmt's Tier 1 backend), using the oxc_formatter_core APIs.
- Built on
oxc_formatter_corefor the language-agnostic IR + Printer + builders + macros- See
crates/oxc_formatter_core/AGENTS.mdfor the IR/pipeline details
- See
- Entry points:
format(): standalone files, on a service-less sessionformat_with_session(): standalone, on the caller'sFormatSessionformat_to_ir(): embedded use via the dispatcher (template_placeholders= the css-in-js parse mode)
Forked parser
Parses with oxc-css-parser, a raffia fork pinned via the workspace Cargo.toml.
The fork adds:
template_placeholderoption (for the css-in-js dispatcher)- Backtick-delimited marker
`<prefix><digits>`with a parameterized inner affix (TemplatePlaceholder { prefix }); - Backtick is invalid CSS/SCSS/Sass (only Less's inline-JS delimiter), so the marker is
unmistakably out-of-band, not a real
@var/$varor at-rule - Only
format_to_irwithtemplate_placeholders: trueenables it (with the option unset a backtick is a syntax error) - MUST be used with
Syntax::Scss; css-in-js isCssVariant::Scss-hardcoded - Tokenized as one typed
Token::Placeholder { index, suffix }accepted in value / selector / statement / declaration-name positions- Per-position layout and coverage: see "css-in-js specifics" below
- Backtick-delimited marker
- Whitespace-sensitive Less
+/-operators (matchinglessc)- A
+/-is aLessBinaryOperationoperator only when followed by whitespace; @a -@bis two values (-@bis aLessNegativeValuesignmargin: -@a -@b= two values, NOT(-@a) - @bsubtraction),@a - @bis subtraction
- A
- Various bug fixes for valid CSS/SCSS/Less syntax
oxc-css-parsermiss-parses or rejects- selector / at-rule / value-token coverage gaps
- Additive leniencies for syntax reference compilers reject but postcss (and so Prettier) accepts
- the IE
*colorhack, Scss dotted words (foo.bar, xstyled / tailwind-theme tokens), plain-CSS%placeholderselectors (postcss-extend-rule), non-standard@importtails, ... - Each is additive: it only accepts previously-erroring input, never changes the AST of input that already parsed
- See "Policy: how to take in non-spec / non-Sass dialect syntax" below
- the IE
Prettier operates on postcss + three sub-parsers (postcss-selector-parser, postcss-values-parser, postcss-media-query-parser) and depends on raws (source gaps).
oxc-css-parser parses everything structurally in one pass; source gaps are recovered by comparing span boundaries (hasEmptyRawBefore(x) == "no gap between spans").
Error semantics
The shared policy applies; CSS specifics:
oxc-css-parseris error-tolerant viaparser.recoverable_errors(), but any parse error still bails out- Exception:
TopLevelDeclaration, tolerated ONLY byformat_to_ir()- The dominant css-in-js shape,
css`display: flex;` - Standalone
format()still rejects it as invalid CSS/SCSS/Less (Dart Sass rejects it too)
- The dominant css-in-js shape,
Comments
oxc-css-parser does not attach comments to the AST;
they are collected via ParserBuilder::comments() into a positional cursor over CssComment { span, inline } (inline = //).
- Statement-level comments: flushed before each statement (
flush_leading_comments); consecutive same-line comments stay glued (*/ /*!), but a comment is always followed by a line break before a node - Value-level comments: block comments between fill runs are standalone fill items (own-line or not);
the rest (leads before the first component, own-line
//) flush at the next entry's head (flush_value_comments), where//comments expand the parent group and force a hardline after - Trailing (
value /* c */;): flushed bywrite_declarationwith the source gap before;preserved - After each statement, the sequence DISCARDS unclaimed comments inside the statement span (cursor must never point before a printed position)
Placement invariants
The shared invariants (FORMATTER_POLICY.md "Comment placement invariants") apply; this section records their CSS translation:
,is a list SEPARATOR: a comment between an element and its comma stays BEFORE the commaa /* c */, b; comments after it lead the next element- Declaration value lists (
write_value_groups) and function arguments (write_function) route every comma throughwrite_group_commawith the comma offset paired to its group split_comma_groupsreturns(group, Option<comma_start>); SCSS/Less lists paircomma_spans- A new comma site must take the pair, the shape makes taking the groups without the commas a visible choice, not an accident
- Adopted by every comma writer (function/include args, maps, paren/sass lists,
@mixinparams,@eachbindings, keyframe selectors)
;is a declaration TERMINATOR, but unlike JS statements Prettier does NOT move comments behind it:value /* c */;keeps the comment before;(measured behavior, not principle, may change in the future)
- The positional cursor makes ownership a bounds discipline, not an attachment one:
- a flush's upper bound must never extend past the next piece of user content,
- and a declaration's
tail_boundmay only be consumed by the LAST comma group (write_value_groupsclears it for every other group)
- Line-boundary rule in CSS terms:
//comments force a hardline after;- own-line comments stay own-line at statement and trailing level, but a value-level own-line BLOCK comment is a plain fill item (joins the line when it fits):
- it carries no line-based semantics, and freezing it own-line would pin a wrapped layout (= not idempotent)
- own-line comments stay own-line at statement and trailing level, but a value-level own-line BLOCK comment is a plain fill item (joins the line when it fits):
Line endings
parse_stylesheet normalizes \r\n and lone \r to \n BEFORE parsing.
Unlike other formatters that normalize locally where needed, CSS has too many verbatim slices to handle case by case.
And without this, raw \r reaching the core text() builder would panic.
Parse and print both use the normalized arena copy, so spans stay consistent.
The configured end_of_line option still applies, the printer emits the chosen line ending when materializing multiline Text IR.
css-in-js specifics
format_to_ir() accepts SCSS-like source with `PLACEHOLDER-N` markers in place of ${} interpolations.
oxc-css-parser tokenizes each as a typed Token::Placeholder via the fork option template_placeholder (format.rs passes the inner affix). Backtick is invalid SCSS, so the marker can never be confused with real syntax (see the TEMPLATE_PLACEHOLDER_PREFIX / _SUFFIX consts in lib.rs).
Each parses into a typed node that the printer emits as a FormatElement::EmbedPlaceholder(N) marker (print/mod.rs::write_placeholder), plus a Text for any glued suffix; the JS host (oxc_formatter/embed/css.rs) substitutes ${exprN} back. No output-side string protocol — the index is carried structurally.
Per-position layout, the non-obvious rules below;
the exact set of supported positions (incl. id / attribute-value / class selector) is whatever the embedded/scss/*-placeholders.scss fixtures exercise, not this list:
- Statement position: a
Statement::Placeholder(write_statement) source-driven layout:- The
;is kept only when the source has one; consecutive placeholders preserve the source whitespace (${a} ${b}on one line,${a}\n${b}on two) - A
;-less placeholder opens a postcss "swallow" run: following declarations keep a source-driven;until a source;ends the run (write_statement_sequence_bounded) ${foo}: ${bar}parses as a declaration whose property NAME is a placeholder
- The
- Value position:
ComponentValue::Placeholder; rides existing gap-based separator rules- One added rule: glued to a paren group →
Separator::SoftBreak(${fn}(30px)breaks BEFORE the parens)
- One added rule: glued to a paren group →
- Selector position:
InterpolableIdent::Placeholder; a placeholder mid-selector still triggers "garbage mode" inwrite_selector_list- Emits the raw source slice with whitespace runs collapsed (sentinels split back out to
EmbedPlaceholderviawrite_text_with_placeholders), never breaking - Mirrors
postcss-selector-parserdegrading on at-words - A statement-position placeholder-led selector must not absorb across a newline (oxc-css-parser
placeholder_starts_qualified_rule):${mixin}\n& > .x {}is two statements
- Emits the raw source slice with whitespace runs collapsed (sentinels split back out to
- String /
url()position: the CSS lexer keeps these opaque, so a sentinel inside them stays in a verbatimText(noEmbedPlaceholder)- The JS host (
oxc_formatter) counts these and substitutes them inline through itsText-sentinel branch, a deliberate string-scan fallback at the edges of the typed path
- The JS host (
tests/fixtures/embedded/scss/*-placeholders.scss is the source of truth for which positions parse and how they print (the embedded/ harness runs format_to_ir with the option on); add a fixture there when extending coverage.
Front matter (yaml-in-css)
The envelope contract (host opt-in, detection, frame composition, refusal semantics) is core's (write_front_matter / spec::front_matter; boundary layer 5 in oxc_formatter_core's AGENTS.md); this crate's side (the embeddable set, blank-and-compose wiring, the gap rule) lives in format.rs.
FM behavior is verified through oxfmt; this crate carries no dispatcher-wired FM tests.
Prettier mapping
Unknown at-rule params print VERBATIM
Prettier's parser hands params to sub-parsers only for a fixed allowlist (is_value_parsed_at_rule);
everything else (@apply, @tailwind, @custom-variant, @variant, @source, ICSS @value, etc) stays a plain string the printer emits raw (write_verbatim_at_rule_tail).
Re-spacing those tokens CORRUPTS Tailwind syntax: dark:bg-x → dark: bg-x, py-1.5 → py-10.5, @custom-variant dark (&:is(...)) → dark(&: is(...)).
We also follow this to keep Prettier compatibility.
Tailwind @apply sorting (CssFormatOptions::sort_tailwindcss)
Ports prettier-plugin-tailwindcss's transformCss: with the option on, @apply params become FormatElement::TailwindClass(index) elements, and a host-supplied TailwindSorter performs the actual ordering/dedup outside this crate.
See write_apply_prelude in at_rule.rs (collection + !important / Less ~"..." extraction) and format.rs (sorter dispatch).
postcss plugin syntax
postcss parses everything permissively and lets plugins interpret syntax at runtime; oxc-css-parser parses strictly, so plugin-specific syntax is rejected by default. Failures emit a LOUD diagnostic.
However, some plugin-flavored constructs work anyway, because:
- 1: Now standard CSS
- CSS nesting
- Tailwind v3/v4 at-rules (
@tailwind,@apply,@layer,@theme,@utility,@variant,@config,@custom-media) - CSS Modules (
@valueincl.from,:global/:local,composes, plain:import/:export)
- 2: CSS forward-compat
- Unknown at-rules (
postcss-mixins's@define-mixin,@mixin, etc.) round-trip asUnknownAtRulewith the prelude held as a verbatimTokenSeq @media/@supportspreludesoxc-css-parsercan't structure fall through to<general-enclosed>as a verbatimTokenSeq
- Unknown at-rules (
Beyond those, we add support per-plugin when there's real demand.
Policy: how to take in non-spec / non-Sass dialect syntax
Plugin dialects (xstyled dotted tokens, Tailwind theme() paths, postcss plugin at-rules, ...) look like an unbounded support surface.
They are not — the oracle is never "the dialect", it is what Prettier does with the bytes, and Prettier's answer is almost always "preserve verbatim".
postcss is a token-soup preserver, not a grammar: everything it doesn't positively recognize is a "word" that round-trips untouched.
So the target behavior is finite: never destroy tokens Prettier wouldn't destroy.
When a dialect report comes in, first translate it: "which GENERAL postcss behavior are we missing?" Not "how do we support plugin Xxx?". Then absorb it at the highest possible rung of the escape-hatch hierarchy (top = cheapest, each rung covers whole classes of dialects at once):
- Unknown at-rule prelude verbatim (
write_verbatim_at_rule_tail) zero-cost bucket: Tailwind, postcss-mixins, ICSS ride it for free - Raw fallbacks when the typed grammar rejects (raw component values,
TokenSeq,ImportPrelude.modifiers)[attr=;], weird import tails - postcss word rules at the separator layer (
is_word_glued_number, the1#{$var}glue, solidus words) variant-agnostic, fixes xstyled +theme()+ future unknown tokens in one place ParserOptionsflag + typed node (postcss-simple-vars) ONLY when the formatter must make layout decisions INSIDE the construct. Promotion criteria, all three: (a) real user demand, (b) Prettier itself formats it structurally (not verbatim), (c) rungs 1-3 can't express it- A dedicated
CssVariantonly for real languages with reference compilers (css/scss/less). Never for a plugin.
Parser-side leniencies (in the oxc-css-parser fork) must be additive:
accept only input that previously errored, and never change the AST of input that already parsed (e.g. dotted words try the typed foo.$var / foo.bar(...) parse first; only dart-sass-invalid shapes take the lenient path).
Every lenient path carries a comment citing the reference-compiler vs postcss behavior, a test pinning the strict shapes, and shows up as a visible expected-error flip in the parser's conformance snapshots.
Triage order for reports follows the shared policy (don't corrupt → then accept → then pretty-print); the CSS-shaped example of the UNSAFE failure is silent token corruption like sandstone.10 → sandstone 0.1.
Red flags that the approach is drifting: specific plugin names accumulating in code, or a leniency that reinterprets previously-valid input. Either means the fix is at the wrong rung.
Supported: postcss-simple-vars (auto-enabled for CssVariant::Css)
Covered:
$var: value !important;declarations (top-level and inside rules)$varreferences in property values$varreferences inside@media/at-rule preludes
NOT covered: $(var) interpolation (margin-$(dir): 10px, .icon.is-$(network)), selector-position bare $var (.$prefix), comment substitutions (<<$(var)>>).
Known divergences
Admission reasons and rules: see FORMATTER_POLICY.md "Known divergences". Notable divergences are:
- An end-of-line trailing
//comment never counts toward the print width (reason 3)- Prettier's postcss printer does not distinguish
//from/* */: it prints both inline, measures them, and breaks the preceding value on overflow - We emit the
//as aline_suffix, the treatment every other formatter crate, and Prettier itself outside the CSS family, gives line comments - Scope: END-OF-LINE positions (statement-level trailing, SCSS map/config trailing).
A value-interior
//(before), glued to an argument, inside a fill entry) stays inline, because tokens still follow on the line and aline_suffixwould move the comment across them - Trailing
/* */comments still count, matching Prettier (both here and in the JS formatter, self-delimiting comments are inline content)
- Prettier's postcss printer does not distinguish
- A COMMENTED keyframe selector list is formatted structurally (one selector per line, comments per the separator rule:
60% /* mid */,)- Prettier keeps the whole list verbatim on one line, interior spacing included (
60% /* mid */ , 70%survives untouched) - Ours prints commented and uncommented lists with the same layout; layout-only, rare trigger
- Prettier keeps the whole list verbatim on one line, interior spacing included (
@nest <selector-list>continuation lines indent at +2- Prettier lands at +4 (comma-separated selectors) / +6 (wrapped selector parts)
- An artifact of its generic at-rule params indent
- Ours matches how selector lists indent everywhere else
- This is layout-only, deprecated syntax, triggers only on width overflow
- Prettier lands at +4 (comma-separated selectors) / +6 (wrapped selector parts)
- A leading
+in anAn+Bargument stays glued to its term (:nth-child(+3n - 2)keeps+3n)- Prettier prints
+ 3n(postcss-selector-parser tokenizes every+as a combinator), but the An+B grammar forbids whitespace between a leading sign and its term, so Prettier's output no longer parses as a selector (= a semantics-breaking artifact, it also breaks formatter idempotency: the second pass fails to parse)
- Prettier prints
- SCSS:
@forwardwithshow/hidemembers AND awith (...)config- Prettier parses the whole prelude as ONE comma list, so the config's forced break spills into the member commas
(
show b,\n c with (even when the head fits) and the config body lands one level deeper (+4 body / +2)) - We break members only on width overflow (fill, matching Prettier's break positions when no config is present)
and keep the config at the standalone
with (...)indent (+2 body / 0), same as@use) - Layout-only, rare combo
- Prettier parses the whole prelude as ONE comma list, so the config's forced break spills into the member commas
(
- SCSS:
@forwardmembers after an over-wide FIRST member pack at +2- The prelude head (
path,as <ns>/as <prefix>-*,show/hide, members) is one flat fill, so overflow breaks at the token seams with a +2 continuation- Matching Prettier's break points (its params are a comma list of
line-joined words in a fill), incl. the trailing-;exclusion from the last chunk's fit
- Matching Prettier's break points (its params are a comma list of
- The one difference: when the FIRST member alone overflows, Prettier indents it at +4 and puts
every later member on its own line at +2 (artifacts of its nested comma-chunk fill);
our flat fill packs the continuation members at +2 (
show\n <wide-member>, second;) - Remaining printers of this overflow class (heads that still never break):
@forbounds and@namespace— Prettier value-parses both, so it breaks their word seams too; extend the same fill shape if reported
- The prelude head (
<general-enclosed>media preludes (@media (not all),(screen and (color))unparsable as<media-condition>) normalize whitespace fully- Source gap → one space, paren inner edges tight
- Prettier only collapses space RUNS inside the unparsable paren, leaving
(not ( screen and ( color ) ))- We print
(not (screen and (color)))
- We print
- Reproducing the half-normalization is pure tokenizer-artifact matching;
- Gap-based spacing never fuses tokens the source kept apart (
and (can't become a function tokenand()
- Gap-based spacing never fuses tokens the source kept apart (
- A source-glued value-position
[...]stays glued to ANY typed left neighbor and prints verbatimtheme(fontSize.af-md[0]),foo[0.50]: matching Prettier, which lexes the run as ONE postcss word; But alsovar(--x)[0], where Prettier printsvar(--x) [0])- Prettier's space there is a word-lexing artifact (
[extends a word, but not across)) - Ours is one gap-based rule for all variants: never add a space the source doesn't have
- Prettier's space there is a word-lexing artifact (
- Less lookups (
@config[@key]) are unaffected: the typed lookup rule wins and keeps printing structurally - With the name GLUED to the
((--viewport-medium(width<=50rem)) - Prettier keeps the whole prelude verbatim (ONE
media-typetoken) - With the name spaced, Prettier still only collapses whitespace RUNS in the prelude (
( width >=500px )→(width >=500px), glue kept);- we print the structured form either way, identical to the same query inside
@media, where Prettier agrees
- we print the structured form either way, identical to the same query inside
- A declaration swallowed by a
;-less css-in-js placeholder (${m}\ncolor: red)- We parse it structurally and FORMAT it (spacing/hex/number normalization)
- Prettier keeps it verbatim, postcss swallows the run as an opaque prelude string it can't format, so
color : red/#FFFFFFsurvive unformatted
- SCSS: Selector-position Sass interpolation normalizes inner spaces (
#{ $name }→#{$name})- We normalize BOTH positions for output consistency
- Prettier keeps SELECTOR interpolation verbatim
- SCSS:
@warn/@errorprelude strings re-quote persingleQuoteoption (@error "x"→@error 'x')oxc-css-parserparses them asSassExpr, so they go through the structured printer (seeat_rule.rs)- Prettier keeps them as a raw string verbatim
- SCSS: A function call directly after a
//comment in nested-args position- Prettier double-indents it
- We print the normal indent (prettier/prettier#19427)
- SCSS: A nested map value prints at the SAME indent in every block context
- Prettier double-indents it (closing
)floating between levels) when the nearest at-rule ancestor is a control directive (@if/@else/@for/@each/@while; selector blocks in between don't shield) = identical source, different indent per context
- Prettier double-indents it (closing
- SCSS: A comment-preceded block map value also prints at the normal nested-map indent
- Prettier double-indents it (
+6body /+4)) - Its dedent applies only when the pair doc is a plain
group(indent(fill)), and a leading comment changes the doc shape - Comment presence must not change layout; same dedent-skip artifact class as the entries above (paren-block KEYS still keep the pair indent, matching Prettier: that trigger is content, not trivia)
- Prettier double-indents it (
- SCSS: The map-item break (one element per line + trailing comma) applies ONLY to parens whose contents are already a comma-separated list (semantics)
(x,)is a single-element list in Sass, so the added comma is a semantic no-op for a comma list and NOWHERE else- Prettier 3.9.6 changes
key: ($a + $b)from a number to a list, restructureskey: (a b)(2-element space list → nested 1-element list), and emits non-compiling output forkey: 2 * ($a + $b)inside$var:declarations (dart-sass:Undefined operation "2 * (3px,)") - Prettier's own #18530 (math siblings in args) / #19091 (single-node scalars) fixed subsets of this; we extend the same rule to every non-comma-list, so these stay inline
- SCSS: An own-line trailing comment before a list's closing
)keeps its own line- Applies to maps AND
@use/@forward with (...)configs ($e: 5\n // c\n)stays as-is; for maps the trailing comma is also kept) - Prettier pulls the comment up onto the last item's line (
$e: 5 // c, alineSuffixartifact of its comma-group printing) and drops the map's trailing comma - Same-line trailing comments still glue (matching Prettier); moving an own-line comment up would destroy the author's visual grouping
- Applies to maps AND
- SCSS: A map whose FIRST item is preceded by a block comment always breaks one-per-line
- Prettier stops treating it as a map item (the comment becomes
groups[0], soisKeyValuePairInParenGroupNodefails) and inlines it when it fits:$b: (/* c */ a: 1); - We reproduce the map-item-ness loss for the trailing comma (dropped, like Prettier)
- But keep the forced break; matching the inline layout needs comment support in the soft map path
- Prettier stops treating it as a map item (the comment becomes
- SCSS: Consecutive
//comments in a comment-only map indent uniformly- Prettier misaligns the second one with a stray extra leading space (
// b), an artifact of itsjoin(line)separator printing before the deferredlineSuffixflushes - A meaningless glitch (may well be fixed upstream); we print the normal indent
- Prettier misaligns the second one with a stray extra leading space (
- SCSS: A
;-less custom-property rule block followed by another declaration (--p: {color:red;} /* <- no semi */ --q: blue;)- SCSS output: we treat it as two separate declarations: format the inner block, add the missing
;, and format--qnormally - Prettier behavior: keeps the whole run verbatim, postcss swallows everything past the
}as an opaque prelude string until a source; - Why SCSS only: It falls out of the AST shape
oxc-css-parserproduces- SCSS parses
{...}declaration values asSassNestingDeclaration, so the formatter handles them like any other nested block - CSS/Less do NOT structure
{...}in declaration value position so the token-soup fallback runs, the formatter emits verbatim, and the output incidentally matches Prettier - Each mode is internally consistent with what its parser produces
- SCSS parses
- The value syntax
--p: { ... }itself is valid CSS, but its only intended consumer was the@apply --p;at-rule from the dropped CSS Apply Rule proposal- With no consumer, real-world usage is near zero, so the cross-mode behavior difference is theoretical
- SCSS output: we treat it as two separate declarations: format the inner block, add the missing
- Less: statement-position
&:extend(...)breaks only on overflow, like the selector-position form- Prettier (3.9.5+) ALWAYS breaks multiple selectors one per line there and never breaks a single one: postcss-less models the statement as a rule node, so the top-level selector-list printer (hardline commas) leaks into the parens (prettier#19550 only fixed the indentation)
- Ours prints BOTH positions with the same pseudo-args layout for consistency (inline when it fits; parens on their own lines + one selector per line on overflow, the same shape as Prettier's break)
- Less:
func(x, + 20px)unary gluing- Prettier prints
+20px;oxc-css-parserASTs, +as a comma-left binary operation, so matching is ad-hoc for a torture-test-only shape
- Prettier prints
- Less: Nested math in a function arg / multi-value shorthand
- Prettier's fill fit-check breaks INSIDE the wide chunk; our core
fill(biome semantics) breaks the SEPARATOR instead. - Principled fix is the shared core-fill fit-check change (needs JS-conformance impact experiment first)
- Prettier's fill fit-check breaks INSIDE the wide chunk; our core
- Less: Value-position
@{var}interpolationoxc-css-parserrejects it matchinglessc; Prettier (postcss) accepts and prints verbatim
- Less: Lookup with whitespace inside (
@config [ option1])oxc-css-parserrejects matchinglessc; Prettier accepts
Verification
Fixture tests
The harness snapshots both --print-width 80 and 100; verify fixtures at both widths.
Prettier conformance
At the current version (v3.9.6), the divergences of eight files have been confirmed and are intentional (see "Known divergences"):
- CSS:
css/stylefmt-repo/at-media/at-media.css,css/stylefmt-repo/cssnext-example/cssnext-example.css,css/stylefmt-repo/media-queries-ranges/media-queries-ranges.css,css/postcss-plugins/postcss-nesting.css - SCSS:
scss/comments/4878.scss,scss/map/function-argument/functional-argument.scss,scss/parens/issue-16594.scss,scss/variables/apply-rule.scss
Two more files fail with MIXED hunks; they can't pass as files (the intentional hunks alone keep them failing), so the remaining diffs are itemized here:
css/fill-value/fill.css(~96% match) one hunk:- a fill break-point inside a math-y value (
... * -1 +vs breaking before/ 2);- the "Less: nested math fill fit-check" divergence class (core-fill semantics)
- a fill break-point inside a math-y value (
css/parens/parens.css(~93% match) token-soup math spacing, three hunk classes:- intentional (Prettier artifact): Prettier splits SOME source-glued
-(into- (prop/prop44, an operator-heuristic side effect; ours keeps them all glued and consistent- and glues a source-spaced
+ 20px(prop34, the documented Lessfunc(x, + 20px)divergence in Css mode)
- normalization-direction difference (open question, low value)
- a math operator adjacent to a function/paren boundary gets uniform
opspacing from Prettier regardless of source (round(1.5)+2->round(1.5) + 2, calc*//); - ours preserves the source spacing per token (
prop13/14,prop57-60,prop73/74)
- a math operator adjacent to a function/paren boundary gets uniform
- within-a-word runs (
1+1+1+1,calc(100%+2px)) match- glued number-ish runs are ONE postcss word and print raw (see
is_word_glued_number)
- glued number-ish runs are ONE postcss word and print raw (see
- intentional (Prettier artifact): Prettier splits SOME source-glued
Manual checks
cargo run -p oxc_formatter_css --example css_formatter file.css # defaults to --print-width 80
cargo run -p oxc_formatter_css --example parse_debug -- --syntax scss file.scss # dump oxc-css-parser AST
cargo run -p oxc_formatter_css --example embedded_debug file.scss # format_to_ir entry
DUMP_IR=1 prints the FormatElement stream before printing.