jq79 0.3.24 → 0.3.26
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/jq79.cjs +9 -9
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +1 -1
- package/dist/jq79.global.js +9 -9
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +9 -9
- package/dist/jq79.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +56 -28
package/src/jq79.ts
CHANGED
|
@@ -24,12 +24,18 @@ type TagBlock = {
|
|
|
24
24
|
const elementAttrs = (el: Element): Record<string, string> =>
|
|
25
25
|
Object.fromEntries(Array.from(el.attributes).map(attr => [attr.name, attr.value]))
|
|
26
26
|
|
|
27
|
+
// text is kept verbatim - not trimmed, not dropped when it's only whitespace.
|
|
28
|
+
// A template is HTML, so the space in `<span>a</span>\n<span>b</span>` is the
|
|
29
|
+
// same space the browser would collapse-and-render between them, and CSS gets
|
|
30
|
+
// to decide what it's worth (nothing in a block or flex container, one space
|
|
31
|
+
// between inline elements). Trimming it here, as this used to, silently glued
|
|
32
|
+
// siblings together and ate the spaces in `hola <b>mundo</b> adios`
|
|
27
33
|
const elementToAST = (el: Element): TemplateNode => ({
|
|
28
34
|
tag: el.tagName.toLowerCase(),
|
|
29
35
|
attrs: elementAttrs(el),
|
|
30
36
|
children: Array.from(el.childNodes).flatMap((node): (TemplateNode | string)[] => {
|
|
31
37
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
32
|
-
const text = node.textContent
|
|
38
|
+
const text = node.textContent ?? ""
|
|
33
39
|
return text ? [text] : []
|
|
34
40
|
}
|
|
35
41
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -113,8 +119,13 @@ const findComponentKey = (scope: Record<string, any>, tag: string): string | nul
|
|
|
113
119
|
// become camelCase. Props stay live: a parent effect re-evaluates each
|
|
114
120
|
// expression and writes it into the child's store. The component variable is
|
|
115
121
|
// reactive too - while it's undefined (e.g. an `await import(...)` still in
|
|
116
|
-
// flight) nothing renders, and the child appears when it resolves
|
|
117
|
-
|
|
122
|
+
// flight) nothing renders, and the child appears when it resolves.
|
|
123
|
+
// `shadow` is the parent's style mode, carried down the whole render: a child
|
|
124
|
+
// of a shadow-rendered component renders inside that shadow root, so its
|
|
125
|
+
// <style> has to go in there with it - document.head can't reach into a shadow
|
|
126
|
+
// tree, and a style that never applies to its own component would still be
|
|
127
|
+
// restyling the page around it
|
|
128
|
+
const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<string, any>, fx: EffectScope, shadow: boolean): Node => {
|
|
118
129
|
const anchor = document.createComment(key)
|
|
119
130
|
const wrapper = document.createDocumentFragment()
|
|
120
131
|
wrapper.appendChild(anchor)
|
|
@@ -160,8 +171,11 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
160
171
|
const seed = untracked(() =>
|
|
161
172
|
Object.fromEntries(Object.entries(props).map(([name, expr]) => [name, evalExpr(expr, scope)]))
|
|
162
173
|
)
|
|
174
|
+
// mounting into a fragment attaches no shadow root of its own: a
|
|
175
|
+
// shadow-rendered child keeps its <style> elements inline, next to the DOM
|
|
176
|
+
// they style, and the parent's shadow root is what scopes both
|
|
163
177
|
const holder = document.createDocumentFragment()
|
|
164
|
-
instance.render(seed).mount(holder)
|
|
178
|
+
;(shadow ? instance.renderShadow(seed) : instance.render(seed)).mount(holder)
|
|
165
179
|
anchor.parentNode!.insertBefore(holder, anchor.nextSibling)
|
|
166
180
|
|
|
167
181
|
const syncFx = createEffectScope(scope)
|
|
@@ -222,7 +236,7 @@ const createWithScope = (expr: string, scope: Record<string, any>): Record<strin
|
|
|
222
236
|
// normally. :if/:elseif/:else/:each are handled by renderNodes, which decides
|
|
223
237
|
// *whether*/*how many times* a node is rendered before calling this. Tags
|
|
224
238
|
// matching a PascalCase scope variable render as nested components instead
|
|
225
|
-
const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: EffectScope): Node => {
|
|
239
|
+
const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: EffectScope, shadow: boolean): Node => {
|
|
226
240
|
// :with applies to the element's own bindings (@events, :attrs) and its
|
|
227
241
|
// whole subtree. On a :each element the item scope is already in place, so
|
|
228
242
|
// :with="item" works
|
|
@@ -230,7 +244,7 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
230
244
|
const scope = withExpr !== undefined ? createWithScope(withExpr, outerScope) : outerScope
|
|
231
245
|
|
|
232
246
|
const componentKey = findComponentKey(scope, node.tag)
|
|
233
|
-
if (componentKey) return renderNestedComponent(componentKey, node, scope, fx)
|
|
247
|
+
if (componentKey) return renderNestedComponent(componentKey, node, scope, fx, shadow)
|
|
234
248
|
|
|
235
249
|
const el = document.createElement(node.tag)
|
|
236
250
|
|
|
@@ -246,7 +260,7 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
246
260
|
const key = findComponentKey(scope, node.tag)
|
|
247
261
|
if (!key) return
|
|
248
262
|
upgraded = true
|
|
249
|
-
el.replaceWith(renderNestedComponent(key, node, scope, fx))
|
|
263
|
+
el.replaceWith(renderNestedComponent(key, node, scope, fx, shadow))
|
|
250
264
|
})
|
|
251
265
|
}
|
|
252
266
|
|
|
@@ -281,7 +295,7 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
281
295
|
} else if (htmlExpr !== undefined) {
|
|
282
296
|
fx.effect(() => { el.innerHTML = sanitizeHTML(String(evalExpr(htmlExpr, scope) ?? "")) })
|
|
283
297
|
} else {
|
|
284
|
-
el.appendChild(renderNodes(node.children, scope, fx))
|
|
298
|
+
el.appendChild(renderNodes(node.children, scope, fx, shadow))
|
|
285
299
|
}
|
|
286
300
|
|
|
287
301
|
return el
|
|
@@ -291,7 +305,7 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
291
305
|
// can be swapped in place without disturbing sibling positions. Only depends
|
|
292
306
|
// on whatever the branch expressions read (e.g. "score"), and skips
|
|
293
307
|
// rebuilding entirely when the active branch hasn't actually changed
|
|
294
|
-
const renderConditional = (branches: ConditionalBranch[], scope: Record<string, any>, fx: EffectScope): Node => {
|
|
308
|
+
const renderConditional = (branches: ConditionalBranch[], scope: Record<string, any>, fx: EffectScope, shadow: boolean): Node => {
|
|
295
309
|
const anchor = document.createComment("if")
|
|
296
310
|
const wrapper = document.createDocumentFragment()
|
|
297
311
|
wrapper.appendChild(anchor)
|
|
@@ -311,7 +325,7 @@ const renderConditional = (branches: ConditionalBranch[], scope: Record<string,
|
|
|
311
325
|
if (!next) return
|
|
312
326
|
|
|
313
327
|
branchFx = createEffectScope(scope)
|
|
314
|
-
current = renderNode(next.node, scope, branchFx)
|
|
328
|
+
current = renderNode(next.node, scope, branchFx, shadow)
|
|
315
329
|
anchor.parentNode!.insertBefore(current, anchor.nextSibling)
|
|
316
330
|
})
|
|
317
331
|
|
|
@@ -340,7 +354,7 @@ type EachEntry = { key: any; item: any; scope: Record<string, any>; node: Node;
|
|
|
340
354
|
// the first change - add :key for anything that gets reordered or filtered.
|
|
341
355
|
// Each item gets its own scope via Object.create(scope), so `item`/`$index`
|
|
342
356
|
// shadow same-named outer bindings without copying the parent scope's keys
|
|
343
|
-
const renderEach = (node: TemplateNode, scope: Record<string, any>, fx: EffectScope): Node => {
|
|
357
|
+
const renderEach = (node: TemplateNode, scope: Record<string, any>, fx: EffectScope, shadow: boolean): Node => {
|
|
344
358
|
const match = node.attrs[":each"].match(EACH_PATTERN)
|
|
345
359
|
if (!match) return document.createComment(`invalid :each expression "${node.attrs[":each"]}"`)
|
|
346
360
|
|
|
@@ -376,7 +390,7 @@ const renderEach = (node: TemplateNode, scope: Record<string, any>, fx: EffectSc
|
|
|
376
390
|
existing?.node.parentNode?.removeChild(existing.node)
|
|
377
391
|
|
|
378
392
|
const itemFx = createEffectScope(scope)
|
|
379
|
-
return { key, item, scope: itemScope, fx: itemFx, node: renderNode(itemNode, itemScope, itemFx) }
|
|
393
|
+
return { key, item, scope: itemScope, fx: itemFx, node: renderNode(itemNode, itemScope, itemFx, shadow) }
|
|
380
394
|
})
|
|
381
395
|
|
|
382
396
|
const nextKeys = new Set(nextEntries.map(entry => entry.key))
|
|
@@ -401,7 +415,7 @@ const renderEach = (node: TemplateNode, scope: Record<string, any>, fx: EffectSc
|
|
|
401
415
|
|
|
402
416
|
// renders a list of sibling template nodes (text + elements), grouping
|
|
403
417
|
// consecutive :if/:elseif/:else nodes into a single conditional block
|
|
404
|
-
const renderNodes = (nodes: (TemplateNode | string)[], scope: Record<string, any>, fx: EffectScope): DocumentFragment => {
|
|
418
|
+
const renderNodes = (nodes: (TemplateNode | string)[], scope: Record<string, any>, fx: EffectScope, shadow = false): DocumentFragment => {
|
|
405
419
|
const fragment = document.createDocumentFragment()
|
|
406
420
|
let i = 0
|
|
407
421
|
|
|
@@ -409,15 +423,17 @@ const renderNodes = (nodes: (TemplateNode | string)[], scope: Record<string, any
|
|
|
409
423
|
const node = nodes[i]
|
|
410
424
|
|
|
411
425
|
if (typeof node === "string") {
|
|
412
|
-
const textNode = document.createTextNode(
|
|
413
|
-
|
|
426
|
+
const textNode = document.createTextNode(node)
|
|
427
|
+
// static text is most of a template (all of its indentation, for a start):
|
|
428
|
+
// only text with a {{ expression }} in it needs an effect to stay in sync
|
|
429
|
+
if (node.includes("{{")) fx.effect(() => { textNode.textContent = interpolate(node, scope) })
|
|
414
430
|
fragment.appendChild(textNode)
|
|
415
431
|
i++
|
|
416
432
|
continue
|
|
417
433
|
}
|
|
418
434
|
|
|
419
435
|
if (":each" in node.attrs) {
|
|
420
|
-
fragment.appendChild(renderEach(node, scope, fx))
|
|
436
|
+
fragment.appendChild(renderEach(node, scope, fx, shadow))
|
|
421
437
|
i++
|
|
422
438
|
continue
|
|
423
439
|
}
|
|
@@ -425,29 +441,41 @@ const renderNodes = (nodes: (TemplateNode | string)[], scope: Record<string, any
|
|
|
425
441
|
if (":if" in node.attrs) {
|
|
426
442
|
const branches: ConditionalBranch[] = [{ expr: node.attrs[":if"], node }]
|
|
427
443
|
i++
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
444
|
+
|
|
445
|
+
// the branches of a chain are siblings in the AST, but the template writes
|
|
446
|
+
// them on their own lines - so the whitespace between them is indentation
|
|
447
|
+
// and nothing else, and it's dropped rather than rendered: only one branch
|
|
448
|
+
// is ever in the DOM, so there is nothing for it to be a space *between*
|
|
449
|
+
const nextBranch = (attr: string): TemplateNode | undefined => {
|
|
450
|
+
let next = i
|
|
451
|
+
while (next < nodes.length && typeof nodes[next] === "string" && !(nodes[next] as string).trim()) next++
|
|
452
|
+
const candidate = nodes[next]
|
|
453
|
+
if (typeof candidate === "object" && attr in candidate.attrs) {
|
|
454
|
+
i = next + 1
|
|
455
|
+
return candidate
|
|
456
|
+
}
|
|
457
|
+
return undefined
|
|
432
458
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
459
|
+
|
|
460
|
+
for (let elseif = nextBranch(":elseif"); elseif; elseif = nextBranch(":elseif")) {
|
|
461
|
+
branches.push({ expr: elseif.attrs[":elseif"], node: elseif })
|
|
436
462
|
}
|
|
463
|
+
const elseNode = nextBranch(":else")
|
|
464
|
+
if (elseNode) branches.push({ node: elseNode })
|
|
437
465
|
|
|
438
|
-
fragment.appendChild(renderConditional(branches, scope, fx))
|
|
466
|
+
fragment.appendChild(renderConditional(branches, scope, fx, shadow))
|
|
439
467
|
continue
|
|
440
468
|
}
|
|
441
469
|
|
|
442
|
-
fragment.appendChild(renderNode(node, scope, fx))
|
|
470
|
+
fragment.appendChild(renderNode(node, scope, fx, shadow))
|
|
443
471
|
i++
|
|
444
472
|
}
|
|
445
473
|
|
|
446
474
|
return fragment
|
|
447
475
|
}
|
|
448
476
|
|
|
449
|
-
export const renderComponent = (component: Component79, data: ReactiveDeepData<Record<string, any
|
|
450
|
-
renderNodes(component.template, data, createEffectScope(data))
|
|
477
|
+
export const renderComponent = (component: Component79, data: ReactiveDeepData<Record<string, any>>, shadow = false): Node =>
|
|
478
|
+
renderNodes(component.template, data, createEffectScope(data), shadow)
|
|
451
479
|
|
|
452
480
|
type ComponentParts = {
|
|
453
481
|
template: TemplateNode[]
|
|
@@ -930,7 +958,7 @@ export class Component79 {
|
|
|
930
958
|
})
|
|
931
959
|
|
|
932
960
|
const content = document.createDocumentFragment()
|
|
933
|
-
content.append(this.startMarker, renderNodes(this.template, store, fx), this.endMarker)
|
|
961
|
+
content.append(this.startMarker, renderNodes(this.template, store, fx, shadow), this.endMarker)
|
|
934
962
|
this.content = content
|
|
935
963
|
|
|
936
964
|
if (shadow) {
|