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/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?.trim() ?? ""
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
- const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<string, any>, fx: EffectScope): Node => {
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
- fx.effect(() => { textNode.textContent = interpolate(node, scope) })
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
- while (i < nodes.length && typeof nodes[i] !== "string" && ":elseif" in (nodes[i] as TemplateNode).attrs) {
429
- const elseifNode = nodes[i] as TemplateNode
430
- branches.push({ expr: elseifNode.attrs[":elseif"], node: elseifNode })
431
- i++
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
- if (i < nodes.length && typeof nodes[i] !== "string" && ":else" in (nodes[i] as TemplateNode).attrs) {
434
- branches.push({ node: nodes[i] as TemplateNode })
435
- i++
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>>): Node =>
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) {