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 x82 x82 x11 x82 x82 x82 x11 x11 x11 x82 x81 x81 x41 x41 x11 x11 x5 x5 x5 x11 x41 x41 x30 x30 x41 x81 x64 x82 x11 x131 x131 x83 x83 x128 x131 x11 x16 x16 x38 x38 x2 x2 x38 x5 x5 x31 x31 x14 x14 x16 x11 x23 x23 x52 x52 x2 x2 x52 x3 x3 x3 x1 x1 x52 x52 x47 x47 x4 x4 x2 x2 x2 x2 x44 x44 x52 x21 x21 x23 x11 x114 x16 x16 x114 x23 x23 x75 x114 x11 x94 x55 x55 x39 x39 x94 x2 x2 x2 x2 x94 x94 x1 x1 x1 x94 x7 x7 x4 x4 x4 x4 x7 x94 x6 x39 x39 x94 x11 x90 x4 x4 x4 x4 x86 x90 x11 x63 x33 x33 x30 x30 x63 x71 x6 x6 x6 x6 x71 x30 x30 x63 x11 x937 x937 x11 x548 x548 x224 x176 x176 x548 x324 x324 x309 x309 x309 x324 x548 x63 x45 x45 x18 x18 x485 x485 x548 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
}
}
|