1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
x11 x11 x92 x92 x11 x92 x92 x92 x11 x11 x11 x92 x172 x172 x636 x212 x223 x223 x228 x684 x228 x223 x212 x212 x241 x241 x212 x172 x155 x92 x11 x141 x141 x224 x224 x268 x141 x11 x27 x27 x195 x65 x67 x67 x65 x70 x70 x96 x96 x41 x41 x27 x11 x34 x34 x258 x86 x88 x88 x86 x89 x89 x89 x90 x90 x86 x86 x133 x133 x137 x137 x139 x139 x139 x139 x177 x177 x86 x55 x55 x34 x11 x124 x140 x140 x124 x147 x147 x198 x124 x11 x105 x160 x160 x144 x144 x105 x107 x107 x107 x107 x105 x105 x143 x143 x143 x105 x112 x112 x116 x116 x116 x116 x112 x105 x111 x144 x144 x105 x11 x101 x105 x315 x105 x105 x187 x101 x11 x74 x107 x107 x162 x810 x74 x203 x209 x209 x209 x209 x203 x162 x162 x74 x11 x944 x944 x11 x556 x556 x780 x956 x956 x556 x877 x877 x1183 x1183 x1183 x877 x556 x619 x664 x664 x637 x637 x1038 x1038 x556 x11 |
// Imports
import type { Optional } from "@libs/typing"
/**
* Expression parser for loops content.
*
* Parse any valid expression that can be evaluated in EcmaScript by `for (<expression>)` syntax.
*
* This simple AST does not guarantee syntax validity (which will be enforced by runtime anyways),
* but it should be able to extract any defined identifiers, whether they're initialized through destructuring, spreading, aliasing, etc.
*
* {@link https://tc39.es/ecma262/#sec-for-statement | Reference}
*
* ```ts
* import { Expression } from "./parse.ts"
*
* const identifiers = Expression.parse("const {a, b:c} of []")
* console.assert(identifiers.join(",") === ["a", "c"].join(","))
* ```
*
* @author Simon Lecoq (lowlighter)
* @license MIT
*/
export class Expression {
/** Parse expression. */
static parse(expression: string): string[] {
return new Expression(expression).#parse()
}
/** Constructor. */
private constructor(expression: string) {
this.#expression = expression.trim()
this.#identifiers = [] as string[]
}
/** Expression. */
#expression
/** Collected identifiers. */
#identifiers
/** Parse expressions. */
#parse() {
if (!this.#peek(";")) {
this.#consume(/^(?:let|const|var) /)
if (!this.#nested()) {
this.#identifiers.push(this.#identifier({ assignment: false }))
// Handle regular `for` loops
if (this.#peek("=")) {
this.#value(/^[\s\S]*?(?=[,;])/)
while (this.#peek(",")) {
this.#consume(",")
this.#identifiers.push(this.#identifier({ value: /^[\s\S]*?(?=[,;])/ }))
}
this.#consume(/;[\s\S]*;[\s\S]*$/)
} // Handle `for..of` and `for..in`
else {
this.#consume(/^(?:of|in) /)
}
}
}
return this.#identifiers
}
/** Consume identifier. */
#identifier({ assignment = true, value = undefined as Optional<RegExp> } = {}) {
const identifier = this.#consume(/^([\p{L}$_][\p{L}\p{N}$_]*)/u)
if (assignment) {
this.#value(value)
}
return identifier
}
/** Consume array. */
#array() {
this.#consume("[")
while (!this.#peek("]")) {
this.#consume(",", { optional: true })
if (this.#spreading()) {
break
}
if (this.#nested()) {
continue
}
this.#identifiers.push(this.#identifier())
}
this.#consume("]")
return true
}
/** Consume object. */
#object() {
this.#consume("{")
while (!this.#peek("}")) {
this.#consume(",", { optional: true })
if (this.#spreading()) {
break
}
// Handle computed properties
if (this.#peek("[")) {
this.#group()
this.#consume(":")
if (!this.#nested()) {
this.#identifiers.push(this.#identifier())
}
} // Handle regular properties
else {
let identifier = this.#identifier()
if (this.#peek(":")) {
this.#consume(":")
if (this.#nested()) {
continue
}
identifier = this.#identifier()
}
this.#identifiers.push(identifier)
}
}
this.#consume("}")
return true
}
/** Consume nested element. */
#nested() {
if (this.#peek("[")) {
return this.#array()
}
if (this.#peek("{")) {
return this.#object()
}
return false
}
/** Consume value. */
#value(value = /^[\s\S]*?(?=[,\]}])/) {
if (!this.#peek("=")) {
return false
}
this.#consume("=")
let f = false
// Handle regular functions
if (this.#peek(/^(?:async\s+)?function(?:\s*\*)?\s*\(/)) {
this.#consume(/^(?:async\s+)?function(?:\s*\*)?/)
f = true
this.#group(/^(\()/)
this.#group(/^(\{)/)
} // Handle rrow functions
else if (this.#peek(/^async\s+\(/)) {
this.#consume("async")
f = true
}
if (this.#peek("(")) {
this.#group(/^(\()/)
if (this.#peek("=>")) {
this.#consume("=>")
this.#group()
f = true
}
}
while ((!f) && (this.#group())) {
// Continue capturing groups to match case like {foo:"bar"}[i], (x => y)(), etc.
}
this.#consume(value)
return true
}
/** Consume spreading. */
#spreading() {
if (this.#peek("...")) {
this.#consume("...")
this.#identifiers.push(this.#identifier({ assignment: false }))
return true
}
return false
}
/** Consume group. */
#group(opener = /^([({[])/) {
if (!this.#peek(opener)) {
return false
}
const open = this.#consume(opener)
const close = { "(": ")", "{": "}", "[": "]" }[open] as string
for (let i = 0; (i < this.#expression.length) && (close !== this.#expression[i]); i++) {
if (opener.test(this.#expression[i])) {
this.#expression = this.#expression.slice(i)
this.#group()
break
}
}
this.#expression = this.#expression.slice(this.#expression.indexOf(close) + 1).trim()
return true
}
/** Peek at next token. */
#peek(token: RegExp | string) {
return typeof token === "string" ? this.#expression.startsWith(token) : token.test(this.#expression)
}
/** Consume token. */
#consume(token: RegExp | string, { optional = false } = {}) {
let captured = null
if (typeof token === "string") {
if (this.#expression.startsWith(token)) {
captured = token
}
} else {
const match = this.#expression.match(token)
if (match) {
captured = match[1]
token = match[0]
}
}
if (captured === null) {
if (optional) {
return ""
}
throw new SyntaxError(`Expected ${token} at ${this.#expression}`)
}
this.#expression = this.#expression.replace(token, "").trim()
return captured
}
}
|