jq79 0.4.8 → 0.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jq79",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity. Single-file build, zero dependencies.",
5
5
  "keywords": [
6
6
  "reactive",
package/src/dom.ts CHANGED
@@ -173,10 +173,16 @@ function sanitizeNode(node: HTMLElement, depth: number, allowUrl?: AllowUrl): HT
173
173
  }
174
174
 
175
175
  export function sanitizeHTML(html: string, options?: SanitizeOptions): string {
176
- const doc = new DOMParser().parseFromString(html, 'text/html');
176
+ // parsear con <template> en vez de con DOMParser: el contenido de un template
177
+ // es un documento inerte igual (ni scripts ni imágenes se ejecutan al asignar
178
+ // innerHTML), pero el parseo de fragmento conserva el espacio en blanco inicial
179
+ // que el modo "before body" de un documento completo descartaría - así una
180
+ // primera línea indentada (un diff, un <pre>) llega con su sangría intacta
181
+ const template = document.createElement('template');
182
+ template.innerHTML = html;
177
183
  const container = document.createElement('div');
178
184
 
179
- appendSanitizedChildren(doc.body, container, 0, options?.allowUrl);
185
+ appendSanitizedChildren(template.content, container, 0, options?.allowUrl);
180
186
 
181
187
  return container.innerHTML;
182
188
  }
package/src/jq79.ts CHANGED
@@ -97,6 +97,12 @@ const interpolate = (template: string, scope: Record<string, any>): string =>
97
97
 
98
98
 
99
99
  const CONTROL_ATTRS = new Set([":attrs", ":class", ":value", ":checked", ":selected", ":if", ":elseif", ":else", ":each", ":key", ":with", ":text", ":html", ":html.allowed"])
100
+
101
+ // a control attribute is one the static-attr loop and nested-component prop
102
+ // collection must skip. The set holds the fixed names; `:class.<name>` (the
103
+ // single-flag shorthand) is open-ended, so it's matched by prefix - it can't be
104
+ // enumerated into the set
105
+ const isControlAttr = (attr: string): boolean => CONTROL_ATTRS.has(attr) || attr.startsWith(":class.")
100
106
  // `item in items`, `item, i in items`, `(value, key) in props` - the second
101
107
  // binding is the array index or the object key, parens optional (Vue-style).
102
108
  // The list expression can span lines, so it matches [\s\S] rather than `.`
@@ -238,7 +244,7 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
238
244
  Object.entries(node.attrs).forEach(([attr, value]) => {
239
245
  // the parent's scope stamp is stamped on every template element, this tag
240
246
  // included - it's not a prop, and the child renders under its own scope
241
- if (attr === SCOPE_ATTR || CONTROL_ATTRS.has(attr)) return
247
+ if (attr === SCOPE_ATTR || isControlAttr(attr)) return
242
248
  if (attr.startsWith("@")) {
243
249
  events.push([attr, value])
244
250
  } else if (attr === ":model" || attr.startsWith(":model.")) {
@@ -491,7 +497,7 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
491
497
  if (!(el instanceof HTMLUnknownElement || node.tag.includes("-"))) {
492
498
  console.warn(`jq79: ${key} on <${node.tag}> does nothing - :model binds component tags only (for now)`)
493
499
  }
494
- } else if (!CONTROL_ATTRS.has(key)) el.setAttribute(key, value)
500
+ } else if (!isControlAttr(key)) el.setAttribute(key, value)
495
501
  })
496
502
 
497
503
  const bindExpr = node.attrs[":attrs"]
@@ -509,17 +515,27 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
509
515
  })
510
516
  }
511
517
 
512
- // :class="expr" adds classes on top of the static `class` attribute. Only
513
- // classes this binding added are ever removed: the static list survives
514
- // every re-run, even when the expression names one of its classes and then
515
- // drops it (class="btn" :class="{ btn: cond }" keeps btn on false)
518
+ // :class="expr" adds classes on top of the static `class` attribute, and
519
+ // :class.<name>="expr" is the single-flag shorthand for `{ <name>: expr }`
520
+ // (the name routed through classNames, so an empty `:class.` can't reach
521
+ // classList.add, which throws on ""). Both feed one effect and one set of
522
+ // added classes: only classes this binding added are ever removed, so the
523
+ // static list survives every re-run, even when the expression names one of
524
+ // its classes and then drops it (class="btn" :class="{ btn: cond }" keeps
525
+ // btn on false)
516
526
  const classExpr = node.attrs[":class"]
517
- if (classExpr !== undefined) {
527
+ const classToggles = Object.entries(node.attrs)
528
+ .filter(([key]) => key.startsWith(":class."))
529
+ .map(([key, expr]): [string, string] => [key.slice(":class.".length), expr])
530
+ if (classExpr !== undefined || classToggles.length) {
518
531
  const staticClasses = new Set(classNames(node.attrs.class ?? ""))
519
532
  let bound: string[] = []
520
533
 
521
534
  fx.effect(() => {
522
- const next = classNames(evalExpr(classExpr, scope))
535
+ const next = classExpr !== undefined ? classNames(evalExpr(classExpr, scope)) : []
536
+ classToggles.forEach(([name, expr]) => {
537
+ if (evalExpr(expr, scope)) next.push(...classNames(name))
538
+ })
523
539
  bound.forEach(name => {
524
540
  if (!next.includes(name) && !staticClasses.has(name)) el.classList.remove(name)
525
541
  })