Skip to content

Commit 4316513

Browse files
committed
feat: port csstree parser
1 parent eaa1780 commit 4316513

File tree

3 files changed

+379
-0
lines changed

3 files changed

+379
-0
lines changed

parser/create.js

+332
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
import OffsetToLocation from '../common/OffsetToLocation.js';
2+
import SyntaxError from '../common/SyntaxError.js';
3+
import TokenStream from '../common/TokenStream.js';
4+
import List from '../common/List.js';
5+
import tokenize from '../tokenizer.js';
6+
import { consumeNumber, findWhiteSpaceStart, cmpChar, cmpStr } from '../tokenizer/utils.js';
7+
import NAME from '../tokenizer/names.js';
8+
import {
9+
WhiteSpace,
10+
Comment,
11+
Ident,
12+
Function: FunctionToken,
13+
Url,
14+
Hash,
15+
Percentage,
16+
Number: NumberToken
17+
} from '../tokenizer/types.js';
18+
import sequence = from './sequence.js';
19+
const noop = () => {};
20+
21+
const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!)
22+
const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#)
23+
const SEMICOLON = 0x003B; // U+003B SEMICOLON (;)
24+
const LEFTCURLYBRACKET = 0x007B; // U+007B LEFT CURLY BRACKET ({)
25+
const NULL = 0;
26+
27+
function createParseContext(name) {
28+
return function() {
29+
return this[name]();
30+
};
31+
}
32+
33+
function fetchParseValues(dict) {
34+
const result = Object.create(null);
35+
36+
for (const name in dict) {
37+
const item = dict[name];
38+
39+
if (item.parse) {
40+
result[name] = item.parse;
41+
}
42+
}
43+
44+
return result;
45+
}
46+
47+
function processConfig(config) {
48+
const parseConfig = {
49+
context: Object.create(null),
50+
scope: Object.assign(Object.create(null), config.scope),
51+
atrule: fetchParseValues(config.atrule),
52+
pseudo: fetchParseValues(config.pseudo),
53+
node: fetchParseValues(config.node)
54+
};
55+
56+
for (const name in config.parseContext) {
57+
switch (typeof config.parseContext[name]) {
58+
case 'function':
59+
parseConfig.context[name] = config.parseContext[name];
60+
break;
61+
62+
case 'string':
63+
parseConfig.context[name] = createParseContext(config.parseContext[name]);
64+
break;
65+
}
66+
}
67+
68+
return {
69+
config: parseConfig,
70+
...parseConfig,
71+
...parseConfig.node
72+
};
73+
}
74+
75+
export default function createParser(config) {
76+
let source = '';
77+
let filename = '<unknown>';
78+
let needPositions = false;
79+
let onParseError = noop;
80+
let onParseErrorThrow = false;
81+
82+
const locationMap = new OffsetToLocation();
83+
const parser = Object.assign(new TokenStream(), processConfig(config || {}), {
84+
parseAtrulePrelude: true,
85+
parseRulePrelude: true,
86+
parseValue: true,
87+
parseCustomProperty: false,
88+
89+
readSequence: sequence,
90+
91+
consumeUntilBalanceEnd: () => 0,
92+
consumeUntilLeftCurlyBracket(code) {
93+
return code === LEFTCURLYBRACKET ? 1 : 0;
94+
},
95+
consumeUntilLeftCurlyBracketOrSemicolon(code) {
96+
return code === LEFTCURLYBRACKET || code === SEMICOLON ? 1 : 0;
97+
},
98+
consumeUntilExclamationMarkOrSemicolon(code) {
99+
return code === EXCLAMATIONMARK || code === SEMICOLON ? 1 : 0;
100+
},
101+
consumeUntilSemicolonIncluded(code) {
102+
return code === SEMICOLON ? 2 : 0;
103+
},
104+
105+
createList() {
106+
return new List();
107+
},
108+
createSingleNodeList(node) {
109+
return new List().appendData(node);
110+
},
111+
getFirstListNode(list) {
112+
return list && list.first;
113+
},
114+
getLastListNode(list) {
115+
return list && list.last;
116+
},
117+
118+
parseWithFallback(consumer, fallback) {
119+
const startToken = this.tokenIndex;
120+
121+
try {
122+
return consumer.call(this);
123+
} catch (e) {
124+
if (onParseErrorThrow) {
125+
throw e;
126+
}
127+
128+
const fallbackNode = fallback.call(this, startToken);
129+
130+
onParseErrorThrow = true;
131+
onParseError(e, fallbackNode);
132+
onParseErrorThrow = false;
133+
134+
return fallbackNode;
135+
}
136+
},
137+
138+
lookupNonWSType(offset) {
139+
let type;
140+
141+
do {
142+
type = this.lookupType(offset++);
143+
if (type !== WhiteSpace) {
144+
return type;
145+
}
146+
} while (type !== NULL);
147+
148+
return NULL;
149+
},
150+
151+
eat(tokenType) {
152+
if (this.tokenType !== tokenType) {
153+
let offset = this.tokenStart;
154+
let message = NAME[tokenType] + ' is expected';
155+
156+
// tweak message and offset
157+
switch (tokenType) {
158+
case Ident:
159+
// when identifier is expected but there is a function or url
160+
if (this.tokenType === FunctionToken || this.tokenType === Url) {
161+
offset = this.tokenEnd - 1;
162+
message = 'Identifier is expected but function found';
163+
} else {
164+
message = 'Identifier is expected';
165+
}
166+
break;
167+
168+
case Hash:
169+
if (this.isDelim(NUMBERSIGN)) {
170+
this.next();
171+
offset++;
172+
message = 'Name is expected';
173+
}
174+
break;
175+
176+
case Percentage:
177+
if (this.tokenType === NumberToken) {
178+
offset = this.tokenEnd;
179+
message = 'Percent sign is expected';
180+
}
181+
break;
182+
}
183+
184+
this.error(message, offset);
185+
}
186+
187+
this.next();
188+
},
189+
eatIdent(name) {
190+
if (this.tokenType !== Ident || this.lookupValue(0, name) === false) {
191+
this.error(`Identifier "${name}" is expected`);
192+
}
193+
this.next();
194+
},
195+
eatDelim(code) {
196+
if (!this.isDelim(code)) {
197+
this.error(`Delim "${String.fromCharCode(code)}" is expected`);
198+
}
199+
this.next();
200+
},
201+
202+
charCodeAt(offset) {
203+
return offset >= 0 && offset < source.length ? source.charCodeAt(offset) : 0;
204+
},
205+
cmpChar(offset, charCode) {
206+
return cmpChar(source, offset, charCode);
207+
},
208+
cmpStr(offsetStart, offsetEnd, str) {
209+
return cmpStr(source, offsetStart, offsetEnd, str);
210+
},
211+
substring(offsetStart, offsetEnd) {
212+
return source.substring(offsetStart, offsetEnd);
213+
},
214+
215+
consume(tokenType) {
216+
const start = this.tokenStart;
217+
218+
this.eat(tokenType);
219+
220+
return this.substrToCursor(start);
221+
},
222+
consumeFunctionName() {
223+
const name = source.substring(this.tokenStart, this.tokenEnd - 1);
224+
225+
this.eat(FunctionToken);
226+
227+
return name;
228+
},
229+
consumeNumber(type) {
230+
const number = source.substring(this.tokenStart, consumeNumber(source, this.tokenStart));
231+
232+
this.eat(type);
233+
234+
return number;
235+
},
236+
237+
getLocation(start, end) {
238+
if (needPositions) {
239+
return locationMap.getLocationRange(
240+
start,
241+
end,
242+
filename
243+
);
244+
}
245+
246+
return null;
247+
},
248+
getLocationFromList(list) {
249+
if (needPositions) {
250+
const head = this.getFirstListNode(list);
251+
const tail = this.getLastListNode(list);
252+
return locationMap.getLocationRange(
253+
head !== null ? head.loc.start.offset - locationMap.startOffset : this.tokenStart,
254+
tail !== null ? tail.loc.end.offset - locationMap.startOffset : this.tokenStart,
255+
filename
256+
);
257+
}
258+
259+
return null;
260+
},
261+
262+
error(message, offset) {
263+
const location = typeof offset !== 'undefined' && offset < source.length
264+
? locationMap.getLocation(offset)
265+
: this.eof
266+
? locationMap.getLocation(findWhiteSpaceStart(source, source.length - 1))
267+
: locationMap.getLocation(this.tokenStart);
268+
269+
throw new SyntaxError(
270+
message || 'Unexpected input',
271+
source,
272+
location.offset,
273+
location.line,
274+
location.column
275+
);
276+
}
277+
});
278+
279+
const parse = function(source_, options) {
280+
source = source_;
281+
options = options || {};
282+
283+
parser.setSource(source, tokenize);
284+
locationMap.setSource(
285+
source,
286+
options.offset,
287+
options.line,
288+
options.column
289+
);
290+
291+
filename = options.filename || '<unknown>';
292+
needPositions = Boolean(options.positions);
293+
onParseError = typeof options.onParseError === 'function' ? options.onParseError : noop;
294+
onParseErrorThrow = false;
295+
296+
parser.parseAtrulePrelude = 'parseAtrulePrelude' in options ? Boolean(options.parseAtrulePrelude) : true;
297+
parser.parseRulePrelude = 'parseRulePrelude' in options ? Boolean(options.parseRulePrelude) : true;
298+
parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true;
299+
parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false;
300+
301+
const { context = 'default', onComment } = options;
302+
303+
if (context in parser.context === false) {
304+
throw new Error('Unknown context `' + context + '`');
305+
}
306+
307+
if (typeof onComment === 'function') {
308+
parser.forEachToken((type, start, end) => {
309+
if (type === Comment) {
310+
const loc = parser.getLocation(start, end);
311+
const value = cmpStr(source, end - 2, end, '*/')
312+
? source.slice(start + 2, end - 2)
313+
: source.slice(start + 2, end);
314+
315+
onComment(value, loc);
316+
}
317+
});
318+
}
319+
320+
const ast = parser.context[context].call(parser, options);
321+
322+
if (!parser.eof) {
323+
parser.error();
324+
}
325+
326+
return ast;
327+
};
328+
329+
return Object.assign(parse, {
330+
config: parser.config
331+
});
332+
};

parser/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import createParse from './create.js';
2+
import config from '../syntax/config/parser.js';
3+
4+
export default createParse(rconfig);

parser/sequence.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { WhiteSpace, Comment } from '../tokenizer/types.js';
2+
3+
export default function readSequence(recognizer) {
4+
const children = this.createList();
5+
let space = false;
6+
const context = {
7+
recognizer
8+
};
9+
10+
while (!this.eof) {
11+
switch (this.tokenType) {
12+
case Comment:
13+
this.next();
14+
continue;
15+
16+
case WhiteSpace:
17+
space = true;
18+
this.next();
19+
continue;
20+
}
21+
22+
let child = recognizer.getNode.call(this, context);
23+
24+
if (child === undefined) {
25+
break;
26+
}
27+
28+
if (space) {
29+
if (recognizer.onWhiteSpace) {
30+
recognizer.onWhiteSpace.call(this, child, children, context);
31+
}
32+
space = false;
33+
}
34+
35+
children.push(child);
36+
}
37+
38+
if (space && recognizer.onWhiteSpace) {
39+
recognizer.onWhiteSpace.call(this, null, children, context);
40+
}
41+
42+
return children;
43+
};

0 commit comments

Comments
 (0)