@jarenjs/view 0.34.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.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @file Shared SVG vnode builders over the view `h()` constructor — the
3
+ * single home for the small helper set that `@jarenjs/calc` and
4
+ * `@jarenjs/mermaid` (and any future SVG-emitting component) draw from.
5
+ *
6
+ * Every helper returns a tagged-array vnode; nothing here touches the DOM.
7
+ * The root is `['svg', {viewBox,…}, …]` so the view patcher namespaces the
8
+ * whole subtree (VIEW-FORMAT §5.4) and the serializer escapes text. These
9
+ * builders are geometry-defensive: coordinates flow through `num` so a
10
+ * NaN/±Inf never reaches the emitted string.
11
+ */
12
+ /**
13
+ * Clamp a value to a finite number (defends the geometry against
14
+ * NaN/±Inf).
15
+ * @param {number} n
16
+ * @param {number} [fallback]
17
+ * @returns {number}
18
+ */
19
+ export declare function num(n: number, fallback?: number): number;
20
+ /**
21
+ * Quantize a coordinate to two decimals — the precision SVG geometry is
22
+ * emitted at, which keeps path data short and makes repeated renders of the
23
+ * same shape produce identical strings. Non-finite input passes straight
24
+ * through, so pair this with `num` wherever the value reaches an attribute.
25
+ * @param {number} v
26
+ * @returns {number}
27
+ */
28
+ export declare function coord(v: number): number;
29
+ /**
30
+ * The point at distance `r` from `(cx, cy)` along `angle` (radians,
31
+ * 0 = east, y growing downward — SVG screen orientation). Raw
32
+ * coordinates: quantization (`coord`/`num`) stays at the call site so a
33
+ * caller's emitted bytes are its own choice.
34
+ * @param {number} cx
35
+ * @param {number} cy
36
+ * @param {number} r
37
+ * @param {number} angle
38
+ * @returns {{ x: number, y: number }}
39
+ */
40
+ export declare function polarPoint(cx: number, cy: number, r: number, angle: number): {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ /**
45
+ * The `text-anchor` for a label placed radially outward at `angle`
46
+ * (radians, 0 = east, y growing downward). Labels near the left or right
47
+ * of the circle read away from it (`end`/`start`); the deadband around
48
+ * ±90° keeps top and bottom labels centred rather than flipping anchor on
49
+ * a hair's difference in angle.
50
+ * @param {number} angle
51
+ * @returns {'start'|'end'|'middle'}
52
+ */
53
+ export declare function anchorForAngle(angle: number): 'start' | 'end' | 'middle';
54
+ /**
55
+ * The root `<svg>`. The caller supplies the `class` string; theme tokens
56
+ * become CSS custom properties on the element (via `theme.cssVars`) so the
57
+ * stylesheet can re-theme without a re-render, and `theme.tokens.fontFamily`
58
+ * is written as the base font.
59
+ * @param {string} className the root element's `class`
60
+ * @param {number} width
61
+ * @param {number} height
62
+ * @param {{ cssVars: Record<string,string>, tokens: Record<string,string> }} theme
63
+ * `max-width: 100%` is written inline so a standalone SVG never overflows its
64
+ * container. It is inline because the theme variables are, and that means it
65
+ * outranks any stylesheet — so a caller whose content must NOT be scaled down
66
+ * (a diagram, where shrinking scales the text below legibility) has to opt out
67
+ * here rather than in CSS.
68
+ * @param {any[]} children
69
+ * @param {string} [key]
70
+ * @param {{ fit?: boolean, fontFamily?: string }} [options] `fit: false` omits
71
+ * the inline `max-width`, leaving the element at its natural size for a
72
+ * scrolling frame; `fontFamily` overrides the theme's base font for a root
73
+ * whose type is part of its meaning (an error box quotes source, so it sets
74
+ * monospace) — it must be written here because the theme variables are
75
+ * inline, and an inline font outranks any stylesheet rule
76
+ * @returns {any}
77
+ */
78
+ export declare function svgRoot(className: string, width: number, height: number, theme: {
79
+ cssVars: Record<string, string>;
80
+ tokens: Record<string, string>;
81
+ }, children: any[], key?: string, options?: {
82
+ fit?: boolean;
83
+ fontFamily?: string;
84
+ }): any;
85
+ /**
86
+ * A `<g>` group.
87
+ * @param {Record<string,any>} props
88
+ * @param {any[]} children
89
+ * @returns {any}
90
+ */
91
+ export declare function group(props: Record<string, any>, children: any[]): any;
92
+ /**
93
+ * A `<rect>` (optionally rounded via props).
94
+ * @param {number} x @param {number} y @param {number} w @param {number} height
95
+ * @param {Record<string,any>} props
96
+ * @returns {any}
97
+ */
98
+ export declare function rect(x: number, y: number, w: number, height: number, props: Record<string, any>): any;
99
+ /**
100
+ * A `<circle>`.
101
+ * @param {number} cx @param {number} cy @param {number} r
102
+ * @param {Record<string,any>} props
103
+ * @returns {any}
104
+ */
105
+ export declare function circle(cx: number, cy: number, r: number, props: Record<string, any>): any;
106
+ /**
107
+ * A `<line>`.
108
+ * @param {number} x1 @param {number} y1 @param {number} x2 @param {number} y2
109
+ * @param {Record<string,any>} [props]
110
+ * @returns {any}
111
+ */
112
+ export declare function line(x1: number, y1: number, x2: number, y2: number, props?: Record<string, any>): any;
113
+ /**
114
+ * A `<path>` from a `d` string.
115
+ * @param {string} d @param {Record<string,any>} [props]
116
+ * @returns {any}
117
+ */
118
+ export declare function path(d: string, props?: Record<string, any>): any;
119
+ /**
120
+ * A `<polyline>` from `{x,y}` points (open; defaults to no fill).
121
+ * @param {{x:number,y:number}[]} points @param {Record<string,any>} [props]
122
+ * @returns {any}
123
+ */
124
+ export declare function polyline(points: {
125
+ x: number;
126
+ y: number;
127
+ }[], props?: Record<string, any>): any;
128
+ /**
129
+ * A `<polygon>` from `{x,y}` points (closed).
130
+ * @param {{x:number,y:number}[]} points @param {Record<string,any>} [props]
131
+ * @returns {any}
132
+ */
133
+ export declare function polygon(points: {
134
+ x: number;
135
+ y: number;
136
+ }[], props?: Record<string, any>): any;
137
+ /**
138
+ * A single-line `<text>` anchored at (x,y). `str` is coerced with
139
+ * `String()` so numeric labels emit as text.
140
+ * @param {number} x @param {number} y @param {any} str @param {number} fontSize
141
+ * @param {Record<string,any>} [props]
142
+ * @returns {any}
143
+ */
144
+ export declare function textAt(x: number, y: number, str: any, fontSize: number, props?: Record<string, any>): any;
145
+ /**
146
+ * A `<text>` block, one `<tspan>` per line, vertically centered on `cy`.
147
+ * @param {number} cx @param {number} cy
148
+ * @param {string[]} lines
149
+ * @param {number} fontSize
150
+ * @param {Record<string,any>} [props]
151
+ * @returns {any}
152
+ */
153
+ export declare function textLines(cx: number, cy: number, lines: string[], fontSize: number, props?: Record<string, any>): any;
154
+ /**
155
+ * Build an SVG path `d` from a list of `{x,y}` points, breaking the line
156
+ * on `null` entries (discontinuities / out-of-range samples). Returns
157
+ * `''` when there is nothing to draw.
158
+ * @param {Array<{x:number,y:number}|null>} points
159
+ * @returns {string}
160
+ */
161
+ export declare function polylinePath(points: Array<{
162
+ x: number;
163
+ y: number;
164
+ } | null>): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @file Shared theme-resolution mechanics for SVG-emitting components.
3
+ *
4
+ * `resolveTheme` is the prefix-driven kernel behind each component's
5
+ * `createTheme`: it resolves a theme name (or an overrides object) into a
6
+ * flat token table of concrete values *and* a matching set of
7
+ * `--<prefix>-*` CSS custom properties. Components keep their own token
8
+ * tables and pass their CSS-variable prefix; the resolution logic lives
9
+ * here once. Theme tokens are a view concern, so this is view/helpers, not
10
+ * core — but the pure `kebabCase` transform it uses is core's.
11
+ */
12
+ /**
13
+ * Resolve a theme against a component's token tables.
14
+ *
15
+ * `nameOrOverrides` is either a theme name (falling back to `default` when
16
+ * unknown) or an overrides object; an overrides object may name a base via
17
+ * its `theme` property. The returned `cssVars` stamp every token as
18
+ * `--<prefix>-<kebab-case-key>`.
19
+ *
20
+ * An overrides object may also carry a reserved `vars` key: a map of token
21
+ * key → **host** custom-property name (e.g. `{ nodeFill: '--accent-soft' }`).
22
+ * Linked tokens keep their concrete value in `tokens` (so presentation
23
+ * attributes stay standalone-valid) but stamp their cssVar as
24
+ * `var(<host-property>, <concrete>)` — the stamped SVG then follows the
25
+ * host's tokens (light/dark and all) live, with the concrete color as the
26
+ * fallback outside any host. `vars` never leaks into `tokens`.
27
+ *
28
+ * @param {Record<string, Record<string, string>>} themes the component's
29
+ * named token tables (must include a `default`)
30
+ * @param {string} prefix the CSS-variable prefix (e.g. `mm`, `calc`),
31
+ * without the leading `--`
32
+ * The name `'host'` is reserved: it selects the component's own host-var
33
+ * map wholesale, which is how a consumer asks for "theme me from the page"
34
+ * without knowing which tokens are linkable. It resolves to
35
+ * `{ vars: hostVars }`, so a component that passes no `hostVars` simply has
36
+ * no `'host'` theme and falls back to `default` like any other unknown name.
37
+ *
38
+ * @param {string | Record<string, any>} [nameOrOverrides]
39
+ * @param {Record<string, string>} [hostVars] the component's token key →
40
+ * host custom-property map, used when `nameOrOverrides` is `'host'`
41
+ * @returns {{ name: string, tokens: Record<string, string>, cssVars: Record<string, string> }}
42
+ */
43
+ export declare function resolveTheme(themes: Record<string, Record<string, string>>, prefix: string, nameOrOverrides?: string | Record<string, any>, hostVars?: Record<string, string>): {
44
+ name: string;
45
+ tokens: Record<string, string>;
46
+ cssVars: Record<string, string>;
47
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @file URL safety policies for values written into vnode attributes.
3
+ *
4
+ * A URL that reaches an `href`/`src` can execute script (`javascript:`,
5
+ * `vbscript:`) or smuggle a whole document into the page's origin
6
+ * (`data:text/html`), so every producer that turns *authored* text into a
7
+ * vnode must filter it. This module is the one home for that decision.
8
+ *
9
+ * There are deliberately **two** policies, because the safe answer depends
10
+ * on where the URL came from, and picking the wrong one is the trap this
11
+ * file exists to prevent:
12
+ *
13
+ * - {@link sanitizeHref} is an **allow-list**: only the listed schemes and
14
+ * anchor/absolute/dot-relative forms survive. Use it for URLs from a
15
+ * constrained producer that is expected to emit fully-formed links — a
16
+ * Mermaid `click` directive, say. It rejects a bare relative reference
17
+ * like `image.png`, which is correct there and wrong for prose.
18
+ * - {@link sanitizeUrl} is a **deny-list**: it rejects the schemes that can
19
+ * execute or impersonate a document and passes everything else,
20
+ * including scheme-less relative references. Use it for URLs authored in
21
+ * ordinary content — Markdown link destinations and image sources — where
22
+ * `docs/guide.md` and `image.png` must keep working.
23
+ *
24
+ * Both return the trimmed URL or `null`; a `null` means the caller should
25
+ * drop the attribute rather than emit an unsafe one.
26
+ */
27
+ /**
28
+ * Reject `javascript:`/`data:` and other non-http(s) URLs, returning the
29
+ * trimmed URL when it is safe or `null` otherwise. An **allow-list**: a
30
+ * scheme-less relative reference (`image.png`) is rejected too, so reach
31
+ * for {@link sanitizeUrl} when relative references must survive.
32
+ *
33
+ * @param {any} url
34
+ * @returns {string|null}
35
+ */
36
+ export declare function sanitizeHref(url: any): string | null;
37
+ /**
38
+ * Reject only the URL schemes that can execute script or stand in for a
39
+ * document — `javascript:`, `vbscript:`, `file:` and `data:` other than a
40
+ * raster image — and pass everything else through, including scheme-less
41
+ * relative references. The **deny-list** policy for authored content.
42
+ *
43
+ * The scheme is read with {@link schemeOf}, which is deliberately not a
44
+ * literal prefix test — see there for why an embedded tab or NUL does not
45
+ * get a URL past this.
46
+ *
47
+ * @param {any} url
48
+ * @returns {string|null} the trimmed URL, or `null` when it must be dropped
49
+ */
50
+ export declare function sanitizeUrl(url: any): string | null;
51
+ /**
52
+ * Percent-encode a URL for a `href`/`src` attribute.
53
+ *
54
+ * Authored text is not a URL: a Markdown destination may hold spaces,
55
+ * backslashes, backticks or any non-ASCII character, and writing those
56
+ * into an attribute produces a link a browser resolves differently than
57
+ * the author wrote — or not at all. This maps them to their UTF-8
58
+ * percent-encoding while leaving the reserved characters that carry URL
59
+ * *structure* (`/?:@&=+$,#`) alone.
60
+ *
61
+ * An existing `%XX` is passed through rather than re-encoded to `%25XX`,
62
+ * so a destination an author already encoded survives a round trip; a
63
+ * lone `%` that does not begin an octet is encoded. Sanitizing and
64
+ * encoding are separate steps on purpose — encode what
65
+ * {@link sanitizeUrl} returned, never the other way around, or an
66
+ * unsafe scheme could hide behind an escape.
67
+ *
68
+ * @param {string} url a URL that has already passed a sanitizer
69
+ * @returns {string} the attribute-ready URL
70
+ */
71
+ export declare function encodeUrlAttribute(url: string): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @file Server-side rendering: vnode JSON → HTML string.
3
+ *
4
+ * No DOM, no state — a pure fold over the vnode tree, usable anywhere
5
+ * (Node, workers, edge runtimes). `on` bindings and `key`s are renderer
6
+ * instructions and produce no markup; hydration is a client-side
7
+ * re-render into the same container (see docs/VIEW-FORMAT.md §6). A
8
+ * `jaren-widget` vnode serializes its host element around the widget's
9
+ * declarative `ssr` fallback (§7) — still pure, no widget is mounted.
10
+ */
11
+ /**
12
+ * Escape text content: `&`, `<`, `>`.
13
+ * @param {string} text
14
+ * @returns {string}
15
+ */
16
+ export declare function escapeText(text: string): string;
17
+ /**
18
+ * Escape a double-quoted attribute value: `&`, `"`.
19
+ * @param {string} value
20
+ * @returns {string}
21
+ */
22
+ export declare function escapeAttribute(value: string): string;
23
+ export type RenderToStringOptions = {
24
+ /**
25
+ * -
26
+ * Registered widget definitions by name: a `jaren-widget` vnode
27
+ * serializes its host element around the widget's `ssr(props)` vnode
28
+ * when the widget is registered and has one, else empty
29
+ * (VIEW-FORMAT §7).
30
+ */
31
+ widgets?: Record<string, {
32
+ ssr?: (props: any) => any;
33
+ }>;
34
+ /**
35
+ * - Serialize under the SAFE policy
36
+ * ({@link createSafePolicy}): treat the vnode as untrusted. Disallowed
37
+ * tags, scripting-sink and inline `on*` properties, injection-shaped tag
38
+ * and attribute names, and unsafe URLs are stripped, and widget vnodes are
39
+ * dropped. This is the SAME policy {@link createDomRenderer} applies, so a
40
+ * document renders to the same safe markup on the server as on the client.
41
+ */
42
+ safe?: boolean;
43
+ /**
44
+ * - In safe mode, called for everything stripped: a `tag`, a
45
+ * `prop`, an `on` binding (`event`) or a `widget`.
46
+ */
47
+ onUnsafe?: (info: {
48
+ kind: 'tag' | 'prop' | 'event' | 'widget';
49
+ name: string;
50
+ }) => void;
51
+ };
52
+ /**
53
+ * @typedef {Object} RenderToStringOptions
54
+ * @property {Record<string, { ssr?: (props: any) => any }>} [widgets] -
55
+ * Registered widget definitions by name: a `jaren-widget` vnode
56
+ * serializes its host element around the widget's `ssr(props)` vnode
57
+ * when the widget is registered and has one, else empty
58
+ * (VIEW-FORMAT §7).
59
+ * @property {boolean} [safe=false] - Serialize under the SAFE policy
60
+ * ({@link createSafePolicy}): treat the vnode as untrusted. Disallowed
61
+ * tags, scripting-sink and inline `on*` properties, injection-shaped tag
62
+ * and attribute names, and unsafe URLs are stripped, and widget vnodes are
63
+ * dropped. This is the SAME policy {@link createDomRenderer} applies, so a
64
+ * document renders to the same safe markup on the server as on the client.
65
+ * @property {(info: { kind: 'tag' | 'prop' | 'event' | 'widget', name: string }) => void}
66
+ * [onUnsafe] - In safe mode, called for everything stripped: a `tag`, a
67
+ * `prop`, an `on` binding (`event`) or a `widget`.
68
+ */
69
+ /**
70
+ * Render a vnode JSON document to an HTML string.
71
+ *
72
+ * @example
73
+ * renderToString(['p', { class: 'note' }, 'a < b'])
74
+ * // -> '<p class="note">a &lt; b</p>'
75
+ *
76
+ * @param {any} vnode
77
+ * @param {RenderToStringOptions} [options]
78
+ * @returns {string}
79
+ */
80
+ export declare function renderToString(vnode: any, options?: RenderToStringOptions): string;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @file @jarenjs/view — the vnode JSON contract, the keyed DOM patcher
3
+ * and the SSR string renderer. See README.md and docs/VIEW-FORMAT.md.
4
+ */
5
+ export { EMPTY_PROPS, WIDGET_TAG, h, isTextNode, isElementNode, isSkippedNode, isSameNode, isWidgetNode, propsOf, keyOf, childrenOf, } from './vnode.js';
6
+ export { createDomRenderer, styleToString, } from './dom.js';
7
+ export { renderToString, escapeText, escapeAttribute, } from './html.js';
8
+ export { createSafePolicy, } from './safe.js';
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @file The safe render policy: one decision surface for untrusted vnodes,
3
+ * shared by the DOM renderer and the SSR serializer so the two can never
4
+ * disagree about what is safe.
5
+ *
6
+ * **Why this exists.** A vnode is plain JSON, and the published grammar
7
+ * ([`jaren-vnode.schema.json`](../schemas/jaren-vnode.schema.json)) proves a
8
+ * document is a well-formed *interface* — it does not prove the document is
9
+ * *safe to render*. The default (trusted) renderer writes any DOM property a
10
+ * node has, including `innerHTML` and inline `on*` handlers, and the SSR
11
+ * serializer trusts the tag and attribute *names* it is given. For a
12
+ * source-authored view that is exactly right — it is the equivalent of
13
+ * writing the DOM by hand. For a view that arrives from somewhere you do not
14
+ * control — a tenant, a remote service, a language model — it is not: a
15
+ * schema-valid document can still carry `['div', { innerHTML: '<img
16
+ * onerror=…>' }]` or a tag spelled `div><script>`.
17
+ *
18
+ * Schema validation is a structural gate, not a sanitizer. This module is the
19
+ * sanitizer. Pass `{ safe: true }` to {@link createDomRenderer} or
20
+ * {@link renderToString} and every element flows through the decisions here:
21
+ *
22
+ * - **Tags** must be on an allow-list of known-inert HTML and SVG elements.
23
+ * `script`, `iframe`, `object`, `style`, `link`, `foreignObject` and the
24
+ * rest are dropped, and so is any tag whose *name* is not a bare
25
+ * identifier — which is what closes structural injection through the tag
26
+ * (`'div><img src=x onerror=alert(1)'`).
27
+ * - **Property names** must be bare identifiers too (closing attribute-name
28
+ * injection like `'x onfocus'`), must not begin with `on` (no inline
29
+ * handlers), and must not be a known scripting sink (`innerHTML`,
30
+ * `outerHTML`, `srcdoc`, …).
31
+ * - **URL attributes** (`href`, `src`, `action`, …) are filtered through the
32
+ * deny-list {@link sanitizeUrl}, so `javascript:` and a document-carrying
33
+ * `data:` are dropped while ordinary links survive.
34
+ * - **Inline `style`** is dropped when it carries the classic CSS vectors
35
+ * (`expression(`, or a `url()` with a script scheme).
36
+ * - **Event bindings** (`on`) are dropped: an untrusted document must not be
37
+ * able to bind the host's application actions. Safe-mode views are
38
+ * display-oriented by construction.
39
+ *
40
+ * The policy is a set of pure decisions. The DOM renderer and the SSR
41
+ * serializer call the *same* functions, which is what guarantees the client
42
+ * and the server neutralize an attack identically.
43
+ */
44
+ /** A tag or property name that is a bare identifier: a letter, then letters,
45
+ * digits or hyphens. No spaces, `>`, `=`, `/`, colons or quotes — the
46
+ * characters every structural-injection payload needs. */
47
+ declare const RE_SAFE_NAME: RegExp;
48
+ /**
49
+ * The HTML elements a user interface legitimately renders. An allow-list, not
50
+ * a deny-list, because a deny-list silently admits the next dangerous element
51
+ * a browser adds. Document structure (`html`, `head`, `body`, `title`),
52
+ * script and style carriers (`script`, `style`, `link`, `meta`, `base`,
53
+ * `noscript`, `template`), and document-embedding elements (`iframe`,
54
+ * `object`, `embed`, `frame`, `frameset`, `portal`) are all absent by design.
55
+ */
56
+ declare const SAFE_HTML_TAGS: Set<string>;
57
+ /**
58
+ * The SVG drawing subset. `foreignObject` is excluded — it re-enters HTML and
59
+ * would reopen every vector above — and so are `script` and the event-bearing
60
+ * SMIL elements.
61
+ */
62
+ declare const SAFE_SVG_TAGS: Set<string>;
63
+ /** Property names denied outright: the sinks that PARSE their value as HTML.
64
+ * `textContent`/`innerText` are deliberately absent — they set text, which a
65
+ * browser escapes, so they are not injection vectors. Compared
66
+ * case-insensitively. */
67
+ declare const DANGEROUS_PROPS: Set<string>;
68
+ /** Attributes whose value is a single URL, filtered through the deny-list
69
+ * sanitizer. Compared case-insensitively. */
70
+ declare const URL_ATTRS: Set<string>;
71
+ /** Attributes whose value is a URL *list*: `srcset` is comma-separated
72
+ * `url descriptor` candidates, `ping` is a whitespace-separated URL list.
73
+ * Running the whole string through a single-URL check would miss an unsafe
74
+ * candidate after the first, so each is parsed and every URL is sanitized. */
75
+ declare const URL_LIST_ATTRS: Set<string>;
76
+ export type SafePolicy = {
77
+ /**
78
+ * - The tag to render, or
79
+ * `null` to drop the element and its subtree.
80
+ */
81
+ tag: (tag: string) => string | null;
82
+ /**
83
+ * - The name/value to write, `{ name, value: null }` to clear an
84
+ * existing attribute (a sanitized-away URL, a dangerous style), or `null` to
85
+ * drop the property entirely (a rejected name).
86
+ */
87
+ prop: (name: string, value: any) => {
88
+ name: string;
89
+ value: any;
90
+ } | null;
91
+ /**
92
+ * - Whether `on` bindings are stripped (always
93
+ * true for the default policy; the renderers read this to skip the `on`
94
+ * path in safe mode).
95
+ */
96
+ dropsEvents: boolean;
97
+ };
98
+ /**
99
+ * @typedef {Object} SafePolicy
100
+ * @property {(tag: string) => string | null} tag - The tag to render, or
101
+ * `null` to drop the element and its subtree.
102
+ * @property {(name: string, value: any) => { name: string, value: any } | null}
103
+ * prop - The name/value to write, `{ name, value: null }` to clear an
104
+ * existing attribute (a sanitized-away URL, a dangerous style), or `null` to
105
+ * drop the property entirely (a rejected name).
106
+ * @property {boolean} dropsEvents - Whether `on` bindings are stripped (always
107
+ * true for the default policy; the renderers read this to skip the `on`
108
+ * path in safe mode).
109
+ */
110
+ /**
111
+ * Build the default safe policy. Stateless and cheap; a renderer builds one
112
+ * per `safe: true` and hands the *same* object to every element it renders.
113
+ *
114
+ * The policy is a set of **pure decisions** — it neither writes nor reports.
115
+ * The renderer owns `onUnsafe` and reports at the point it acts on a
116
+ * rejection, which is what lets it report the strips a policy never sees (an
117
+ * `on` binding, a widget) with one contract.
118
+ * @returns {SafePolicy}
119
+ */
120
+ export declare function createSafePolicy(): SafePolicy;
121
+ export { SAFE_HTML_TAGS, SAFE_SVG_TAGS, DANGEROUS_PROPS, URL_ATTRS, URL_LIST_ATTRS, RE_SAFE_NAME, };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @file The Jaren vnode contract — the JSON vocabulary for user interfaces.
3
+ *
4
+ * A vnode is a plain JSON value, designed to be the *output* of a JSLT
5
+ * stylesheet (`@jarenjs/json/jslt`) the way HTML is the output of XSLT:
6
+ *
7
+ * - text: a `string` or a `number` (rendered as `String(value)`)
8
+ * - element: an array whose first item is a string tag:
9
+ * `[tag, props?, ...children]`
10
+ * - list: an array whose first item is NOT a string — spliced into the
11
+ * parent's children in place (the natural shape of a JSLT
12
+ * `[{ "$apply": ... }]` body)
13
+ * - skipped: `null`, `undefined`, `true` and `false` render nothing
14
+ * (so `{"$if": ...}` bodies compose without wrappers)
15
+ *
16
+ * The second item of an element is its props object when it is a plain
17
+ * object; otherwise it is the first child. Recognized special props:
18
+ *
19
+ * - `key` — reconciliation identity for keyed children (never rendered)
20
+ * - `on` — `{ [eventType]: binding }`; the binding is opaque JSON handed
21
+ * to the renderer's `onEvent` hook (in `@jarenjs/app`: an
22
+ * action name or `{ "action": name, "with": payload }`)
23
+ * - `style` — a CSS string or an object of declarations
24
+ *
25
+ * Everything in this module is allocation-light and does no validation
26
+ * beyond shape dispatch: the vnode grammar is published as JSON Schema in
27
+ * `schemas/jaren-vnode.schema.json` for validating untrusted documents.
28
+ */
29
+ /** Frozen empty props object shared by all prop-less elements. */
30
+ export declare const EMPTY_PROPS: Readonly<{}>;
31
+ /**
32
+ * The reserved widget tag (VIEW-FORMAT §7). A valid custom-element name,
33
+ * so a renderer that predates the widget vocabulary degrades to an inert,
34
+ * harmless element — and the dash makes collision with real HTML tags
35
+ * impossible. (`$widget` is impossible: in query/JSLT rule bodies a
36
+ * string leaf starting with `$` is a path expression.)
37
+ */
38
+ export declare const WIDGET_TAG = "jaren-widget";
39
+ export type VNodeJson = string | number | boolean | null | undefined | VNodeElement | VNodeJson[];
40
+ export type VNodeElement = Array<any>;
41
+ /**
42
+ * A vnode JSON value.
43
+ * @typedef {string | number | boolean | null | undefined | VNodeElement | VNodeJson[]} VNodeJson
44
+ */
45
+ /**
46
+ * An element vnode: `[tag, props?, ...children]`.
47
+ * @typedef {Array<any>} VNodeElement
48
+ */
49
+ /**
50
+ * Is this vnode a text node (string or finite number)?
51
+ * @param {any} vnode
52
+ * @returns {vnode is string | number}
53
+ */
54
+ export declare function isTextNode(vnode: any): vnode is string | number;
55
+ /**
56
+ * Is this vnode an element (`[tag, ...]` with a string tag)?
57
+ * @param {any} vnode
58
+ * @returns {vnode is VNodeElement}
59
+ */
60
+ export declare function isElementNode(vnode: any): vnode is VNodeElement;
61
+ /**
62
+ * Is this value skipped by the renderer (`null`/`undefined`/booleans)?
63
+ * @param {any} vnode
64
+ * @returns {boolean}
65
+ */
66
+ export declare function isSkippedNode(vnode: any): boolean;
67
+ /**
68
+ * Is this vnode a widget node (`["jaren-widget", props]`, VIEW-FORMAT §7)?
69
+ * @param {any} vnode
70
+ * @returns {vnode is VNodeElement}
71
+ */
72
+ export declare function isWidgetNode(vnode: any): vnode is VNodeElement;
73
+ /**
74
+ * The props of an element vnode; `EMPTY_PROPS` when it has none.
75
+ * @param {VNodeElement} vnode
76
+ * @returns {Record<string, any>}
77
+ */
78
+ export declare function propsOf(vnode: VNodeElement): Record<string, any>;
79
+ /**
80
+ * The reconciliation key of an element vnode, or `undefined`.
81
+ * @param {any} vnode
82
+ * @returns {string | number | undefined}
83
+ */
84
+ export declare function keyOf(vnode: any): string | number | undefined;
85
+ /**
86
+ * The renderable children of an element vnode as a flat array: nested
87
+ * lists are spliced in place, skipped values are dropped.
88
+ * @param {VNodeElement} vnode
89
+ * @returns {VNodeJson[]}
90
+ */
91
+ export declare function childrenOf(vnode: VNodeElement): VNodeJson[];
92
+ /**
93
+ * Convenience element constructor for JavaScript-side views and tests —
94
+ * produces the same JSON an authored stylesheet would.
95
+ *
96
+ * @example
97
+ * h('li', { key: 7, class: 'todo' }, 'Buy milk')
98
+ * // -> ['li', { key: 7, class: 'todo' }, 'Buy milk']
99
+ *
100
+ * @param {string} tag
101
+ * @param {Record<string, any>} [props]
102
+ * @param {...any} children
103
+ * @returns {VNodeElement}
104
+ */
105
+ export declare function h(tag: string, props?: Record<string, any>, ...children: any[]): VNodeElement;
106
+ /**
107
+ * Two vnodes are "the same node" for reconciliation when a patch in place
108
+ * is possible: both text, or elements with equal tag and equal key.
109
+ * @param {VNodeJson} a
110
+ * @param {VNodeJson} b
111
+ * @returns {boolean}
112
+ */
113
+ export declare function isSameNode(a: VNodeJson, b: VNodeJson): boolean;