@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/dist/kit.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The URL-policy-bound API: `createHtml()` and the default kit.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, HtmlValueError } from "./errors.js";
|
|
5
|
+
import { escapeHtml } from "./escape.js";
|
|
6
|
+
import { renderValue } from "./render.js";
|
|
7
|
+
import { analyze, templateError, URL_ATTRIBUTES } from "./scanner.js";
|
|
8
|
+
import { isTrusted, makeTrusted, trustedString } from "./trusted.js";
|
|
9
|
+
import { createPolicy, normalizeSchemes, vetUrl } from "./url.js";
|
|
10
|
+
const ATTR_NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*$/;
|
|
11
|
+
const DESCRIPTOR = /^(?:\d+(?:\.\d+)?x|\d+w)$/;
|
|
12
|
+
const BLANK = /^[\t\n\f\r ]*$/;
|
|
13
|
+
/** Percent-encodes what would break `srcset` parsing: ASCII whitespace, edge commas. */
|
|
14
|
+
function encodeSrcsetUrl(u) {
|
|
15
|
+
return u
|
|
16
|
+
.replace(/[\t\n\f\r ]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0"))
|
|
17
|
+
.replace(/^,+|,+$/g, (m) => "%2C".repeat(m.length));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a kit whose `html`, `attrs`, `url` and `srcset` use the given URL policy.
|
|
21
|
+
* Throws `TypeError` on malformed options.
|
|
22
|
+
*/
|
|
23
|
+
export function createHtml(options = {}) {
|
|
24
|
+
const policy = createPolicy(options);
|
|
25
|
+
const schemeLists = new WeakMap();
|
|
26
|
+
function html(strings, ...values) {
|
|
27
|
+
const a = analyze(strings);
|
|
28
|
+
const slots = a.slots;
|
|
29
|
+
if (values.length !== slots.length) {
|
|
30
|
+
throw templateError(strings, "html must be called as a tagged template: html`…`");
|
|
31
|
+
}
|
|
32
|
+
const st = { neutral: a.neutral };
|
|
33
|
+
let out = strings[0];
|
|
34
|
+
for (let i = 0; i < slots.length; i++) {
|
|
35
|
+
const slot = slots[i];
|
|
36
|
+
if (slot.scheme !== "" && !policy.schemes.has(slot.scheme)) {
|
|
37
|
+
throw templateError(strings, `the static URL text before the slot uses the scheme "${slot.scheme}:", which this kit does not allow; build the whole URL in code and pass it through url(value, { schemes })`, i);
|
|
38
|
+
}
|
|
39
|
+
out += renderValue(values[i], slot, i, policy, st) + strings[i + 1];
|
|
40
|
+
}
|
|
41
|
+
return makeTrusted("html", out, st.neutral);
|
|
42
|
+
}
|
|
43
|
+
function attrValue(lower, value, n) {
|
|
44
|
+
if (lower === "srcset" || lower === "imagesrcset") {
|
|
45
|
+
if (isTrusted(value, "srcset"))
|
|
46
|
+
return escapeHtml(trustedString(value));
|
|
47
|
+
if (isTrusted(value, "unsafe"))
|
|
48
|
+
return trustedString(value);
|
|
49
|
+
throw new HtmlValueError(`attrs(): "${lower}" takes srcset([…])`, {
|
|
50
|
+
received: describe(value),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
switch (typeof value) {
|
|
54
|
+
case "string":
|
|
55
|
+
case "number":
|
|
56
|
+
case "bigint": {
|
|
57
|
+
const s = String(value);
|
|
58
|
+
if (!URL_ATTRIBUTES.has(lower) && lower !== "data")
|
|
59
|
+
return escapeHtml(s);
|
|
60
|
+
return escapeHtml(vetUrl(s, policy, undefined, lower) ?? policy.blockedUrl);
|
|
61
|
+
}
|
|
62
|
+
case "object":
|
|
63
|
+
if (isTrusted(value, "url") || isTrusted(value, "srcset")) {
|
|
64
|
+
return escapeHtml(trustedString(value));
|
|
65
|
+
}
|
|
66
|
+
if (isTrusted(value, "unsafe"))
|
|
67
|
+
return trustedString(value);
|
|
68
|
+
}
|
|
69
|
+
throw new HtmlValueError(`attrs(): the value of attribute #${n} has no attribute rendering`, {
|
|
70
|
+
received: describe(value),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function attrs(record) {
|
|
74
|
+
if (record === null || typeof record !== "object" || Array.isArray(record) ||
|
|
75
|
+
isTrusted(record)) {
|
|
76
|
+
throw new HtmlValueError("attrs() takes a plain object", {
|
|
77
|
+
received: describe(record),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
let out = "";
|
|
81
|
+
const names = Object.keys(record);
|
|
82
|
+
for (let n = 0; n < names.length; n++) {
|
|
83
|
+
const name = names[n];
|
|
84
|
+
if (!ATTR_NAME.test(name)) {
|
|
85
|
+
throw new HtmlValueError(`attrs(): attribute #${n} has an invalid name (must match ${ATTR_NAME})`, { received: "attribute name" });
|
|
86
|
+
}
|
|
87
|
+
const lower = name.toLowerCase();
|
|
88
|
+
if (lower.startsWith("on")) {
|
|
89
|
+
throw new HtmlValueError(`attrs(): attribute #${n} is an event handler (on*)`, {
|
|
90
|
+
received: "attribute name",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (lower === "srcdoc" || lower === "ping") {
|
|
94
|
+
throw new HtmlValueError(`attrs(): "${lower}" is not supported`, {
|
|
95
|
+
received: "attribute name",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const value = record[name];
|
|
99
|
+
if (value === null || value === undefined || value === false)
|
|
100
|
+
continue;
|
|
101
|
+
out += value === true
|
|
102
|
+
? " " + name
|
|
103
|
+
: " " + name + '="' + attrValue(lower, value, n) + '"';
|
|
104
|
+
}
|
|
105
|
+
return makeTrusted("attrs", out);
|
|
106
|
+
}
|
|
107
|
+
function url(value, options) {
|
|
108
|
+
if (typeof value !== "string") {
|
|
109
|
+
throw new HtmlValueError("url() takes a string", {
|
|
110
|
+
received: describe(value),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
let schemes = policy.schemes;
|
|
114
|
+
const list = options?.schemes;
|
|
115
|
+
if (list !== undefined) {
|
|
116
|
+
// cache only frozen lists: a mutable array could change between calls
|
|
117
|
+
schemes = schemeLists.get(list) ?? normalizeSchemes(list);
|
|
118
|
+
if (Object.isFrozen(list))
|
|
119
|
+
schemeLists.set(list, schemes);
|
|
120
|
+
}
|
|
121
|
+
return makeTrusted("url", vetUrl(value, policy, undefined, undefined, schemes) ?? policy.blockedUrl);
|
|
122
|
+
}
|
|
123
|
+
function srcset(candidates) {
|
|
124
|
+
if (!Array.isArray(candidates)) {
|
|
125
|
+
throw new HtmlValueError("srcset() takes an array", {
|
|
126
|
+
received: describe(candidates),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const parts = [];
|
|
130
|
+
for (let n = 0; n < candidates.length; n++) {
|
|
131
|
+
const c = candidates[n];
|
|
132
|
+
if (c === null || typeof c !== "object") {
|
|
133
|
+
throw new HtmlValueError(`srcset(): candidate #${n} must be an object`, {
|
|
134
|
+
received: describe(c),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const { url: u, descriptor } = c;
|
|
138
|
+
if (descriptor !== undefined &&
|
|
139
|
+
(typeof descriptor !== "string" || !DESCRIPTOR.test(descriptor))) {
|
|
140
|
+
throw new HtmlValueError(`srcset(): candidate #${n} has an invalid descriptor (expected e.g. "2x", "1.5x", "800w")`, { received: describe(descriptor) });
|
|
141
|
+
}
|
|
142
|
+
let vetted;
|
|
143
|
+
if (typeof u === "string") {
|
|
144
|
+
vetted = vetUrl(u, policy, undefined, "srcset");
|
|
145
|
+
}
|
|
146
|
+
else if (isTrusted(u, "url"))
|
|
147
|
+
vetted = trustedString(u);
|
|
148
|
+
else {
|
|
149
|
+
throw new HtmlValueError(`srcset(): candidate #${n} url must be a string or url()`, {
|
|
150
|
+
received: describe(u),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (vetted === null || BLANK.test(vetted))
|
|
154
|
+
continue;
|
|
155
|
+
const enc = encodeSrcsetUrl(vetted);
|
|
156
|
+
parts.push(descriptor ? `${enc} ${descriptor}` : enc);
|
|
157
|
+
}
|
|
158
|
+
return makeTrusted("srcset", parts.join(", "));
|
|
159
|
+
}
|
|
160
|
+
return Object.freeze({ html, attrs, url, srcset });
|
|
161
|
+
}
|
|
162
|
+
const defaultKit = createHtml();
|
|
163
|
+
/** The template tag of the default kit. Every interpolated value is escaped for its context. */
|
|
164
|
+
export const html = defaultKit.html;
|
|
165
|
+
/** `attrs()` of the default kit. */
|
|
166
|
+
export const attrs = defaultKit.attrs;
|
|
167
|
+
/** `url()` of the default kit. */
|
|
168
|
+
export const url = defaultKit.url;
|
|
169
|
+
/** `srcset()` of the default kit. */
|
|
170
|
+
export const srcset = defaultKit.srcset;
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* HTML tagged templates with contextual escaping. Every interpolated value is escaped for the
|
|
4
|
+
* place it appears (text, attribute, URL, `<script>`, `<style>`), worked out once per call site
|
|
5
|
+
* from the template's static markup. Slot positions that can never be made safe are rejected.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { html } from "@marianmeres/safe-html";
|
|
10
|
+
*
|
|
11
|
+
* const link = (href: string, label: string) => html`<a href="${href}">${label}</a>`;
|
|
12
|
+
* String(link("javascript:alert(1)", "<b>hi</b>"));
|
|
13
|
+
* // <a href="about:invalid#blocked"><b>hi</b></a>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export type { AttrValue, BlockedUrlInfo, HtmlKit, HtmlOptions, Renderable, SafeAttrs, SafeHtml, SafeSrcset, SafeUrl, SlotContext, SrcsetCandidate, Trusted, TrustedKind, } from "./types.js";
|
|
17
|
+
export { attrs, createHtml, html, srcset, url } from "./kit.js";
|
|
18
|
+
export { join, jsonScript, scriptText, styleText, unsafeRaw } from "./helpers.js";
|
|
19
|
+
export { escapeHtml } from "./escape.js";
|
|
20
|
+
export { isTrusted } from "./trusted.js";
|
|
21
|
+
export { HtmlTemplateError, HtmlValueError } from "./errors.js";
|
|
22
|
+
export { DEFAULT_URL_SCHEMES } from "./url.js";
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { attrs, createHtml, html, srcset, url } from "./kit.js";
|
|
2
|
+
export { join, jsonScript, scriptText, styleText, unsafeRaw } from "./helpers.js";
|
|
3
|
+
export { escapeHtml } from "./escape.js";
|
|
4
|
+
export { isTrusted } from "./trusted.js";
|
|
5
|
+
export { HtmlTemplateError, HtmlValueError } from "./errors.js";
|
|
6
|
+
export { DEFAULT_URL_SCHEMES } from "./url.js";
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SlotInfo } from "./scanner.js";
|
|
2
|
+
import { type UrlPolicy } from "./url.js";
|
|
3
|
+
/** Mutable state of one render. */
|
|
4
|
+
export interface RenderState {
|
|
5
|
+
/** Stays true while every HTML fragment placed in a text slot is neutral. */
|
|
6
|
+
neutral: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Renders one interpolated value for a slot. */
|
|
9
|
+
export declare function renderValue(v: unknown, slot: SlotInfo, index: number | undefined, policy: UrlPolicy | undefined, st: RenderState, depth?: number): string;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering: value × context rules. Whether a render throws depends on a value's type or
|
|
3
|
+
* trusted kind, never on the content of a string.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, HtmlValueError } from "./errors.js";
|
|
6
|
+
import { escapeHtml } from "./escape.js";
|
|
7
|
+
import { isNeutral, isTrusted, trustedString } from "./trusted.js";
|
|
8
|
+
import { vetUrl } from "./url.js";
|
|
9
|
+
/** Guards against cyclic arrays. */
|
|
10
|
+
const MAX_DEPTH = 100;
|
|
11
|
+
const HINTS = {
|
|
12
|
+
text: "",
|
|
13
|
+
rcdata: "<title> and <textarea> hold text only: pass a string",
|
|
14
|
+
"attr-list": "use attrs({ … })",
|
|
15
|
+
"attr-value": "attribute values take strings, numbers or url()",
|
|
16
|
+
url: "URL attributes take a string or url()",
|
|
17
|
+
srcset: "use srcset([…])",
|
|
18
|
+
script: "use jsonScript(data) for data, or scriptText(source) for code",
|
|
19
|
+
style: "use styleText(source)",
|
|
20
|
+
};
|
|
21
|
+
/** Renders one interpolated value for a slot. */
|
|
22
|
+
export function renderValue(v, slot, index, policy, st, depth = 0) {
|
|
23
|
+
const ctx = slot.context;
|
|
24
|
+
if (typeof v === "string") {
|
|
25
|
+
if (ctx === "text" || ctx === "attr-value" || ctx === "rcdata") {
|
|
26
|
+
return escapeHtml(v);
|
|
27
|
+
}
|
|
28
|
+
if (ctx === "url")
|
|
29
|
+
return escapeHtml(vet(v, slot, policy));
|
|
30
|
+
return refuse(v, slot, index);
|
|
31
|
+
}
|
|
32
|
+
if (v === null || v === undefined || typeof v === "boolean")
|
|
33
|
+
return "";
|
|
34
|
+
if (typeof v === "object") {
|
|
35
|
+
if (isTrusted(v)) {
|
|
36
|
+
const out = renderTrusted(v, slot, st);
|
|
37
|
+
if (out !== undefined)
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
else if (Array.isArray(v) &&
|
|
41
|
+
(ctx === "text" || ctx === "rcdata" || ctx === "attr-list")) {
|
|
42
|
+
if (depth >= MAX_DEPTH) {
|
|
43
|
+
throw new HtmlValueError("arrays nested too deeply (a cycle?)", {
|
|
44
|
+
slot: index,
|
|
45
|
+
context: ctx,
|
|
46
|
+
received: "array",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
let out = "";
|
|
50
|
+
for (let i = 0; i < v.length; i++) {
|
|
51
|
+
out += renderValue(v[i], slot, index, policy, st, depth + 1);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (typeof v === "number" || typeof v === "bigint") {
|
|
57
|
+
if (ctx === "text" || ctx === "attr-value" || ctx === "rcdata")
|
|
58
|
+
return String(v);
|
|
59
|
+
if (ctx === "url")
|
|
60
|
+
return escapeHtml(vet(String(v), slot, policy));
|
|
61
|
+
}
|
|
62
|
+
return refuse(v, slot, index);
|
|
63
|
+
}
|
|
64
|
+
function renderTrusted(v, slot, st) {
|
|
65
|
+
const ctx = slot.context;
|
|
66
|
+
const s = trustedString(v);
|
|
67
|
+
switch (v.kind) {
|
|
68
|
+
case "unsafe":
|
|
69
|
+
return s;
|
|
70
|
+
case "html":
|
|
71
|
+
if (ctx !== "text")
|
|
72
|
+
return undefined;
|
|
73
|
+
if (!isNeutral(v)) {
|
|
74
|
+
if (slot.foreign)
|
|
75
|
+
return undefined;
|
|
76
|
+
st.neutral = false;
|
|
77
|
+
}
|
|
78
|
+
return s;
|
|
79
|
+
case "attrs":
|
|
80
|
+
return ctx === "attr-list" ? s : undefined;
|
|
81
|
+
case "url":
|
|
82
|
+
return ctx === "text" || ctx === "rcdata" || ctx === "attr-value" ||
|
|
83
|
+
ctx === "url"
|
|
84
|
+
? escapeHtml(s)
|
|
85
|
+
: undefined;
|
|
86
|
+
case "srcset":
|
|
87
|
+
return ctx === "srcset" || ctx === "attr-value" ? escapeHtml(s) : undefined;
|
|
88
|
+
case "json":
|
|
89
|
+
case "js":
|
|
90
|
+
return ctx === "script" ? s : undefined;
|
|
91
|
+
case "css":
|
|
92
|
+
return ctx === "style" ? s : undefined;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
function vet(value, slot, policy) {
|
|
97
|
+
return vetUrl(value, policy, slot.tag, slot.attr) ??
|
|
98
|
+
policy.blockedUrl;
|
|
99
|
+
}
|
|
100
|
+
function refuse(v, slot, index) {
|
|
101
|
+
const ctx = slot.context;
|
|
102
|
+
const received = describe(v);
|
|
103
|
+
let msg;
|
|
104
|
+
// deno-lint-ignore no-explicit-any
|
|
105
|
+
if (typeof v?.then === "function") {
|
|
106
|
+
msg = "async values are not supported: await before rendering";
|
|
107
|
+
}
|
|
108
|
+
else if (isTrusted(v)) {
|
|
109
|
+
if (v.kind === "html" && ctx === "text") {
|
|
110
|
+
msg =
|
|
111
|
+
"this HTML fragment can't go inside <svg>/<math>: it contains markup that parses differently there (e.g. <style>, <p>)";
|
|
112
|
+
}
|
|
113
|
+
else if (v.kind === "html" && (ctx === "attr-value" || ctx === "url")) {
|
|
114
|
+
msg = "an html fragment can't be an attribute value";
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
msg = `cannot render ${received} in ${ctx} context: ${HINTS[ctx] || "not allowed"}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (typeof v === "function" || typeof v === "symbol" ||
|
|
121
|
+
(typeof v === "object" && !Array.isArray(v))) {
|
|
122
|
+
msg = "this value has no HTML rendering: convert it to a string first";
|
|
123
|
+
}
|
|
124
|
+
else if (Array.isArray(v)) {
|
|
125
|
+
msg =
|
|
126
|
+
`arrays render only in text and attribute-list slots, not in ${ctx} context`;
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
msg = `cannot render ${received} in ${ctx} context: ${HINTS[ctx]}`;
|
|
130
|
+
throw new HtmlValueError(msg, { slot: index, context: ctx, received });
|
|
131
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context analysis (the scanner).
|
|
3
|
+
*
|
|
4
|
+
* Purpose: classify every slot of a template from its static strings alone, once per call
|
|
5
|
+
* site, and cache the result (including a failure) by template object.
|
|
6
|
+
*
|
|
7
|
+
* Invariants:
|
|
8
|
+
* - Models only the part of the HTML tokenizer, and of the tree construction that switches the
|
|
9
|
+
* tokenizer (raw text, RCDATA, SVG/MathML foreign content), needed to classify slots.
|
|
10
|
+
* Anything it does not model is an error, never a guess.
|
|
11
|
+
* - A slot never changes the scanner state: every value renders so that it can't.
|
|
12
|
+
* - A template ends where it started: in text, outside any tag, comment, raw text or foreign
|
|
13
|
+
* content. That makes a fragment safe to drop into another template's text slot.
|
|
14
|
+
* - Foreign content: a fragment is "neutral" when a second scan that starts inside SVG/MathML
|
|
15
|
+
* agrees with the HTML scan; only neutral fragments may go into SVG/MathML text slots.
|
|
16
|
+
*/
|
|
17
|
+
import { HtmlTemplateError } from "./errors.js";
|
|
18
|
+
import type { SlotContext } from "./types.js";
|
|
19
|
+
/** What the scanner knows about one slot. All fields are always present (one object shape). */
|
|
20
|
+
export interface SlotInfo {
|
|
21
|
+
readonly context: SlotContext;
|
|
22
|
+
/** Lowercased tag name (attribute and raw-text slots), else `""`. */
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
/** Lowercased attribute name (attribute value slots), else `""`. */
|
|
25
|
+
readonly attr: string;
|
|
26
|
+
/** Scheme written statically before a URL slot, else `""`; checked against the kit. */
|
|
27
|
+
readonly scheme: string;
|
|
28
|
+
/** A `text` slot inside SVG/MathML: HTML fragments must be neutral. */
|
|
29
|
+
readonly foreign: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Creates a slot description (always the same shape, which keeps rendering monomorphic). */
|
|
32
|
+
export declare function slotInfo(context: SlotContext, tag?: string, attr?: string, scheme?: string, foreign?: boolean): SlotInfo;
|
|
33
|
+
/** The cached result for one call site. */
|
|
34
|
+
export interface Analysis {
|
|
35
|
+
readonly slots: readonly SlotInfo[];
|
|
36
|
+
/** Parses the same inside SVG/MathML as in HTML (see module doc). */
|
|
37
|
+
readonly neutral: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Attributes whose value is a URL (plus `data` on `<object>`). */
|
|
40
|
+
export declare const URL_ATTRIBUTES: ReadonlySet<string>;
|
|
41
|
+
/** Number of analyses run so far. For tests only (not exported from mod.ts). */
|
|
42
|
+
export declare function analysisCount(): number;
|
|
43
|
+
/**
|
|
44
|
+
* The analysis of a template, from the cache or computed once. Throws `HtmlTemplateError` if
|
|
45
|
+
* `strings` is not a genuine template object, or if the template is refused.
|
|
46
|
+
*/
|
|
47
|
+
export declare function analyze(strings: TemplateStringsArray): Analysis;
|
|
48
|
+
/**
|
|
49
|
+
* An `HtmlTemplateError` whose excerpt shows the static text with slots as `${…}` and the
|
|
50
|
+
* error position (a slot, or a chunk offset) marked `⟨here⟩`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function templateError(strings: TemplateStringsArray, reason: string, slot?: number, chunk?: number, offset?: number, useRaw?: boolean): HtmlTemplateError;
|