Repository navigation
Expand file tree
/
Copy pathutils.js
More file actions
99 lines (93 loc) · 3.45 KB
/
Copy pathutils.js
File metadata and controls
99 lines (93 loc) · 3.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/*
* Shared helpers: HTML escaping/sanitizing, selector parsing, image cropping.
*/
export function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
const ALLOWED_TAGS = new Set([
'em', 'strong', 'i', 'b', 'u', 's', 'span', 'br', 'p', 'code',
'sub', 'sup', 'monogram', 'a', 'small', 'mark',
])
const ALLOWED_ATTRS = new Set(['decoration', 'shape', 'title', 'href'])
// Removed outright, with their subtree: unwrapping these would leak content
// (style/script text) or leave an embeddable vector behind.
const DROP_TAGS = new Set([
'script', 'style', 'template', 'iframe', 'object', 'embed', 'svg', 'math', 'noscript',
])
export function sanitizeHtml(html) {
const doc = new DOMParser().parseFromString(String(html ?? ''), 'text/html')
const walk = (node) => {
for (const child of [...node.childNodes]) {
if (child.nodeType !== Node.ELEMENT_NODE) continue
const tag = child.tagName.toLowerCase()
if (DROP_TAGS.has(tag)) {
child.remove()
continue
}
// Sanitize descendants first: unwrapping a disallowed wrapper promotes
// its children, and they must already be clean.
walk(child)
if (!ALLOWED_TAGS.has(tag)) {
child.replaceWith(...child.childNodes)
continue
}
for (const attr of [...child.attributes]) {
if (attr.name === 'href' && tag === 'a') {
if (!/^https?:\/\//i.test(attr.value)) child.removeAttribute('href')
} else if (!ALLOWED_ATTRS.has(attr.name) || attr.name.startsWith('on')) {
child.removeAttribute(attr.name)
}
}
}
}
walk(doc.body)
return doc.body.innerHTML
}
// The words of an HTML fragment without its tags, for labels a screen reader
// speaks.
export function plainText(html) {
return new DOMParser().parseFromString(sanitizeHtml(html), 'text/html').body.textContent
}
export function parseXYWH(selector) {
if (!selector) return null
// FragmentSelector values may carry a unit prefix: xywh=pixel:1,2,3,4
const m = String(selector).match(/xywh=(?:pixel:)?([\d.]+),([\d.]+),([\d.]+),([\d.]+)/)
if (!m) return null
return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3]), parseFloat(m[4])]
}
export function cropImage(imageUrl, canvasWidth, xywh) {
return new Promise((resolve) => {
let usedCors = true
const img = new Image()
img.onload = () => {
const [x, y, w, h] = xywh
const scale = img.naturalWidth / canvasWidth
const out = document.createElement('canvas')
out.width = Math.max(1, Math.round(w * scale))
out.height = Math.max(1, Math.round(h * scale))
const ctx = out.getContext('2d')
try {
ctx.drawImage(img, x * scale, y * scale, w * scale, h * scale, 0, 0, out.width, out.height)
resolve({ dataUrl: out.toDataURL(), tainted: false, canvas: out })
} catch {
resolve({ dataUrl: null, tainted: true, canvas: out })
}
}
img.onerror = () => {
if (!usedCors) return resolve({ dataUrl: null, tainted: true, canvas: null })
// Image hosts without Access-Control-Allow-Origin fail the CORS request.
// Retry plain: the canvas is tainted (no toDataURL, no PNG download) but
// still displays the crop.
usedCors = false
img.removeAttribute('crossorigin')
img.src = imageUrl
}
img.crossOrigin = 'anonymous'
img.src = imageUrl
})
}