@readium/navigator 2.11.1 → 2.12.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/divina/DivinaNavigator.js +1 -1
- package/dist/epub/EpubNavigator.js +1 -1
- package/dist/epub/frame/FramePoolManager.js +1 -1
- package/dist/index.js +1 -1
- package/dist/preferences/guards.js +1 -1
- package/dist/readaloud/GuidedNavigationProvider.js +1 -0
- package/dist/readaloud/ReadAloudNavigator.js +1 -0
- package/dist/readaloud/ReadingUnit.js +1 -0
- package/dist/readaloud/SpeechProgress.js +1 -0
- package/dist/readaloud/preferences/ReadAloudDefaults.js +1 -0
- package/dist/readaloud/preferences/ReadAloudPreferences.js +1 -0
- package/dist/readaloud/preferences/ReadAloudPreferencesEditor.js +1 -0
- package/dist/readaloud/preferences/ReadAloudSettings.js +1 -0
- package/dist/webpub/WebPubFrameManager.js +1 -1
- package/dist/webpub/WebPubFramePoolManager.js +1 -1
- package/dist/webpub/WebPubNavigator.js +1 -1
- package/package.json +8 -6
- package/src/divina/DivinaNavigator.ts +1 -0
- package/src/epub/EpubNavigator.ts +127 -21
- package/src/epub/frame/FramePoolManager.ts +5 -0
- package/src/index.ts +1 -0
- package/src/preferences/guards.ts +11 -0
- package/src/readaloud/GuidedNavigationProvider.ts +87 -0
- package/src/readaloud/ReadAloudNavigator.ts +949 -0
- package/src/readaloud/ReadingUnit.ts +154 -0
- package/src/readaloud/SpeechProgress.ts +60 -0
- package/src/readaloud/index.ts +17 -0
- package/src/readaloud/preferences/ReadAloudDefaults.ts +24 -0
- package/src/readaloud/preferences/ReadAloudPreferences.ts +104 -0
- package/src/readaloud/preferences/ReadAloudPreferencesEditor.ts +115 -0
- package/src/readaloud/preferences/ReadAloudSettings.ts +44 -0
- package/src/readaloud/preferences/index.ts +4 -0
- package/src/webpub/WebPubFrameManager.ts +4 -0
- package/src/webpub/WebPubFramePoolManager.ts +5 -0
- package/src/webpub/WebPubNavigator.ts +114 -10
- package/types/src/epub/EpubNavigator.d.ts +26 -1
- package/types/src/epub/frame/FramePoolManager.d.ts +2 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/preferences/guards.d.ts +1 -0
- package/types/src/readaloud/GuidedNavigationProvider.d.ts +28 -0
- package/types/src/readaloud/ReadAloudNavigator.d.ts +158 -0
- package/types/src/readaloud/ReadingUnit.d.ts +58 -0
- package/types/src/readaloud/SpeechProgress.d.ts +19 -0
- package/types/src/readaloud/index.d.ts +6 -0
- package/types/src/readaloud/preferences/ReadAloudDefaults.d.ts +11 -0
- package/types/src/readaloud/preferences/ReadAloudPreferences.d.ts +58 -0
- package/types/src/readaloud/preferences/ReadAloudPreferencesEditor.d.ts +37 -0
- package/types/src/readaloud/preferences/ReadAloudSettings.d.ts +26 -0
- package/types/src/readaloud/preferences/index.d.ts +4 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +1 -0
- package/types/src/webpub/WebPubFramePoolManager.d.ts +2 -0
- package/types/src/webpub/WebPubNavigator.d.ts +25 -2
|
@@ -2,7 +2,7 @@ import { Layout, Link, Locator, LocatorText, Profile, Publication, ReadingProgre
|
|
|
2
2
|
import { Configurable, ConfigurableSettings, LineLengths, ProgressionRange, VisualNavigator, VisualNavigatorViewport } from "../index.ts";
|
|
3
3
|
import { FramePoolManager } from "./frame/FramePoolManager.ts";
|
|
4
4
|
import { FXLFramePoolManager } from "./fxl/FXLFramePoolManager.ts";
|
|
5
|
-
import { CommsEventKey, ContextMenuEvent, DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData, FXLModules, ModuleLibrary, ModuleName, ReflowableModules, BasicTextSelection, FrameClickEvent, SuspiciousActivityEvent, KeyboardPeripheralEvent } from "@readium/navigator-html-injectables";
|
|
5
|
+
import { CommsEventKey, ContextMenuEvent, DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData, FXLModules, ModuleLibrary, ModuleName, ReflowableModules, BasicTextSelection, FrameClickEvent, SuspiciousActivityEvent, KeyboardPeripheralEvent, TextLayout, PointedPieceRequest, PointedPieceResponse } from "@readium/navigator-html-injectables";
|
|
6
6
|
import { Decoration, OnDecorationActivatedEvent, OnDecorationPointerEnterEvent, OnDecorationPointerLeaveEvent, DecorationObserver, DecorableNavigator, DecoratorConfig, decorationsEqual, resolveDecorationForWire, supportsDecorationStyle as canRenderDecorationStyle, DecorationStyleType } from "../decorations/index.ts";
|
|
7
7
|
import * as path from "path-browserify";
|
|
8
8
|
import { FXLFrameManager } from "./fxl/FXLFrameManager.ts";
|
|
@@ -56,6 +56,9 @@ export interface EpubNavigatorListeners {
|
|
|
56
56
|
// showToc: () => void;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Expired comms callbacks are dropped, never called.
|
|
60
|
+
const POINTED_PIECE_TIMEOUT = 1000;
|
|
61
|
+
|
|
59
62
|
const defaultListeners = (listeners: EpubNavigatorListeners): EpubNavigatorListeners => ({
|
|
60
63
|
frameLoaded: listeners.frameLoaded || (() => {}),
|
|
61
64
|
positionChanged: listeners.positionChanged || (() => {}),
|
|
@@ -122,6 +125,11 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
122
125
|
private _decorationHoverState: Map<string, boolean> = new Map();
|
|
123
126
|
private _decorationActivationConsumed = false;
|
|
124
127
|
private _decorationResizeSelectors: Set<string>;
|
|
128
|
+
private _textLayoutWatch?: { href: string; pieces: unknown[]; cb: (layout: TextLayout) => void };
|
|
129
|
+
private _textLayoutFrame?: FrameManager;
|
|
130
|
+
private _navigationLocked = false;
|
|
131
|
+
private readonly _pointedPieceRequests = new Map<string, (index: number) => void>();
|
|
132
|
+
private _pointedPieceRequestId = 0;
|
|
125
133
|
|
|
126
134
|
private reflowViewport: VisualNavigatorViewport = {
|
|
127
135
|
readingOrder: [],
|
|
@@ -491,7 +499,18 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
491
499
|
} else {
|
|
492
500
|
this._reapplyDecorationsToCurrentFrames();
|
|
493
501
|
}
|
|
502
|
+
this._watchTextLayoutInCurrentFrame(true);
|
|
503
|
+
this._sendNavigationLock();
|
|
504
|
+
break;
|
|
505
|
+
case "text_layout":
|
|
506
|
+
// A frame being hidden after moving to another resource lays out again, collapsed.
|
|
507
|
+
if (sourceFrame && sourceFrame === this._textLayoutFrame && this.currentLocation.href.split("#")[0] === this._textLayoutWatch?.href) this._textLayoutWatch.cb(data as TextLayout);
|
|
494
508
|
break;
|
|
509
|
+
case "pointed_piece": {
|
|
510
|
+
const { id, index } = data as PointedPieceResponse;
|
|
511
|
+
this._pointedPieceRequests.get(id)?.(index);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
495
514
|
case "first_visible_locator":
|
|
496
515
|
const loc = Locator.deserialize(data as string);
|
|
497
516
|
if(!loc) break;
|
|
@@ -549,7 +568,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
549
568
|
) {
|
|
550
569
|
const origHref = element.attributes.getNamedItem("href")?.value!;
|
|
551
570
|
if (origHref.startsWith("#")) {
|
|
552
|
-
this.go(this.currentLocation.copyWithLocations({
|
|
571
|
+
if (!this._navigationLocked) this.go(this.currentLocation.copyWithLocations({
|
|
553
572
|
fragments: [origHref.substring(1)]
|
|
554
573
|
}), false, () => { });
|
|
555
574
|
} else if(
|
|
@@ -561,7 +580,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
561
580
|
this.listeners.handleLocator(new Link({
|
|
562
581
|
href: origHref,
|
|
563
582
|
}).locator);
|
|
564
|
-
} else {
|
|
583
|
+
} else if (!this._navigationLocked) {
|
|
565
584
|
try {
|
|
566
585
|
this.goLink(new Link({
|
|
567
586
|
href: path.join(path.dirname(this.currentLocation.href), origHref)
|
|
@@ -578,21 +597,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
578
597
|
if(this._layout === Layout.fixed && (this.framePool as FXLFramePoolManager).doNotDisturb)
|
|
579
598
|
edata.doNotDisturb = true;
|
|
580
599
|
|
|
581
|
-
|
|
582
|
-
&& (
|
|
583
|
-
this.currentProgression === ReadingProgression.rtl ||
|
|
584
|
-
this.currentProgression === ReadingProgression.ltr
|
|
585
|
-
)
|
|
586
|
-
) {
|
|
587
|
-
if(this.framePool.currentFrames.length > 1) {
|
|
588
|
-
// Spread page dimensions
|
|
589
|
-
const cfs = this.framePool.currentFrames;
|
|
590
|
-
if(edata.targetFrameSrc === cfs[this.currentProgression === ReadingProgression.rtl ? 0 : 1]?.source) {
|
|
591
|
-
// The right page (screen-wise) was clicked, so we add the left page's width to the click's x
|
|
592
|
-
edata.x += (cfs[this.currentProgression === ReadingProgression.rtl ? 1 : 0]?.iframe.contentWindow?.innerWidth ?? 0) * window.devicePixelRatio;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
600
|
+
edata.x += this._spreadOffsetX(edata.targetFrameSrc);
|
|
596
601
|
|
|
597
602
|
const handled = key === "click" ? this.listeners.click(edata) : this.listeners.tap(edata);
|
|
598
603
|
if(handled) break;
|
|
@@ -600,6 +605,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
600
605
|
const oneQuarter = ((this._cframes.length === 2 ? this._cframes[0]!.window.innerWidth + this._cframes[1]!.window.innerWidth : this._cframes[0]!.window.innerWidth) * window.devicePixelRatio) / 4;
|
|
601
606
|
// open UI if middle screen is clicked/tapped
|
|
602
607
|
if (edata.x >= oneQuarter && edata.x <= oneQuarter * 3) this.listeners.miscPointer(1);
|
|
608
|
+
if (this._navigationLocked) break;
|
|
603
609
|
if (edata.x < oneQuarter) this.goLeft(false, () => { }); // Go left if left quarter clicked
|
|
604
610
|
else if (edata.x > oneQuarter * 3) this.goRight(false, () => { }); // Go right if right quarter clicked
|
|
605
611
|
}
|
|
@@ -608,10 +614,10 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
608
614
|
this.listeners.miscPointer(data as number);
|
|
609
615
|
break;
|
|
610
616
|
case "no_more":
|
|
611
|
-
this.changeResource(1);
|
|
617
|
+
if (!this._navigationLocked) this.changeResource(1);
|
|
612
618
|
break;
|
|
613
619
|
case "no_less":
|
|
614
|
-
this.changeResource(-1);
|
|
620
|
+
if (!this._navigationLocked) this.changeResource(-1);
|
|
615
621
|
break;
|
|
616
622
|
case "swipe":
|
|
617
623
|
// Swipe event
|
|
@@ -622,9 +628,13 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
622
628
|
case "zoom":
|
|
623
629
|
this.listeners.zoom(data as number);
|
|
624
630
|
break;
|
|
625
|
-
case "progress":
|
|
631
|
+
case "progress": {
|
|
632
|
+
// The previous resource's frame can still report once moving to another, which would take its progress.
|
|
633
|
+
const frame = sourceFrame instanceof FrameManager ? (this.framePool as FramePoolManager)._frameFor(this.currentLocation.href.split("#")[0]) : undefined;
|
|
634
|
+
if (sourceFrame instanceof FrameManager && sourceFrame !== frame) break;
|
|
626
635
|
this.syncLocation(data as ProgressionRange);
|
|
627
636
|
break;
|
|
637
|
+
}
|
|
628
638
|
case "content_protection":
|
|
629
639
|
const activity = data as SuspiciousActivityEvent;
|
|
630
640
|
this.listeners.contentProtection(activity.type, activity);
|
|
@@ -685,6 +695,8 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
685
695
|
}
|
|
686
696
|
})
|
|
687
697
|
this._reapplyDecorationsToCurrentFrames();
|
|
698
|
+
this._watchTextLayoutInCurrentFrame();
|
|
699
|
+
this._sendNavigationLock();
|
|
688
700
|
}
|
|
689
701
|
|
|
690
702
|
private async apply() {
|
|
@@ -859,6 +871,100 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
859
871
|
});
|
|
860
872
|
}
|
|
861
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Reports where the text of `locators`, all in one reflowable resource, starts each line or column
|
|
876
|
+
* along the scroll axis, with the viewport. Reported again whenever that resource is scrolled or laid out again.
|
|
877
|
+
* Replaces any previous watch, and returns a function that stops this one.
|
|
878
|
+
*/
|
|
879
|
+
public watchTextLayout(locators: Locator[], cb: (layout: TextLayout) => void): () => void {
|
|
880
|
+
const watch = {
|
|
881
|
+
href: locators[0]?.href.split("#")[0] ?? "",
|
|
882
|
+
pieces: locators.map(locator => [locator.text?.serialize(), getCssSelector(locator.locations)]),
|
|
883
|
+
cb,
|
|
884
|
+
};
|
|
885
|
+
this._textLayoutWatch = watch;
|
|
886
|
+
this._watchTextLayoutInCurrentFrame(true);
|
|
887
|
+
return () => {
|
|
888
|
+
if (this._textLayoutWatch !== watch) return;
|
|
889
|
+
this._textLayoutWatch = undefined;
|
|
890
|
+
this._watchTextLayoutInCurrentFrame();
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Locks or unlocks navigating in the content: scrolling, swiping, tapping its edges and following its links.
|
|
896
|
+
* Calls to the navigator still move it.
|
|
897
|
+
*/
|
|
898
|
+
public lockNavigation(locked: boolean): void {
|
|
899
|
+
this._navigationLocked = locked;
|
|
900
|
+
this._sendNavigationLock();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Fixed-layout frames don't scroll, and their swipes are reported as no_more and no_less.
|
|
904
|
+
private _sendNavigationLock(): void {
|
|
905
|
+
this._cframes.forEach(frame => {
|
|
906
|
+
if (frame instanceof FrameManager) frame.msg?.send("lock_navigation", this._navigationLocked);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** The left page's width when `targetFrameSrc` is the right page (screen-wise) of a fixed-layout spread, else 0. */
|
|
911
|
+
private _spreadOffsetX(targetFrameSrc: string): number {
|
|
912
|
+
if (this._layout !== Layout.fixed || (
|
|
913
|
+
this.currentProgression !== ReadingProgression.rtl &&
|
|
914
|
+
this.currentProgression !== ReadingProgression.ltr
|
|
915
|
+
)) return 0;
|
|
916
|
+
const cfs = this.framePool.currentFrames;
|
|
917
|
+
if (cfs.length < 2) return 0;
|
|
918
|
+
const rtl = this.currentProgression === ReadingProgression.rtl;
|
|
919
|
+
if (targetFrameSrc !== cfs[rtl ? 0 : 1]?.source) return 0;
|
|
920
|
+
return (cfs[rtl ? 1 : 0]?.iframe.contentWindow?.innerWidth ?? 0) * window.devicePixelRatio;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Returns the first of `pieces` under the point of `event`, else the first containing the element it targets,
|
|
925
|
+
* in the resource of the frame it comes from. Undefined when none does.
|
|
926
|
+
*/
|
|
927
|
+
public async findPointedPiece(event: FrameClickEvent, pieces: Locator[]): Promise<Locator | undefined> {
|
|
928
|
+
const frames = this._cframes.filter(f => !!f) as (FrameManager | FXLFrameManager)[];
|
|
929
|
+
const i = frames.findIndex(frame => frame.source === event.targetFrameSrc);
|
|
930
|
+
const href = i >= 0 ? this.viewport.readingOrder[i] : undefined;
|
|
931
|
+
if (!href || !event.cssSelector) return undefined;
|
|
932
|
+
const candidates = pieces.filter(piece => piece.href.split("#")[0] === href);
|
|
933
|
+
if (candidates.length === 0) return undefined;
|
|
934
|
+
|
|
935
|
+
const id = `${++this._pointedPieceRequestId}`;
|
|
936
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
937
|
+
const index = await new Promise<number>(resolve => {
|
|
938
|
+
this._pointedPieceRequests.set(id, resolve);
|
|
939
|
+
timeout = setTimeout(() => resolve(-1), POINTED_PIECE_TIMEOUT);
|
|
940
|
+
const request: PointedPieceRequest = {
|
|
941
|
+
id,
|
|
942
|
+
cssSelector: event.cssSelector!,
|
|
943
|
+
x: event.x - this._spreadOffsetX(event.targetFrameSrc),
|
|
944
|
+
y: event.y,
|
|
945
|
+
pieces: candidates.map(piece => [piece.text?.serialize(), getCssSelector(piece.locations)])
|
|
946
|
+
};
|
|
947
|
+
// The reply is posted before the ack, so a pending request on ack got no reply.
|
|
948
|
+
const sent = frames[i].msg?.send("pointed_piece", request, () => resolve(-1));
|
|
949
|
+
if (!sent) resolve(-1);
|
|
950
|
+
});
|
|
951
|
+
clearTimeout(timeout);
|
|
952
|
+
this._pointedPieceRequests.delete(id);
|
|
953
|
+
return candidates[index];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// The previous frame stops watching, so only the displayed resource reports its layout.
|
|
957
|
+
private _watchTextLayoutInCurrentFrame(resend = false): void {
|
|
958
|
+
const frame = this._cframes[0];
|
|
959
|
+
const watch = this._textLayoutWatch;
|
|
960
|
+
const target = watch && frame instanceof FrameManager && this.currentLocation?.href.split("#")[0] === watch.href ? frame : undefined;
|
|
961
|
+
if (target === this._textLayoutFrame && !resend) return;
|
|
962
|
+
const previous = this._textLayoutFrame;
|
|
963
|
+
if (previous && previous !== target && !previous.isDestroyed) previous.msg?.send("watch_text_layout", []);
|
|
964
|
+
this._textLayoutFrame = target;
|
|
965
|
+
if (target && watch) target.msg?.send("watch_text_layout", watch.pieces);
|
|
966
|
+
}
|
|
967
|
+
|
|
862
968
|
private _handleDecorationActivated(data: DecorationActivatedEvent): boolean {
|
|
863
969
|
const observers = this._decorationObservers.get(data.group);
|
|
864
970
|
if (!observers || observers.size === 0) return false;
|
|
@@ -327,6 +327,11 @@ export class FramePoolManager {
|
|
|
327
327
|
return [this._currentFrame];
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/** The pooled frame of `href`, whether or not it's shown. */
|
|
331
|
+
_frameFor(href: string): FrameManager | undefined {
|
|
332
|
+
return this.pool.get(href);
|
|
333
|
+
}
|
|
334
|
+
|
|
330
335
|
get currentBounds(): DOMRect {
|
|
331
336
|
const ret = {
|
|
332
337
|
x: 0,
|
package/src/index.ts
CHANGED
|
@@ -82,6 +82,17 @@ export function ensureValueInRange(value: number | null | undefined, range: [num
|
|
|
82
82
|
return value >= min && value <= max ? value : undefined;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
export function ensureDecorationStyle<T extends object>(value: T | false | null | undefined): T | false | null | undefined {
|
|
86
|
+
if (value === undefined || value === null || value === false) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value !== "object") {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const type = (value as { type?: unknown }).type;
|
|
93
|
+
return type === undefined || typeof type === "string" ? value : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
export function withFallback<T>(value: T | null | undefined, defaultValue: T | null): T | null {
|
|
86
97
|
return value === undefined ? defaultValue : value;
|
|
87
98
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { GuidedNavigationObject, Link, Publication } from "@readium/shared";
|
|
2
|
+
import { decodeTextref, makeGnd } from "@readium/guided-navigation";
|
|
3
|
+
|
|
4
|
+
export interface GuidedNavigationProvider {
|
|
5
|
+
/**
|
|
6
|
+
* Guided Navigation objects of a resource, with every textref qualified by the resource's href.
|
|
7
|
+
* Resolves to undefined when the resource has nothing to read.
|
|
8
|
+
*/
|
|
9
|
+
guideFor(link: Link): Promise<GuidedNavigationObject[] | undefined>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PublicationGuidedNavigationProviderOptions {
|
|
13
|
+
/** Generates from the resource's markup without using the publication's Guided Navigation document. */
|
|
14
|
+
generateFromMarkup?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Uses the publication's Guided Navigation document when its text can be located in the resource,
|
|
19
|
+
* and generates one from the resource's markup otherwise, or always with `generateFromMarkup`.
|
|
20
|
+
* Only the objects of the resource are kept from a document covering the whole publication.
|
|
21
|
+
*/
|
|
22
|
+
export class PublicationGuidedNavigationProvider implements GuidedNavigationProvider {
|
|
23
|
+
constructor(private readonly publication: Publication, private readonly options: PublicationGuidedNavigationProviderOptions = {}) {}
|
|
24
|
+
|
|
25
|
+
async guideFor(link: Link): Promise<GuidedNavigationObject[] | undefined> {
|
|
26
|
+
const href = link.href.split("#")[0];
|
|
27
|
+
if (!this.options.generateFromMarkup) {
|
|
28
|
+
const document = await this.publication.guideForLink(link, { skipAudio: true }).catch(() => undefined);
|
|
29
|
+
if (document) {
|
|
30
|
+
const guided = this.objectsIn(document.guided, href);
|
|
31
|
+
if (guided.length > 0 && this.isLocatable(guided)) return guided;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return this.generate(link, href);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async generate(link: Link, href: string): Promise<GuidedNavigationObject[] | undefined> {
|
|
38
|
+
const resource = this.publication.get(link);
|
|
39
|
+
try {
|
|
40
|
+
const resourceLink = await resource.link();
|
|
41
|
+
if (!resourceLink.mediaType.isHTML) return undefined;
|
|
42
|
+
const doc = await resource.readAsXML();
|
|
43
|
+
if (!doc || doc.getElementsByTagNameNS("*", "parsererror").length > 0) return undefined;
|
|
44
|
+
const body = doc.querySelector("body");
|
|
45
|
+
if (!body) return undefined;
|
|
46
|
+
const mediaType = resourceLink.mediaType.essence === "text/html" ? "text/html" : "application/xhtml+xml";
|
|
47
|
+
return makeGnd(body, mediaType, { textrefs: { roles: true }, href })?.guided;
|
|
48
|
+
} finally {
|
|
49
|
+
resource.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Objects without a resource of their own belong to their parent's, and are kept when that is `href` or unknown.
|
|
54
|
+
private objectsIn(objects: GuidedNavigationObject[], href: string, parentHref?: string): GuidedNavigationObject[] {
|
|
55
|
+
return objects.flatMap(object => {
|
|
56
|
+
const objectHref = this.resourceOf(object) ?? parentHref;
|
|
57
|
+
const children = object.children ? this.objectsIn(object.children, href, objectHref) : [];
|
|
58
|
+
if (objectHref !== undefined && objectHref !== href) return children;
|
|
59
|
+
if (children.length === 0 && !object.text && !object.textref && !object.imgref) return [];
|
|
60
|
+
// A fragment-only textref, as a resource's own document may use, is in that resource.
|
|
61
|
+
const textref = object.textref?.startsWith("#") ? href + object.textref : object.textref;
|
|
62
|
+
return [new GuidedNavigationObject({ ...object, textref, children: children.length > 0 ? children : undefined })];
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private resourceOf(object: GuidedNavigationObject): string | undefined {
|
|
67
|
+
const textHref = object.textref?.split("#")[0];
|
|
68
|
+
if (textHref) return textHref;
|
|
69
|
+
return this.imageOf(object);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// An imgref only names a resource when it's an image of the reading order, as in Divina.
|
|
73
|
+
private imageOf(object: GuidedNavigationObject): string | undefined {
|
|
74
|
+
const href = object.imgref?.split("#")[0];
|
|
75
|
+
return href && this.publication.readingOrder.findWithHref(href) ? href : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Every object with text must be locatable by its own ref or an ancestor's, or following would miss it.
|
|
79
|
+
private isLocatable(objects: GuidedNavigationObject[], ancestorLocatable = false): boolean {
|
|
80
|
+
return objects.every(object => {
|
|
81
|
+
const own = object.textref ? decodeTextref(object) !== undefined : this.imageOf(object) !== undefined;
|
|
82
|
+
const locatable = ancestorLocatable || own;
|
|
83
|
+
if (object.text && !locatable) return false;
|
|
84
|
+
return !object.children || this.isLocatable(object.children, locatable);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|