@segnavia/html 0.37.0 → 0.38.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/dist/book/book-contents.js +33 -0
- package/dist/book/book-document-nodes.js +30 -0
- package/dist/book/book-document.d.ts +2 -0
- package/dist/book/book-document.js +150 -154
- package/dist/book/book-flow.js +23 -7
- package/dist/book/book-held.d.ts +4 -0
- package/dist/book/book-held.js +60 -7
- package/dist/book/book-issues.js +24 -0
- package/dist/book/book-part-build.js +50 -0
- package/dist/book/dom.d.ts +1 -0
- package/dist/book/notes.d.ts +2 -0
- package/dist/book/notes.js +101 -47
- package/dist/book/paginate-book.d.ts +2 -1
- package/dist/book/paginate-book.js +34 -26
- package/dist/book/paginate-sequence.d.ts +1 -1
- package/dist/book/paginate-sequence.js +60 -28
- package/dist/book/paginate-types.d.ts +10 -5
- package/dist/book/region-fit.d.ts +16 -0
- package/dist/book/region-fit.js +111 -0
- package/dist/book/types.d.ts +4 -2
- package/dist/book/types.js +2 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/motifs.d.ts +2 -0
- package/dist/motifs.js +5 -2
- package/dist/printed-pages.d.ts +2 -7
- package/dist/printed-pages.js +1 -1
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +78 -43
- package/package.json +2 -2
package/dist/sanitize.js
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { sanitizeCss } from "./sanitize-css.js";
|
|
2
2
|
import { isTag } from "domhandler";
|
|
3
|
+
import { dependencyDecision, dependencyOriginSet, originSet } from "@segnavia/format";
|
|
3
4
|
import render from "dom-serializer";
|
|
4
5
|
import { parseDocument } from "htmlparser2";
|
|
5
6
|
//#region src/sanitize.ts
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
const REFUSED = {
|
|
8
|
+
script: "always",
|
|
9
|
+
iframe: "always",
|
|
10
|
+
frame: "always",
|
|
11
|
+
frameset: "always",
|
|
12
|
+
object: "always",
|
|
13
|
+
embed: "always",
|
|
14
|
+
applet: "always",
|
|
15
|
+
base: "always",
|
|
16
|
+
portal: "always",
|
|
17
|
+
link: "always",
|
|
18
|
+
form: "unless-scripts",
|
|
19
|
+
input: "unless-scripts",
|
|
20
|
+
button: "unless-scripts",
|
|
21
|
+
select: "unless-scripts",
|
|
22
|
+
textarea: "unless-scripts"
|
|
23
|
+
};
|
|
24
|
+
const REFUSED_ELEMENTS = new Set(Object.keys(REFUSED));
|
|
23
25
|
const REFUSED_SVG_ELEMENTS = /* @__PURE__ */ new Set([
|
|
24
26
|
"script",
|
|
25
27
|
"animate",
|
|
@@ -78,14 +80,6 @@ function insideSvg(element) {
|
|
|
78
80
|
function normalizeUrl(value) {
|
|
79
81
|
return [...value].filter((c) => c.charCodeAt(0) > 32 && c.charCodeAt(0) !== 127).join("").toLowerCase();
|
|
80
82
|
}
|
|
81
|
-
function origins(values) {
|
|
82
|
-
const set = /* @__PURE__ */ new Set();
|
|
83
|
-
for (const value of values ?? []) try {
|
|
84
|
-
const url = new URL(value);
|
|
85
|
-
if (url.protocol === "https:") set.add(url.origin.toLowerCase());
|
|
86
|
-
} catch {}
|
|
87
|
-
return set;
|
|
88
|
-
}
|
|
89
83
|
function safeUrl(attribute, value, allowed) {
|
|
90
84
|
const url = normalizeUrl(value);
|
|
91
85
|
if (attribute === "href" || attribute === "xlink:href") {
|
|
@@ -104,68 +98,109 @@ function safeUrl(attribute, value, allowed) {
|
|
|
104
98
|
function refusedElement(element, refused, keepScripts) {
|
|
105
99
|
const name = element.name.toLowerCase();
|
|
106
100
|
if (name === "script") return !keepScripts;
|
|
101
|
+
if (name === "link" && keepScripts && element.attribs.rel?.toLowerCase() === "stylesheet") return false;
|
|
107
102
|
if (refused.has(name)) return true;
|
|
108
103
|
if (insideSvg(element) && REFUSED_SVG_ELEMENTS.has(name)) return true;
|
|
109
104
|
return name === "meta" && Object.keys(element.attribs).some((attribute) => attribute.toLowerCase() === "http-equiv");
|
|
110
105
|
}
|
|
111
|
-
function
|
|
106
|
+
function dependencyAttribute(element, attribute, value, options, dependencies) {
|
|
107
|
+
if (!options.keepScripts) return void 0;
|
|
108
|
+
const tag = element.name.toLowerCase();
|
|
109
|
+
if (!(tag === "script" && attribute === "src" || tag === "link" && element.attribs.rel?.toLowerCase() === "stylesheet" && attribute === "href")) return void 0;
|
|
110
|
+
const decision = dependencyDecision(value, dependencies);
|
|
111
|
+
return decision.allowed ? "" : decision.reason;
|
|
112
|
+
}
|
|
113
|
+
function refusedAttribute(element, name, value, allowed, options, dependencies) {
|
|
112
114
|
const attribute = name.toLowerCase();
|
|
113
|
-
if (attribute.startsWith("on")) return
|
|
114
|
-
|
|
115
|
+
if (attribute.startsWith("on")) return options.keepScripts ? void 0 : "";
|
|
116
|
+
const dependency = dependencyAttribute(element, attribute, value, options, dependencies);
|
|
117
|
+
if (dependency !== void 0) return dependency || void 0;
|
|
118
|
+
return REFUSED_ATTRIBUTES.has(attribute) || URL_ATTRIBUTES.has(attribute) && !safeUrl(attribute, value, allowed) ? "" : void 0;
|
|
115
119
|
}
|
|
116
120
|
function refusedSet(options) {
|
|
117
|
-
|
|
121
|
+
const defaults = options.keepScripts ? Object.entries(REFUSED).filter(([, rule]) => rule === "always").map(([name]) => name) : REFUSED_ELEMENTS;
|
|
122
|
+
return new Set([...defaults, ...options.refuseElements ?? []].map((name) => name.toLowerCase()));
|
|
118
123
|
}
|
|
119
124
|
function styleText(element) {
|
|
120
125
|
return element.children.map((child) => "data" in child ? child.data : "").join("");
|
|
121
126
|
}
|
|
122
|
-
function
|
|
127
|
+
function dependencyCss(source, declarations, options, dependencies) {
|
|
128
|
+
if (!options.keepScripts) return sanitizeCss(source, declarations, options.dataFonts ?? false);
|
|
129
|
+
const restores = [];
|
|
130
|
+
const hold = (url) => {
|
|
131
|
+
if (!dependencyDecision(url, dependencies).allowed) return url;
|
|
132
|
+
return `data:image/segnavia-dependency-${restores.push(url) - 1},x`;
|
|
133
|
+
};
|
|
134
|
+
let marker = "segnavia-held-import";
|
|
135
|
+
while (source.includes(marker)) marker += "-x";
|
|
136
|
+
const imports = [];
|
|
137
|
+
let protectedCss = source.replace(/@import\s+(?:url\(\s*)?["']?(https:\/\/[^\s"')]+)["']?\s*\)?[^;]*;/gi, (rule, url) => {
|
|
138
|
+
if (hold(url) === url) return rule;
|
|
139
|
+
const index = imports.push(rule) - 1;
|
|
140
|
+
return `.${marker}-${index}{}`;
|
|
141
|
+
});
|
|
142
|
+
protectedCss = protectedCss.replace(/@font-face\s*\{[^}]*\}/gi, (block) => block.replace(/https:\/\/[^\s"')]+/gi, hold));
|
|
143
|
+
const result = sanitizeCss(protectedCss, declarations, options.dataFonts ?? false);
|
|
144
|
+
const restore = (value) => value.replace(/data:image\/segnavia-dependency-(\d+),x/g, (_, index) => restores[Number(index)] ?? "");
|
|
145
|
+
let css = restore(result.css);
|
|
146
|
+
for (const [index, rule] of imports.entries()) css = css.replace(`.${marker}-${index}{}`, rule);
|
|
147
|
+
return {
|
|
148
|
+
css,
|
|
149
|
+
removed: result.removed.map(restore)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function styleFindings(element, options, dependencies) {
|
|
123
153
|
const found = [];
|
|
124
|
-
if (element.name.toLowerCase() === "style") for (const construct of
|
|
154
|
+
if (element.name.toLowerCase() === "style") for (const construct of dependencyCss(styleText(element), false, options, dependencies).removed) found.push(`<style> ${construct}`);
|
|
125
155
|
for (const [name, value] of Object.entries(element.attribs)) {
|
|
126
156
|
if (name.toLowerCase() !== "style") continue;
|
|
127
|
-
for (const construct of
|
|
157
|
+
for (const construct of dependencyCss(value, true, options, dependencies).removed) found.push(`${element.name}[${name}] ${construct}`);
|
|
128
158
|
}
|
|
129
159
|
return found;
|
|
130
160
|
}
|
|
131
161
|
function findActiveContent(html, options = {}) {
|
|
132
|
-
const allowed =
|
|
162
|
+
const allowed = originSet(options.linkOrigins);
|
|
163
|
+
const dependencies = dependencyOriginSet(options.dependencyOrigins);
|
|
133
164
|
const refused = refusedSet(options);
|
|
134
165
|
const keepScripts = options.keepScripts ?? false;
|
|
135
166
|
const found = [];
|
|
136
167
|
for (const element of elements(parse(html).children)) {
|
|
137
168
|
if (refusedElement(element, refused, keepScripts)) found.push(`<${element.name}>`);
|
|
138
|
-
found.push(...styleFindings(element, options
|
|
139
|
-
for (const [name, value] of Object.entries(element.attribs))
|
|
169
|
+
found.push(...styleFindings(element, options, dependencies));
|
|
170
|
+
for (const [name, value] of Object.entries(element.attribs)) {
|
|
171
|
+
const reason = refusedAttribute(element, name, value, allowed, options, dependencies);
|
|
172
|
+
if (reason !== void 0) found.push(`${element.name}[${name}]${reason ? ` ${reason}` : ""}`);
|
|
173
|
+
}
|
|
140
174
|
}
|
|
141
175
|
return found;
|
|
142
176
|
}
|
|
143
|
-
function cleanStyleElement(element,
|
|
177
|
+
function cleanStyleElement(element, options, dependencies) {
|
|
144
178
|
if (element.name.toLowerCase() !== "style") return;
|
|
145
179
|
const children = element.children.filter((candidate) => "data" in candidate);
|
|
146
180
|
const child = children[0];
|
|
147
181
|
if (!child || !("data" in child)) return;
|
|
148
|
-
child.data =
|
|
182
|
+
child.data = dependencyCss(styleText(element), false, options, dependencies).css;
|
|
149
183
|
for (const extra of children.slice(1)) if ("data" in extra) extra.data = "";
|
|
150
184
|
}
|
|
151
|
-
function cleanAttributes(element, allowed,
|
|
152
|
-
for (const [name, value] of Object.entries(element.attribs)) if (refusedAttribute(name, value, allowed,
|
|
185
|
+
function cleanAttributes(element, allowed, options, dependencies) {
|
|
186
|
+
for (const [name, value] of Object.entries(element.attribs)) if (refusedAttribute(element, name, value, allowed, options, dependencies) !== void 0) delete element.attribs[name];
|
|
153
187
|
else if (name.toLowerCase() === "style") {
|
|
154
|
-
const css =
|
|
188
|
+
const css = dependencyCss(value, true, options, dependencies).css.trim();
|
|
155
189
|
if (css) element.attribs[name] = css;
|
|
156
190
|
else delete element.attribs[name];
|
|
157
191
|
}
|
|
158
192
|
}
|
|
159
193
|
function sanitize(html, options = {}) {
|
|
160
|
-
const allowed =
|
|
194
|
+
const allowed = originSet(options.linkOrigins);
|
|
195
|
+
const dependencies = dependencyOriginSet(options.dependencyOrigins);
|
|
161
196
|
const refused = refusedSet(options);
|
|
162
197
|
const keepScripts = options.keepScripts ?? false;
|
|
163
198
|
const document = parse(html);
|
|
164
199
|
const prune = (nodes) => nodes.filter((node) => {
|
|
165
200
|
if (!isTag(node)) return true;
|
|
166
201
|
if (refusedElement(node, refused, keepScripts)) return false;
|
|
167
|
-
cleanStyleElement(node, options
|
|
168
|
-
cleanAttributes(node, allowed,
|
|
202
|
+
cleanStyleElement(node, options, dependencies);
|
|
203
|
+
cleanAttributes(node, allowed, options, dependencies);
|
|
169
204
|
node.children = prune(node.children);
|
|
170
205
|
return true;
|
|
171
206
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@segnavia/format": "0.
|
|
29
|
+
"@segnavia/format": "0.38.0",
|
|
30
30
|
"dom-serializer": "^2.0.0",
|
|
31
31
|
"domhandler": "^5.0.3",
|
|
32
32
|
"htmlparser2": "^10.0.0",
|