@readium/navigator 2.1.0 → 2.2.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 (32) hide show
  1. package/dist/index.js +3082 -2649
  2. package/dist/index.umd.cjs +86 -36
  3. package/package.json +8 -8
  4. package/src/epub/css/Properties.ts +1 -5
  5. package/src/epub/css/ReadiumCSS.ts +0 -1
  6. package/src/epub/preferences/EpubDefaults.ts +1 -5
  7. package/src/epub/preferences/EpubPreferences.ts +0 -4
  8. package/src/epub/preferences/EpubPreferencesEditor.ts +1 -14
  9. package/src/epub/preferences/EpubSettings.ts +1 -4
  10. package/src/index.ts +1 -0
  11. package/src/preferences/Types.ts +0 -6
  12. package/src/webpub/WebPubBlobBuilder.ts +145 -0
  13. package/src/webpub/WebPubFrameManager.ts +140 -0
  14. package/src/webpub/WebPubFramePoolManager.ts +174 -0
  15. package/src/webpub/WebPubNavigator.ts +417 -0
  16. package/src/webpub/index.ts +4 -0
  17. package/types/src/epub/css/Properties.d.ts +1 -3
  18. package/types/src/epub/preferences/EpubDefaults.d.ts +1 -3
  19. package/types/src/epub/preferences/EpubPreferences.d.ts +1 -3
  20. package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +1 -2
  21. package/types/src/epub/preferences/EpubSettings.d.ts +1 -3
  22. package/types/src/index.d.ts +1 -0
  23. package/types/src/preferences/Types.d.ts +0 -5
  24. package/types/src/web/WebPubBlobBuilder.d.ts +10 -0
  25. package/types/src/web/WebPubFrameManager.d.ts +20 -0
  26. package/types/src/web/WebPubNavigator.d.ts +48 -0
  27. package/types/src/web/index.d.ts +3 -0
  28. package/types/src/webpub/WebPubBlobBuilder.d.ts +12 -0
  29. package/types/src/webpub/WebPubFrameManager.d.ts +20 -0
  30. package/types/src/webpub/WebPubFramePoolManager.d.ts +16 -0
  31. package/types/src/webpub/WebPubNavigator.d.ts +50 -0
  32. package/types/src/webpub/index.d.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "Next generation SDK for publications in Web Apps",
6
6
  "author": "readium",
@@ -31,9 +31,9 @@
31
31
  "types": "./types/src/index.d.ts",
32
32
  "exports": {
33
33
  ".": {
34
+ "types": "./types/src/index.d.ts",
34
35
  "import": "./dist/index.js",
35
- "require": "./dist/index.umd.cjs",
36
- "types": "./types/src/index.d.ts"
36
+ "require": "./dist/index.umd.cjs"
37
37
  }
38
38
  },
39
39
  "files": [
@@ -48,17 +48,17 @@
48
48
  "build": "tsc && vite build"
49
49
  },
50
50
  "devDependencies": {
51
- "@laynezh/vite-plugin-lib-assets": "^0.5.25",
52
- "@readium/css": ">=2.0.0-beta.18",
51
+ "@laynezh/vite-plugin-lib-assets": "^2.1.0",
52
+ "@readium/css": "2.0.0-beta.19",
53
53
  "@readium/navigator-html-injectables": "workspace:*",
54
54
  "@readium/shared": "workspace:*",
55
55
  "@types/path-browserify": "^1.0.3",
56
56
  "css-selector-generator": "^3.6.9",
57
57
  "path-browserify": "^1.0.1",
58
58
  "tslib": "^2.8.1",
59
- "typescript": "^5.6.3",
60
- "typescript-plugin-css-modules": "^5.1.0",
59
+ "typescript": "^5.9.2",
60
+ "typescript-plugin-css-modules": "^5.2.0",
61
61
  "user-agent-data-types": "^0.4.2",
62
- "vite": "^4.5.5"
62
+ "vite": "^7.1.5"
63
63
  }
64
64
  }
@@ -1,4 +1,4 @@
1
- import { TextAlignment, Theme } from "../../preferences/Types";
1
+ import { TextAlignment } from "../../preferences/Types";
2
2
 
3
3
  export type BodyHyphens = "auto" | "none";
4
4
  export type BoxSizing = "content-box" | "border-box";
@@ -51,7 +51,6 @@ abstract class Properties {
51
51
  export interface IUserProperties {
52
52
  advancedSettings?: boolean | null;
53
53
  a11yNormalize?: boolean | null;
54
- appearance?: Theme | null;
55
54
  backgroundColor?: string | null;
56
55
  blendFilter?: boolean | null;
57
56
  bodyHyphens?: BodyHyphens | null;
@@ -87,7 +86,6 @@ export interface IUserProperties {
87
86
 
88
87
  export class UserProperties extends Properties {
89
88
  a11yNormalize: boolean | null;
90
- appearance: Theme | null;
91
89
  backgroundColor: string | null;
92
90
  blendFilter: boolean | null;
93
91
  bodyHyphens: BodyHyphens | null;
@@ -123,7 +121,6 @@ export class UserProperties extends Properties {
123
121
  constructor(props: IUserProperties) {
124
122
  super();
125
123
  this.a11yNormalize = props.a11yNormalize ?? null;
126
- this.appearance = props.appearance ?? null;
127
124
  this.backgroundColor = props.backgroundColor ?? null;
128
125
  this.blendFilter = props.blendFilter ?? null;
129
126
  this.bodyHyphens = props.bodyHyphens ?? null;
@@ -161,7 +158,6 @@ export class UserProperties extends Properties {
161
158
  const cssProperties: { [key: string]: string } = {};
162
159
 
163
160
  if (this.a11yNormalize) cssProperties["--USER__a11yNormalize"] = this.toFlag("a11y");
164
- if (this.appearance) cssProperties["--USER__appearance"] = this.toFlag(this.appearance);
165
161
  if (this.backgroundColor) cssProperties["--USER__backgroundColor"] = this.backgroundColor;
166
162
  if (this.blendFilter) cssProperties["--USER__blendFilter"] = this.toFlag("blend");
167
163
  if (this.bodyHyphens) cssProperties["--USER__bodyHyphens"] = this.bodyHyphens;
@@ -73,7 +73,6 @@ export class ReadiumCSS {
73
73
 
74
74
  const updated: IUserProperties = {
75
75
  a11yNormalize: settings.textNormalization,
76
- appearance: settings.theme,
77
76
  backgroundColor: settings.backgroundColor,
78
77
  blendFilter: settings.blendFilter,
79
78
  bodyHyphens: typeof settings.hyphens !== "boolean"
@@ -2,8 +2,7 @@ import {
2
2
  fontSizeRangeConfig,
3
3
  fontWeightRangeConfig,
4
4
  fontWidthRangeConfig,
5
- TextAlignment,
6
- Theme
5
+ TextAlignment
7
6
  } from "../../preferences/Types";
8
7
 
9
8
  import {
@@ -59,7 +58,6 @@ export interface IEpubDefaults {
59
58
  textAlign?: TextAlignment | null,
60
59
  textColor?: string | null,
61
60
  textNormalization?: boolean | null,
62
- theme?: Theme | null,
63
61
  visitedColor?: string | null,
64
62
  wordSpacing?: number | null
65
63
  }
@@ -103,7 +101,6 @@ export class EpubDefaults {
103
101
  textAlign: TextAlignment | null;
104
102
  textColor: string | null;
105
103
  textNormalization: boolean | null;
106
- theme: Theme | null;
107
104
  visitedColor: string | null;
108
105
  wordSpacing: number | null;
109
106
 
@@ -150,7 +147,6 @@ export class EpubDefaults {
150
147
  this.textAlign = ensureEnumValue<TextAlignment>(defaults.textAlign, TextAlignment) || null;
151
148
  this.textColor = ensureString(defaults.textColor) || null;
152
149
  this.textNormalization = ensureBoolean(defaults.textNormalization) ?? false;
153
- this.theme = ensureEnumValue<Theme>(defaults.theme, Theme) || null;
154
150
  this.visitedColor = ensureString(defaults.visitedColor) || null;
155
151
  this.wordSpacing = ensureNonNegative(defaults.wordSpacing) || null;
156
152
 
@@ -2,7 +2,6 @@ import { ConfigurablePreferences } from "../../preferences/Configurable";
2
2
 
3
3
  import {
4
4
  TextAlignment,
5
- Theme,
6
5
  fontSizeRangeConfig,
7
6
  fontWeightRangeConfig,
8
7
  fontWidthRangeConfig
@@ -56,7 +55,6 @@ export interface IEpubPreferences {
56
55
  textAlign?: TextAlignment | null,
57
56
  textColor?: string | null,
58
57
  textNormalization?: boolean | null,
59
- theme?: Theme | null,
60
58
  visitedColor?: string | null,
61
59
  wordSpacing?: number | null
62
60
  }
@@ -100,7 +98,6 @@ export class EpubPreferences implements ConfigurablePreferences {
100
98
  textAlign?: TextAlignment | null;
101
99
  textColor?: string | null;
102
100
  textNormalization?: boolean | null;
103
- theme?: Theme | null;
104
101
  visitedColor?: string | null;
105
102
  wordSpacing?: number | null;
106
103
 
@@ -140,7 +137,6 @@ export class EpubPreferences implements ConfigurablePreferences {
140
137
  this.textAlign = ensureEnumValue<TextAlignment>(preferences.textAlign, TextAlignment);
141
138
  this.textColor = ensureString(preferences.textColor);
142
139
  this.textNormalization = ensureBoolean(preferences.textNormalization);
143
- this.theme = ensureEnumValue<Theme>(preferences.theme, Theme);
144
140
  this.visitedColor = ensureString(preferences.visitedColor);
145
141
  this.wordSpacing = ensureNonNegative(preferences.wordSpacing);
146
142
 
@@ -5,13 +5,12 @@ import { EpubSettings } from "./EpubSettings";
5
5
  import { BooleanPreference, EnumPreference, Preference, RangePreference } from "../../preferences/Preference";
6
6
  import {
7
7
  TextAlignment,
8
- Theme,
9
8
  fontSizeRangeConfig,
10
9
  fontWeightRangeConfig,
11
10
  fontWidthRangeConfig
12
11
  } from "../../preferences/Types";
13
12
 
14
- import defaultColors from "@readium/css/css/vars/day.json";
13
+ import defaultColors from "@readium/css/css/vars/colors.json";
15
14
 
16
15
  // WIP: will change cos’ of all the missing pieces
17
16
  export class EpubPreferencesEditor implements IPreferencesEditor {
@@ -483,18 +482,6 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
483
482
  });
484
483
  }
485
484
 
486
- get theme(): EnumPreference<Theme> {
487
- return new EnumPreference<Theme>({
488
- initialValue: this.preferences.theme,
489
- effectiveValue: this.settings.theme || null,
490
- isEffective: this.layout !== Layout.fixed,
491
- onChange: (newValue: Theme | null | undefined) => {
492
- this.updatePreference("theme", newValue || null);
493
- },
494
- supportedValues: Object.values(Theme)
495
- });
496
- }
497
-
498
485
  get visitedColor(): Preference<string> {
499
486
  return new Preference<string>({
500
487
  initialValue: this.preferences.visitedColor,
@@ -1,5 +1,5 @@
1
1
  import { ConfigurableSettings } from "../../preferences/Configurable";
2
- import { TextAlignment, Theme } from "../../preferences/Types";
2
+ import { TextAlignment } from "../../preferences/Types";
3
3
  import { EpubDefaults } from "./EpubDefaults";
4
4
  import { EpubPreferences } from "./EpubPreferences";
5
5
 
@@ -44,7 +44,6 @@ export interface IEpubSettings {
44
44
  textAlign?: TextAlignment | null,
45
45
  textColor?: string | null,
46
46
  textNormalization?: boolean | null,
47
- theme?: Theme | null,
48
47
  visitedColor?: string | null,
49
48
  wordSpacing?: number | null
50
49
  }
@@ -88,7 +87,6 @@ export class EpubSettings implements ConfigurableSettings {
88
87
  textAlign: TextAlignment | null;
89
88
  textColor: string | null;
90
89
  textNormalization: boolean | null;
91
- theme: Theme | null;
92
90
  visitedColor: string | null;
93
91
  wordSpacing: number | null;
94
92
 
@@ -225,7 +223,6 @@ export class EpubSettings implements ConfigurableSettings {
225
223
  this.textNormalization = typeof preferences.textNormalization === "boolean"
226
224
  ? preferences.textNormalization
227
225
  : defaults.textNormalization ?? null;
228
- this.theme = preferences.theme || defaults.theme || null;
229
226
  this.visitedColor = preferences.visitedColor || defaults.visitedColor || null;
230
227
  this.wordSpacing = preferences.wordSpacing !== undefined
231
228
  ? preferences.wordSpacing
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './Navigator';
2
+ export * from './webpub';
2
3
  export * from './epub';
3
4
  export * from './audio';
4
5
  export * from './helpers';
@@ -5,12 +5,6 @@ export enum TextAlignment {
5
5
  justify = "justify"
6
6
  };
7
7
 
8
- export enum Theme {
9
- sepia = "sepia",
10
- night = "night",
11
- custom = "custom"
12
- }
13
-
14
8
  export type RangeConfig = {
15
9
  range: [number, number],
16
10
  step: number
@@ -0,0 +1,145 @@
1
+ import { Link, Publication } from "@readium/shared";
2
+
3
+ // Utilities (matching FrameBlobBuilder pattern)
4
+ const blobify = (source: string, type: string) => URL.createObjectURL(new Blob([source], { type }));
5
+ const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
6
+ const scriptify = (doc: Document, source: string) => {
7
+ const s = doc.createElement("script");
8
+ s.dataset.readium = "true";
9
+ s.src = source.startsWith("blob:") ? source : blobify(source, "text/javascript");
10
+ return s;
11
+ }
12
+
13
+ type CacheFunction = () => string;
14
+ const resourceBlobCache = new Map<string, string>();
15
+ const cached = (key: string, cacher: CacheFunction) => {
16
+ if (resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
17
+ const value = cacher();
18
+ resourceBlobCache.set(key, value);
19
+ return value;
20
+ };
21
+
22
+ const cssSelectorGenerator = (doc: Document) => scriptify(doc, cached("css-selector-generator", () => blobify(
23
+ "!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})()));",
24
+ "text/javascript"
25
+ )));
26
+
27
+ const readiumPropertiesScript = `
28
+ window._readium_blockedEvents = [];
29
+ window._readium_blockEvents = false; // WebPub doesn't need event blocking
30
+ window._readium_eventBlocker = null;
31
+ `;
32
+
33
+ const rBefore = (doc: Document) => scriptify(doc, cached("webpub-js-before", () => blobify(stripJS(readiumPropertiesScript), "text/javascript")));
34
+ const rAfter = (doc: Document) => scriptify(doc, cached("webpub-js-after", () => blobify(stripJS(`
35
+ if(window.onload) window.onload = new Proxy(window.onload, {
36
+ apply: function(target, receiver, args) {
37
+ if(!window._readium_blockEvents) {
38
+ Reflect.apply(target, receiver, args);
39
+ return;
40
+ }
41
+ _readium_blockedEvents.push([0, target, receiver, args]);
42
+ }
43
+ });`), "text/javascript")));
44
+
45
+ export class WebPubBlobBuilder {
46
+ private readonly item: Link;
47
+ private readonly burl: string;
48
+ private readonly pub: Publication;
49
+
50
+ constructor(pub: Publication, baseURL: string, item: Link) {
51
+ this.pub = pub;
52
+ this.item = item;
53
+ this.burl = item.toURL(baseURL) || "";
54
+ }
55
+
56
+ public async build(): Promise<string> {
57
+ if (!this.item.mediaType.isHTML) {
58
+ throw new Error(`Unsupported media type for WebPub: ${this.item.mediaType.string}`);
59
+ }
60
+
61
+ return await this.buildHtmlFrame();
62
+ }
63
+
64
+ private async buildHtmlFrame(): Promise<string> {
65
+ // Load the HTML resource
66
+ const txt = await this.pub.get(this.item).readAsString();
67
+ if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
68
+ const doc = new DOMParser().parseFromString(
69
+ txt,
70
+ this.item.mediaType.string as DOMParserSupportedType
71
+ );
72
+ const perror = doc.querySelector("parsererror");
73
+ if(perror) {
74
+ const details = perror.querySelector("div");
75
+ throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
76
+ }
77
+ return this.finalizeDOM(doc, this.burl, this.item.mediaType, txt);
78
+ }
79
+
80
+ private hasExecutable(doc: Document): boolean {
81
+ return (
82
+ !!doc.querySelector("script") ||
83
+ !!doc.querySelector("body[onload]:not(body[onload=''])")
84
+ );
85
+ }
86
+
87
+ private finalizeDOM(doc: Document, base: string | undefined, mediaType: any, txt?: string): string {
88
+ if(!doc) return "";
89
+
90
+ doc.body.querySelectorAll("img").forEach((img) => {
91
+ img.setAttribute("fetchpriority", "high");
92
+ });
93
+
94
+ if(base !== undefined) {
95
+ const b = doc.createElement("base");
96
+ b.href = base;
97
+ b.dataset.readium = "true";
98
+ doc.head.firstChild!.before(b);
99
+ }
100
+
101
+
102
+ const hasExecutable = this.hasExecutable(doc);
103
+ if(hasExecutable) doc.head.firstChild!.before(rBefore(doc));
104
+ doc.head.firstChild!.before(cssSelectorGenerator(doc));
105
+ if(hasExecutable) doc.head.appendChild(rAfter(doc));
106
+
107
+ // Serialize properly based on content type
108
+ let serializedContent: string;
109
+
110
+ if (mediaType.string === "application/xhtml+xml") {
111
+ // XHTML: Use XMLSerializer for proper XML formatting
112
+ serializedContent = new XMLSerializer().serializeToString(doc);
113
+ } else {
114
+ // HTML: Use custom HTML serialization to preserve HTML formatting
115
+ serializedContent = this.serializeAsHTML(doc, txt || "");
116
+ }
117
+
118
+ // Make blob from doc
119
+ return URL.createObjectURL(
120
+ new Blob([serializedContent], {
121
+ type: mediaType.isHTML
122
+ ? mediaType.string
123
+ : "application/xhtml+xml",
124
+ })
125
+ );
126
+ }
127
+
128
+ private serializeAsHTML(doc: Document, txt: string): string {
129
+ // For HTML content, try to preserve the original HTML structure
130
+ // while injecting our scripts
131
+
132
+ // Extract the original DOCTYPE if present
133
+ const doctypeMatch = txt.match(/<!DOCTYPE[^>]*>/i);
134
+ const doctype = doctypeMatch ? doctypeMatch[0] + "\n" : "";
135
+
136
+ // Get the HTML element and serialize it as HTML
137
+ const htmlElement = doc.documentElement;
138
+ let htmlContent = htmlElement.outerHTML;
139
+
140
+ // Try to preserve the original HTML structure
141
+ // This is a best-effort approach since there's no perfect HTML serializer
142
+
143
+ return doctype + htmlContent;
144
+ }
145
+ }
@@ -0,0 +1,140 @@
1
+ import { Loader, ModuleName } from "@readium/navigator-html-injectables";
2
+ import { FrameComms } from "../epub/frame/FrameComms";
3
+ import { ReadiumWindow } from "../../../navigator-html-injectables/types/src/helpers/dom";
4
+ import { sML } from "../helpers";
5
+
6
+ export class WebPubFrameManager {
7
+ private frame: HTMLIFrameElement;
8
+ private loader: Loader | undefined;
9
+ public readonly source: string;
10
+ private comms: FrameComms | undefined;
11
+ private destroyed: boolean = false;
12
+
13
+ private currModules: ModuleName[] = [];
14
+
15
+ constructor(source: string) {
16
+ this.frame = document.createElement("iframe");
17
+ this.frame.classList.add("readium-navigator-iframe");
18
+ this.frame.style.visibility = "hidden";
19
+ this.frame.style.setProperty("aria-hidden", "true");
20
+ this.frame.style.opacity = "0";
21
+ this.frame.style.position = "absolute";
22
+ this.frame.style.pointerEvents = "none";
23
+ this.frame.style.transition = "visibility 0s, opacity 0.1s linear";
24
+ // Protect against background color bleeding
25
+ this.frame.style.backgroundColor = "#FFFFFF";
26
+ this.source = source;
27
+ }
28
+
29
+ async load(modules: ModuleName[] = []): Promise<Window> {
30
+ return new Promise((res, rej) => {
31
+ if(this.loader) {
32
+ const wnd = this.frame.contentWindow!;
33
+ // Check if currently loaded modules are equal
34
+ if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
35
+ try { res(wnd); } catch (error) {};
36
+ return;
37
+ }
38
+ this.comms?.halt();
39
+ this.loader.destroy();
40
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
41
+ this.currModules = modules;
42
+ this.comms = undefined;
43
+ try { res(wnd); } catch (error) {}
44
+ return;
45
+ }
46
+ this.frame.onload = () => {
47
+ const wnd = this.frame.contentWindow!;
48
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
49
+ this.currModules = modules;
50
+ try { res(wnd); } catch (error) {}
51
+ };
52
+ this.frame.onerror = (err) => {
53
+ try { rej(err); } catch (error) {}
54
+ }
55
+ this.frame.contentWindow!.location.replace(this.source);
56
+ });
57
+ }
58
+
59
+ async destroy() {
60
+ await this.hide();
61
+ this.loader?.destroy();
62
+ this.frame.remove();
63
+ this.destroyed = true;
64
+ }
65
+
66
+ async hide(): Promise<void> {
67
+ if(this.destroyed) return;
68
+ this.frame.style.visibility = "hidden";
69
+ this.frame.style.setProperty("aria-hidden", "true");
70
+ this.frame.style.opacity = "0";
71
+ this.frame.style.pointerEvents = "none";
72
+
73
+ if(this.frame.parentElement) {
74
+ if(this.comms === undefined || !this.comms.ready) return;
75
+ return new Promise((res, _) => {
76
+ this.comms?.send("unfocus", undefined, (_: boolean) => {
77
+ this.comms?.halt();
78
+ res();
79
+ });
80
+ });
81
+ } else {
82
+ this.comms?.halt();
83
+ }
84
+ }
85
+
86
+ async show(atProgress?: number): Promise<void> {
87
+ if (this.destroyed) throw Error("Trying to show frame when it doesn't exist");
88
+ if (!this.frame.parentElement) throw Error("Trying to show frame that is not attached to the DOM");
89
+ if (this.comms) this.comms.resume();
90
+ else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
91
+
92
+ return new Promise((res, _) => {
93
+ this.comms?.send("activate", undefined, () => {
94
+ this.comms?.send("focus", undefined, () => {
95
+ const remove = () => {
96
+ this.frame.style.removeProperty("visibility");
97
+ this.frame.style.removeProperty("aria-hidden");
98
+ this.frame.style.removeProperty("opacity");
99
+ this.frame.style.removeProperty("pointer-events");
100
+
101
+ if (sML.UA.WebKit) {
102
+ this.comms?.send("force_webkit_recalc", undefined);
103
+ }
104
+
105
+ res();
106
+ }
107
+
108
+ if (atProgress !== undefined) {
109
+ this.comms?.send("go_progression", atProgress, remove);
110
+ } else {
111
+ remove();
112
+ }
113
+ });
114
+ });
115
+ });
116
+ }
117
+
118
+ get iframe() {
119
+ if(this.destroyed) throw Error("Trying to use frame when it doesn't exist");
120
+ return this.frame;
121
+ }
122
+
123
+ get realSize() {
124
+ if(this.destroyed) throw Error("Trying to use frame client rect when it doesn't exist");
125
+ return this.frame.getBoundingClientRect();
126
+ }
127
+
128
+ get window() {
129
+ if(this.destroyed || !this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
130
+ return this.frame.contentWindow;
131
+ }
132
+
133
+ get msg() {
134
+ return this.comms;
135
+ }
136
+
137
+ get ldr() {
138
+ return this.loader;
139
+ }
140
+ }