0
# PostCSS Safe Parser
1
2
PostCSS Safe Parser is a fault-tolerant CSS parser for PostCSS that can handle malformed or incomplete CSS syntax without throwing errors. It automatically finds and fixes syntax errors, making it capable of parsing any CSS input including legacy code with browser hacks and incomplete stylesheets.
3
4
## Package Information
5
6
- **Package Name**: postcss-safe-parser
7
- **Package Type**: npm
8
- **Language**: JavaScript
9
- **Installation**: `npm install postcss-safe-parser`
10
11
## Core Imports
12
13
```javascript
14
const safe = require('postcss-safe-parser');
15
```
16
17
ESM (if using a build tool that supports it):
18
19
```javascript
20
import safe from 'postcss-safe-parser';
21
```
22
23
## Basic Usage
24
25
```javascript
26
const postcss = require('postcss');
27
const safe = require('postcss-safe-parser');
28
29
// Parse malformed CSS that would break the standard parser
30
const badCss = 'a { color: black';
31
const result = await postcss().process(badCss, { parser: safe });
32
console.log(result.css); // 'a { color: black}'
33
34
// Direct parsing without PostCSS
35
const root = safe('a { /* unclosed comment');
36
console.log(root.toString()); // 'a { /* unclosed comment */}'
37
```
38
39
## Architecture
40
41
PostCSS Safe Parser extends PostCSS's core parser with fault-tolerant parsing capabilities. The architecture consists of:
42
43
- **SafeParser Class**: Extends PostCSS's `Parser` class, overriding key methods to handle malformed syntax
44
- **Error Recovery System**: Custom tokenizer with `ignoreErrors: true` that continues parsing despite syntax errors
45
- **Graceful Degradation**: Methods like `checkMissedSemicolon()`, `unclosedBracket()`, and `unexpectedClose()` are overridden to handle errors silently
46
- **PostCSS Integration**: Seamlessly integrates with PostCSS's processing pipeline as a drop-in parser replacement
47
48
The safe parser creates the same PostCSS AST structure as the standard parser, ensuring full compatibility with all PostCSS plugins and tools.
49
50
## Capabilities
51
52
### Safe CSS Parsing
53
54
Parses CSS with automatic error recovery and syntax fixing.
55
56
```javascript { .api }
57
/**
58
* Parse CSS string with fault tolerance
59
* @param css - CSS string to parse (can contain syntax errors)
60
* @param opts - Options object passed to PostCSS Input constructor
61
* @param opts.from - Input file path for source map generation
62
* @param opts.to - Output file path for source map generation
63
* @param opts.origin - Custom origin function for Input
64
* @param opts.map - Source map options or boolean
65
* @returns PostCSS Root AST node with fault-tolerant parsing applied
66
*/
67
function safeParse(css, opts = {});
68
```
69
70
**Parameters:**
71
72
- `css` (string): CSS input to parse - can contain malformed syntax, unclosed blocks, missing semicolons, etc.
73
- `opts` (object, optional): Options object passed to PostCSS Input constructor
74
- `from` (string): Input file path for source map generation
75
- `origin` (function): Custom origin function for Input
76
- Any other options supported by PostCSS Input
77
78
**Returns:**
79
80
PostCSS Root node containing the parsed CSS AST with all syntax errors automatically fixed.
81
82
**Error Recovery Features:**
83
84
The parser automatically handles and fixes:
85
86
- **Unclosed blocks**: `a {` becomes `a {}`
87
- **Unclosed comments**: `/* comment` becomes `/* comment */`
88
- **Missing semicolons**: `a { color: red font-size: 12px }` becomes `a { color: red; font-size: 12px }`
89
- **Unclosed quotes**: `content: "text` becomes `content: "text"`
90
- **Unclosed brackets**: `:not(input` becomes `:not(input)`
91
- **Properties without values**: `a { color; }` preserves structure
92
- **Nameless at-rules**: `@` becomes valid at-rule node
93
- **Double colons**: `a { prop:: value }` becomes `a { prop: : value }`
94
- **Complex nested JSON-like properties**: Handles CSS custom properties with JSON values
95
96
**Usage Examples:**
97
98
```javascript
99
// Parse CSS with unclosed blocks
100
const root = safe('@media (screen) { a {\n');
101
console.log(root.toString()); // '@media (screen) { a {\n}}'
102
103
// Parse CSS with missing semicolons
104
const root = safe('a { color: red font-size: 12px }');
105
console.log(root.toString()); // 'a { color: red; font-size: 12px }'
106
107
// Parse CSS with unclosed comments
108
const root = safe('a { /* comment ');
109
console.log(root.toString()); // 'a { /* comment */}'
110
111
// Parse CSS with complex JSON-like custom properties
112
const root = safe(':root { --config: {"nested": {"key": "value"}}; }');
113
console.log(root.toString()); // Preserves the complex structure
114
```
115
116
### Integration with PostCSS
117
118
Use as a parser option in PostCSS processing pipelines.
119
120
```javascript { .api }
121
// Standard PostCSS integration
122
postcss(plugins).process(css, { parser: safeParse })
123
124
// With async/await
125
const result = await postcss([autoprefixer]).process(badCss, {
126
parser: safe,
127
from: 'input.css'
128
});
129
```
130
131
**Common Integration Patterns:**
132
133
```javascript
134
const postcss = require('postcss');
135
const autoprefixer = require('autoprefixer');
136
const safe = require('postcss-safe-parser');
137
138
// Live CSS editing tools - parse as user types
139
async function parseUserInput(userCss) {
140
try {
141
const result = await postcss([autoprefixer])
142
.process(userCss, { parser: safe });
143
return result.css;
144
} catch (error) {
145
// Safe parser won't throw, but plugins might
146
return userCss;
147
}
148
}
149
150
// Legacy CSS processing
151
async function processLegacyCSS(legacyCss) {
152
const result = await postcss([
153
// Your plugins here
154
]).process(legacyCss, {
155
parser: safe,
156
from: 'legacy.css'
157
});
158
return result;
159
}
160
161
// Browser hacks and malformed CSS
162
const browserHacksCSS = `
163
.selector {
164
property: value\\9; /* IE hack */
165
*property: value; /* IE6/7 hack */
166
_property: value /* IE6 hack
167
`;
168
169
const cleaned = safe(browserHacksCSS);
170
console.log(cleaned.toString()); // Properly closed and parsed
171
```
172
173
### SafeParser Class
174
175
Direct access to the SafeParser class for advanced usage scenarios.
176
177
```javascript { .api }
178
const SafeParser = require('postcss-safe-parser/lib/safe-parser');
179
const { Input } = require('postcss');
180
181
/**
182
* SafeParser class extending PostCSS Parser
183
* Provides fault-tolerant CSS parsing with error recovery
184
*/
185
class SafeParser extends Parser {
186
constructor(input);
187
parse(): void;
188
createTokenizer(): void;
189
comment(token): void;
190
decl(tokens): void;
191
endFile(): void;
192
precheckMissedSemicolon(tokens): void;
193
unclosedBracket(): void;
194
unexpectedClose(): void;
195
unknownWord(tokens): void;
196
unnamedAtrule(node): void;
197
}
198
```
199
200
**Direct SafeParser Usage:**
201
202
```javascript
203
const { Input } = require('postcss');
204
const SafeParser = require('postcss-safe-parser/lib/safe-parser');
205
206
// Create parser instance directly
207
const input = new Input(cssString, { from: 'input.css' });
208
const parser = new SafeParser(input);
209
parser.parse();
210
211
// Access the parsed root
212
const root = parser.root;
213
console.log(root.toString());
214
```
215
216
**Key Method Behaviors:**
217
218
- `comment(token)`: Handles unclosed comments by automatically closing them
219
- `decl(tokens)`: Validates declaration tokens before processing
220
- `endFile()`: Ensures proper file closure even with unclosed blocks
221
- `unclosedBracket()`: Silently handles unclosed brackets without throwing
222
- `unexpectedClose()`: Handles extra closing braces by adding them to `raws.after`
223
- `unknownWord(tokens)`: Treats unknown tokens as whitespace instead of throwing errors
224
225
**Advanced Usage Patterns:**
226
227
```javascript
228
// Custom input processing with source maps
229
const { Input } = require('postcss');
230
const SafeParser = require('postcss-safe-parser/lib/safe-parser');
231
232
function parseWithCustomInput(css, filename) {
233
const input = new Input(css, {
234
from: filename,
235
origin: (offset, file) => {
236
// Custom source mapping logic
237
const lines = css.substring(0, offset).split('\n');
238
return { line: lines.length, col: lines[lines.length - 1].length + 1 };
239
}
240
});
241
242
const parser = new SafeParser(input);
243
parser.parse();
244
return parser.root;
245
}
246
247
// Parser with custom error recovery tracking
248
class TrackedSafeParser extends SafeParser {
249
constructor(input) {
250
super(input);
251
this.recoveredErrors = [];
252
}
253
254
unexpectedClose() {
255
super.unexpectedClose();
256
this.recoveredErrors.push({
257
type: 'unexpected_close',
258
position: this.tokenizer.position()
259
});
260
}
261
262
unclosedBracket() {
263
super.unclosedBracket();
264
this.recoveredErrors.push({
265
type: 'unclosed_bracket',
266
position: this.tokenizer.position()
267
});
268
}
269
}
270
```
271
272
## Error Handling
273
274
The safe parser is designed to never throw parsing errors. It will always return a valid PostCSS AST, regardless of the input CSS quality. However, downstream PostCSS plugins may still throw errors during processing.
275
276
```javascript
277
// Safe parser never throws
278
const root = safe('completely { malformed css {{{'); // Always succeeds
279
280
// But plugins might throw during processing
281
try {
282
const result = await postcss([somePlugin])
283
.process(malformedCss, { parser: safe });
284
} catch (pluginError) {
285
// Handle plugin errors, not parser errors
286
}
287
```
288
289
## Types
290
291
For TypeScript projects, the parser follows PostCSS type definitions:
292
293
```typescript { .api }
294
import { Root, Input, Parser, Node, Comment, Rule, AtRule, Declaration } from 'postcss';
295
296
/**
297
* Main safe parsing function
298
* @param css - CSS string to parse with fault tolerance
299
* @param opts - Options passed to PostCSS Input constructor
300
* @returns PostCSS Root AST node
301
*/
302
declare function safeParse(css: string, opts?: ProcessOptions): Root;
303
304
interface ProcessOptions {
305
from?: string;
306
to?: string;
307
origin?: (offset: number, file?: string) => { line: number; col: number };
308
map?: SourceMapOptions | boolean;
309
}
310
311
interface SourceMapOptions {
312
inline?: boolean;
313
prev?: string | object | boolean;
314
sourcesContent?: boolean;
315
annotation?: boolean | string;
316
from?: string;
317
to?: string;
318
}
319
320
/**
321
* SafeParser class extending PostCSS Parser
322
*/
323
declare class SafeParser extends Parser {
324
constructor(input: Input);
325
326
parse(): void;
327
createTokenizer(): void;
328
comment(token: [string, string, number, number]): void;
329
decl(tokens: Array<[string, string, number, number]>): void;
330
endFile(): void;
331
precheckMissedSemicolon(tokens: Array<[string, string, number, number]>): void;
332
unclosedBracket(): void;
333
unexpectedClose(): void;
334
unknownWord(tokens: Array<[string, string, number, number]>): void;
335
unnamedAtrule(node: AtRule): void;
336
337
// Inherited from Parser
338
root: Root;
339
input: Input;
340
current: Node;
341
spaces: string;
342
semicolon: boolean;
343
customProperty: boolean;
344
}
345
346
interface Root {
347
type: 'root';
348
nodes: Node[];
349
source?: {
350
input: Input;
351
start?: { line: number; column: number; offset: number };
352
end?: { line: number; column: number; offset: number };
353
};
354
raws: {
355
before?: string;
356
after?: string;
357
semicolon?: boolean;
358
};
359
360
// Methods
361
toString(stringifier?: any): string;
362
toResult(opts?: any): any;
363
append(...nodes: Node[]): Root;
364
prepend(...nodes: Node[]): Root;
365
insertAfter(exist: Node, add: Node): Root;
366
insertBefore(exist: Node, add: Node): Root;
367
removeAll(): Root;
368
removeChild(child: Node): Root;
369
each(callback: (node: Node, index: number) => void | false): void;
370
walk(callback: (node: Node, index: number) => void | false): void;
371
walkRules(callback: (rule: Rule, index: number) => void | false): void;
372
walkAtRules(callback: (atrule: AtRule, index: number) => void | false): void;
373
walkComments(callback: (comment: Comment, index: number) => void | false): void;
374
walkDecls(callback: (decl: Declaration, index: number) => void | false): void;
375
}
376
```