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/README.md +1 -1
- package/dist/dom.d.ts +4 -2
- package/dist/jq79.cjs +9 -9
- package/dist/jq79.cjs.map +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/dist/vite.cjs.map +1 -1
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/src/dom.ts +35 -35
- package/src/jq79.ts +29 -10
- package/src/vite.ts +0 -1
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
|
-
|
|
5
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|