@openbrf/display 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSE-EXCEPTION.md +28 -0
  3. package/README.md +109 -2
  4. package/dist/contract.d.ts +244 -0
  5. package/dist/contract.d.ts.map +1 -0
  6. package/dist/contract.js +54 -0
  7. package/dist/contract.js.map +1 -0
  8. package/dist/fetch-loop.d.ts +87 -0
  9. package/dist/fetch-loop.d.ts.map +1 -0
  10. package/dist/fetch-loop.js +351 -0
  11. package/dist/fetch-loop.js.map +1 -0
  12. package/dist/fit.d.ts +44 -0
  13. package/dist/fit.d.ts.map +1 -0
  14. package/dist/fit.js +257 -0
  15. package/dist/fit.js.map +1 -0
  16. package/dist/index.d.ts +23 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +18 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/parse.d.ts +39 -0
  21. package/dist/parse.d.ts.map +1 -0
  22. package/dist/parse.js +494 -0
  23. package/dist/parse.js.map +1 -0
  24. package/dist/renderer.d.ts +74 -0
  25. package/dist/renderer.d.ts.map +1 -0
  26. package/dist/renderer.js +473 -0
  27. package/dist/renderer.js.map +1 -0
  28. package/dist/rotation.d.ts +129 -0
  29. package/dist/rotation.d.ts.map +1 -0
  30. package/dist/rotation.js +330 -0
  31. package/dist/rotation.js.map +1 -0
  32. package/dist/slides.d.ts +36 -0
  33. package/dist/slides.d.ts.map +1 -0
  34. package/dist/slides.js +333 -0
  35. package/dist/slides.js.map +1 -0
  36. package/dist/styles.d.ts +7 -0
  37. package/dist/styles.d.ts.map +1 -0
  38. package/dist/styles.js +104 -0
  39. package/dist/styles.js.map +1 -0
  40. package/dist/text.d.ts +16 -0
  41. package/dist/text.d.ts.map +1 -0
  42. package/dist/text.js +62 -0
  43. package/dist/text.js.map +1 -0
  44. package/dist/time.d.ts +39 -0
  45. package/dist/time.d.ts.map +1 -0
  46. package/dist/time.js +94 -0
  47. package/dist/time.js.map +1 -0
  48. package/dist/translate.d.ts +18 -0
  49. package/dist/translate.d.ts.map +1 -0
  50. package/dist/translate.js +35 -0
  51. package/dist/translate.js.map +1 -0
  52. package/dist/words.d.ts +41 -0
  53. package/dist/words.d.ts.map +1 -0
  54. package/dist/words.js +73 -0
  55. package/dist/words.js.map +1 -0
  56. package/package.json +49 -3
  57. package/src/contract.ts +279 -0
  58. package/src/fetch-loop.ts +472 -0
  59. package/src/fit.ts +348 -0
  60. package/src/index.ts +77 -0
  61. package/src/parse.ts +605 -0
  62. package/src/renderer.ts +688 -0
  63. package/src/rotation.ts +508 -0
  64. package/src/slides.ts +583 -0
  65. package/src/styles.ts +110 -0
  66. package/src/text.ts +65 -0
  67. package/src/time.ts +147 -0
  68. package/src/translate.ts +56 -0
  69. package/src/words.ts +91 -0
@@ -0,0 +1,351 @@
1
+ import { SCREEN_CONTENT_LIMITS } from "./contract.js";
2
+ import { parseScreenContent } from "./parse.js";
3
+ const SECOND = 1000;
4
+ /** How long the last good content may be shown, from when it was fetched. */
5
+ export const STALE_CONTENT_LIMIT_MS = 24 * 60 * 60 * SECOND;
6
+ /** How long one request may take before it counts as a network failure. */
7
+ const REQUEST_TIMEOUT_MS = 30 * SECOND;
8
+ const RETRY_SECONDS = 60;
9
+ /** A cache entry stamped this far in the future is a clock gone wrong, not fresh content. */
10
+ const CLOCK_SKEW_MS = 5 * 60 * SECOND;
11
+ /**
12
+ * Starts fetching screen content: at once, then `refreshSeconds` after each
13
+ * 200 or 304, held to 30 to 60, and 60 seconds after anything else.
14
+ *
15
+ * Each request is `GET url` with the token in `Authorization: Bearer`, the
16
+ * cached ETag in `If-None-Match`, no cookie (`credentials: "omit"`) and no
17
+ * HTTP cache (`cache: "no-store"`). Only one is ever in flight. A cache entry
18
+ * younger than 24 hours is shown at once, as stale, before the first answer.
19
+ */
20
+ export function startFetchLoop(options) {
21
+ const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
22
+ const now = () => (options.now ?? (() => new Date()))().getTime();
23
+ const schedule = options.setTimeout ??
24
+ ((callback, ms) => setTimeout(callback, ms));
25
+ const unschedule = options.clearTimeout ??
26
+ ((handle) => clearTimeout(handle));
27
+ let stopped = false;
28
+ let timer = null;
29
+ let inFlight = null;
30
+ /** After a 304 with nothing cached, the next request asks unconditionally. */
31
+ let unconditional = false;
32
+ /** The last successful fetch, remembered after the cache is cleared. */
33
+ let lastFetchedAt = null;
34
+ /** The cache entry, when it is still a document this package reads. */
35
+ function readCache() {
36
+ let entry;
37
+ try {
38
+ entry = options.cache.read();
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ if (entry === null) {
44
+ return null;
45
+ }
46
+ const fetchedAt = Date.parse(entry.fetchedAt);
47
+ try {
48
+ if (!Number.isFinite(fetchedAt)) {
49
+ throw new Error("fetchedAt is not an instant.");
50
+ }
51
+ const content = parseScreenContent(entry.content);
52
+ return {
53
+ entry: {
54
+ content,
55
+ etag: typeof entry.etag === "string" ? entry.etag : null,
56
+ fetchedAt: entry.fetchedAt,
57
+ },
58
+ fetchedAt,
59
+ };
60
+ }
61
+ catch {
62
+ options.cache.clear();
63
+ return null;
64
+ }
65
+ }
66
+ function fresh(fetchedAt) {
67
+ const age = now() - fetchedAt;
68
+ return age > -CLOCK_SKEW_MS && age < STALE_CONTENT_LIMIT_MS;
69
+ }
70
+ function stop() {
71
+ stopped = true;
72
+ if (timer !== null) {
73
+ unschedule(timer);
74
+ timer = null;
75
+ }
76
+ inFlight?.abort();
77
+ inFlight = null;
78
+ }
79
+ function revoke() {
80
+ stop();
81
+ options.cache.clear();
82
+ options.onRevoked();
83
+ }
84
+ function next(seconds) {
85
+ if (stopped) {
86
+ return;
87
+ }
88
+ if (timer !== null) {
89
+ unschedule(timer);
90
+ }
91
+ timer = schedule(() => {
92
+ timer = null;
93
+ void request();
94
+ }, seconds * SECOND);
95
+ }
96
+ function remember(fetchedAt) {
97
+ lastFetchedAt = Math.max(lastFetchedAt ?? fetchedAt, fetchedAt);
98
+ }
99
+ /** Any answer but 200, 304, a revoking 401, 404 and 429. */
100
+ function unreachable() {
101
+ const cached = readCache();
102
+ if (cached !== null) {
103
+ remember(cached.fetchedAt);
104
+ }
105
+ if (cached !== null && fresh(cached.fetchedAt)) {
106
+ options.onContent(cached.entry.content, {
107
+ fetchedAt: new Date(cached.fetchedAt),
108
+ stale: true,
109
+ });
110
+ }
111
+ else {
112
+ options.cache.clear();
113
+ options.onDisconnected(lastFetchedAt === null ? null : new Date(lastFetchedAt));
114
+ }
115
+ next(RETRY_SECONDS);
116
+ }
117
+ function shown(content, fetchedAt) {
118
+ remember(fetchedAt);
119
+ options.onContent(content, {
120
+ fetchedAt: new Date(fetchedAt),
121
+ stale: false,
122
+ });
123
+ next(Math.min(SCREEN_CONTENT_LIMITS.refreshSecondsMax, Math.max(SCREEN_CONTENT_LIMITS.refreshSecondsMin, content.refreshSeconds)));
124
+ }
125
+ async function request() {
126
+ if (stopped || inFlight !== null) {
127
+ return;
128
+ }
129
+ const token = options.token();
130
+ if (token === null || token === "") {
131
+ revoke();
132
+ return;
133
+ }
134
+ const cached = readCache();
135
+ const headers = {
136
+ Accept: "application/json",
137
+ Authorization: `Bearer ${token}`,
138
+ };
139
+ if (cached !== null && cached.entry.etag !== null && !unconditional) {
140
+ headers["If-None-Match"] = cached.entry.etag;
141
+ }
142
+ const controller = new AbortController();
143
+ inFlight = controller;
144
+ const timeout = schedule(() => controller.abort(), REQUEST_TIMEOUT_MS);
145
+ let answer;
146
+ try {
147
+ const response = await fetchImpl(options.url, {
148
+ method: "GET",
149
+ headers,
150
+ cache: "no-store",
151
+ credentials: "omit",
152
+ mode: "cors",
153
+ redirect: "error",
154
+ signal: controller.signal,
155
+ });
156
+ answer = await read(response, now());
157
+ }
158
+ catch {
159
+ answer = { kind: "other" };
160
+ }
161
+ finally {
162
+ unschedule(timeout);
163
+ }
164
+ if (stopped || inFlight !== controller) {
165
+ return;
166
+ }
167
+ inFlight = null;
168
+ handle(answer, cached);
169
+ }
170
+ function handle(answer, cached) {
171
+ switch (answer.kind) {
172
+ case "content": {
173
+ const fetchedAt = now();
174
+ unconditional = false;
175
+ options.cache.write({
176
+ content: answer.content,
177
+ etag: answer.etag,
178
+ fetchedAt: new Date(fetchedAt).toISOString(),
179
+ });
180
+ shown(answer.content, fetchedAt);
181
+ return;
182
+ }
183
+ case "not-modified": {
184
+ if (cached === null) {
185
+ unconditional = true;
186
+ unreachable();
187
+ return;
188
+ }
189
+ const fetchedAt = now();
190
+ options.cache.write({
191
+ ...cached.entry,
192
+ fetchedAt: new Date(fetchedAt).toISOString(),
193
+ });
194
+ shown(cached.entry.content, fetchedAt);
195
+ return;
196
+ }
197
+ case "revoked":
198
+ revoke();
199
+ return;
200
+ case "paused":
201
+ options.cache.clear();
202
+ if (options.onPaused === undefined) {
203
+ options.onDisconnected(null);
204
+ }
205
+ else {
206
+ options.onPaused();
207
+ }
208
+ next(RETRY_SECONDS);
209
+ return;
210
+ case "slow-down": {
211
+ const current = readCache();
212
+ if (current !== null && !fresh(current.fetchedAt)) {
213
+ // Slowing down does not stretch the 24 hours.
214
+ unreachable();
215
+ return;
216
+ }
217
+ next(answer.retryAfter);
218
+ return;
219
+ }
220
+ case "other":
221
+ unreachable();
222
+ return;
223
+ }
224
+ }
225
+ const cachedAtStart = readCache();
226
+ if (cachedAtStart !== null) {
227
+ remember(cachedAtStart.fetchedAt);
228
+ if (fresh(cachedAtStart.fetchedAt)) {
229
+ options.onContent(cachedAtStart.entry.content, {
230
+ fetchedAt: new Date(cachedAtStart.fetchedAt),
231
+ stale: true,
232
+ });
233
+ }
234
+ else {
235
+ options.cache.clear();
236
+ }
237
+ }
238
+ void request();
239
+ return {
240
+ stop,
241
+ refreshNow() {
242
+ if (stopped || inFlight !== null) {
243
+ return;
244
+ }
245
+ if (timer !== null) {
246
+ unschedule(timer);
247
+ timer = null;
248
+ }
249
+ void request();
250
+ },
251
+ };
252
+ }
253
+ async function read(response, now) {
254
+ switch (response.status) {
255
+ case 200: {
256
+ const text = await response.text();
257
+ if (new TextEncoder().encode(text).byteLength >
258
+ SCREEN_CONTENT_LIMITS.documentBytes) {
259
+ return { kind: "other" };
260
+ }
261
+ try {
262
+ return {
263
+ kind: "content",
264
+ content: parseScreenContent(JSON.parse(text)),
265
+ etag: response.headers.get("ETag"),
266
+ };
267
+ }
268
+ catch {
269
+ return { kind: "other" };
270
+ }
271
+ }
272
+ case 304:
273
+ return { kind: "not-modified" };
274
+ case 401:
275
+ return (await revokes(response))
276
+ ? { kind: "revoked" }
277
+ : { kind: "other" };
278
+ case 404:
279
+ return { kind: "paused" };
280
+ case 429:
281
+ return {
282
+ kind: "slow-down",
283
+ retryAfter: retryAfter(response.headers.get("Retry-After"), now),
284
+ };
285
+ default:
286
+ return { kind: "other" };
287
+ }
288
+ }
289
+ /**
290
+ * Whether a 401 is the instance refusing the token: the challenge names
291
+ * `realm="device"`, and the body's `reason` (or the challenge's `error`) is
292
+ * `device-token-invalid`. Any other 401, such as one from a proxy in between,
293
+ * is not a reason to forget the token.
294
+ */
295
+ async function revokes(response) {
296
+ const challenge = parseChallenge(response.headers.get("WWW-Authenticate"));
297
+ if (challenge === null || challenge.get("realm") !== "device") {
298
+ return false;
299
+ }
300
+ if (challenge.get("error") === "device-token-invalid") {
301
+ return true;
302
+ }
303
+ try {
304
+ const body = await response.json();
305
+ return (typeof body === "object" &&
306
+ body !== null &&
307
+ body.reason === "device-token-invalid");
308
+ }
309
+ catch {
310
+ return false;
311
+ }
312
+ }
313
+ /** The parameters of a `Bearer` challenge, or null when there is none. */
314
+ function parseChallenge(header) {
315
+ if (header === null) {
316
+ return null;
317
+ }
318
+ const match = /^\s*Bearer(?:\s+(.*))?$/i.exec(header);
319
+ if (match === null) {
320
+ return null;
321
+ }
322
+ const parameters = new Map();
323
+ const pattern = /([A-Za-z_][\w-]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g;
324
+ for (const parameter of (match[1] ?? "").matchAll(pattern)) {
325
+ const name = parameter[1];
326
+ if (name !== undefined) {
327
+ parameters.set(name.toLowerCase(), parameter[2]?.replace(/\\(.)/g, "$1") ?? parameter[3] ?? "");
328
+ }
329
+ }
330
+ return parameters;
331
+ }
332
+ /** `Retry-After` in seconds, held to 1 to 60; 60 when absent or unreadable. */
333
+ function retryAfter(header, now) {
334
+ if (header === null) {
335
+ return RETRY_SECONDS;
336
+ }
337
+ const value = header.trim();
338
+ let seconds;
339
+ if (/^\d+$/.test(value)) {
340
+ seconds = Number(value);
341
+ }
342
+ else {
343
+ const date = Date.parse(value);
344
+ if (!Number.isFinite(date)) {
345
+ return RETRY_SECONDS;
346
+ }
347
+ seconds = Math.ceil((date - now) / SECOND);
348
+ }
349
+ return Math.min(RETRY_SECONDS, Math.max(1, seconds));
350
+ }
351
+ //# sourceMappingURL=fetch-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-loop.js","sourceRoot":"","sources":["../src/fetch-loop.ts"],"names":[],"mappings":"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"}
package/dist/fit.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Fitting a slide to the canvas.
3
+ *
4
+ * The contract lets a slide hold more than one canvas can show at a size read
5
+ * from across a stairwell: eight events, twelve resources, an agenda of forty
6
+ * items, sixty table rows, a hundred and twenty doors, two thousand characters
7
+ * of news in as many paragraphs and lines as it likes. Nothing a slide holds is
8
+ * cut off; a slide the canvas cannot hold is shown a page at a time:
9
+ *
10
+ * 1. A slide's list is shown as many of its items as the canvas holds whole.
11
+ * 2. A page still too tall, such as one long event or a news item, is set
12
+ * smaller, down to `MIN_SCALE` of its size.
13
+ * 3. A page too tall even then continues on the next page, its text broken
14
+ * between two words, or a word longer than a page holds between two of its
15
+ * characters.
16
+ *
17
+ * It measures the slide in the page, so it does nothing where nothing is laid
18
+ * out: before the element has a size, and under a DOM without layout.
19
+ */
20
+ /** The attribute on a slide's list that may be shown a page at a time. */
21
+ export declare const PAGED_LIST_ATTRIBUTE = "data-obrf-display-paged";
22
+ /**
23
+ * The attribute on an element of a slide whose text is all on other pages of
24
+ * it. The stylesheet takes it out of the page, except a table's cell, which
25
+ * stays empty to keep its row's columns.
26
+ */
27
+ export declare const ELSEWHERE_ATTRIBUTE = "data-obrf-display-elsewhere";
28
+ /** Which page of the slide is shown, of how many, and whether it fits. */
29
+ export interface SlideFit {
30
+ page: number;
31
+ pages: number;
32
+ /**
33
+ * False when the page still holds more than it shows: a single character
34
+ * too wide or too tall for the canvas even at `MIN_SCALE`.
35
+ */
36
+ fits: boolean;
37
+ }
38
+ /**
39
+ * Fits a slide that is on the stage, showing page `page` of it (the last page
40
+ * when it has fewer). `unit` is the canvas's unit in pixels, a hundredth of
41
+ * its shorter side, or 0 when it has no size.
42
+ */
43
+ export declare function fitSlide(slide: HTMLElement, unit: number, page: number): SlideFit;
44
+ //# sourceMappingURL=fit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fit.d.ts","sourceRoot":"","sources":["../src/fit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,4BAA4B,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAOjE,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;CACf;AAaD;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,QAAQ,CAwCV"}
package/dist/fit.js ADDED
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Fitting a slide to the canvas.
3
+ *
4
+ * The contract lets a slide hold more than one canvas can show at a size read
5
+ * from across a stairwell: eight events, twelve resources, an agenda of forty
6
+ * items, sixty table rows, a hundred and twenty doors, two thousand characters
7
+ * of news in as many paragraphs and lines as it likes. Nothing a slide holds is
8
+ * cut off; a slide the canvas cannot hold is shown a page at a time:
9
+ *
10
+ * 1. A slide's list is shown as many of its items as the canvas holds whole.
11
+ * 2. A page still too tall, such as one long event or a news item, is set
12
+ * smaller, down to `MIN_SCALE` of its size.
13
+ * 3. A page too tall even then continues on the next page, its text broken
14
+ * between two words, or a word longer than a page holds between two of its
15
+ * characters.
16
+ *
17
+ * It measures the slide in the page, so it does nothing where nothing is laid
18
+ * out: before the element has a size, and under a DOM without layout.
19
+ */
20
+ /** The attribute on a slide's list that may be shown a page at a time. */
21
+ export const PAGED_LIST_ATTRIBUTE = "data-obrf-display-paged";
22
+ /**
23
+ * The attribute on an element of a slide whose text is all on other pages of
24
+ * it. The stylesheet takes it out of the page, except a table's cell, which
25
+ * stays empty to keep its row's columns.
26
+ */
27
+ export const ELSEWHERE_ATTRIBUTE = "data-obrf-display-elsewhere";
28
+ /** The smallest a slide is set, as a share of its size. */
29
+ const MIN_SCALE = 0.6;
30
+ const SCALE_STEP = 0.05;
31
+ /**
32
+ * Fits a slide that is on the stage, showing page `page` of it (the last page
33
+ * when it has fewer). `unit` is the canvas's unit in pixels, a hundredth of
34
+ * its shorter side, or 0 when it has no size.
35
+ */
36
+ export function fitSlide(slide, unit, page) {
37
+ const list = slide.querySelector(`[${PAGED_LIST_ATTRIBUTE}]`);
38
+ const items = list === null
39
+ ? []
40
+ : Array.from(list.children);
41
+ restore(slide, items);
42
+ if (unit <= 0 || !overflows(slide)) {
43
+ return { page: 0, pages: 1, fits: true };
44
+ }
45
+ const pages = [];
46
+ for (const [from, to] of itemPages(slide, items)) {
47
+ restore(slide, items);
48
+ show(items, from, to);
49
+ const scaled = shrink(slide, unit);
50
+ if (!overflows(slide)) {
51
+ pages.push({ from, to, unit: scaled, text: null });
52
+ continue;
53
+ }
54
+ const parts = textPages(slide, textFlow(slide));
55
+ for (const text of parts.length > 0 ? parts : [null]) {
56
+ pages.push({ from, to, unit: scaled, text });
57
+ }
58
+ }
59
+ const shown = Math.min(Math.max(0, page), pages.length - 1);
60
+ const chosen = pages[shown] ?? {
61
+ from: 0,
62
+ to: items.length,
63
+ unit: null,
64
+ text: null,
65
+ };
66
+ restore(slide, items);
67
+ show(items, chosen.from, chosen.to);
68
+ if (chosen.unit !== null) {
69
+ slide.style.setProperty("--obrf-display-u", chosen.unit);
70
+ }
71
+ if (chosen.text !== null) {
72
+ textFlow(slide).show(...chosen.text);
73
+ }
74
+ return { page: shown, pages: pages.length, fits: !overflows(slide) };
75
+ }
76
+ /** Shows the items from `from` up to `to`, and hides the rest. */
77
+ function show(items, from, to) {
78
+ items.forEach((item, index) => {
79
+ item.hidden = index < from || index >= to;
80
+ });
81
+ }
82
+ /** The ranges of the list's items a page each holds whole: all of it when it has one item or none. */
83
+ function itemPages(slide, items) {
84
+ if (items.length <= 1) {
85
+ return [[0, items.length]];
86
+ }
87
+ const pages = [];
88
+ for (let start = 0; start < items.length;) {
89
+ const count = fitting(slide, items, start);
90
+ pages.push([start, start + count]);
91
+ start += count;
92
+ }
93
+ return pages;
94
+ }
95
+ /** How many items from `start` the slide holds whole: at least one. */
96
+ function fitting(slide, items, start) {
97
+ let low = 1;
98
+ let high = items.length - start;
99
+ while (low < high) {
100
+ const count = Math.ceil((low + high) / 2);
101
+ show(items, start, start + count);
102
+ if (overflows(slide)) {
103
+ high = count - 1;
104
+ }
105
+ else {
106
+ low = count;
107
+ }
108
+ }
109
+ return low;
110
+ }
111
+ /** Sets the slide smaller until it fits or reaches `MIN_SCALE`, and hands back the unit it set. */
112
+ function shrink(slide, unit) {
113
+ let set = null;
114
+ for (let scale = 1 - SCALE_STEP; overflows(slide) && scale >= MIN_SCALE - SCALE_STEP / 2; scale -= SCALE_STEP) {
115
+ set = `${unit * scale}px`;
116
+ slide.style.setProperty("--obrf-display-u", set);
117
+ }
118
+ return set;
119
+ }
120
+ /**
121
+ * The pages of text the slide holds, each at least one character. A page ends
122
+ * between two words. A word longer than a page holds starts on the page it
123
+ * comes to, is broken between two of its characters, and goes on on the next.
124
+ */
125
+ function textPages(slide, flow) {
126
+ const pages = [];
127
+ for (let start = 0; start < flow.length;) {
128
+ const words = flow.wordEnds.filter((wordEnd) => wordEnd > start);
129
+ let end = furthest(slide, flow, start, words);
130
+ const next = words.find((wordEnd) => wordEnd > end);
131
+ flow.show(start, end);
132
+ if (overflows(slide)) {
133
+ // The page is one word, and the word longer than it.
134
+ end = furthest(slide, flow, start, flow.characterEnds(start, end));
135
+ }
136
+ else if (next !== undefined) {
137
+ // The next word would be longer than a page even on a page of its own.
138
+ flow.show(end, next);
139
+ if (overflows(slide)) {
140
+ end = furthest(slide, flow, start, [
141
+ end,
142
+ ...flow.characterEnds(end, next),
143
+ ]);
144
+ }
145
+ }
146
+ pages.push([start, end]);
147
+ start = end;
148
+ }
149
+ return pages;
150
+ }
151
+ /** The furthest of `ends` the slide holds its text up to from `start`: at least the first. */
152
+ function furthest(slide, flow, start, ends) {
153
+ let low = 0;
154
+ let high = ends.length - 1;
155
+ while (low < high) {
156
+ const middle = Math.ceil((low + high) / 2);
157
+ flow.show(start, ends[middle] ?? flow.length);
158
+ if (overflows(slide)) {
159
+ high = middle - 1;
160
+ }
161
+ else {
162
+ low = middle;
163
+ }
164
+ }
165
+ return ends[low] ?? flow.length;
166
+ }
167
+ /** Each text node's own text, while a page shows part of it. */
168
+ const originals = new WeakMap();
169
+ function textNodes(slide) {
170
+ const nodes = [];
171
+ const walker = slide.ownerDocument.createTreeWalker(slide, NodeFilter.SHOW_TEXT);
172
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
173
+ nodes.push(node);
174
+ }
175
+ return nodes;
176
+ }
177
+ /** Puts every text back, shows every element and item, and sets the slide at the canvas's unit. */
178
+ function restore(slide, items) {
179
+ slide.style.removeProperty("--obrf-display-u");
180
+ for (const node of textNodes(slide)) {
181
+ const text = originals.get(node);
182
+ if (text !== undefined && node.data !== text) {
183
+ node.data = text;
184
+ }
185
+ }
186
+ for (const node of Array.from(slide.querySelectorAll(`[${ELSEWHERE_ATTRIBUTE}]`))) {
187
+ node.removeAttribute(ELSEWHERE_ATTRIBUTE);
188
+ }
189
+ show(items, 0, items.length);
190
+ }
191
+ function textFlow(slide) {
192
+ const texts = [];
193
+ const wordEnds = [];
194
+ let whole = "";
195
+ for (const node of textNodes(slide)) {
196
+ if (node.parentElement?.closest("[hidden]") != null) {
197
+ continue;
198
+ }
199
+ const text = originals.get(node) ?? node.data;
200
+ originals.set(node, text);
201
+ // A word ends where the next one starts, so that a page ends after the
202
+ // space or line break that follows its last word.
203
+ const ends = Array.from(text.matchAll(/\S+\s*/g), (match) => whole.length + match.index + match[0].length);
204
+ if (ends.length > 0) {
205
+ texts.push({ node, text, first: whole.length });
206
+ wordEnds.push(...ends);
207
+ whole += text;
208
+ }
209
+ }
210
+ return {
211
+ length: whole.length,
212
+ wordEnds,
213
+ characterEnds(from, to) {
214
+ const part = whole.slice(from, to);
215
+ if (typeof Intl.Segmenter === "function") {
216
+ return Array.from(new Intl.Segmenter().segment(part), ({ index, segment }) => from + index + segment.length);
217
+ }
218
+ // A browser without it breaks between two code points.
219
+ const ends = [];
220
+ let end = from;
221
+ for (const character of part) {
222
+ end += character.length;
223
+ ends.push(end);
224
+ }
225
+ return ends;
226
+ },
227
+ show(from, to) {
228
+ const holding = new Set();
229
+ const empty = new Set();
230
+ for (const { node, text, first } of texts) {
231
+ const start = Math.max(from - first, 0);
232
+ const end = Math.min(to - first, text.length);
233
+ const shown = start >= end ? "" : text.slice(start, end);
234
+ node.data = end < text.length ? shown.trimEnd() : shown;
235
+ for (let element = node.parentElement; element !== null && element !== slide; element = element.parentElement) {
236
+ (shown === "" ? empty : holding).add(element);
237
+ }
238
+ }
239
+ for (const element of new Set([...empty, ...holding])) {
240
+ element.toggleAttribute(ELSEWHERE_ATTRIBUTE, !holding.has(element));
241
+ }
242
+ },
243
+ };
244
+ }
245
+ /**
246
+ * Whether the slide, or a box in it that clips its own text, holds more than
247
+ * it shows.
248
+ */
249
+ function overflows(slide) {
250
+ const boxes = [
251
+ slide,
252
+ ...Array.from(slide.querySelectorAll(".obrf-display-copy")),
253
+ ];
254
+ return boxes.some((box) => box.scrollHeight > box.clientHeight + 1 ||
255
+ box.scrollWidth > box.clientWidth + 1);
256
+ }
257
+ //# sourceMappingURL=fit.js.map