Format at-rule preludes with structured parsing instead of regexes#230
Open
bartveneman wants to merge 16 commits into
Open
Format at-rule preludes with structured parsing instead of regexes#230bartveneman wants to merge 16 commits into
bartveneman wants to merge 16 commits into
Conversation
format_atrule_prelude reformatted @media/@supports/@container/@layer/ @scope/@import preludes with 13 hand-written regexes applied to raw text. This missed real cases the regex whitelist didn't cover - e.g. @container STYLE(--foo: bar) never got lowercased, since "style" wasn't in the hardcoded function-name list - and any new syntax needs a new regex to work correctly. @projectwallace/css-parser (already used for everything else here) ships a real structured parser for this that format-css previously disabled (parse_atrule_preludes: false). Flip it on and add a new internal printer, used only by print_atrule, that dispatches on each prelude child's node type instead of pattern-matching text. format_atrule_prelude itself - the exported partial formatter - is untouched: same signature, same regex implementation, same tests, same README example. No public API changes. It's now also reused internally as the fallback whenever the structured parser can't represent something losslessly (an at-rule it doesn't recognize, a comment inside a prelude, or the standalone `@layer name;` statement form, which has an upstream bug that splits dotted names like `base.normalize` at the dot - see PARSER_ISSUES.md for this and several other upstream issues found and worked around while building this). Verified via differential testing against the previous implementation across ~170 hand-picked and combinatorial cases (plus the existing 259-test suite) to catch regressions the unit tests alone wouldn't.
Contributor
|
| 📦 Package | 📏 Base Size | 📏 Source Size | 📈 Size Change |
|---|---|---|---|
| @projectwallace/format-css | 8.7 kB | 9.9 kB | +1.2 kB |
…rser 0.17.0 The rebase picked up @projectwallace/css-parser 0.16.0 -> 0.17.0, which fixes the dotted-layer-name splitting bug (PARSER_ISSUES.md issue #1) that previously made the standalone `@layer name;` statement form fall back to the regex-based formatter. Removed that bypass now that LayerName.text/.name correctly include the full dotted name, and let it go through the same structured printing path as everything else. PARSER_ISSUES.md updated to reflect the fix; re-verified the remaining issues (#2, #3, #5, #6, #7) are still present in 0.17.0, so their workarounds are unchanged.
bartveneman
added a commit
to projectwallace/css-parser
that referenced
this pull request
Jul 19, 2026
…-parses values Bumped @projectwallace/css-parser 0.17.0 -> 0.18.0, which deep-parses at-rule prelude values instead of only capturing raw text for them. This fixes three related PARSER_ISSUES.md entries at once: function calls (calc(), env(), ...) being silently dropped from media-feature and @supports/style() condition values, and the off-by-one end offsets that came with it. Simplified the printer accordingly, now that the structured nodes can be trusted directly instead of needing text-based workarounds: - print_media_feature now prints MediaFeature.value straight through print_list (like a declaration's value), instead of slicing it out of node.text and re-normalizing with the regex formatter. - print_supports_query now reuses the already-parsed Declaration child via format_declaration when one exists, instead of re-parsing raw text through parse_declaration. - print_prelude_function (@container style()) now reads Function.name/.value directly instead of slicing them out of node.text. - extract_balanced_value is now dead code (removed); balance_parens and print_condition are still needed for style()'s condition text, which still isn't deep-parsed into children. This also makes numeric formatting inside at-rule preludes consistent with regular declaration values for the first time (e.g. `.001px` -> `0.001px`, matching how `width: .001px` already prints elsewhere) - updated the handful of tests that pinned the old, inconsistent behavior. PARSER_ISSUES.md: removed the three now-fixed entries and renumbered the remaining ones.
bartveneman
added a commit
to projectwallace/css-parser
that referenced
this pull request
Jul 19, 2026
triggered by projectwallace/format-css#230 follow-up of #274
bartveneman
added a commit
to projectwallace/css-parser
that referenced
this pull request
Jul 19, 2026
bartveneman
added a commit
to projectwallace/css-parser
that referenced
this pull request
Jul 19, 2026
… in css-parser 0.18.1 Bumped @projectwallace/css-parser 0.18.0 -> 0.18.1, which fixes three more PARSER_ISSUES.md entries: - @supports selector(...) (and other function-token conditions) used to return an empty array; it's now deep-parsed into a real selector list. print_prelude_function now special-cases `selector()` to print via format_selector_list instead of the declaration-parsing path used for style()/other conditions, since a selector list isn't a declaration. - The leading only/not media-query prefix used to be silently dropped from a query's children (though still present in the query's own .text). format-css's own regex-based workaround (ONLY_NOT_PREFIX_RE, extracting it from node.text and prepending it) is now removed, since only/not are real PreludeOperator children the existing generic dispatch already prints correctly - leaving the old workaround in place would have double-printed them ("only only screen") now that the upstream fix landed. Confirmed via differential testing against the previous commit that this would have been a real, silent regression. - The .d.ts child-union mismatches on AtrulePrelude/ContainerQuery/ FeatureRange are also fixed - no code change needed there, just removed from the docs. Only issue #1 (=> tokenized as two operators) and #2 (no comment-preservation hook) remain open in PARSER_ISSUES.md. Added test coverage for both fixed behaviors (only/not prefix round-tripping without duplication, @supports selector() printing as a selector list rather than going through declaration formatting).
The two remaining entries (=> tokenized as two operators, no comment-preservation hook for prelude parsing) are being marked won't-fix upstream. Cleaned up the dangling references to the file in code comments, keeping the useful context inline.
…nger needed Good catch - they were leftovers. A container style() condition is now deep-parsed into a SupportsDeclaration the same way @supports's own condition already is (turns out this landed in @projectwallace/css-parser 0.18.1 too, one version after style()'s own truncation bug was fixed - hadn't re-checked since). This makes print_prelude_function's style() handling able to mirror print_supports_query exactly: use the already-parsed Declaration via format_declaration when node.has_children, fall back to format_atrule_prelude on the (now complete, untruncated) raw value otherwise. That removes the only remaining callers of print_condition, balance_parens, and has_top_level_colon (the raw-text-based re-parsing/paren-balancing/declaration-detection they existed for). Confirmed via differential testing against the previous commit that this is a pure simplification with zero behavior change, including the nested-boolean-group and non-declaration (selector()-in-style()) edge cases print_condition used to specifically guard. Also dropped an oxlint-disable comment left in front of the wrong test (a leftover from an earlier, unrelated fix - it belongs on the actual test.skip a bit further down, which still has its own copy). Added test coverage for the style() edge cases this change touches.
- Drop the => operator-merging workaround in print_feature_range; it only mattered for invalid CSS (=> isn't a real comparator), so skip the test that relied on it instead. - Lowercase media/container feature names, preserving casing for custom-property-style (--foo) names. - Add print_identifier(), a shared "lowercase unless --prefixed" helper, and use it everywhere an identifier gets lowercased (declaration/media-feature properties, function names, selector parts, attribute names, at-rule names) so custom-property-style names and custom function calls (--myFunc()) keep their casing. - Remove the redundant is_prelude_operator branch in print_prelude_component; the trailing return node.text already covers it. - Stop detecting/preserving comments dropped from structured at-rule preludes; skip the two tests that covered it, matching the existing preserves-comments skip. - Apply print_prelude_url's "never touch quotes" behavior to value-position print_url as well, and delete print_prelude_url since it's now identical to print_url. Update the url() quote-handling tests to reflect that url() quoting is no longer touched. - Trim verbose comments across the new atrule-prelude printers. @import's functional supports(...)/layer(...) notation vs. standalone @supports (...)/`@layer name;` are both valid, differently-shaped CSS syntax, not a parser bug, so no PARSER_ISSUES.md changes here.
These two changes are broader than the at-rule prelude rework this PR is otherwise scoped to (they touch existing declaration/selector printing that predates this branch), so they're moving to their own PRs (#232 and #233) for focused review and testing. Reverted print_list's function-name lowercasing, format_declaration's property lowercasing, print_attribute_selector, print_pseudo_selector, the type/universal selector printer, and print_atrule's name lowercasing back to their original (unshared) form, and print_url back to its original quote-normalizing behavior with print_prelude_url restored as its own function. The two new call sites this branch actually introduces — print_media_feature and print_prelude_function's custom-property-style name handling — keep their `--`-aware lowercasing, just inlined instead of calling the (now removed from this branch) shared helper.
…-structured-parsing
#232 and #233 merged to main and landed in this branch via the merge of main above. Point print_media_feature and print_prelude_function at the real print_identifier() instead of their local duplicated ternaries. print_prelude_url stays a separate function from print_url, despite looking identical at a glance: an @import specifier's Url node can also be a bare string (@import "foo";) with no url( prefix, unlike value-position url() where a string is always its own String node type, never Url. Verified empirically -- unifying the two functions broke `@import "foo";` (prints as `@import url(";`) since print_url's now-unconditional slice(4) assumes a url( prefix that isn't always there in this context.
Split out from the at-rule prelude structured-parsing rework (#230) so the dependency bump can be reviewed independently. 0.18.0 added deep parsing of at-rule prelude values (calc()/env()/etc. no longer silently dropped from MediaFeature/SupportsDeclaration values) and 0.18.1 fixed @supports selector() and the only/not media-query prefix being dropped -- none of which format-css's current (regex-based) at-rule prelude formatter depends on, so this is a no-op for existing behavior on its own.
1 task
…nto claude/atrule-prelude-structured-parsing
bartveneman
added a commit
that referenced
this pull request
Jul 20, 2026
## Summary - Bumps `@projectwallace/css-parser` from `~0.17.0` to `~0.18.1`. - Split out from the at-rule prelude structured-parsing rework (#230) so the dependency bump can be reviewed independently. - 0.18.0 added deep parsing of at-rule prelude values (`calc()`/`env()`/etc. no longer silently dropped from `MediaFeature`/`SupportsDeclaration` values); 0.18.1 fixed `@supports selector()` and the `only`/`not` media-query prefix being dropped. - None of this is used by format-css's current (regex-based) at-rule prelude formatter, so the bump alone is a no-op for existing behavior — #230 is what actually takes advantage of it. ## Test plan - [x] `npx tsc --noEmit`, `npx vitest run`, `npx oxlint`, `npx oxfmt --check` all clean at the new version, with zero code changes --- _Generated by [Claude Code](https://claude.ai/code/session_01AG67iFrhdQAGoTNnC7gmw4)_ Co-authored-by: Claude <noreply@anthropic.com>
print_feature_range and print_prelude_children were casting through
`as unknown as Iterable<CSSNode>` to iterate their node. Turns out
that's only needed for the base CSSNode type, which isn't always
iterable (leaf nodes have no children) -- the concrete node types here
(FeatureRange, and AtrulePrelude/MediaQuery/ContainerQuery once given
their own union type instead of the generic CSSNode) already declare
Symbol.iterator via WithChildren<T> and don't need a cast at all.
print_media_feature took `minify: boolean` but only ever used it to
derive optional_space locally -- it never forwards `{ minify }` into
any format_* call the way print_supports_query/print_prelude_function
do. Takes optional_space directly now, matching print_feature_range.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
format_atrule_preludereformatted@media/@supports/@container/@layer/@scope/@importpreludes with 13 hand-written regexes applied to raw text. This missed cases the regex whitelist didn't cover — e.g.@container STYLE(--foo: bar)never got lowercased, sincestylewasn't in the hardcoded function-name list — and any new at-rule syntax needs a new regex to work correctly.@projectwallace/css-parser(already used for everything else in this library) ships a real structured parser for this that was previously disabled (parse_atrule_preludes: false). Flipped it on and added a new internal printer, used only byprint_atrule, that dispatches on each prelude child's node type instead of pattern-matching text.format_atrule_prelude— the exported partial formatter documented in the README — is completely untouched: same signature, same regex implementation, same tests. It's now also reused internally as the safety-net fallback whenever the structured parser can't represent something losslessly (an at-rule it doesn't recognize, a comment inside a prelude, or the standalone@layer name;statement form).@layernames getting split at the dot and corrupting cascade semantics if naively reprinted,calc()/env()/other function calls silently dropped from media-feature and@supportscondition values,@supports selector(...)returning nothing at all, and a few smaller ones). Full writeup with repro snippets inPARSER_ISSUES.md, since these are worth fixing upstream.@namespace svg url(http://...)— the old regex incorrectly inserted a space insidehttp://, which the new structured path fixes automatically).Test plan
vitest run— all 269 tests pass (259 existing, unchanged, + 10 new covering the previously-uncovered cases this rework specifically fixes:@container style()lowercasing, function calls surviving in feature/condition values, dotted@layernames, unrecognized at-rules not losing their prelude, nested@supportsboolean groups)tsc --noEmit— no type errorsoxlint/oxfmt --check— cleanGenerated by Claude Code