jq79 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom.d.ts +6 -1
- package/dist/jq79.cjs +13 -11
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.global.js +13 -11
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +13 -11
- package/dist/jq79.js.map +1 -1
- package/package.json +1 -1
- package/src/dom.ts +75 -7
- package/src/jq79.ts +205 -13
- package/src/transform.ts +163 -19
package/src/transform.ts
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
|
|
23
23
|
type SetupTransform = { vars: string[]; code: string }
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// a declaration whose target is an identifier (`let x`), an object pattern
|
|
26
|
+
// (`let { a }`, space optional) or an array pattern (`let [x]`)
|
|
27
|
+
const DECLARATION_START_RE = /(?:let|var|const)(?:\s+(?=[A-Za-z_$])|\s*(?=[{[]))/y
|
|
26
28
|
const REACTIVE_LABEL_RE = /\$:\s*/y
|
|
27
29
|
const IMPORT_CALL_RE = /import(?=\s*\()/y
|
|
28
30
|
const REACTIVE_ASSIGN_RE = /\$:\s*([A-Za-z_$][\w$]*)\s*=(?!=)/y
|
|
@@ -139,6 +141,28 @@ const skipRegex = (src: string, start: number): number => {
|
|
|
139
141
|
// inserts the semicolon before them
|
|
140
142
|
const CONTINUATION_RE = /^(\?\.|\?\?|&&|\|\||\*\*|[.,+\-*/%&|^<>=?:([])/
|
|
141
143
|
|
|
144
|
+
// the last meaningful character before `at`: walks back past whitespace and
|
|
145
|
+
// block comments, the way regexAllowed does. A line comment can't be skipped
|
|
146
|
+
// from behind (its start is only findable forwards), so a line ending in one
|
|
147
|
+
// reports the comment's text instead - callers treat that as "not the char I
|
|
148
|
+
// was looking for", which degrades to ending the statement, exactly as
|
|
149
|
+
// before this helper existed
|
|
150
|
+
const lastMeaningfulBefore = (src: string, at: number): string => {
|
|
151
|
+
let i = at - 1
|
|
152
|
+
while (i >= 0) {
|
|
153
|
+
const ch = src[i]
|
|
154
|
+
if (/\s/.test(ch)) { i--; continue }
|
|
155
|
+
if (ch === "/" && src[i - 1] === "*") {
|
|
156
|
+
const open = src.lastIndexOf("/*", i - 2)
|
|
157
|
+
if (open === -1) return ""
|
|
158
|
+
i = open - 1
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
return ch
|
|
162
|
+
}
|
|
163
|
+
return ""
|
|
164
|
+
}
|
|
165
|
+
|
|
142
166
|
// end of a statement starting at `start`: the first `;` or line break that
|
|
143
167
|
// isn't inside a string/comment or unbalanced brackets. A line break only
|
|
144
168
|
// ends the statement if the next line can't continue it, so leading-dot
|
|
@@ -147,6 +171,11 @@ const CONTINUATION_RE = /^(\?\.|\?\?|&&|\|\||\*\*|[.,+\-*/%&|^<>=?:([])/
|
|
|
147
171
|
// $: total = items
|
|
148
172
|
// .filter(item => item.active) <- still the same statement
|
|
149
173
|
// .length
|
|
174
|
+
//
|
|
175
|
+
// ...and only if the current line *can* end it: a line whose last meaningful
|
|
176
|
+
// character is `,` or `=` left its expression incomplete (a multi-line
|
|
177
|
+
// declarator list writes exactly this), so the statement continues - the
|
|
178
|
+
// same ASI call as the leading-token check, made from the other side
|
|
150
179
|
const findStatementEnd = (src: string, start: number): number => {
|
|
151
180
|
let depth = 0
|
|
152
181
|
let i = start
|
|
@@ -161,7 +190,10 @@ const findStatementEnd = (src: string, start: number): number => {
|
|
|
161
190
|
else if (depth <= 0 && ch === ";") return i
|
|
162
191
|
else if (depth <= 0 && ch === "\n") {
|
|
163
192
|
const next = skipToToken(src, i + 1)
|
|
164
|
-
|
|
193
|
+
const continues =
|
|
194
|
+
next < src.length &&
|
|
195
|
+
(CONTINUATION_RE.test(src.slice(next, next + 2)) || [",", "="].includes(lastMeaningfulBefore(src, i)))
|
|
196
|
+
if (!continues) return i
|
|
165
197
|
i = next
|
|
166
198
|
continue
|
|
167
199
|
}
|
|
@@ -170,6 +202,108 @@ const findStatementEnd = (src: string, start: number): number => {
|
|
|
170
202
|
return src.length
|
|
171
203
|
}
|
|
172
204
|
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// top-level declarations. `let x = 1` loses its keyword and becomes a scope
|
|
207
|
+
// assignment (x pre-declared on the store). A destructuring declarator
|
|
208
|
+
// becomes an *assignment pattern*: inside `with`, `({ a, b } = obj)` writes
|
|
209
|
+
// every binding through the reactive proxy - which is what makes it reactive.
|
|
210
|
+
// The parens keep the `{` from opening a block, and a leading `;` keeps the
|
|
211
|
+
// `(` from gluing onto the previous line as a call. Multi-declarator
|
|
212
|
+
// statements (`let a = 1, b = 2`) register every binding, not just the first.
|
|
213
|
+
// These lean on the pattern helpers defined with the props signature below
|
|
214
|
+
// (splitTopLevel, indexOfTopLevel, defaultAssignIndex); the scanner only
|
|
215
|
+
// runs long after the module evaluates, so the order is cosmetic
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
type Declarator = { raw: string; codeEnd: number }
|
|
219
|
+
|
|
220
|
+
// splits a declarator list at top-level commas, keeping each segment's raw
|
|
221
|
+
// text (layout and comments included) and where its last meaningful token
|
|
222
|
+
// ends - the spot a closing paren must go, so a trailing comment can't
|
|
223
|
+
// swallow it
|
|
224
|
+
const splitDeclarators = (src: string): Declarator[] => {
|
|
225
|
+
const parts: Declarator[] = []
|
|
226
|
+
let depth = 0
|
|
227
|
+
let start = 0
|
|
228
|
+
let lastEnd = 0
|
|
229
|
+
const flush = (end: number) => {
|
|
230
|
+
parts.push({ raw: src.slice(start, end), codeEnd: Math.max(0, lastEnd - start) })
|
|
231
|
+
start = end + 1
|
|
232
|
+
lastEnd = start
|
|
233
|
+
}
|
|
234
|
+
let i = 0
|
|
235
|
+
while (i < src.length) {
|
|
236
|
+
const ch = src[i]
|
|
237
|
+
if (ch === "'" || ch === '"' || ch === "`") { i = skipString(src, i); lastEnd = i; continue }
|
|
238
|
+
if (ch === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
|
|
239
|
+
if (ch === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
|
|
240
|
+
if (ch === "/" && regexAllowed(src, i)) { i = skipRegex(src, i); lastEnd = i; continue }
|
|
241
|
+
if ("([{".includes(ch)) depth++
|
|
242
|
+
else if (")]}".includes(ch)) depth--
|
|
243
|
+
else if (ch === "," && depth <= 0) { flush(i); i++; continue }
|
|
244
|
+
if (!/\s/.test(ch)) lastEnd = i + 1
|
|
245
|
+
i++
|
|
246
|
+
}
|
|
247
|
+
flush(src.length)
|
|
248
|
+
return parts
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// the *binding* names a destructuring pattern declares - unlike
|
|
252
|
+
// parsePropsPattern, which answers "which props" (the keys), this answers
|
|
253
|
+
// "which variables": `{ a: x }` binds x, `{ a: { b } }` binds b,
|
|
254
|
+
// `[x, ...rest]` binds x and rest. Defaults are stripped; what remains is a
|
|
255
|
+
// nested pattern (recurse) or the bound identifier
|
|
256
|
+
const patternBindings = (src: string): string[] => {
|
|
257
|
+
const pattern = src.trim()
|
|
258
|
+
if (!pattern.startsWith("{") && !pattern.startsWith("[")) {
|
|
259
|
+
return IDENTIFIER_RE.test(pattern) ? [pattern] : []
|
|
260
|
+
}
|
|
261
|
+
const names: string[] = []
|
|
262
|
+
for (let part of splitTopLevel(pattern.slice(1, patternCloseIndex(pattern)))) {
|
|
263
|
+
if (part.startsWith("...")) part = part.slice(3).trim()
|
|
264
|
+
const assign = defaultAssignIndex(part)
|
|
265
|
+
if (assign !== -1) part = part.slice(0, assign).trim()
|
|
266
|
+
if (pattern.startsWith("{")) {
|
|
267
|
+
const colon = indexOfTopLevel(part, ":")
|
|
268
|
+
if (colon !== -1) {
|
|
269
|
+
names.push(...patternBindings(part.slice(colon + 1)))
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
names.push(...patternBindings(part))
|
|
274
|
+
}
|
|
275
|
+
return names
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// one `let/var/const` declarator list, rewritten to scope assignments.
|
|
279
|
+
// Each segment's initializer is re-scanned so an `import()` inside it is
|
|
280
|
+
// rewritten like anywhere else - nothing else can match in there, since a
|
|
281
|
+
// nested top-level declaration inside a declarator is a SyntaxError in JS
|
|
282
|
+
const rewriteDeclarators = (src: string): SetupTransform => {
|
|
283
|
+
const vars: string[] = []
|
|
284
|
+
const rewritten = splitDeclarators(src).map(({ raw, codeEnd }) => {
|
|
285
|
+
const lead = raw.match(/^\s*/)![0]
|
|
286
|
+
if (codeEnd <= lead.length) return { text: raw, empty: true }
|
|
287
|
+
const body = raw.slice(lead.length, codeEnd)
|
|
288
|
+
const tail = raw.slice(codeEnd)
|
|
289
|
+
const assign = defaultAssignIndex(body)
|
|
290
|
+
const target = (assign === -1 ? body : body.slice(0, assign)).trim()
|
|
291
|
+
const isPattern = body[0] === "{" || body[0] === "["
|
|
292
|
+
if (isPattern) vars.push(...patternBindings(target))
|
|
293
|
+
else if (IDENTIFIER_RE.test(target)) vars.push(target)
|
|
294
|
+
const code = transformSetupScript(body).code
|
|
295
|
+
return { text: `${lead}${isPattern ? `(${code})` : code}${tail}`, empty: false }
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// a trailing comment-only segment (`let a = 1, // note` cut at its line
|
|
299
|
+
// end) is re-attached without its comma, so the output stays a statement
|
|
300
|
+
const tail: string[] = []
|
|
301
|
+
while (rewritten.length && rewritten[rewritten.length - 1].empty) tail.unshift(rewritten.pop()!.text)
|
|
302
|
+
let code = rewritten.map(part => part.text).join(",") + tail.join("")
|
|
303
|
+
if (code.trimStart().startsWith("(")) code = `;${code}`
|
|
304
|
+
return { vars, code }
|
|
305
|
+
}
|
|
306
|
+
|
|
173
307
|
export const transformSetupScript = (src: string): SetupTransform => {
|
|
174
308
|
const vars: string[] = []
|
|
175
309
|
let out = ""
|
|
@@ -217,12 +351,15 @@ export const transformSetupScript = (src: string): SetupTransform => {
|
|
|
217
351
|
}
|
|
218
352
|
|
|
219
353
|
if (depth === 0 && atStatementStart) {
|
|
220
|
-
|
|
221
|
-
const decl =
|
|
354
|
+
DECLARATION_START_RE.lastIndex = i
|
|
355
|
+
const decl = DECLARATION_START_RE.exec(src)
|
|
222
356
|
if (decl) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
357
|
+
const start = i + decl[0].length
|
|
358
|
+
const end = findStatementEnd(src, start)
|
|
359
|
+
const { vars: names, code } = rewriteDeclarators(src.slice(start, end))
|
|
360
|
+
vars.push(...names)
|
|
361
|
+
out += code
|
|
362
|
+
i = end
|
|
226
363
|
atStatementStart = false
|
|
227
364
|
continue
|
|
228
365
|
}
|
|
@@ -342,6 +479,24 @@ export type PropDecl = { name: string; default?: string }
|
|
|
342
479
|
|
|
343
480
|
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/
|
|
344
481
|
|
|
482
|
+
// index of the bracket that closes the one opening at src[0], skipping
|
|
483
|
+
// strings, comments and regex literals; src.length when unbalanced
|
|
484
|
+
const patternCloseIndex = (src: string): number => {
|
|
485
|
+
let depth = 0
|
|
486
|
+
let i = 0
|
|
487
|
+
while (i < src.length) {
|
|
488
|
+
const ch = src[i]
|
|
489
|
+
if (ch === "'" || ch === '"' || ch === "`") { i = skipString(src, i); continue }
|
|
490
|
+
if (ch === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
|
|
491
|
+
if (ch === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
|
|
492
|
+
if (ch === "/" && regexAllowed(src, i)) { i = skipRegex(src, i); continue }
|
|
493
|
+
if ("([{".includes(ch)) depth++
|
|
494
|
+
else if (")]}".includes(ch) && --depth === 0) return i
|
|
495
|
+
i++
|
|
496
|
+
}
|
|
497
|
+
return src.length
|
|
498
|
+
}
|
|
499
|
+
|
|
345
500
|
// index of the first `ch` at bracket depth 0, skipping strings and comments
|
|
346
501
|
const indexOfTopLevel = (src: string, ch: string): number => {
|
|
347
502
|
let depth = 0
|
|
@@ -407,18 +562,7 @@ export const parsePropsPattern = (pattern: string | undefined): PropDecl[] | nul
|
|
|
407
562
|
|
|
408
563
|
// to the `}` that closes the pattern, so a parameter's own default value
|
|
409
564
|
// (`({ label } = {})`) is left out of it
|
|
410
|
-
|
|
411
|
-
let close = 0
|
|
412
|
-
while (close < src.length) {
|
|
413
|
-
const ch = src[close]
|
|
414
|
-
if (ch === "'" || ch === '"' || ch === "`") { close = skipString(src, close); continue }
|
|
415
|
-
if (ch === "/" && src[close + 1] === "/") { close = skipLineComment(src, close); continue }
|
|
416
|
-
if (ch === "/" && src[close + 1] === "*") { close = skipBlockComment(src, close); continue }
|
|
417
|
-
if (ch === "/" && regexAllowed(src, close)) { close = skipRegex(src, close); continue }
|
|
418
|
-
if ("([{".includes(ch)) depth++
|
|
419
|
-
else if (")]}".includes(ch) && --depth === 0) break
|
|
420
|
-
close++
|
|
421
|
-
}
|
|
565
|
+
const close = patternCloseIndex(src)
|
|
422
566
|
if (close >= src.length) return null // unbalanced: not a pattern we can read
|
|
423
567
|
|
|
424
568
|
const props: PropDecl[] = []
|