Skip to content

Commit 0b001ac

Browse files
committed
2 parents 48edf1d + 3727fed commit 0b001ac

15 files changed

Lines changed: 933 additions & 126 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ const formatted = stringify(ast, { indent: ' ' })
3232
// Minify output
3333
const minified = stringify(ast, { compress: true })
3434
// => "body{font-size:12px}"
35+
36+
// Identity mode: round-trip CSS exactly as written
37+
const original = 'body { font-size: 12px; }'
38+
const ast2 = parse(original, { preserveFormatting: true })
39+
stringify(ast2, { identity: true }) === original // true
40+
41+
// Remove empty rules
42+
const ast3 = parse('.empty {} .keep { color: red; }')
43+
stringify(ast3, { removeEmptyRules: true })
44+
// => ".keep {\n color: red;\n}"
3545
```
3646

3747
## API
@@ -45,6 +55,7 @@ Parses CSS code and returns an Abstract Syntax Tree (AST).
4555
- `options` (object, optional) - Parsing options
4656
- `silent` (boolean) - Silently fail on parse errors instead of throwing
4757
- `source` (string) - File path for better error reporting
58+
- `preserveFormatting` (boolean) - Insert whitespace AST nodes and store raw formatting properties for identity round-trip (default: `false`)
4859

4960
**Returns:** `CssStylesheetAST` - The parsed CSS as an AST
5061

@@ -57,6 +68,8 @@ Converts a CSS AST back to CSS string with configurable formatting.
5768
- `options` (object, optional) - Stringification options
5869
- `indent` (string) - Indentation string (default: `' '`)
5970
- `compress` (boolean) - Whether to compress/minify the output (default: `false`)
71+
- `identity` (boolean) - Reproduce the original CSS exactly as parsed; requires `preserveFormatting` during parsing (default: `false`)
72+
- `removeEmptyRules` (boolean) - Remove rules with empty declaration blocks (default: `false`)
6073

6174
**Returns:** `string` - The formatted CSS string
6275

@@ -65,7 +78,9 @@ Converts a CSS AST back to CSS string with configurable formatting.
6578
- **Complete CSS Support**: All standard CSS features including selectors, properties, values, at-rules, and comments
6679
- **TypeScript Support**: Full type definitions for all AST nodes and functions
6780
- **Error Handling**: Configurable error handling with detailed position information
68-
- **Formatting Options**: Pretty print, minify, or custom formatting
81+
- **Formatting Options**: Pretty print, minify, identity (round-trip), or custom formatting
82+
- **Identity Round-Trip**: Parse and stringify CSS back to the exact original formatting
83+
- **Empty Rule Removal**: Optionally strip rules with empty declaration blocks
6984
- **Performance Optimized**: Efficient parsing and stringification for large CSS files
7085
- **Source Maps**: Track original source positions for debugging and tooling
7186

docs/API.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +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) - 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`
2526

2627
#### Returns
2728

@@ -53,6 +54,8 @@ Converts a CSS AST back to CSS string with configurable formatting.
5354
- `options` (CompilerOptions, optional) - Stringification options
5455
- `indent` (string) - Indentation string (default: `' '`)
5556
- `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. 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`
58+
- `removeEmptyRules` (boolean) - Remove rules with empty declaration blocks from the output. Works in all modes (beautified, compressed, identity). Default: `false`
5659

5760
#### Returns
5861

@@ -92,7 +95,7 @@ type CssStylesheetAST = {
9295
type: CssTypes.stylesheet;
9396
stylesheet: {
9497
source?: string;
95-
rules: CssRuleAST[];
98+
rules: Array<CssAtRuleAST | CssWhitespaceAST>;
9699
parsingErrors?: CssParseError[];
97100
};
98101
};
@@ -188,8 +191,10 @@ Options for the stringifier.
188191

189192
```typescript
190193
type CompilerOptions = {
191-
indent?: string; // Default: ' '
192-
compress?: boolean; // Default: false
194+
indent?: string; // Default: ' '
195+
compress?: boolean; // Default: false
196+
identity?: boolean; // Default: false
197+
removeEmptyRules?: boolean; // Default: false
193198
};
194199
```
195200

@@ -290,6 +295,49 @@ console.log(compressed);
290295
// Output: .example{color:red;font-size:16px}
291296
```
292297
298+
### Identity Round-Trip
299+
300+
Reproduce the original CSS exactly as it was written, preserving all whitespace, comments, and formatting:
301+
302+
```javascript
303+
import { parse, stringify } from '@adobe/css-tools';
304+
305+
const css = '.example { color: red; font-size:16px }';
306+
307+
// Parse with preserveFormatting to store original source
308+
const ast = parse(css, { preserveFormatting: true });
309+
310+
// Stringify with identity mode to reproduce the original CSS
311+
const output = stringify(ast, { identity: true });
312+
console.log(output === css); // true
313+
```
314+
315+
When `preserveFormatting` was not used during parsing, identity mode falls back to beautified output.
316+
317+
### Removing Empty Rules
318+
319+
Strip rules with empty declaration blocks from the output:
320+
321+
```javascript
322+
import { parse, stringify } from '@adobe/css-tools';
323+
324+
const css = '.empty {} .keep { color: red; }';
325+
const ast = parse(css);
326+
327+
// Beautified without empty rules
328+
const output = stringify(ast, { removeEmptyRules: true });
329+
console.log(output);
330+
// Output:
331+
// .keep {
332+
// color: red;
333+
// }
334+
335+
// Also works with compressed mode
336+
const compressed = stringify(ast, { compress: true, removeEmptyRules: true });
337+
console.log(compressed);
338+
// Output: .keep{color:red;}
339+
```
340+
293341
## TypeScript Integration
294342
295343
The library provides comprehensive TypeScript support with full type definitions for all AST nodes and functions:

docs/AST.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ The root node representing an entire CSS document.
3636

3737
**Properties:**
3838
- `stylesheet.source` (optional): Source file path
39-
- `stylesheet.rules`: Array of top-level rules
39+
- `stylesheet.rules`: Array of top-level rules (may include `whitespace` nodes when `preserveFormatting: true`)
4040
- `stylesheet.parsingErrors` (optional): Array of parse errors when `silent` option is used
4141

4242
**Example:**
@@ -106,6 +106,21 @@ A CSS comment.
106106
}
107107
```
108108

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+
109124
### `media`
110125

111126
A `@media` rule.
@@ -364,3 +379,39 @@ This is useful for:
364379
- Source mapping
365380
- Code analysis tools
366381
- 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
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
14+
- Add `removeEmptyRules` compiler option to strip rules with empty declaration blocks
15+
- Add raw formatting properties: `rawPrelude`, `rawBetween`, `rawValue`, `rawSource` on relevant AST nodes
16+
- Export `CompilerOptions`, `ParseOptions`, and `CssWhitespaceAST` types from the public API
17+
818
## [4.4.4] - 2025-07-22
919

1020
### Changed

docs/EXAMPLES.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,53 @@ console.log(formatted);
245245
// }
246246
```
247247
248+
### Identity Round-Trip
249+
250+
Reproduce the original CSS exactly as it was written, preserving all whitespace,
251+
comments, and formatting:
252+
253+
```javascript
254+
import { parse, stringify } from '@adobe/css-tools';
255+
256+
const css = `body {
257+
font-size: 12px;
258+
color: #333;
259+
}`;
260+
261+
// Parse with preserveFormatting to store original source
262+
const ast = parse(css, { preserveFormatting: true });
263+
264+
// Stringify with identity mode
265+
const output = stringify(ast, { identity: true });
266+
console.log(output === css); // true — exact round-trip
267+
```
268+
269+
### Removing Empty Rules
270+
271+
```javascript
272+
import { parse, stringify } from '@adobe/css-tools';
273+
274+
const css = `
275+
.unused {}
276+
.active { color: red; }
277+
`;
278+
279+
const ast = parse(css);
280+
281+
// Remove empty rules in beautified output
282+
const output = stringify(ast, { removeEmptyRules: true });
283+
console.log(output);
284+
// Output:
285+
// .active {
286+
// color: red;
287+
// }
288+
289+
// Also works with compressed mode
290+
const compressed = stringify(ast, { compress: true, removeEmptyRules: true });
291+
console.log(compressed);
292+
// Output: .active{color:red;}
293+
```
294+
248295
## Working with Complex CSS
249296
250297
### Nested Rules and At-Rules

src/CssPosition.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
* Store position information for a node
33
*/
44
export default class Position {
5-
start: { line: number; column: number };
6-
end: { line: number; column: number };
5+
start: { line: number; column: number; offset?: number };
6+
end: { line: number; column: number; offset?: number };
77
source?: string;
88

99
constructor(
10-
start: { line: number; column: number },
11-
end: { line: number; column: number },
10+
start: { line: number; column: number; offset?: number },
11+
end: { line: number; column: number; offset?: number },
1212
source: string,
1313
) {
1414
this.start = start;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ export const parse = parseFn;
44
export const stringify = stringifyFn;
55
export * from './CssParseError';
66
export * from './CssPosition';
7+
export type { ParseOptions } from './parse';
8+
export type { CompilerOptions } from './stringify';
79
export * from './type';
810
export default { parse, stringify };

0 commit comments

Comments
 (0)