jq79 0.3.19 → 0.3.21

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/dom.ts CHANGED
@@ -1,16 +1,25 @@
1
1
  // DOM helpers: tiny query/create utilities, also injected into component
2
2
  // scripts as $, $$ and $create
3
3
 
4
- export const $ = (selectorOrEl: string | Element, selector?: string) =>
5
- typeof selectorOrEl === "string"
4
+ // $(selector) queries the document; $(el, selector) queries within el. The
5
+ // selector is required in the element form - an empty one is a SyntaxError
6
+ export function $(selector: string): Element | null
7
+ export function $(el: Element, selector: string): Element | null
8
+ export function $(selectorOrEl: string | Element, selector?: string): Element | null {
9
+ return typeof selectorOrEl === "string"
6
10
  ? document.querySelector(selectorOrEl)
7
- : selectorOrEl.querySelector(selector || "")
11
+ : selectorOrEl.querySelector(selector!)
12
+ }
8
13
 
9
- export const $$ = (selectorOrEl: string | Element, selector?: string) => Array.from(
10
- typeof selectorOrEl === "string"
11
- ? document.querySelectorAll(selectorOrEl)
12
- : selectorOrEl.querySelectorAll(selector || "")
13
- )
14
+ export function $$(selector: string): Element[]
15
+ export function $$(el: Element, selector: string): Element[]
16
+ export function $$(selectorOrEl: string | Element, selector?: string): Element[] {
17
+ return Array.from(
18
+ typeof selectorOrEl === "string"
19
+ ? document.querySelectorAll(selectorOrEl)
20
+ : selectorOrEl.querySelectorAll(selector!)
21
+ )
22
+ }
14
23
 
15
24
  // $create(tag, attrs): attrs are set as attributes, except className, which
16
25
  // may be a string or an array of class names.
@@ -56,13 +65,21 @@ export function isSafeUrl(value: string): boolean {
56
65
  }
57
66
  }
58
67
 
59
- function sanitizeNode(node: HTMLElement): HTMLElement | null {
60
- // Nodo de texto: siempre seguro, se deja tal cual
61
- if (node.nodeType === Node.TEXT_NODE) return node;
62
-
63
- // Comentarios y cualquier otra cosa: fuera
64
- if (node.nodeType !== Node.ELEMENT_NODE) return null;
68
+ // copia los hijos de `source` en `target`, saneando los elementos y clonando
69
+ // el texto; cualquier otra cosa (comentarios, etc.) se descarta
70
+ function appendSanitizedChildren(source: ParentNode, target: HTMLElement): void {
71
+ for (const child of Array.from(source.childNodes)) {
72
+ if (child.nodeType === Node.ELEMENT_NODE) {
73
+ const sanitizedChild = sanitizeNode(child as HTMLElement);
74
+ if (sanitizedChild) target.appendChild(sanitizedChild);
75
+ } else if (child.nodeType === Node.TEXT_NODE) {
76
+ target.appendChild(child.cloneNode());
77
+ }
78
+ }
79
+ }
65
80
 
81
+ // sanea un elemento (los llamadores solo pasan nodos ELEMENT_NODE)
82
+ function sanitizeNode(node: HTMLElement): HTMLElement | null {
66
83
  const tag = node.tagName.toLowerCase();
67
84
  if (!ALLOWED_TAGS.has(tag)) return null; // tag no permitido → se descarta el nodo entero
68
85
 
@@ -79,20 +96,10 @@ function sanitizeNode(node: HTMLElement): HTMLElement | null {
79
96
  clean.setAttribute(name, attr.value);
80
97
  }
81
98
 
82
- // fuerza rel seguro en enlaces
83
- if (tag === 'a') {
84
- clean.setAttribute('rel', 'noopener noreferrer');
85
- if (clean.hasAttribute('target')) clean.removeAttribute('target');
86
- }
99
+ // fuerza rel seguro en enlaces (target nunca se copia: no está permitido)
100
+ if (tag === 'a') clean.setAttribute('rel', 'noopener noreferrer');
87
101
 
88
- for (const child of Array.from(node.childNodes)) {
89
- if (child.nodeType === Node.ELEMENT_NODE) {
90
- const sanitizedChild = sanitizeNode(child as HTMLElement);
91
- if (sanitizedChild) clean.appendChild(sanitizedChild);
92
- } else if (child.nodeType === Node.TEXT_NODE) {
93
- clean.appendChild(child.cloneNode());
94
- }
95
- }
102
+ appendSanitizedChildren(node, clean);
96
103
 
97
104
  return clean;
98
105
  }
@@ -101,14 +108,7 @@ export function sanitizeHTML(html: string): string {
101
108
  const doc = new DOMParser().parseFromString(html, 'text/html');
102
109
  const container = document.createElement('div');
103
110
 
104
- for (const child of Array.from(doc.body.childNodes)) {
105
- if (child.nodeType === Node.ELEMENT_NODE) {
106
- const sanitizedChild = sanitizeNode(child as HTMLElement);
107
- if (sanitizedChild) container.appendChild(sanitizedChild);
108
- } else if (child.nodeType === Node.TEXT_NODE) {
109
- container.appendChild(child.cloneNode());
110
- }
111
- }
111
+ appendSanitizedChildren(doc.body, container);
112
112
 
113
113
  return container.innerHTML;
114
114
  }
package/src/jq79.ts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { $, $$, $create } from "./dom"
2
+ import { $, $$, $create, sanitizeHTML } from "./dom"
3
3
  import { $reactive, untracked, createEffectScope } from "./reactive"
4
4
  import type { ReactiveDeepData, EffectScope } from "./reactive"
5
5
  import { transformSetupScript, transformFactoryScript } from "./transform"
@@ -56,7 +56,7 @@ const interpolate = (template: string, scope: Record<string, any>): string =>
56
56
  template.replace(/{{\s*(.+?)\s*}}/g, (_, expr) => evalExpr(expr, scope) ?? "")
57
57
 
58
58
 
59
- const CONTROL_ATTRS = new Set([":attrs", ":if", ":elseif", ":else", ":each", ":key", ":with"])
59
+ const CONTROL_ATTRS = new Set([":attrs", ":if", ":elseif", ":else", ":each", ":key", ":with", ":text", ":html"])
60
60
  const EACH_PATTERN = /^\s*(\w+)\s+in\s+(.+)$/
61
61
 
62
62
  type ConditionalBranch = { expr?: string; node: TemplateNode }
@@ -202,10 +202,11 @@ const createWithScope = (expr: string, scope: Record<string, any>): Record<strin
202
202
  }
203
203
 
204
204
  // renders a single element node: static attrs, @event listeners, a reactive
205
- // :attrs object, and its (reactive) children. :if/:elseif/:else/:each are
206
- // handled by renderNodes, which decides *whether*/*how many times* a node is
207
- // rendered before calling this. Tags matching a PascalCase scope variable
208
- // render as nested components instead
205
+ // :attrs object, and its content - :text/:html override the element's own
206
+ // children with a reactive textContent/innerHTML, otherwise children render
207
+ // normally. :if/:elseif/:else/:each are handled by renderNodes, which decides
208
+ // *whether*/*how many times* a node is rendered before calling this. Tags
209
+ // matching a PascalCase scope variable render as nested components instead
209
210
  const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: EffectScope): Node => {
210
211
  // :with applies to the element's own bindings (@events, :attrs) and its
211
212
  // whole subtree. On a :each element the item scope is already in place, so
@@ -254,7 +255,19 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
254
255
  })
255
256
  }
256
257
 
257
- el.appendChild(renderNodes(node.children, scope, fx))
258
+ // :text="expr" sets textContent reactively, replacing any children.
259
+ // :html="expr" sets innerHTML reactively, sanitizing the value first so
260
+ // untrusted content can't inject scripts/attributes (see sanitizeHTML in
261
+ // ./dom). Both skip rendering the element's own children/interpolation
262
+ const textExpr = node.attrs[":text"]
263
+ const htmlExpr = node.attrs[":html"]
264
+ if (textExpr !== undefined) {
265
+ fx.effect(() => { el.textContent = String(evalExpr(textExpr, scope) ?? "") })
266
+ } else if (htmlExpr !== undefined) {
267
+ fx.effect(() => { el.innerHTML = sanitizeHTML(String(evalExpr(htmlExpr, scope) ?? "")) })
268
+ } else {
269
+ el.appendChild(renderNodes(node.children, scope, fx))
270
+ }
258
271
 
259
272
  return el
260
273
  }
@@ -588,9 +601,15 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
588
601
  const merge = (bindings: any) => {
589
602
  if (bindings && typeof bindings === "object") Object.assign(scope, bindings)
590
603
  }
591
- const returned = factory({ $data: scope, $effect: effect, ...instanceHelpers })
592
- if (returned instanceof Promise) returned.then(merge).catch(logError)
593
- else merge(returned)
604
+ // the sync path is invoked straight from render(), so a throwing factory
605
+ // must be caught here too - not just by the `result` rejection handler
606
+ try {
607
+ const returned = factory({ $data: scope, $effect: effect, ...instanceHelpers })
608
+ if (returned instanceof Promise) returned.then(merge).catch(logError)
609
+ else merge(returned)
610
+ } catch (error) {
611
+ logError(error)
612
+ }
594
613
  }
595
614
 
596
615
  result.then(invoke, logError)
package/src/vite.ts CHANGED
@@ -37,7 +37,6 @@ const IMPORT_LITERAL_RE = /(?<![\w$.])import\s*\(\s*(["'])([^"'\n]+?)\1\s*\)/g
37
37
  const STATIC_IMPORT_LITERAL_RE = /(?<![\w$.])import\s*(?:[\w$\s,{}*]+?\s*from\s*)?(["'])([^"'\n]+)\1/g
38
38
 
39
39
  const isHtmlUrl = (spec: string) => /\.html?([?#]|$)/.test(spec)
40
- const isRelative = (spec: string) => spec.startsWith("./") || spec.startsWith("../")
41
40
  const isExternalUrl = (spec: string) => /^[a-z][a-z0-9+.-]*:/i.test(spec) || spec.startsWith("/")
42
41
 
43
42
  // literal import specifiers in the component's script blocks - dynamic