@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.
- package/dist/index.js +3082 -2649
- package/dist/index.umd.cjs +86 -36
- package/package.json +8 -8
- package/src/epub/css/Properties.ts +1 -5
- package/src/epub/css/ReadiumCSS.ts +0 -1
- package/src/epub/preferences/EpubDefaults.ts +1 -5
- package/src/epub/preferences/EpubPreferences.ts +0 -4
- package/src/epub/preferences/EpubPreferencesEditor.ts +1 -14
- package/src/epub/preferences/EpubSettings.ts +1 -4
- package/src/index.ts +1 -0
- package/src/preferences/Types.ts +0 -6
- package/src/webpub/WebPubBlobBuilder.ts +145 -0
- package/src/webpub/WebPubFrameManager.ts +140 -0
- package/src/webpub/WebPubFramePoolManager.ts +174 -0
- package/src/webpub/WebPubNavigator.ts +417 -0
- package/src/webpub/index.ts +4 -0
- package/types/src/epub/css/Properties.d.ts +1 -3
- package/types/src/epub/preferences/EpubDefaults.d.ts +1 -3
- package/types/src/epub/preferences/EpubPreferences.d.ts +1 -3
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +1 -2
- package/types/src/epub/preferences/EpubSettings.d.ts +1 -3
- package/types/src/index.d.ts +1 -0
- package/types/src/preferences/Types.d.ts +0 -5
- package/types/src/web/WebPubBlobBuilder.d.ts +10 -0
- package/types/src/web/WebPubFrameManager.d.ts +20 -0
- package/types/src/web/WebPubNavigator.d.ts +48 -0
- package/types/src/web/index.d.ts +3 -0
- package/types/src/webpub/WebPubBlobBuilder.d.ts +12 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +20 -0
- package/types/src/webpub/WebPubFramePoolManager.d.ts +16 -0
- package/types/src/webpub/WebPubNavigator.d.ts +50 -0
- 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.
|
|
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": "^
|
|
52
|
-
"@readium/css": "
|
|
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.
|
|
60
|
-
"typescript-plugin-css-modules": "^5.
|
|
59
|
+
"typescript": "^5.9.2",
|
|
60
|
+
"typescript-plugin-css-modules": "^5.2.0",
|
|
61
61
|
"user-agent-data-types": "^0.4.2",
|
|
62
|
-
"vite": "^
|
|
62
|
+
"vite": "^7.1.5"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextAlignment
|
|
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/
|
|
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
|
|
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
package/src/preferences/Types.ts
CHANGED
|
@@ -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
|
+
}
|