Merge pull request #3604 from lightpanda-io/sanitizer

webapi: Start of Sanitizer WebAPI
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-09-23 21:23:20 +08:00
commit 1dd33ee7ec
4 files changed
+1823

No files matched your search

+1
View File
@@ -1271,6 +1271,7 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/XPathExpression.zig"),
@import("../webapi/XPathEvaluator.zig"),
@import("../webapi/collections/DOMStringList.zig"),
@import("../webapi/Sanitizer.zig"),
});
// APIs available on EVERY worker global — dedicated, shared and service. This
+370
View File
@@ -0,0 +1,370 @@
<!DOCTYPE html>
<script src="testing.js"></script>
<body></body>
<script id=constructor>
{
testing.expectEqual('function', typeof Sanitizer);
testing.expectEqual(true, new Sanitizer() instanceof Sanitizer);
testing.expectEqual(true, new Sanitizer({}) instanceof Sanitizer);
testing.expectEqual(true, new Sanitizer(null) instanceof Sanitizer);
testing.expectEqual(true, new Sanitizer(undefined) instanceof Sanitizer);
testing.expectEqual(true, new Sanitizer('default') instanceof Sanitizer);
testing.expectEqual(true, new Sanitizer({unknown: [1, 2]}) instanceof Sanitizer);
testing.expectError('TypeError', () => new Sanitizer('nope'));
}
</script>
<script id=emptyConfig>
{
// Neither half of a pair given: canonicalization fills in the empty
// remove-lists, which is what "allow everything" looks like.
testing.expectEqual({
removeElements: [],
removeAttributes: [],
removeProcessingInstructions: [],
comments: true,
javascriptURLs: true,
}, new Sanitizer({}).get());
}
</script>
<script id=canonicalization>
{
const get = (config) => new Sanitizer(config).get();
testing.expectEqual([{name: 'div', namespace: 'http://www.w3.org/1999/xhtml', removeAttributes: []}],
get({elements: ['div']}).elements);
testing.expectEqual([{name: 'b', namespace: 'http://www.w3.org/1999/xhtml'}],
get({removeElements: [{name: 'b'}]}).removeElements);
testing.expectEqual([{name: 'b', namespace: null}],
get({removeElements: [{name: 'b', namespace: null}]}).removeElements);
// An empty namespace is the null namespace.
testing.expectEqual([{name: 'b', namespace: null}],
get({removeElements: [{name: 'b', namespace: ''}]}).removeElements);
// Attributes default to the null namespace, elements to XHTML.
testing.expectEqual([{name: 'href', namespace: null}], get({attributes: ['href']}).attributes);
testing.expectEqual([{target: 'xml-stylesheet'}],
get({processingInstructions: ['xml-stylesheet']}).processingInstructions);
}
</script>
<script id=sorting>
{
// Namespace first, with null below every URI; then local name.
const config = new Sanitizer({attributes: [
{name: 'b', namespace: 'http://example.org/'},
{name: 'z'},
{name: 'a', namespace: 'http://example.org/'},
{name: 'a'},
]}).get();
testing.expectEqual([
{name: 'a', namespace: null},
{name: 'z', namespace: null},
{name: 'a', namespace: 'http://example.org/'},
{name: 'b', namespace: 'http://example.org/'},
], config.attributes);
}
</script>
<script id=booleanDefaults>
{
testing.expectEqual(false, new Sanitizer().get().comments);
testing.expectEqual(true, new Sanitizer({}).get().comments);
testing.expectEqual(false, new Sanitizer({comments: false}).get().comments);
testing.expectEqual(false, new Sanitizer().get().dataAttributes);
testing.expectEqual(true, new Sanitizer({attributes: []}).get().dataAttributes);
// dataAttributes only exists alongside an attribute allow-list.
testing.expectEqual(false, 'dataAttributes' in new Sanitizer({}).get());
testing.expectEqual(false, 'dataAttributes' in new Sanitizer({removeAttributes: []}).get());
// Web IDL: an explicit null is ToBoolean(null) = false, while an explicit
// undefined leaves the member absent and so takes the default.
testing.expectEqual(false, new Sanitizer({comments: null}).get().comments);
testing.expectEqual(true, new Sanitizer({comments: 'abc'}).get().comments);
// The setters report whether they changed anything.
const s = new Sanitizer();
testing.expectEqual(true, s.setComments(true));
testing.expectEqual(false, s.setComments(true));
testing.expectEqual(true, s.get().comments);
testing.expectEqual(true, s.setDataAttributes(true));
testing.expectEqual(false, s.setDataAttributes(true));
testing.expectEqual(true, s.get().dataAttributes);
// Without an attribute allow-list there is no dataAttributes to set.
testing.expectEqual(false, new Sanitizer({removeAttributes: []}).setDataAttributes(true));
}
</script>
<script id=javascriptUrls>
{
testing.expectEqual(false, new Sanitizer().get().javascriptURLs);
testing.expectEqual(true, new Sanitizer({}).get().javascriptURLs);
testing.expectEqual(false, new Sanitizer({javascriptURLs: false}).get().javascriptURLs);
testing.expectEqual(false, new Sanitizer({javascriptURLs: null}).get().javascriptURLs);
testing.expectEqual(true, new Sanitizer({javascriptURLs: undefined}).get().javascriptURLs);
const s = new Sanitizer();
testing.expectEqual(true, s.setJavascriptURLs(true));
testing.expectEqual(false, s.setJavascriptURLs(true));
testing.expectEqual(true, s.get().javascriptURLs);
// removeUnsafe turns them back off, and reports that it changed something.
testing.expectEqual(true, s.removeUnsafe());
testing.expectEqual(false, s.get().javascriptURLs);
}
{
// Allowing every data attribute subsumes any named individually, and the
// config has to stay valid.
const s = new Sanitizer({attributes: ['data-x', 'id'], elements: [{name: 'div', attributes: ['data-y']}]});
testing.expectEqual(true, s.setDataAttributes(true));
testing.expectEqual([{name: 'id', namespace: null}], s.get().attributes);
testing.expectEqual([], s.get().elements[0].attributes);
}
</script>
<script id=invalidConfigs>
{
// Each pair is mutually exclusive.
testing.expectError('TypeError', () => new Sanitizer({elements: [], removeElements: []}));
testing.expectError('TypeError', () => new Sanitizer({attributes: [], removeAttributes: []}));
// No duplicates, whichever spelling they arrive in.
testing.expectError('TypeError', () => new Sanitizer({elements: ['abc', 'abc']}));
testing.expectError('TypeError', () => new Sanitizer({elements: ['abc', {name: 'abc'}]}));
testing.expectError('TypeError', () => new Sanitizer({attributes: ['abc', {name: 'abc', namespace: null}]}));
// <html>, <svg> and <math> can never be replaced with their children.
testing.expectError('TypeError', () => new Sanitizer({replaceWithChildrenElements: ['html']}));
testing.expectError('TypeError', () => new Sanitizer({
replaceWithChildrenElements: [{name: 'svg', namespace: 'http://www.w3.org/2000/svg'}],
}));
// ... but in another namespace it is just an element.
testing.expectEqual(1, new Sanitizer({replaceWithChildrenElements: ['svg']}).get().replaceWithChildrenElements.length);
testing.expectError('TypeError', () => new Sanitizer({elements: ['p'], replaceWithChildrenElements: ['p']}));
testing.expectError('TypeError', () => new Sanitizer({removeElements: ['p'], replaceWithChildrenElements: ['p']}));
// A per-element list may not restate a global one.
testing.expectError('TypeError', () => new Sanitizer({
attributes: ['id'],
elements: [{name: 'div', attributes: ['id']}],
}));
// A per-element removeAttributes must be a subset of the global allow-list.
testing.expectError('TypeError', () => new Sanitizer({
attributes: ['class'],
elements: [{name: 'div', removeAttributes: ['title']}],
}));
testing.expectError('TypeError', () => new Sanitizer({
attributes: ['data-bar'],
dataAttributes: true,
}));
testing.expectError('TypeError', () => new Sanitizer({removeAttributes: [], dataAttributes: false}));
testing.expectError('TypeError', () => new Sanitizer({
removeAttributes: [],
elements: [{name: 'div', attributes: [], removeAttributes: []}],
}));
testing.expectError('TypeError', () => new Sanitizer({processingInstructions: [], removeProcessingInstructions: []}));
testing.expectError('TypeError', () => new Sanitizer({removeProcessingInstructions: ['x', {target: 'x'}]}));
}
</script>
<script id=perElementAttributes>
{
{
const s = new Sanitizer({elements: [{name: 'div', attributes: ['href', 'src']}]});
testing.expectEqual(true, 'attributes' in s.get().elements[0]);
testing.expectEqual(false, 'removeAttributes' in s.get().elements[0]);
testing.expectEqual(2, s.get().elements[0].attributes.length);
// allowElement overwrites the per-element list rather than merging.
s.allowElement({name: 'div', namespace: 'http://www.w3.org/1999/xhtml', attributes: ['class']});
testing.expectEqual([{name: 'class', namespace: null}], s.get().elements[0].attributes);
}
{
// An element with neither list still reports an empty remove-list.
const s = new Sanitizer({elements: ['div']});
testing.expectEqual([], s.get().elements[0].removeAttributes);
testing.expectEqual(false, 'attributes' in s.get().elements[0]);
}
}
</script>
<script id=elementModifiers>
{
const s = new Sanitizer({elements: ['div', 'p']});
testing.expectEqual(2, s.get().elements.length);
testing.expectEqual(true, s.allowElement('bla'));
testing.expectEqual(false, s.allowElement('bla'));
testing.expectEqual(3, s.get().elements.length);
testing.expectEqual(true, s.removeElement({name: 'div'}));
testing.expectEqual(2, s.get().elements.length);
testing.expectEqual(true, s.replaceElementWithChildren({name: 'p', namespace: 'http://www.w3.org/1999/xhtml'}));
testing.expectEqual([{name: 'bla', namespace: 'http://www.w3.org/1999/xhtml', removeAttributes: []}],
s.get().elements);
testing.expectEqual([{name: 'p', namespace: 'http://www.w3.org/1999/xhtml'}],
s.get().replaceWithChildrenElements);
testing.expectEqual(false, s.replaceElementWithChildren('html'));
}
{
// With a remove-list the same calls flip meaning.
const s = new Sanitizer({removeElements: ['div', 'p']});
testing.expectEqual(true, s.removeElement('bla'));
testing.expectEqual(3, s.get().removeElements.length);
testing.expectEqual(true, s.allowElement('p'));
testing.expectEqual(2, s.get().removeElements.length);
// A remove-list config has nowhere to put per-element attributes.
testing.expectEqual(false, s.allowElement({name: 'div', attributes: ['id']}));
}
{
// An element with neither attribute list is the same as one with an empty
// removeAttributes, so re-allowing it either way is not a change.
const s = new Sanitizer({elements: ['div', {name: 'p', removeAttributes: []}]});
testing.expectEqual(false, s.allowElement({name: 'div', removeAttributes: []}));
testing.expectEqual(false, s.allowElement('p'));
testing.expectEqual(true, s.allowElement('span'));
testing.expectEqual(false, s.allowElement({name: 'span', removeAttributes: []}));
testing.expectEqual(true, s.allowElement({name: 'span', attributes: []}));
}
</script>
<script id=attributeModifiers>
{
const s = new Sanitizer({attributes: ['href', 'src']});
testing.expectEqual(true, s.allowAttribute('id'));
testing.expectEqual(false, s.allowAttribute('id'));
testing.expectEqual(3, s.get().attributes.length);
// A different namespace is a different attribute.
testing.expectEqual(false, s.removeAttribute({name: 'href', namespace: 'http://example.org/'}));
testing.expectEqual(true, s.removeAttribute({name: 'href'}));
testing.expectEqual(true, s.removeAttribute({name: 'src', namespace: null}));
testing.expectEqual([{name: 'id', namespace: null}], s.get().attributes);
}
{
const s = new Sanitizer({removeAttributes: ['href', 'src']});
testing.expectEqual(true, s.removeAttribute('id'));
testing.expectEqual(3, s.get().removeAttributes.length);
testing.expectEqual(false, s.allowAttribute({name: 'href', namespace: 'http://example.org/'}));
testing.expectEqual(true, s.allowAttribute('href'));
testing.expectEqual(true, s.allowAttribute({name: 'src', namespace: null}));
testing.expectEqual([{name: 'id', namespace: null}], s.get().removeAttributes);
}
{
// A global allow subsumes the per-element lists.
const s = new Sanitizer({
attributes: ['id', 'title'],
elements: [{name: 'div', attributes: ['class', 'dir'], removeAttributes: ['title']}],
});
testing.expectEqual(true, s.removeAttribute('dir'));
testing.expectEqual([{name: 'class', namespace: null}], s.get().elements[0].attributes);
testing.expectEqual(true, s.removeAttribute('title'));
testing.expectEqual([{name: 'id', namespace: null}], s.get().attributes);
testing.expectEqual([], s.get().elements[0].removeAttributes);
}
</script>
<script id=processingInstructionModifiers>
{
const s = new Sanitizer({processingInstructions: ['target-1', 'target-2']});
testing.expectEqual(true, s.allowProcessingInstruction('target-3'));
testing.expectEqual(false, s.allowProcessingInstruction('target-3'));
testing.expectEqual(false, s.removeProcessingInstruction({target: 'target-4'}));
testing.expectEqual(true, s.removeProcessingInstruction({target: 'target-1'}));
testing.expectEqual(true, s.removeProcessingInstruction({target: 'target-2'}));
testing.expectEqual([{target: 'target-3'}], s.get().processingInstructions);
}
</script>
<script id=removeUnsafe>
{
const s = new Sanitizer({});
testing.expectEqual(true, s.removeUnsafe());
testing.expectEqual(false, s.removeUnsafe());
const config = s.get();
testing.expectEqual(false, 'elements' in config);
testing.expectEqual(false, 'attributes' in config);
testing.expectEqual([
{name: 'base', namespace: 'http://www.w3.org/1999/xhtml'},
{name: 'embed', namespace: 'http://www.w3.org/1999/xhtml'},
{name: 'frame', namespace: 'http://www.w3.org/1999/xhtml'},
{name: 'iframe', namespace: 'http://www.w3.org/1999/xhtml'},
{name: 'object', namespace: 'http://www.w3.org/1999/xhtml'},
{name: 'script', namespace: 'http://www.w3.org/1999/xhtml'},
{name: 'script', namespace: 'http://www.w3.org/2000/svg'},
{name: 'use', namespace: 'http://www.w3.org/2000/svg'},
], config.removeElements);
const names = config.removeAttributes.map((a) => a.name);
testing.expectEqual(true, names.length > 0);
for (const attribute of config.removeAttributes) {
testing.expectEqual(null, attribute.namespace);
testing.expectEqual(true, attribute.name.startsWith('on'));
}
testing.expectEqual(true, names.every((n, i) => i === 0 || names[i - 1] < n));
testing.expectEqual(true, names.includes('onclick'));
}
{
// Nothing in the default config is unsafe, so removeUnsafe is a no-op on it.
const before = new Sanitizer('default').get();
const after = new Sanitizer('default');
testing.expectEqual(false, after.removeUnsafe());
testing.expectEqual(true, before.elements.length > 0);
testing.expectEqual(before.elements.length, after.get().elements.length);
testing.expectEqual(before.attributes.length, after.get().attributes.length);
testing.expectEqual(false, 'removeElements' in after.get());
testing.expectEqual(false, 'removeAttributes' in after.get());
}
{
// lightpanda-specific: our event handler list is HTML's merged with the
// handlers we compile ourselves, so neither side can go missing. Another
// engine ships only its own set and is expected to fail this.
const s = new Sanitizer({});
s.removeUnsafe();
const names = s.get().removeAttributes.map((a) => a.name);
for (const name of ['onunload', 'onmouseenter', 'onbeforematch', 'onfullscreenchange']) {
testing.expectEqual(true, names.includes(name), {script_id: 'removeUnsafe'});
}
}
</script>
<script id=defaultConfig>
{
const config = new Sanitizer().get();
testing.expectEqual(121, config.elements.length);
testing.expectEqual(58, config.attributes.length);
testing.expectEqual(false, config.comments);
testing.expectEqual(false, config.dataAttributes);
// Sorted by namespace URI, which puts MathML (1998) first and SVG (2000) last.
testing.expectEqual({name: 'math', namespace: 'http://www.w3.org/1998/Math/MathML', attributes: []},
config.elements[0]);
testing.expectEqual('http://www.w3.org/2000/svg', config.elements[config.elements.length - 1].namespace);
// Nothing that runs script is in there.
const names = config.elements.map((e) => e.name);
for (const unsafe of ['script', 'iframe', 'object', 'embed', 'frame']) {
testing.expectEqual(false, names.includes(unsafe));
}
testing.expectEqual(false, config.attributes.some((a) => a.name.startsWith('on')));
testing.expectEqual([], config.processingInstructions);
testing.expectEqual(false, config.javascriptURLs);
}
</script>
File diff suppressed because it is too large. Load diff
+429
View File
@@ -0,0 +1,429 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// default configuration for Sanitizer
const std = @import("std");
const global_event_handlers = @import("global_event_handlers.zig");
const Namespace = @import("Sanitizer.zig").Namespace;
pub const xhtml_ns = "http://www.w3.org/1999/xhtml";
pub const svg_ns = "http://www.w3.org/2000/svg";
pub const mathml_ns = "http://www.w3.org/1998/Math/MathML";
pub const xlink_ns = "http://www.w3.org/1999/xlink";
pub const xml_ns = "http://www.w3.org/XML/1998/namespace";
pub const xmlns_ns = "http://www.w3.org/2000/xmlns/";
// A name as a table writes it, and as one arrives from JS: still a plain slice,
// because a `Sanitizer.Name` holds an `lp.String`, which cannot be built at
// comptime past 12 bytes -- and `animateTransform` and friends are longer.
// `Sanitizer.staticName` / `ownName` turn one of these into a `Name`.
pub const Name = struct {
name: []const u8,
namespace: Namespace,
};
pub const Element = struct {
name: []const u8,
namespace: Namespace,
attributes: []const Name = &.{},
};
// https://html.spec.whatwg.org/#built-in-non-replaceable-elements-list
pub const non_replaceable_elements: []const Name = &.{
.{ .name = "html", .namespace = .xhtml },
.{ .name = "svg", .namespace = .svg },
.{ .name = "math", .namespace = .mathml },
};
// https://wicg.github.io/sanitizer-api/#built-in-safe-default-configuration
pub const default_elements: []const Element = &.{
.{ .name = "math", .namespace = .mathml },
.{ .name = "merror", .namespace = .mathml },
.{ .name = "mfrac", .namespace = .mathml },
.{ .name = "mi", .namespace = .mathml },
.{ .name = "mmultiscripts", .namespace = .mathml },
.{ .name = "mn", .namespace = .mathml },
.{ .name = "mo", .namespace = .mathml, .attributes = &.{ .{ .name = "fence", .namespace = .none }, .{ .name = "form", .namespace = .none }, .{ .name = "largeop", .namespace = .none }, .{ .name = "lspace", .namespace = .none }, .{ .name = "maxsize", .namespace = .none }, .{ .name = "minsize", .namespace = .none }, .{ .name = "movablelimits", .namespace = .none }, .{ .name = "rspace", .namespace = .none }, .{ .name = "separator", .namespace = .none }, .{ .name = "stretchy", .namespace = .none }, .{ .name = "symmetric", .namespace = .none } } },
.{ .name = "mover", .namespace = .mathml, .attributes = &.{.{ .name = "accent", .namespace = .none }} },
.{ .name = "mpadded", .namespace = .mathml, .attributes = &.{ .{ .name = "depth", .namespace = .none }, .{ .name = "height", .namespace = .none }, .{ .name = "lspace", .namespace = .none }, .{ .name = "voffset", .namespace = .none }, .{ .name = "width", .namespace = .none } } },
.{ .name = "mphantom", .namespace = .mathml },
.{ .name = "mprescripts", .namespace = .mathml },
.{ .name = "mroot", .namespace = .mathml },
.{ .name = "mrow", .namespace = .mathml },
.{ .name = "ms", .namespace = .mathml },
.{ .name = "mspace", .namespace = .mathml, .attributes = &.{ .{ .name = "depth", .namespace = .none }, .{ .name = "height", .namespace = .none }, .{ .name = "width", .namespace = .none } } },
.{ .name = "msqrt", .namespace = .mathml },
.{ .name = "mstyle", .namespace = .mathml },
.{ .name = "msub", .namespace = .mathml },
.{ .name = "msubsup", .namespace = .mathml },
.{ .name = "msup", .namespace = .mathml },
.{ .name = "mtable", .namespace = .mathml },
.{ .name = "mtd", .namespace = .mathml, .attributes = &.{ .{ .name = "columnspan", .namespace = .none }, .{ .name = "rowspan", .namespace = .none } } },
.{ .name = "mtext", .namespace = .mathml },
.{ .name = "mtr", .namespace = .mathml },
.{ .name = "munder", .namespace = .mathml, .attributes = &.{.{ .name = "accentunder", .namespace = .none }} },
.{ .name = "munderover", .namespace = .mathml, .attributes = &.{ .{ .name = "accent", .namespace = .none }, .{ .name = "accentunder", .namespace = .none } } },
.{ .name = "semantics", .namespace = .mathml },
.{ .name = "a", .namespace = .xhtml, .attributes = &.{ .{ .name = "href", .namespace = .none }, .{ .name = "hreflang", .namespace = .none }, .{ .name = "type", .namespace = .none } } },
.{ .name = "abbr", .namespace = .xhtml },
.{ .name = "address", .namespace = .xhtml },
.{ .name = "article", .namespace = .xhtml },
.{ .name = "aside", .namespace = .xhtml },
.{ .name = "b", .namespace = .xhtml },
.{ .name = "bdi", .namespace = .xhtml },
.{ .name = "bdo", .namespace = .xhtml },
.{ .name = "blockquote", .namespace = .xhtml, .attributes = &.{.{ .name = "cite", .namespace = .none }} },
.{ .name = "body", .namespace = .xhtml },
.{ .name = "br", .namespace = .xhtml },
.{ .name = "caption", .namespace = .xhtml },
.{ .name = "cite", .namespace = .xhtml },
.{ .name = "code", .namespace = .xhtml },
.{ .name = "col", .namespace = .xhtml, .attributes = &.{.{ .name = "span", .namespace = .none }} },
.{ .name = "colgroup", .namespace = .xhtml, .attributes = &.{.{ .name = "span", .namespace = .none }} },
.{ .name = "data", .namespace = .xhtml, .attributes = &.{.{ .name = "value", .namespace = .none }} },
.{ .name = "dd", .namespace = .xhtml },
.{ .name = "del", .namespace = .xhtml, .attributes = &.{ .{ .name = "cite", .namespace = .none }, .{ .name = "datetime", .namespace = .none } } },
.{ .name = "dfn", .namespace = .xhtml },
.{ .name = "div", .namespace = .xhtml },
.{ .name = "dl", .namespace = .xhtml },
.{ .name = "dt", .namespace = .xhtml },
.{ .name = "em", .namespace = .xhtml },
.{ .name = "figcaption", .namespace = .xhtml },
.{ .name = "figure", .namespace = .xhtml },
.{ .name = "footer", .namespace = .xhtml },
.{ .name = "h1", .namespace = .xhtml },
.{ .name = "h2", .namespace = .xhtml },
.{ .name = "h3", .namespace = .xhtml },
.{ .name = "h4", .namespace = .xhtml },
.{ .name = "h5", .namespace = .xhtml },
.{ .name = "h6", .namespace = .xhtml },
.{ .name = "head", .namespace = .xhtml },
.{ .name = "header", .namespace = .xhtml },
.{ .name = "hgroup", .namespace = .xhtml },
.{ .name = "hr", .namespace = .xhtml },
.{ .name = "html", .namespace = .xhtml },
.{ .name = "i", .namespace = .xhtml },
.{ .name = "ins", .namespace = .xhtml, .attributes = &.{ .{ .name = "cite", .namespace = .none }, .{ .name = "datetime", .namespace = .none } } },
.{ .name = "kbd", .namespace = .xhtml },
.{ .name = "li", .namespace = .xhtml, .attributes = &.{.{ .name = "value", .namespace = .none }} },
.{ .name = "main", .namespace = .xhtml },
.{ .name = "mark", .namespace = .xhtml },
.{ .name = "menu", .namespace = .xhtml },
.{ .name = "nav", .namespace = .xhtml },
.{ .name = "ol", .namespace = .xhtml, .attributes = &.{ .{ .name = "reversed", .namespace = .none }, .{ .name = "start", .namespace = .none }, .{ .name = "type", .namespace = .none } } },
.{ .name = "p", .namespace = .xhtml },
.{ .name = "pre", .namespace = .xhtml },
.{ .name = "q", .namespace = .xhtml },
.{ .name = "rp", .namespace = .xhtml },
.{ .name = "rt", .namespace = .xhtml },
.{ .name = "ruby", .namespace = .xhtml },
.{ .name = "s", .namespace = .xhtml },
.{ .name = "samp", .namespace = .xhtml },
.{ .name = "search", .namespace = .xhtml },
.{ .name = "section", .namespace = .xhtml },
.{ .name = "small", .namespace = .xhtml },
.{ .name = "span", .namespace = .xhtml },
.{ .name = "strong", .namespace = .xhtml },
.{ .name = "sub", .namespace = .xhtml },
.{ .name = "sup", .namespace = .xhtml },
.{ .name = "table", .namespace = .xhtml },
.{ .name = "tbody", .namespace = .xhtml },
.{ .name = "td", .namespace = .xhtml, .attributes = &.{ .{ .name = "colspan", .namespace = .none }, .{ .name = "headers", .namespace = .none }, .{ .name = "rowspan", .namespace = .none } } },
.{ .name = "tfoot", .namespace = .xhtml },
.{ .name = "th", .namespace = .xhtml, .attributes = &.{ .{ .name = "abbr", .namespace = .none }, .{ .name = "colspan", .namespace = .none }, .{ .name = "headers", .namespace = .none }, .{ .name = "rowspan", .namespace = .none }, .{ .name = "scope", .namespace = .none } } },
.{ .name = "thead", .namespace = .xhtml },
.{ .name = "time", .namespace = .xhtml, .attributes = &.{.{ .name = "datetime", .namespace = .none }} },
.{ .name = "title", .namespace = .xhtml },
.{ .name = "tr", .namespace = .xhtml },
.{ .name = "u", .namespace = .xhtml },
.{ .name = "ul", .namespace = .xhtml },
.{ .name = "var", .namespace = .xhtml },
.{ .name = "wbr", .namespace = .xhtml },
.{ .name = "a", .namespace = .svg, .attributes = &.{ .{ .name = "href", .namespace = .none }, .{ .name = "hreflang", .namespace = .none }, .{ .name = "type", .namespace = .none } } },
.{ .name = "circle", .namespace = .svg, .attributes = &.{ .{ .name = "cx", .namespace = .none }, .{ .name = "cy", .namespace = .none }, .{ .name = "pathLength", .namespace = .none }, .{ .name = "r", .namespace = .none } } },
.{ .name = "defs", .namespace = .svg },
.{ .name = "desc", .namespace = .svg },
.{ .name = "ellipse", .namespace = .svg, .attributes = &.{ .{ .name = "cx", .namespace = .none }, .{ .name = "cy", .namespace = .none }, .{ .name = "pathLength", .namespace = .none }, .{ .name = "rx", .namespace = .none }, .{ .name = "ry", .namespace = .none } } },
.{ .name = "foreignObject", .namespace = .svg, .attributes = &.{ .{ .name = "height", .namespace = .none }, .{ .name = "width", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
.{ .name = "g", .namespace = .svg },
.{ .name = "line", .namespace = .svg, .attributes = &.{ .{ .name = "pathLength", .namespace = .none }, .{ .name = "x1", .namespace = .none }, .{ .name = "x2", .namespace = .none }, .{ .name = "y1", .namespace = .none }, .{ .name = "y2", .namespace = .none } } },
.{ .name = "marker", .namespace = .svg, .attributes = &.{ .{ .name = "markerHeight", .namespace = .none }, .{ .name = "markerUnits", .namespace = .none }, .{ .name = "markerWidth", .namespace = .none }, .{ .name = "orient", .namespace = .none }, .{ .name = "preserveAspectRatio", .namespace = .none }, .{ .name = "refX", .namespace = .none }, .{ .name = "refY", .namespace = .none }, .{ .name = "viewBox", .namespace = .none } } },
.{ .name = "metadata", .namespace = .svg },
.{ .name = "path", .namespace = .svg, .attributes = &.{ .{ .name = "d", .namespace = .none }, .{ .name = "pathLength", .namespace = .none } } },
.{ .name = "polygon", .namespace = .svg, .attributes = &.{ .{ .name = "pathLength", .namespace = .none }, .{ .name = "points", .namespace = .none } } },
.{ .name = "polyline", .namespace = .svg, .attributes = &.{ .{ .name = "pathLength", .namespace = .none }, .{ .name = "points", .namespace = .none } } },
.{ .name = "rect", .namespace = .svg, .attributes = &.{ .{ .name = "height", .namespace = .none }, .{ .name = "pathLength", .namespace = .none }, .{ .name = "rx", .namespace = .none }, .{ .name = "ry", .namespace = .none }, .{ .name = "width", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
.{ .name = "svg", .namespace = .svg, .attributes = &.{ .{ .name = "height", .namespace = .none }, .{ .name = "preserveAspectRatio", .namespace = .none }, .{ .name = "viewBox", .namespace = .none }, .{ .name = "width", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
.{ .name = "text", .namespace = .svg, .attributes = &.{ .{ .name = "dx", .namespace = .none }, .{ .name = "dy", .namespace = .none }, .{ .name = "lengthAdjust", .namespace = .none }, .{ .name = "rotate", .namespace = .none }, .{ .name = "textLength", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
.{ .name = "textPath", .namespace = .svg, .attributes = &.{ .{ .name = "lengthAdjust", .namespace = .none }, .{ .name = "method", .namespace = .none }, .{ .name = "path", .namespace = .none }, .{ .name = "side", .namespace = .none }, .{ .name = "spacing", .namespace = .none }, .{ .name = "startOffset", .namespace = .none }, .{ .name = "textLength", .namespace = .none } } },
.{ .name = "title", .namespace = .svg },
.{ .name = "tspan", .namespace = .svg, .attributes = &.{ .{ .name = "dx", .namespace = .none }, .{ .name = "dy", .namespace = .none }, .{ .name = "lengthAdjust", .namespace = .none }, .{ .name = "rotate", .namespace = .none }, .{ .name = "textLength", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
};
pub const default_attributes: []const Name = &.{
.{ .name = "alignment-baseline", .namespace = .none },
.{ .name = "baseline-shift", .namespace = .none },
.{ .name = "clip-path", .namespace = .none },
.{ .name = "clip-rule", .namespace = .none },
.{ .name = "color", .namespace = .none },
.{ .name = "color-interpolation", .namespace = .none },
.{ .name = "cursor", .namespace = .none },
.{ .name = "dir", .namespace = .none },
.{ .name = "direction", .namespace = .none },
.{ .name = "display", .namespace = .none },
.{ .name = "displaystyle", .namespace = .none },
.{ .name = "dominant-baseline", .namespace = .none },
.{ .name = "fill", .namespace = .none },
.{ .name = "fill-opacity", .namespace = .none },
.{ .name = "fill-rule", .namespace = .none },
.{ .name = "font-family", .namespace = .none },
.{ .name = "font-size", .namespace = .none },
.{ .name = "font-size-adjust", .namespace = .none },
.{ .name = "font-stretch", .namespace = .none },
.{ .name = "font-style", .namespace = .none },
.{ .name = "font-variant", .namespace = .none },
.{ .name = "font-weight", .namespace = .none },
.{ .name = "lang", .namespace = .none },
.{ .name = "letter-spacing", .namespace = .none },
.{ .name = "marker-end", .namespace = .none },
.{ .name = "marker-mid", .namespace = .none },
.{ .name = "marker-start", .namespace = .none },
.{ .name = "mathbackground", .namespace = .none },
.{ .name = "mathcolor", .namespace = .none },
.{ .name = "mathsize", .namespace = .none },
.{ .name = "opacity", .namespace = .none },
.{ .name = "paint-order", .namespace = .none },
.{ .name = "pointer-events", .namespace = .none },
.{ .name = "scriptlevel", .namespace = .none },
.{ .name = "shape-rendering", .namespace = .none },
.{ .name = "stop-color", .namespace = .none },
.{ .name = "stop-opacity", .namespace = .none },
.{ .name = "stroke", .namespace = .none },
.{ .name = "stroke-dasharray", .namespace = .none },
.{ .name = "stroke-dashoffset", .namespace = .none },
.{ .name = "stroke-linecap", .namespace = .none },
.{ .name = "stroke-linejoin", .namespace = .none },
.{ .name = "stroke-miterlimit", .namespace = .none },
.{ .name = "stroke-opacity", .namespace = .none },
.{ .name = "stroke-width", .namespace = .none },
.{ .name = "text-anchor", .namespace = .none },
.{ .name = "text-decoration", .namespace = .none },
.{ .name = "text-overflow", .namespace = .none },
.{ .name = "text-rendering", .namespace = .none },
.{ .name = "title", .namespace = .none },
.{ .name = "transform", .namespace = .none },
.{ .name = "transform-origin", .namespace = .none },
.{ .name = "unicode-bidi", .namespace = .none },
.{ .name = "vector-effect", .namespace = .none },
.{ .name = "visibility", .namespace = .none },
.{ .name = "white-space", .namespace = .none },
.{ .name = "word-spacing", .namespace = .none },
.{ .name = "writing-mode", .namespace = .none },
};
// https://html.spec.whatwg.org/#built-in-safe-baseline-configuration
// Every HTML element the spec marks "Sanitization: Unsafe" (base, embed,
// iframe, object, script), plus the obsolete frame and SVG's script and use.
pub const baseline_remove_elements: []const Name = &.{
.{ .name = "base", .namespace = .xhtml },
.{ .name = "embed", .namespace = .xhtml },
.{ .name = "frame", .namespace = .xhtml },
.{ .name = "iframe", .namespace = .xhtml },
.{ .name = "object", .namespace = .xhtml },
.{ .name = "script", .namespace = .xhtml },
.{ .name = "script", .namespace = .svg },
.{ .name = "use", .namespace = .svg },
};
// The baseline's own removeAttributes list is empty; `remove unsafe` instead
// walks every "event handler content attribute". We fold lightpanda's own
// handler set into HTML's list so that a handler added to `Handler` -- which is
// what an `on*` content attribute is compiled against -- can never be left
// behind by removeUnsafe().
pub const event_handler_attributes: []const []const u8 = blk: {
@setEvalBranchQuota(200_000);
const handlers = std.meta.fieldNames(global_event_handlers.Handler);
var all: [html_event_handler_attributes.len + handlers.len][]const u8 = undefined;
for (html_event_handler_attributes, 0..) |name, i| {
all[i] = name;
}
for (handlers, 0..) |name, i| {
all[html_event_handler_attributes.len + i] = name;
}
std.mem.sort([]const u8, &all, {}, struct {
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
return std.mem.lessThan(u8, a, b);
}
}.lessThan);
var unique: [all.len][]const u8 = undefined;
var len: usize = 0;
for (all) |name| {
if (len == 0 or std.mem.eql(u8, unique[len - 1], name) == false) {
unique[len] = name;
len += 1;
}
}
const final = unique[0..len].*;
break :blk &final;
};
const html_event_handler_attributes: []const []const u8 = &.{
"onabort",
"onactivate",
"onafterprint",
"onanimationcancel",
"onanimationend",
"onanimationiteration",
"onanimationstart",
"onautofill",
"onauxclick",
"onbeforecopy",
"onbeforecut",
"onbeforefilter",
"onbeforeinput",
"onbeforepaste",
"onbeforeprint",
"onbeforetoggle",
"onbeforeunload",
"onbegin",
"onblur",
"oncancel",
"oncanplay",
"oncanplaythrough",
"onchange",
"onclick",
"onclose",
"oncommand",
"oncontentvisibilityautostatechange",
"oncontextlost",
"oncontextmenu",
"oncontextrestored",
"oncopy",
"oncuechange",
"oncut",
"ondblclick",
"ondrag",
"ondragend",
"ondragenter",
"ondragleave",
"ondragover",
"ondragstart",
"ondrop",
"ondurationchange",
"onemptied",
"onend",
"onended",
"onerror",
"onfocus",
"onfocusin",
"onfocusout",
"onformdata",
"ongotpointercapture",
"onhashchange",
"oninput",
"oninstallresult",
"oninvalid",
"onkeydown",
"onkeypress",
"onkeyup",
"onlanguagechange",
"onload",
"onloadeddata",
"onloadedmetadata",
"onloadstart",
"onlocation",
"onlostpointercapture",
"onmessage",
"onmessageerror",
"onmousedown",
"onmouseenter",
"onmouseleave",
"onmousemove",
"onmouseout",
"onmouseover",
"onmouseup",
"onmousewheel",
"onmove",
"onoffline",
"ononline",
"onorientationchange",
"onpagehide",
"onpageshow",
"onpaste",
"onpause",
"onplay",
"onplaying",
"onpointercancel",
"onpointerdown",
"onpointerenter",
"onpointerleave",
"onpointermove",
"onpointerout",
"onpointerover",
"onpointerrawupdate",
"onpointerup",
"onpopstate",
"onprogress",
"onpromptaction",
"onpromptdismiss",
"onratechange",
"onrepeat",
"onreset",
"onresize",
"onscroll",
"onscrollend",
"onscrollsnapchange",
"onscrollsnapchanging",
"onsearch",
"onsecuritypolicyviolation",
"onseeked",
"onseeking",
"onselect",
"onselectionchange",
"onselectstart",
"onshow",
"onslotchange",
"onstalled",
"onstream",
"onstorage",
"onsubmit",
"onsuspend",
"ontimeupdate",
"ontimezonechange",
"ontoggle",
"ontouchcancel",
"ontouchend",
"ontouchmove",
"ontouchstart",
"ontransitionend",
"onunload",
"onvalidationstatuschange",
"onvolumechange",
"onwaiting",
"onwebkitanimationend",
"onwebkitanimationiteration",
"onwebkitanimationstart",
"onwebkitfullscreenchange",
"onwebkitfullscreenerror",
"onwebkittransitionend",
"onwheel",
};