@marianmeres/safe-html 0.2.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/AGENTS.md +92 -0
- package/API.md +404 -0
- package/CLAUDE.md +3 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +63 -0
- package/dist/escape.d.ts +6 -0
- package/dist/escape.js +40 -0
- package/dist/helpers.d.ts +35 -0
- package/dist/helpers.js +124 -0
- package/dist/kit.d.ts +14 -0
- package/dist/kit.js +170 -0
- package/dist/mod.d.ts +22 -0
- package/dist/mod.js +6 -0
- package/dist/render.d.ts +9 -0
- package/dist/render.js +131 -0
- package/dist/scanner.d.ts +52 -0
- package/dist/scanner.js +742 -0
- package/dist/trusted.d.ts +35 -0
- package/dist/trusted.js +68 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +4 -0
- package/dist/url.d.ts +30 -0
- package/dist/url.js +93 -0
- package/docs/design.md +346 -0
- package/package.json +32 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @marianmeres/safe-html — Agent Guide
|
|
2
|
+
|
|
3
|
+
## Quick Reference
|
|
4
|
+
|
|
5
|
+
- **What**: `html` tagged template with contextual escaping + helpers (`attrs`, `url`,
|
|
6
|
+
`srcset`, `jsonScript`, `scriptText`, `styleText`, `join`, `unsafeRaw`, `createHtml`).
|
|
7
|
+
- **Stack**: TypeScript, Deno (primary), npm via `@marianmeres/npmbuild`. Zero runtime deps.
|
|
8
|
+
- **Test**: `deno task test` | **Types**: `deno task check` | **Bench**: `deno task bench` |
|
|
9
|
+
**npm build**: `deno task npm:build`
|
|
10
|
+
- **Dev-only dep**: `npm:parse5` (tests only; parses rendered output as a browser would).
|
|
11
|
+
- **Status**: `0.x`; stay there until a real page has been ported onto it.
|
|
12
|
+
|
|
13
|
+
## Project Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
src/mod.ts public exports only
|
|
17
|
+
src/types.ts public types (Trusted, Renderable, SlotContext, HtmlKit, …)
|
|
18
|
+
src/trusted.ts TrustedValue (brand via Symbol.for keys, private fields), isTrusted
|
|
19
|
+
src/escape.ts escapeHtml
|
|
20
|
+
src/url.ts URL policy: urlScheme (URL-parser-accurate), vetUrl, createPolicy
|
|
21
|
+
src/scanner.ts context analysis: tokenizer model, slot rules, neutrality, WeakMap cache
|
|
22
|
+
src/render.ts value × context rules (renderValue)
|
|
23
|
+
src/helpers.ts policy-free helpers: jsonScript, scriptText, styleText, unsafeRaw, join
|
|
24
|
+
src/kit.ts createHtml (html, attrs, url, srcset bound to a policy) + default kit
|
|
25
|
+
tests/ golden scanner tests, render table, helpers, SVG/MathML, parse5 invariance
|
|
26
|
+
bench/ deno bench vs hand-written concatenation
|
|
27
|
+
docs/design.md normative rules (read before changing scanner or render behavior)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Critical Conventions
|
|
31
|
+
|
|
32
|
+
1. **Safety invariants** (see [docs/design.md](./docs/design.md) §2): given templates that
|
|
33
|
+
pass analysis and no `unsafeRaw()`, no string value may change document structure. Every
|
|
34
|
+
change must keep `tests/invariance.test.ts` green.
|
|
35
|
+
2. **Errors depend on the template or the value's type/kind, never on string content.**
|
|
36
|
+
Blocked URLs are replaced, `</script` is rewritten — never thrown.
|
|
37
|
+
3. **Error messages never contain interpolated values** (attribute names in `attrs()` too:
|
|
38
|
+
refer to them by index). Template excerpts contain static text only.
|
|
39
|
+
4. **Strictness**: anything the scanner does not model is an `HtmlTemplateError`, never a
|
|
40
|
+
guess. Relaxing a rule is a minor change; tightening one breaks users.
|
|
41
|
+
5. **Analysis is kit-independent** (one shared cache). Kit-dependent checks (static URL
|
|
42
|
+
scheme) run at render from `SlotInfo.scheme`.
|
|
43
|
+
6. **`SlotInfo` objects always have the same shape** (create via `slotInfo()`); rendering is
|
|
44
|
+
hot and relies on monomorphic property access.
|
|
45
|
+
7. **No `Object.freeze` on trusted values** (costs more than a small render); immutability
|
|
46
|
+
comes from private fields.
|
|
47
|
+
8. **`deno fmt` rewrites the contents of `html`-tagged templates.** Every file whose `html`
|
|
48
|
+
templates must stay byte-exact (all tests, bench, API.md) starts with
|
|
49
|
+
`// deno-fmt-ignore-file` (`<!-- deno-fmt-ignore-file -->` in Markdown). Keep it when
|
|
50
|
+
adding such files.
|
|
51
|
+
9. **No `console.*`** in `src/` (visibility goes through `onBlockedUrl`).
|
|
52
|
+
10. **U+2028 / U+2029 in source**: always write them as `\u` escapes. Some tools turn escapes
|
|
53
|
+
into the literal characters, which are line terminators and break regex literals.
|
|
54
|
+
|
|
55
|
+
## Common Tasks
|
|
56
|
+
|
|
57
|
+
### Add a URL attribute
|
|
58
|
+
|
|
59
|
+
1. Add the lowercased name to `URL_ATTRIBUTES` in `src/scanner.ts` (also used by `attrs()`).
|
|
60
|
+
2. Add a golden case to `tests/scanner.test.ts` ("URL attributes") and to `URL_ATTRS` in
|
|
61
|
+
`tests/invariance.test.ts`.
|
|
62
|
+
3. Update docs/design.md §3.4, API.md (`attrs`).
|
|
63
|
+
|
|
64
|
+
### Change a scanner rule
|
|
65
|
+
|
|
66
|
+
1. Read docs/design.md §3; locate the state in `Scanner.chunk()`, the slot rule in
|
|
67
|
+
`Scanner.slot()` / `valueSlot()`, tag effects in `emitTag()` / `endTag()`.
|
|
68
|
+
2. Add golden tests (context or exact error substring), and a corpus template in
|
|
69
|
+
`tests/invariance.test.ts` if a new context shape is involved.
|
|
70
|
+
3. Consider the neutrality scan (`new Scanner(strings, true)`): a rule that differs between
|
|
71
|
+
HTML and foreign content must make the two scans disagree, not silently match.
|
|
72
|
+
4. Update docs/design.md (and §8 if it departs from earlier behavior).
|
|
73
|
+
|
|
74
|
+
### Change the value × context table
|
|
75
|
+
|
|
76
|
+
Edit `renderValue` / `renderTrusted` in `src/render.ts`, the table test in
|
|
77
|
+
`tests/render.test.ts`, and docs/design.md §4 plus the table in API.md.
|
|
78
|
+
|
|
79
|
+
## Before Making Changes
|
|
80
|
+
|
|
81
|
+
- [ ] Read docs/design.md for the rule you touch
|
|
82
|
+
- [ ] `deno task test` (includes parse5 invariance and round-trips) and `deno task check`
|
|
83
|
+
- [ ] `deno lint` and `deno fmt --check`
|
|
84
|
+
- [ ] `deno task bench` if touching `render.ts`, `kit.ts`, `trusted.ts`, `escape.ts`, `url.ts`
|
|
85
|
+
- [ ] Update README / API.md / docs/design.md for user-visible behavior
|
|
86
|
+
|
|
87
|
+
## Documentation Index
|
|
88
|
+
|
|
89
|
+
- [README.md](./README.md) — overview, safety lists, usage, porting from Svelte
|
|
90
|
+
- [API.md](./API.md) — full API reference
|
|
91
|
+
- [docs/design.md](./docs/design.md) — normative analysis and rendering rules, algorithms,
|
|
92
|
+
changes from the original draft
|
package/API.md
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
<!-- deno-fmt-ignore-file -->
|
|
2
|
+
<!-- (deno fmt would rewrite the html`…` templates in the examples) -->
|
|
3
|
+
|
|
4
|
+
# API
|
|
5
|
+
|
|
6
|
+
All exports come from the package root:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { html, attrs, url, srcset, createHtml, jsonScript, scriptText, styleText,
|
|
10
|
+
join, unsafeRaw, escapeHtml, isTrusted, HtmlTemplateError, HtmlValueError,
|
|
11
|
+
DEFAULT_URL_SCHEMES } from "@marianmeres/safe-html";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The rules for which value may go where are in [docs/design.md](docs/design.md) (§3 analysis,
|
|
15
|
+
§4 value × context table).
|
|
16
|
+
|
|
17
|
+
## Template tag
|
|
18
|
+
|
|
19
|
+
### `` html`…` ``
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
function html(strings: TemplateStringsArray, ...values: Renderable[]): SafeHtml;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The template tag. Returns a trusted, immutable `SafeHtml`. Each interpolated value is rendered
|
|
26
|
+
for the context its slot sits in, as worked out once per call site from the static markup:
|
|
27
|
+
|
|
28
|
+
| Context | Where | Accepts |
|
|
29
|
+
| ------------ | --------------------------------------- | -------------------------------------------------------------- |
|
|
30
|
+
| `text` | element content | strings and numbers (escaped), `html`/`join()` fragments, `url()`, arrays |
|
|
31
|
+
| `rcdata` | `<title>`, `<textarea>` content | strings and numbers (escaped), `url()`, arrays |
|
|
32
|
+
| `attr-list` | `<tag ${…}>` | `attrs()`, arrays of them |
|
|
33
|
+
| `attr-value` | inside a quoted attribute value | strings and numbers (escaped), `url()`, `srcset()` |
|
|
34
|
+
| `url` | a URL attribute value the slot starts | strings and numbers (URL policy, then escaped), `url()` |
|
|
35
|
+
| `srcset` | a whole `srcset`/`imagesrcset` value | `srcset()` |
|
|
36
|
+
| `script` | `<script>` content | `jsonScript()`, `scriptText()` |
|
|
37
|
+
| `style` | `<style>` content | `styleText()` |
|
|
38
|
+
|
|
39
|
+
Everywhere: `null`, `undefined`, `false` and `true` render nothing; `unsafeRaw()` is inserted
|
|
40
|
+
verbatim. Anything else throws `HtmlValueError`.
|
|
41
|
+
|
|
42
|
+
**Throws:** `HtmlTemplateError` if the template is refused (slot in an unquoted attribute,
|
|
43
|
+
event handler, tag name, comment, …) or `html` is not called as a tagged template;
|
|
44
|
+
`HtmlValueError` if a value has no rendering in its context.
|
|
45
|
+
|
|
46
|
+
**Example:**
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const Card = (c: { title: string; href: string; tags: string[] }) =>
|
|
50
|
+
html`<article>
|
|
51
|
+
<h2><a href="${c.href}">${c.title}</a></h2>
|
|
52
|
+
${c.tags.length > 0 && html`<p>${join(c.tags, ", ")}</p>`}
|
|
53
|
+
</article>`;
|
|
54
|
+
|
|
55
|
+
String(Card({ title: "<Hi>", href: "javascript:x", tags: [] }));
|
|
56
|
+
// <article>
|
|
57
|
+
// <h2><a href="about:invalid#blocked"><Hi></a></h2>
|
|
58
|
+
//
|
|
59
|
+
// </article>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Kit (URL policy)
|
|
65
|
+
|
|
66
|
+
### `createHtml(options?)`
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
function createHtml(options?: HtmlOptions): HtmlKit;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Creates a kit whose `html`, `attrs`, `url` and `srcset` use the given URL policy. The default
|
|
73
|
+
exports (`html`, `attrs`, `url`, `srcset`) are `createHtml()` with default options. Analyses are
|
|
74
|
+
cached per call site and shared by all kits.
|
|
75
|
+
|
|
76
|
+
**Parameters:**
|
|
77
|
+
- `options.urlSchemes` (`readonly string[]`, optional) — allowed schemes, case-insensitive, with
|
|
78
|
+
or without the colon. Default: `DEFAULT_URL_SCHEMES` (`http`, `https`, `mailto`, `tel`).
|
|
79
|
+
- `options.blockedUrl` (`string`, optional) — what a blocked URL renders as. Default:
|
|
80
|
+
`"about:invalid#blocked"`.
|
|
81
|
+
- `options.onBlockedUrl` (`(info: BlockedUrlInfo) => void`, optional) — called once per blocked
|
|
82
|
+
URL (e.g. to log a probe). If it throws, the render throws.
|
|
83
|
+
|
|
84
|
+
**Returns:** `HtmlKit` — `{ html, attrs, url, srcset }` (frozen; the functions need no `this`).
|
|
85
|
+
|
|
86
|
+
**Throws:** `TypeError` on malformed options.
|
|
87
|
+
|
|
88
|
+
**Example:**
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
export const { html, attrs, url, srcset } = createHtml({
|
|
92
|
+
urlSchemes: [...DEFAULT_URL_SCHEMES, "sms"],
|
|
93
|
+
onBlockedUrl: ({ tag, attribute }) => console.warn("blocked URL", tag, attribute),
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A template whose static URL text writes a scheme (`href="mailto:${a}"`) is checked against the
|
|
98
|
+
kit's list at render: a scheme outside it (`href="javascript:go('${id}')"`) is an
|
|
99
|
+
`HtmlTemplateError`.
|
|
100
|
+
|
|
101
|
+
### `attrs(record)`
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
function attrs(record: Readonly<Record<string, AttrValue>>): SafeAttrs;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
An attribute list for an `attr-list` slot: `<button ${attrs({ type: "submit", disabled })}>`.
|
|
108
|
+
|
|
109
|
+
- Each attribute renders as ` name="value"` (leading space, value escaped), in key order.
|
|
110
|
+
- `true` → ` name` (boolean attribute); `false`, `null`, `undefined` → omitted.
|
|
111
|
+
- URL attributes (`href`, `src`, `action`, `formaction`, `poster`, `cite`, `background`,
|
|
112
|
+
`longdesc`, `manifest`, `codebase`, `icon`, `xlink:href`, `data`) go through the URL policy
|
|
113
|
+
unless the value is `url()` or `unsafeRaw()`.
|
|
114
|
+
- `srcset` / `imagesrcset` take `srcset()` (or `unsafeRaw()`).
|
|
115
|
+
- Values may also be `url()` / `srcset()` (escaped) or `unsafeRaw()` (verbatim) anywhere.
|
|
116
|
+
|
|
117
|
+
**Throws:** `HtmlValueError` for a name not matching `/^[A-Za-z_][A-Za-z0-9_.:-]*$/`, an `on*`
|
|
118
|
+
name, `srcdoc` or `ping`, or a value with no attribute rendering (object, `html`, …). Names
|
|
119
|
+
are expected to come from code.
|
|
120
|
+
|
|
121
|
+
**Example:**
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
html`<input ${attrs({ type: "email", name: "email", required: true, value: draft })}>`;
|
|
125
|
+
// <input type="email" name="email" required value="…">
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `url(value, options?)`
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
function url(value: string, options?: { schemes?: readonly string[] }): SafeUrl;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Checks `value` against the scheme allowlist and returns a `SafeUrl`: vetted, **not escaped**
|
|
135
|
+
(it is escaped wherever it is rendered). A blocked value becomes the kit's `blockedUrl`
|
|
136
|
+
(and `onBlockedUrl` is called); it never throws on content. `options.schemes` replaces the
|
|
137
|
+
kit's list for this one value.
|
|
138
|
+
|
|
139
|
+
Relative URLs (`/x`, `./x`, `?q`, `#f`, `//host`, `x/y`) are allowed. Scheme detection
|
|
140
|
+
follows the URL parser: leading control characters and spaces are skipped and tab/LF/CR are
|
|
141
|
+
ignored, so `" java\tscript:"` is caught.
|
|
142
|
+
|
|
143
|
+
**Example:**
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
html`<img src="${url(`data:image/png;base64,${b64}`, { schemes: ["data"] })}">`;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `srcset(candidates)`
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
function srcset(candidates: readonly SrcsetCandidate[]): SafeSrcset;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A `srcset` value. Each candidate's URL goes through the URL policy unless it is a `SafeUrl`;
|
|
156
|
+
blocked and empty candidates are **dropped**. ASCII whitespace and leading/trailing commas in
|
|
157
|
+
a URL are percent-encoded. Returns an unescaped, vetted value (escaped where rendered).
|
|
158
|
+
|
|
159
|
+
**Throws:** `HtmlValueError` for a descriptor not matching `/^\d+(\.\d+)?x$|^\d+w$/` (e.g.
|
|
160
|
+
`"2x"`, `"1.5x"`, `"800w"`), or a malformed candidate.
|
|
161
|
+
|
|
162
|
+
**Example:**
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
html`<img src="${img.src}" srcset="${srcset([
|
|
166
|
+
{ url: img.src },
|
|
167
|
+
{ url: img.src2x, descriptor: "2x" },
|
|
168
|
+
])}" alt="${img.alt}">`;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Policy-free helpers
|
|
174
|
+
|
|
175
|
+
### `jsonScript(value, space?)`
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
function jsonScript(value: unknown, space?: number): Trusted<"json">;
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`JSON.stringify(value, null, space)` with `<`, `>`, `&`, U+2028 and U+2029 written as `\uXXXX`
|
|
182
|
+
escapes: valid JSON of the same value, a valid JS expression, and unable to end a `<script>`.
|
|
183
|
+
Accepted in `script` context only. Use it for `application/ld+json`, `application/json` data
|
|
184
|
+
islands, or `const x = ${jsonScript(v)};`. `SafeHtml` values inside serialize as their string.
|
|
185
|
+
|
|
186
|
+
**Throws:** `HtmlValueError` when there is no JSON representation (`undefined`, a function or
|
|
187
|
+
a symbol at the root). `JSON.stringify`'s own `TypeError`s (cycles, `BigInt`) propagate.
|
|
188
|
+
|
|
189
|
+
**Example:**
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
html`<script type="application/ld+json">${jsonScript({ "@type": "Article", headline })}</script>`;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `scriptText(source)`
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
function scriptText(source: string): Trusted<"js">;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Inline JavaScript that is code, not data (typically a constant). Every case-insensitive
|
|
202
|
+
`</script` becomes `<\/script`, every `<!--` becomes `\x3C!--`, and a `<` that ends the source
|
|
203
|
+
(alone or followed by the start of `/script` or `!--`) becomes `\x3C`. These keep the meaning
|
|
204
|
+
inside strings, template literals and regexes. Accepted in `script` context only. Never throws
|
|
205
|
+
on content.
|
|
206
|
+
|
|
207
|
+
**Example:**
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
html`<script>${scriptText(`document.documentElement.dataset.theme = "dark";`)}</script>`;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `styleText(source)`
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
function styleText(source: string): Trusted<"css">;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Inline CSS. Every case-insensitive `</style` becomes `<\/style`, and a `<` that ends the source
|
|
220
|
+
(alone or followed by the start of `/style`) becomes `\00003C`. Other `<` are kept
|
|
221
|
+
(media-query ranges use them). Accepted in `style` context only. Prevents breakout, **not**
|
|
222
|
+
CSS injection: build dynamic CSS from validated tokens.
|
|
223
|
+
|
|
224
|
+
**Example:**
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
html`<style>${styleText(`:root { --accent: ${validatedHexColor}; }`)}</style>`;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### `unsafeRaw(value)`
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
function unsafeRaw(value: string): Trusted<"unsafe">;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The escape hatch: inserted verbatim in **any** context, with no checks. It means *trusted*,
|
|
237
|
+
never *cleaned*; this package is not an HTML sanitizer. The name is loud on purpose so every
|
|
238
|
+
use is greppable.
|
|
239
|
+
|
|
240
|
+
**Example:**
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
html`<article>${unsafeRaw(sanitizedByYourSanitizer)}</article>`;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### `join(values, separator?)`
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
function join(values: readonly Renderable[], separator?: string | SafeHtml): SafeHtml;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Renders each value under the `text` rules, separated by `separator` (default `""`; a string is
|
|
253
|
+
escaped, `html` is inserted as is). `null`, `undefined` and booleans are skipped, so
|
|
254
|
+
conditional items leave no stray separator. The result is `html`, so `<title>` and
|
|
255
|
+
`<textarea>` refuse it: pass a plain string there (`parts.join(" | ")`).
|
|
256
|
+
|
|
257
|
+
**Example:**
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
html`<p>${join([author, date, isDraft && html`<em>draft</em>`], " · ")}</p>`;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### `escapeHtml(value)`
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
function escapeHtml(value: string): string;
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Escapes `&`, `<`, `>`, `"`, `'` (as `&`, `<`, `>`, `"`, `'`). Returns the input
|
|
270
|
+
unchanged when there is nothing to escape. For use outside templates.
|
|
271
|
+
|
|
272
|
+
### `isTrusted(v, kind?)`
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
function isTrusted<K extends TrustedKind>(v: unknown, kind?: K): v is Trusted<K>;
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Whether `v` is a value produced by this package (optionally of the given kind). Recognizes
|
|
279
|
+
values from other copies of the package in the same process; never true for parsed JSON.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Errors
|
|
284
|
+
|
|
285
|
+
### `HtmlTemplateError`
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
class HtmlTemplateError extends Error {
|
|
289
|
+
readonly slot?: number; // offending slot, when the error is about a slot
|
|
290
|
+
readonly excerpt: string; // static text, slots as ${…}, the error at ⟨here⟩
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
A refused template, or a call that is not a genuine tagged template. Depends on the template
|
|
295
|
+
only (and, for a static URL scheme, on the kit's allowlist). A failed analysis is cached and
|
|
296
|
+
rethrown on every render of that call site. The excerpt contains only developer-written text,
|
|
297
|
+
so it is safe to log:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
HtmlTemplateError: unquoted attribute value: quote it, e.g. name="${…}"
|
|
301
|
+
slot 0 in: <a href=${⟨here⟩}>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### `HtmlValueError`
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
class HtmlValueError extends TypeError {
|
|
308
|
+
readonly slot?: number; // when raised while rendering a template
|
|
309
|
+
readonly context?: SlotContext; // when raised while rendering a template or join()
|
|
310
|
+
readonly received: string; // type or kind, e.g. "object", "Promise", "Trusted<html>"
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
A value with no rendering in its context (depends on the value's type or kind), an invalid
|
|
315
|
+
attribute name in `attrs()` or descriptor in `srcset()` (content expected from code), or
|
|
316
|
+
arrays nested deeper than 100 (a cycle). Messages never include values.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Constants
|
|
321
|
+
|
|
322
|
+
### `DEFAULT_URL_SCHEMES`
|
|
323
|
+
|
|
324
|
+
`readonly ["http", "https", "mailto", "tel"]` — the default scheme allowlist (frozen).
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Types
|
|
329
|
+
|
|
330
|
+
### `Trusted`, `TrustedKind`
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
type TrustedKind = "html" | "attrs" | "url" | "srcset" | "json" | "js" | "css" | "unsafe";
|
|
334
|
+
|
|
335
|
+
interface Trusted<K extends TrustedKind = TrustedKind> {
|
|
336
|
+
readonly kind: K;
|
|
337
|
+
toString(): string; // also String(v), `${v}`
|
|
338
|
+
toJSON(): string; // JSON.stringify(v) gives the string
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
type SafeHtml = Trusted<"html">;
|
|
342
|
+
type SafeAttrs = Trusted<"attrs">;
|
|
343
|
+
type SafeUrl = Trusted<"url">;
|
|
344
|
+
type SafeSrcset = Trusted<"srcset">;
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Immutable, branded values. `SafeUrl` and `SafeSrcset` hold the vetted, unescaped value.
|
|
348
|
+
|
|
349
|
+
### `Renderable`
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
type Renderable =
|
|
353
|
+
| Trusted | string | number | bigint | boolean | null | undefined
|
|
354
|
+
| readonly Renderable[];
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
What `html` accepts. Objects, functions, symbols, dates and promises are type errors.
|
|
358
|
+
|
|
359
|
+
### `AttrValue`
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
type AttrValue =
|
|
363
|
+
| string | number | bigint | boolean | null | undefined
|
|
364
|
+
| Trusted<"url" | "srcset" | "unsafe">;
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### `SrcsetCandidate`
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
interface SrcsetCandidate {
|
|
371
|
+
url: string | SafeUrl;
|
|
372
|
+
descriptor?: string; // "2x", "1.5x", "800w"; omitted = 1x
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `HtmlOptions`, `HtmlKit`, `BlockedUrlInfo`
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
interface HtmlOptions {
|
|
380
|
+
urlSchemes?: readonly string[];
|
|
381
|
+
blockedUrl?: string;
|
|
382
|
+
onBlockedUrl?: (info: BlockedUrlInfo) => void;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
interface HtmlKit {
|
|
386
|
+
html(strings: TemplateStringsArray, ...values: Renderable[]): SafeHtml;
|
|
387
|
+
attrs(record: Readonly<Record<string, AttrValue>>): SafeAttrs;
|
|
388
|
+
url(value: string, options?: { schemes?: readonly string[] }): SafeUrl;
|
|
389
|
+
srcset(candidates: readonly SrcsetCandidate[]): SafeSrcset;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface BlockedUrlInfo {
|
|
393
|
+
url: string; // as given
|
|
394
|
+
tag?: string; // lowercased, when known
|
|
395
|
+
attribute?: string; // lowercased, when known
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### `SlotContext`
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
type SlotContext =
|
|
403
|
+
| "text" | "rcdata" | "attr-list" | "attr-value" | "url" | "srcset" | "script" | "style";
|
|
404
|
+
```
|
package/CLAUDE.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marian Meres
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|