Skip to content

Commit 7b6cc76

Browse files
committed
refactor(parse): Use switch for parsing
And make the loop terminate as we reach the end BREAKING: An empty selector will now return an empty array, instead of an array containing only an empty array.
1 parent 552d8d2 commit 7b6cc76

File tree

2 files changed

+138
-90
lines changed

2 files changed

+138
-90
lines changed

src/__fixtures__/out.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"": [[]],
3-
"\t": [[]],
2+
"": [],
3+
"\t": [],
44
"\t#qunit-fixture p": [
55
[
66
{
@@ -81,7 +81,7 @@
8181
}
8282
]
8383
],
84-
" ": [[]],
84+
" ": [],
8585
" #qunit-fixture p": [
8686
[
8787
{

src/parse.ts

+135-87
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const enum CharCode {
1818
LeftSquareBracket = 91,
1919
RightSquareBracket = 93,
2020
Comma = 44,
21-
Dot = 46,
21+
Period = 46,
2222
Colon = 58,
2323
SingleQuote = 39,
2424
DoubleQuote = 34,
@@ -57,18 +57,6 @@ const actionTypes = new Map<number, AttributeAction>([
5757
[CharCode.Pipe, AttributeAction.Hyphen],
5858
]);
5959

60-
const Traversals: Map<number, TraversalType> = new Map([
61-
[CharCode.GreaterThan, SelectorType.Child],
62-
[CharCode.LessThan, SelectorType.Parent],
63-
[CharCode.Tilde, SelectorType.Sibling],
64-
[CharCode.Plus, SelectorType.Adjacent],
65-
]);
66-
67-
const attribSelectors: Map<number, [string, AttributeAction]> = new Map([
68-
[CharCode.Hash, ["id", AttributeAction.Equals]],
69-
[CharCode.Dot, ["class", AttributeAction.Element]],
70-
]);
71-
7260
// Pseudos, whose data property is parsed as well.
7361
const unpackPseudos = new Set([
7462
"has",
@@ -217,7 +205,6 @@ function parseSelector(
217205
selectorIndex: number
218206
): number {
219207
let tokens: Selector[] = [];
220-
let sawWS = false;
221208

222209
function getName(offset: number): string {
223210
const match = selector.slice(selectorIndex + offset).match(reName);
@@ -234,9 +221,14 @@ function parseSelector(
234221
}
235222

236223
function stripWhitespace(offset: number) {
237-
while (isWhitespace(selector.charCodeAt(selectorIndex + offset)))
238-
offset++;
239224
selectorIndex += offset;
225+
226+
while (
227+
selectorIndex < selector.length &&
228+
isWhitespace(selector.charCodeAt(selectorIndex))
229+
) {
230+
selectorIndex++;
231+
}
240232
}
241233

242234
function isEscaped(pos: number): boolean {
@@ -252,56 +244,112 @@ function parseSelector(
252244
}
253245
}
254246

247+
function addTraversal(type: TraversalType) {
248+
if (
249+
tokens.length > 0 &&
250+
tokens[tokens.length - 1].type === SelectorType.Descendant
251+
) {
252+
tokens[tokens.length - 1].type = type;
253+
return;
254+
}
255+
256+
ensureNotTraversal();
257+
258+
tokens.push({ type });
259+
}
260+
261+
function addSpecialAttribute(name: string, action: AttributeAction) {
262+
tokens.push({
263+
type: SelectorType.Attribute,
264+
name,
265+
action,
266+
value: getName(1),
267+
namespace: null,
268+
// TODO: Add quirksMode option, which makes `ignoreCase` `true` for HTML.
269+
ignoreCase: options.xmlMode ? null : false,
270+
});
271+
}
272+
273+
/**
274+
* We have finished parsing the current part of the selector.
275+
*
276+
* Remove descendant tokens at the end if they exist,
277+
* and return the last index, so that parsing can be
278+
* picked up from here.
279+
*/
280+
function finalizeSubselector() {
281+
if (
282+
tokens.length &&
283+
tokens[tokens.length - 1].type === SelectorType.Descendant
284+
) {
285+
tokens.pop();
286+
}
287+
288+
if (tokens.length === 0) {
289+
throw new Error("Empty sub-selector");
290+
}
291+
292+
subselects.push(tokens);
293+
}
294+
255295
stripWhitespace(0);
256296

257-
for (;;) {
297+
if (selector.length === selectorIndex) {
298+
return selectorIndex;
299+
}
300+
301+
loop: while (selectorIndex < selector.length) {
258302
const firstChar = selector.charCodeAt(selectorIndex);
259303

260-
if (isWhitespace(firstChar)) {
261-
sawWS = true;
262-
stripWhitespace(1);
263-
} else if (Traversals.has(firstChar)) {
264-
ensureNotTraversal();
265-
tokens.push({ type: Traversals.get(firstChar)! });
266-
sawWS = false;
267-
268-
stripWhitespace(1);
269-
} else if (firstChar === CharCode.Comma) {
270-
if (tokens.length === 0) {
271-
throw new Error("Empty sub-selector");
304+
switch (firstChar) {
305+
// Whitespace
306+
case CharCode.Space:
307+
case CharCode.Tab:
308+
case CharCode.NewLine:
309+
case CharCode.FormFeed:
310+
case CharCode.CarriageReturn: {
311+
if (
312+
tokens.length === 0 ||
313+
tokens[0].type !== SelectorType.Descendant
314+
) {
315+
ensureNotTraversal();
316+
tokens.push({ type: SelectorType.Descendant });
317+
}
318+
319+
stripWhitespace(1);
320+
break;
272321
}
273-
subselects.push(tokens);
274-
tokens = [];
275-
sawWS = false;
276-
stripWhitespace(1);
277-
} else if (selector.startsWith("/*", selectorIndex)) {
278-
const endIndex = selector.indexOf("*/", selectorIndex + 2);
279-
280-
if (endIndex < 0) {
281-
throw new Error("Comment was not terminated");
322+
// Traversals
323+
case CharCode.GreaterThan: {
324+
addTraversal(SelectorType.Child);
325+
stripWhitespace(1);
326+
break;
282327
}
283-
284-
selectorIndex = endIndex + 2;
285-
} else {
286-
if (sawWS) {
287-
ensureNotTraversal();
288-
tokens.push({ type: SelectorType.Descendant });
289-
sawWS = false;
328+
case CharCode.LessThan: {
329+
addTraversal(SelectorType.Parent);
330+
stripWhitespace(1);
331+
break;
290332
}
291-
292-
const attribSelector = attribSelectors.get(firstChar);
293-
if (attribSelector) {
294-
const [name, action] = attribSelector;
295-
tokens.push({
296-
type: SelectorType.Attribute,
297-
name,
298-
action,
299-
value: getName(1),
300-
namespace: null,
301-
// TODO: Add quirksMode option, which makes `ignoreCase` `true` for HTML.
302-
ignoreCase: options.xmlMode ? null : false,
303-
});
304-
} else if (firstChar === CharCode.LeftSquareBracket) {
333+
case CharCode.Tilde: {
334+
addTraversal(SelectorType.Sibling);
335+
stripWhitespace(1);
336+
break;
337+
}
338+
case CharCode.Plus: {
339+
addTraversal(SelectorType.Adjacent);
340+
stripWhitespace(1);
341+
break;
342+
}
343+
// Special attribute selectors: .class, #id
344+
case CharCode.Period: {
345+
addSpecialAttribute("class", AttributeAction.Element);
346+
break;
347+
}
348+
case CharCode.Hash: {
349+
addSpecialAttribute("id", AttributeAction.Equals);
350+
break;
351+
}
352+
case CharCode.LeftSquareBracket: {
305353
stripWhitespace(1);
306354

307355
// Determine attribute name and namespace
@@ -445,7 +493,9 @@ function parseSelector(
445493
};
446494

447495
tokens.push(attributeSelector);
448-
} else if (firstChar === CharCode.Colon) {
496+
break;
497+
}
498+
case CharCode.Colon: {
449499
if (selector.charCodeAt(selectorIndex + 1) === CharCode.Colon) {
450500
tokens.push({
451501
type: SelectorType.PseudoElement,
@@ -533,35 +583,38 @@ function parseSelector(
533583
}
534584

535585
tokens.push({ type: SelectorType.Pseudo, name, data });
536-
} else {
586+
break;
587+
}
588+
case CharCode.Comma: {
589+
finalizeSubselector();
590+
tokens = [];
591+
stripWhitespace(1);
592+
break;
593+
}
594+
default: {
595+
if (selector.startsWith("/*", selectorIndex)) {
596+
const endIndex = selector.indexOf("*/", selectorIndex + 2);
597+
598+
if (endIndex < 0) {
599+
throw new Error("Comment was not terminated");
600+
}
601+
602+
selectorIndex = endIndex + 2;
603+
break;
604+
}
605+
537606
let namespace = null;
538607
let name: string;
539608

540609
if (firstChar === CharCode.Asterisk) {
541610
selectorIndex += 1;
542611
name = "*";
612+
} else if (firstChar === CharCode.Pipe) {
613+
name = "";
543614
} else if (reName.test(selector.slice(selectorIndex))) {
544-
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe) {
545-
namespace = "";
546-
selectorIndex += 1;
547-
}
548615
name = getName(0);
549616
} else {
550-
/*
551-
* We have finished parsing the selector.
552-
* Remove descendant tokens at the end if they exist,
553-
* and return the last index, so that parsing can be
554-
* picked up from here.
555-
*/
556-
if (
557-
tokens.length &&
558-
tokens[tokens.length - 1].type ===
559-
SelectorType.Descendant
560-
) {
561-
tokens.pop();
562-
}
563-
addToken(subselects, tokens);
564-
return selectorIndex;
617+
break loop;
565618
}
566619

567620
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe) {
@@ -589,12 +642,7 @@ function parseSelector(
589642
}
590643
}
591644
}
592-
}
593-
594-
function addToken(subselects: Selector[][], tokens: Selector[]) {
595-
if (subselects.length > 0 && tokens.length === 0) {
596-
throw new Error("Empty sub-selector");
597-
}
598645

599-
subselects.push(tokens);
646+
finalizeSubselector();
647+
return selectorIndex;
600648
}

0 commit comments

Comments
 (0)