bookish-press 0.9.4 → 0.10.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 (154) hide show
  1. package/dist/components/Auth.svelte +37 -21
  2. package/dist/components/BookPreview.svelte +4 -4
  3. package/dist/components/BookPreview.svelte.d.ts +1 -1
  4. package/dist/components/Link.svelte +19 -12
  5. package/dist/components/app/Button.svelte +1 -8
  6. package/dist/components/app/Link.svelte +1 -7
  7. package/dist/components/app/Options.svelte +1 -6
  8. package/dist/components/app/TextInput.svelte +2 -8
  9. package/dist/components/chapter/Atom.svelte +2 -1
  10. package/dist/components/chapter/Atom.svelte.d.ts +1 -1
  11. package/dist/components/chapter/Block.svelte +9 -9
  12. package/dist/components/chapter/Block.svelte.d.ts +1 -1
  13. package/dist/components/chapter/Callout.svelte.d.ts +1 -1
  14. package/dist/components/chapter/CaptionedCode.svelte +1 -0
  15. package/dist/components/chapter/CaptionedCode.svelte.d.ts +1 -1
  16. package/dist/components/chapter/ChapterBody.svelte +5 -2
  17. package/dist/components/chapter/ChapterBody.svelte.d.ts +1 -1
  18. package/dist/components/chapter/ChapterBody.test.js +3 -3
  19. package/dist/components/chapter/Citations.svelte +17 -16
  20. package/dist/components/chapter/Citations.svelte.d.ts +1 -1
  21. package/dist/components/chapter/Code.svelte +8 -14
  22. package/dist/components/chapter/Comment.svelte +7 -12
  23. package/dist/components/chapter/Comment.svelte.d.ts +1 -1
  24. package/dist/components/chapter/Definition.svelte +8 -9
  25. package/dist/components/chapter/Definition.svelte.d.ts +1 -1
  26. package/dist/components/chapter/Embed.svelte +41 -61
  27. package/dist/components/chapter/Embed.svelte.d.ts +1 -1
  28. package/dist/components/chapter/ErrorMessage.svelte.d.ts +1 -1
  29. package/dist/components/chapter/Figure.svelte +14 -14
  30. package/dist/components/chapter/Figure.svelte.d.ts +4 -4
  31. package/dist/components/chapter/Footnote.svelte +11 -17
  32. package/dist/components/chapter/Footnote.svelte.d.ts +1 -1
  33. package/dist/components/chapter/Format.svelte +11 -11
  34. package/dist/components/chapter/Format.svelte.d.ts +1 -1
  35. package/dist/components/chapter/InlineCode.svelte.d.ts +1 -1
  36. package/dist/components/chapter/Label.svelte +1 -1
  37. package/dist/components/chapter/Label.svelte.d.ts +1 -1
  38. package/dist/components/chapter/LineBreak.svelte +1 -0
  39. package/dist/components/chapter/LineBreak.svelte.d.ts +1 -1
  40. package/dist/components/chapter/Link.svelte +14 -12
  41. package/dist/components/chapter/Link.svelte.d.ts +1 -1
  42. package/dist/components/chapter/List.svelte +1 -1
  43. package/dist/components/chapter/List.svelte.d.ts +1 -1
  44. package/dist/components/chapter/Marginal.svelte +36 -42
  45. package/dist/components/chapter/Paragraph.svelte +4 -4
  46. package/dist/components/chapter/Paragraph.svelte.d.ts +1 -1
  47. package/dist/components/chapter/Positioned.svelte +1 -0
  48. package/dist/components/chapter/Positioned.svelte.d.ts +1 -1
  49. package/dist/components/chapter/Python.svelte +42 -31
  50. package/dist/components/chapter/Python.svelte.d.ts +1 -1
  51. package/dist/components/chapter/Quote.svelte.d.ts +1 -1
  52. package/dist/components/chapter/Rule.svelte +1 -0
  53. package/dist/components/chapter/Rule.svelte.d.ts +1 -1
  54. package/dist/components/chapter/Table.svelte.d.ts +1 -1
  55. package/dist/components/chapter/Text.svelte +41 -50
  56. package/dist/components/chapter/Text.svelte.d.ts +1 -1
  57. package/dist/components/controls/DarkToggle.svelte +4 -4
  58. package/dist/components/editor/BookishEditor.svelte +605 -569
  59. package/dist/components/editor/BookishEditor.svelte.d.ts +1 -1
  60. package/dist/components/editor/CalloutEditor.svelte +1 -1
  61. package/dist/components/editor/CalloutEditor.svelte.d.ts +1 -1
  62. package/dist/components/editor/CaptionedCodeEditor.svelte +1 -1
  63. package/dist/components/editor/CaptionedCodeEditor.svelte.d.ts +1 -1
  64. package/dist/components/editor/CaretContext.d.ts +14 -14
  65. package/dist/components/editor/CaretState.d.ts +3 -3
  66. package/dist/components/editor/CaretView.svelte +8 -19
  67. package/dist/components/editor/CitationsEditor.svelte +10 -18
  68. package/dist/components/editor/CitationsEditor.svelte.d.ts +1 -1
  69. package/dist/components/editor/Clipboard.d.ts +1 -1
  70. package/dist/components/editor/Command.d.ts +1 -1
  71. package/dist/components/editor/Commands.js +19 -19
  72. package/dist/components/editor/ConfirmButton.svelte +1 -6
  73. package/dist/components/editor/DefinitionEditor.svelte +9 -7
  74. package/dist/components/editor/DefinitionEditor.svelte.d.ts +1 -1
  75. package/dist/components/editor/EmbedEditor.svelte +25 -34
  76. package/dist/components/editor/EmbedEditor.svelte.d.ts +1 -1
  77. package/dist/components/editor/ImageChooser.svelte +8 -7
  78. package/dist/components/editor/ImageChooser.svelte.d.ts +1 -1
  79. package/dist/components/editor/ImageThumbnail.svelte +21 -18
  80. package/dist/components/editor/InlineCodeEditor.svelte +1 -1
  81. package/dist/components/editor/InlineCodeEditor.svelte.d.ts +1 -1
  82. package/dist/components/editor/LabelEditor.svelte +2 -2
  83. package/dist/components/editor/LabelEditor.svelte.d.ts +1 -1
  84. package/dist/components/editor/LanguageEditor.svelte +8 -1
  85. package/dist/components/editor/LinkEditor.svelte +42 -44
  86. package/dist/components/editor/LinkEditor.svelte.d.ts +1 -1
  87. package/dist/components/editor/Permissions.svelte +23 -29
  88. package/dist/components/editor/PositionEditor.svelte +1 -1
  89. package/dist/components/editor/PositionEditor.svelte.d.ts +1 -1
  90. package/dist/components/editor/QuoteEditor.svelte +1 -1
  91. package/dist/components/editor/QuoteEditor.svelte.d.ts +1 -1
  92. package/dist/components/editor/Switch.svelte +5 -11
  93. package/dist/components/editor/TableEditor.svelte +1 -1
  94. package/dist/components/editor/TableEditor.svelte.d.ts +1 -1
  95. package/dist/components/editor/TextEditor.svelte +61 -58
  96. package/dist/components/editor/Toggle.svelte +16 -14
  97. package/dist/components/editor/Toolbar.svelte +81 -94
  98. package/dist/components/editor/UndoState.d.ts +2 -2
  99. package/dist/components/page/Acknowledgements.svelte +2 -8
  100. package/dist/components/page/Authors.svelte +12 -21
  101. package/dist/components/page/BookLoader.svelte +32 -34
  102. package/dist/components/page/BulkReferenceEditor.svelte +21 -41
  103. package/dist/components/page/Chapter.svelte +139 -114
  104. package/dist/components/page/Chapter.svelte.d.ts +1 -1
  105. package/dist/components/page/ChapterTitle.svelte +1 -1
  106. package/dist/components/page/Contexts.js +3 -1
  107. package/dist/components/page/DefinitionView.svelte +30 -42
  108. package/dist/components/page/DefinitionView.svelte.d.ts +1 -1
  109. package/dist/components/page/Description.svelte +2 -8
  110. package/dist/components/page/DownloadEPUB.svelte +83 -41
  111. package/dist/components/page/EditableHeader.svelte.d.ts +1 -1
  112. package/dist/components/page/Edition.svelte +44 -30
  113. package/dist/components/page/Edition.svelte.d.ts +1 -1
  114. package/dist/components/page/EditionLoader.svelte +101 -100
  115. package/dist/components/page/Editions.svelte +13 -13
  116. package/dist/components/page/Glossary.svelte +9 -10
  117. package/dist/components/page/Header.svelte +15 -28
  118. package/dist/components/page/Index.svelte +17 -18
  119. package/dist/components/page/License.svelte +2 -8
  120. package/dist/components/page/Media.svelte +7 -14
  121. package/dist/components/page/MediaPreview.svelte +14 -17
  122. package/dist/components/page/Outline.svelte +74 -66
  123. package/dist/components/page/Page.svelte +23 -17
  124. package/dist/components/page/PossibleChapter.svelte +2 -2
  125. package/dist/components/page/PossibleReference.svelte +1 -1
  126. package/dist/components/page/PossibleReference.svelte.d.ts +2 -2
  127. package/dist/components/page/Print.svelte +5 -8
  128. package/dist/components/page/Publish.svelte +33 -39
  129. package/dist/components/page/Reference.svelte +9 -11
  130. package/dist/components/page/Reference.svelte.d.ts +1 -1
  131. package/dist/components/page/References.svelte +1 -1
  132. package/dist/components/page/Search.svelte +39 -40
  133. package/dist/components/page/SubdomainEditor.svelte +11 -10
  134. package/dist/components/page/TableOfContents.svelte +52 -48
  135. package/dist/components/page/TableOfContentsImage.svelte +2 -2
  136. package/dist/components/page/TableOfContentsRow.svelte +9 -22
  137. package/dist/components/page/TableOfContentsRow.svelte.d.ts +2 -2
  138. package/dist/components/page/ThemeEditor.svelte +10 -17
  139. package/dist/components/page/ThemeEditorPreview.svelte +3 -6
  140. package/dist/components/page/ThemeEditorPreview.svelte.d.ts +1 -1
  141. package/dist/components/page/ThemeVariableEditor.svelte +6 -6
  142. package/dist/components/page/Unknown.svelte +1 -1
  143. package/dist/epub/build.d.ts +6 -0
  144. package/dist/epub/build.js +16 -3
  145. package/dist/epub/build.test.js +65 -0
  146. package/dist/epub/images.d.ts +7 -5
  147. package/dist/epub/images.js +15 -10
  148. package/dist/epub/images.test.js +8 -6
  149. package/dist/epub/package.js +25 -3
  150. package/dist/models/FirebaseAvailable.d.ts +9 -0
  151. package/dist/models/FirebaseAvailable.js +10 -0
  152. package/dist/models/book/BookMedia.d.ts +2 -1
  153. package/dist/models/book/BookMedia.js +33 -9
  154. package/package.json +18 -14
@@ -1,59 +1,80 @@
1
1
  <script lang="ts">import { onDestroy } from "svelte";
2
2
  import { getBase, getEdition } from "./Contexts";
3
- import { buildEPUB } from "../../epub/build";
4
- import { DEFAULT_SIZE, SIZES } from "../../epub/images";
3
+ import { buildEPUB } from "../../epub/build.js";
4
+ import { DEFAULT_SIZE, SIZES } from "../../epub/images.js";
5
5
  let edition = getEdition();
6
6
  let base = getBase();
7
7
  let sizeID = $state(DEFAULT_SIZE.id);
8
+ let grayscale = $state(false);
8
9
  let building = $state(false);
9
- let progress = $state(void 0);
10
- let error = $state(void 0);
10
+ let progress = $state(undefined);
11
+ let error = $state(undefined);
11
12
  let warnings = $state([]);
12
- let url = $state(void 0);
13
+ let url = $state(undefined);
13
14
  let filename = $state("book.epub");
15
+ /** Release the previous blob before replacing or discarding it. */
14
16
  function release() {
15
- if (url !== void 0) {
16
- URL.revokeObjectURL(url);
17
- url = void 0;
18
- }
17
+ if (url !== undefined) {
18
+ URL.revokeObjectURL(url);
19
+ url = undefined;
20
+ }
21
+ }
22
+ /**
23
+ * Any change to the controls makes an already-built file stale: without
24
+ * this the reader could flip a setting and download something that doesn't
25
+ * match what the controls say.
26
+ */
27
+ function invalidate() {
28
+ release();
29
+ warnings = [];
30
+ error = undefined;
19
31
  }
20
32
  function chooseSize(id) {
21
- sizeID = id;
22
- release();
23
- warnings = [];
24
- error = void 0;
33
+ sizeID = id;
34
+ invalidate();
35
+ }
36
+ function chooseGrayscale(on) {
37
+ grayscale = on;
38
+ invalidate();
25
39
  }
26
40
  async function build() {
27
- if ($edition === void 0 || building) return;
28
- building = true;
29
- release();
30
- error = void 0;
31
- warnings = [];
32
- progress = { phase: "images", done: 0, total: 0 };
33
- try {
34
- const result = await buildEPUB($edition, {
35
- base: $base ?? "",
36
- size: SIZES[sizeID] ?? DEFAULT_SIZE,
37
- onProgress: (update) => progress = update
38
- });
39
- url = URL.createObjectURL(result.blob);
40
- filename = result.filename;
41
- warnings = result.warnings;
42
- } catch (problem) {
43
- console.error(problem);
44
- error = "Something went wrong building the e-book.";
45
- } finally {
46
- building = false;
47
- progress = void 0;
48
- }
41
+ if ($edition === undefined || building) return;
42
+ building = true;
43
+ release();
44
+ error = undefined;
45
+ warnings = [];
46
+ progress = {
47
+ phase: "images",
48
+ done: 0,
49
+ total: 0
50
+ };
51
+ try {
52
+ // Everything below runs only on a click, so the browser-only APIs
53
+ // it reaches are never touched while server rendering.
54
+ const result = await buildEPUB($edition, {
55
+ base: $base ?? "",
56
+ size: SIZES[sizeID] ?? DEFAULT_SIZE,
57
+ grayscale,
58
+ onProgress: (update) => progress = update
59
+ });
60
+ url = URL.createObjectURL(result.blob);
61
+ filename = result.filename;
62
+ warnings = result.warnings;
63
+ } catch (problem) {
64
+ console.error(problem);
65
+ error = "Something went wrong building the e-book.";
66
+ } finally {
67
+ building = false;
68
+ progress = undefined;
69
+ }
49
70
  }
71
+ // $derived.by rather than $derived: svelte2tsx mis-narrows the type of a
72
+ // value read inside a multiline $derived expression.
50
73
  let status = $derived.by(() => {
51
- if (progress === void 0) return void 0;
52
- if (progress.phase === "images")
53
- return progress.total === 0 ? "Gathering images\u2026" : `Shrinking image ${progress.done} of ${progress.total}\u2026`;
54
- if (progress.phase === "chapters")
55
- return `Preparing chapter ${progress.done} of ${progress.total}\u2026`;
56
- return "Packaging the book\u2026";
74
+ if (progress === undefined) return undefined;
75
+ if (progress.phase === "images") return progress.total === 0 ? "Gathering images…" : `Shrinking image ${progress.done} of ${progress.total}…`;
76
+ if (progress.phase === "chapters") return `Preparing chapter ${progress.done} of ${progress.total}…`;
77
+ return "Packaging the book…";
57
78
  });
58
79
  onDestroy(release);
59
80
  </script>
@@ -76,6 +97,19 @@ onDestroy(release);
76
97
  </select>
77
98
  </span>
78
99
  </label>
100
+ <label class="greyscale">
101
+ <input
102
+ type="checkbox"
103
+ checked={grayscale}
104
+ disabled={building}
105
+ onchange={(event) =>
106
+ chooseGrayscale(event.currentTarget.checked)}
107
+ />
108
+ Greyscale
109
+ <span class="note"
110
+ >(a little smaller; e-readers convert anyway)</span
111
+ >
112
+ </label>
79
113
  <!-- A plain button rather than the app's, whose --app-* variables are
80
114
  only defined in the (app) layout, so it renders unstyled here and
81
115
  in books compiled by bookish-reader. -->
@@ -226,6 +260,14 @@ onDestroy(release);
226
260
  text-decoration: underline;
227
261
  }
228
262
 
263
+ .greyscale {
264
+ gap: 0.35em;
265
+ }
266
+
267
+ .greyscale .note {
268
+ color: var(--bookish-muted-color);
269
+ }
270
+
229
271
  .warnings {
230
272
  font-size: var(--bookish-small-font-size);
231
273
  font-style: italic;
@@ -1,4 +1,4 @@
1
- import type { TOCHeaderKey } from '../../models/book/ChapterID';
1
+ import type { TOCHeaderKey } from '../../models/book/ChapterID.js';
2
2
  interface Props {
3
3
  id: TOCHeaderKey;
4
4
  /** ARIA label */
@@ -2,47 +2,61 @@
2
2
  import { goto } from "$app/navigation";
3
3
  import { onMount, setContext, untrack } from "svelte";
4
4
  import { writable } from "svelte/store";
5
- import {
6
- BASE,
7
- DARK_MODE,
8
- getBook
9
- } from "./Contexts";
10
- import { BookishTheme } from "../../models/book/Theme";
5
+ import { BASE, DARK_MODE, getBook } from "./Contexts";
6
+ import { BookishTheme } from "../../models/book/Theme.js";
11
7
  import { isDark, setDark } from "../../util/dark";
12
8
  import { page } from "$app/state";
13
9
  import Title from "./Title.svelte";
14
10
  import Page from "./Page.svelte";
15
- import Parser from "../../models/chapter/Parser";
16
- import EmbedNode from "../../models/chapter/EmbedNode";
11
+ import Parser from "../../models/chapter/Parser.js";
12
+ import EmbedNode from "../../models/chapter/EmbedNode.js";
13
+ // Poly fill smooth scrolling for Safari.
17
14
  onMount(() => smoothscroll.polyfill());
18
15
  let { edition, base = "", children } = $props();
16
+ /** The cover image's URL for social previews. The cover is stored as
17
+ * Bookish embed markup, so it must be parsed; emitting the raw markup
18
+ * would produce a nonsense og:image (and stray pages, when a static
19
+ * site generator crawls it as a link). */
19
20
  let coverURL = $derived.by(() => {
20
- const cover = edition?.getImage("cover");
21
- if (!cover) return void 0;
22
- const node = Parser.parseEmbed(edition, cover);
23
- const url = node instanceof EmbedNode ? node.getURL() : void 0;
24
- if (!url || url.length === 0) return void 0;
25
- return url.startsWith("http") ? url : `${base}/images/${url}`;
21
+ const cover = edition?.getImage("cover");
22
+ if (!cover) return undefined;
23
+ const node = Parser.parseEmbed(edition, cover);
24
+ const url = node instanceof EmbedNode ? node.getURL() : undefined;
25
+ if (!url || url.length === 0) return undefined;
26
+ // Resolve local images the same way Embed.svelte does.
27
+ return url.startsWith("http") ? url : `${base}/images/${url}`;
26
28
  });
27
29
  let book = getBook();
30
+ // Expose the base path to descendants, keeping it up to date when it changes.
31
+ // Deliberately seeded with the initial base (untracked); the effect
32
+ // below keeps it in sync when the prop changes.
28
33
  let baseStore = writable(untrack(() => base));
29
34
  setContext(BASE, baseStore);
30
35
  $effect.pre(() => {
31
- baseStore.set(base);
36
+ baseStore.set(base);
32
37
  });
38
+ // Default dark mode to whatever's stored in local storage, if anything.
39
+ // respect user choice on the website despite the system theme
33
40
  let darkMode = writable(isDark());
41
+ // Expose dark mode to descendants
34
42
  setContext(DARK_MODE, darkMode);
35
- let themeCSS = $derived(
36
- themeToCSS(edition?.getTheme() ?? BookishTheme)
37
- );
43
+ // The CSS to set on the edition, kept up to date with the edition's theme.
44
+ let themeCSS = $derived(themeToCSS(edition?.getTheme() ?? BookishTheme));
45
+ // When dark mode changes, update the body's class list.
38
46
  $effect.pre(() => {
39
- setDark($darkMode);
47
+ setDark($darkMode);
40
48
  });
41
- if (typeof window !== "undefined" && window.location.hash.startsWith("#/"))
42
- goto(location.hash.replace("#", ""));
49
+ // Redirect old hash routes by simply replacing their hash before routing.
50
+ // SvelteKit 3's goto rejects paths that match no route, where SvelteKit 2
51
+ // fell back to a full page load; keep that fallback.
52
+ if (typeof window !== "undefined" && window.location.hash.startsWith("#/")) {
53
+ const path = location.hash.replace("#", "");
54
+ goto(path).catch(() => window.location.href = path);
55
+ }
56
+ /** Given a theme, compute the CSS rules that apply the theme. */
43
57
  function themeToCSS(theme) {
44
- if (theme === null) theme = BookishTheme;
45
- return `
58
+ if (theme === null) theme = BookishTheme;
59
+ return `
46
60
  ${(theme.imports ?? []).map((url) => `@import url(${url});`).join("\n")}
47
61
  .bookish {
48
62
  ${theme.light ? toRules(theme.light) : ""}
@@ -57,13 +71,13 @@ function themeToCSS(theme) {
57
71
  `;
58
72
  }
59
73
  function toRules(set) {
60
- return Object.keys(set).map((name) => {
61
- const value = set[name];
62
- if (value.length > 0) {
63
- const cssVariable = "--bookish-" + name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((s) => s.toLowerCase()).join("-").replace(/([0-9])/g, "-$1-");
64
- return `${cssVariable}: ${value};`;
65
- }
66
- }).join("\n ");
74
+ return Object.keys(set).map((name) => {
75
+ const value = set[name];
76
+ if (value.length > 0) {
77
+ const cssVariable = "--bookish-" + name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((s) => s.toLowerCase()).join("-").replace(/([0-9])/g, "-$1-");
78
+ return `${cssVariable}: ${value};`;
79
+ }
80
+ }).join("\n ");
67
81
  }
68
82
  </script>
69
83
 
@@ -1,4 +1,4 @@
1
- import type EditionModel from '../../models/book/Edition';
1
+ import type EditionModel from '../../models/book/Edition.js';
2
2
  interface Props {
3
3
  edition: EditionModel | undefined;
4
4
  base?: string;
@@ -1,14 +1,8 @@
1
1
  <script lang="ts">import Loading from "./Loading.svelte";
2
2
  import Edition from "./Edition.svelte";
3
3
  import Feedback from "../app/Feedback.svelte";
4
- import {
5
- getBook,
6
- getEdition,
7
- getStatus,
8
- getUser,
9
- getChapterText
10
- } from "./Contexts";
11
- import BookSaveStatus from "../../models/book/BookSaveStatus";
4
+ import { getBook, getEdition, getStatus, getUser, getChapterText } from "./Contexts.js";
5
+ import BookSaveStatus from "../../models/book/BookSaveStatus.js";
12
6
  import { get } from "svelte/store";
13
7
  import { onDestroy, untrack } from "svelte";
14
8
  import { page } from "$app/state";
@@ -19,117 +13,124 @@ let auth = getUser();
19
13
  let book = getBook();
20
14
  let edition = getEdition();
21
15
  let status = getStatus();
16
+ /** Whether the edition store has been set by something other than this
17
+ * loader's own snapshot application (i.e., a local edit) since the last
18
+ * completed save. While true, incoming snapshots are by definition older
19
+ * than local state and applying them would lose the newer edits, so they
20
+ * are skipped; the pending save's own confirmation snapshot arrives
21
+ * after the save completes and is applied then. (Reader routes never
22
+ * edit, so this stays false there.) */
22
23
  let locallyEdited = false;
23
24
  let applyingSnapshot = false;
24
25
  const localEditWatch = edition.subscribe(() => {
25
- if (!applyingSnapshot) locallyEdited = true;
26
+ if (!applyingSnapshot) locallyEdited = true;
26
27
  });
28
+ // When a save completes, local state and server state agree again.
27
29
  const statusWatch = status?.subscribe((current) => {
28
- if (current === BookSaveStatus.Saved) locallyEdited = false;
30
+ if (current === BookSaveStatus.Saved) locallyEdited = false;
29
31
  });
30
32
  function skipStaleSnapshot() {
31
- const current = status ? get(status) : void 0;
32
- return locallyEdited && (current === BookSaveStatus.Changed || current === BookSaveStatus.Saving);
33
+ const current = status ? get(status) : undefined;
34
+ return locallyEdited && (current === BookSaveStatus.Changed || current === BookSaveStatus.Saving);
33
35
  }
34
36
  function applySnapshot(apply) {
35
- applyingSnapshot = true;
36
- try {
37
- apply();
38
- } finally {
39
- applyingSnapshot = false;
40
- }
37
+ applyingSnapshot = true;
38
+ try {
39
+ apply();
40
+ } finally {
41
+ applyingSnapshot = false;
42
+ }
41
43
  }
42
- let chapterText = /* @__PURE__ */ new Map();
43
- let editionUnsub = void 0;
44
- let chaptersUnsub = void 0;
45
- let error = $state(void 0);
44
+ /** Mapping from Firestore docuemnt IDs to chapter text */
45
+ let chapterText = new Map();
46
+ let editionUnsub = undefined;
47
+ let chaptersUnsub = undefined;
48
+ let error = $state(undefined);
46
49
  function unsub() {
47
- if (editionUnsub) editionUnsub();
48
- if (chaptersUnsub) chaptersUnsub();
50
+ // Unsubscribe to the old edition document if we were listening to one.
51
+ if (editionUnsub) editionUnsub();
52
+ if (chaptersUnsub) chaptersUnsub();
49
53
  }
50
54
  function listen() {
51
- unsub();
52
- error = void 0;
53
- if ($book === void 0) {
54
- edition.set(void 0);
55
- error = "Unknown book.";
56
- return;
57
- } else {
58
- currentEditionID = page.params.editionid;
59
- const latestPublished = !write;
60
- let editionID;
61
- if (currentEditionID === void 0) {
62
- if (latestPublished) {
63
- editionID = $book.getLatestPublishedEditionID();
64
- if (editionID === void 0)
65
- error = "There is no published edition of this book.";
66
- } else {
67
- editionID = $book.getLatestEditionID();
68
- }
69
- } else {
70
- editionID = $book.getEditionNumberID(
71
- parseFloat(currentEditionID)
72
- );
73
- if (editionID === void 0)
74
- error = `There is no ${currentEditionID} edition of this book`;
75
- }
76
- if (editionID !== void 0) {
77
- editionUnsub = listenToEdition(
78
- $book.getID(),
79
- editionID,
80
- (ed) => {
81
- if (skipStaleSnapshot()) return;
82
- if (ed) {
83
- applySnapshot(
84
- () => edition.set(ed.withChapterText(chapterText))
85
- );
86
- } else {
87
- applySnapshot(() => edition.set(void 0));
88
- error = "Unable to load edition";
89
- }
90
- }
91
- );
92
- chaptersUnsub = listenToChapters(
93
- $book.getID(),
94
- editionID,
95
- (chapters) => {
96
- const stale = skipStaleSnapshot();
97
- for (const [ref, text] of chapters)
98
- if (!stale || !chapterText.has(ref.id))
99
- chapterText.set(ref.id, text);
100
- if ($edition) {
101
- applySnapshot(
102
- () => edition.set(
103
- $edition.withChapterText(chapterText)
104
- )
105
- );
106
- }
107
- }
108
- );
109
- }
110
- }
55
+ unsub();
56
+ error = undefined;
57
+ // If we no longer have a book, we can't get an edition.
58
+ if ($book === undefined) {
59
+ edition.set(undefined);
60
+ error = "Unknown book.";
61
+ return;
62
+ } else {
63
+ currentEditionID = page.params.editionid;
64
+ const latestPublished = !write;
65
+ // Figure out which edition to load.
66
+ let editionID;
67
+ if (currentEditionID === undefined) {
68
+ if (latestPublished) {
69
+ editionID = $book.getLatestPublishedEditionID();
70
+ if (editionID === undefined) error = "There is no published edition of this book.";
71
+ } else {
72
+ editionID = $book.getLatestEditionID();
73
+ }
74
+ } else {
75
+ editionID = $book.getEditionNumberID(parseFloat(currentEditionID));
76
+ if (editionID === undefined) error = `There is no ${currentEditionID} edition of this book`;
77
+ }
78
+ // Listen to the doc for changes and listen to all of its chapters.
79
+ if (editionID !== undefined) {
80
+ editionUnsub = listenToEdition($book.getID(), editionID, (ed) => {
81
+ if (skipStaleSnapshot()) return;
82
+ if (ed) {
83
+ // Before setting the new edition, augmented it with the chapter text.
84
+ applySnapshot(() => edition.set(ed.withChapterText(chapterText)));
85
+ } else {
86
+ applySnapshot(() => edition.set(undefined));
87
+ error = "Unable to load edition";
88
+ }
89
+ });
90
+ chaptersUnsub = listenToChapters($book.getID(), editionID, (chapters) => {
91
+ // Cache the text of the chapters we received — except
92
+ // text we already have while local edits await a save,
93
+ // which may be a stale echo of what the user since
94
+ // revised. Text for a chapter not yet cached is load
95
+ // data, not an echo; it must always be kept, since
96
+ // nothing will re-deliver it. (The effect below keeps
97
+ // this cache current with local edits, so applying it
98
+ // to the edition never loses anything.)
99
+ const stale = skipStaleSnapshot();
100
+ for (const [ref, text] of chapters) if (!stale || !chapterText.has(ref.id)) chapterText.set(ref.id, text);
101
+ // Update the chapter text in the current edition.
102
+ if ($edition) {
103
+ applySnapshot(() => edition.set($edition.withChapterText(chapterText)));
104
+ }
105
+ });
106
+ }
107
+ }
111
108
  }
109
+ // Remember the chapter text whenever the edition changes, so we don't overwrite stale text.
110
+ // when other things change.
112
111
  $effect.pre(() => {
113
- if ($edition) {
114
- for (const chapter of $edition.chapters) {
115
- if (chapter.ref && chapter.text !== null)
116
- chapterText.set(chapter.ref.id, chapter.text);
117
- }
118
- }
112
+ if ($edition) {
113
+ for (const chapter of $edition.chapters) {
114
+ if (chapter.ref && chapter.text !== null) chapterText.set(chapter.ref.id, chapter.text);
115
+ }
116
+ }
119
117
  });
118
+ // Whenever the requested edition or the user changes, change the listener.
119
+ // currentEditionID is read untracked because listen() writes it; tracking
120
+ // it would make this effect invalidate itself.
120
121
  $effect(() => {
121
- const requestedEditionID = page.params.editionid;
122
- const user = $auth?.user;
123
- untrack(() => {
124
- if ((currentEditionID === void 0 || requestedEditionID !== currentEditionID) && user !== null && user !== void 0)
125
- listen();
126
- });
122
+ const requestedEditionID = page.params.editionid;
123
+ const user = $auth?.user;
124
+ untrack(() => {
125
+ if ((currentEditionID === undefined || requestedEditionID !== currentEditionID) && user !== null && user !== undefined) listen();
126
+ });
127
127
  });
128
+ // When unmounted, unset the stores — no longer viewing a book.
128
129
  onDestroy(() => {
129
- localEditWatch();
130
- statusWatch?.();
131
- edition.set(void 0);
132
- unsub();
130
+ localEditWatch();
131
+ statusWatch?.();
132
+ edition.set(undefined);
133
+ unsub();
133
134
  });
134
135
  </script>
135
136
 
@@ -1,5 +1,4 @@
1
- <script lang="ts">import { createNewEdition } from "../../models/CRUD";
2
- import Instructions from "./Instructions.svelte";
1
+ <script lang="ts">import Instructions from "./Instructions.svelte";
3
2
  import Note from "../editor/Note.svelte";
4
3
  import { getBook, getUser, isBookEditable } from "./Contexts";
5
4
  import Button from "../app/Button.svelte";
@@ -13,23 +12,24 @@ let editable = isBookEditable();
13
12
  let user = getUser();
14
13
  let publisher = $state(false);
15
14
  async function updateUserClaims() {
16
- if ($user === void 0 || $user.user === null) return;
17
- const token = await $user.user.getIdTokenResult();
18
- publisher = "publisher" in token.claims && token.claims.publisher === true;
15
+ if ($user === undefined || $user.user === null) return;
16
+ const token = await $user.user.getIdTokenResult();
17
+ publisher = "publisher" in token.claims && token.claims.publisher === true;
19
18
  }
20
19
  async function createEdition() {
21
- if ($book === void 0) return;
22
- try {
23
- const revisedBook = await createNewEdition($book);
24
- book.set(revisedBook);
25
- } catch (err) {
26
- console.error("Error creating new edition:", err);
27
- }
20
+ if ($book === undefined) return;
21
+ try {
22
+ const { createNewEdition } = await import("../../models/CRUD");
23
+ const revisedBook = await createNewEdition($book);
24
+ book.set(revisedBook);
25
+ } catch (err) {
26
+ console.error("Error creating new edition:", err);
27
+ }
28
28
  }
29
29
  let editions = $derived($book?.getEditions());
30
30
  let latestPublishedID = $derived($book?.getLatestPublishedEditionID());
31
31
  $effect(() => {
32
- if ($user && $user.user) updateUserClaims();
32
+ if ($user && $user.user) updateUserClaims();
33
33
  });
34
34
  </script>
35
35
 
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import Header from "./Header.svelte";
2
2
  import Outline from "./Outline.svelte";
3
3
  import Page from "./Page.svelte";
4
- import ChapterIDs from "../../models/book/ChapterID";
4
+ import ChapterIDs from "../../models/book/ChapterID.js";
5
5
  import DefinitionView from "./DefinitionView.svelte";
6
6
  import { getUser, getEdition, isEditionEditable } from "./Contexts";
7
7
  import Instructions from "./Instructions.svelte";
@@ -10,16 +10,15 @@ let auth = getUser();
10
10
  let edition = getEdition();
11
11
  let editable = isEditionEditable();
12
12
  let glossary = $derived($edition?.getGlossary() ?? {});
13
- let keys = $derived($edition === void 0 ? [] : glossary === void 0 || Object.keys(glossary).length === 0 ? null : Object.keys(glossary).sort(
14
- (a, b) => glossary[a].phrase.localeCompare(glossary[b].phrase)
15
- ));
13
+ // Sort by canonical phrases
14
+ let keys = $derived($edition === undefined ? [] : glossary === undefined || Object.keys(glossary).length === 0 ? null : Object.keys(glossary).sort((a, b) => glossary[a].phrase.localeCompare(glossary[b].phrase)));
16
15
  function addEmptyDefinition() {
17
- if ($edition === void 0) return;
18
- const letters = "abcdefghijklmnopqrstuvwxyz".split("");
19
- let id = "";
20
- while (id.length < 4 || $edition.hasDefinition(id))
21
- id = id + letters[Math.round(Math.random() * 26)];
22
- edition.set($edition.withDefinition(id, "", "", []));
16
+ if ($edition === undefined) return;
17
+ // Generate an ID.
18
+ const letters = "abcdefghijklmnopqrstuvwxyz".split("");
19
+ let id = "";
20
+ while (id.length < 4 || $edition.hasDefinition(id)) id = id + letters[Math.round(Math.random() * 26)];
21
+ edition.set($edition.withDefinition(id, "", "", []));
23
22
  }
24
23
  </script>
25
24
 
@@ -1,4 +1,4 @@
1
- <script lang="ts">import Parser from "../../models/chapter/Parser";
1
+ <script lang="ts">import Parser from "../../models/chapter/Parser.js";
2
2
  import Embed from "../chapter/Embed.svelte";
3
3
  import BookishEditor from "../editor/BookishEditor.svelte";
4
4
  import Title from "./Title.svelte";
@@ -7,45 +7,32 @@ import TextEditor from "../editor/TextEditor.svelte";
7
7
  import { getUser, getEdition, getLeasee, lease } from "./Contexts";
8
8
  import Button from "../app/Button.svelte";
9
9
  import ConfirmButton from "../editor/ConfirmButton.svelte";
10
- let {
11
- editable,
12
- label,
13
- header,
14
- id,
15
- subtitle = void 0,
16
- print = false,
17
- tags = void 0,
18
- getImage = void 0,
19
- setImage = void 0,
20
- save = null,
21
- outline,
22
- before,
23
- after
24
- } = $props();
10
+ let { editable, label, header, id, subtitle = undefined, print = false, tags = undefined, getImage = undefined, setImage = undefined, save = null, outline, before, after } = $props();
25
11
  let title = $state(null);
26
12
  let showReminder = $state(true);
27
13
  let auth = getUser();
28
14
  let edition = getEdition();
29
15
  function updateScrollReminder() {
30
- if (title)
31
- showReminder = window.scrollY + window.innerHeight <= title.getBoundingClientRect().top + window.scrollY;
16
+ // If the bottom of the window is below the top of the title, hide the reminder.
17
+ if (title) showReminder = window.scrollY + window.innerHeight <= title.getBoundingClientRect().top + window.scrollY;
32
18
  }
33
19
  function addCover() {
34
- if (setImage) setImage("|||||");
20
+ if (setImage) setImage("|||||");
35
21
  }
36
22
  function removeCover() {
37
- if (setImage) setImage(null);
23
+ if (setImage) setImage(null);
38
24
  }
25
+ // When the title becomes visible or hidden, update the scroll reminder.
39
26
  onMount(() => {
40
- const intersectionObserver = new IntersectionObserver(
41
- () => updateScrollReminder()
42
- );
43
- if (title) intersectionObserver.observe(title);
44
- updateScrollReminder();
45
- return () => {
46
- if (title) intersectionObserver.unobserve(title);
47
- };
27
+ const intersectionObserver = new IntersectionObserver(() => updateScrollReminder());
28
+ if (title) intersectionObserver.observe(title);
29
+ updateScrollReminder();
30
+ // Stop observing when unmounted.
31
+ return () => {
32
+ if (title) intersectionObserver.unobserve(title);
33
+ };
48
34
  });
35
+ // Get the embed, update when getImage function prop changes.
49
36
  let embed = $derived(getImage ? getImage() : null);
50
37
  </script>
51
38