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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
x14 x14 x115 x115 x14 x115 x115 x115 x14 x14 x14 x115 x114 x114 x61 x61 x11 x11 x5 x5 x5 x11 x61 x61 x50 x50 x61 x114 x94 x115 x14 x186 x186 x118 x118 x183 x186 x14 x20 x20 x47 x24 x47 x1 x1 x47 x2 x2 x47 x7 x7 x7 x37 x37 x18 x18 x20 x14 x36 x36 x76 x76 x2 x2 x76 x2 x2 x76 x8 x4 x4 x4 x4 x8 x8 x2 x8 x5 x5 x76 x76 x64 x64 x16 x16 x6 x6 x6 x10 x10 x57 x57 x76 x34 x34 x36 x14 x169 x20 x20 x169 x36 x36 x113 x169 x14 x144 x100 x100 x44 x44 x144 x2 x2 x2 x2 x144 x144 x1 x1 x1 x144 x7 x7 x4 x4 x4 x4 x7 x144 x10 x44 x44 x144 x14 x120 x4 x4 x4 x4 x116 x120 x14 x73 x38 x38 x35 x35 x73 x72 x6 x6 x6 x6 x72 x35 x35 x73 x14 x1418 x1418 x14 x783 x783 x337 x249 x249 x783 x446 x446 x428 x428 x428 x446 x783 x106 x85 x85 x21 x21 x677 x677 x783 x14 |
// 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("]")) {
while (this.#consume(",", { optional: true })) {
// Skip holes and trailing commas
}
if (this.#peek("]")) {
break
}
if (this.#spreading()) {
break
}
if (this.#nested()) {
this.#value()
continue
}
this.#identifiers.push(this.#identifier())
}
this.#consume("]")
return true
}
/** Consume object. */
#object() {
this.#consume("{")
while (!this.#peek("}")) {
this.#consume(",", { optional: true })
if (this.#peek("}")) {
break
}
if (this.#spreading()) {
break
}
// Handle computed, string and numeric properties
if (this.#peek(/^[[\d"']/)) {
if (this.#peek("[")) {
this.#group()
} else {
this.#consume(/^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\d[\p{L}\p{N}$_.]*)/u)
}
this.#consume(":")
if (this.#nested()) {
this.#value()
} else {
this.#identifiers.push(this.#identifier())
}
} // Handle regular properties
else {
let identifier = this.#identifier()
if (this.#peek(":")) {
this.#consume(":")
if (this.#nested()) {
this.#value()
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
}
}
|