@projectwallace/css-parser 0.11.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/parse-anplusb.js +1 -1
- package/dist/parse-atrule-prelude.js +1 -1
- package/dist/parse-declaration.js +1 -1
- package/dist/parse-selector.js +24 -27
- package/dist/parse-value.js +1 -1
- package/dist/parse.d.ts +2 -1
- package/dist/parse.js +1 -2
- package/dist/tokenize.d.ts +9 -2
- package/dist/tokenize.js +27 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Built for speed and efficiency, this parser handles large CSS files with minimal memory overhead and blazing-fast parse times. Designed with a data-oriented architecture using a single contiguous memory arena for zero allocations during parsing.
|
|
9
9
|
|
|
10
|
-
This parser was heavily influenced by [CSSTree](https://github.com/csstree/csstree), one of the most robust CSS parsers available. Some of the parsing mechanics are taken from CSSTree, as well as some of the performance mechanics, but a lot of things are very different which is why this isn't a direct fork.
|
|
10
|
+
This parser was heavily influenced by [CSSTree](https://github.com/csstree/csstree), one of the most robust CSS parsers available. Some of the parsing mechanics are taken from CSSTree, as well as some of the performance mechanics, but a lot of things are very different which is why this isn't a direct fork and there is very little overlap in API's.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
@@ -15,6 +15,7 @@ This parser was heavily influenced by [CSSTree](https://github.com/csstree/csstr
|
|
|
15
15
|
- **Error recovery** - Continues parsing on malformed CSS
|
|
16
16
|
- **Location tracking** - Line, column, offset, and length for all nodes
|
|
17
17
|
- **Performance** - Low memory usage and excellent parsing speed
|
|
18
|
+
- **Small bundle size** - Fast download and installation in any environment
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { walk, traverse, SKIP, BREAK } from './walk';
|
|
|
9
9
|
export { is_custom, is_vendor_prefixed, str_equals, str_starts_with, str_index_of } from './string-utils';
|
|
10
10
|
export { type ParserOptions } from './parse';
|
|
11
11
|
export { CSSNode, type CSSNodeType, TYPE_NAMES, type CloneOptions, type PlainCSSNode } from './css-node';
|
|
12
|
-
export type { LexerPosition } from './tokenize';
|
|
12
|
+
export type { LexerPosition, CommentInfo } from './tokenize';
|
|
13
13
|
export { ATTR_OPERATOR_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_NONE, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, } from './arena';
|
|
14
14
|
export * from './constants';
|
|
15
15
|
export * from './token-types';
|
package/dist/parse-anplusb.js
CHANGED
|
@@ -13,7 +13,7 @@ class AtRulePreludeParser {
|
|
|
13
13
|
constructor(arena, source) {
|
|
14
14
|
this.arena = arena;
|
|
15
15
|
this.source = source;
|
|
16
|
-
this.lexer = new Lexer(source
|
|
16
|
+
this.lexer = new Lexer(source);
|
|
17
17
|
this.prelude_end = 0;
|
|
18
18
|
}
|
|
19
19
|
// Parse an at-rule prelude into nodes (standalone use)
|
|
@@ -17,7 +17,7 @@ class DeclarationParser {
|
|
|
17
17
|
}
|
|
18
18
|
// Parse a declaration range into a declaration node (standalone use)
|
|
19
19
|
parse_declaration(start, end, line = 1, column = 1) {
|
|
20
|
-
const lexer = new Lexer(this.source
|
|
20
|
+
const lexer = new Lexer(this.source);
|
|
21
21
|
lexer.pos = start;
|
|
22
22
|
lexer.line = line;
|
|
23
23
|
lexer.column = column;
|
package/dist/parse-selector.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Lexer } from './tokenize.js';
|
|
2
2
|
import { CSSDataArena, SELECTOR_LIST, SELECTOR, COMBINATOR, NESTING_SELECTOR, ID_SELECTOR, TYPE_SELECTOR, UNIVERSAL_SELECTOR, CLASS_SELECTOR, ATTRIBUTE_SELECTOR, ATTR_OPERATOR_NONE, ATTR_FLAG_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, PSEUDO_ELEMENT_SELECTOR, PSEUDO_CLASS_SELECTOR, FLAG_HAS_PARENS, LANG_SELECTOR, NTH_OF_SELECTOR } from './arena.js';
|
|
3
|
-
import {
|
|
3
|
+
import { TOKEN_COMMA, TOKEN_DELIM, TOKEN_EOF, TOKEN_WHITESPACE, TOKEN_FUNCTION, TOKEN_COLON, TOKEN_LEFT_BRACKET, TOKEN_HASH, TOKEN_IDENT, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_STRING } from './token-types.js';
|
|
4
4
|
import { skip_whitespace_and_comments_forward, skip_whitespace_and_comments_backward, skip_whitespace_forward } from './parse-utils.js';
|
|
5
|
-
import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_TAB, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, str_equals } from './string-utils.js';
|
|
5
|
+
import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_TAB, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, str_equals, CHAR_FORWARD_SLASH } from './string-utils.js';
|
|
6
6
|
import { ANplusBParser } from './parse-anplusb.js';
|
|
7
7
|
import { CSSNode } from './css-node.js';
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ class SelectorParser {
|
|
|
14
14
|
constructor(arena, source) {
|
|
15
15
|
this.arena = arena;
|
|
16
16
|
this.source = source;
|
|
17
|
-
this.lexer = new Lexer(source
|
|
17
|
+
this.lexer = new Lexer(source);
|
|
18
18
|
this.selector_end = 0;
|
|
19
19
|
}
|
|
20
20
|
// Parse a selector range into selector nodes (standalone use)
|
|
@@ -52,29 +52,10 @@ class SelectorParser {
|
|
|
52
52
|
}
|
|
53
53
|
this.skip_whitespace();
|
|
54
54
|
if (this.lexer.pos >= this.selector_end) break;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.lexer.next_token_fast(false);
|
|
58
|
-
token_type = this.lexer.token_type;
|
|
59
|
-
if (token_type === TOKEN_COMMENT) {
|
|
60
|
-
this.skip_whitespace();
|
|
61
|
-
} else {
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
55
|
+
this.lexer.next_token_fast(false);
|
|
56
|
+
let token_type = this.lexer.token_type;
|
|
65
57
|
if (token_type === TOKEN_COMMA) {
|
|
66
58
|
this.skip_whitespace();
|
|
67
|
-
while (this.lexer.pos < this.selector_end) {
|
|
68
|
-
const saved = this.lexer.save_position();
|
|
69
|
-
this.lexer.next_token_fast(false);
|
|
70
|
-
token_type = this.lexer.token_type;
|
|
71
|
-
if (token_type === TOKEN_COMMENT) {
|
|
72
|
-
this.skip_whitespace();
|
|
73
|
-
} else {
|
|
74
|
-
this.lexer.restore_position(saved);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
59
|
continue;
|
|
79
60
|
} else {
|
|
80
61
|
break;
|
|
@@ -610,10 +591,26 @@ class SelectorParser {
|
|
|
610
591
|
this.arena.set_content_length(node, end - start);
|
|
611
592
|
return node;
|
|
612
593
|
}
|
|
613
|
-
// Helper to skip whitespace and
|
|
594
|
+
// Helper to skip whitespace and comments, updating line/column
|
|
614
595
|
skip_whitespace() {
|
|
615
|
-
while (this.lexer.pos < this.selector_end
|
|
616
|
-
this.lexer.
|
|
596
|
+
while (this.lexer.pos < this.selector_end) {
|
|
597
|
+
let ch = this.source.charCodeAt(this.lexer.pos);
|
|
598
|
+
if (is_whitespace(ch)) {
|
|
599
|
+
this.lexer.advance();
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (ch === CHAR_FORWARD_SLASH && this.lexer.pos + 1 < this.selector_end && this.source.charCodeAt(this.lexer.pos + 1) === CHAR_ASTERISK) {
|
|
603
|
+
this.lexer.advance(2);
|
|
604
|
+
while (this.lexer.pos < this.selector_end) {
|
|
605
|
+
if (this.source.charCodeAt(this.lexer.pos) === CHAR_ASTERISK && this.lexer.pos + 1 < this.selector_end && this.source.charCodeAt(this.lexer.pos + 1) === CHAR_FORWARD_SLASH) {
|
|
606
|
+
this.lexer.advance(2);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
this.lexer.advance();
|
|
610
|
+
}
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
617
614
|
}
|
|
618
615
|
}
|
|
619
616
|
}
|
package/dist/parse-value.js
CHANGED
|
@@ -12,7 +12,7 @@ class ValueParser {
|
|
|
12
12
|
constructor(arena, source) {
|
|
13
13
|
this.arena = arena;
|
|
14
14
|
this.source = source;
|
|
15
|
-
this.lexer = new Lexer(source
|
|
15
|
+
this.lexer = new Lexer(source);
|
|
16
16
|
this.value_end = 0;
|
|
17
17
|
}
|
|
18
18
|
// Parse a declaration value range into a VALUE wrapper node
|
package/dist/parse.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type CommentInfo } from './tokenize';
|
|
1
2
|
import { CSSNode } from './css-node';
|
|
2
3
|
export interface ParserOptions {
|
|
3
|
-
skip_comments?: boolean;
|
|
4
4
|
parse_values?: boolean;
|
|
5
5
|
parse_selectors?: boolean;
|
|
6
6
|
parse_atrule_preludes?: boolean;
|
|
7
|
+
on_comment?: (info: CommentInfo) => void;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Parse CSS and return an AST
|
package/dist/parse.js
CHANGED
|
@@ -23,11 +23,10 @@ class Parser {
|
|
|
23
23
|
constructor(source, options) {
|
|
24
24
|
this.source = source;
|
|
25
25
|
let opts = options || {};
|
|
26
|
-
let skip_comments = opts.skip_comments ?? true;
|
|
27
26
|
this.parse_values_enabled = opts.parse_values ?? true;
|
|
28
27
|
this.parse_selectors_enabled = opts.parse_selectors ?? true;
|
|
29
28
|
this.parse_atrule_preludes_enabled = opts.parse_atrule_preludes ?? true;
|
|
30
|
-
this.lexer = new Lexer(source,
|
|
29
|
+
this.lexer = new Lexer(source, opts.on_comment);
|
|
31
30
|
let capacity = CSSDataArena.capacity_for_source(source.length);
|
|
32
31
|
this.arena = new CSSDataArena(capacity);
|
|
33
32
|
this.selector_parser = this.parse_selectors_enabled ? new SelectorParser(this.arena, source) : null;
|
package/dist/tokenize.d.ts
CHANGED
|
@@ -9,10 +9,17 @@ export interface LexerPosition {
|
|
|
9
9
|
token_line: number;
|
|
10
10
|
token_column: number;
|
|
11
11
|
}
|
|
12
|
+
export interface CommentInfo {
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
length: number;
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
}
|
|
12
19
|
/**
|
|
13
20
|
* Tokenize CSS source code
|
|
14
21
|
* @param source - The CSS source code to tokenize
|
|
15
|
-
* @param
|
|
22
|
+
* @param on_comment - Optional callback for comment tokens
|
|
16
23
|
* @yields CSS tokens
|
|
17
24
|
*/
|
|
18
|
-
export declare function tokenize(source: string,
|
|
25
|
+
export declare function tokenize(source: string, on_comment?: (info: CommentInfo) => void): Generator<Token, void, undefined>;
|
package/dist/tokenize.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { char_types, CHAR_WHITESPACE, CHAR_NEWLINE, CHAR_DIGIT, is_ident_start, is_newline, is_hex_digit, is_whitespace, is_ident_char } from './char-types.js';
|
|
2
|
-
import { TOKEN_EOF, TOKEN_RIGHT_PAREN, TOKEN_LEFT_PAREN, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_BRACKET, TOKEN_COMMA, TOKEN_SEMICOLON, TOKEN_COLON, TOKEN_RIGHT_BRACE, TOKEN_LEFT_BRACE, TOKEN_CDO, TOKEN_CDC, TOKEN_DELIM, TOKEN_WHITESPACE,
|
|
2
|
+
import { TOKEN_EOF, TOKEN_RIGHT_PAREN, TOKEN_LEFT_PAREN, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_BRACKET, TOKEN_COMMA, TOKEN_SEMICOLON, TOKEN_COLON, TOKEN_RIGHT_BRACE, TOKEN_LEFT_BRACE, TOKEN_CDO, TOKEN_CDC, TOKEN_DELIM, TOKEN_WHITESPACE, TOKEN_STRING, TOKEN_BAD_STRING, TOKEN_PERCENTAGE, TOKEN_DIMENSION, TOKEN_NUMBER, TOKEN_FUNCTION, TOKEN_IDENT, TOKEN_AT_KEYWORD, TOKEN_HASH } from './token-types.js';
|
|
3
3
|
|
|
4
4
|
const CHAR_LEFT_BRACE = 123;
|
|
5
5
|
const CHAR_RIGHT_BRACE = 125;
|
|
@@ -33,19 +33,19 @@ class Lexer {
|
|
|
33
33
|
pos;
|
|
34
34
|
line;
|
|
35
35
|
column;
|
|
36
|
-
|
|
36
|
+
on_comment;
|
|
37
37
|
// Current token properties (avoiding object allocation)
|
|
38
38
|
token_type;
|
|
39
39
|
token_start;
|
|
40
40
|
token_end;
|
|
41
41
|
token_line;
|
|
42
42
|
token_column;
|
|
43
|
-
constructor(source,
|
|
43
|
+
constructor(source, on_comment) {
|
|
44
44
|
this.source = source;
|
|
45
45
|
this.pos = 0;
|
|
46
46
|
this.line = 1;
|
|
47
47
|
this.column = 1;
|
|
48
|
-
this.
|
|
48
|
+
this.on_comment = on_comment;
|
|
49
49
|
this.token_type = TOKEN_EOF;
|
|
50
50
|
this.token_start = 0;
|
|
51
51
|
this.token_end = 0;
|
|
@@ -101,19 +101,29 @@ class Lexer {
|
|
|
101
101
|
return this.consume_whitespace(start_line, start_column);
|
|
102
102
|
}
|
|
103
103
|
if (ch === CHAR_FORWARD_SLASH && this.peek() === CHAR_ASTERISK) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
let comment_start = start;
|
|
105
|
+
let comment_line = start_line;
|
|
106
|
+
let comment_column = start_column;
|
|
107
|
+
this.advance(2);
|
|
108
|
+
while (this.pos < this.source.length - 1) {
|
|
109
|
+
let ch2 = this.source.charCodeAt(this.pos);
|
|
110
|
+
if (ch2 === CHAR_ASTERISK && this.peek() === CHAR_FORWARD_SLASH) {
|
|
111
|
+
this.advance(2);
|
|
112
|
+
break;
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
this.advance();
|
|
115
|
+
}
|
|
116
|
+
let comment_end = this.pos;
|
|
117
|
+
if (this.on_comment) {
|
|
118
|
+
this.on_comment({
|
|
119
|
+
start: comment_start,
|
|
120
|
+
end: comment_end,
|
|
121
|
+
length: comment_end - comment_start,
|
|
122
|
+
line: comment_line,
|
|
123
|
+
column: comment_column
|
|
124
|
+
});
|
|
115
125
|
}
|
|
116
|
-
return this.
|
|
126
|
+
return this.next_token_fast(skip_whitespace);
|
|
117
127
|
}
|
|
118
128
|
if (ch === CHAR_DOUBLE_QUOTE || ch === CHAR_SINGLE_QUOTE) {
|
|
119
129
|
return this.consume_string(ch, start_line, start_column);
|
|
@@ -185,19 +195,6 @@ class Lexer {
|
|
|
185
195
|
}
|
|
186
196
|
return this.make_token(TOKEN_WHITESPACE, start, this.pos, start_line, start_column);
|
|
187
197
|
}
|
|
188
|
-
consume_comment(start_line, start_column) {
|
|
189
|
-
let start = this.pos;
|
|
190
|
-
this.advance(2);
|
|
191
|
-
while (this.pos < this.source.length - 1) {
|
|
192
|
-
let ch = this.source.charCodeAt(this.pos);
|
|
193
|
-
if (ch === CHAR_ASTERISK && this.peek() === CHAR_FORWARD_SLASH) {
|
|
194
|
-
this.advance(2);
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
this.advance();
|
|
198
|
-
}
|
|
199
|
-
return this.make_token(TOKEN_COMMENT, start, this.pos, start_line, start_column);
|
|
200
|
-
}
|
|
201
198
|
consume_string(quote, start_line, start_column) {
|
|
202
199
|
let start = this.pos;
|
|
203
200
|
this.advance();
|
|
@@ -442,8 +439,8 @@ class Lexer {
|
|
|
442
439
|
this.token_column = saved.token_column;
|
|
443
440
|
}
|
|
444
441
|
}
|
|
445
|
-
function* tokenize(source,
|
|
446
|
-
const lexer = new Lexer(source,
|
|
442
|
+
function* tokenize(source, on_comment) {
|
|
443
|
+
const lexer = new Lexer(source, on_comment);
|
|
447
444
|
while (true) {
|
|
448
445
|
const token = lexer.next_token();
|
|
449
446
|
if (!token || token.type === TOKEN_EOF) {
|
package/package.json
CHANGED