@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.
- package/README.md +181 -0
- package/dist/types/dom.d.ts +226 -0
- package/dist/types/helpers/index.d.ts +15 -0
- package/dist/types/helpers/memo.d.ts +49 -0
- package/dist/types/helpers/metrics.d.ts +31 -0
- package/dist/types/helpers/svg.d.ts +164 -0
- package/dist/types/helpers/theme.d.ts +47 -0
- package/dist/types/helpers/url.d.ts +71 -0
- package/dist/types/html.d.ts +80 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/safe.d.ts +121 -0
- package/dist/types/vnode.d.ts +113 -0
- package/docs/VIEW-FORMAT.md +538 -0
- package/package.json +63 -0
- package/schemas/jaren-vnode-safe.schema.json +92 -0
- package/schemas/jaren-vnode.schema.json +127 -0
- package/src/dom.js +1173 -0
- package/src/helpers/index.js +35 -0
- package/src/helpers/memo.js +61 -0
- package/src/helpers/metrics.js +116 -0
- package/src/helpers/svg.js +233 -0
- package/src/helpers/theme.js +75 -0
- package/src/helpers/url.js +154 -0
- package/src/html.js +197 -0
- package/src/index.js +34 -0
- package/src/safe.js +278 -0
- package/src/vnode.js +170 -0
|
@@ -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 < 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;
|