|
15 | 15 | export function toPath(path) {
|
16 | 16 | if (Array.isArray(path)) return path
|
17 | 17 |
|
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 |
20 | 40 |
|
21 | 41 | let parts = []
|
| 42 | + let inBrackets = false |
22 | 43 | let partStart = 0
|
23 | 44 | let partEnd = 0
|
24 | 45 |
|
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] |
33 | 49 |
|
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)) |
36 | 57 |
|
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 |
39 | 71 | }
|
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() |
46 | 79 | }
|
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() |
61 | 104 | }
|
62 |
| - |
63 |
| - state = 'in_property' |
64 |
| - partStart = i + 1 |
65 | 105 | } 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() |
73 | 130 | }
|
74 | 131 | }
|
75 | 132 | }
|
76 | 133 |
|
77 |
| - if (state === 'in_brackets') { |
78 |
| - throw new Error(`Unclosed path: ${path}`) |
79 |
| - } |
80 |
| - |
81 | 134 | return parts
|
82 | 135 | }
|
0 commit comments