@bendyline/gezel 0.1.0 → 1.0.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/{index-D_dch9Qh.d.ts → index-BtxY_EhX.d.ts} +185 -40
- package/dist/index.d.ts +136 -7
- package/dist/index.js +5781 -5169
- package/dist/markdown/index.d.ts +1 -1
- package/dist/markdown/index.js +754 -662
- package/dist/paths.d.ts +16 -3
- package/dist/paths.js +54 -0
- package/dist/{report-action-DzdQHzGG.d.ts → report-action-DVQvpL-V.d.ts} +40 -22
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.js +5321 -5114
- package/dist/svg/index.d.ts +11 -0
- package/dist/svg/index.js +202 -0
- package/package.json +6 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduce arbitrary input to a small, inert, presentation-only SVG subset.
|
|
3
|
+
*
|
|
4
|
+
* The allowlist deliberately excludes style, links, image/use references,
|
|
5
|
+
* filters, animation, foreign content, and every event-bearing or namespaced
|
|
6
|
+
* attribute. The result is suitable for persistence and transport; render it
|
|
7
|
+
* as an image rather than inserting it into a live HTML DOM.
|
|
8
|
+
*/
|
|
9
|
+
declare function sanitizePresentationSvg(raw: string): string | null;
|
|
10
|
+
|
|
11
|
+
export { sanitizePresentationSvg };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/svg/sanitize.ts
|
|
2
|
+
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
|
3
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
4
|
+
var XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
5
|
+
var MAX_SVG_BYTES = 1e5;
|
|
6
|
+
var MAX_ELEMENTS = 2048;
|
|
7
|
+
var MAX_DEPTH = 32;
|
|
8
|
+
var MAX_ATTRIBUTE_VALUE = 4096;
|
|
9
|
+
var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
|
|
10
|
+
"svg",
|
|
11
|
+
"g",
|
|
12
|
+
"path",
|
|
13
|
+
"circle",
|
|
14
|
+
"ellipse",
|
|
15
|
+
"rect",
|
|
16
|
+
"line",
|
|
17
|
+
"polyline",
|
|
18
|
+
"polygon",
|
|
19
|
+
"defs",
|
|
20
|
+
"linearGradient",
|
|
21
|
+
"radialGradient",
|
|
22
|
+
"stop",
|
|
23
|
+
"clipPath",
|
|
24
|
+
"mask",
|
|
25
|
+
"title",
|
|
26
|
+
"desc"
|
|
27
|
+
]);
|
|
28
|
+
var DRAWABLE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
29
|
+
"path",
|
|
30
|
+
"circle",
|
|
31
|
+
"ellipse",
|
|
32
|
+
"rect",
|
|
33
|
+
"line",
|
|
34
|
+
"polyline",
|
|
35
|
+
"polygon"
|
|
36
|
+
]);
|
|
37
|
+
var ALLOWED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
38
|
+
"xmlns",
|
|
39
|
+
"viewBox",
|
|
40
|
+
"preserveAspectRatio",
|
|
41
|
+
"color",
|
|
42
|
+
"fill",
|
|
43
|
+
"fill-opacity",
|
|
44
|
+
"fill-rule",
|
|
45
|
+
"stroke",
|
|
46
|
+
"stroke-width",
|
|
47
|
+
"stroke-linecap",
|
|
48
|
+
"stroke-linejoin",
|
|
49
|
+
"stroke-miterlimit",
|
|
50
|
+
"stroke-dasharray",
|
|
51
|
+
"stroke-dashoffset",
|
|
52
|
+
"stroke-opacity",
|
|
53
|
+
"clip-rule",
|
|
54
|
+
"opacity",
|
|
55
|
+
"transform",
|
|
56
|
+
"vector-effect",
|
|
57
|
+
"paint-order",
|
|
58
|
+
"shape-rendering",
|
|
59
|
+
"d",
|
|
60
|
+
"points",
|
|
61
|
+
"x",
|
|
62
|
+
"y",
|
|
63
|
+
"x1",
|
|
64
|
+
"y1",
|
|
65
|
+
"x2",
|
|
66
|
+
"y2",
|
|
67
|
+
"cx",
|
|
68
|
+
"cy",
|
|
69
|
+
"r",
|
|
70
|
+
"rx",
|
|
71
|
+
"ry",
|
|
72
|
+
"width",
|
|
73
|
+
"height",
|
|
74
|
+
"offset",
|
|
75
|
+
"stop-color",
|
|
76
|
+
"stop-opacity",
|
|
77
|
+
"clip-path",
|
|
78
|
+
"mask",
|
|
79
|
+
"id",
|
|
80
|
+
"role",
|
|
81
|
+
"aria-hidden"
|
|
82
|
+
]);
|
|
83
|
+
function extractSvg(raw) {
|
|
84
|
+
if (!raw || raw.length > MAX_SVG_BYTES) return null;
|
|
85
|
+
if (/<!DOCTYPE|<!ENTITY/i.test(raw)) return null;
|
|
86
|
+
const start = raw.search(/<svg(?:\s|>)/);
|
|
87
|
+
if (start < 0) return null;
|
|
88
|
+
const end = raw.lastIndexOf("</svg>");
|
|
89
|
+
if (end < start) return null;
|
|
90
|
+
return raw.slice(start, end + "</svg>".length);
|
|
91
|
+
}
|
|
92
|
+
function hasUnsafeCharacters(value) {
|
|
93
|
+
for (const character of value) {
|
|
94
|
+
const code = character.codePointAt(0) ?? 0;
|
|
95
|
+
if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127) return true;
|
|
96
|
+
if (code >= 8234 && code <= 8238) return true;
|
|
97
|
+
if (code >= 8294 && code <= 8297) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
function isSafeLocalUrl(value) {
|
|
102
|
+
return /^url\(#[A-Za-z_][A-Za-z0-9_.:-]*\)$/.test(value.trim());
|
|
103
|
+
}
|
|
104
|
+
function attributeAllowed(element, root, name, value) {
|
|
105
|
+
if (!ALLOWED_ATTRIBUTES.has(name)) return false;
|
|
106
|
+
if (value.length > MAX_ATTRIBUTE_VALUE || hasUnsafeCharacters(value)) return false;
|
|
107
|
+
if (/url\s*\(/i.test(value) && !isSafeLocalUrl(value)) return false;
|
|
108
|
+
if (name === "id" && !/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(value)) return false;
|
|
109
|
+
if (name === "xmlns") {
|
|
110
|
+
return element === root && value === SVG_NAMESPACE;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
function sanitizeElement(element, root, depth, state) {
|
|
115
|
+
state.elements += 1;
|
|
116
|
+
if (state.elements > MAX_ELEMENTS || depth > MAX_DEPTH) {
|
|
117
|
+
state.invalid = true;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (element.localName && DRAWABLE_ELEMENTS.has(element.localName)) state.drawableElements += 1;
|
|
121
|
+
for (let index = element.attributes.length - 1; index >= 0; index -= 1) {
|
|
122
|
+
const attribute = element.attributes.item(index);
|
|
123
|
+
if (!attribute) continue;
|
|
124
|
+
const plainAttribute = !attribute.prefix && !attribute.namespaceURI;
|
|
125
|
+
const rootXmlns = element === root && attribute.name === "xmlns" && attribute.namespaceURI === XMLNS_NAMESPACE;
|
|
126
|
+
if (!plainAttribute && !rootXmlns || attribute.name !== attribute.localName || !attributeAllowed(element, root, attribute.localName, attribute.value)) {
|
|
127
|
+
element.removeAttributeNode(attribute);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let child = element.firstChild;
|
|
131
|
+
while (child) {
|
|
132
|
+
const next = child.nextSibling;
|
|
133
|
+
if (child.nodeType === 1) {
|
|
134
|
+
const childElement = child;
|
|
135
|
+
if (childElement.namespaceURI !== SVG_NAMESPACE || !!childElement.prefix || !childElement.localName || !ALLOWED_ELEMENTS.has(childElement.localName)) {
|
|
136
|
+
element.removeChild(child);
|
|
137
|
+
} else {
|
|
138
|
+
sanitizeElement(childElement, root, depth + 1, state);
|
|
139
|
+
}
|
|
140
|
+
} else if (child.nodeType === 3) {
|
|
141
|
+
const mayContainText = element.localName === "title" || element.localName === "desc";
|
|
142
|
+
if (!mayContainText && child.nodeValue?.trim()) element.removeChild(child);
|
|
143
|
+
} else {
|
|
144
|
+
element.removeChild(child);
|
|
145
|
+
}
|
|
146
|
+
child = next;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function removeBrokenLocalReferences(root) {
|
|
150
|
+
const ids = /* @__PURE__ */ new Set();
|
|
151
|
+
const visit = (element) => {
|
|
152
|
+
const id = element.getAttribute("id");
|
|
153
|
+
if (id) {
|
|
154
|
+
if (ids.has(id)) element.removeAttribute("id");
|
|
155
|
+
else ids.add(id);
|
|
156
|
+
}
|
|
157
|
+
for (let child = element.firstChild; child; child = child.nextSibling) {
|
|
158
|
+
if (child.nodeType === 1) visit(child);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
visit(root);
|
|
162
|
+
const validate = (element) => {
|
|
163
|
+
for (let index = element.attributes.length - 1; index >= 0; index -= 1) {
|
|
164
|
+
const attribute = element.attributes.item(index);
|
|
165
|
+
if (!attribute) continue;
|
|
166
|
+
const match = /^url\(#([A-Za-z_][A-Za-z0-9_.:-]*)\)$/.exec(attribute.value.trim());
|
|
167
|
+
if (match && !ids.has(match[1])) element.removeAttributeNode(attribute);
|
|
168
|
+
}
|
|
169
|
+
for (let child = element.firstChild; child; child = child.nextSibling) {
|
|
170
|
+
if (child.nodeType === 1) validate(child);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
validate(root);
|
|
174
|
+
}
|
|
175
|
+
function sanitizePresentationSvg(raw) {
|
|
176
|
+
const candidate = extractSvg(raw);
|
|
177
|
+
if (!candidate) return null;
|
|
178
|
+
let document;
|
|
179
|
+
try {
|
|
180
|
+
document = new DOMParser({
|
|
181
|
+
locator: false,
|
|
182
|
+
onError: (_level, message) => {
|
|
183
|
+
throw new Error(message);
|
|
184
|
+
}
|
|
185
|
+
}).parseFromString(candidate, "image/svg+xml");
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const root = document.documentElement;
|
|
190
|
+
if (!root || root.localName !== "svg" || root.namespaceURI !== SVG_NAMESPACE || !!root.prefix || root.parentNode !== document) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const state = { elements: 0, drawableElements: 0, invalid: false };
|
|
194
|
+
sanitizeElement(root, root, 0, state);
|
|
195
|
+
if (state.invalid || state.drawableElements === 0) return null;
|
|
196
|
+
removeBrokenLocalReferences(root);
|
|
197
|
+
const serialized = new XMLSerializer().serializeToString(root);
|
|
198
|
+
return serialized.length <= MAX_SVG_BYTES ? serialized : null;
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
sanitizePresentationSvg
|
|
202
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/gezel",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Gezel core: types, schemas, path helpers, agent-markdown parser.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -60,6 +60,10 @@
|
|
|
60
60
|
"./checks": {
|
|
61
61
|
"types": "./dist/checks/index.d.ts",
|
|
62
62
|
"import": "./dist/checks/index.js"
|
|
63
|
+
},
|
|
64
|
+
"./svg": {
|
|
65
|
+
"types": "./dist/svg/index.d.ts",
|
|
66
|
+
"import": "./dist/svg/index.js"
|
|
63
67
|
}
|
|
64
68
|
},
|
|
65
69
|
"files": [
|
|
@@ -67,6 +71,7 @@
|
|
|
67
71
|
"!dist/**/*.map"
|
|
68
72
|
],
|
|
69
73
|
"dependencies": {
|
|
74
|
+
"@xmldom/xmldom": "^0.9.10",
|
|
70
75
|
"yaml": "^2.9.0",
|
|
71
76
|
"zod": "^4.4.3"
|
|
72
77
|
},
|