@readium/shared 2.3.1 → 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.
Files changed (166) hide show
  1. package/README.MD +7 -8
  2. package/dist/ar-DyHX_uy2.js +7 -0
  3. package/dist/da-Dct0PS3E.js +7 -0
  4. package/dist/fr-C5HEel98.js +7 -0
  5. package/dist/index.js +4993 -1
  6. package/dist/index.umd.cjs +3 -0
  7. package/dist/it-DFOBoXGy.js +7 -0
  8. package/dist/pt_PT-Di3sVjze.js +7 -0
  9. package/dist/sv-BfzAFsVN.js +7 -0
  10. package/package.json +19 -26
  11. package/src/fetcher/HttpFetcher.ts +5 -5
  12. package/src/fetcher/Resource.ts +10 -5
  13. package/src/publication/Link.ts +6 -2
  14. package/src/publication/Locator.ts +3 -21
  15. package/src/publication/Manifest.ts +10 -10
  16. package/src/publication/Publication.ts +1 -2
  17. package/src/publication/accessibility/Localization.ts +14 -12
  18. package/src/publication/accessibility/SupportedLanguages.ts +1 -1
  19. package/src/publication/encryption/Properties.ts +15 -3
  20. package/src/publication/epub/Metadata.ts +12 -6
  21. package/src/publication/epub/Properties.ts +13 -3
  22. package/src/publication/epub/Publication.ts +39 -18
  23. package/src/publication/html/Locations.ts +101 -54
  24. package/src/publication/opds/Properties.ts +66 -21
  25. package/src/publication/opds/Publication.ts +9 -3
  26. package/src/publication/services/content/iterators/HTMLResourceContentIterator.ts +1 -3
  27. package/src/publication/services/timeline/Timeline.ts +59 -305
  28. package/src/publication/services/timeline/TimelineItem.ts +3 -3
  29. package/src/publication/services/timeline/index.ts +0 -12
  30. package/src/util/npt.ts +0 -19
  31. package/types/src/fetcher/HttpFetcher.d.ts +3 -3
  32. package/types/src/fetcher/Resource.d.ts +6 -2
  33. package/types/src/publication/encryption/Properties.d.ts +9 -2
  34. package/types/src/publication/epub/Metadata.d.ts +6 -3
  35. package/types/src/publication/epub/Properties.d.ts +10 -2
  36. package/types/src/publication/epub/Publication.d.ts +18 -7
  37. package/types/src/publication/html/Locations.d.ts +44 -15
  38. package/types/src/publication/opds/Properties.d.ts +36 -8
  39. package/types/src/publication/opds/Publication.d.ts +5 -2
  40. package/types/src/publication/services/timeline/Timeline.d.ts +12 -91
  41. package/types/src/publication/services/timeline/TimelineItem.d.ts +3 -3
  42. package/types/src/publication/services/timeline/index.d.ts +0 -6
  43. package/types/src/util/npt.d.ts +0 -10
  44. package/dist/_virtual/preload-helper.js +0 -1
  45. package/dist/fetcher/Fetcher.js +0 -1
  46. package/dist/fetcher/HttpFetcher.js +0 -1
  47. package/dist/fetcher/Resource.js +0 -1
  48. package/dist/locales/publication-metadata/ar.json.js +0 -1
  49. package/dist/locales/publication-metadata/da.json.js +0 -1
  50. package/dist/locales/publication-metadata/el.json.js +0 -1
  51. package/dist/locales/publication-metadata/en.json.js +0 -1
  52. package/dist/locales/publication-metadata/es.json.js +0 -1
  53. package/dist/locales/publication-metadata/et.json.js +0 -1
  54. package/dist/locales/publication-metadata/fi.json.js +0 -1
  55. package/dist/locales/publication-metadata/fr.json.js +0 -1
  56. package/dist/locales/publication-metadata/it.json.js +0 -1
  57. package/dist/locales/publication-metadata/pl.json.js +0 -1
  58. package/dist/locales/publication-metadata/pt_PT.json.js +0 -1
  59. package/dist/locales/publication-metadata/sv.json.js +0 -1
  60. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/constants.js +0 -1
  61. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/index.js +0 -1
  62. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-attribute.js +0 -1
  63. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-class.js +0 -1
  64. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-fallback.js +0 -1
  65. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-id.js +0 -1
  66. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-nth-child.js +0 -1
  67. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-nth-of-type.js +0 -1
  68. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-tag.js +0 -1
  69. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/types.js +0 -1
  70. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-cartesian.js +0 -1
  71. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-data.js +0 -1
  72. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-dom.js +0 -1
  73. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-element-data.js +0 -1
  74. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-iselement.js +0 -1
  75. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-messages.js +0 -1
  76. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-options.js +0 -1
  77. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-powerset.js +0 -1
  78. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-selectors.js +0 -1
  79. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-typescript.js +0 -1
  80. package/dist/opds/Acquisition.js +0 -1
  81. package/dist/opds/Availability.js +0 -1
  82. package/dist/opds/Copies.js +0 -1
  83. package/dist/opds/Holds.js +0 -1
  84. package/dist/opds/Price.js +0 -1
  85. package/dist/publication/AltIdentifier.js +0 -1
  86. package/dist/publication/BelongsTo.js +0 -1
  87. package/dist/publication/Contributor.js +0 -1
  88. package/dist/publication/GuidedNavigation.js +0 -1
  89. package/dist/publication/Layout.js +0 -1
  90. package/dist/publication/Link.js +0 -1
  91. package/dist/publication/LocalizedString.js +0 -1
  92. package/dist/publication/Locator.js +0 -1
  93. package/dist/publication/LocatorCollection.js +0 -1
  94. package/dist/publication/Manifest.js +0 -1
  95. package/dist/publication/Metadata.js +0 -1
  96. package/dist/publication/Profiles.js +0 -1
  97. package/dist/publication/Properties.js +0 -1
  98. package/dist/publication/Publication.js +0 -1
  99. package/dist/publication/PublicationCollection.js +0 -1
  100. package/dist/publication/ReadingProgression.js +0 -1
  101. package/dist/publication/Subject.js +0 -1
  102. package/dist/publication/TDM.js +0 -1
  103. package/dist/publication/accessibility/Accessibility.js +0 -1
  104. package/dist/publication/accessibility/AccessibilityMetadataDisplayGuide.js +0 -1
  105. package/dist/publication/accessibility/Localization.js +0 -1
  106. package/dist/publication/accessibility/SupportedLanguages.js +0 -1
  107. package/dist/publication/encryption/Encryption.js +0 -1
  108. package/dist/publication/encryption/Properties.js +0 -1
  109. package/dist/publication/encryption/index.js +0 -1
  110. package/dist/publication/epub/MediaOverlay.js +0 -1
  111. package/dist/publication/epub/Metadata.js +0 -1
  112. package/dist/publication/epub/Properties.js +0 -1
  113. package/dist/publication/epub/Publication.js +0 -1
  114. package/dist/publication/epub/index.js +0 -1
  115. package/dist/publication/html/DomRange.js +0 -1
  116. package/dist/publication/html/DomRangePoint.js +0 -1
  117. package/dist/publication/html/Locations.js +0 -1
  118. package/dist/publication/html/index.js +0 -1
  119. package/dist/publication/opds/Properties.js +0 -1
  120. package/dist/publication/opds/Publication.js +0 -1
  121. package/dist/publication/opds/index.js +0 -1
  122. package/dist/publication/services/content/Content.js +0 -2
  123. package/dist/publication/services/content/ContentTokenizer.js +0 -1
  124. package/dist/publication/services/content/Iterator.js +0 -1
  125. package/dist/publication/services/content/element/attributes.js +0 -1
  126. package/dist/publication/services/content/element/element.js +0 -1
  127. package/dist/publication/services/content/element/text_role.js +0 -1
  128. package/dist/publication/services/content/iterators/HTMLResourceContentIterator.js +0 -1
  129. package/dist/publication/services/content/iterators/PDFTextContentIterator.js +0 -1
  130. package/dist/publication/services/content/iterators/PublicationContentIterator.js +0 -1
  131. package/dist/publication/services/content/iterators/helpers.js +0 -1
  132. package/dist/publication/services/timeline/Timeline.js +0 -1
  133. package/dist/publication/services/timeline/audio.js +0 -1
  134. package/dist/publication/services/timeline/html.js +0 -1
  135. package/dist/publication/services/timeline/index.js +0 -1
  136. package/dist/util/JSONParse.js +0 -1
  137. package/dist/util/URITemplate.js +0 -1
  138. package/dist/util/mediatype/MediaType.js +0 -1
  139. package/dist/util/npt.js +0 -1
  140. package/dist/util/tokenizer/TextTokenizer.js +0 -1
  141. package/dist/util/tokenizer/tokenize-english/abbreviations.js +0 -1
  142. package/dist/util/tokenizer/tokenize-english/index.js +0 -2
  143. package/dist/util/tokenizer/tokenize-english/utils.js +0 -1
  144. package/dist/util/tokenizer/tokenize-text/index.js +0 -1
  145. package/dist/util/tokenizer/tokenize-text/tokens.js +0 -1
  146. package/src/locales/publication-metadata/ar.json +0 -402
  147. package/src/locales/publication-metadata/brh.json +0 -1
  148. package/src/locales/publication-metadata/da.json +0 -310
  149. package/src/locales/publication-metadata/el.json +0 -310
  150. package/src/locales/publication-metadata/en.json +0 -310
  151. package/src/locales/publication-metadata/es.json +0 -333
  152. package/src/locales/publication-metadata/et.json +0 -310
  153. package/src/locales/publication-metadata/fi.json +0 -310
  154. package/src/locales/publication-metadata/fr.json +0 -333
  155. package/src/locales/publication-metadata/he.json +0 -9
  156. package/src/locales/publication-metadata/it.json +0 -333
  157. package/src/locales/publication-metadata/lt.json +0 -8
  158. package/src/locales/publication-metadata/pl.json +0 -333
  159. package/src/locales/publication-metadata/pt_PT.json +0 -329
  160. package/src/locales/publication-metadata/sv.json +0 -310
  161. package/src/locales/publication-metadata/tr.json +0 -51
  162. package/src/locales/publication-metadata/uk.json +0 -1
  163. package/src/publication/services/timeline/audio.ts +0 -31
  164. package/src/publication/services/timeline/html.ts +0 -5
  165. package/types/src/publication/services/timeline/audio.d.ts +0 -4
  166. 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
- export interface PublicationLike {
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
- * `contextualizedToc` returns the authored TOC hierarchy (see
41
- * `ContextualizedTocEntry`), each entry contextualized with progression.
42
- * `tocEntryFor(item)` maps a `TimelineItem` (e.g. from
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, child collection, and `contextualizedToc` traversal may
48
- * look. Level 1 = top-level TOC entries; level 2 = their children; etc.
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, conformsTo, tocLinks, depth);
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 = getTime(locator.locations);
173
- const htmlId = getHtmlId(locator.locations);
174
- const progression = locator.locations.progression;
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
117
  }
190
118
 
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
- }
199
-
200
- // EPUB: best-match on scroll progression.
201
- if (progression !== undefined) {
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
- // Fallback: bare href match.
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.itemMatchesHref(i, bare));
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,52 +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
- * trying three fallbacks in order:
272
- * 1. A direct match on `current`'s own link.
273
- * 2. The nearest preceding toc entry within the same resource.
274
- * 3. When no toc entry references the resource at all, the nearest
275
- * preceding resource's toc entry.
276
- */
277
- tocEntryFor(current: TimelineItem): ContextualizedTocEntry | undefined {
278
- const link = this.linkFor(current);
279
- if (!link) return undefined;
280
-
281
- const direct = this.findTocEntryByLink(this.contextualizedToc, link);
282
- if (direct) return direct;
283
-
284
- const nearest = this.nearestTocEntryForResource(link.href, current);
285
- if (nearest) return nearest;
286
-
287
- return this.previousResolvedTocEntry(link);
288
- }
289
-
290
173
  private get flat(): TimelineItem[] {
291
174
  if (!this._flat) this._flat = this.flattenItems(this.items);
292
175
  return this._flat;
293
176
  }
294
177
 
295
- /** All items flattened from _allItems, depth-independent — used by augment(). */
296
- private get flatAll(): TimelineItem[] {
297
- return this.flattenItems(this._allItems);
298
- }
299
-
300
178
  // -------------------------------------------------------------------------
301
179
  // TOC title resolution
302
180
  // -------------------------------------------------------------------------
@@ -337,17 +215,8 @@ export class Timeline {
337
215
  const result: TimelineItem[] = [];
338
216
 
339
217
  for (const link of tocLinks) {
340
- const linkBare = Timeline.bareHref(link.href);
341
- // Fragment-only hrefs (e.g. "#t=60" in single-track audio) have no bare href,
342
- // so they are associated with the current resource.
343
- const matchesBare = linkBare === bare || linkBare === '';
344
- if (matchesBare && link.title && !Timeline.isStartOfResource(link.href)) {
345
- // Prepend the resource href when the link uses a fragment-only reference.
346
- const ref = linkBare === '' ? bare + link.href : link.href;
347
- const child: TimelineItem = {
348
- title: link.title,
349
- references: [ref],
350
- };
218
+ if (Timeline.bareHref(link.href) === bare && link.title && !Timeline.isStartOfResource(link.href)) {
219
+ const child: TimelineItem = { title: link.title, references: [link.href] };
351
220
  linkMap.set(child, link);
352
221
  result.push(child);
353
222
  }
@@ -381,8 +250,7 @@ export class Timeline {
381
250
  const fragments: Link[] = [];
382
251
 
383
252
  for (const link of tocLinks) {
384
- const linkBare = Timeline.bareHref(link.href);
385
- if ((linkBare === bare || linkBare === '') && link.title) {
253
+ if (Timeline.bareHref(link.href) === bare && link.title) {
386
254
  if (Timeline.isStartOfResource(link.href)) {
387
255
  atStart.push(link);
388
256
  } else {
@@ -414,120 +282,6 @@ export class Timeline {
414
282
  return match !== null && isNptStartOfResource(match[1]);
415
283
  }
416
284
 
417
- // -------------------------------------------------------------------------
418
- // Contextualized TOC
419
- // -------------------------------------------------------------------------
420
-
421
- private get linkToItem(): Map<Link, TimelineItem> {
422
- if (!this._linkToItem) {
423
- this._linkToItem = new Map();
424
- for (const [item, link] of this.linkMap) this._linkToItem.set(link, item);
425
- }
426
- return this._linkToItem;
427
- }
428
-
429
- private buildTocEntries(links: Link[], maxDepth: number | undefined, currentDepth: number): ContextualizedTocEntry[] {
430
- if (maxDepth !== undefined && currentDepth > maxDepth) return [];
431
- return links.map(link => {
432
- const item = this.linkToItem.get(link) ?? this.resourceStartItem(link.href);
433
- const kids = link.children?.items?.length
434
- ? this.buildTocEntries(link.children.items, maxDepth, currentDepth + 1)
435
- : [];
436
- return { ...this.entryFor(link, item), children: kids.length > 0 ? kids : undefined };
437
- });
438
- }
439
-
440
- /** All toc Links flattened, respecting the same depth limit as `buildTocEntries`. */
441
- private tocLinksFlat(links: Link[], maxDepth: number | undefined, currentDepth: number): Link[] {
442
- if (maxDepth !== undefined && currentDepth > maxDepth) return [];
443
- const result: Link[] = [];
444
- for (const link of links) {
445
- result.push(link);
446
- if (link.children?.items?.length) {
447
- result.push(...this.tocLinksFlat(link.children.items, maxDepth, currentDepth + 1));
448
- }
449
- }
450
- return result;
451
- }
452
-
453
- private entryFor(link: Link, item: TimelineItem | undefined): ContextualizedTocEntry {
454
- const isAudio = this._conformsTo.includes(Profile.AUDIOBOOK);
455
- return {
456
- link,
457
- position: !isAudio && item?.position !== undefined ? String(item.position) : undefined,
458
- timestamp: isAudio && item?.position !== undefined ? formatNptTime(item.position) : undefined,
459
- };
460
- }
461
-
462
- private resourceStartItem(href: string): TimelineItem | undefined {
463
- const bare = Timeline.bareHref(href);
464
- return this._allItems.find(i => this.itemMatchesHref(i, bare));
465
- }
466
-
467
- private findTocEntryByLink(entries: ContextualizedTocEntry[], link: Link): ContextualizedTocEntry | undefined {
468
- for (const entry of entries) {
469
- if (entry.link === link) return entry;
470
- if (entry.children) {
471
- const found = this.findTocEntryByLink(entry.children, link);
472
- if (found) return found;
473
- }
474
- }
475
- return undefined;
476
- }
477
-
478
- /**
479
- * Tier-2 fallback for `tocEntryFor`: among the toc entries that reference
480
- * `href`'s own resource (e.g. chapter markers within one audio file),
481
- * return the nearest one at or before `current`'s position/scroll.
482
- */
483
- private nearestTocEntryForResource(href: string, current: TimelineItem): ContextualizedTocEntry | undefined {
484
- const bare = Timeline.bareHref(href);
485
- // Re-walk with raw items (not the display-formatted TocEntry tree) so we can compare
486
- // current.position/current.scroll directly against each candidate's source TimelineItem.
487
- const candidates = this.tocLinksFlat(this.tocLinks, this._tocDepth, 1)
488
- .filter(link => Timeline.bareHref(link.href) === bare)
489
- .map(link => ({ link, item: this.linkToItem.get(link) ?? this.resourceStartItem(link.href) }));
490
- if (candidates.length === 0) return undefined;
491
-
492
- const isAudio = this._conformsTo.includes(Profile.AUDIOBOOK);
493
- const currentValue = isAudio ? (current.position ?? 0) : (current.scroll ?? 0);
494
- let best: { link: Link; item: TimelineItem | undefined } | undefined;
495
- let bestValue = -Infinity;
496
- for (const c of candidates) {
497
- const v = isAudio ? (c.item?.position ?? 0) : (c.item?.scroll ?? 0);
498
- if (v <= currentValue && v > bestValue) { bestValue = v; best = c; }
499
- }
500
- const chosen = best ?? candidates[0];
501
- return this.findTocEntryByLink(this.contextualizedToc, chosen.link);
502
- }
503
-
504
- /**
505
- * Tier-3 fallback for `tocEntryFor`: when no toc entry references
506
- * `href`'s resource at all, walk backward through the reading order and
507
- * return the nearest preceding resource's toc entry.
508
- */
509
- private previousResolvedTocEntry(link: Link): ContextualizedTocEntry | undefined {
510
- const index = this._allItems.findIndex(item => this.linkFor(item) === link);
511
- if (index === -1) return undefined;
512
-
513
- for (let i = index - 1; i >= 0; i--) {
514
- const precedingLink = this.linkFor(this._allItems[i]);
515
- if (!precedingLink) continue;
516
- const precedingBare = Timeline.bareHref(precedingLink.href);
517
- const { atStart, fragments } = Timeline.collectTocCandidates(this.tocLinks, precedingBare, this._tocDepth, 1);
518
- // Exclude fragment-only entries (bareHref "") — collectTocCandidates
519
- // treats them as matching any bare href for single-track audio, but
520
- // here they must belong to this specific preceding resource.
521
- const ownAtStart = atStart.filter(l => Timeline.bareHref(l.href) === precedingBare);
522
- const ownFragments = fragments.filter(l => Timeline.bareHref(l.href) === precedingBare);
523
- const chosen = ownAtStart[0] ?? (ownFragments.length > 0 ? ownFragments[ownFragments.length - 1] : undefined);
524
- if (!chosen) continue;
525
- const entry = this.findTocEntryByLink(this.contextualizedToc, chosen);
526
- if (entry) return entry;
527
- }
528
- return undefined;
529
- }
530
-
531
285
  // -------------------------------------------------------------------------
532
286
  // Shared utilities
533
287
  // -------------------------------------------------------------------------
@@ -565,8 +319,7 @@ export class Timeline {
565
319
  const hashIndex = ref.indexOf("#");
566
320
  const refHref = hashIndex >= 0 ? ref.slice(0, hashIndex) : ref;
567
321
  const refFragment = hashIndex >= 0 ? ref.slice(hashIndex + 1) : undefined;
568
- // Fragment-only reference (e.g. "#t=60") has no resource of its own — use the queried href.
569
- const effectiveHref = refHref || href;
322
+ const effectiveHref = refHref || this.bareHrefFromItem(item);
570
323
  if (effectiveHref !== href) continue;
571
324
  if (!refFragment) return undefined;
572
325
  const match = refFragment.match(/(?:^|&)t=([^&]+)/);
@@ -575,12 +328,17 @@ export class Timeline {
575
328
  return undefined;
576
329
  }
577
330
 
578
- private itemMatchesHref(item: TimelineItem, href: string): boolean {
579
- return item.references.some(ref => {
580
- const bare = ref.split("#")[0];
581
- // Fragment-only reference (single-track audio) matches the current resource.
582
- return bare === href || bare === '';
583
- });
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;
584
342
  }
585
343
 
586
344
  private ancestorPath(items: TimelineItem[], target: TimelineItem): TimelineItem[] | null {
@@ -594,10 +352,6 @@ export class Timeline {
594
352
  return null;
595
353
  }
596
354
 
597
- private itemScrollPosition(item: TimelineItem): number | undefined {
598
- return item.scroll;
599
- }
600
-
601
355
  private static bareHref(href: string): string {
602
356
  return href.split("#")[0];
603
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, when one could be derived. */
10
- title?: string;
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
- /** Raw position: book-global seconds for audio, a Positions List position for EPUB, a page number for PDF. */
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<XMLDocument>;
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<XMLDocument | undefined>;
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
- export declare function getEncryption(properties: Properties): Encryption | undefined;
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 { Metadata } from '../Metadata.ts';
2
- import { MediaOverlay } from './MediaOverlay.ts';
3
- export declare function getMediaOverlay(metadata: Metadata): MediaOverlay | undefined;
1
+ import { MediaOverlay } from "./MediaOverlay.ts";
2
+ declare module '../Metadata' {
3
+ interface Metadata {
4
+ getMediaOverlay(): MediaOverlay | undefined;
5
+ }
6
+ }
@@ -1,2 +1,10 @@
1
- import { Properties } from '../Properties.ts';
2
- export declare function getContains(properties: Properties): Set<string>;
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
- import { Publication } from '../Publication.ts';
3
- export declare function getPageList(pub: Publication): Links | undefined;
4
- export declare function getLandmarks(pub: Publication): Links | undefined;
5
- export declare function getListOfAudioClips(pub: Publication): Links | undefined;
6
- export declare function getListOfIllustrations(pub: Publication): Links | undefined;
7
- export declare function getListOfTables(pub: Publication): Links | undefined;
8
- export declare function getListOfVideoClips(pub: Publication): Links | undefined;
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
+ }