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 |
x10 x10 x90 x90 x10 x90 x90 x90 x10 x10 x10 x90 x169 x169 x627 x209 x220 x220 x225 x675 x225 x220 x209 x209 x238 x238 x209 x169 x153 x90 x10 x140 x140 x223 x223 x267 x140 x10 x26 x26 x192 x64 x66 x66 x64 x69 x69 x95 x95 x40 x40 x26 x10 x33 x33 x255 x85 x87 x87 x85 x88 x88 x88 x89 x89 x85 x85 x132 x132 x136 x136 x138 x138 x138 x138 x176 x176 x85 x54 x54 x33 x10 x123 x139 x139 x123 x146 x146 x197 x123 x10 x104 x159 x159 x143 x143 x104 x106 x106 x106 x106 x104 x104 x142 x142 x142 x104 x111 x111 x115 x115 x115 x115 x111 x104 x110 x143 x143 x104 x10 x100 x104 x312 x104 x104 x186 x100 x10 x73 x106 x106 x161 x805 x73 x202 x208 x208 x208 x208 x202 x161 x161 x73 x10 x942 x942 x10 x554 x554 x778 x954 x954 x554 x874 x874 x1180 x1180 x1180 x874 x554 x616 x661 x661 x633 x633 x1036 x1036 x554 x10 |
// 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) {
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
}
}
|