@readium/navigator-html-injectables 2.4.4 → 2.6.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/Loader.js +1 -0
- package/dist/comms/comms.js +1 -0
- package/dist/comms/mid.js +1 -0
- package/dist/helpers/animation.js +1 -0
- package/dist/helpers/css.js +1 -0
- package/dist/helpers/document.js +1 -0
- package/dist/helpers/dom.js +1 -0
- package/dist/helpers/locator.js +1 -0
- package/dist/helpers/rect.js +1 -0
- package/dist/helpers/sanitize.js +1 -0
- package/dist/index.js +1 -4174
- package/dist/keyboard/KeyCombinationManager.js +1 -0
- package/dist/keyboard/KeyboardCombinations.js +1 -0
- package/dist/modules/Decorator.js +24 -0
- package/dist/modules/Module.js +1 -0
- package/dist/modules/ModuleLibrary.js +1 -0
- package/dist/modules/Peripherals.js +1 -0
- package/dist/modules/setup/FixedSetup.js +13 -0
- package/dist/modules/setup/ReflowableSetup.js +1 -0
- package/dist/modules/setup/Setup.js +1 -0
- package/dist/modules/setup/WebPubSetup.js +1 -0
- package/dist/modules/snapper/CJKVerticalSnapper.js +12 -0
- package/dist/modules/snapper/ColumnSnapper.js +35 -0
- package/dist/modules/snapper/ScrollSnapper.js +9 -0
- package/dist/modules/snapper/Snapper.js +5 -0
- package/dist/modules/snapper/WebPubSnapper.js +1 -0
- package/dist/protection/BulkCopyProtector.js +1 -0
- package/dist/protection/PatternAnalyzer.js +1 -0
- package/dist/protection/PrintProtector.js +15 -0
- package/dist/protection/SelectionAnalyzer.js +1 -0
- package/dist/protection/config.js +1 -0
- package/dist/vendor/approx-string-match/index.js +1 -0
- package/dist/vendor/hypothesis/anchoring/match-quote.js +1 -0
- package/dist/vendor/hypothesis/anchoring/text-range.js +1 -0
- package/dist/vendor/hypothesis/anchoring/trim-range.js +1 -0
- package/dist/vendor/hypothesis/anchoring/types.js +1 -0
- package/package.json +9 -7
- package/src/comms/comms.ts +11 -1
- package/src/comms/keys.ts +8 -3
- package/src/helpers/css.ts +30 -1
- package/src/helpers/document.ts +123 -3
- package/src/helpers/locator.ts +5 -5
- package/src/helpers/rect.ts +88 -22
- package/src/helpers/sanitize.ts +77 -0
- package/src/index.ts +2 -1
- package/src/modules/Decorator.ts +924 -178
- package/src/modules/Peripherals.ts +3 -0
- package/src/modules/snapper/CJKVerticalSnapper.ts +34 -7
- package/src/modules/snapper/ColumnSnapper.ts +56 -7
- package/src/modules/snapper/ScrollSnapper.ts +30 -7
- package/src/modules/snapper/Snapper.ts +144 -0
- package/src/modules/snapper/WebPubSnapper.ts +31 -7
- package/types/src/comms/comms.d.ts +10 -1
- package/types/src/comms/keys.d.ts +2 -2
- package/types/src/helpers/css.d.ts +7 -1
- package/types/src/helpers/document.d.ts +69 -3
- package/types/src/helpers/rect.d.ts +2 -1
- package/types/src/helpers/sanitize.d.ts +13 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/modules/Decorator.d.ts +92 -17
- package/types/src/modules/Peripherals.d.ts +3 -0
- package/types/src/modules/snapper/CJKVerticalSnapper.d.ts +1 -0
- package/types/src/modules/snapper/ColumnSnapper.d.ts +14 -1
- package/types/src/modules/snapper/ScrollSnapper.d.ts +1 -0
- package/types/src/modules/snapper/Snapper.d.ts +58 -0
- package/types/src/modules/snapper/WebPubSnapper.d.ts +1 -0
- package/dist/ar-DyHX_uy2.js +0 -7
- package/dist/da-Dct0PS3E.js +0 -7
- package/dist/fr-C5HEel98.js +0 -7
- package/dist/index.umd.cjs +0 -95
- package/dist/it-DFOBoXGy.js +0 -7
- package/dist/pt_PT-Di3sVjze.js +0 -7
- package/dist/sv-BfzAFsVN.js +0 -7
- package/src/helpers/color.ts +0 -205
- package/types/src/helpers/color.d.ts +0 -26
package/src/helpers/document.ts
CHANGED
|
@@ -1,19 +1,139 @@
|
|
|
1
1
|
import { ReadiumWindow } from "./dom.ts";
|
|
2
2
|
|
|
3
|
-
export function isRTL(wnd:
|
|
3
|
+
export function isRTL(wnd: Window): boolean {
|
|
4
4
|
// Check documentElement first, then fall back to body.
|
|
5
5
|
const dir = wnd.document.documentElement.dir || wnd.document.body.dir;
|
|
6
6
|
return dir.toLowerCase() === "rtl";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function isVerticalLR(wnd:
|
|
9
|
+
export function isVerticalLR(wnd: Window): boolean {
|
|
10
10
|
// Check documentElement first, then fall back to body.
|
|
11
11
|
const writingMode = wnd.getComputedStyle(wnd.document.documentElement).writingMode
|
|
12
12
|
|| wnd.getComputedStyle(wnd.document.body).writingMode;
|
|
13
13
|
return writingMode === 'vertical-lr';
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function
|
|
16
|
+
export function isVerticalWriting(wnd: Window): boolean {
|
|
17
|
+
const writingMode = wnd.getComputedStyle(wnd.document.documentElement).writingMode
|
|
18
|
+
|| wnd.getComputedStyle(wnd.document.body).writingMode;
|
|
19
|
+
return writingMode === 'vertical-rl' || writingMode === 'vertical-lr';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Axis-normalizing context for decoration layout.
|
|
24
|
+
*
|
|
25
|
+
* Translates CSS physical properties into logical inline/block terms so that
|
|
26
|
+
* callers can position elements identically regardless of writing mode:
|
|
27
|
+
*
|
|
28
|
+
* horizontal writing → inline = horizontal, block = vertical
|
|
29
|
+
* vertical writing → inline = vertical, block = horizontal
|
|
30
|
+
*/
|
|
31
|
+
export interface WritingContext {
|
|
32
|
+
isVertical: boolean;
|
|
33
|
+
isVertLR: boolean;
|
|
34
|
+
/** Viewport size along the inline axis (innerWidth / innerHeight). */
|
|
35
|
+
viewportInlineSize: number;
|
|
36
|
+
/** Viewport size along the block axis (innerHeight / innerWidth). */
|
|
37
|
+
viewportBlockSize: number;
|
|
38
|
+
/**
|
|
39
|
+
* Size of a single page along the inline axis.
|
|
40
|
+
* Horizontal: viewportWidth / columnCount.
|
|
41
|
+
* Vertical: viewportHeight (no columns).
|
|
42
|
+
*/
|
|
43
|
+
pageInlineSize: number;
|
|
44
|
+
/**
|
|
45
|
+
* Physical x offset to add to a client-space x coordinate to get document space.
|
|
46
|
+
* Horizontal: scrollLeft (non-negative).
|
|
47
|
+
* Vertical-rl: scrollWidth − viewportWidth + scrollLeft (scrollLeft is negative,
|
|
48
|
+
* so this normalises it to the viewport's left edge in document coordinates).
|
|
49
|
+
* Vertical-lr: scrollLeft (non-negative, same as horizontal).
|
|
50
|
+
*/
|
|
51
|
+
xDocOffset: number;
|
|
52
|
+
/** Physical y offset to add to a client-space y coordinate to get document space (scrollTop). */
|
|
53
|
+
yDocOffset: number;
|
|
54
|
+
/** Scroll offset along the inline axis, derived from xDocOffset / yDocOffset. */
|
|
55
|
+
inlineScrollOffset: number;
|
|
56
|
+
/** Scroll offset along the block axis, derived from xDocOffset / yDocOffset. */
|
|
57
|
+
blockScrollOffset: number;
|
|
58
|
+
|
|
59
|
+
/** Logical inline start of a rect (left / top). */
|
|
60
|
+
inlineStart(r: DOMRect | { top: number; left: number }): number;
|
|
61
|
+
/** Logical block start of a rect (top / left). */
|
|
62
|
+
blockStart(r: DOMRect | { top: number; left: number }): number;
|
|
63
|
+
/** Logical inline size of a rect (width / height). */
|
|
64
|
+
inlineSize(r: DOMRect | { width: number; height: number }): number;
|
|
65
|
+
/** Logical block size of a rect (height / width). */
|
|
66
|
+
blockSize(r: DOMRect | { width: number; height: number }): number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set absolute position on an element using logical coordinates.
|
|
70
|
+
* `iz` is the inverse zoom factor (1 in non-Blink, 1/zoom in Blink).
|
|
71
|
+
*/
|
|
72
|
+
applyPosition(el: HTMLElement, inlineStart: number, blockStart: number, inlineSize: number, blockSize: number, iz: number): void;
|
|
73
|
+
|
|
74
|
+
/** Convert logical coordinates back to a physical DOMRect. */
|
|
75
|
+
toRect(inlineStart: number, blockStart: number, inlineSize: number, blockSize: number): DOMRect;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function makeWritingContext(wnd: Window): WritingContext {
|
|
79
|
+
const isVert = isVerticalWriting(wnd);
|
|
80
|
+
const isVLR = isVert && isVerticalLR(wnd);
|
|
81
|
+
const vw = wnd.innerWidth;
|
|
82
|
+
const vh = wnd.innerHeight;
|
|
83
|
+
const se = wnd.document.scrollingElement!;
|
|
84
|
+
const xOff = se.scrollLeft; // negative for vertical-rl, non-negative otherwise
|
|
85
|
+
const yOff = se.scrollTop;
|
|
86
|
+
const cols = parseInt(wnd.getComputedStyle(wnd.document.documentElement).getPropertyValue("column-count"));
|
|
87
|
+
|
|
88
|
+
// In vertical-rl, scrollLeft starts at 0 (document start = right side) and goes
|
|
89
|
+
// negative as the reader scrolls left. The viewport's left edge in document space
|
|
90
|
+
// is therefore (scrollWidth − viewportWidth + scrollLeft). For vertical-lr and
|
|
91
|
+
// horizontal modes, scrollLeft is non-negative and is the offset directly.
|
|
92
|
+
const xDocOffset = (isVert && !isVLR)
|
|
93
|
+
? se.scrollWidth - vw + xOff
|
|
94
|
+
: xOff;
|
|
95
|
+
const yDocOffset = yOff;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
isVertical: isVert,
|
|
99
|
+
isVertLR: isVLR,
|
|
100
|
+
viewportInlineSize: isVert ? vh : vw,
|
|
101
|
+
viewportBlockSize: isVert ? vw : vh,
|
|
102
|
+
pageInlineSize: isVert ? vh : vw / (cols || 1),
|
|
103
|
+
xDocOffset,
|
|
104
|
+
yDocOffset,
|
|
105
|
+
inlineScrollOffset: isVert ? yDocOffset : xDocOffset,
|
|
106
|
+
blockScrollOffset: isVert ? xDocOffset : yDocOffset,
|
|
107
|
+
|
|
108
|
+
inlineStart: (r) => isVert ? r.top : r.left,
|
|
109
|
+
blockStart: (r) => isVert ? r.left : r.top,
|
|
110
|
+
inlineSize: (r) => isVert ? r.height : r.width,
|
|
111
|
+
blockSize: (r) => isVert ? r.width : r.height,
|
|
112
|
+
|
|
113
|
+
applyPosition(el, inlineStart, blockStart, inlineSize, blockSize, iz) {
|
|
114
|
+
el.style.position = "absolute";
|
|
115
|
+
if (isVert) {
|
|
116
|
+
el.style.top = `${inlineStart * iz}px`;
|
|
117
|
+
el.style.left = `${blockStart * iz}px`;
|
|
118
|
+
el.style.height = `${inlineSize * iz}px`;
|
|
119
|
+
el.style.width = `${blockSize * iz}px`;
|
|
120
|
+
} else {
|
|
121
|
+
el.style.left = `${inlineStart * iz}px`;
|
|
122
|
+
el.style.top = `${blockStart * iz}px`;
|
|
123
|
+
el.style.width = `${inlineSize * iz}px`;
|
|
124
|
+
el.style.height = `${blockSize * iz}px`;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
toRect(inlineStart, blockStart, inlineSize, blockSize) {
|
|
129
|
+
return isVert
|
|
130
|
+
? new DOMRect(blockStart, inlineStart, blockSize, inlineSize)
|
|
131
|
+
: new DOMRect(inlineStart, blockStart, inlineSize, blockSize);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getColumnCountPerScreen(wnd: Window): number {
|
|
17
137
|
return parseInt(
|
|
18
138
|
wnd.getComputedStyle(
|
|
19
139
|
wnd.document.documentElement
|
package/src/helpers/locator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Locator } from "@readium/shared";
|
|
1
|
+
import { getCssSelector, Locator } from "@readium/shared";
|
|
2
2
|
import { TextQuoteAnchor } from "../vendor/hypothesis/anchoring/types.ts";
|
|
3
3
|
|
|
4
4
|
function isReplacedLikeElement(element: Element): boolean {
|
|
@@ -13,8 +13,8 @@ export function rangeFromLocator(doc: Document, locator: Locator) {
|
|
|
13
13
|
const text = locator.text;
|
|
14
14
|
if (text && text.highlight) {
|
|
15
15
|
let root;
|
|
16
|
-
if (locations &&
|
|
17
|
-
root = doc.querySelector(
|
|
16
|
+
if (locations && getCssSelector(locations)) {
|
|
17
|
+
root = doc.querySelector(getCssSelector(locations)!);
|
|
18
18
|
}
|
|
19
19
|
if (!root) {
|
|
20
20
|
root = doc.body;
|
|
@@ -36,8 +36,8 @@ export function rangeFromLocator(doc: Document, locator: Locator) {
|
|
|
36
36
|
if (locations) {
|
|
37
37
|
let element = null;
|
|
38
38
|
|
|
39
|
-
if (!element &&
|
|
40
|
-
element = doc.querySelector(
|
|
39
|
+
if (!element && getCssSelector(locations)) {
|
|
40
|
+
element = doc.querySelector(getCssSelector(locations)!);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if (!element && locations.fragments) {
|
package/src/helpers/rect.ts
CHANGED
|
@@ -10,33 +10,96 @@ export interface Rect {
|
|
|
10
10
|
width: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function getTextClientRects(range: Range, skipTags: string[]): Rect[] {
|
|
14
|
+
const upperTags = skipTags.map(t => t.toUpperCase());
|
|
15
|
+
const rects: Rect[] = [];
|
|
16
|
+
|
|
17
|
+
const ancestor = range.commonAncestorContainer;
|
|
18
|
+
// TreeWalker only visits descendants, never the root itself. If the common
|
|
19
|
+
// ancestor is already a text node (single-node selection), walk from its
|
|
20
|
+
// parent so the text node itself is included in the traversal.
|
|
21
|
+
const walkerRoot = ancestor.nodeType === Node.TEXT_NODE ? ancestor.parentNode! : ancestor;
|
|
22
|
+
const walker = range.startContainer.ownerDocument!.createTreeWalker(
|
|
23
|
+
walkerRoot,
|
|
24
|
+
NodeFilter.SHOW_TEXT
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
let node = walker.nextNode();
|
|
28
|
+
while (node) {
|
|
29
|
+
if (range.intersectsNode(node)) {
|
|
30
|
+
let ancestor = node.parentNode;
|
|
31
|
+
let skip = false;
|
|
32
|
+
while (ancestor) {
|
|
33
|
+
if (ancestor.nodeType === Node.ELEMENT_NODE && upperTags.includes((ancestor as Element).tagName.toUpperCase())) {
|
|
34
|
+
skip = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
ancestor = ancestor.parentNode;
|
|
38
|
+
}
|
|
39
|
+
if (!skip) {
|
|
40
|
+
const textRange = range.cloneRange();
|
|
41
|
+
textRange.selectNode(node);
|
|
42
|
+
if (textRange.compareBoundaryPoints(Range.START_TO_START, range) < 0)
|
|
43
|
+
textRange.setStart(range.startContainer, range.startOffset);
|
|
44
|
+
if (textRange.compareBoundaryPoints(Range.END_TO_END, range) > 0)
|
|
45
|
+
textRange.setEnd(range.endContainer, range.endOffset);
|
|
46
|
+
for (const r of textRange.getClientRects()) {
|
|
47
|
+
rects.push({ left: r.left, right: r.right, top: r.top, bottom: r.bottom, width: r.width, height: r.height });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
node = walker.nextNode();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return rects;
|
|
55
|
+
}
|
|
56
|
+
|
|
13
57
|
export function getClientRectsNoOverlap(
|
|
14
|
-
|
|
15
|
-
doNotMergeHorizontallyAlignedRects: boolean
|
|
58
|
+
source: Range | Rect[],
|
|
59
|
+
doNotMergeHorizontallyAlignedRects: boolean,
|
|
60
|
+
doNotMergeVerticallyAlignedRects: boolean = false,
|
|
61
|
+
expand: number = 0
|
|
16
62
|
) {
|
|
17
|
-
|
|
63
|
+
const tolerance = 1;
|
|
64
|
+
let originalRects: Rect[];
|
|
18
65
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
66
|
+
if (Array.isArray(source)) {
|
|
67
|
+
originalRects = source;
|
|
68
|
+
} else {
|
|
69
|
+
let clientRects = source.getClientRects();
|
|
23
70
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
71
|
+
// Try falling back to the client rects of the common ancestor of the range if it's an HTML Element
|
|
72
|
+
if(!clientRects.length)
|
|
73
|
+
if(source.commonAncestorContainer.nodeType === Node.ELEMENT_NODE)
|
|
74
|
+
clientRects = (source.commonAncestorContainer as HTMLElement).getClientRects();
|
|
75
|
+
|
|
76
|
+
originalRects = [];
|
|
77
|
+
for (const rangeClientRect of clientRects) {
|
|
78
|
+
originalRects.push({
|
|
79
|
+
bottom: rangeClientRect.bottom,
|
|
80
|
+
height: rangeClientRect.height,
|
|
81
|
+
left: rangeClientRect.left,
|
|
82
|
+
right: rangeClientRect.right,
|
|
83
|
+
top: rangeClientRect.top,
|
|
84
|
+
width: rangeClientRect.width,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (expand) {
|
|
89
|
+
for (const rect of originalRects) {
|
|
90
|
+
rect.left -= expand;
|
|
91
|
+
rect.top -= expand;
|
|
92
|
+
rect.right += expand;
|
|
93
|
+
rect.bottom += expand;
|
|
94
|
+
rect.width += expand * 2;
|
|
95
|
+
rect.height += expand * 2;
|
|
96
|
+
}
|
|
35
97
|
}
|
|
36
98
|
const mergedRects = mergeTouchingRects(
|
|
37
99
|
originalRects,
|
|
38
100
|
tolerance,
|
|
39
|
-
doNotMergeHorizontallyAlignedRects
|
|
101
|
+
doNotMergeHorizontallyAlignedRects,
|
|
102
|
+
doNotMergeVerticallyAlignedRects
|
|
40
103
|
);
|
|
41
104
|
const noContainedRects = removeContainedRects(mergedRects, tolerance);
|
|
42
105
|
const newRects = replaceOverlapingRects(noContainedRects);
|
|
@@ -61,7 +124,8 @@ export function getClientRectsNoOverlap(
|
|
|
61
124
|
function mergeTouchingRects(
|
|
62
125
|
rects: Rect[],
|
|
63
126
|
tolerance: number,
|
|
64
|
-
doNotMergeHorizontallyAlignedRects: boolean
|
|
127
|
+
doNotMergeHorizontallyAlignedRects: boolean,
|
|
128
|
+
doNotMergeVerticallyAlignedRects: boolean = false
|
|
65
129
|
): Rect[] {
|
|
66
130
|
for (let i = 0; i < rects.length; i++) {
|
|
67
131
|
for (let j = i + 1; j < rects.length; j++) {
|
|
@@ -78,8 +142,9 @@ function mergeTouchingRects(
|
|
|
78
142
|
almostEqual(rect1.left, rect2.left, tolerance) &&
|
|
79
143
|
almostEqual(rect1.right, rect2.right, tolerance);
|
|
80
144
|
const horizontalAllowed = !doNotMergeHorizontallyAlignedRects;
|
|
145
|
+
const verticalAllowed = !doNotMergeVerticallyAlignedRects;
|
|
81
146
|
const aligned =
|
|
82
|
-
(rectsLineUpHorizontally && horizontalAllowed) ||
|
|
147
|
+
(rectsLineUpHorizontally && horizontalAllowed && verticalAllowed) ||
|
|
83
148
|
(rectsLineUpVertically && !rectsLineUpHorizontally);
|
|
84
149
|
const canMerge = aligned && rectsTouchOrOverlap(rect1, rect2, tolerance);
|
|
85
150
|
if (canMerge) {
|
|
@@ -94,7 +159,8 @@ function mergeTouchingRects(
|
|
|
94
159
|
return mergeTouchingRects(
|
|
95
160
|
newRects,
|
|
96
161
|
tolerance,
|
|
97
|
-
doNotMergeHorizontallyAlignedRects
|
|
162
|
+
doNotMergeHorizontallyAlignedRects,
|
|
163
|
+
doNotMergeVerticallyAlignedRects
|
|
98
164
|
);
|
|
99
165
|
}
|
|
100
166
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elements a decoration template is allowed to contain.
|
|
3
|
+
* Allowlisting instead of denylisting: anything not listed here is stripped,
|
|
4
|
+
* including elements that do not yet exist in the HTML spec.
|
|
5
|
+
*/
|
|
6
|
+
const ALLOWED_ELEMENTS = [
|
|
7
|
+
// Structure / presentation
|
|
8
|
+
"div", "span", "p", "br", "hr",
|
|
9
|
+
"b", "i", "em", "strong", "s", "u", "mark", "small", "sub", "sup",
|
|
10
|
+
"abbr", "cite", "code", "data", "dfn", "kbd", "q", "samp", "time", "var",
|
|
11
|
+
"blockquote", "pre",
|
|
12
|
+
// SVG — useful for icon-style decorations (e.g. sidemarks)
|
|
13
|
+
"svg", "g", "path", "circle", "ellipse", "rect", "line",
|
|
14
|
+
"polygon", "polyline", "text", "tspan", "defs", "use",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/** Attributes that introduce executable code on any element. */
|
|
18
|
+
const DANGEROUS_ATTR = /^on/i;
|
|
19
|
+
/** Attributes that carry URLs and must be checked for unsafe schemes. */
|
|
20
|
+
const URL_ATTRS = new Set(["href", "src", "action", "formaction", "xlink:href"]);
|
|
21
|
+
/** URL schemes that must not appear in URL-bearing attributes. */
|
|
22
|
+
const DANGEROUS_SCHEME = /^\s*(javascript|data):/i;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parses `html` and returns its first element child with all executable
|
|
26
|
+
* content removed. Uses the Sanitizer API when available, falls back to a
|
|
27
|
+
* manual DOMParser scrub otherwise.
|
|
28
|
+
*
|
|
29
|
+
* The allowlist approach is intentional: unknown or future elements are
|
|
30
|
+
* stripped by default rather than permitted by oversight.
|
|
31
|
+
*
|
|
32
|
+
* @param wnd Window whose document is used when adopting nodes.
|
|
33
|
+
* @param html Raw HTML string supplied by the caller.
|
|
34
|
+
* @returns The sanitized first element child, or `null` for empty input.
|
|
35
|
+
*/
|
|
36
|
+
export function sanitizeHTML(wnd: Window, html: string): Element | null {
|
|
37
|
+
const host = wnd.document.createElement("div");
|
|
38
|
+
|
|
39
|
+
if ("Sanitizer" in wnd && typeof (host as any).setHTML === "function") {
|
|
40
|
+
try {
|
|
41
|
+
const sanitizer = new (wnd as any).Sanitizer({ allowElements: ALLOWED_ELEMENTS });
|
|
42
|
+
(host as any).setHTML(html, { sanitizer });
|
|
43
|
+
return host.firstElementChild as Element | null;
|
|
44
|
+
} catch {
|
|
45
|
+
// Sanitizer API present but call failed — fall through to DOMParser.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// DOMParser fallback: parse in an isolated document then scrub manually.
|
|
50
|
+
const scratch = wnd.document.implementation.createHTMLDocument("");
|
|
51
|
+
scratch.body.innerHTML = html;
|
|
52
|
+
scrubNode(scratch.body, new Set(ALLOWED_ELEMENTS));
|
|
53
|
+
while (scratch.body.firstChild) {
|
|
54
|
+
host.appendChild(wnd.document.adoptNode(scratch.body.firstChild));
|
|
55
|
+
}
|
|
56
|
+
return host.firstElementChild as Element | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scrubNode(root: Element, allowed: Set<string>): void {
|
|
60
|
+
// Walk in reverse so removals don't shift indices.
|
|
61
|
+
const all = Array.from(root.querySelectorAll("*")).reverse();
|
|
62
|
+
for (const el of all) {
|
|
63
|
+
if (!allowed.has(el.localName)) {
|
|
64
|
+
// Replace disallowed element with its children to preserve text.
|
|
65
|
+
el.replaceWith(...Array.from(el.childNodes));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const { name, value } of Array.from(el.attributes)) {
|
|
69
|
+
if (
|
|
70
|
+
DANGEROUS_ATTR.test(name) ||
|
|
71
|
+
(URL_ATTRS.has(name) && DANGEROUS_SCHEME.test(value))
|
|
72
|
+
) {
|
|
73
|
+
el.removeAttribute(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,4 +2,5 @@ export * from './comms/index.ts';
|
|
|
2
2
|
export * from './modules/index.ts';
|
|
3
3
|
export * from './Loader.ts';
|
|
4
4
|
export * from './protection/index.ts';
|
|
5
|
-
export
|
|
5
|
+
export type { ReadiumWindow } from './helpers/dom.ts';
|
|
6
|
+
export * from './keyboard/index.ts'
|