@readium/navigator 2.2.6 → 2.2.8

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 (47) hide show
  1. package/dist/ar-DyHX_uy2-DyHX_uy2-DyHX_uy2.js +7 -0
  2. package/dist/ar-DyHX_uy2-DyHX_uy2.js +7 -0
  3. package/dist/da-Dct0PS3E-Dct0PS3E-Dct0PS3E.js +7 -0
  4. package/dist/da-Dct0PS3E-Dct0PS3E.js +7 -0
  5. package/dist/fr-C5HEel98-C5HEel98-C5HEel98.js +7 -0
  6. package/dist/fr-C5HEel98-C5HEel98.js +7 -0
  7. package/dist/index.js +4389 -2401
  8. package/dist/index.umd.cjs +1571 -39
  9. package/dist/it-DFOBoXGy-DFOBoXGy-DFOBoXGy.js +7 -0
  10. package/dist/it-DFOBoXGy-DFOBoXGy.js +7 -0
  11. package/dist/pt_PT-Di3sVjze-Di3sVjze-Di3sVjze.js +7 -0
  12. package/dist/pt_PT-Di3sVjze-Di3sVjze.js +7 -0
  13. package/dist/sv-BfzAFsVN-BfzAFsVN-BfzAFsVN.js +7 -0
  14. package/dist/sv-BfzAFsVN-BfzAFsVN.js +7 -0
  15. package/package.json +6 -5
  16. package/src/dom/_readium_cssSelectorGenerator.js +1 -0
  17. package/src/dom/_readium_executionCleanup.js +13 -0
  18. package/src/dom/_readium_executionPrevention.js +65 -0
  19. package/src/dom/_readium_webpubExecution.js +4 -0
  20. package/src/epub/EpubNavigator.ts +26 -2
  21. package/src/epub/frame/FrameBlobBuilder.ts +37 -130
  22. package/src/epub/frame/FramePoolManager.ts +34 -5
  23. package/src/epub/fxl/FXLFramePoolManager.ts +20 -2
  24. package/src/helpers/minify.ts +14 -0
  25. package/src/index.ts +2 -1
  26. package/src/injection/Injectable.ts +85 -0
  27. package/src/injection/Injector.ts +356 -0
  28. package/src/injection/epubInjectables.ts +90 -0
  29. package/src/injection/index.ts +2 -0
  30. package/src/injection/webpubInjectables.ts +59 -0
  31. package/src/webpub/WebPubBlobBuilder.ts +19 -76
  32. package/src/webpub/WebPubFramePoolManager.ts +29 -4
  33. package/src/webpub/WebPubNavigator.ts +15 -1
  34. package/types/src/epub/EpubNavigator.d.ts +3 -0
  35. package/types/src/epub/frame/FrameBlobBuilder.d.ts +7 -4
  36. package/types/src/epub/frame/FramePoolManager.d.ts +3 -1
  37. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +3 -1
  38. package/types/src/helpers/minify.d.ts +12 -0
  39. package/types/src/index.d.ts +1 -0
  40. package/types/src/injection/Injectable.d.ts +68 -0
  41. package/types/src/injection/Injector.d.ts +22 -0
  42. package/types/src/injection/epubInjectables.d.ts +6 -0
  43. package/types/src/injection/index.d.ts +2 -0
  44. package/types/src/injection/webpubInjectables.d.ts +5 -0
  45. package/types/src/webpub/WebPubBlobBuilder.d.ts +7 -3
  46. package/types/src/webpub/WebPubFramePoolManager.d.ts +3 -1
  47. package/types/src/webpub/WebPubNavigator.d.ts +3 -0
@@ -1,85 +1,6 @@
1
1
  import { MediaType } from "@readium/shared";
2
2
  import { Link, Publication } from "@readium/shared";
3
-
4
- // Readium CSS imports
5
- // The "?inline" query is to prevent some bundlers from injecting these into the page (e.g. vite)
6
- // @ts-ignore
7
- import readiumCSSAfter from "@readium/css/css/dist/ReadiumCSS-after.css?inline";
8
- // @ts-ignore
9
- import readiumCSSBefore from "@readium/css/css/dist/ReadiumCSS-before.css?inline";
10
- // @ts-ignore
11
- import readiumCSSDefault from "@readium/css/css/dist/ReadiumCSS-default.css?inline";
12
-
13
- // Utilities
14
- const blobify = (source: string, type: string) => URL.createObjectURL(new Blob([source], { type }));
15
- const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
16
- const stripCSS = (source: string) => source.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, '').replace(/ {2,}/g, ' ')
17
- // Fully resolve absolute local URLs created by bundlers since it's going into a blob
18
- .replace(/url\((?!(https?:)?\/\/)("?)\/([^\)]+)/g, `url($2${window.location.origin}/$3`);
19
- const scriptify = (doc: Document, source: string) => {
20
- const s = doc.createElement("script");
21
- s.dataset.readium = "true";
22
- s.src = source.startsWith("blob:") ? source : blobify(source, "text/javascript");
23
- return s;
24
- }
25
- const styleify = (doc: Document, source: string) => {
26
- const s = doc.createElement("link");
27
- s.dataset.readium = "true";
28
- s.rel = "stylesheet";
29
- s.type = "text/css";
30
- s.href = source.startsWith("blob:") ? source : blobify(source, "text/css");
31
- return s;
32
- }
33
-
34
- type CacheFunction = () => string;
35
- const resourceBlobCache = new Map<string, string>();
36
- const cached = (key: string, cacher: CacheFunction) => {
37
- if(resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
38
- const value = cacher();
39
- resourceBlobCache.set(key, value);
40
- return value;
41
- };
42
-
43
- // https://unpkg.com/css-selector-generator@3.6.4/build/index.js
44
- // CssSelectorGenerator --> _readium_cssSelectorGenerator
45
- // This has to be injected because you need to be in the iframe's context for it to work properly
46
- const cssSelectorGenerator = (doc: Document) => scriptify(doc, cached("css-selector-generator", () => blobify(
47
- "!function(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports._readium_cssSelectorGenerator=e():t._readium_cssSelectorGenerator=e()}(self,(()=>(()=>{\"use strict\";var t,e,n={d:(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})}},o={};function r(t){return t&&t instanceof Element}function i(t=\"unknown problem\",...e){console.warn(`CssSelectorGenerator: ${t}`,...e)}n.r(o),n.d(o,{default:()=>z,getCssSelector:()=>U}),function(t){t.NONE=\"none\",t.DESCENDANT=\"descendant\",t.CHILD=\"child\"}(t||(t={})),function(t){t.id=\"id\",t.class=\"class\",t.tag=\"tag\",t.attribute=\"attribute\",t.nthchild=\"nthchild\",t.nthoftype=\"nthoftype\"}(e||(e={}));const c={selectors:[e.id,e.class,e.tag,e.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function u(t){return t instanceof RegExp}function s(t){return[\"string\",\"function\"].includes(typeof t)||u(t)}function l(t){return Array.isArray(t)?t.filter(s):[]}function a(t){const e=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(t){return t instanceof Node}(t)&&e.includes(t.nodeType)}function f(t,e){if(a(t))return t.contains(e)||i(\"element root mismatch\",\"Provided root does not contain the element. This will most likely result in producing a fallback selector using element\'s real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended.\"),t;const n=e.getRootNode({composed:!1});return a(n)?(n!==document&&i(\"shadow root inferred\",\"You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended.\"),n):e.ownerDocument.querySelector(\":root\")}function d(t){return\"number\"==typeof t?t:Number.POSITIVE_INFINITY}function m(t=[]){const[e=[],...n]=t;return 0===n.length?e:n.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function p(t){return[].concat(...t)}function h(t){const e=t.map((t=>{if(u(t))return e=>t.test(e);if(\"function\"==typeof t)return e=>{const n=t(e);return\"boolean\"!=typeof n?(i(\"pattern matcher function invalid\",\"Provided pattern matching function does not return boolean. It\'s result will be ignored.\",t),!1):n};if(\"string\"==typeof t){const e=new RegExp(\"^\"+t.replace(\/[|\\\\{}()[\\]^$+?.]\/g,\"\\\\$&\").replace(\/\\*\/g,\".+\")+\"$\");return t=>e.test(t)}return i(\"pattern matcher invalid\",\"Pattern matching only accepts strings, regular expressions and\/or functions. This item is invalid and will be ignored.\",t),()=>!1}));return t=>e.some((e=>e(t)))}function g(t,e,n){const o=Array.from(f(n,t[0]).querySelectorAll(e));return o.length===t.length&&t.every((t=>o.includes(t)))}function y(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(\":root\")}(t);const n=[];let o=t;for(;r(o)&&o!==e;)n.push(o),o=o.parentElement;return n}function b(t,e){return m(t.map((t=>y(t,e))))}const N={[t.NONE]:{type:t.NONE,value:\"\"},[t.DESCENDANT]:{type:t.DESCENDANT,value:\" > \"},[t.CHILD]:{type:t.CHILD,value:\" \"}},S=new RegExp([\"^$\",\"\\\\s\"].join(\"|\")),E=new RegExp([\"^$\"].join(\"|\")),w=[e.nthoftype,e.tag,e.id,e.class,e.attribute,e.nthchild],v=h([\"class\",\"id\",\"ng-*\"]);function C({nodeName:t}){return`[${t}]`}function O({nodeName:t,nodeValue:e}){return`[${t}=\'${L(e)}\']`}function T(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const n=e.tagName.toLowerCase();return!([\"input\",\"option\"].includes(n)&&\"value\"===t||v(t))}(e,t)));return[...e.map(C),...e.map(O)]}function I(t){return(t.getAttribute(\"class\")||\"\").trim().split(\/\\s+\/).filter((t=>!E.test(t))).map((t=>`.${L(t)}`))}function x(t){const e=t.getAttribute(\"id\")||\"\",n=`#${L(e)}`,o=t.getRootNode({composed:!1});return!S.test(e)&&g([t],n,o)?[n]:[]}function j(t){const e=t.parentNode;if(e){const n=Array.from(e.childNodes).filter(r).indexOf(t);if(n>-1)return[`:nth-child(${n+1})`]}return[]}function A(t){return[L(t.tagName.toLowerCase())]}function D(t){const e=[...new Set(p(t.map(A)))];return 0===e.length||e.length>1?[]:[e[0]]}function $(t){const e=D([t])[0],n=t.parentElement;if(n){const o=Array.from(n.children).filter((t=>t.tagName.toLowerCase()===e)),r=o.indexOf(t);if(r>-1)return[`${e}:nth-of-type(${r+1})`]}return[]}function R(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){const n=[];let o=0,r=k(1);for(;r.length<=t.length&&o<e;)o+=1,n.push(r.map((e=>t[e]))),r=P(r,t.length-1);return n}function P(t=[],e=0){const n=t.length;if(0===n)return[];const o=[...t];o[n-1]+=1;for(let t=n-1;t>=0;t--)if(o[t]>e){if(0===t)return k(n+1);o[t-1]++,o[t]=o[t-1]+1}return o[n-1]>e?k(n+1):o}function k(t=1){return Array.from(Array(t).keys())}const _=\":\".charCodeAt(0).toString(16).toUpperCase(),M=\/[ !\"#$%&\'()\\[\\]{|}<>*+,.\/;=?@^`~\\\\]\/;function L(t=\"\"){var e,n;return null!==(n=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==n?n:function(t=\"\"){return t.split(\"\").map((t=>\":\"===t?`\\\\${_} `:M.test(t)?`\\\\${t}`:escape(t).replace(\/%\/g,\"\\\\\"))).join(\"\")}(t)}const q={tag:D,id:function(t){return 0===t.length||t.length>1?[]:x(t[0])},class:function(t){return m(t.map(I))},attribute:function(t){return m(t.map(T))},nthchild:function(t){return m(t.map(j))},nthoftype:function(t){return m(t.map($))}},F={tag:A,id:x,class:I,attribute:T,nthchild:j,nthoftype:$};function V(t){return t.includes(e.tag)||t.includes(e.nthoftype)?[...t]:[...t,e.tag]}function Y(t={}){const n=[...w];return t[e.tag]&&t[e.nthoftype]&&n.splice(n.indexOf(e.tag),1),n.map((e=>{return(o=t)[n=e]?o[n].join(\"\"):\"\";var n,o})).join(\"\")}function B(t,e,n=\"\",o){const r=function(t,e){return\"\"===e?t:function(t,e){return[...t.map((t=>e+\" \"+t)),...t.map((t=>e+\" > \"+t))]}(t,e)}(function(t,e,n){const o=function(t,e){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=e,c=h(n),u=h(o);return function(t){const{selectors:e,includeTag:n}=t,o=[].concat(e);return n&&!o.includes(\"tag\")&&o.push(\"tag\"),o}(e).reduce(((e,n)=>{const o=function(t,e){var n;return(null!==(n=q[e])&&void 0!==n?n:()=>[])(t)}(t,n),s=function(t=[],e,n){return t.filter((t=>n(t)||!e(t)))}(o,c,u),l=function(t=[],e){return t.sort(((t,n)=>{const o=e(t),r=e(n);return o&&!r?-1:!o&&r?1:0}))}(s,u);return e[n]=r?R(l,{maxResults:i}):l.map((t=>[t])),e}),{})}(t,n),r=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=t,i=n?R(e,{maxResults:r}):e.map((t=>[t]));return o?i.map(V):i}(e).map((e=>function(t,e){const n={};return t.forEach((t=>{const o=e[t];o.length>0&&(n[t]=o)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,n])=>{e=n.flatMap((n=>0===e.length?[{[t]:n}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:n})))))})),e}(n).map(Y)}(e,t))).filter((t=>t.length>0))}(o,n),i=p(r);return[...new Set(i)]}(t,o.root,o),n);for(const e of r)if(g(t,e,o.root))return e;return null}function G(t){return{value:t,include:!1}}function W({selectors:t,operator:n}){let o=[...w];t[e.tag]&&t[e.nthoftype]&&(o=o.filter((t=>t!==e.tag)));let r=\"\";return o.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(r+=t)}))})),n.value+r}function H(n){return[\":root\",...y(n).reverse().map((n=>{const o=function(e,n,o=t.NONE){const r={};return n.forEach((t=>{Reflect.set(r,t,function(t,e){return F[e](t)}(e,t).map(G))})),{element:e,operator:N[o],selectors:r}}(n,[e.nthchild],t.DESCENDANT);return o.selectors.nthchild.forEach((t=>{t.include=!0})),o})).map(W)].join(\"\")}function U(t,n={}){const o=function(t){const e=(Array.isArray(t)?t:[t]).filter(r);return[...new Set(e)]}(t),i=function(t,n={}){const o=Object.assign(Object.assign({},c),n);return{selectors:(r=o.selectors,Array.isArray(r)?r.filter((t=>{return n=e,o=t,Object.values(n).includes(o);var n,o})):[]),whitelist:l(o.whitelist),blacklist:l(o.blacklist),root:f(o.root,t),combineWithinSelector:!!o.combineWithinSelector,combineBetweenSelectors:!!o.combineBetweenSelectors,includeTag:!!o.includeTag,maxCombinations:d(o.maxCombinations),maxCandidates:d(o.maxCandidates)};var r}(o[0],n);let u=\"\",s=i.root;function a(){return function(t,e,n=\"\",o){if(0===t.length)return null;const r=[t.length>1?t:[],...b(t,e).map((t=>[t]))];for(const t of r){const e=B(t,0,n,o);if(e)return{foundElements:t,selector:e}}return null}(o,s,u,i)}let m=a();for(;m;){const{foundElements:t,selector:e}=m;if(g(o,e,i.root))return e;s=t[0],u=e,m=a()}return o.length>1?o.map((t=>U(t,i))).join(\", \"):function(t){return t.map(H).join(\", \")}(o)}const z=U;return o})()));",
48
- "text/javascript"
49
- )));
50
-
51
- // Note: we aren't blocking some of the events right now to try and be as nonintrusive as possible.
52
- // For a more comprehensive implementation, see https://github.com/hackademix/noscript/blob/3a83c0e4a506f175e38b0342dad50cdca3eae836/src/content/syncFetchPolicy.js#L142
53
- // The snippet of code at the beginning of this source is an attempt at defence against JS using persistent storage
54
- const rBefore = (doc: Document) => scriptify(doc, cached("JS-Before", () => blobify(stripJS(`
55
- const noop=()=>{},emptyObj={},emptyPromise=()=>Promise.resolve(void 0),fakeStorage={getItem:noop,setItem:noop,removeItem:noop,clear:noop,key:noop,length:0};["localStorage","sessionStorage"].forEach((e=>Object.defineProperty(window,e,{get:()=>fakeStorage,configurable:!0}))),Object.defineProperty(document,"cookie",{get:()=>"",set:noop,configurable:!0}),Object.defineProperty(window,"indexedDB",{get:()=>{},configurable:!0}),Object.defineProperty(window,"caches",{get:()=>emptyObj,configurable:!0}),Object.defineProperty(navigator,"storage",{get:()=>({persist:emptyPromise,persisted:emptyPromise,estimate:()=>Promise.resolve({quota:0,usage:0})}),configurable:!0}),Object.defineProperty(navigator,"serviceWorker",{get:()=>({register:emptyPromise,getRegistration:emptyPromise,ready:emptyPromise()}),configurable:!0});
56
-
57
- window._readium_blockedEvents = [];
58
- window._readium_blockEvents = true;
59
- window._readium_eventBlocker = (e) => {
60
- if(!window._readium_blockEvents) return;
61
- e.preventDefault();
62
- e.stopImmediatePropagation();
63
- _readium_blockedEvents.push([
64
- 1, e, e.currentTarget || e.target
65
- ]);
66
- };
67
- window.addEventListener("DOMContentLoaded", window._readium_eventBlocker, true);
68
- window.addEventListener("load", window._readium_eventBlocker, true);`
69
- ), "text/javascript")));
70
- const rAfter = (doc: Document) => scriptify(doc, cached("JS-After", () => blobify(stripJS(`
71
- if(window.onload) window.onload = new Proxy(window.onload, {
72
- apply: function(target, receiver, args) {
73
- if(!window._readium_blockEvents) {
74
- Reflect.apply(target, receiver, args);
75
- return;
76
- }
77
- _readium_blockedEvents.push([
78
- 0, target, receiver, args
79
- ]);
80
- }
81
- });`
82
- ), "text/javascript")));
3
+ import { Injector } from "../../injection/Injector";
83
4
 
84
5
  const csp = (domains: string[]) => {
85
6
  const d = domains.join(" ");
@@ -104,12 +25,22 @@ export default class FrameBlobBuider {
104
25
  private readonly burl: string;
105
26
  private readonly pub: Publication;
106
27
  private readonly cssProperties?: { [key: string]: string };
107
-
108
- constructor(pub: Publication, baseURL: string, item: Link, cssProperties?: { [key: string]: string }) {
28
+ private readonly injector: Injector | null = null;
29
+
30
+ constructor(
31
+ pub: Publication,
32
+ baseURL: string,
33
+ item: Link,
34
+ options: {
35
+ cssProperties?: { [key: string]: string };
36
+ injector?: Injector | null;
37
+ }
38
+ ) {
109
39
  this.pub = pub;
110
40
  this.item = item;
111
41
  this.burl = item.toURL(baseURL) || "";
112
- this.cssProperties = cssProperties;
42
+ this.cssProperties = options.cssProperties;
43
+ this.injector = options.injector ?? null;
113
44
  }
114
45
 
115
46
  public async build(fxl = false): Promise<string> {
@@ -127,15 +58,23 @@ export default class FrameBlobBuider {
127
58
  // Load the HTML resource
128
59
  const txt = await this.pub.get(this.item).readAsString();
129
60
  if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
61
+
130
62
  const doc = new DOMParser().parseFromString(
131
63
  txt,
132
64
  this.item.mediaType.string as DOMParserSupportedType
133
65
  );
66
+
134
67
  const perror = doc.querySelector("parsererror");
135
- if(perror) {
68
+ if (perror) {
136
69
  const details = perror.querySelector("div");
137
70
  throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
138
71
  }
72
+
73
+ // Apply resource injections if injection service is provided
74
+ if (this.injector) {
75
+ await this.injector.injectForDocument(doc, this.item);
76
+ }
77
+
139
78
  return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
140
79
  }
141
80
 
@@ -150,29 +89,6 @@ export default class FrameBlobBuider {
150
89
  return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
151
90
  }
152
91
 
153
- // Has JS that may have side-effects when the document is loaded, without any user interaction
154
- private hasExecutable(doc: Document): boolean {
155
- // This is not a 100% comprehensive check of all possibilities for JS execution,
156
- // but it covers what the prevention scripts cover. Other possibilities include:
157
- // - <iframe> src
158
- // - <img> with onload/onerror
159
- // - <meta http-equiv="refresh" content="xxx">
160
- return (
161
- !!doc.querySelector("script") || // Any <script> elements
162
- !!doc.querySelector("body[onload]:not(body[onload=''])") // <body> that executes JS on load
163
- );
164
- }
165
-
166
- private hasStyle(doc: Document): boolean {
167
- if(
168
- doc.querySelector("link[rel='stylesheet']") || // Any CSS link
169
- doc.querySelector("style") || // Any <style> element
170
- doc.querySelector("[style]:not([style=''])") // Any element with style attribute set
171
- ) return true;
172
-
173
- return false;
174
- }
175
-
176
92
  private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
177
93
  for (const key in cssProperties) {
178
94
  const value = cssProperties[key];
@@ -183,22 +99,20 @@ export default class FrameBlobBuider {
183
99
  private finalizeDOM(doc: Document, root: string | undefined, base: string | undefined, mediaType: MediaType, fxl = false, cssProperties?: { [key: string]: string }): string {
184
100
  if(!doc) return "";
185
101
 
186
- // Inject styles
187
- if(!fxl) {
188
- // Readium CSS Before
189
- const rcssBefore = styleify(doc, cached("ReadiumCSS-before", () => blobify(stripCSS(readiumCSSBefore), "text/css")));
190
- doc.head.firstChild ? doc.head.firstChild.before(rcssBefore) : doc.head.appendChild(rcssBefore);
191
-
192
- // Readium CSS defaults
193
- if(!this.hasStyle(doc))
194
- rcssBefore.after(styleify(doc, cached("ReadiumCSS-default", () => blobify(stripCSS(readiumCSSDefault), "text/css"))))
102
+ // Get allowed domains from injector if it exists
103
+ const allowedDomains = this.injector?.getAllowedDomains?.() || [];
104
+
105
+ // Always include the root domain if provided
106
+ const domains = [...new Set([
107
+ ...(root ? [root] : []),
108
+ ...allowedDomains
109
+ ])].filter(Boolean);
195
110
 
196
- // Readium CSS After
197
- doc.head.appendChild(styleify(doc, cached("ReadiumCSS-after", () => blobify(stripCSS(readiumCSSAfter), "text/css"))));
111
+ // CSS and script injection is now handled by the Injector system
198
112
 
199
- if (cssProperties) {
200
- this.setProperties(cssProperties, doc);
201
- }
113
+ // Apply CSS properties if provided (only for reflowable)
114
+ if (cssProperties && !fxl) {
115
+ this.setProperties(cssProperties, doc);
202
116
  }
203
117
 
204
118
  // Set all <img> elements to high priority
@@ -263,20 +177,13 @@ export default class FrameBlobBuider {
263
177
  doc.head.firstChild!.before(b);
264
178
  }
265
179
 
266
- // Inject script to prevent in-publication scripts from executing until we want them to
267
- const hasExecutable = this.hasExecutable(doc);
268
- if (hasExecutable) doc.head.firstChild!.before(rBefore(doc));
269
- doc.head.firstChild!.before(cssSelectorGenerator(doc)); // CSS selector utility
270
- if (hasExecutable) doc.head.appendChild(rAfter(doc)); // Another execution prevention script
271
-
272
- // Add CSP
180
+ // Add CSP with allowed domains
273
181
  const meta = doc.createElement("meta");
274
182
  meta.httpEquiv = "Content-Security-Policy";
275
- meta.content = csp(root ? [root] : []);
183
+ meta.content = csp(domains);
276
184
  meta.dataset.readium = "true";
277
185
  doc.head.firstChild!.before(meta);
278
186
 
279
-
280
187
  // Make blob from doc
281
188
  return URL.createObjectURL(
282
189
  new Blob([new XMLSerializer().serializeToString(doc)], {
@@ -2,6 +2,7 @@ import { ModuleName } from "@readium/navigator-html-injectables";
2
2
  import { Locator, Publication } from "@readium/shared";
3
3
  import FrameBlobBuider from "./FrameBlobBuilder";
4
4
  import { FrameManager } from "./FrameManager";
5
+ import { Injector } from "../../injection/Injector";
5
6
 
6
7
  const UPPER_BOUNDARY = 5;
7
8
  const LOWER_BOUNDARY = 3;
@@ -16,11 +17,18 @@ export class FramePoolManager {
16
17
  private readonly inprogress: Map<string, Promise<void>> = new Map();
17
18
  private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
18
19
  private currentBaseURL: string | undefined;
20
+ private readonly injector: Injector | null = null;
19
21
 
20
- constructor(container: HTMLElement, positions: Locator[], cssProperties?: { [key: string]: string }) {
22
+ constructor(
23
+ container: HTMLElement,
24
+ positions: Locator[],
25
+ cssProperties?: { [key: string]: string },
26
+ injector?: Injector | null
27
+ ) {
21
28
  this.container = container;
22
29
  this.positions = positions;
23
30
  this.currentCssProperties = cssProperties;
31
+ this.injector = injector ?? null;
24
32
  }
25
33
 
26
34
  async destroy() {
@@ -47,7 +55,13 @@ export class FramePoolManager {
47
55
  this.pool.clear();
48
56
 
49
57
  // Revoke all blobs
50
- this.blobs.forEach(v => URL.revokeObjectURL(v));
58
+ this.blobs.forEach(v => {
59
+ this.injector?.releaseBlobUrl?.(v);
60
+ URL.revokeObjectURL(v);
61
+ });
62
+
63
+ // Clean up injector if it exists
64
+ this.injector?.dispose();
51
65
 
52
66
  // Empty container of elements
53
67
  this.container.childNodes.forEach(v => {
@@ -90,7 +104,10 @@ export class FramePoolManager {
90
104
  // Check if base URL of publication has changed
91
105
  if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
92
106
  // Revoke all blobs
93
- this.blobs.forEach(v => URL.revokeObjectURL(v));
107
+ this.blobs.forEach(v => {
108
+ this.injector?.releaseBlobUrl?.(v);
109
+ URL.revokeObjectURL(v);
110
+ });
94
111
  this.blobs.clear();
95
112
  }
96
113
  this.currentBaseURL = pub.baseURL;
@@ -103,13 +120,17 @@ export class FramePoolManager {
103
120
  // when navigating backwards, where paginated will go the
104
121
  // start of the resource instead of the end due to the
105
122
  // corrupted width ColumnSnapper (injectables) gets on init
106
- this.blobs.forEach(v => URL.revokeObjectURL(v));
123
+ this.blobs.forEach(v => {
124
+ this.injector?.releaseBlobUrl?.(v);
125
+ URL.revokeObjectURL(v);
126
+ });
107
127
  this.blobs.clear();
108
128
  this.pendingUpdates.clear();
109
129
  }
110
130
  if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
111
131
  const url = this.blobs.get(href);
112
132
  if(url) {
133
+ this.injector?.releaseBlobUrl?.(url);
113
134
  URL.revokeObjectURL(url);
114
135
  this.blobs.delete(href);
115
136
  this.pendingUpdates.delete(href);
@@ -129,7 +150,15 @@ export class FramePoolManager {
129
150
  const itm = pub.readingOrder.findWithHref(href);
130
151
  if(!itm) return; // TODO throw?
131
152
  if(!this.blobs.has(href)) {
132
- const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm, this.currentCssProperties);
153
+ const blobBuilder = new FrameBlobBuider(
154
+ pub,
155
+ this.currentBaseURL || "",
156
+ itm,
157
+ {
158
+ cssProperties: this.currentCssProperties,
159
+ injector: this.injector
160
+ }
161
+ );
133
162
  const blobURL = await blobBuilder.build();
134
163
  this.blobs.set(href, blobURL);
135
164
  }
@@ -6,6 +6,7 @@ import { FXLFrameManager } from "./FXLFrameManager";
6
6
  import { FXLPeripherals } from "./FXLPeripherals";
7
7
  import { FXLSpreader, Orientation, Spread } from "./FXLSpreader";
8
8
  import { VisualNavigatorViewport } from "../../Navigator";
9
+ import { Injector } from "../../injection/Injector";
9
10
 
10
11
  const UPPER_BOUNDARY = 8;
11
12
  const LOWER_BOUNDARY = 5;
@@ -26,6 +27,7 @@ export class FXLFramePoolManager {
26
27
  private readonly delayedTimeout: Map<string, number> = new Map();
27
28
  private currentBaseURL: string | undefined;
28
29
  private previousFrames: FXLFrameManager[] = [];
30
+ private readonly injector: Injector | null = null;
29
31
 
30
32
  // NEW
31
33
  private readonly bookElement: HTMLDivElement;
@@ -44,10 +46,16 @@ export class FXLFramePoolManager {
44
46
  // private readonly pages: FXLFrameManager[] = [];
45
47
  public readonly peripherals: FXLPeripherals;
46
48
 
47
- constructor(container: HTMLElement, positions: Locator[], pub: Publication) {
49
+ constructor(
50
+ container: HTMLElement,
51
+ positions: Locator[],
52
+ pub: Publication,
53
+ injector?: Injector | null
54
+ ) {
48
55
  this.container = container;
49
56
  this.positions = positions;
50
57
  this.pub = pub;
58
+ this.injector = injector ?? null;
51
59
  this.spreadPresentation = pub.metadata.otherMetadata?.spread || Spread.auto;
52
60
 
53
61
  if(this.pub.metadata.effectiveReadingProgression !== ReadingProgression.rtl && this.pub.metadata.effectiveReadingProgression !== ReadingProgression.ltr)
@@ -393,6 +401,9 @@ export class FXLFramePoolManager {
393
401
  // Revoke all blobs
394
402
  this.blobs.forEach(v => URL.revokeObjectURL(v));
395
403
 
404
+ // Clean up injector if it exists
405
+ this.injector?.dispose();
406
+
396
407
  // Empty container of elements
397
408
  this.container.childNodes.forEach(v => {
398
409
  if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
@@ -495,7 +506,14 @@ export class FXLFramePoolManager {
495
506
  const itm = pub.readingOrder.items[index];
496
507
  if(!itm) return; // TODO throw?
497
508
  if(!this.blobs.has(href)) {
498
- const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm);
509
+ const blobBuilder = new FrameBlobBuider(
510
+ pub,
511
+ this.currentBaseURL || "",
512
+ itm,
513
+ {
514
+ injector: this.injector
515
+ }
516
+ );
499
517
  const blobURL = await blobBuilder.build(true);
500
518
  this.blobs.set(href, blobURL);
501
519
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Utilities for processing CSS and JavaScript content
3
+ */
4
+
5
+ /**
6
+ * Minifies JavaScript by removing comments and normalizing whitespace
7
+ */
8
+ export const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
9
+
10
+ /**
11
+ * Minifies CSS by removing comments and normalizing whitespace
12
+ * Note: URL resolution should be handled by the caller with correct context
13
+ */
14
+ export const stripCSS = (source: string) => source.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, "").replace(/ {2,}/g, " ");
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export * from './epub';
4
4
  export * from './audio';
5
5
  export * from './helpers';
6
6
  export * from './preferences';
7
- export * from './css';
7
+ export * from './css';
8
+ export * from './injection';
@@ -0,0 +1,85 @@
1
+ import { Link } from "@readium/shared";
2
+
3
+ type ForbiddenAttributes = "type" | "rel" | "href" | "src";
4
+ type AllowedAttributes = {
5
+ [K in string]: K extends ForbiddenAttributes
6
+ ? never
7
+ : (string | boolean | undefined);
8
+ } & {
9
+ [K in ForbiddenAttributes]?: never;
10
+ };
11
+
12
+ export interface IBaseInjectable {
13
+ id?: string;
14
+ target?: "head" | "body";
15
+ type?: string;
16
+ condition?: (doc: Document) => boolean;
17
+
18
+ // Extra attributes - type and rel are forbidden here since they are at root
19
+ attributes?: AllowedAttributes;
20
+ }
21
+
22
+ export interface IScriptInjectable extends IBaseInjectable {
23
+ as: "script";
24
+ rel?: never; // Scripts don't have rel
25
+ }
26
+
27
+ export interface ILinkInjectable extends IBaseInjectable {
28
+ as: "link";
29
+ rel: string;
30
+ }
31
+
32
+ export interface IUrlInjectable {
33
+ url: string;
34
+ }
35
+
36
+ export interface IBlobInjectable {
37
+ blob: Blob;
38
+ }
39
+
40
+ export type IInjectable = (IScriptInjectable | ILinkInjectable) & (IUrlInjectable | IBlobInjectable);
41
+
42
+ /**
43
+ * Defines a rule for resource injection, specifying which resources to inject into which documents.
44
+ */
45
+ export interface IInjectableRule {
46
+ /**
47
+ * List of resource URLs or patterns that this rule applies to.
48
+ * Can be exact URLs or patterns with wildcards.
49
+ */
50
+ resources: Array<string | RegExp>;
51
+
52
+ /**
53
+ * Resources to inject at the beginning of the target (in array order)
54
+ */
55
+ prepend?: IInjectable[];
56
+
57
+ /**
58
+ * Resources to inject at the end of the target (in array order)
59
+ */
60
+ append?: IInjectable[];
61
+ }
62
+
63
+ export interface IInjectablesConfig {
64
+ rules: IInjectableRule[];
65
+ allowedDomains?: string[];
66
+ }
67
+
68
+ export interface IInjector {
69
+ /**
70
+ * Injects resources into a document based on matching rules
71
+ * @param doc The document to inject resources into
72
+ * @param link The link being loaded, used to match against injection rules
73
+ */
74
+ injectForDocument(doc: Document, link: Link): Promise<void>;
75
+
76
+ /**
77
+ * Cleans up any resources used by the injector
78
+ */
79
+ dispose(): void;
80
+
81
+ /**
82
+ * Get the list of allowed domains
83
+ */
84
+ getAllowedDomains(): string[]
85
+ }