Fault-tolerant CSS parser for PostCSS that can handle malformed or incomplete CSS syntax without throwing errors
npx @tessl/cli install tessl/npm-postcss-safe-parser@7.0.0PostCSS 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.
npm install postcss-safe-parserconst safe = require('postcss-safe-parser');ESM (if using a build tool that supports it):
import safe from 'postcss-safe-parser';const postcss = require('postcss');
const safe = require('postcss-safe-parser');
// Parse malformed CSS that would break the standard parser
const badCss = 'a { color: black';
const result = await postcss().process(badCss, { parser: safe });
console.log(result.css); // 'a { color: black}'
// Direct parsing without PostCSS
const root = safe('a { /* unclosed comment');
console.log(root.toString()); // 'a { /* unclosed comment */}'PostCSS Safe Parser extends PostCSS's core parser with fault-tolerant parsing capabilities. The architecture consists of:
Parser class, overriding key methods to handle malformed syntaxignoreErrors: true that continues parsing despite syntax errorscheckMissedSemicolon(), unclosedBracket(), and unexpectedClose() are overridden to handle errors silentlyThe safe parser creates the same PostCSS AST structure as the standard parser, ensuring full compatibility with all PostCSS plugins and tools.
Parses CSS with automatic error recovery and syntax fixing.
/**
* Parse CSS string with fault tolerance
* @param css - CSS string to parse (can contain syntax errors)
* @param opts - Options object passed to PostCSS Input constructor
* @param opts.from - Input file path for source map generation
* @param opts.to - Output file path for source map generation
* @param opts.origin - Custom origin function for Input
* @param opts.map - Source map options or boolean
* @returns PostCSS Root AST node with fault-tolerant parsing applied
*/
function safeParse(css, opts = {});Parameters:
css (string): CSS input to parse - can contain malformed syntax, unclosed blocks, missing semicolons, etc.opts (object, optional): Options object passed to PostCSS Input constructor
from (string): Input file path for source map generationorigin (function): Custom origin function for InputReturns:
PostCSS Root node containing the parsed CSS AST with all syntax errors automatically fixed.
Error Recovery Features:
The parser automatically handles and fixes:
a { becomes a {}/* comment becomes /* comment */a { color: red font-size: 12px } becomes a { color: red; font-size: 12px }content: "text becomes content: "text":not(input becomes :not(input)a { color; } preserves structure@ becomes valid at-rule nodea { prop:: value } becomes a { prop: : value }Usage Examples:
// Parse CSS with unclosed blocks
const root = safe('@media (screen) { a {\n');
console.log(root.toString()); // '@media (screen) { a {\n}}'
// Parse CSS with missing semicolons
const root = safe('a { color: red font-size: 12px }');
console.log(root.toString()); // 'a { color: red; font-size: 12px }'
// Parse CSS with unclosed comments
const root = safe('a { /* comment ');
console.log(root.toString()); // 'a { /* comment */}'
// Parse CSS with complex JSON-like custom properties
const root = safe(':root { --config: {"nested": {"key": "value"}}; }');
console.log(root.toString()); // Preserves the complex structureUse as a parser option in PostCSS processing pipelines.
// Standard PostCSS integration
postcss(plugins).process(css, { parser: safeParse })
// With async/await
const result = await postcss([autoprefixer]).process(badCss, {
parser: safe,
from: 'input.css'
});Common Integration Patterns:
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const safe = require('postcss-safe-parser');
// Live CSS editing tools - parse as user types
async function parseUserInput(userCss) {
try {
const result = await postcss([autoprefixer])
.process(userCss, { parser: safe });
return result.css;
} catch (error) {
// Safe parser won't throw, but plugins might
return userCss;
}
}
// Legacy CSS processing
async function processLegacyCSS(legacyCss) {
const result = await postcss([
// Your plugins here
]).process(legacyCss, {
parser: safe,
from: 'legacy.css'
});
return result;
}
// Browser hacks and malformed CSS
const browserHacksCSS = `
.selector {
property: value\\9; /* IE hack */
*property: value; /* IE6/7 hack */
_property: value /* IE6 hack
`;
const cleaned = safe(browserHacksCSS);
console.log(cleaned.toString()); // Properly closed and parsedDirect access to the SafeParser class for advanced usage scenarios.
const SafeParser = require('postcss-safe-parser/lib/safe-parser');
const { Input } = require('postcss');
/**
* SafeParser class extending PostCSS Parser
* Provides fault-tolerant CSS parsing with error recovery
*/
class SafeParser extends Parser {
constructor(input);
parse(): void;
createTokenizer(): void;
comment(token): void;
decl(tokens): void;
endFile(): void;
precheckMissedSemicolon(tokens): void;
unclosedBracket(): void;
unexpectedClose(): void;
unknownWord(tokens): void;
unnamedAtrule(node): void;
}Direct SafeParser Usage:
const { Input } = require('postcss');
const SafeParser = require('postcss-safe-parser/lib/safe-parser');
// Create parser instance directly
const input = new Input(cssString, { from: 'input.css' });
const parser = new SafeParser(input);
parser.parse();
// Access the parsed root
const root = parser.root;
console.log(root.toString());Key Method Behaviors:
comment(token): Handles unclosed comments by automatically closing themdecl(tokens): Validates declaration tokens before processingendFile(): Ensures proper file closure even with unclosed blocksunclosedBracket(): Silently handles unclosed brackets without throwingunexpectedClose(): Handles extra closing braces by adding them to raws.afterunknownWord(tokens): Treats unknown tokens as whitespace instead of throwing errorsAdvanced Usage Patterns:
// Custom input processing with source maps
const { Input } = require('postcss');
const SafeParser = require('postcss-safe-parser/lib/safe-parser');
function parseWithCustomInput(css, filename) {
const input = new Input(css, {
from: filename,
origin: (offset, file) => {
// Custom source mapping logic
const lines = css.substring(0, offset).split('\n');
return { line: lines.length, col: lines[lines.length - 1].length + 1 };
}
});
const parser = new SafeParser(input);
parser.parse();
return parser.root;
}
// Parser with custom error recovery tracking
class TrackedSafeParser extends SafeParser {
constructor(input) {
super(input);
this.recoveredErrors = [];
}
unexpectedClose() {
super.unexpectedClose();
this.recoveredErrors.push({
type: 'unexpected_close',
position: this.tokenizer.position()
});
}
unclosedBracket() {
super.unclosedBracket();
this.recoveredErrors.push({
type: 'unclosed_bracket',
position: this.tokenizer.position()
});
}
}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.
// Safe parser never throws
const root = safe('completely { malformed css {{{'); // Always succeeds
// But plugins might throw during processing
try {
const result = await postcss([somePlugin])
.process(malformedCss, { parser: safe });
} catch (pluginError) {
// Handle plugin errors, not parser errors
}For TypeScript projects, the parser follows PostCSS type definitions:
import { Root, Input, Parser, Node, Comment, Rule, AtRule, Declaration } from 'postcss';
/**
* Main safe parsing function
* @param css - CSS string to parse with fault tolerance
* @param opts - Options passed to PostCSS Input constructor
* @returns PostCSS Root AST node
*/
declare function safeParse(css: string, opts?: ProcessOptions): Root;
interface ProcessOptions {
from?: string;
to?: string;
origin?: (offset: number, file?: string) => { line: number; col: number };
map?: SourceMapOptions | boolean;
}
interface SourceMapOptions {
inline?: boolean;
prev?: string | object | boolean;
sourcesContent?: boolean;
annotation?: boolean | string;
from?: string;
to?: string;
}
/**
* SafeParser class extending PostCSS Parser
*/
declare class SafeParser extends Parser {
constructor(input: Input);
parse(): void;
createTokenizer(): void;
comment(token: [string, string, number, number]): void;
decl(tokens: Array<[string, string, number, number]>): void;
endFile(): void;
precheckMissedSemicolon(tokens: Array<[string, string, number, number]>): void;
unclosedBracket(): void;
unexpectedClose(): void;
unknownWord(tokens: Array<[string, string, number, number]>): void;
unnamedAtrule(node: AtRule): void;
// Inherited from Parser
root: Root;
input: Input;
current: Node;
spaces: string;
semicolon: boolean;
customProperty: boolean;
}
interface Root {
type: 'root';
nodes: Node[];
source?: {
input: Input;
start?: { line: number; column: number; offset: number };
end?: { line: number; column: number; offset: number };
};
raws: {
before?: string;
after?: string;
semicolon?: boolean;
};
// Methods
toString(stringifier?: any): string;
toResult(opts?: any): any;
append(...nodes: Node[]): Root;
prepend(...nodes: Node[]): Root;
insertAfter(exist: Node, add: Node): Root;
insertBefore(exist: Node, add: Node): Root;
removeAll(): Root;
removeChild(child: Node): Root;
each(callback: (node: Node, index: number) => void | false): void;
walk(callback: (node: Node, index: number) => void | false): void;
walkRules(callback: (rule: Rule, index: number) => void | false): void;
walkAtRules(callback: (atrule: AtRule, index: number) => void | false): void;
walkComments(callback: (comment: Comment, index: number) => void | false): void;
walkDecls(callback: (decl: Declaration, index: number) => void | false): void;
}