@zoijs/core 1.4.0 → 1.6.0
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/CHANGELOG.md +31 -0
- package/README.md +23 -4
- package/package.json +5 -1
- package/src/core/html.js +24 -5
- package/src/core/mount.js +10 -3
- package/src/core/renderer.js +28 -8
- package/src/index.d.ts +16 -1
- package/src/server.d.ts +53 -0
- package/src/server.js +42 -0
- package/src/utils/security.js +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ All notable changes to Zoijs are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/), and Zoijs follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/) (see `VERSIONING.md`).
|
|
6
6
|
|
|
7
|
+
## [1.6.0] — 2026-06-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Hydration — `mount(component, target, { hydrate: true })`.** The client now
|
|
11
|
+
**adopts** server-rendered DOM in place instead of re-creating it: with `hydrate`,
|
|
12
|
+
`mount` reuses the existing elements inside `target` exactly — never cloning or
|
|
13
|
+
replacing them — and attaches event handlers and reactive attribute bindings to
|
|
14
|
+
those live nodes. Each dynamic child slot's server content is cleared (back to the
|
|
15
|
+
`<!--zoijs:[-->` start marker that [`@zoijs/ssr`](https://www.npmjs.com/package/@zoijs/ssr)
|
|
16
|
+
emits) and re-rendered in place; since the values match the server, there is no
|
|
17
|
+
visible change and no flash. This is the client half of full SSR — pair it with
|
|
18
|
+
`renderToString(component, { hydratable: true })`. The default `mount` path is
|
|
19
|
+
byte-for-byte unchanged (the option is additive), and the nine-function main surface
|
|
20
|
+
is the same. `@zoijs/ssr` re-exports this as `hydrate()`. See
|
|
21
|
+
[RFC 0008](docs/rfcs/0008-ssr.md).
|
|
22
|
+
|
|
23
|
+
## [1.5.0] — 2026-06-26
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **DOM-free template compiler + a `@zoijs/core/server` subpath.** `html\`…\`` now
|
|
27
|
+
compiles to a static HTML string + part descriptors **without touching the DOM**;
|
|
28
|
+
the `<template>` element is built lazily on first client render. This means a
|
|
29
|
+
component can be evaluated on a server (no DOM) so [`@zoijs/ssr`](https://www.npmjs.com/package/@zoijs/ssr)
|
|
30
|
+
can render it to a string. The new subpath exposes the building blocks a server
|
|
31
|
+
renderer needs — the static HTML/parts of a result, template/list markers, and the
|
|
32
|
+
**same** security predicates the client uses (`escapeText`, `escapeAttr`,
|
|
33
|
+
`isSafeUrl`, `isSafeAttributeName`, `URL_ATTRS`) so server and client make
|
|
34
|
+
identical safety decisions. Client rendering is byte-for-byte unchanged; the
|
|
35
|
+
learnable nine-function main surface is unchanged. See
|
|
36
|
+
[RFC 0008](docs/rfcs/0008-ssr.md).
|
|
37
|
+
|
|
7
38
|
## [1.4.0] — 2026-06-26
|
|
8
39
|
|
|
9
40
|
### Added
|
package/README.md
CHANGED
|
@@ -97,19 +97,36 @@ Relies on baseline-modern platform APIs: ES modules, `<template>`, `TreeWalker`,
|
|
|
97
97
|
|
|
98
98
|
## Public API
|
|
99
99
|
|
|
100
|
+
The whole framework is **nine functions** — learnable in one sitting and frozen for 1.x:
|
|
101
|
+
|
|
100
102
|
```js
|
|
101
|
-
import {
|
|
103
|
+
import {
|
|
104
|
+
html, mount, each, boundary,
|
|
105
|
+
createState, computed, effect,
|
|
106
|
+
configure, onCleanup,
|
|
107
|
+
} from "@zoijs/core";
|
|
102
108
|
```
|
|
103
109
|
|
|
104
110
|
- `html` — tagged template; parsed once, cached.
|
|
105
111
|
- `mount(component, target)` — render a component; returns `unmount()`.
|
|
112
|
+
- `each(itemsFn, keyFn, renderFn)` — keyed list rendering (reuse / move / remove nodes).
|
|
113
|
+
- `boundary(child, fallback)` — render-time error boundary: if `child` throws while building its markup, dispose the partial work and render `fallback`.
|
|
106
114
|
- `createState(value)` — a reactive value (`get` / `set` / `peek`).
|
|
107
115
|
- `computed(fn)` — a lazy, cached, **value-gated** derived value (`get` / `peek`).
|
|
108
|
-
- `
|
|
116
|
+
- `effect(fn)` — a side effect that re-runs when a value it reads changes; returns `{ dispose }` and may return a per-run cleanup.
|
|
109
117
|
- `configure({ dev })` — toggle development warnings (default `dev: true`).
|
|
110
118
|
- `onCleanup(fn)` — register teardown for a component or list item (timers, subscriptions).
|
|
111
119
|
|
|
112
|
-
|
|
120
|
+
Plus the **`ref`** binding (`html\`<input ref=${(el) => el.focus()} />\``) — no export; it's a template
|
|
121
|
+
attribute that hands you the rendered element.
|
|
122
|
+
|
|
123
|
+
**Devtools (dev-only).** A separate subpath, `@zoijs/core/devtools`, exposes a read-only inspection
|
|
124
|
+
hook — `attachInspector(inspector)` and `inspecting()` — that [`@zoijs/devtools`](https://www.npmjs.com/package/@zoijs/devtools)
|
|
125
|
+
(or a browser extension) uses to observe the reactive graph. It's off by default, never instruments
|
|
126
|
+
the hot read path, and is a no-op under `configure({ dev: false })`, so it costs production nothing and
|
|
127
|
+
leaves the nine-function main surface unchanged.
|
|
128
|
+
|
|
129
|
+
See the [Documentation site](https://zoijs.dev) for the full guide, tutorials, and API reference.
|
|
113
130
|
|
|
114
131
|
**TypeScript:** ships type definitions ([`src/index.d.ts`](src/index.d.ts)) for autocomplete and optional type-checking — JS-first, no build step required. `createState<T>`, `computed<T>`, and `each<T>` are generic. Type-check with `npm run test:types`.
|
|
115
132
|
|
|
@@ -118,7 +135,9 @@ See the [Documentation site](docs/README.md) for the full guide, tutorials, and
|
|
|
118
135
|
- Fine-grained text/attribute bindings — `${() => state.get()}` updates one node in place; setup runs once (no re-render).
|
|
119
136
|
- Native events, secure-by-default rendering (inert text, URL-scheme guards, no `eval`).
|
|
120
137
|
- `computed()` derived values — lazy, cached, nestable, and **value-gated** (unchanged results don't wake downstream).
|
|
121
|
-
- `
|
|
138
|
+
- `effect()` side effects — re-run on change, with owner-scoped auto-disposal and a per-run cleanup.
|
|
139
|
+
- `boundary()` render-time error boundary — a failing subtree shows a fallback instead of breaking `mount`.
|
|
140
|
+
- `each()` keyed list reconciliation — minimal DOM moves; preserves focus / input / scroll on reorder.
|
|
122
141
|
- Microtask batching, push-pull dependency tracking, **owner-scoped cleanup** (unmount and removed items dispose their subscriptions).
|
|
123
142
|
- Production mode via `configure({ dev: false })` — no build step.
|
|
124
143
|
- Safety: self-triggering effects are warned + stopped; a throwing binding doesn't break others.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoijs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Zoijs — a beginner-friendly, no-build, no-Virtual-DOM frontend framework. Plain HTML, CSS, and JavaScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"./devtools": {
|
|
14
14
|
"types": "./src/devtools.d.ts",
|
|
15
15
|
"default": "./src/reactivity/devtools.js"
|
|
16
|
+
},
|
|
17
|
+
"./server": {
|
|
18
|
+
"types": "./src/server.d.ts",
|
|
19
|
+
"default": "./src/server.js"
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"files": [
|
package/src/core/html.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// attribute value, an event, etc. Dynamic values are kept in a separate channel
|
|
8
8
|
// from the static HTML; a value can never change the template's structure.
|
|
9
9
|
//
|
|
10
|
-
// Output of the parse (cached per `strings`)
|
|
11
|
-
//
|
|
10
|
+
// Output of the parse (cached per `strings`) is DOM-free — a static HTML string
|
|
11
|
+
// plus part descriptors. The <template> element is built lazily on first client
|
|
12
|
+
// render; on a server, @zoijs/ssr reads the static HTML + parts directly.
|
|
13
|
+
// html : the static HTML string, with unique markers (no DOM)
|
|
12
14
|
// parts : ordered list of binding descriptors
|
|
13
15
|
// { type: "child" } ← a comment-marker anchor
|
|
14
16
|
// { type: "element", attrs: [AttrPart] } ← element carrying data-zoijs-bind
|
|
@@ -29,10 +31,24 @@ const cache = new WeakMap();
|
|
|
29
31
|
export function html(strings, ...values) {
|
|
30
32
|
let compiled = cache.get(strings);
|
|
31
33
|
if (!compiled) {
|
|
32
|
-
compiled = compile(strings);
|
|
34
|
+
compiled = compile(strings); // pure: a static HTML string + parts, NO DOM
|
|
33
35
|
cache.set(strings, compiled);
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
// The <template> element is created lazily, on first client render — so html()
|
|
38
|
+
// itself touches no DOM and works on a server (where @zoijs/ssr reads the static
|
|
39
|
+
// HTML + parts instead). The brand lets consumers detect a result without
|
|
40
|
+
// tripping the getter.
|
|
41
|
+
return {
|
|
42
|
+
__zoijsTemplate: true,
|
|
43
|
+
parts: compiled.parts,
|
|
44
|
+
hasElements: compiled.hasElements,
|
|
45
|
+
__staticHTML: compiled.html,
|
|
46
|
+
get template() {
|
|
47
|
+
if (!compiled.template) compiled.template = createTemplate(compiled.html);
|
|
48
|
+
return compiled.template;
|
|
49
|
+
},
|
|
50
|
+
values,
|
|
51
|
+
};
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
// ---- scanner ----------------------------------------------------------------
|
|
@@ -239,5 +255,8 @@ function compile(strings) {
|
|
|
239
255
|
}
|
|
240
256
|
|
|
241
257
|
const hasElements = parts.some((p) => p.type === "element");
|
|
242
|
-
|
|
258
|
+
// Pure output: the static HTML string and the part descriptors. The DOM
|
|
259
|
+
// <template> is built later, lazily (see html()), so compile() needs no DOM and
|
|
260
|
+
// its result can be serialized on a server.
|
|
261
|
+
return { html: out, parts, hasElements, template: null };
|
|
243
262
|
}
|
package/src/core/mount.js
CHANGED
|
@@ -11,18 +11,25 @@ import { createOwner, runWithOwner, disposeOwner } from "../reactivity/owner.js"
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {Function|object} component a component function, or an html() result
|
|
13
13
|
* @param {Element|string} target a DOM element or a CSS selector
|
|
14
|
+
* @param {{ hydrate?: boolean }} [options] with `hydrate: true`, adopt the
|
|
15
|
+
* server-rendered DOM already inside `target` instead of replacing it — the
|
|
16
|
+
* elements/attributes/events are reused in place (used by @zoijs/ssr's
|
|
17
|
+
* `hydrate()`). The returned unmount() disposes everything either way.
|
|
14
18
|
* @returns {Function} unmount
|
|
15
19
|
*/
|
|
16
|
-
export function mount(component, target) {
|
|
20
|
+
export function mount(component, target, options) {
|
|
17
21
|
const el = resolveTarget(target);
|
|
18
22
|
const owner = createOwner();
|
|
23
|
+
const hydrate = !!(options && options.hydrate);
|
|
19
24
|
|
|
20
25
|
let node;
|
|
21
26
|
runWithOwner(owner, () => {
|
|
22
27
|
const result = typeof component === "function" ? component() : component;
|
|
23
|
-
|
|
28
|
+
// Hydration binds to el's existing children in place; a fresh mount builds a
|
|
29
|
+
// detached fragment we then swap in.
|
|
30
|
+
node = render(result, hydrate ? el : undefined).node;
|
|
24
31
|
});
|
|
25
|
-
el.replaceChildren(node);
|
|
32
|
+
if (!hydrate) el.replaceChildren(node);
|
|
26
33
|
|
|
27
34
|
return () => {
|
|
28
35
|
disposeOwner(owner);
|
package/src/core/renderer.js
CHANGED
|
@@ -17,23 +17,28 @@ import { labelNext } from "../reactivity/devtools.js";
|
|
|
17
17
|
import { createState } from "../reactivity/state.js";
|
|
18
18
|
import { createOwner, runWithOwner, disposeOwner, onCleanup } from "../reactivity/owner.js";
|
|
19
19
|
import { isDev } from "../reactivity/env.js";
|
|
20
|
-
import { toText, isSafeUrl, isSafeAttributeName } from "../utils/security.js";
|
|
20
|
+
import { toText, isSafeUrl, isSafeAttributeName, URL_ATTRS } from "../utils/security.js";
|
|
21
21
|
|
|
22
22
|
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
23
|
-
const URL_ATTRS = new Set(["href", "src", "action", "formaction", "poster", "ping", "xlink:href"]);
|
|
24
23
|
const noop = () => {};
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* @param {{ template: HTMLTemplateElement, parts: object[], values: any[] }} result
|
|
28
|
-
* @
|
|
27
|
+
* @param {Element} [hydrateRoot] when given, bind to this EXISTING server-rendered
|
|
28
|
+
* subtree in place (no clone) instead of building fresh DOM — see hydration below.
|
|
29
|
+
* @returns {{ node: Node, dispose: Function }}
|
|
29
30
|
*/
|
|
30
|
-
export function render(result) {
|
|
31
|
+
export function render(result, hydrateRoot) {
|
|
31
32
|
const owner = createOwner(); // nested under the active owner
|
|
32
|
-
|
|
33
|
+
// Hydration adopts the server DOM in place — elements, attributes, and events are
|
|
34
|
+
// reused, never re-created; each dynamic slot is cleared + re-rendered (same
|
|
35
|
+
// values → no visible change). Otherwise build a fresh clone.
|
|
36
|
+
const hydrating = hydrateRoot != null;
|
|
37
|
+
const fragment = hydrating ? hydrateRoot : result.template.content.cloneNode(true);
|
|
33
38
|
const { parts, values } = result;
|
|
34
39
|
|
|
35
|
-
// Collect every part's target node
|
|
36
|
-
//
|
|
40
|
+
// Collect every part's target node first (document order), so a binding that
|
|
41
|
+
// inserts children can't shadow a later part.
|
|
37
42
|
const nodes = collectNodes(fragment, parts, result.hasElements);
|
|
38
43
|
|
|
39
44
|
runWithOwner(owner, () => {
|
|
@@ -43,6 +48,7 @@ export function render(result) {
|
|
|
43
48
|
if (!node) continue;
|
|
44
49
|
|
|
45
50
|
if (part.type === "child") {
|
|
51
|
+
if (hydrating) clearHydratedSlot(node); // drop this slot's server content
|
|
46
52
|
bindChild(node, values[part.hole]);
|
|
47
53
|
} else {
|
|
48
54
|
node.removeAttribute("data-zoijs-bind");
|
|
@@ -54,6 +60,20 @@ export function render(result) {
|
|
|
54
60
|
return { node: fragment, dispose: () => disposeOwner(owner) };
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
// Remove a hydrated slot's server content: the nodes before its anchor, back
|
|
64
|
+
// through the slot's `<!--zoijs:[-->` start marker (emitted by @zoijs/ssr). Static
|
|
65
|
+
// text outside the slot is preserved; the normal binding then renders fresh content
|
|
66
|
+
// in the same place. No start marker → leave the DOM as-is (not a hydratable slot).
|
|
67
|
+
const isSlotStart = (n) => n && n.nodeType === 8 && n.data === "zoijs:[";
|
|
68
|
+
function clearHydratedSlot(anchor) {
|
|
69
|
+
let start = anchor.previousSibling;
|
|
70
|
+
while (start && !isSlotStart(start)) start = start.previousSibling;
|
|
71
|
+
if (!start) return;
|
|
72
|
+
let n;
|
|
73
|
+
while ((n = anchor.previousSibling) !== start) n.remove();
|
|
74
|
+
start.remove();
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
// Match parts to nodes by document order: a child part ↔ the next marker comment,
|
|
58
78
|
// an element part ↔ the next element carrying data-zoijs-bind. Unique markers make
|
|
59
79
|
// this collision-proof across nested templates and list items.
|
|
@@ -469,5 +489,5 @@ function applyAttribute(el, name, value) {
|
|
|
469
489
|
// ---- helpers -----------------------------------------------------------------
|
|
470
490
|
|
|
471
491
|
function isHtmlResult(v) {
|
|
472
|
-
return v &&
|
|
492
|
+
return v && v.__zoijsTemplate === true;
|
|
473
493
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -66,11 +66,26 @@ export type Ref<E extends Element = Element> = (element: E) => void | (() => voi
|
|
|
66
66
|
*/
|
|
67
67
|
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
|
|
68
68
|
|
|
69
|
+
/** Options for {@link mount}. */
|
|
70
|
+
export interface MountOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Adopt the server-rendered DOM already inside `target` (from `@zoijs/ssr`'s
|
|
73
|
+
* `renderToString(..., { hydratable: true })`) instead of replacing it: elements,
|
|
74
|
+
* attributes, and events are reused in place. Used by `@zoijs/ssr`'s `hydrate()`.
|
|
75
|
+
*/
|
|
76
|
+
hydrate?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
80
|
* Render a component (or a template) into a DOM element or CSS selector.
|
|
71
81
|
* Returns an `unmount()` that detaches the DOM and disposes all reactivity.
|
|
82
|
+
* With `{ hydrate: true }`, binds to the existing server-rendered DOM in place.
|
|
72
83
|
*/
|
|
73
|
-
export function mount(
|
|
84
|
+
export function mount(
|
|
85
|
+
component: Component | TemplateResult,
|
|
86
|
+
target: Element | string,
|
|
87
|
+
options?: MountOptions
|
|
88
|
+
): () => void;
|
|
74
89
|
|
|
75
90
|
/**
|
|
76
91
|
* Create a reactive value.
|
package/src/server.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Type surface for `@zoijs/core/server` — DOM-free building blocks for server
|
|
2
|
+
// rendering (@zoijs/ssr). Server tooling, not part of the stable nine-function API.
|
|
3
|
+
|
|
4
|
+
/** Coerce a value to a string (null/undefined → ""). */
|
|
5
|
+
export function toText(value: unknown): string;
|
|
6
|
+
/** Escape a value for insertion as HTML text content. */
|
|
7
|
+
export function escapeText(value: unknown): string;
|
|
8
|
+
/** Escape a value for inside a double-quoted attribute value. */
|
|
9
|
+
export function escapeAttr(value: unknown): string;
|
|
10
|
+
/** Is this URL safe for a URL-bearing attribute (scheme allowlist)? */
|
|
11
|
+
export function isSafeUrl(url: string): boolean;
|
|
12
|
+
/** Is this attribute name allowed to be bound from data (blocks on*, srcdoc)? */
|
|
13
|
+
export function isSafeAttributeName(name: string): boolean;
|
|
14
|
+
/** Attribute names whose values carry a URL (scheme-checked). */
|
|
15
|
+
export const URL_ATTRS: ReadonlySet<string>;
|
|
16
|
+
|
|
17
|
+
/** A part descriptor: a child slot, or a dynamic element with attribute parts. */
|
|
18
|
+
export type Part =
|
|
19
|
+
| { type: "child"; hole: number }
|
|
20
|
+
| { type: "element"; attrs: AttrPart[] };
|
|
21
|
+
|
|
22
|
+
/** A dynamic attribute descriptor. */
|
|
23
|
+
export interface AttrPart {
|
|
24
|
+
name: string;
|
|
25
|
+
strings: string[];
|
|
26
|
+
holes: number[];
|
|
27
|
+
event: boolean;
|
|
28
|
+
whole: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An `html\`…\`` result, viewed without its DOM template. */
|
|
32
|
+
export interface TemplateResult {
|
|
33
|
+
__zoijsTemplate: true;
|
|
34
|
+
__staticHTML: string;
|
|
35
|
+
parts: Part[];
|
|
36
|
+
values: unknown[];
|
|
37
|
+
hasElements: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** An `each(...)` list marker. */
|
|
41
|
+
export interface EachMarker {
|
|
42
|
+
__zoijsEach: true;
|
|
43
|
+
items: unknown;
|
|
44
|
+
keyFn: (item: unknown) => unknown;
|
|
45
|
+
renderFn: (item: unknown) => unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True for an `html\`…\`` result (does not build the DOM template). */
|
|
49
|
+
export function isTemplateResult(value: unknown): value is TemplateResult;
|
|
50
|
+
/** True for an `each(...)` list marker. */
|
|
51
|
+
export function isEachMarker(value: unknown): value is EachMarker;
|
|
52
|
+
/** The static HTML skeleton of a template result. */
|
|
53
|
+
export function templateHTML(result: TemplateResult): string;
|
package/src/server.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// server.js — DOM-free building blocks for server rendering (@zoijs/ssr).
|
|
2
|
+
//
|
|
3
|
+
// Reached via the subpath `@zoijs/core/server`. It exposes exactly what a string
|
|
4
|
+
// renderer needs to walk an html() result and emit safe HTML WITHOUT a DOM:
|
|
5
|
+
//
|
|
6
|
+
// - the static HTML skeleton + part descriptors of a template result,
|
|
7
|
+
// - markers for the two value kinds (a template result, an each() list),
|
|
8
|
+
// - and the SAME security predicates the client renderer uses, so server output
|
|
9
|
+
// and client output make identical safety decisions (escaping, URL schemes,
|
|
10
|
+
// unsafe attribute names).
|
|
11
|
+
//
|
|
12
|
+
// Nothing here touches the DOM, and the main nine-function surface is unchanged —
|
|
13
|
+
// this is server tooling behind its own subpath, like `@zoijs/core/devtools`.
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
toText,
|
|
17
|
+
escapeText,
|
|
18
|
+
escapeAttr,
|
|
19
|
+
isSafeUrl,
|
|
20
|
+
isSafeAttributeName,
|
|
21
|
+
URL_ATTRS,
|
|
22
|
+
} from "./utils/security.js";
|
|
23
|
+
|
|
24
|
+
/** True for an `html\`…\`` result (brand check — does not build the DOM template). */
|
|
25
|
+
export function isTemplateResult(value) {
|
|
26
|
+
return !!(value && value.__zoijsTemplate === true);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True for an `each(...)` list marker. */
|
|
30
|
+
export function isEachMarker(value) {
|
|
31
|
+
return !!(value && value.__zoijsEach === true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The static HTML skeleton of a template result — the author's HTML with unique
|
|
36
|
+
* markers (`<!--zoijs-->` for a child slot, ` data-zoijs-bind` on a dynamic
|
|
37
|
+
* element). A server renderer injects values at those markers. The result's
|
|
38
|
+
* `parts` (document-ordered descriptors) and `values` are read directly.
|
|
39
|
+
*/
|
|
40
|
+
export function templateHTML(result) {
|
|
41
|
+
return result.__staticHTML;
|
|
42
|
+
}
|
package/src/utils/security.js
CHANGED
|
@@ -14,6 +14,27 @@ export function toText(value) {
|
|
|
14
14
|
return value === null || value === undefined ? "" : String(value);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// HTML-escaping for SERVER string rendering (@zoijs/ssr). The client renderer
|
|
18
|
+
// never needs these — it writes text via Text nodes and values via setAttribute,
|
|
19
|
+
// both of which are inert/escaped by the platform. On a server there is no DOM, so
|
|
20
|
+
// the same safety must be applied by escaping into the HTML string. Kept here so
|
|
21
|
+
// escaping lives in one place alongside the other security predicates.
|
|
22
|
+
|
|
23
|
+
/** Escape a value for insertion as HTML TEXT content. */
|
|
24
|
+
export function escapeText(value) {
|
|
25
|
+
return toText(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Escape a value for insertion inside a double-quoted attribute value. */
|
|
29
|
+
export function escapeAttr(value) {
|
|
30
|
+
return toText(value).replace(/&/g, "&").replace(/"/g, """);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Attribute names that carry a URL — their values are scheme-checked (isSafeUrl).
|
|
34
|
+
// Shared by the client renderer (DOM) and @zoijs/ssr (string) so both make the
|
|
35
|
+
// exact same safety decision.
|
|
36
|
+
export const URL_ATTRS = new Set(["href", "src", "action", "formaction", "poster", "ping", "xlink:href"]);
|
|
37
|
+
|
|
17
38
|
// Allowlisted URL schemes for URL-bearing attributes (href, src, ...).
|
|
18
39
|
const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
19
40
|
// data: is allowed only for raster image MIME types (never text/html, never SVG,
|