@readium/shared 2.3.0 → 2.4.0-alpha.1
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/LICENSE +28 -0
- package/README.MD +7 -8
- package/dist/ar-DyHX_uy2.js +7 -0
- package/dist/da-Dct0PS3E.js +7 -0
- package/dist/fr-C5HEel98.js +7 -0
- package/dist/index.js +4993 -1
- package/dist/index.umd.cjs +3 -0
- package/dist/it-DFOBoXGy.js +7 -0
- package/dist/pt_PT-Di3sVjze.js +7 -0
- package/dist/sv-BfzAFsVN.js +7 -0
- package/package.json +26 -33
- package/src/fetcher/HttpFetcher.ts +5 -5
- package/src/fetcher/Resource.ts +10 -5
- package/src/publication/Link.ts +6 -2
- package/src/publication/Locator.ts +3 -21
- package/src/publication/Manifest.ts +10 -10
- package/src/publication/Publication.ts +1 -2
- package/src/publication/accessibility/Localization.ts +14 -12
- package/src/publication/accessibility/SupportedLanguages.ts +1 -1
- package/src/publication/encryption/Properties.ts +15 -3
- package/src/publication/epub/Metadata.ts +12 -6
- package/src/publication/epub/Properties.ts +13 -3
- package/src/publication/epub/Publication.ts +39 -18
- package/src/publication/html/Locations.ts +101 -54
- package/src/publication/opds/Properties.ts +66 -21
- package/src/publication/opds/Publication.ts +9 -3
- package/src/publication/services/content/iterators/HTMLResourceContentIterator.ts +1 -3
- package/src/publication/services/timeline/Timeline.ts +59 -268
- package/src/publication/services/timeline/TimelineItem.ts +3 -3
- package/src/publication/services/timeline/index.ts +0 -12
- package/src/util/npt.ts +0 -19
- package/types/src/fetcher/HttpFetcher.d.ts +3 -3
- package/types/src/fetcher/Resource.d.ts +6 -2
- package/types/src/publication/encryption/Properties.d.ts +9 -2
- package/types/src/publication/epub/Metadata.d.ts +6 -3
- package/types/src/publication/epub/Properties.d.ts +10 -2
- package/types/src/publication/epub/Publication.d.ts +18 -7
- package/types/src/publication/html/Locations.d.ts +44 -15
- package/types/src/publication/opds/Properties.d.ts +36 -8
- package/types/src/publication/opds/Publication.d.ts +5 -2
- package/types/src/publication/services/timeline/Timeline.d.ts +12 -78
- package/types/src/publication/services/timeline/TimelineItem.d.ts +3 -3
- package/types/src/publication/services/timeline/index.d.ts +0 -6
- package/types/src/util/npt.d.ts +0 -10
- package/dist/_virtual/preload-helper.js +0 -1
- package/dist/fetcher/Fetcher.js +0 -1
- package/dist/fetcher/HttpFetcher.js +0 -1
- package/dist/fetcher/Resource.js +0 -1
- package/dist/locales/publication-metadata/ar.json.js +0 -1
- package/dist/locales/publication-metadata/da.json.js +0 -1
- package/dist/locales/publication-metadata/el.json.js +0 -1
- package/dist/locales/publication-metadata/en.json.js +0 -1
- package/dist/locales/publication-metadata/es.json.js +0 -1
- package/dist/locales/publication-metadata/et.json.js +0 -1
- package/dist/locales/publication-metadata/fi.json.js +0 -1
- package/dist/locales/publication-metadata/fr.json.js +0 -1
- package/dist/locales/publication-metadata/it.json.js +0 -1
- package/dist/locales/publication-metadata/pl.json.js +0 -1
- package/dist/locales/publication-metadata/pt_PT.json.js +0 -1
- package/dist/locales/publication-metadata/sv.json.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/constants.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/index.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-attribute.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-class.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-fallback.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-id.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-nth-child.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-nth-of-type.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-tag.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/types.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-cartesian.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-data.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-dom.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-element-data.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-iselement.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-messages.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-options.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-powerset.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-selectors.js +0 -1
- package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-typescript.js +0 -1
- package/dist/opds/Acquisition.js +0 -1
- package/dist/opds/Availability.js +0 -1
- package/dist/opds/Copies.js +0 -1
- package/dist/opds/Holds.js +0 -1
- package/dist/opds/Price.js +0 -1
- package/dist/publication/AltIdentifier.js +0 -1
- package/dist/publication/BelongsTo.js +0 -1
- package/dist/publication/Contributor.js +0 -1
- package/dist/publication/GuidedNavigation.js +0 -1
- package/dist/publication/Layout.js +0 -1
- package/dist/publication/Link.js +0 -1
- package/dist/publication/LocalizedString.js +0 -1
- package/dist/publication/Locator.js +0 -1
- package/dist/publication/LocatorCollection.js +0 -1
- package/dist/publication/Manifest.js +0 -1
- package/dist/publication/Metadata.js +0 -1
- package/dist/publication/Profiles.js +0 -1
- package/dist/publication/Properties.js +0 -1
- package/dist/publication/Publication.js +0 -1
- package/dist/publication/PublicationCollection.js +0 -1
- package/dist/publication/ReadingProgression.js +0 -1
- package/dist/publication/Subject.js +0 -1
- package/dist/publication/TDM.js +0 -1
- package/dist/publication/accessibility/Accessibility.js +0 -1
- package/dist/publication/accessibility/AccessibilityMetadataDisplayGuide.js +0 -1
- package/dist/publication/accessibility/Localization.js +0 -1
- package/dist/publication/accessibility/SupportedLanguages.js +0 -1
- package/dist/publication/encryption/Encryption.js +0 -1
- package/dist/publication/encryption/Properties.js +0 -1
- package/dist/publication/encryption/index.js +0 -1
- package/dist/publication/epub/MediaOverlay.js +0 -1
- package/dist/publication/epub/Metadata.js +0 -1
- package/dist/publication/epub/Properties.js +0 -1
- package/dist/publication/epub/Publication.js +0 -1
- package/dist/publication/epub/index.js +0 -1
- package/dist/publication/html/DomRange.js +0 -1
- package/dist/publication/html/DomRangePoint.js +0 -1
- package/dist/publication/html/Locations.js +0 -1
- package/dist/publication/html/index.js +0 -1
- package/dist/publication/opds/Properties.js +0 -1
- package/dist/publication/opds/Publication.js +0 -1
- package/dist/publication/opds/index.js +0 -1
- package/dist/publication/services/content/Content.js +0 -2
- package/dist/publication/services/content/ContentTokenizer.js +0 -1
- package/dist/publication/services/content/Iterator.js +0 -1
- package/dist/publication/services/content/element/attributes.js +0 -1
- package/dist/publication/services/content/element/element.js +0 -1
- package/dist/publication/services/content/element/text_role.js +0 -1
- package/dist/publication/services/content/iterators/HTMLResourceContentIterator.js +0 -1
- package/dist/publication/services/content/iterators/PDFTextContentIterator.js +0 -1
- package/dist/publication/services/content/iterators/PublicationContentIterator.js +0 -1
- package/dist/publication/services/content/iterators/helpers.js +0 -1
- package/dist/publication/services/timeline/Timeline.js +0 -1
- package/dist/publication/services/timeline/audio.js +0 -1
- package/dist/publication/services/timeline/html.js +0 -1
- package/dist/publication/services/timeline/index.js +0 -1
- package/dist/util/JSONParse.js +0 -1
- package/dist/util/URITemplate.js +0 -1
- package/dist/util/mediatype/MediaType.js +0 -1
- package/dist/util/npt.js +0 -1
- package/dist/util/tokenizer/TextTokenizer.js +0 -1
- package/dist/util/tokenizer/tokenize-english/abbreviations.js +0 -1
- package/dist/util/tokenizer/tokenize-english/index.js +0 -2
- package/dist/util/tokenizer/tokenize-english/utils.js +0 -1
- package/dist/util/tokenizer/tokenize-text/index.js +0 -1
- package/dist/util/tokenizer/tokenize-text/tokens.js +0 -1
- package/src/locales/publication-metadata/ar.json +0 -402
- package/src/locales/publication-metadata/brh.json +0 -1
- package/src/locales/publication-metadata/da.json +0 -310
- package/src/locales/publication-metadata/el.json +0 -310
- package/src/locales/publication-metadata/en.json +0 -310
- package/src/locales/publication-metadata/es.json +0 -333
- package/src/locales/publication-metadata/et.json +0 -310
- package/src/locales/publication-metadata/fi.json +0 -310
- package/src/locales/publication-metadata/fr.json +0 -333
- package/src/locales/publication-metadata/he.json +0 -9
- package/src/locales/publication-metadata/it.json +0 -333
- package/src/locales/publication-metadata/lt.json +0 -8
- package/src/locales/publication-metadata/pl.json +0 -333
- package/src/locales/publication-metadata/pt_PT.json +0 -329
- package/src/locales/publication-metadata/sv.json +0 -310
- package/src/locales/publication-metadata/tr.json +0 -51
- package/src/locales/publication-metadata/uk.json +0 -1
- package/src/publication/services/timeline/audio.ts +0 -31
- package/src/publication/services/timeline/html.ts +0 -5
- package/types/src/publication/services/timeline/audio.d.ts +0 -4
- package/types/src/publication/services/timeline/html.d.ts +0 -4
|
@@ -1,31 +1,11 @@
|
|
|
1
1
|
import { Link, Links } from "../../Link.ts";
|
|
2
2
|
import { Locator } from "../../Locator.ts";
|
|
3
|
-
import { getHtmlId, getTime } from "../../html/Locations.ts";
|
|
4
|
-
import { Profile } from "../../Profiles.ts";
|
|
5
|
-
import { formatNptTime, isNptStartOfResource, parseNptTime } from "../../../util/npt.ts";
|
|
6
3
|
import { TimelineItem } from "./TimelineItem.ts";
|
|
4
|
+
import { isNptStartOfResource, parseNptTime } from "../../../util/npt.ts";
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
interface PublicationLike {
|
|
9
7
|
toc?: Links;
|
|
10
8
|
readingOrder: Links;
|
|
11
|
-
metadata?: { conformsTo?: Profile[] };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* A TOC entry, mirroring `publication.toc`'s authored hierarchy and
|
|
16
|
-
* contextualized with display-ready progression. When a publication has no
|
|
17
|
-
* `toc` at all, falls back to one flat entry per reading-order item.
|
|
18
|
-
*
|
|
19
|
-
* Exactly one of `position`/`timestamp` is populated, depending on the
|
|
20
|
-
* publication's profile.
|
|
21
|
-
*/
|
|
22
|
-
export interface ContextualizedTocEntry {
|
|
23
|
-
link: Link;
|
|
24
|
-
/** Display-ready label for non-audio profiles: a Positions List position for EPUB, a page number for PDF (e.g. "42"). */
|
|
25
|
-
position?: string;
|
|
26
|
-
/** Display-ready formatted time for audiobooks (e.g. "27:27"). */
|
|
27
|
-
timestamp?: string;
|
|
28
|
-
children?: ContextualizedTocEntry[];
|
|
29
9
|
}
|
|
30
10
|
|
|
31
11
|
/**
|
|
@@ -37,61 +17,32 @@ export interface ContextualizedTocEntry {
|
|
|
37
17
|
* 2. Populate flat children — all TOC fragment entries that reference the
|
|
38
18
|
* resource, collected depth-first in TOC declaration order.
|
|
39
19
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `locate()`) back to its entry in that hierarchy. TOC entries whose href
|
|
44
|
-
* does not match any reading order item are ignored.
|
|
20
|
+
* No TOC hierarchy is reconstructed; that requires role context and is not yet
|
|
21
|
+
* implemented. TOC entries whose href does not match any reading order item
|
|
22
|
+
* are ignored.
|
|
45
23
|
*
|
|
46
|
-
* The `depth` build option limits how many levels deep into the TOC tree
|
|
47
|
-
* title resolution
|
|
48
|
-
*
|
|
49
|
-
* `undefined` means no limit.
|
|
24
|
+
* The `depth` build option limits how many levels deep into the TOC tree both
|
|
25
|
+
* title resolution and child collection may look. Level 1 = top-level TOC
|
|
26
|
+
* entries; level 2 = their children; etc. `undefined` means no limit.
|
|
50
27
|
*/
|
|
51
28
|
export class Timeline {
|
|
52
29
|
private readonly _allItems: TimelineItem[];
|
|
53
30
|
private readonly linkMap: Map<TimelineItem, Link>;
|
|
54
|
-
private readonly _conformsTo: readonly Profile[];
|
|
55
|
-
private readonly tocLinks: Link[];
|
|
56
31
|
private _depth: number | undefined;
|
|
57
|
-
/**
|
|
58
|
-
* Depth used for `contextualizedToc` traversal. Tracks `_depth`, but is
|
|
59
|
-
* seeded independently from `Timeline.build()`'s `depth` option: unlike
|
|
60
|
-
* `_depth`, which only ever triggers `items`/`flat` re-trimming via the
|
|
61
|
-
* runtime `depth` setter, the TOC tree is walked fresh from `tocLinks`
|
|
62
|
-
* every time, so there's no equivalent "already baked in, don't reapply"
|
|
63
|
-
* hazard to avoid.
|
|
64
|
-
*/
|
|
65
|
-
private _tocDepth: number | undefined;
|
|
66
32
|
private _items: TimelineItem[] | undefined;
|
|
67
33
|
private _flat: TimelineItem[] | undefined;
|
|
68
|
-
private _toc: ContextualizedTocEntry[] | undefined;
|
|
69
|
-
private _linkToItem: Map<Link, TimelineItem> | undefined;
|
|
70
34
|
/** Populated when depth is set; maps cloned items from trimToDepth back to their Links. */
|
|
71
35
|
private _trimmedLinkMap: Map<TimelineItem, Link> = new Map();
|
|
72
36
|
|
|
73
|
-
constructor(
|
|
74
|
-
items: TimelineItem[],
|
|
75
|
-
linkMap: Map<TimelineItem, Link>,
|
|
76
|
-
conformsTo: readonly Profile[] = [],
|
|
77
|
-
tocLinks: Link[] = [],
|
|
78
|
-
tocDepth: number | undefined = undefined,
|
|
79
|
-
) {
|
|
37
|
+
constructor(items: TimelineItem[], linkMap: Map<TimelineItem, Link>) {
|
|
80
38
|
this._allItems = items;
|
|
81
39
|
this.linkMap = linkMap;
|
|
82
|
-
this._conformsTo = conformsTo;
|
|
83
|
-
this.tocLinks = tocLinks;
|
|
84
|
-
this._tocDepth = tocDepth;
|
|
85
40
|
}
|
|
86
41
|
|
|
87
|
-
static build(
|
|
88
|
-
publication: PublicationLike,
|
|
89
|
-
options: { depth?: number } = {},
|
|
90
|
-
): Timeline {
|
|
42
|
+
static build(publication: PublicationLike, options: { depth?: number } = {}): Timeline {
|
|
91
43
|
const tocLinks = publication.toc?.items ?? [];
|
|
92
44
|
const roLinks = publication.readingOrder.items;
|
|
93
45
|
const { depth } = options;
|
|
94
|
-
const conformsTo = publication.metadata?.conformsTo ?? [];
|
|
95
46
|
const linkMap = new Map<TimelineItem, Link>();
|
|
96
47
|
const items: TimelineItem[] = [];
|
|
97
48
|
|
|
@@ -101,7 +52,8 @@ export class Timeline {
|
|
|
101
52
|
|
|
102
53
|
const title =
|
|
103
54
|
ro.title ??
|
|
104
|
-
Timeline.findTitleInToc(tocLinks, bare, depth)
|
|
55
|
+
Timeline.findTitleInToc(tocLinks, bare, depth) ??
|
|
56
|
+
`Resource ${i + 1}`;
|
|
105
57
|
|
|
106
58
|
const tocChildren = Timeline.collectChildrenFromToc(tocLinks, bare, depth, 1, linkMap);
|
|
107
59
|
|
|
@@ -115,25 +67,7 @@ export class Timeline {
|
|
|
115
67
|
items.push(item);
|
|
116
68
|
}
|
|
117
69
|
|
|
118
|
-
return new Timeline(items, linkMap
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Augments all items in the timeline by applying the mapper's returned patch.
|
|
123
|
-
* The mapper receives the item and its original manifest Link, and returns
|
|
124
|
-
* a partial TimelineItem — any fields it sets will overwrite the item's
|
|
125
|
-
* current value. Use this to populate `position`, `scroll`, `role`, or any
|
|
126
|
-
* future TimelineItem fields from format-specific data.
|
|
127
|
-
*/
|
|
128
|
-
augment(mapper: (item: TimelineItem, link: Link) => Partial<TimelineItem>): void {
|
|
129
|
-
for (const item of this.flatAll) {
|
|
130
|
-
const link = this.linkFor(item);
|
|
131
|
-
if (!link) continue;
|
|
132
|
-
const patch = mapper(item, link);
|
|
133
|
-
if (patch.position !== undefined) item.position = patch.position;
|
|
134
|
-
if (patch.scroll !== undefined) item.scroll = patch.scroll;
|
|
135
|
-
if (patch.role !== undefined) item.role = patch.role;
|
|
136
|
-
}
|
|
70
|
+
return new Timeline(items, linkMap);
|
|
137
71
|
}
|
|
138
72
|
|
|
139
73
|
/**
|
|
@@ -148,10 +82,8 @@ export class Timeline {
|
|
|
148
82
|
set depth(value: number | undefined) {
|
|
149
83
|
if (this._depth === value) return;
|
|
150
84
|
this._depth = value;
|
|
151
|
-
this._tocDepth = value;
|
|
152
85
|
this._items = undefined;
|
|
153
86
|
this._flat = undefined;
|
|
154
|
-
this._toc = undefined;
|
|
155
87
|
}
|
|
156
88
|
|
|
157
89
|
/** Top-level timeline items. Cached; invalidated when `depth` changes. */
|
|
@@ -169,63 +101,26 @@ export class Timeline {
|
|
|
169
101
|
|
|
170
102
|
locate(locator: Locator): TimelineItem | undefined {
|
|
171
103
|
const href = locator.href.split("#")[0];
|
|
172
|
-
const time =
|
|
173
|
-
|
|
174
|
-
|
|
104
|
+
const time = locator.locations?.time();
|
|
105
|
+
|
|
106
|
+
let match: TimelineItem | undefined;
|
|
175
107
|
|
|
176
|
-
// Audio: best-match on t= start time.
|
|
177
108
|
if (time !== undefined) {
|
|
178
109
|
let bestTime = -Infinity;
|
|
179
|
-
let match: TimelineItem | undefined;
|
|
180
110
|
for (const item of this.flat) {
|
|
181
|
-
if (!this.itemMatchesHref(item, href)) continue;
|
|
182
111
|
const t = this.itemStartTime(item, href);
|
|
183
112
|
if (t !== undefined && t <= time && t > bestTime) {
|
|
184
113
|
bestTime = t;
|
|
185
114
|
match = item;
|
|
186
115
|
}
|
|
187
116
|
}
|
|
188
|
-
if (match) return match;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// EPUB: match on HTML ID fragment.
|
|
192
|
-
if (htmlId) {
|
|
193
|
-
const match = this.flat.find(item => {
|
|
194
|
-
if (!this.itemMatchesHref(item, href)) return false;
|
|
195
|
-
return item.references.some(ref => ref.split("#")[1] === htmlId);
|
|
196
|
-
});
|
|
197
|
-
if (match) return match;
|
|
198
117
|
}
|
|
199
118
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let bestScroll = -Infinity;
|
|
203
|
-
let match: TimelineItem | undefined;
|
|
204
|
-
for (const item of this.flat) {
|
|
205
|
-
if (!this.itemMatchesHref(item, href)) continue;
|
|
206
|
-
const s = this.itemScrollPosition(item);
|
|
207
|
-
if (s !== undefined && s <= progression && s > bestScroll) {
|
|
208
|
-
bestScroll = s;
|
|
209
|
-
match = item;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (match) return match;
|
|
213
|
-
|
|
214
|
-
// No scroll data resolved anywhere in this resource yet: guess a
|
|
215
|
-
// child by evenly dividing the fraction across its children,
|
|
216
|
-
// rather than naming the whole resource.
|
|
217
|
-
const container = this.items.find(i => this.itemMatchesHref(i, href));
|
|
218
|
-
if (container?.children?.length && !container.children.some(c => c.scroll !== undefined)) {
|
|
219
|
-
const index = Math.min(
|
|
220
|
-
Math.floor(progression * container.children.length),
|
|
221
|
-
container.children.length - 1,
|
|
222
|
-
);
|
|
223
|
-
return container.children[index];
|
|
224
|
-
}
|
|
119
|
+
if (!match) {
|
|
120
|
+
match = this.flat.find(item => this.bareHrefFromItem(item) === href);
|
|
225
121
|
}
|
|
226
122
|
|
|
227
|
-
|
|
228
|
-
return this.flat.find(item => this.itemMatchesHref(item, href));
|
|
123
|
+
return match;
|
|
229
124
|
}
|
|
230
125
|
|
|
231
126
|
adjacentTo(item: TimelineItem): { previous: TimelineItem | undefined; next: TimelineItem | undefined } {
|
|
@@ -238,11 +133,35 @@ export class Timeline {
|
|
|
238
133
|
|
|
239
134
|
segmentsForHref(href: string): TimelineItem[] {
|
|
240
135
|
const bare = href.split("#")[0];
|
|
241
|
-
const item = this.items.find(i => this.
|
|
136
|
+
const item = this.items.find(i => this.bareHrefFromItem(i) === bare);
|
|
242
137
|
if (!item) return [];
|
|
243
138
|
return item.children?.length ? item.children : [item];
|
|
244
139
|
}
|
|
245
140
|
|
|
141
|
+
itemAtProgression(href: string, progression: number, duration?: number): TimelineItem | undefined {
|
|
142
|
+
const bare = href.split("#")[0];
|
|
143
|
+
const item = this.items.find(i => this.bareHrefFromItem(i) === bare);
|
|
144
|
+
if (!item) return undefined;
|
|
145
|
+
if (!item.children?.length) return item;
|
|
146
|
+
|
|
147
|
+
if (duration !== undefined) {
|
|
148
|
+
const time = progression * duration;
|
|
149
|
+
let match: TimelineItem = item;
|
|
150
|
+
let bestTime = -Infinity;
|
|
151
|
+
for (const child of item.children) {
|
|
152
|
+
const t = this.timeFromItem(child);
|
|
153
|
+
if (t !== undefined && t <= time && t > bestTime) {
|
|
154
|
+
bestTime = t;
|
|
155
|
+
match = child;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return match;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const index = Math.min(Math.floor(progression * item.children.length), item.children.length - 1);
|
|
162
|
+
return item.children[index];
|
|
163
|
+
}
|
|
164
|
+
|
|
246
165
|
ancestors(item: TimelineItem): TimelineItem[] {
|
|
247
166
|
return this.ancestorPath(this.items, item) ?? [];
|
|
248
167
|
}
|
|
@@ -251,47 +170,11 @@ export class Timeline {
|
|
|
251
170
|
return this.linkMap.get(item) ?? this._trimmedLinkMap.get(item);
|
|
252
171
|
}
|
|
253
172
|
|
|
254
|
-
/**
|
|
255
|
-
* The authored TOC, contextualized with display-ready progression. Mirrors
|
|
256
|
-
* `publication.toc`'s authored hierarchy (respecting `depth`), falling
|
|
257
|
-
* back to one flat entry per reading-order item when there's no toc at
|
|
258
|
-
* all. Cached; invalidated when `depth` changes.
|
|
259
|
-
*/
|
|
260
|
-
get contextualizedToc(): ContextualizedTocEntry[] {
|
|
261
|
-
if (!this._toc) {
|
|
262
|
-
this._toc = this.tocLinks.length > 0
|
|
263
|
-
? this.buildTocEntries(this.tocLinks, this._tocDepth, 1)
|
|
264
|
-
: this._allItems.map(item => this.entryFor(this.linkFor(item)!, item));
|
|
265
|
-
}
|
|
266
|
-
return this._toc;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Maps a `TimelineItem` (typically from `locate()`) to its `ContextualizedTocEntry`:
|
|
271
|
-
* a direct match first, falling back to the nearest preceding toc entry
|
|
272
|
-
* for that resource when there's no exact match (e.g. a mid-resource
|
|
273
|
-
* audio position between chapter markers).
|
|
274
|
-
*/
|
|
275
|
-
tocEntryFor(current: TimelineItem): ContextualizedTocEntry | undefined {
|
|
276
|
-
const link = this.linkFor(current);
|
|
277
|
-
if (!link) return undefined;
|
|
278
|
-
|
|
279
|
-
const direct = this.findTocEntryByLink(this.contextualizedToc, link);
|
|
280
|
-
if (direct) return direct;
|
|
281
|
-
|
|
282
|
-
return this.nearestTocEntryForResource(link.href, current);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
173
|
private get flat(): TimelineItem[] {
|
|
286
174
|
if (!this._flat) this._flat = this.flattenItems(this.items);
|
|
287
175
|
return this._flat;
|
|
288
176
|
}
|
|
289
177
|
|
|
290
|
-
/** All items flattened from _allItems, depth-independent — used by augment(). */
|
|
291
|
-
private get flatAll(): TimelineItem[] {
|
|
292
|
-
return this.flattenItems(this._allItems);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
178
|
// -------------------------------------------------------------------------
|
|
296
179
|
// TOC title resolution
|
|
297
180
|
// -------------------------------------------------------------------------
|
|
@@ -332,17 +215,8 @@ export class Timeline {
|
|
|
332
215
|
const result: TimelineItem[] = [];
|
|
333
216
|
|
|
334
217
|
for (const link of tocLinks) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// so they are associated with the current resource.
|
|
338
|
-
const matchesBare = linkBare === bare || linkBare === '';
|
|
339
|
-
if (matchesBare && link.title && !Timeline.isStartOfResource(link.href)) {
|
|
340
|
-
// Prepend the resource href when the link uses a fragment-only reference.
|
|
341
|
-
const ref = linkBare === '' ? bare + link.href : link.href;
|
|
342
|
-
const child: TimelineItem = {
|
|
343
|
-
title: link.title,
|
|
344
|
-
references: [ref],
|
|
345
|
-
};
|
|
218
|
+
if (Timeline.bareHref(link.href) === bare && link.title && !Timeline.isStartOfResource(link.href)) {
|
|
219
|
+
const child: TimelineItem = { title: link.title, references: [link.href] };
|
|
346
220
|
linkMap.set(child, link);
|
|
347
221
|
result.push(child);
|
|
348
222
|
}
|
|
@@ -376,8 +250,7 @@ export class Timeline {
|
|
|
376
250
|
const fragments: Link[] = [];
|
|
377
251
|
|
|
378
252
|
for (const link of tocLinks) {
|
|
379
|
-
|
|
380
|
-
if ((linkBare === bare || linkBare === '') && link.title) {
|
|
253
|
+
if (Timeline.bareHref(link.href) === bare && link.title) {
|
|
381
254
|
if (Timeline.isStartOfResource(link.href)) {
|
|
382
255
|
atStart.push(link);
|
|
383
256
|
} else {
|
|
@@ -409,88 +282,6 @@ export class Timeline {
|
|
|
409
282
|
return match !== null && isNptStartOfResource(match[1]);
|
|
410
283
|
}
|
|
411
284
|
|
|
412
|
-
// -------------------------------------------------------------------------
|
|
413
|
-
// Contextualized TOC
|
|
414
|
-
// -------------------------------------------------------------------------
|
|
415
|
-
|
|
416
|
-
private get linkToItem(): Map<Link, TimelineItem> {
|
|
417
|
-
if (!this._linkToItem) {
|
|
418
|
-
this._linkToItem = new Map();
|
|
419
|
-
for (const [item, link] of this.linkMap) this._linkToItem.set(link, item);
|
|
420
|
-
}
|
|
421
|
-
return this._linkToItem;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private buildTocEntries(links: Link[], maxDepth: number | undefined, currentDepth: number): ContextualizedTocEntry[] {
|
|
425
|
-
if (maxDepth !== undefined && currentDepth > maxDepth) return [];
|
|
426
|
-
return links.map(link => {
|
|
427
|
-
const item = this.linkToItem.get(link) ?? this.resourceStartItem(link.href);
|
|
428
|
-
const kids = link.children?.items?.length
|
|
429
|
-
? this.buildTocEntries(link.children.items, maxDepth, currentDepth + 1)
|
|
430
|
-
: [];
|
|
431
|
-
return { ...this.entryFor(link, item), children: kids.length > 0 ? kids : undefined };
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** All toc Links flattened, respecting the same depth limit as `buildTocEntries`. */
|
|
436
|
-
private tocLinksFlat(links: Link[], maxDepth: number | undefined, currentDepth: number): Link[] {
|
|
437
|
-
if (maxDepth !== undefined && currentDepth > maxDepth) return [];
|
|
438
|
-
const result: Link[] = [];
|
|
439
|
-
for (const link of links) {
|
|
440
|
-
result.push(link);
|
|
441
|
-
if (link.children?.items?.length) {
|
|
442
|
-
result.push(...this.tocLinksFlat(link.children.items, maxDepth, currentDepth + 1));
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return result;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
private entryFor(link: Link, item: TimelineItem | undefined): ContextualizedTocEntry {
|
|
449
|
-
const isAudio = this._conformsTo.includes(Profile.AUDIOBOOK);
|
|
450
|
-
return {
|
|
451
|
-
link,
|
|
452
|
-
position: !isAudio && item?.position !== undefined ? String(item.position) : undefined,
|
|
453
|
-
timestamp: isAudio && item?.position !== undefined ? formatNptTime(item.position) : undefined,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
private resourceStartItem(href: string): TimelineItem | undefined {
|
|
458
|
-
const bare = Timeline.bareHref(href);
|
|
459
|
-
return this._allItems.find(i => this.itemMatchesHref(i, bare));
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
private findTocEntryByLink(entries: ContextualizedTocEntry[], link: Link): ContextualizedTocEntry | undefined {
|
|
463
|
-
for (const entry of entries) {
|
|
464
|
-
if (entry.link === link) return entry;
|
|
465
|
-
if (entry.children) {
|
|
466
|
-
const found = this.findTocEntryByLink(entry.children, link);
|
|
467
|
-
if (found) return found;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return undefined;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
private nearestTocEntryForResource(href: string, current: TimelineItem): ContextualizedTocEntry | undefined {
|
|
474
|
-
const bare = Timeline.bareHref(href);
|
|
475
|
-
// Re-walk with raw items (not the display-formatted TocEntry tree) so we can compare
|
|
476
|
-
// current.position/current.scroll directly against each candidate's source TimelineItem.
|
|
477
|
-
const candidates = this.tocLinksFlat(this.tocLinks, this._tocDepth, 1)
|
|
478
|
-
.filter(link => Timeline.bareHref(link.href) === bare)
|
|
479
|
-
.map(link => ({ link, item: this.linkToItem.get(link) ?? this.resourceStartItem(link.href) }));
|
|
480
|
-
if (candidates.length === 0) return undefined;
|
|
481
|
-
|
|
482
|
-
const isAudio = this._conformsTo.includes(Profile.AUDIOBOOK);
|
|
483
|
-
const currentValue = isAudio ? (current.position ?? 0) : (current.scroll ?? 0);
|
|
484
|
-
let best: { link: Link; item: TimelineItem | undefined } | undefined;
|
|
485
|
-
let bestValue = -Infinity;
|
|
486
|
-
for (const c of candidates) {
|
|
487
|
-
const v = isAudio ? (c.item?.position ?? 0) : (c.item?.scroll ?? 0);
|
|
488
|
-
if (v <= currentValue && v > bestValue) { bestValue = v; best = c; }
|
|
489
|
-
}
|
|
490
|
-
const chosen = best ?? candidates[0];
|
|
491
|
-
return this.findTocEntryByLink(this.contextualizedToc, chosen.link);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
285
|
// -------------------------------------------------------------------------
|
|
495
286
|
// Shared utilities
|
|
496
287
|
// -------------------------------------------------------------------------
|
|
@@ -528,8 +319,7 @@ export class Timeline {
|
|
|
528
319
|
const hashIndex = ref.indexOf("#");
|
|
529
320
|
const refHref = hashIndex >= 0 ? ref.slice(0, hashIndex) : ref;
|
|
530
321
|
const refFragment = hashIndex >= 0 ? ref.slice(hashIndex + 1) : undefined;
|
|
531
|
-
|
|
532
|
-
const effectiveHref = refHref || href;
|
|
322
|
+
const effectiveHref = refHref || this.bareHrefFromItem(item);
|
|
533
323
|
if (effectiveHref !== href) continue;
|
|
534
324
|
if (!refFragment) return undefined;
|
|
535
325
|
const match = refFragment.match(/(?:^|&)t=([^&]+)/);
|
|
@@ -538,12 +328,17 @@ export class Timeline {
|
|
|
538
328
|
return undefined;
|
|
539
329
|
}
|
|
540
330
|
|
|
541
|
-
private
|
|
542
|
-
return item.references.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
331
|
+
private bareHrefFromItem(item: TimelineItem): string {
|
|
332
|
+
return (item.references[0] ?? "").split("#")[0];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private timeFromItem(item: TimelineItem): number | undefined {
|
|
336
|
+
const ref = item.references[0];
|
|
337
|
+
if (!ref) return undefined;
|
|
338
|
+
const fragment = ref.split("#")[1];
|
|
339
|
+
if (!fragment) return undefined;
|
|
340
|
+
const match = fragment.match(/(?:^|&)t=([^&]+)/);
|
|
341
|
+
return match ? parseNptTime(match[1]) : undefined;
|
|
547
342
|
}
|
|
548
343
|
|
|
549
344
|
private ancestorPath(items: TimelineItem[], target: TimelineItem): TimelineItem[] | null {
|
|
@@ -557,10 +352,6 @@ export class Timeline {
|
|
|
557
352
|
return null;
|
|
558
353
|
}
|
|
559
354
|
|
|
560
|
-
private itemScrollPosition(item: TimelineItem): number | undefined {
|
|
561
|
-
return item.scroll;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
355
|
private static bareHref(href: string): string {
|
|
565
356
|
return href.split("#")[0];
|
|
566
357
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* show previous/next as chapter titles, group search results by chapter, etc.
|
|
7
7
|
*/
|
|
8
8
|
export interface TimelineItem {
|
|
9
|
-
/** Display title of this entry
|
|
10
|
-
title
|
|
9
|
+
/** Display title of this entry. */
|
|
10
|
+
title: string;
|
|
11
11
|
/**
|
|
12
12
|
* References as hrefs with optional fragments.
|
|
13
13
|
* e.g. ["track1.mp3#t=60"] for audio, ["chapter1.html"] for EPUB, ["#page=6"] for PDF.
|
|
@@ -15,7 +15,7 @@ export interface TimelineItem {
|
|
|
15
15
|
references: string[];
|
|
16
16
|
/** Structural roles of this entry, e.g. ["chapter"], ["part"]. */
|
|
17
17
|
role?: string[];
|
|
18
|
-
/**
|
|
18
|
+
/** Position number in the reading order context. */
|
|
19
19
|
position?: number;
|
|
20
20
|
/** Scroll progression within the resource (0 to 1), for entries that start mid-way in a resource. */
|
|
21
21
|
scroll?: number;
|
|
@@ -1,14 +1,2 @@
|
|
|
1
|
-
import { Profile } from "../../Profiles.ts";
|
|
2
|
-
import { PublicationLike, Timeline } from "./Timeline.ts";
|
|
3
|
-
import { buildAudioTimeline } from "./audio.ts";
|
|
4
|
-
import { buildHtmlTimeline } from "./html.ts";
|
|
5
|
-
|
|
6
1
|
export * from './Timeline.ts';
|
|
7
2
|
export * from './TimelineItem.ts';
|
|
8
|
-
export { buildHtmlTimeline } from './html.ts';
|
|
9
|
-
export { buildAudioTimeline } from './audio.ts';
|
|
10
|
-
|
|
11
|
-
export function buildTimeline(pub: PublicationLike, options?: { depth?: number }): Timeline {
|
|
12
|
-
const isAudio = pub.metadata?.conformsTo?.includes(Profile.AUDIOBOOK) ?? false;
|
|
13
|
-
return isAudio ? buildAudioTimeline(pub, options) : buildHtmlTimeline(pub, options);
|
|
14
|
-
}
|
package/src/util/npt.ts
CHANGED
|
@@ -49,22 +49,3 @@ export function isNptStartOfResource(raw: string): boolean {
|
|
|
49
49
|
const t = parseNptTime(raw);
|
|
50
50
|
return t !== undefined && t === 0;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Formats a duration in seconds as a human-readable string suitable for
|
|
55
|
-
* display in a Table of Contents or progress indicator.
|
|
56
|
-
*
|
|
57
|
-
* Outputs "H:MM:SS" when hours > 0, otherwise "M:SS".
|
|
58
|
-
* Minutes and seconds are always zero-padded to two digits.
|
|
59
|
-
*
|
|
60
|
-
* Examples: 0 → "0:00", 90 → "1:30", 3661 → "1:01:01"
|
|
61
|
-
*/
|
|
62
|
-
export function formatNptTime(seconds: number): string {
|
|
63
|
-
const totalSeconds = Math.floor(seconds);
|
|
64
|
-
const h = Math.floor(totalSeconds / 3600);
|
|
65
|
-
const m = Math.floor((totalSeconds % 3600) / 60);
|
|
66
|
-
const s = totalSeconds % 60;
|
|
67
|
-
const mm = String(m).padStart(2, "0");
|
|
68
|
-
const ss = String(s).padStart(2, "0");
|
|
69
|
-
return h > 0 ? `${h}:${mm}:${ss}` : `${m}:${ss}`;
|
|
70
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Link } from '../publication/Link.ts';
|
|
2
2
|
import { Fetcher } from './Fetcher.ts';
|
|
3
|
-
import { NumberRange, Resource } from './Resource.ts';
|
|
3
|
+
import { NumberRange, Resource, ResourceReadOptions } from './Resource.ts';
|
|
4
4
|
export type FetchImplementation = (input: RequestInfo | URL, init?: RequestInit | undefined) => Promise<Response>;
|
|
5
5
|
export declare class HttpFetcher implements Fetcher {
|
|
6
6
|
private readonly baseUrl?;
|
|
@@ -20,9 +20,9 @@ export declare class HttpResource implements Resource {
|
|
|
20
20
|
private headResponse;
|
|
21
21
|
close(): void;
|
|
22
22
|
link(): Promise<Link>;
|
|
23
|
-
read(range?: NumberRange): Promise<Uint8Array | undefined>;
|
|
23
|
+
read(range?: NumberRange, options?: ResourceReadOptions): Promise<Uint8Array | undefined>;
|
|
24
24
|
length(): Promise<number>;
|
|
25
25
|
readAsJSON(): Promise<unknown>;
|
|
26
26
|
readAsString(): Promise<string>;
|
|
27
|
-
readAsXML(): Promise<
|
|
27
|
+
readAsXML(): Promise<Document>;
|
|
28
28
|
}
|
|
@@ -4,12 +4,16 @@ export declare class NumberRange {
|
|
|
4
4
|
readonly endInclusive: number;
|
|
5
5
|
constructor(start: number, endInclusive: number);
|
|
6
6
|
}
|
|
7
|
+
export interface ResourceReadOptions {
|
|
8
|
+
/** Aborts the underlying request, e.g. when a prefetched resource is no longer needed */
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
7
11
|
export declare abstract class Resource {
|
|
8
12
|
abstract link(): Promise<Link>;
|
|
9
13
|
abstract length(): Promise<number | undefined>;
|
|
10
|
-
abstract read(range?: NumberRange): Promise<Uint8Array | undefined>;
|
|
14
|
+
abstract read(range?: NumberRange, options?: ResourceReadOptions): Promise<Uint8Array | undefined>;
|
|
11
15
|
readAsString(): Promise<string | undefined>;
|
|
12
16
|
readAsJSON(): Promise<unknown | undefined>;
|
|
13
|
-
readAsXML(): Promise<
|
|
17
|
+
readAsXML(): Promise<Document | undefined>;
|
|
14
18
|
abstract close(): void;
|
|
15
19
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import { Properties } from '../Properties.ts';
|
|
2
1
|
import { Encryption } from './Encryption.ts';
|
|
3
|
-
|
|
2
|
+
declare module '../Properties' {
|
|
3
|
+
interface Properties {
|
|
4
|
+
/**
|
|
5
|
+
* Indicates that a resource is encrypted/obfuscated and provides relevant information for
|
|
6
|
+
* decryption.
|
|
7
|
+
*/
|
|
8
|
+
encryption: Encryption | undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { MediaOverlay } from "./MediaOverlay.ts";
|
|
2
|
+
declare module '../Metadata' {
|
|
3
|
+
interface Metadata {
|
|
4
|
+
getMediaOverlay(): MediaOverlay | undefined;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
declare module '../Properties' {
|
|
2
|
+
interface Properties {
|
|
3
|
+
/**
|
|
4
|
+
* Identifies content contained in the linked resource, that cannot be strictly identified using a
|
|
5
|
+
* media type.
|
|
6
|
+
*/
|
|
7
|
+
getContains(): Set<string> | undefined;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { Links } from '../Link.ts';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
declare module '../Publication' {
|
|
3
|
+
interface Publication {
|
|
4
|
+
/**
|
|
5
|
+
* Provides navigation to positions in the Publication content that correspond to the locations of
|
|
6
|
+
* page boundaries present in a print source being represented by this EPUB Publication.
|
|
7
|
+
*/
|
|
8
|
+
getPageList(): Links | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Identifies fundamental structural components of the publication in order to enable Reading
|
|
11
|
+
* Systems to provide the User efficient access to them.
|
|
12
|
+
*/
|
|
13
|
+
getLandmarks(): Links | undefined;
|
|
14
|
+
getListOfAudioClips(): Links | undefined;
|
|
15
|
+
getListOfIllustrations(): Links | undefined;
|
|
16
|
+
getListOfTables(): Links | undefined;
|
|
17
|
+
getListOfVideoClips(): Links | undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|