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.
- package/dist/components/BookPreview.svelte +1 -0
- package/dist/components/chapter/Callout.svelte +0 -1
- package/dist/components/chapter/Embed.svelte +8 -6
- package/dist/components/chapter/ExtendedPrism.d.ts +1 -1
- package/dist/components/chapter/Figure.svelte +2 -1
- package/dist/components/chapter/Footnote.svelte +1 -0
- package/dist/components/chapter/List.svelte +16 -33
- package/dist/components/chapter/Marginal.svelte +4 -1
- package/dist/components/chapter/Paragraph.svelte +7 -2
- package/dist/components/chapter/Quote.svelte +6 -6
- package/dist/components/chapter/Table.svelte +4 -0
- package/dist/components/controls/DarkToggle.svelte +1 -1
- package/dist/components/editor/BookishEditor.svelte +21 -26
- package/dist/components/editor/Commands.js +14 -14
- package/dist/components/editor/ConfirmButton.svelte +8 -2
- package/dist/components/editor/EmbedEditor.svelte +3 -3
- package/dist/components/editor/ImageChooser.svelte +8 -27
- package/dist/components/editor/ImageThumbnail.svelte +65 -0
- package/dist/components/editor/ImageThumbnail.svelte.d.ts +19 -0
- package/dist/components/editor/Permissions.svelte +2 -0
- package/dist/components/editor/TextEditor.svelte +5 -2
- package/dist/components/editor/Toggle.svelte +1 -1
- package/dist/components/editor/Toolbar.svelte +2 -1
- package/dist/components/page/Chapter.svelte +1 -1
- package/dist/components/page/DefinitionView.svelte +43 -38
- package/dist/components/page/Edition.svelte +41 -68
- package/dist/components/page/EditionLoader.svelte +12 -3
- package/dist/components/page/Editions.svelte +1 -1
- package/dist/components/page/Header.svelte +73 -69
- package/dist/components/page/Media.svelte +5 -4
- package/dist/components/page/MediaPreview.svelte +8 -2
- package/dist/components/page/MediaPreview.svelte.d.ts +1 -0
- package/dist/components/page/Outline.svelte +1 -1
- package/dist/components/page/Page.svelte +6 -3
- package/dist/components/page/Reference.svelte +1 -1
- package/dist/components/page/Search.svelte +1 -1
- package/dist/components/page/TableOfContents.svelte +3 -6
- package/dist/components/page/TableOfContentsRow.svelte +1 -1
- package/dist/components/page/ThemeEditor.svelte +1 -1
- package/dist/models/CRUD.d.ts +2 -1
- package/dist/models/CRUD.js +18 -5
- package/dist/models/book/BookMedia.d.ts +3 -0
- package/dist/models/book/BookMedia.js +22 -5
- package/dist/models/book/Chapter.d.ts +9 -7
- package/dist/models/book/Chapter.js +16 -6
- package/dist/models/book/Edition.d.ts +6 -1
- package/dist/models/book/Edition.js +22 -10
- package/dist/models/book/Words.js +0 -1
- package/dist/models/chapter/ChapterNode.d.ts +1 -0
- package/dist/models/chapter/ChapterNode.js +3 -0
- package/dist/models/chapter/DefinitionNode.js +1 -1
- package/dist/models/chapter/FormatNode.js +4 -1
- package/dist/models/chapter/InlineCodeNode.js +1 -1
- package/dist/models/chapter/LinkNode.js +1 -1
- package/dist/models/chapter/ListNode.js +1 -1
- package/dist/models/chapter/Parser.js +3 -6
- package/package.json +25 -23
package/dist/models/CRUD.d.ts
CHANGED
|
@@ -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>;
|
package/dist/models/CRUD.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { db, functions } from './Firebase';
|
|
2
|
-
import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc,
|
|
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
|
-
|
|
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'),
|
|
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 (
|
|
197
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ??
|
|
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)
|
|
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
|
-
|
|
166
|
+
const wordCount = this.getWordCount(edition);
|
|
167
|
+
return wordCount === undefined || this.isForthcoming()
|
|
160
168
|
? undefined
|
|
161
|
-
: Math.max(1, Math.round(
|
|
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
|
|
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():
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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().
|
|
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.
|
|
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().
|
|
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().
|
|
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.
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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.
|
|
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
|
|
29
|
-
"@sveltejs/kit": "^1.
|
|
30
|
-
"@sveltejs/package": "^2.0
|
|
31
|
-
"@types/prismjs": "^1.26
|
|
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
|
|
34
|
-
"prettier": "^2.8
|
|
35
|
-
"prettier-plugin-svelte": "^2.
|
|
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.
|
|
38
|
-
"svelte-preprocess": "^5.0
|
|
39
|
-
"tslib": "^2.5
|
|
40
|
-
"typescript": "^
|
|
41
|
-
"vite": "^4.
|
|
42
|
-
"vitest": "^0.
|
|
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
|
|
46
|
-
"ajv-formats": "^2.1
|
|
47
|
+
"ajv": "^8.12",
|
|
48
|
+
"ajv-formats": "^2.1",
|
|
47
49
|
"firebase": "latest",
|
|
48
|
-
"firebase-functions": "
|
|
49
|
-
"prismjs": "^1.29
|
|
50
|
-
"smoothscroll-polyfill": "^0.4
|
|
51
|
-
"svelte": "^
|
|
52
|
-
"@sveltejs/kit": "^1.
|
|
53
|
-
"uuid": "^9.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": {
|