Skip to content

Commit f255d9f

Browse files
committed
Rewrite parser with explicit state transitions
1 parent 9928e78 commit f255d9f

File tree

1 file changed

+101
-48
lines changed

1 file changed

+101
-48
lines changed

src/util/toPath.js

Lines changed: 101 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,68 +15,121 @@
1515
export function toPath(path) {
1616
if (Array.isArray(path)) return path
1717

18-
/** @type {'start' | 'in_property' | 'in_brackets' | 'end'} */
19-
let state = 'start'
18+
// The general outline of a path string is as follows:
19+
// property.another[some.other.property.with.dots][0][0].and.so.on
20+
// Properties may be separated by dots, optionally enclosed in brackets, can access array elements, etc...
21+
22+
// Tokens:
23+
// [ = lb
24+
// ] = rb
25+
// . = dot if not in brackets
26+
// . = ident if in brackets
27+
// everything else = ident
28+
29+
// Valid transitions:
30+
// start -> dot | lb | ident | end
31+
// dot -> ident
32+
// lb -> rb | ident
33+
// rb -> dot | lb
34+
// ident -> dot | rb | ident | end
35+
// ident -> lb if not in brackets
36+
37+
// Additional rules:
38+
// lb :: set in brackets = true
39+
// rb :: set in brackets = false
2040

2141
let parts = []
42+
let inBrackets = false
2243
let partStart = 0
2344
let partEnd = 0
2445

25-
for (let i = 0, len = path.length; i < len; i++) {
26-
let c = path[i]
27-
let previousState = state
28-
29-
if (c === '[') {
30-
if (previousState === 'in_brackets') {
31-
throw new Error(`Invalid path: ${path}\n` + `${' '.repeat(14 + i)}^`)
32-
}
46+
for (let i = 0, len = path.length; i <= len; i++) {
47+
let prev = path[i - 1]
48+
let curr = path[i]
3349

34-
// Append the current part to the parts if non-empty (would be in the case of concesutive brackets e.g. a[b][c])
35-
partEnd = i
50+
let error = () => {
51+
throw new Error(`Invalid path: ${path}\n` + `${' '.repeat(14 + i)}^`)
52+
}
53+
let startNew = () => (partStart = partEnd = i)
54+
let advance = () => (partEnd = i)
55+
let setInBrackets = (v) => (inBrackets = v)
56+
let capture = () => parts.push(path.slice(partStart, partEnd))
3657

37-
if (partStart !== partEnd) {
38-
parts.push(path.slice(partStart, partEnd))
58+
if (prev === undefined) {
59+
if (curr === '.') {
60+
// start -> dot
61+
capture()
62+
} else if (curr === '[') {
63+
// start -> lb
64+
startNew()
65+
setInBrackets(true)
66+
} else if (curr === ']') {
67+
// start -> rb
68+
return error()
69+
} else {
70+
// start -> ident | end
3971
}
40-
41-
state = 'in_brackets'
42-
partStart = i + 1
43-
} else if (c === ']') {
44-
if (previousState !== 'in_brackets') {
45-
throw new Error(`Invalid path: ${path}\n` + `${' '.repeat(14 + i)}^`)
72+
} else if (prev === '.' && !inBrackets) {
73+
if (curr === '[' || curr === ']' || curr === '.' || curr === undefined) {
74+
// dot -> lb | rb | dot | end
75+
return error()
76+
} else {
77+
// dot -> ident
78+
startNew()
4679
}
47-
48-
// Append the part between brackets
49-
partEnd = i
50-
parts.push(path.slice(partStart, partEnd))
51-
52-
state = 'in_property'
53-
partStart = i + 1
54-
} else if (c === '.' && previousState !== 'in_brackets') {
55-
// Append the current part to the parts if non-empty (would be in the case of concesutive brackets e.g. a[b][c])
56-
// The exception is the start of the path
57-
partEnd = i
58-
59-
if (partStart !== partEnd || previousState === 'start') {
60-
parts.push(path.slice(partStart, partEnd))
80+
} else if (prev === '[') {
81+
if (curr === '[' || curr === undefined) {
82+
// lb -> lb | end
83+
return error()
84+
} else if (curr === ']') {
85+
// lb -> rb
86+
capture()
87+
setInBrackets(false)
88+
} else {
89+
// lb -> ident
90+
startNew()
91+
}
92+
} else if (prev === ']') {
93+
if (curr === '.') {
94+
// rb -> dot
95+
startNew()
96+
} else if (curr === '[') {
97+
// rb -> lb
98+
setInBrackets(true)
99+
} else if (curr === undefined) {
100+
// rb -> end
101+
} else {
102+
// rb -> rb | ident
103+
return error()
61104
}
62-
63-
state = 'in_property'
64-
partStart = i + 1
65105
} else {
66-
// Keep collecting this part of the path
67-
partEnd = i + 1
68-
69-
// We've hit the end of the path
70-
// Append the last part
71-
if (partEnd === len) {
72-
parts.push(path.slice(partStart, partEnd))
106+
if (curr === '.' && !inBrackets) {
107+
// ident -> dot
108+
advance()
109+
capture()
110+
} else if (curr === ']') {
111+
// ident -> rb
112+
advance()
113+
capture()
114+
setInBrackets(false)
115+
} else if (curr === '[' && inBrackets) {
116+
// ident -> lb if in brackets
117+
return error()
118+
} else if (curr === '[' && !inBrackets) {
119+
// ident -> lb
120+
advance()
121+
capture()
122+
setInBrackets(true)
123+
} else if (curr === undefined) {
124+
// ident -> end
125+
advance()
126+
capture()
127+
} else {
128+
// ident -> ident
129+
advance()
73130
}
74131
}
75132
}
76133

77-
if (state === 'in_brackets') {
78-
throw new Error(`Unclosed path: ${path}`)
79-
}
80-
81134
return parts
82135
}

0 commit comments

Comments
 (0)