bookish-press 0.5.2 → 0.5.4

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 (57) hide show
  1. package/dist/components/BookPreview.svelte +1 -0
  2. package/dist/components/chapter/Callout.svelte +0 -1
  3. package/dist/components/chapter/Embed.svelte +8 -6
  4. package/dist/components/chapter/ExtendedPrism.d.ts +1 -1
  5. package/dist/components/chapter/Figure.svelte +2 -1
  6. package/dist/components/chapter/Footnote.svelte +1 -0
  7. package/dist/components/chapter/List.svelte +16 -33
  8. package/dist/components/chapter/Marginal.svelte +4 -1
  9. package/dist/components/chapter/Paragraph.svelte +7 -2
  10. package/dist/components/chapter/Quote.svelte +6 -6
  11. package/dist/components/chapter/Table.svelte +4 -0
  12. package/dist/components/controls/DarkToggle.svelte +1 -1
  13. package/dist/components/editor/BookishEditor.svelte +21 -26
  14. package/dist/components/editor/Commands.js +14 -14
  15. package/dist/components/editor/ConfirmButton.svelte +8 -2
  16. package/dist/components/editor/EmbedEditor.svelte +3 -3
  17. package/dist/components/editor/ImageChooser.svelte +8 -27
  18. package/dist/components/editor/ImageThumbnail.svelte +65 -0
  19. package/dist/components/editor/ImageThumbnail.svelte.d.ts +19 -0
  20. package/dist/components/editor/Permissions.svelte +2 -0
  21. package/dist/components/editor/TextEditor.svelte +5 -2
  22. package/dist/components/editor/Toggle.svelte +1 -1
  23. package/dist/components/editor/Toolbar.svelte +2 -1
  24. package/dist/components/page/Chapter.svelte +1 -1
  25. package/dist/components/page/DefinitionView.svelte +43 -38
  26. package/dist/components/page/Edition.svelte +41 -68
  27. package/dist/components/page/EditionLoader.svelte +12 -3
  28. package/dist/components/page/Editions.svelte +1 -1
  29. package/dist/components/page/Header.svelte +73 -69
  30. package/dist/components/page/Media.svelte +5 -4
  31. package/dist/components/page/MediaPreview.svelte +8 -2
  32. package/dist/components/page/MediaPreview.svelte.d.ts +1 -0
  33. package/dist/components/page/Outline.svelte +1 -1
  34. package/dist/components/page/Page.svelte +6 -3
  35. package/dist/components/page/Reference.svelte +1 -1
  36. package/dist/components/page/Search.svelte +1 -1
  37. package/dist/components/page/TableOfContents.svelte +3 -6
  38. package/dist/components/page/TableOfContentsRow.svelte +1 -1
  39. package/dist/components/page/ThemeEditor.svelte +1 -1
  40. package/dist/models/CRUD.d.ts +2 -1
  41. package/dist/models/CRUD.js +18 -5
  42. package/dist/models/book/BookMedia.d.ts +3 -0
  43. package/dist/models/book/BookMedia.js +22 -5
  44. package/dist/models/book/Chapter.d.ts +9 -7
  45. package/dist/models/book/Chapter.js +16 -6
  46. package/dist/models/book/Edition.d.ts +6 -1
  47. package/dist/models/book/Edition.js +22 -10
  48. package/dist/models/book/Words.js +0 -1
  49. package/dist/models/chapter/ChapterNode.d.ts +1 -0
  50. package/dist/models/chapter/ChapterNode.js +3 -0
  51. package/dist/models/chapter/DefinitionNode.js +1 -1
  52. package/dist/models/chapter/FormatNode.js +4 -1
  53. package/dist/models/chapter/InlineCodeNode.js +1 -1
  54. package/dist/models/chapter/LinkNode.js +1 -1
  55. package/dist/models/chapter/ListNode.js +1 -1
  56. package/dist/models/chapter/Parser.js +3 -6
  57. package/package.json +25 -23
@@ -87,7 +87,7 @@ function getEmptyGroup(group) {
87
87
  {:else}
88
88
  <ConfirmButton
89
89
  tooltip="change theme to the default"
90
- confirm="delete your theme?"
90
+ confirm="delete theme"
91
91
  command={() =>
92
92
  $edition
93
93
  ? edition.set($edition.withTheme(null))
@@ -1,4 +1,4 @@
1
- import { DocumentReference } from 'firebase/firestore';
1
+ import { type DocumentReference } from 'firebase/firestore';
2
2
  import Edition from './book/Edition';
3
3
  import Book from './book/Book';
4
4
  import type { Unsubscribe } from 'firebase/auth';
@@ -20,6 +20,7 @@ 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>;
23
24
  export declare function updateBook(book: Book): Promise<void>;
24
25
  export declare function isSubdomainAvailable(subdomain: string, book: Book): Promise<boolean>;
25
26
  export declare function getBookIDWithSubdomain(subdomain: string): Promise<string | null>;
@@ -1,5 +1,5 @@
1
1
  import { db, functions } from './Firebase';
2
- import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc, DocumentReference, onSnapshot, QueryFieldFilterConstraint, } from 'firebase/firestore';
2
+ import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc, onSnapshot, } from 'firebase/firestore';
3
3
  import { httpsCallable } from 'firebase/functions';
4
4
  import Edition from './book/Edition';
5
5
  import Book, {} from './book/Book';
@@ -60,8 +60,12 @@ export function listenToChapters(bookID, editionID, react) {
60
60
  throw NO_DATABASE_CONNECTION;
61
61
  return onSnapshot(collection(db, 'books', bookID, 'editions', editionID, 'chapters'), (docs) => {
62
62
  const chapters = [];
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.
63
64
  docs.forEach((doc) => {
64
- chapters.push([doc.ref, doc.data().text]);
65
+ if (doc.exists() && !doc.metadata.hasPendingWrites)
66
+ // If the chapter text is somehow null, set it to an empty string.
67
+ // This accounts for previous defects that could lead to null.
68
+ chapters.push([doc.ref, doc.data().text ?? '']);
65
69
  });
66
70
  react(chapters);
67
71
  });
@@ -182,7 +186,9 @@ export async function updateEdition(book, previousEdition, newEdition) {
182
186
  for (const newChapter of newEdition.chapters) {
183
187
  if (newChapter.ref === undefined) {
184
188
  // Make the new chapter's text.
185
- const newChapterRef = await addDoc(collection(db, editionPath(newEdition.bookRef.id, editionRef.id), 'chapters'), { text: newChapter.text });
189
+ const newChapterRef = await addDoc(collection(db, editionPath(newEdition.bookRef.id, editionRef.id), 'chapters'),
190
+ // If the new chapter has null text, set it to empty string
191
+ { text: newChapter.text ?? '' });
186
192
  // Add the ref of the new chapter to the revised chapter.
187
193
  newEdition = newEdition.withRevisedChapter(newChapter, newChapter.withRef(newChapterRef));
188
194
  // Map the chapter ref to it's id.
@@ -193,8 +199,12 @@ export async function updateEdition(book, previousEdition, newEdition) {
193
199
  const previousChapter = previousEdition === undefined
194
200
  ? undefined
195
201
  : previousEdition.getChapterByRef(newChapter.ref);
196
- if (previousChapter === undefined ||
197
- newChapter.text !== previousChapter.text) {
202
+ if (
203
+ // A null chapter text means that we haven't loaded it yet from the database, so we
204
+ // only set it if it's not null.
205
+ newChapter.text !== null &&
206
+ (previousChapter === undefined ||
207
+ newChapter.text !== previousChapter.text)) {
198
208
  await setDoc(doc(db, editionPath(newEdition.bookRef.id, editionRef.id), 'chapters', newChapter.ref.id), { text: newChapter.text });
199
209
  }
200
210
  }
@@ -225,6 +235,9 @@ export async function updateEdition(book, previousEdition, newEdition) {
225
235
  await setDoc(doc(db, editionPath(newEdition.bookRef.id, editionRef.id)), newEdition.toObject());
226
236
  return newChapterRefs;
227
237
  }
238
+ export async function deleteChapterText(ref) {
239
+ deleteDoc(ref);
240
+ }
228
241
  export async function updateBook(book) {
229
242
  if (!db)
230
243
  throw Error("Can't update book, not connected to Firebase.");
@@ -2,6 +2,7 @@ import type { StorageReference } from 'firebase/storage';
2
2
  import type Book from './Book';
3
3
  export type Image = {
4
4
  url: string;
5
+ thumb: string;
5
6
  ref: StorageReference;
6
7
  };
7
8
  export default class BookMedia {
@@ -17,6 +18,8 @@ export default class BookMedia {
17
18
  /** Get a listing of all of the images for the book. */
18
19
  cacheImages(): Promise<Image[]>;
19
20
  retrieveImages(): Promise<(Image | undefined)[]>;
21
+ stripTokenFromURL(url: string): string;
22
+ makeThumbnailURL(url: string): string;
20
23
  getThumbnailURL(url: string, thumbnailRef: StorageReference, error: (message: string) => void, finished: (url: string, thumbnailURL: string) => void, attempt?: number): void;
21
24
  upload(file: File, _: (progress: number) => void, errorHandler: (message: string) => void, finishedHandler: (url: string, thumbnailURL: string) => void): void;
22
25
  remove(image: Image): Promise<Image[] | undefined>;
@@ -1,5 +1,4 @@
1
1
  import { deleteObject, getDownloadURL, listAll, ref, uploadBytes, } from 'firebase/storage';
2
- import { v4 as uuidv4 } from 'uuid';
3
2
  import { storage } from '../Firebase';
4
3
  export default class BookMedia {
5
4
  book;
@@ -43,7 +42,8 @@ export default class BookMedia {
43
42
  // Find all the prefixes and items.
44
43
  return listAll(listRef).then((res) => Promise.all(res.items.map((itemRef) => getDownloadURL(itemRef).then((url) => {
45
44
  const image = {
46
- url: url,
45
+ url,
46
+ thumb: this.makeThumbnailURL(url),
47
47
  description: '',
48
48
  ref: itemRef,
49
49
  };
@@ -53,6 +53,22 @@ export default class BookMedia {
53
53
  return image;
54
54
  }))));
55
55
  }
56
+ stripTokenFromURL(url) {
57
+ const tokenIndex = url.indexOf('&token=');
58
+ return tokenIndex >= 0 ? (url = url.substring(0, tokenIndex)) : url;
59
+ }
60
+ // Construct a thumbnail URL from the base URL.
61
+ makeThumbnailURL(url) {
62
+ // The structure of the URL is
63
+ // https://[firebase-domain]/v0/b/[projectid].appspot.com/o/images%2F[bookID]%2F[imagename]?alt=media
64
+ // And we want
65
+ // https://[firebase-domain]/v0/b/[projectid].appspot.com/o/images%2F[bookID]%2Fthumbnails%2F[imagename]?alt=media
66
+ const bookID = this.book.getID();
67
+ const bookIDIndex = url.indexOf(bookID);
68
+ return bookIDIndex >= 0
69
+ ? `${url.substring(0, bookIDIndex)}${bookID}%2Fthumbnails${url.substring(bookIDIndex + bookID.length)}`
70
+ : url;
71
+ }
56
72
  getThumbnailURL(url, thumbnailRef, error, finished, attempt = 0) {
57
73
  getDownloadURL(thumbnailRef)
58
74
  .then((thumbnail) => finished(url, thumbnail))
@@ -64,9 +80,10 @@ export default class BookMedia {
64
80
  upload(file, _, errorHandler, finishedHandler) {
65
81
  if (storage === undefined)
66
82
  return;
83
+ // We use the file name given, but append a unique string to avoid collisions.
84
+ const imageName = file.name;
67
85
  // The canonical path format for Bookish images in the store is image/{bookid}/{imageid}
68
- // where {bookid} is the Firestore ID of the book being edited and {imageid} is just a random id.
69
- const imageName = `image-${uuidv4()}`;
86
+ // where {bookid} is the Firestore document ID of the book being edited and {imageid} is just a random id.
70
87
  const imageRef = ref(storage, `${this.getImagePath()}/${imageName}`);
71
88
  const thumbnailRef = ref(storage, `${this.getImagePath()}/thumbnails/${imageName}`);
72
89
  uploadBytes(imageRef, file)
@@ -74,7 +91,7 @@ export default class BookMedia {
74
91
  getDownloadURL(snapshot.ref)
75
92
  // After we get the download URL for the uploaded image,
76
93
  // try to get the thumbnail URL for the resized image on the server.
77
- .then((url) => this.getThumbnailURL(url, thumbnailRef, errorHandler, finishedHandler))
94
+ .then((url) => finishedHandler(this.stripTokenFromURL(url), this.makeThumbnailURL(this.stripTokenFromURL(url))))
78
95
  .catch(() => errorHandler('Unable to get image URL.'));
79
96
  })
80
97
  .then(() => this.cacheImages())
@@ -10,7 +10,8 @@ export type ChapterSpecification = {
10
10
  numbered: boolean;
11
11
  forthcoming: boolean;
12
12
  section?: string;
13
- text?: string;
13
+ /** A string means that we know the chapter's text; null means we haven't loaded it yet from the database and should not be editing or saving it. */
14
+ text?: string | null;
14
15
  uids: string[];
15
16
  };
16
17
  export type ChapterContent = {
@@ -30,7 +31,7 @@ export default class Chapter {
30
31
  readonly numbered: boolean;
31
32
  readonly forthcoming: boolean;
32
33
  readonly section: string | undefined;
33
- readonly text: string;
34
+ readonly text: string | null;
34
35
  readonly uids: string[];
35
36
  _ast: ChapterNode | undefined;
36
37
  _index: Record<string, Array<Match>> | undefined;
@@ -41,15 +42,16 @@ export default class Chapter {
41
42
  }[];
42
43
  lastEdit: number;
43
44
  listeners: Set<(text: string) => void>;
44
- constructor(ref: DocumentReference | undefined, id: string, title: string, authors: string[], image: string | null, numbered: boolean, forthcoming: boolean, section: string | undefined, text: string, uids: string[]);
45
+ constructor(ref: DocumentReference | undefined, id: string, title: string, authors: string[], image: string | null, numbered: boolean, forthcoming: boolean, section: string | undefined, text: string | null, uids: string[]);
45
46
  /**
46
47
  * Convert a chapter spec and it's text into a chapter object.
47
48
  * The spec may contain the text itself, but it may not, so we require it to be
48
49
  * repeated here to guarantee its provided.
49
50
  */
50
- static fromJSON(spec: ChapterSpecification, text: string): Chapter;
51
+ static fromJSON(spec: ChapterSpecification): Chapter;
51
52
  toJSON(): ChapterSpecification;
52
53
  getRef(): DocumentReference<import("@firebase/firestore").DocumentData> | undefined;
54
+ hasRefID(id: string): boolean;
53
55
  withEditors(uids: string[]): Chapter;
54
56
  withRef(ref: DocumentReference): Chapter;
55
57
  withoutRef(): Chapter;
@@ -62,7 +64,7 @@ export default class Chapter {
62
64
  isNumbered(): boolean;
63
65
  asNumbered(numbered: boolean): Chapter;
64
66
  isEditor(uid: string): boolean;
65
- getText(): string;
67
+ getText(): string | null;
66
68
  withText(text: string): Chapter;
67
69
  withAST(ast: ChapterNode): Chapter;
68
70
  getAuthors(): string[];
@@ -73,8 +75,8 @@ export default class Chapter {
73
75
  withTitle(title: string): Chapter;
74
76
  getImage(): string | null;
75
77
  withImage(image: string | null): Chapter;
76
- getAST(edition: Edition): ChapterNode;
77
- getWordCount(edition: Edition): number;
78
+ getAST(edition: Edition): ChapterNode | undefined;
79
+ getWordCount(edition: Edition): number | undefined;
78
80
  getReadingTime(edition: Edition): number | undefined;
79
81
  getIndex(edition: Edition): Record<string, Match[]>;
80
82
  toString(): string;
@@ -39,9 +39,9 @@ export default class Chapter {
39
39
  * The spec may contain the text itself, but it may not, so we require it to be
40
40
  * repeated here to guarantee its provided.
41
41
  */
42
- static fromJSON(spec, text) {
42
+ static fromJSON(spec) {
43
43
  // Copy the spec, filling in defaults as necessary.
44
- return new Chapter(spec.ref, spec.id, spec.title, spec.authors ?? [], spec.image ?? null, spec.numbered === true || spec.numbered === undefined, spec.forthcoming === true, spec.section, spec.text ?? text, spec.uids);
44
+ return new Chapter(spec.ref, spec.id, spec.title, spec.authors ?? [], spec.image ?? null, spec.numbered === true || spec.numbered === undefined, spec.forthcoming === true, spec.section, spec.text ?? null, spec.uids);
45
45
  }
46
46
  toJSON() {
47
47
  // Note that we don't include the chapter text in the payload; that's stored in the chapters subcollection.
@@ -64,6 +64,9 @@ export default class Chapter {
64
64
  getRef() {
65
65
  return this.ref;
66
66
  }
67
+ hasRefID(id) {
68
+ return this.ref !== undefined && this.ref.id === id;
69
+ }
67
70
  withEditors(uids) {
68
71
  return new Chapter(this.ref, this.id, this.title, this.authors, this.image, this.numbered, this.forthcoming, this.section, this.text, uids);
69
72
  }
@@ -144,6 +147,8 @@ export default class Chapter {
144
147
  return new Chapter(this.ref, this.id, this.title, this.authors, image, this.numbered, this.forthcoming, this.section, this.text, this.uids);
145
148
  }
146
149
  getAST(edition) {
150
+ if (this.text === null)
151
+ return undefined;
147
152
  // Compute the cache if necessary
148
153
  if (this._ast === undefined)
149
154
  this._ast = Parser.parseChapter(edition, this.text);
@@ -151,18 +156,23 @@ export default class Chapter {
151
156
  }
152
157
  getWordCount(edition) {
153
158
  if (this._wordCount === undefined)
154
- this._wordCount = this.getAST(edition).toText().split(/\s+/).length;
159
+ this._wordCount = this.getAST(edition)
160
+ ?.toText()
161
+ .split(/\s+/).length;
155
162
  return this._wordCount;
156
163
  }
157
164
  // Utility function
158
165
  getReadingTime(edition) {
159
- return this.isForthcoming()
166
+ const wordCount = this.getWordCount(edition);
167
+ return wordCount === undefined || this.isForthcoming()
160
168
  ? undefined
161
- : Math.max(1, Math.round(this.getWordCount(edition) / 150));
169
+ : Math.max(1, Math.round(wordCount / 150));
162
170
  }
163
171
  getIndex(edition) {
164
172
  if (this._index === undefined) {
165
173
  const ast = this.getAST(edition);
174
+ if (ast === undefined)
175
+ return {};
166
176
  // Build a list of common words
167
177
  let commonWords = new Set();
168
178
  CommonWords.forEach((word) => commonWords.add(word));
@@ -207,6 +217,6 @@ export default class Chapter {
207
217
  return this._index;
208
218
  }
209
219
  toString() {
210
- return `${this.title} by ${this.authors.join(', ')}: ${this.text.substring(0, 32)}}`;
220
+ return `${this.title} by ${this.authors.join(', ')}: ${this.text?.substring(0, 32)}}`;
211
221
  }
212
222
  }
@@ -83,6 +83,7 @@ export default class Edition {
83
83
  withoutRefs(): Edition;
84
84
  withIncrementedEditionNumber(): Edition;
85
85
  withNewChapter(): Edition;
86
+ hasChapterRefID(id: string): boolean;
86
87
  withoutChapter(chapterID: string): Edition;
87
88
  withMovedChapter(chapterID: string, increment: number): Edition;
88
89
  getSymbols(): Record<string, string>;
@@ -107,6 +108,7 @@ export default class Edition {
107
108
  withTheme(theme: Theme | null): Edition;
108
109
  withThemeValue(group: string, name: string, value: string): Edition;
109
110
  getAuthors(): string[];
111
+ getAuthorsText(): string;
110
112
  withAuthors(authors: string[]): Edition;
111
113
  withAuthor(name: string): Edition;
112
114
  withAuthorName(index: number, name: string): Edition;
@@ -130,7 +132,10 @@ export default class Edition {
130
132
  getBookIndex(): Record<string, Set<string>>;
131
133
  getNextChapterID(chapterID: string): string | null;
132
134
  getPreviousChapterID(chapterID: string): string | null;
133
- getEmbeds(): EmbedNode[];
135
+ getEmbeds(): {
136
+ embed: EmbedNode;
137
+ chapterID: string | undefined;
138
+ }[];
134
139
  isLatestEdition(book: Book): boolean;
135
140
  isLatestPublishedEdition(book: Book): boolean;
136
141
  getEditionNumber(): number;
@@ -59,7 +59,7 @@ export default class Edition {
59
59
  static fromJSON(editionRef, spec) {
60
60
  // Copy all of the specification metadata to fields.
61
61
  // 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);
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);
63
63
  }
64
64
  static parseReference(citationID, ref, edition, short = false) {
65
65
  if (typeof ref === 'string')
@@ -99,7 +99,7 @@ export default class Edition {
99
99
  symbols: Object.assign({}, this.symbols),
100
100
  glossary: Object.assign({}, this.glossary),
101
101
  theme: this.theme,
102
- base: this.base,
102
+ base: this.base && this.base.length > 0 ? this.base : null,
103
103
  uids: this.uids.slice(),
104
104
  chapteruids: this.getChapterUIDS(),
105
105
  active: this.active,
@@ -175,15 +175,19 @@ export default class Edition {
175
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);
176
176
  }
177
177
  withChapterText(text) {
178
- return this.withChapters(this.chapters.map((chapter) => {
178
+ // No need to make a new edition if no chapter was changed.
179
+ let modified = false;
180
+ const newChapters = this.chapters.map((chapter) => {
179
181
  if (chapter.ref === undefined)
180
182
  return chapter;
181
183
  const match = text.find(([ref, text]) => chapter.ref !== undefined &&
182
184
  chapter.ref.id === ref.id &&
183
- chapter.text !== text &&
184
- text !== undefined);
185
+ chapter.text !== text);
186
+ if (match)
187
+ modified = true;
185
188
  return match ? chapter.withText(match[1]) : chapter;
186
- }));
189
+ });
190
+ return modified ? this.withChapters(newChapters) : this;
187
191
  }
188
192
  withRevisedChapter(previous, edited) {
189
193
  const index = this.chapters.indexOf(previous);
@@ -210,6 +214,9 @@ export default class Edition {
210
214
  const newChapter = new Chapter(undefined, `chapter${number}`, 'Untitled Chapter', [], null, true, false, undefined, '', []);
211
215
  return this.withChapters([...this.chapters, newChapter]);
212
216
  }
217
+ hasChapterRefID(id) {
218
+ return this.chapters.some((chapter) => chapter.hasRefID(id));
219
+ }
213
220
  withoutChapter(chapterID) {
214
221
  let index = this.chapters.findIndex((chapter) => chapter.getID() === chapterID);
215
222
  if (index < 0)
@@ -340,6 +347,11 @@ export default class Edition {
340
347
  getAuthors() {
341
348
  return [...this.authors];
342
349
  }
350
+ getAuthorsText() {
351
+ return this.getAuthors()
352
+ .map((author) => Parser.parseFormat(this, author).toText())
353
+ .join(', ');
354
+ }
343
355
  withAuthors(authors) {
344
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);
345
357
  }
@@ -563,18 +575,18 @@ export default class Edition {
563
575
  if (cover) {
564
576
  let coverNode = Parser.parseEmbed(this, cover);
565
577
  if (coverNode instanceof EmbedNode)
566
- embeds.push(coverNode);
578
+ embeds.push({ embed: coverNode, chapterID: undefined });
567
579
  }
568
580
  // Add the cover and images from each chapter.
569
581
  this.getChapters().forEach((c) => {
570
582
  const image = c.getImage();
571
583
  let cover = image === null ? undefined : Parser.parseEmbed(this, image);
572
584
  if (cover && cover instanceof EmbedNode)
573
- embeds.push(cover);
585
+ embeds.push({ embed: cover, chapterID: c.id });
574
586
  // Get the chapter body's embeds
575
587
  let bodyEmbeds = c?.getAST(this)?.getEmbeds();
576
588
  if (bodyEmbeds)
577
- bodyEmbeds.forEach((embed) => embeds.push(embed));
589
+ bodyEmbeds.forEach((embed) => embeds.push({ embed, chapterID: c.id }));
578
590
  });
579
591
  // Add the back matter covers
580
592
  let backmatter = ['references', 'glossary', 'index', 'search', 'media'];
@@ -583,7 +595,7 @@ export default class Edition {
583
595
  if (img) {
584
596
  let image = Parser.parseEmbed(this, img);
585
597
  if (image instanceof EmbedNode)
586
- embeds.push(image);
598
+ embeds.push({ embed: image, chapterID: id });
587
599
  }
588
600
  });
589
601
  return embeds;
@@ -62,7 +62,6 @@ const CommonWords = [
62
62
  'other',
63
63
  'our',
64
64
  'out',
65
- 'people',
66
65
  'say',
67
66
  'see',
68
67
  'she',
@@ -22,6 +22,7 @@ export default class ChapterNode extends BlocksNode {
22
22
  getEmbeds(): EmbedNode[];
23
23
  getComments(): CommentNode[];
24
24
  getLabels(): LabelNode[];
25
+ getFirstParagraph(): ParagraphNode | undefined;
25
26
  hasLabel(labelID: string): boolean;
26
27
  getCitationNumber(citationID: string): number | null;
27
28
  toText(): string;
@@ -45,6 +45,9 @@ export default class ChapterNode extends BlocksNode {
45
45
  getLabels() {
46
46
  return this.getNodes().filter((n) => n instanceof LabelNode);
47
47
  }
48
+ getFirstParagraph() {
49
+ return this.getBlocks().find((b) => b instanceof ParagraphNode);
50
+ }
48
51
  hasLabel(labelID) {
49
52
  return (this.getLabels().find((label) => label.getMeta() === labelID) !==
50
53
  undefined);
@@ -14,7 +14,7 @@ export default class DefinitionNode extends MetadataNode {
14
14
  return `${this.getText().toHTML()}`;
15
15
  }
16
16
  toBookdown() {
17
- return `~${this.getText().toBookdown()}~${this.getMeta()}`;
17
+ return `~${this.getText().getText()}~${this.getMeta()}`;
18
18
  }
19
19
  copy() {
20
20
  return new DefinitionNode(this.getText(), this.getMeta());
@@ -18,6 +18,9 @@ export default class FormatNode extends Node {
18
18
  const previous = list[position - 1];
19
19
  return !(previous instanceof TextNode && previous.getLength() === 0);
20
20
  });
21
+ // Always ensure an empty text node.
22
+ if (this.#segments.length === 0)
23
+ this.#segments.push(new TextNode());
21
24
  }
22
25
  getType() {
23
26
  return 'formatted';
@@ -149,7 +152,7 @@ export default class FormatNode extends Node {
149
152
  const text = this.getTextNodes();
150
153
  if (text.length === 0)
151
154
  return undefined;
152
- return { node: this.getTextNodes()[0], index: 0 };
155
+ return { node: this.getFirstTextNode(), index: 0 };
153
156
  }
154
157
  getLastCaret() {
155
158
  const text = this.getTextNodes();
@@ -15,7 +15,7 @@ export default class InlineCodeNode extends MetadataNode {
15
15
  }
16
16
  toBookdown() {
17
17
  return ('`' +
18
- this.getText().toBookdown().replace(/`/g, '\\`') +
18
+ this.getText().getText().replace(/`/g, '\\`') +
19
19
  '`' +
20
20
  (this.getMeta() === 'plaintext' ? '' : this.getMeta()));
21
21
  }
@@ -14,7 +14,7 @@ export default class LinkNode extends MetadataNode {
14
14
  return `<a href="${this.getMeta()}">${this.getText().toHTML()}</a>`;
15
15
  }
16
16
  toBookdown() {
17
- return `[${this.getText().toBookdown()}|${this.getMeta()}]`;
17
+ return `[${this.getText().getText()}|${this.getMeta()}]`;
18
18
  }
19
19
  copy() {
20
20
  return new LinkNode(this.getText(), this.getMeta());
@@ -101,7 +101,7 @@ export default class ListNode extends BlockNode {
101
101
  return (format !== undefined &&
102
102
  caret.node.hasAncestor(root, this) &&
103
103
  caret.index === 0 &&
104
- format.getTextNodes()[0] === caret.node);
104
+ format.getFirstTextNode() === caret.node);
105
105
  }
106
106
  getItemContaining(caret) {
107
107
  const match = this.#items.find((n) => n.getParentOf(caret.node) !== undefined);
@@ -739,12 +739,9 @@ export default class Parser {
739
739
  let code = '';
740
740
  while (this.more() && !this.nextIs('`')) {
741
741
  let next = this.read(false);
742
- if (next === '\\') {
743
- if (this.nextIs('`')) {
744
- this.read();
745
- next = '`';
746
- }
747
- }
742
+ // Skip backslash of escaped characters.
743
+ if (next === '\\')
744
+ next = this.read();
748
745
  code = code + next;
749
746
  }
750
747
  // Read the backtick.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bookish-press",
3
3
  "private": false,
4
- "version": "0.5.2",
4
+ "version": "0.5.4",
5
5
  "type": "module",
6
6
  "description": "A platform for reading and writing readable, accessible, beautiful online books.",
7
7
  "license": "MIT",
@@ -22,35 +22,37 @@
22
22
  "//stage": "echo 'switch to the dev project, deploy, relying on Firebase's predeploy hook to run the build script above before release'",
23
23
  "stage": "firebase use dev && firebase deploy",
24
24
  "//release": "echo 'switch to the prod project, deploy, relying on Firebase's predeploy hook to run the build script above before release'",
25
- "release": "firebase use prod && firebase deploy"
25
+ "release": "firebase use prod && firebase deploy",
26
+ "//merge": "merge dev into main, push to remote",
27
+ "merge": "git checkout main && git merge dev && git push origin main && git checkout dev"
26
28
  },
27
29
  "devDependencies": {
28
- "@sveltejs/adapter-static": "^2.0.1",
29
- "@sveltejs/kit": "^1.7.2",
30
- "@sveltejs/package": "^2.0.0",
31
- "@types/prismjs": "^1.26.0",
30
+ "@sveltejs/adapter-static": "^2.0",
31
+ "@sveltejs/kit": "^1.2",
32
+ "@sveltejs/package": "^2.0",
33
+ "@types/prismjs": "^1.26",
32
34
  "@types/smoothscroll-polyfill": "^0.3.1",
33
- "@types/uuid": "^9.0.0",
34
- "prettier": "^2.8.4",
35
- "prettier-plugin-svelte": "^2.9.0",
35
+ "@types/uuid": "^9.0",
36
+ "prettier": "^2.8",
37
+ "prettier-plugin-svelte": "^2.10",
36
38
  "svelte-adapter-firebase": "^0.15.0",
37
- "svelte-check": "^3.0.3",
38
- "svelte-preprocess": "^5.0.1",
39
- "tslib": "^2.5.0",
40
- "typescript": "^4.9.5",
41
- "vite": "^4.1.2",
42
- "vitest": "^0.28.5"
39
+ "svelte-check": "^3.4",
40
+ "svelte-preprocess": "^5.0",
41
+ "tslib": "^2.5",
42
+ "typescript": "^5",
43
+ "vite": "^4.3",
44
+ "vitest": "^0.32"
43
45
  },
44
46
  "dependencies": {
45
- "ajv": "^8.12.0",
46
- "ajv-formats": "^2.1.1",
47
+ "ajv": "^8.12",
48
+ "ajv-formats": "^2.1",
47
49
  "firebase": "latest",
48
- "firebase-functions": "^4.2.1",
49
- "prismjs": "^1.29.0",
50
- "smoothscroll-polyfill": "^0.4.4",
51
- "svelte": "^3.55.1",
52
- "@sveltejs/kit": "^1.7.2",
53
- "uuid": "^9.0.0"
50
+ "firebase-functions": "latest",
51
+ "prismjs": "^1.29",
52
+ "smoothscroll-polyfill": "^0.4",
53
+ "svelte": "^4.0",
54
+ "@sveltejs/kit": "^1.20",
55
+ "uuid": "^9.0"
54
56
  },
55
57
  "types": "./dist/index.d.ts",
56
58
  "exports": {