@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.
Files changed (167) hide show
  1. package/LICENSE +28 -0
  2. package/README.MD +7 -8
  3. package/dist/ar-DyHX_uy2.js +7 -0
  4. package/dist/da-Dct0PS3E.js +7 -0
  5. package/dist/fr-C5HEel98.js +7 -0
  6. package/dist/index.js +4993 -1
  7. package/dist/index.umd.cjs +3 -0
  8. package/dist/it-DFOBoXGy.js +7 -0
  9. package/dist/pt_PT-Di3sVjze.js +7 -0
  10. package/dist/sv-BfzAFsVN.js +7 -0
  11. package/package.json +26 -33
  12. package/src/fetcher/HttpFetcher.ts +5 -5
  13. package/src/fetcher/Resource.ts +10 -5
  14. package/src/publication/Link.ts +6 -2
  15. package/src/publication/Locator.ts +3 -21
  16. package/src/publication/Manifest.ts +10 -10
  17. package/src/publication/Publication.ts +1 -2
  18. package/src/publication/accessibility/Localization.ts +14 -12
  19. package/src/publication/accessibility/SupportedLanguages.ts +1 -1
  20. package/src/publication/encryption/Properties.ts +15 -3
  21. package/src/publication/epub/Metadata.ts +12 -6
  22. package/src/publication/epub/Properties.ts +13 -3
  23. package/src/publication/epub/Publication.ts +39 -18
  24. package/src/publication/html/Locations.ts +101 -54
  25. package/src/publication/opds/Properties.ts +66 -21
  26. package/src/publication/opds/Publication.ts +9 -3
  27. package/src/publication/services/content/iterators/HTMLResourceContentIterator.ts +1 -3
  28. package/src/publication/services/timeline/Timeline.ts +59 -268
  29. package/src/publication/services/timeline/TimelineItem.ts +3 -3
  30. package/src/publication/services/timeline/index.ts +0 -12
  31. package/src/util/npt.ts +0 -19
  32. package/types/src/fetcher/HttpFetcher.d.ts +3 -3
  33. package/types/src/fetcher/Resource.d.ts +6 -2
  34. package/types/src/publication/encryption/Properties.d.ts +9 -2
  35. package/types/src/publication/epub/Metadata.d.ts +6 -3
  36. package/types/src/publication/epub/Properties.d.ts +10 -2
  37. package/types/src/publication/epub/Publication.d.ts +18 -7
  38. package/types/src/publication/html/Locations.d.ts +44 -15
  39. package/types/src/publication/opds/Properties.d.ts +36 -8
  40. package/types/src/publication/opds/Publication.d.ts +5 -2
  41. package/types/src/publication/services/timeline/Timeline.d.ts +12 -78
  42. package/types/src/publication/services/timeline/TimelineItem.d.ts +3 -3
  43. package/types/src/publication/services/timeline/index.d.ts +0 -6
  44. package/types/src/util/npt.d.ts +0 -10
  45. package/dist/_virtual/preload-helper.js +0 -1
  46. package/dist/fetcher/Fetcher.js +0 -1
  47. package/dist/fetcher/HttpFetcher.js +0 -1
  48. package/dist/fetcher/Resource.js +0 -1
  49. package/dist/locales/publication-metadata/ar.json.js +0 -1
  50. package/dist/locales/publication-metadata/da.json.js +0 -1
  51. package/dist/locales/publication-metadata/el.json.js +0 -1
  52. package/dist/locales/publication-metadata/en.json.js +0 -1
  53. package/dist/locales/publication-metadata/es.json.js +0 -1
  54. package/dist/locales/publication-metadata/et.json.js +0 -1
  55. package/dist/locales/publication-metadata/fi.json.js +0 -1
  56. package/dist/locales/publication-metadata/fr.json.js +0 -1
  57. package/dist/locales/publication-metadata/it.json.js +0 -1
  58. package/dist/locales/publication-metadata/pl.json.js +0 -1
  59. package/dist/locales/publication-metadata/pt_PT.json.js +0 -1
  60. package/dist/locales/publication-metadata/sv.json.js +0 -1
  61. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/constants.js +0 -1
  62. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/index.js +0 -1
  63. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-attribute.js +0 -1
  64. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-class.js +0 -1
  65. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-fallback.js +0 -1
  66. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-id.js +0 -1
  67. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-nth-child.js +0 -1
  68. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-nth-of-type.js +0 -1
  69. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/selector-tag.js +0 -1
  70. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/types.js +0 -1
  71. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-cartesian.js +0 -1
  72. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-data.js +0 -1
  73. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-dom.js +0 -1
  74. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-element-data.js +0 -1
  75. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-iselement.js +0 -1
  76. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-messages.js +0 -1
  77. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-options.js +0 -1
  78. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-powerset.js +0 -1
  79. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-selectors.js +0 -1
  80. package/dist/node_modules/.pnpm/css-selector-generator@3.8.0/node_modules/css-selector-generator/esm/utilities-typescript.js +0 -1
  81. package/dist/opds/Acquisition.js +0 -1
  82. package/dist/opds/Availability.js +0 -1
  83. package/dist/opds/Copies.js +0 -1
  84. package/dist/opds/Holds.js +0 -1
  85. package/dist/opds/Price.js +0 -1
  86. package/dist/publication/AltIdentifier.js +0 -1
  87. package/dist/publication/BelongsTo.js +0 -1
  88. package/dist/publication/Contributor.js +0 -1
  89. package/dist/publication/GuidedNavigation.js +0 -1
  90. package/dist/publication/Layout.js +0 -1
  91. package/dist/publication/Link.js +0 -1
  92. package/dist/publication/LocalizedString.js +0 -1
  93. package/dist/publication/Locator.js +0 -1
  94. package/dist/publication/LocatorCollection.js +0 -1
  95. package/dist/publication/Manifest.js +0 -1
  96. package/dist/publication/Metadata.js +0 -1
  97. package/dist/publication/Profiles.js +0 -1
  98. package/dist/publication/Properties.js +0 -1
  99. package/dist/publication/Publication.js +0 -1
  100. package/dist/publication/PublicationCollection.js +0 -1
  101. package/dist/publication/ReadingProgression.js +0 -1
  102. package/dist/publication/Subject.js +0 -1
  103. package/dist/publication/TDM.js +0 -1
  104. package/dist/publication/accessibility/Accessibility.js +0 -1
  105. package/dist/publication/accessibility/AccessibilityMetadataDisplayGuide.js +0 -1
  106. package/dist/publication/accessibility/Localization.js +0 -1
  107. package/dist/publication/accessibility/SupportedLanguages.js +0 -1
  108. package/dist/publication/encryption/Encryption.js +0 -1
  109. package/dist/publication/encryption/Properties.js +0 -1
  110. package/dist/publication/encryption/index.js +0 -1
  111. package/dist/publication/epub/MediaOverlay.js +0 -1
  112. package/dist/publication/epub/Metadata.js +0 -1
  113. package/dist/publication/epub/Properties.js +0 -1
  114. package/dist/publication/epub/Publication.js +0 -1
  115. package/dist/publication/epub/index.js +0 -1
  116. package/dist/publication/html/DomRange.js +0 -1
  117. package/dist/publication/html/DomRangePoint.js +0 -1
  118. package/dist/publication/html/Locations.js +0 -1
  119. package/dist/publication/html/index.js +0 -1
  120. package/dist/publication/opds/Properties.js +0 -1
  121. package/dist/publication/opds/Publication.js +0 -1
  122. package/dist/publication/opds/index.js +0 -1
  123. package/dist/publication/services/content/Content.js +0 -2
  124. package/dist/publication/services/content/ContentTokenizer.js +0 -1
  125. package/dist/publication/services/content/Iterator.js +0 -1
  126. package/dist/publication/services/content/element/attributes.js +0 -1
  127. package/dist/publication/services/content/element/element.js +0 -1
  128. package/dist/publication/services/content/element/text_role.js +0 -1
  129. package/dist/publication/services/content/iterators/HTMLResourceContentIterator.js +0 -1
  130. package/dist/publication/services/content/iterators/PDFTextContentIterator.js +0 -1
  131. package/dist/publication/services/content/iterators/PublicationContentIterator.js +0 -1
  132. package/dist/publication/services/content/iterators/helpers.js +0 -1
  133. package/dist/publication/services/timeline/Timeline.js +0 -1
  134. package/dist/publication/services/timeline/audio.js +0 -1
  135. package/dist/publication/services/timeline/html.js +0 -1
  136. package/dist/publication/services/timeline/index.js +0 -1
  137. package/dist/util/JSONParse.js +0 -1
  138. package/dist/util/URITemplate.js +0 -1
  139. package/dist/util/mediatype/MediaType.js +0 -1
  140. package/dist/util/npt.js +0 -1
  141. package/dist/util/tokenizer/TextTokenizer.js +0 -1
  142. package/dist/util/tokenizer/tokenize-english/abbreviations.js +0 -1
  143. package/dist/util/tokenizer/tokenize-english/index.js +0 -2
  144. package/dist/util/tokenizer/tokenize-english/utils.js +0 -1
  145. package/dist/util/tokenizer/tokenize-text/index.js +0 -1
  146. package/dist/util/tokenizer/tokenize-text/tokens.js +0 -1
  147. package/src/locales/publication-metadata/ar.json +0 -402
  148. package/src/locales/publication-metadata/brh.json +0 -1
  149. package/src/locales/publication-metadata/da.json +0 -310
  150. package/src/locales/publication-metadata/el.json +0 -310
  151. package/src/locales/publication-metadata/en.json +0 -310
  152. package/src/locales/publication-metadata/es.json +0 -333
  153. package/src/locales/publication-metadata/et.json +0 -310
  154. package/src/locales/publication-metadata/fi.json +0 -310
  155. package/src/locales/publication-metadata/fr.json +0 -333
  156. package/src/locales/publication-metadata/he.json +0 -9
  157. package/src/locales/publication-metadata/it.json +0 -333
  158. package/src/locales/publication-metadata/lt.json +0 -8
  159. package/src/locales/publication-metadata/pl.json +0 -333
  160. package/src/locales/publication-metadata/pt_PT.json +0 -329
  161. package/src/locales/publication-metadata/sv.json +0 -310
  162. package/src/locales/publication-metadata/tr.json +0 -51
  163. package/src/locales/publication-metadata/uk.json +0 -1
  164. package/src/publication/services/timeline/audio.ts +0 -31
  165. package/src/publication/services/timeline/html.ts +0 -5
  166. package/types/src/publication/services/timeline/audio.d.ts +0 -4
  167. 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
- }
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
- // 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,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
- const linkBare = Timeline.bareHref(link.href);
336
- // Fragment-only hrefs (e.g. "#t=60" in single-track audio) have no bare href,
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
- const linkBare = Timeline.bareHref(link.href);
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
- // Fragment-only reference (e.g. "#t=60") has no resource of its own — use the queried href.
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 itemMatchesHref(item: TimelineItem, href: string): boolean {
542
- return item.references.some(ref => {
543
- const bare = ref.split("#")[0];
544
- // Fragment-only reference (single-track audio) matches the current resource.
545
- return bare === href || bare === '';
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, 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
+ }