bookish-press 0.5.62 → 0.5.73

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 (43) hide show
  1. package/README.md +30 -1
  2. package/dist/components/app/Options.svelte +1 -0
  3. package/dist/components/app/Table.svelte +0 -1
  4. package/dist/components/chapter/CaptionedCode.svelte +21 -19
  5. package/dist/components/chapter/Figure.svelte +5 -3
  6. package/dist/components/chapter/Figure.svelte.d.ts +1 -0
  7. package/dist/components/chapter/Link.svelte +4 -0
  8. package/dist/components/chapter/Paragraph.svelte +18 -0
  9. package/dist/components/chapter/Table.svelte +1 -1
  10. package/dist/components/editor/BookishEditor.svelte +15 -6
  11. package/dist/components/editor/CaretView.svelte +8 -3
  12. package/dist/components/editor/Commands.js +22 -4
  13. package/dist/components/editor/LinkEditor.svelte +18 -5
  14. package/dist/components/editor/icons/header4.svg +7 -0
  15. package/dist/components/page/Analytics.svelte +19 -0
  16. package/dist/components/page/Analytics.svelte.d.ts +16 -0
  17. package/dist/components/page/BulkReferenceEditor.svelte +1 -1
  18. package/dist/components/page/Chapter.svelte +4 -5
  19. package/dist/components/page/DefinitionView.svelte +4 -3
  20. package/dist/components/page/Edition.svelte +2 -2
  21. package/dist/components/page/EditionLoader.svelte +9 -18
  22. package/dist/components/page/Editions.svelte +4 -3
  23. package/dist/components/page/Glossary.svelte +8 -2
  24. package/dist/components/page/Index.svelte +5 -2
  25. package/dist/components/page/Media.svelte +5 -2
  26. package/dist/components/page/Reference.svelte +24 -15
  27. package/dist/components/page/References.svelte +6 -2
  28. package/dist/components/page/Search.svelte +5 -3
  29. package/dist/components/page/SubdomainEditor.svelte +4 -3
  30. package/dist/components/page/TableOfContents.svelte +42 -11
  31. package/dist/components/page/TableOfContentsRow.svelte +3 -1
  32. package/dist/models/CRUD.d.ts +1 -2
  33. package/dist/models/CRUD.js +4 -6
  34. package/dist/models/book/BookMedia.js +8 -3
  35. package/dist/models/book/Chapter.js +2 -3
  36. package/dist/models/book/Edition.d.ts +10 -4
  37. package/dist/models/book/Edition.js +35 -34
  38. package/dist/models/chapter/ParagraphNode.js +4 -14
  39. package/dist/models/chapter/Parser.d.ts +1 -0
  40. package/dist/models/chapter/Parser.js +2 -1
  41. package/dist/models/chapter/TableNode.js +3 -1
  42. package/dist/util/mineReference.js +8 -6
  43. package/package.json +13 -10
@@ -231,7 +231,7 @@ function move(field, direction) {
231
231
  </tr>
232
232
  </Table>
233
233
  {:else}
234
- <!-- If not editable, just render the reference. -->
234
+ <!-- Otherwise, just render the reference. -->
235
235
  <p class="reference">
236
236
  {#if reference.authors}{reference.authors}{:else}<em>Authors</em>{/if}
237
237
  {#if reference.year}({reference.year}){:else}<em>Year</em>{/if}. {#if reference.url === null || reference.url.length === 0}{#if reference.title}{reference.title}{:else}<em
@@ -247,26 +247,31 @@ function move(field, direction) {
247
247
  </p>
248
248
  {/if}
249
249
  {#if edit && editable && !reference.short}
250
- <ConfirmButton
251
- tooltip="delete reference {reference.title}"
252
- confirm="delete reference"
253
- command={() =>
254
- $edition
255
- ? edition.set($edition.withoutReference(reference.citationID))
256
- : undefined}>⨉</ConfirmButton
257
- >
258
- <Button
259
- tooltip="stop editing reference"
260
- command={() => (editing ? stopEditing() : startEditing())}
261
- >{editing ? 'done' : 'edit'}</Button
262
- >
250
+ <div class="controls">
251
+ {#if editable}<Note>{reference.citationID}</Note>{/if}
252
+ <ConfirmButton
253
+ tooltip="delete reference {reference.title}"
254
+ confirm="delete reference"
255
+ command={() =>
256
+ $edition
257
+ ? edition.set(
258
+ $edition.withoutReference(reference.citationID)
259
+ )
260
+ : undefined}>⨉</ConfirmButton
261
+ >
262
+ <Button
263
+ tooltip="stop editing reference"
264
+ command={() => (editing ? stopEditing() : startEditing())}
265
+ >{editing ? 'done' : 'edit'}</Button
266
+ >
267
+ </div>
263
268
  {/if}
264
269
 
265
270
  <style>
266
271
  .reference {
267
272
  line-height: var(--bookish-paragraph-line-height-tight);
268
273
  margin-top: 0;
269
- margin-bottom: var(--bookish-block-padding);
274
+ margin-top: var(--bookish-block-padding);
270
275
  }
271
276
 
272
277
  p {
@@ -289,4 +294,8 @@ function move(field, direction) {
289
294
  td {
290
295
  line-height: 1;
291
296
  }
297
+
298
+ .controls {
299
+ margin-top: calc(var(--bookish-block-padding) / 2);
300
+ }
292
301
  </style>
@@ -36,9 +36,13 @@ if (references !== null) {
36
36
  <Outline
37
37
  slot="outline"
38
38
  previous={$edition.getPreviousChapterID(
39
- ChapterIDs.ReferencesID
39
+ ChapterIDs.ReferencesID,
40
+ editable
41
+ )}
42
+ next={$edition.getNextChapterID(
43
+ ChapterIDs.ReferencesID,
44
+ editable
40
45
  )}
41
- next={$edition.getNextChapterID(ChapterIDs.ReferencesID)}
42
46
  />
43
47
  </Header>
44
48
  {#if editable}<BulkReferenceEditor />{/if}
@@ -5,7 +5,6 @@ import Link from '../Link.svelte';
5
5
  import Chapter from '../../models/book/Chapter';
6
6
  import ChapterIDs from '../../models/book/ChapterID';
7
7
  import { getBase, getEdition, isEditionEditable } from './Contexts';
8
- import TextInput from '../app/TextInput.svelte';
9
8
  import ChapterTitle from './ChapterTitle.svelte';
10
9
  import ChapterNumber from './ChapterNumber.svelte';
11
10
  import PageHeader from './PageHeader.svelte';
@@ -76,8 +75,11 @@ $: {
76
75
  >
77
76
  <Outline
78
77
  slot="outline"
79
- previous={$edition.getPreviousChapterID(ChapterIDs.SearchID)}
80
- next={$edition.getNextChapterID(ChapterIDs.SearchID)}
78
+ previous={$edition.getPreviousChapterID(
79
+ ChapterIDs.SearchID,
80
+ false
81
+ )}
82
+ next={$edition.getNextChapterID(ChapterIDs.SearchID, false)}
81
83
  />
82
84
  </Header>
83
85
 
@@ -1,6 +1,6 @@
1
1
  <script>import TextEditor from '../editor/TextEditor.svelte';
2
2
  import { isSubdomainAvailable } from '../../models/CRUD';
3
- import Note from '../editor/Note.svelte';
3
+ import { getSubdomain } from '../../util/getSubdomain';
4
4
  import { getBook } from './Contexts';
5
5
  import Muted from './Muted.svelte';
6
6
  let book = getBook();
@@ -22,9 +22,10 @@ const invalidNames = [
22
22
  save={// Save the new domain
23
23
  async (domain) => {
24
24
  if ($book) {
25
- const available = await isSubdomainAvailable(domain, $book);
25
+ const available = await isSubdomainAvailable(domain);
26
26
  if (available) book.set($book.withSubdomain(domain));
27
- else throw Error("Domain isn't available");
27
+ else if ($book.getSubdomain() !== domain)
28
+ throw Error("Domain isn't available");
28
29
  }
29
30
  }}
30
31
  placeholder="book name"
@@ -1,5 +1,4 @@
1
1
  <script>import Instructions from './Instructions.svelte';
2
- import Parser from '../../models/chapter/Parser';
3
2
  import TableOfContentsRow from './TableOfContentsRow.svelte';
4
3
  import ChapterIDs from '../../models/book/ChapterID';
5
4
  import Page from './Page.svelte';
@@ -95,7 +94,7 @@ onMount(() => {
95
94
  <Outline
96
95
  slot="outline"
97
96
  previous={null}
98
- next={$edition.getNextChapterID('')}
97
+ next={$edition.getNextChapterID('', editable)}
99
98
  />
100
99
  <div slot="after">
101
100
  <Authors
@@ -126,7 +125,7 @@ onMount(() => {
126
125
  <Description />
127
126
 
128
127
  <Instructions {editable}>
129
- This will appear on the <Link to="{$base}/read">book browsing</Link>
128
+ This will appear on the <Link to="/read">book browsing</Link>
130
129
  page and in your table of contents. Write an informative description
131
130
  of what your book is about.
132
131
  </Instructions>
@@ -153,16 +152,20 @@ onMount(() => {
153
152
  readingTime === undefined
154
153
  ? 'Forthcoming'
155
154
  : readingTime < 5
156
- ? '<5 min read'
157
- : readingTime < 60
158
- ? '~' + Math.floor(readingTime / 5) * 5 + ' min read'
159
- : '~' + Math.floor(readingTime / 60) + ' hour read'}
155
+ ? '<5 min read'
156
+ : readingTime < 60
157
+ ? '~' +
158
+ Math.floor(readingTime / 5) * 5 +
159
+ ' min read'
160
+ : '~' + Math.floor(readingTime / 60) + ' hour read'}
160
161
  {@const section = chapter.getSection()}
161
162
  {@const etc =
162
163
  readingEstimate +
163
164
  (!chapter.isForthcoming()
164
165
  ? getProgressDescription(
165
- chapterID in progress ? progress[chapterID] : null
166
+ chapterID in progress
167
+ ? progress[chapterID]
168
+ : null,
166
169
  )
167
170
  : '')}
168
171
 
@@ -184,7 +187,7 @@ onMount(() => {
184
187
  setChapter(
185
188
  edition,
186
189
  chapter,
187
- chapter.withSection(text)
190
+ chapter.withSection(text),
188
191
  )}
189
192
  />
190
193
  {:else if section}{section}{/if}
@@ -192,7 +195,7 @@ onMount(() => {
192
195
  You can edit this chapter.
193
196
  {/if}
194
197
  </span>
195
- <span slot="etc">
198
+ <span class="etc" slot="etc">
196
199
  <Muted>
197
200
  {#if editable}
198
201
  <Toggle
@@ -201,7 +204,7 @@ onMount(() => {
201
204
  setChapter(
202
205
  edition,
203
206
  chapter,
204
- chapter.asForthcoming(on)
207
+ chapter.asForthcoming(on),
205
208
  )}
206
209
  >
207
210
  {etc}
@@ -288,6 +291,28 @@ onMount(() => {
288
291
  <Editions editors={false} />
289
292
  {/if}
290
293
 
294
+ {#if editable && $edition}
295
+ <PageHeader id="editions">Analytics</PageHeader>
296
+ <Instructions {editable}
297
+ >Want to use Google Analytics to track traffic? Enter your tag
298
+ ID here.</Instructions
299
+ >
300
+ <TextEditor
301
+ label={'Google Analytics ag'}
302
+ text={$edition.gtagid ?? ''}
303
+ placeholder="gtag ID"
304
+ valid={() => undefined}
305
+ save={(text) =>
306
+ $edition
307
+ ? edition.set(
308
+ $edition.withGTag(
309
+ text.length === 0 ? null : text,
310
+ ),
311
+ )
312
+ : undefined}
313
+ />
314
+ {/if}
315
+
291
316
  {#if $book && (editable || isEditionPartiallyEditable())}
292
317
  <PageHeader id="editors">Editors</PageHeader>
293
318
  <Instructions {editable}>
@@ -310,3 +335,9 @@ onMount(() => {
310
335
  {/if}
311
336
  </Page>
312
337
  {/if}
338
+
339
+ <style>
340
+ .etc {
341
+ white-space: nowrap;
342
+ }
343
+ </style>
@@ -82,7 +82,7 @@ $: chapterURL =
82
82
  ? edition.set(
83
83
  $edition.withoutChapter(chapter.getID())
84
84
  )
85
- : undefined}>- chapter</ConfirmButton
85
+ : undefined}>⨉</ConfirmButton
86
86
  >
87
87
  {/if}
88
88
  </td>
@@ -102,6 +102,8 @@ $: chapterURL =
102
102
  display: flex;
103
103
  flex-direction: row;
104
104
  flex-wrap: nowrap;
105
+ justify-content: end;
106
+ margin-inline-end: var(--app-chrome-padding);
105
107
  gap: var(--app-chrome-padding);
106
108
  }
107
109
 
@@ -20,9 +20,8 @@ export declare function updateLock(edition: Edition, uid: string, content: strin
20
20
  * returns a map of DocumentReferences for new chapters so they can be persisted client side.
21
21
  */
22
22
  export declare function updateEdition(book: Book, previousEdition: Edition | undefined, newEdition: Edition): Promise<Map<string, DocumentReference>>;
23
- export declare function deleteChapterText(ref: DocumentReference): Promise<void>;
24
23
  export declare function updateBook(book: Book): Promise<void>;
25
- export declare function isSubdomainAvailable(subdomain: string, book: Book): Promise<boolean>;
24
+ export declare function isSubdomainAvailable(subdomain: string): Promise<boolean>;
26
25
  export declare function getBookIDWithSubdomain(subdomain: string): Promise<string | null>;
27
26
  export declare function publish(edition: Edition): Promise<string | {
28
27
  url: string;
@@ -62,7 +62,8 @@ export function listenToChapters(bookID, editionID, react) {
62
62
  const chapters = [];
63
63
  // For each chapter in the book, if the doc exists and it's not a pending local write, send updated chapters to the front end.
64
64
  docs.forEach((doc) => {
65
- if (doc.exists() && !doc.metadata.hasPendingWrites)
65
+ // We propagate pending writes to cycle back local edits to the client.
66
+ if (doc.exists())
66
67
  // If the chapter text is somehow null, set it to an empty string.
67
68
  // This accounts for previous defects that could lead to null.
68
69
  chapters.push([doc.ref, doc.data().text ?? '']);
@@ -112,7 +113,7 @@ export async function createBook(userID) {
112
113
  // Create a model to store it.
113
114
  let book = Book.fromJSON(bookRef.id, newBookData);
114
115
  // Make a new default edition with a single chapter with this user id.
115
- let newEdition = new Edition(bookRef, undefined, [], 'Untitled', [], 1, '', null, {}, '', [], 'All rights reserved.', '', [], {}, {}, {}, {}, null, null, {}).withNewChapter();
116
+ let newEdition = new Edition(bookRef, undefined, [], 'Untitled', [], 1, '', null, {}, '', [], 'All rights reserved.', '', [], {}, {}, {}, {}, null, null, {}, null).withNewChapter();
116
117
  // Add the edition to the editions collection in the database so we can get an edition ref.
117
118
  const editionRef = await addDoc(collection(db, 'books', bookRef.id, 'editions'), newEdition.toObject());
118
119
  // Remember the ref.
@@ -235,15 +236,12 @@ export async function updateEdition(book, previousEdition, newEdition) {
235
236
  await setDoc(doc(db, editionPath(newEdition.bookRef.id, editionRef.id)), newEdition.toObject());
236
237
  return newChapterRefs;
237
238
  }
238
- export async function deleteChapterText(ref) {
239
- deleteDoc(ref);
240
- }
241
239
  export async function updateBook(book) {
242
240
  if (!db)
243
241
  throw Error("Can't update book, not connected to Firebase.");
244
242
  await setDoc(doc(db, 'books', book.getID()), book.toJSON());
245
243
  }
246
- export async function isSubdomainAvailable(subdomain, book) {
244
+ export async function isSubdomainAvailable(subdomain) {
247
245
  if (!db)
248
246
  throw Error('Not connected to Firebase.');
249
247
  return (await getBookIDWithSubdomain(subdomain)) === null;
@@ -85,17 +85,22 @@ export default class BookMedia {
85
85
  // The canonical path format for Bookish images in the store is image/{bookid}/{imageid}
86
86
  // where {bookid} is the Firestore document ID of the book being edited and {imageid} is just a random id.
87
87
  const imageRef = ref(storage, `${this.getImagePath()}/${imageName}`);
88
- const thumbnailRef = ref(storage, `${this.getImagePath()}/thumbnails/${imageName}`);
89
88
  uploadBytes(imageRef, file)
90
89
  .then((snapshot) => {
91
90
  getDownloadURL(snapshot.ref)
92
91
  // After we get the download URL for the uploaded image,
93
92
  // try to get the thumbnail URL for the resized image on the server.
94
93
  .then((url) => finishedHandler(this.stripTokenFromURL(url), this.makeThumbnailURL(this.stripTokenFromURL(url))))
95
- .catch(() => errorHandler('Unable to get image URL.'));
94
+ .catch((error) => {
95
+ console.error(error);
96
+ errorHandler('Unable to get image URL.');
97
+ });
96
98
  })
97
99
  .then(() => this.cacheImages())
98
- .catch(() => errorHandler('Unable to upload image.'));
100
+ .catch((error) => {
101
+ console.error(error);
102
+ errorHandler('Unable to upload image.');
103
+ });
99
104
  // This seems to be broken in Firebase right now. Switched above to a no-feedback approach.
100
105
  // const uploadTask = uploadBytesResumable(imageRef, file);
101
106
  // uploadTask.on("state_changed",
@@ -156,9 +156,8 @@ export default class Chapter {
156
156
  }
157
157
  getWordCount(edition) {
158
158
  if (this._wordCount === undefined)
159
- this._wordCount = this.getAST(edition)
160
- ?.toText()
161
- .split(/\s+/).length;
159
+ this._wordCount = this.getAST(edition)?.toText().split(/\s+/)
160
+ .length;
162
161
  return this._wordCount;
163
162
  }
164
163
  // Utility function
@@ -27,9 +27,13 @@ export type EditionSpecification = {
27
27
  glossary: Record<string, Definition>;
28
28
  theme: Theme | null;
29
29
  base: string | null;
30
+ /** People who have edit access to this edition */
30
31
  uids: string[];
32
+ /** All uids who have edit access to at least one chapter. Used in firestore rules to give permission to update book. */
31
33
  chapteruids: string[];
32
34
  active: Record<string, string>;
35
+ /** An optional Google Analytics tag ID */
36
+ gtagid: string | null;
33
37
  };
34
38
  export default class Edition {
35
39
  readonly bookRef: DocumentReference;
@@ -53,7 +57,8 @@ export default class Edition {
53
57
  readonly theme: Theme | null;
54
58
  readonly base: string | null;
55
59
  readonly active: Record<string, string>;
56
- constructor(bookRef: DocumentReference, editionRef: DocumentReference | undefined, uids: string[], title: string, authors: string[], number: number, summary: string, published: number | null, images: Record<string, string | null>, description: string, chapters: Chapter[], license: string, acknowledgements: string, tags: string[], sources: Record<string, string>, references: Record<string, string | string[]> | Record<string, FormatNode | Reference>, symbols: Record<string, string>, glossary: Record<string, Definition>, theme: Theme | null, base: string | null, active: Record<string, string>);
60
+ readonly gtagid: string | null;
61
+ constructor(bookRef: DocumentReference, editionRef: DocumentReference | undefined, uids: string[], title: string, authors: string[], number: number, summary: string, published: number | null, images: Record<string, string | null>, description: string, chapters: Chapter[], license: string, acknowledgements: string, tags: string[], sources: Record<string, string>, references: Record<string, string | string[]> | Record<string, FormatNode | Reference>, symbols: Record<string, string>, glossary: Record<string, Definition>, theme: Theme | null, base: string | null, active: Record<string, string>, gtagid: string | null);
57
62
  static fromJSON(editionRef: DocumentReference | undefined, spec: EditionSpecification): Edition;
58
63
  static parseReference(citationID: string, ref: string | string[], edition: Edition, short?: boolean): FormatNode | Reference;
59
64
  toObject(): EditionSpecification;
@@ -78,7 +83,7 @@ export default class Edition {
78
83
  getChapterPosition(chapterID: string): number | undefined;
79
84
  getChapterCount(): number;
80
85
  withChapters(chapters: Chapter[]): Edition;
81
- withChapterText(text: [DocumentReference, string][]): Edition;
86
+ withChapterText(textByID: Map<string, string>): Edition;
82
87
  withRevisedChapter(previous: Chapter, edited: Chapter): Edition;
83
88
  withoutRefs(): Edition;
84
89
  withIncrementedEditionNumber(): Edition;
@@ -123,6 +128,7 @@ export default class Edition {
123
128
  hasLease(uid: string, content: string): boolean;
124
129
  isLeased(contentID: string): boolean;
125
130
  withLock(uid: string, content: string, lock: boolean): Edition;
131
+ withGTag(gtagid: string | null): Edition;
126
132
  getBookReadingTime(): number | undefined;
127
133
  getChapterNumber(chapterID: string): undefined;
128
134
  getSource(sourceID: string): string | null;
@@ -130,8 +136,8 @@ export default class Edition {
130
136
  getChapterSection(chapterID: string): string | undefined;
131
137
  getFootnoteSymbol(number: number): string;
132
138
  getBookIndex(): Record<string, Set<string>>;
133
- getNextChapterID(chapterID: string): string | null;
134
- getPreviousChapterID(chapterID: string): string | null;
139
+ getNextChapterID(chapterID: string, editable: boolean): string | null;
140
+ getPreviousChapterID(chapterID: string, editable: boolean): string | null;
135
141
  getEmbeds(): {
136
142
  embed: EmbedNode;
137
143
  chapterID: string | undefined;
@@ -25,9 +25,10 @@ export default class Edition {
25
25
  theme;
26
26
  base;
27
27
  active;
28
+ gtagid;
28
29
  // Given an object with a valid specification and an object mapping chapter IDs to chapter text,
29
30
  // construct an object representing a book.
30
- constructor(bookRef, editionRef, uids, title, authors, number, summary, published, images, description, chapters, license, acknowledgements, tags, sources, references, symbols, glossary, theme, base, active) {
31
+ constructor(bookRef, editionRef, uids, title, authors, number, summary, published, images, description, chapters, license, acknowledgements, tags, sources, references, symbols, glossary, theme, base, active, gtagid) {
31
32
  this.bookRef = bookRef;
32
33
  this.editionRef = editionRef;
33
34
  this.uids = uids;
@@ -55,11 +56,12 @@ export default class Edition {
55
56
  this.theme = theme;
56
57
  this.base = base;
57
58
  this.active = active;
59
+ this.gtagid = gtagid;
58
60
  }
59
61
  static fromJSON(editionRef, spec) {
60
62
  // Copy all of the specification metadata to fields.
61
63
  // Choose suitable defaults if the spec is lacking a field.
62
- return new Edition(spec.bookRef, editionRef, spec.uids, spec.title, spec.authors, spec.number, spec.summary, spec.published, spec.images, spec.description, spec.chapters.map((chap) => Chapter.fromJSON(chap)), spec.license, spec.acknowledgements, spec.tags ?? [], spec.sources ?? {}, spec.references ?? {}, spec.symbols ?? {}, spec.glossary ?? {}, spec.theme, spec.base, spec.active);
64
+ return new Edition(spec.bookRef, editionRef, spec.uids, spec.title, spec.authors, spec.number, spec.summary, spec.published, spec.images, spec.description, spec.chapters.map((chap) => Chapter.fromJSON(chap)), spec.license, spec.acknowledgements, spec.tags ?? [], spec.sources ?? {}, spec.references ?? {}, spec.symbols ?? {}, spec.glossary ?? {}, spec.theme, spec.base, spec.active, spec.gtagid);
63
65
  }
64
66
  static parseReference(citationID, ref, edition, short = false) {
65
67
  if (typeof ref === 'string')
@@ -103,6 +105,7 @@ export default class Edition {
103
105
  uids: this.uids.slice(),
104
106
  chapteruids: this.getChapterUIDS(),
105
107
  active: this.active,
108
+ gtagid: this.gtagid ?? null,
106
109
  };
107
110
  return editionJSON;
108
111
  }
@@ -116,37 +119,37 @@ export default class Edition {
116
119
  return Array.from(new Set(this.chapters.map((chapter) => chapter.uids).flat()));
117
120
  }
118
121
  withRef(editionRef) {
119
- return new Edition(this.bookRef, editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
122
+ return new Edition(this.bookRef, editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
120
123
  }
121
124
  getTitle() {
122
125
  return this.title;
123
126
  }
124
127
  withTitle(title) {
125
- return new Edition(this.bookRef, this.editionRef, this.uids, title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
128
+ return new Edition(this.bookRef, this.editionRef, this.uids, title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
126
129
  }
127
130
  getDescription() {
128
131
  return this.description;
129
132
  }
130
133
  setDescription(description) {
131
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
134
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
132
135
  }
133
136
  getAcknowledgements() {
134
137
  return this.acknowledgements;
135
138
  }
136
139
  withAcknowledgements(acks) {
137
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, acks, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
140
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, acks, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
138
141
  }
139
142
  withSummary(summary) {
140
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
143
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
141
144
  }
142
145
  asPublished(published) {
143
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, published ? Date.now() : null, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
146
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, published ? Date.now() : null, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
144
147
  }
145
148
  getLicense() {
146
149
  return this.license;
147
150
  }
148
151
  setLicense(license) {
149
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
152
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
150
153
  }
151
154
  getChapters() {
152
155
  return this.chapters.slice();
@@ -172,22 +175,17 @@ export default class Edition {
172
175
  return this.chapters.length;
173
176
  }
174
177
  withChapters(chapters) {
175
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
178
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
176
179
  }
177
- withChapterText(text) {
178
- // No need to make a new edition if no chapter was changed.
179
- let modified = false;
180
+ withChapterText(textByID) {
180
181
  const newChapters = this.chapters.map((chapter) => {
181
- if (chapter.ref === undefined)
182
+ if (chapter.ref === undefined) {
182
183
  return chapter;
183
- const match = text.find(([ref, text]) => chapter.ref !== undefined &&
184
- chapter.ref.id === ref.id &&
185
- chapter.text !== text);
186
- if (match)
187
- modified = true;
188
- return match ? chapter.withText(match[1]) : chapter;
184
+ }
185
+ const text = textByID.get(chapter.ref.id);
186
+ return text === undefined ? chapter : chapter.withText(text);
189
187
  });
190
- return modified ? this.withChapters(newChapters) : this;
188
+ return this.withChapters(newChapters);
191
189
  }
192
190
  withRevisedChapter(previous, edited) {
193
191
  const index = this.chapters.indexOf(previous);
@@ -203,7 +201,7 @@ export default class Edition {
203
201
  return this.withRef(undefined).withChapters(this.chapters.map((chap) => chap.withoutRef()));
204
202
  }
205
203
  withIncrementedEditionNumber() {
206
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number + 1, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
204
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number + 1, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
207
205
  }
208
206
  withNewChapter() {
209
207
  // Synthesize a chapter ID placeholder that doesn't overlap with existing chapter names
@@ -235,7 +233,7 @@ export default class Edition {
235
233
  const temp = newChapters[index + increment];
236
234
  newChapters[index + increment] = newChapters[index];
237
235
  newChapters[index] = temp;
238
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, newChapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
236
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, newChapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
239
237
  }
240
238
  getSymbols() {
241
239
  return this.symbols;
@@ -252,7 +250,7 @@ export default class Edition {
252
250
  : undefined;
253
251
  }
254
252
  withReferences(newReferences) {
255
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, newReferences, this.symbols, this.glossary, this.theme, this.base, this.active);
253
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, newReferences, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
256
254
  }
257
255
  withNewReferences(references) {
258
256
  const newReferences = Object.assign({}, this.references);
@@ -293,7 +291,7 @@ export default class Edition {
293
291
  return id in this.glossary;
294
292
  }
295
293
  withGlossary(glossary) {
296
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, glossary, this.theme, this.base, this.active);
294
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, glossary, this.theme, this.base, this.active, this.gtagid);
297
295
  }
298
296
  withDefinition(id, phrase, definition, synonyms) {
299
297
  const newGlossary = Object.assign({}, this.glossary);
@@ -330,7 +328,7 @@ export default class Edition {
330
328
  return this.theme;
331
329
  }
332
330
  withTheme(theme) {
333
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, theme, this.base, this.active);
331
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, theme, this.base, this.active, this.gtagid);
334
332
  }
335
333
  withThemeValue(group, name, value) {
336
334
  if (this.theme === null)
@@ -353,7 +351,7 @@ export default class Edition {
353
351
  .join(', ');
354
352
  }
355
353
  withAuthors(authors) {
356
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
354
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
357
355
  }
358
356
  withAuthor(name) {
359
357
  return this.withAuthors([...this.authors, name]);
@@ -376,7 +374,7 @@ export default class Edition {
376
374
  ]);
377
375
  }
378
376
  withEditors(uids) {
379
- return new Edition(this.bookRef, this.editionRef, uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
377
+ return new Edition(this.bookRef, this.editionRef, uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
380
378
  }
381
379
  hasImage(id) {
382
380
  return this.images[id] !== null;
@@ -387,7 +385,7 @@ export default class Edition {
387
385
  withImage(id, embed) {
388
386
  const newImages = Object.assign({}, this.images);
389
387
  newImages[id] = embed;
390
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, newImages, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
388
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, newImages, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, this.gtagid);
391
389
  }
392
390
  isEditor(uid) {
393
391
  return this.uids.includes(uid);
@@ -410,7 +408,10 @@ export default class Edition {
410
408
  newActive[content] = uid;
411
409
  else
412
410
  delete newActive[content];
413
- return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, newActive);
411
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, newActive, this.gtagid);
412
+ }
413
+ withGTag(gtagid) {
414
+ return new Edition(this.bookRef, this.editionRef, this.uids, this.title, this.authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active, gtagid);
414
415
  }
415
416
  getBookReadingTime() {
416
417
  return this.chapters
@@ -482,7 +483,7 @@ export default class Edition {
482
483
  return bookIndex;
483
484
  }
484
485
  // Given the current chapter, find the available chapter after it.
485
- getNextChapterID(chapterID) {
486
+ getNextChapterID(chapterID, editable) {
486
487
  // Handle back matter chapters.
487
488
  switch (chapterID) {
488
489
  case ChapterIDs.ReferencesID:
@@ -508,7 +509,7 @@ export default class Edition {
508
509
  if (chapter.getID() === chapterID)
509
510
  after = true;
510
511
  // If we're after the given chapter and it's not forthcoming.
511
- else if (after && !chapter.isForthcoming())
512
+ else if (after && (!chapter.isForthcoming() || editable))
512
513
  return chapter.getID();
513
514
  }
514
515
  // If the given ID was the last chapter, go to the next back matter chapter.
@@ -524,7 +525,7 @@ export default class Edition {
524
525
  }
525
526
  }
526
527
  // Given a chapter id, find the available chapter before it.
527
- getPreviousChapterID(chapterID) {
528
+ getPreviousChapterID(chapterID, editable) {
528
529
  switch (chapterID) {
529
530
  // Handle back matter chapters.
530
531
  case ChapterIDs.ReferencesID:
@@ -556,7 +557,7 @@ export default class Edition {
556
557
  if (chapter.getID() === chapterID)
557
558
  before = true;
558
559
  // If we're before the given chapter and it's not forthcoming.
559
- else if (before && !chapter.isForthcoming())
560
+ else if (before && (!chapter.isForthcoming() || editable))
560
561
  return chapter.getID();
561
562
  }
562
563
  // If the given ID was the last chapter, go to the next back matter chapter.