@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.
Files changed (75) hide show
  1. package/dist/Loader.js +1 -0
  2. package/dist/comms/comms.js +1 -0
  3. package/dist/comms/mid.js +1 -0
  4. package/dist/helpers/animation.js +1 -0
  5. package/dist/helpers/css.js +1 -0
  6. package/dist/helpers/document.js +1 -0
  7. package/dist/helpers/dom.js +1 -0
  8. package/dist/helpers/locator.js +1 -0
  9. package/dist/helpers/rect.js +1 -0
  10. package/dist/helpers/sanitize.js +1 -0
  11. package/dist/index.js +1 -4174
  12. package/dist/keyboard/KeyCombinationManager.js +1 -0
  13. package/dist/keyboard/KeyboardCombinations.js +1 -0
  14. package/dist/modules/Decorator.js +24 -0
  15. package/dist/modules/Module.js +1 -0
  16. package/dist/modules/ModuleLibrary.js +1 -0
  17. package/dist/modules/Peripherals.js +1 -0
  18. package/dist/modules/setup/FixedSetup.js +13 -0
  19. package/dist/modules/setup/ReflowableSetup.js +1 -0
  20. package/dist/modules/setup/Setup.js +1 -0
  21. package/dist/modules/setup/WebPubSetup.js +1 -0
  22. package/dist/modules/snapper/CJKVerticalSnapper.js +12 -0
  23. package/dist/modules/snapper/ColumnSnapper.js +35 -0
  24. package/dist/modules/snapper/ScrollSnapper.js +9 -0
  25. package/dist/modules/snapper/Snapper.js +5 -0
  26. package/dist/modules/snapper/WebPubSnapper.js +1 -0
  27. package/dist/protection/BulkCopyProtector.js +1 -0
  28. package/dist/protection/PatternAnalyzer.js +1 -0
  29. package/dist/protection/PrintProtector.js +15 -0
  30. package/dist/protection/SelectionAnalyzer.js +1 -0
  31. package/dist/protection/config.js +1 -0
  32. package/dist/vendor/approx-string-match/index.js +1 -0
  33. package/dist/vendor/hypothesis/anchoring/match-quote.js +1 -0
  34. package/dist/vendor/hypothesis/anchoring/text-range.js +1 -0
  35. package/dist/vendor/hypothesis/anchoring/trim-range.js +1 -0
  36. package/dist/vendor/hypothesis/anchoring/types.js +1 -0
  37. package/package.json +9 -7
  38. package/src/comms/comms.ts +11 -1
  39. package/src/comms/keys.ts +8 -3
  40. package/src/helpers/css.ts +30 -1
  41. package/src/helpers/document.ts +123 -3
  42. package/src/helpers/locator.ts +5 -5
  43. package/src/helpers/rect.ts +88 -22
  44. package/src/helpers/sanitize.ts +77 -0
  45. package/src/index.ts +2 -1
  46. package/src/modules/Decorator.ts +924 -178
  47. package/src/modules/Peripherals.ts +3 -0
  48. package/src/modules/snapper/CJKVerticalSnapper.ts +34 -7
  49. package/src/modules/snapper/ColumnSnapper.ts +56 -7
  50. package/src/modules/snapper/ScrollSnapper.ts +30 -7
  51. package/src/modules/snapper/Snapper.ts +144 -0
  52. package/src/modules/snapper/WebPubSnapper.ts +31 -7
  53. package/types/src/comms/comms.d.ts +10 -1
  54. package/types/src/comms/keys.d.ts +2 -2
  55. package/types/src/helpers/css.d.ts +7 -1
  56. package/types/src/helpers/document.d.ts +69 -3
  57. package/types/src/helpers/rect.d.ts +2 -1
  58. package/types/src/helpers/sanitize.d.ts +13 -0
  59. package/types/src/index.d.ts +1 -0
  60. package/types/src/modules/Decorator.d.ts +92 -17
  61. package/types/src/modules/Peripherals.d.ts +3 -0
  62. package/types/src/modules/snapper/CJKVerticalSnapper.d.ts +1 -0
  63. package/types/src/modules/snapper/ColumnSnapper.d.ts +14 -1
  64. package/types/src/modules/snapper/ScrollSnapper.d.ts +1 -0
  65. package/types/src/modules/snapper/Snapper.d.ts +58 -0
  66. package/types/src/modules/snapper/WebPubSnapper.d.ts +1 -0
  67. package/dist/ar-DyHX_uy2.js +0 -7
  68. package/dist/da-Dct0PS3E.js +0 -7
  69. package/dist/fr-C5HEel98.js +0 -7
  70. package/dist/index.umd.cjs +0 -95
  71. package/dist/it-DFOBoXGy.js +0 -7
  72. package/dist/pt_PT-Di3sVjze.js +0 -7
  73. package/dist/sv-BfzAFsVN.js +0 -7
  74. package/src/helpers/color.ts +0 -205
  75. package/types/src/helpers/color.d.ts +0 -26
@@ -1,19 +1,139 @@
1
1
  import { ReadiumWindow } from "./dom.ts";
2
2
 
3
- export function isRTL(wnd: ReadiumWindow): boolean {
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: ReadiumWindow): boolean {
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 getColumnCountPerScreen(wnd: ReadiumWindow): number {
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
@@ -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 && locations.getCssSelector()) {
17
- root = doc.querySelector(locations.getCssSelector()!);
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 && locations.getCssSelector()) {
40
- element = doc.querySelector(locations.getCssSelector()!);
39
+ if (!element && getCssSelector(locations)) {
40
+ element = doc.querySelector(getCssSelector(locations)!);
41
41
  }
42
42
 
43
43
  if (!element && locations.fragments) {
@@ -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
- range: Range,
15
- doNotMergeHorizontallyAlignedRects: boolean
58
+ source: Range | Rect[],
59
+ doNotMergeHorizontallyAlignedRects: boolean,
60
+ doNotMergeVerticallyAlignedRects: boolean = false,
61
+ expand: number = 0
16
62
  ) {
17
- let clientRects = range.getClientRects();
63
+ const tolerance = 1;
64
+ let originalRects: Rect[];
18
65
 
19
- // Try falling back to the client rects of the common ancestor of the range if it's an HTML Element
20
- if(!clientRects.length)
21
- if(range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE)
22
- clientRects = (range.commonAncestorContainer as HTMLElement).getClientRects();
66
+ if (Array.isArray(source)) {
67
+ originalRects = source;
68
+ } else {
69
+ let clientRects = source.getClientRects();
23
70
 
24
- const tolerance = 1;
25
- const originalRects: Rect[] = [];
26
- for (const rangeClientRect of clientRects) {
27
- originalRects.push({
28
- bottom: rangeClientRect.bottom,
29
- height: rangeClientRect.height,
30
- left: rangeClientRect.left,
31
- right: rangeClientRect.right,
32
- top: rangeClientRect.top,
33
- width: rangeClientRect.width,
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 * from './keyboard/index.ts'
5
+ export type { ReadiumWindow } from './helpers/dom.ts';
6
+ export * from './keyboard/index.ts'