Skip to content

Commit fe851f3

Browse files
Copilotjogibear9988
andcommitted
fix: remove all as any casts, add rawSource to types, make CssWhitespaceAST extend CssCommonPositionAST, remove unused closeWithPos, update docs
Co-authored-by: jogibear9988 <364896+jogibear9988@users.noreply.github.com>
1 parent ea23d3f commit fe851f3

7 files changed

Lines changed: 79 additions & 39 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Parses CSS code and returns an Abstract Syntax Tree (AST).
5555
- `options` (object, optional) - Parsing options
5656
- `silent` (boolean) - Silently fail on parse errors instead of throwing
5757
- `source` (string) - File path for better error reporting
58-
- `preserveFormatting` (boolean) - Store source offsets and original CSS text for identity round-trip (default: `false`)
58+
- `preserveFormatting` (boolean) - Insert whitespace AST nodes and store raw formatting properties for identity round-trip (default: `false`)
5959

6060
**Returns:** `CssStylesheetAST` - The parsed CSS as an AST
6161

docs/API.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Parses CSS code and returns an Abstract Syntax Tree (AST).
2222
- `options` (object, optional) - Parsing options
2323
- `silent` (boolean) - Silently fail on parse errors instead of throwing. When `true`, errors are collected in `ast.stylesheet.parsingErrors`
2424
- `source` (string) - File path for better error reporting
25-
- `preserveFormatting` (boolean) - Store source character offsets and original CSS text on the AST for identity round-trip. When `true`, position nodes include an `offset` field and `ast.stylesheet.originalSource` is set. Default: `false`
25+
- `preserveFormatting` (boolean) - Insert whitespace AST nodes and store raw formatting properties on nodes for identity round-trip. When `true`, whitespace between siblings is preserved as `CssWhitespaceAST` nodes, and raw formatting properties (`rawPrelude`, `rawBetween`, `rawValue`, `rawSource`) are stored on relevant nodes. Default: `false`
2626

2727
#### Returns
2828

@@ -54,7 +54,7 @@ Converts a CSS AST back to CSS string with configurable formatting.
5454
- `options` (CompilerOptions, optional) - Stringification options
5555
- `indent` (string) - Indentation string (default: `' '`)
5656
- `compress` (boolean) - Whether to compress/minify the output (default: `false`)
57-
- `identity` (boolean) - Reproduce the original CSS exactly as parsed. Requires `preserveFormatting: true` during parsing. Falls back to beautified output when original source is not available. Default: `false`
57+
- `identity` (boolean) - Reproduce the original CSS exactly as parsed. Requires `preserveFormatting: true` during parsing. Walks the AST including whitespace nodes and uses raw formatting properties to reconstruct the original output. Inserted or modified nodes without raw properties are emitted in beautified format. Falls back to beautified output when `preserveFormatting` was not used. Default: `false`
5858
- `removeEmptyRules` (boolean) - Remove rules with empty declaration blocks from the output. Works in all modes (beautified, compressed, identity). Default: `false`
5959

6060
#### Returns
@@ -95,9 +95,8 @@ type CssStylesheetAST = {
9595
type: CssTypes.stylesheet;
9696
stylesheet: {
9797
source?: string;
98-
rules: CssRuleAST[];
98+
rules: Array<CssAtRuleAST | CssWhitespaceAST>;
9999
parsingErrors?: CssParseError[];
100-
originalSource?: string; // Set when preserveFormatting is true
101100
};
102101
};
103102
```
@@ -163,12 +162,10 @@ type CssPosition = {
163162
start: {
164163
line: number;
165164
column: number;
166-
offset?: number; // Set when preserveFormatting is true
167165
};
168166
end: {
169167
line: number;
170168
column: number;
171-
offset?: number; // Set when preserveFormatting is true
172169
};
173170
};
174171
```

docs/AST.md

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ Position information for the node in the source code:
1818

1919
```typescript
2020
{
21-
start: { line: number; column: number; offset?: number };
22-
end: { line: number; column: number; offset?: number };
21+
start: { line: number; column: number };
22+
end: { line: number; column: number };
2323
source?: string;
2424
}
2525
```
2626

27-
The `offset` field is a character offset into the original source string, present when `preserveFormatting: true` is used during parsing.
28-
2927
### `parent` (optional)
3028

3129
Reference to the parent node in the AST.
@@ -38,9 +36,8 @@ The root node representing an entire CSS document.
3836

3937
**Properties:**
4038
- `stylesheet.source` (optional): Source file path
41-
- `stylesheet.rules`: Array of top-level rules
39+
- `stylesheet.rules`: Array of top-level rules (may include `whitespace` nodes when `preserveFormatting: true`)
4240
- `stylesheet.parsingErrors` (optional): Array of parse errors when `silent` option is used
43-
- `stylesheet.originalSource` (optional): The original CSS source string, present when `preserveFormatting: true` is used during parsing
4441

4542
**Example:**
4643
```json
@@ -109,6 +106,21 @@ A CSS comment.
109106
}
110107
```
111108

109+
### `whitespace`
110+
111+
A whitespace node that preserves original formatting between sibling nodes. Only present when `preserveFormatting: true` is used during parsing.
112+
113+
**Properties:**
114+
- `value`: The whitespace text (spaces, newlines, tabs, etc.)
115+
116+
**Example:**
117+
```json
118+
{
119+
"type": "whitespace",
120+
"value": "\n\n"
121+
}
122+
```
123+
112124
### `media`
113125

114126
A `@media` rule.
@@ -367,3 +379,39 @@ This is useful for:
367379
- Source mapping
368380
- Code analysis tools
369381
- IDE integration
382+
383+
## Formatting Properties (`preserveFormatting: true`)
384+
385+
When parsing with `preserveFormatting: true`, the parser stores additional formatting metadata on AST nodes to support exact round-trip via identity mode. These properties are optional and only present when formatting is preserved.
386+
387+
### Whitespace Nodes
388+
389+
`CssWhitespaceAST` nodes are inserted between sibling nodes in all arrays (rules, declarations, keyframes) to preserve the original whitespace and line breaks.
390+
391+
### Raw Properties on Nodes
392+
393+
- **`rawPrelude`** (on rules, at-rules with blocks): The exact original text before `{`, preserving whitespace between selector/condition and brace
394+
- **`rawBetween`** (on declarations): The text between the property name and value, including `:` and any surrounding whitespace (e.g., `": "` or `": "`)
395+
- **`rawValue`** (on declarations): The untrimmed original value text
396+
- **`rawSource`** (on statement at-rules: import, charset, namespace, custom-media, layer statement): The exact original text of the entire at-rule
397+
398+
### AST Modification with Identity Mode
399+
400+
Because formatting data is stored per-node rather than as a single cached string, you can freely insert, remove, or modify AST nodes and identity mode will reflect the changes:
401+
402+
```typescript
403+
const ast = parse(css, { preserveFormatting: true });
404+
405+
// Insert a new rule — it will be beautified since it has no raw properties
406+
ast.stylesheet.rules.push({
407+
type: CssTypes.rule,
408+
selectors: ['.new'],
409+
declarations: [{ type: CssTypes.declaration, property: 'color', value: 'blue' }]
410+
});
411+
412+
// Remove a rule — the surrounding whitespace nodes naturally adapt
413+
ast.stylesheet.rules.splice(1, 1);
414+
415+
// Identity mode outputs original formatting for untouched nodes + beautified for new nodes
416+
stringify(ast, { identity: true });
417+
```

docs/CHANGELOG.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11-
- Add `preserveFormatting` parser option to store source character offsets and original CSS text on the AST
12-
- Add `identity` compiler option to reproduce original CSS exactly as parsed (round-trip fidelity)
11+
- Add `preserveFormatting` parser option to insert whitespace AST nodes and store raw formatting properties for identity round-trip
12+
- Add `CssTypes.whitespace` node type (`CssWhitespaceAST`) to represent whitespace between sibling nodes
13+
- Add `identity` compiler option to reproduce original CSS exactly as parsed (round-trip fidelity), with support for AST modifications
1314
- Add `removeEmptyRules` compiler option to strip rules with empty declaration blocks
14-
- Add optional `offset` field to position `start`/`end` for source reconstruction
15-
- Add `originalSource` field to `CssStylesheetAST` for identity mode
16-
- Export `CompilerOptions` and `ParseOptions` types from the public API
15+
- Add raw formatting properties: `rawPrelude`, `rawBetween`, `rawValue`, `rawSource` on relevant AST nodes
16+
- Export `CompilerOptions`, `ParseOptions`, and `CssWhitespaceAST` types from the public API
1717

1818
## [4.4.4] - 2025-07-22
1919

src/parse/index.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,6 @@ export const parse = (
232232
return { ok: lexer.tryOpenBrace(), afterOpen };
233233
}
234234

235-
/**
236-
* Track brace position and call close().
237-
*/
238-
function closeWithPos(): { ok: boolean; beforeClose: number } {
239-
const beforeClose = lexer.pos;
240-
return { ok: lexer.tryCloseBrace(), beforeClose };
241-
}
242-
243235
/**
244236
* Closing brace.
245237
*/

src/stringify/compiler.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,8 @@ class Compiler {
338338
this.emit('}')
339339
);
340340
}
341-
if (this.identity && !node.rules && (node as any).rawSource) {
342-
return this.emit((node as any).rawSource, node.position);
341+
if (this.identity && !node.rules && node.rawSource) {
342+
return this.emit(node.rawSource, node.position);
343343
}
344344
const rules = node.rules
345345
? this.stripWhitespace(<CssAllNodesAST[]>node.rules)
@@ -364,8 +364,8 @@ class Compiler {
364364
* Visit import node.
365365
*/
366366
import(node: CssImportAST) {
367-
if (this.identity && (node as any).rawSource) {
368-
return this.emit((node as any).rawSource, node.position);
367+
if (this.identity && node.rawSource) {
368+
return this.emit(node.rawSource, node.position);
369369
}
370370
return this.emit(`@import ${node.import};`, node.position);
371371
}
@@ -416,8 +416,8 @@ class Compiler {
416416
* Visit charset node.
417417
*/
418418
charset(node: CssCharsetAST) {
419-
if (this.identity && (node as any).rawSource) {
420-
return this.emit((node as any).rawSource, node.position);
419+
if (this.identity && node.rawSource) {
420+
return this.emit(node.rawSource, node.position);
421421
}
422422
return this.emit(`@charset ${node.charset};`, node.position);
423423
}
@@ -426,8 +426,8 @@ class Compiler {
426426
* Visit namespace node.
427427
*/
428428
namespace(node: CssNamespaceAST) {
429-
if (this.identity && (node as any).rawSource) {
430-
return this.emit((node as any).rawSource, node.position);
429+
if (this.identity && node.rawSource) {
430+
return this.emit(node.rawSource, node.position);
431431
}
432432
return this.emit(`@namespace ${node.namespace};`, node.position);
433433
}
@@ -631,8 +631,8 @@ class Compiler {
631631
* Visit custom-media node.
632632
*/
633633
customMedia(node: CssCustomMediaAST) {
634-
if (this.identity && (node as any).rawSource) {
635-
return this.emit((node as any).rawSource, node.position);
634+
if (this.identity && node.rawSource) {
635+
return this.emit(node.rawSource, node.position);
636636
}
637637
return this.emit(
638638
`@custom-media ${node.name} ${node.media};`,

src/type.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,9 @@ export type CssCommonPositionAST = CssCommonAST & {
4141
parent?: unknown;
4242
};
4343

44-
export type CssWhitespaceAST = CssCommonAST & {
44+
export type CssWhitespaceAST = CssCommonPositionAST & {
4545
type: CssTypes.whitespace;
4646
value: string;
47-
position?: Position;
48-
parent?: unknown;
4947
};
5048

5149
export type CssStylesheetAST = CssCommonAST & {
@@ -88,11 +86,13 @@ export type CssContainerAST = CssCommonPositionAST & {
8886
export type CssCharsetAST = CssCommonPositionAST & {
8987
type: CssTypes.charset;
9088
charset: string;
89+
rawSource?: string;
9190
};
9291
export type CssCustomMediaAST = CssCommonPositionAST & {
9392
type: CssTypes.customMedia;
9493
name: string;
9594
media: string;
95+
rawSource?: string;
9696
};
9797
export type CssDocumentAST = CssCommonPositionAST & {
9898
type: CssTypes.document;
@@ -114,6 +114,7 @@ export type CssHostAST = CssCommonPositionAST & {
114114
export type CssImportAST = CssCommonPositionAST & {
115115
type: CssTypes.import;
116116
import: string;
117+
rawSource?: string;
117118
};
118119
export type CssKeyframesAST = CssCommonPositionAST & {
119120
type: CssTypes.keyframes;
@@ -133,6 +134,7 @@ export type CssLayerAST = CssCommonPositionAST & {
133134
layer: string;
134135
rules?: Array<CssAtRuleAST | CssDeclarationAST | CssWhitespaceAST>;
135136
rawPrelude?: string;
137+
rawSource?: string;
136138
};
137139
export type CssMediaAST = CssCommonPositionAST & {
138140
type: CssTypes.media;
@@ -143,6 +145,7 @@ export type CssMediaAST = CssCommonPositionAST & {
143145
export type CssNamespaceAST = CssCommonPositionAST & {
144146
type: CssTypes.namespace;
145147
namespace: string;
148+
rawSource?: string;
146149
};
147150
export type CssPageAST = CssCommonPositionAST & {
148151
type: CssTypes.page;

0 commit comments

Comments
 (0)