bookish-press 0.4.31 → 0.4.33

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.
@@ -39,21 +39,6 @@ $: blocks = node.getBlocks();
39
39
  min-width: 100%;
40
40
  }
41
41
 
42
- /* This implements a drop cap in the first letter of the first text in the first format of the first paragraph of a chapter (but only chapters). */
43
- :global(
44
- .chapter-content
45
- .bookish-chapter-body
46
- p:first-of-type:not(.placeholder)::first-letter
47
- ) {
48
- padding: 0 0.25rem;
49
- margin: 0 0.25rem 0 0;
50
- font-size: 4rem;
51
- font-weight: 700;
52
- float: left;
53
- line-height: 1;
54
- color: var(--bookish-paragraph-color);
55
- }
56
-
57
42
  :global(.bookish-content-highlight) {
58
43
  position: relative;
59
44
  }
@@ -35,20 +35,22 @@ function isInvalidChapterLink() {
35
35
  <a href={url} target="_blank" rel="noreferrer"><Text node={content} /></a>
36
36
  <!-- If's a chapter link with a label -->
37
37
  {:else if url.indexOf(':') >= 0}
38
- <!-- If the chapter isn't specified, set to the current chapter's id. -->
39
38
  {#if url.split(':')[0] === ''}
39
+ <!-- If the chapter isn't specified, assume this chapter. -->
40
40
  <a href={'#' + url.split(':')[1]}><Text node={content} /></a>
41
41
  {:else}
42
- <a href={'/' + url.split(':')[0] + '#' + url.split(':')[1]}
42
+ <!-- If it is specified, include the base relative chapter ID and the label -->
43
+ <a href={`${$base}${url.split(':')[0]}#${url.split(':')[1]}`}
43
44
  ><Text node={content} /></a
44
45
  >
45
46
  {/if}
46
- <!-- If this is internal link, make a route link to the chapter. -->
47
47
  {:else}
48
- <a href={$base + '/' + url}><Text node={content} /></a>
48
+ <!-- If this is internal link, make a route link to the chapter. -->
49
+ <a href={`${$base}${url}`}><Text node={content} /></a>
49
50
  {/if}
50
51
 
51
52
  <style>
53
+ a,
52
54
  .bookish-editor-link {
53
55
  color: var(--bookish-link-color);
54
56
  font-weight: var(--bookish-link-font-weight);
@@ -19,6 +19,11 @@ export let node;
19
19
  </svelte:element>
20
20
 
21
21
  <style>
22
+ ol,
23
+ ul {
24
+ line-height: var(--bookish-paragraph-line-height-tight);
25
+ }
26
+
22
27
  ol {
23
28
  list-style: none;
24
29
  counter-reset: item;
@@ -56,6 +56,17 @@ $: classes =
56
56
  min-height: var(--bookish-paragraph-line-height);
57
57
  }
58
58
 
59
+ /* This implements a drop cap in the first letter of the first text in the first format of the first paragraph of a chapter (but only chapters). */
60
+ :global(.chapter-content) p:first-of-type:not(.placeholder)::first-letter {
61
+ padding: 0 0.25rem;
62
+ margin: 0 0.25rem 0 0;
63
+ font-size: 4rem;
64
+ font-weight: 700;
65
+ float: left;
66
+ line-height: 1;
67
+ color: var(--bookish-paragraph-color);
68
+ }
69
+
59
70
  .placeholder {
60
71
  color: var(--bookish-muted-color);
61
72
  font-style: italic;
@@ -12,7 +12,7 @@ export let edition;
12
12
  // For example, when the book is hosted alone, all routes might start with the bare root "/",
13
13
  // but when the book is being viewed or edited in the Bookish app, it needs a prefix for the
14
14
  // route in the app.
15
- export let base = '/';
15
+ export let base = '';
16
16
  // When the base changes, update the context.
17
17
  let baseStore = writable(base);
18
18
  $: setContext(BASE, baseStore);
@@ -264,7 +264,7 @@ function move(field, direction) {
264
264
 
265
265
  <style>
266
266
  .reference {
267
- line-height: var(--bookish-paragraph-line-height);
267
+ line-height: var(--bookish-paragraph-line-height-tight);
268
268
  margin-top: 0;
269
269
  margin-bottom: var(--bookish-header-spacing);
270
270
  }
package/index.d.ts CHANGED
@@ -11,3 +11,4 @@ export { default as Feedback } from "./components/app/Feedback.svelte";
11
11
  export { default as Loading } from "./components/page/Loading.svelte";
12
12
  export { default as EditionModel } from "./models/book/Edition";
13
13
  export { EDITION } from "./components/page/Contexts";
14
+ export { default as Schema } from "./models/book/Schema";
package/index.js CHANGED
@@ -11,4 +11,4 @@ export { default as Feedback } from './components/app/Feedback.svelte';
11
11
  export { default as Loading } from './components/page/Loading.svelte';
12
12
  export { default as EditionModel } from './models/book/Edition';
13
13
  export { EDITION } from './components/page/Contexts';
14
- export { Schema } from './models/book/Schema';
14
+ export { default as Schema } from './models/book/Schema';
package/models/CRUD.js CHANGED
@@ -108,7 +108,7 @@ export async function createBook(userID) {
108
108
  // Create a model to store it.
109
109
  let book = Book.fromJSON(bookRef, newBookData);
110
110
  // Make a new default edition with a single chapter with this user id.
111
- let newEdition = new Edition(bookRef, undefined, [], 'Untitled', [], 1, '', null, {}, '', [], 'All rights reserved.', '', [], {}, {}, {}, {}, null, {}).withNewChapter();
111
+ let newEdition = new Edition(bookRef, undefined, [], 'Untitled', [], 1, '', null, {}, '', [], 'All rights reserved.', '', [], {}, {}, {}, {}, null, null, {}).withNewChapter();
112
112
  // Add the edition to the editions collection in the database so we can get an edition ref.
113
113
  const editionRef = await addDoc(collection(db, 'books', bookRef.id, 'editions'), newEdition.toObject());
114
114
  // Remember the ref.
@@ -26,6 +26,7 @@ export type EditionSpecification = {
26
26
  symbols: Record<string, string>;
27
27
  glossary: Record<string, Definition>;
28
28
  theme: Theme | null;
29
+ base: string | null;
29
30
  uids: string[];
30
31
  chapteruids: string[];
31
32
  active: Record<string, string>;
@@ -50,8 +51,9 @@ export default class Edition {
50
51
  readonly symbols: Record<string, string>;
51
52
  readonly glossary: Record<string, Definition>;
52
53
  readonly theme: Theme | null;
54
+ readonly base: string | null;
53
55
  readonly active: Record<string, string>;
54
- 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, 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>);
55
57
  static fromJSON(editionRef: DocumentReference | undefined, spec: EditionSpecification): Edition;
56
58
  static parseReference(citationID: string, ref: string | string[], edition: Edition, short?: boolean): FormatNode | Reference;
57
59
  toObject(): EditionSpecification;
@@ -23,10 +23,11 @@ export default class Edition {
23
23
  symbols;
24
24
  glossary;
25
25
  theme;
26
+ base;
26
27
  active;
27
28
  // Given an object with a valid specification and an object mapping chapter IDs to chapter text,
28
29
  // construct an object representing a book.
29
- constructor(bookRef, editionRef, uids, title, authors, number, summary, published, images, description, chapters, license, acknowledgements, tags, sources, references, symbols, glossary, theme, active) {
30
+ constructor(bookRef, editionRef, uids, title, authors, number, summary, published, images, description, chapters, license, acknowledgements, tags, sources, references, symbols, glossary, theme, base, active) {
30
31
  this.bookRef = bookRef;
31
32
  this.editionRef = editionRef;
32
33
  this.uids = uids;
@@ -52,12 +53,13 @@ export default class Edition {
52
53
  this.symbols = Object.assign({}, symbols);
53
54
  this.glossary = Object.assign({}, glossary);
54
55
  this.theme = theme;
56
+ this.base = base;
55
57
  this.active = active;
56
58
  }
57
59
  static fromJSON(editionRef, spec) {
58
60
  // Copy all of the specification metadata to fields.
59
61
  // Choose suitable defaults if the spec is lacking a field.
60
- 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.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);
61
63
  }
62
64
  static parseReference(citationID, ref, edition, short = false) {
63
65
  if (typeof ref === 'string')
@@ -97,6 +99,7 @@ export default class Edition {
97
99
  symbols: Object.assign({}, this.symbols),
98
100
  glossary: Object.assign({}, this.glossary),
99
101
  theme: this.theme,
102
+ base: this.base,
100
103
  uids: this.uids.slice(),
101
104
  chapteruids: this.getChapterUIDS(),
102
105
  active: this.active,
@@ -113,37 +116,37 @@ export default class Edition {
113
116
  return Array.from(new Set(this.chapters.map((chapter) => chapter.uids).flat()));
114
117
  }
115
118
  withRef(editionRef) {
116
- 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.active);
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);
117
120
  }
118
121
  getTitle() {
119
122
  return this.title;
120
123
  }
121
124
  withTitle(title) {
122
- 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.active);
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);
123
126
  }
124
127
  getDescription() {
125
128
  return this.description;
126
129
  }
127
130
  setDescription(description) {
128
- 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.active);
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);
129
132
  }
130
133
  getAcknowledgements() {
131
134
  return this.acknowledgements;
132
135
  }
133
136
  withAcknowledgements(acks) {
134
- 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.active);
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);
135
138
  }
136
139
  withSummary(summary) {
137
- 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.active);
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);
138
141
  }
139
142
  asPublished(published) {
140
- 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.active);
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);
141
144
  }
142
145
  getLicense() {
143
146
  return this.license;
144
147
  }
145
148
  setLicense(license) {
146
- 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.active);
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);
147
150
  }
148
151
  getChapters() {
149
152
  return this.chapters.slice();
@@ -169,7 +172,7 @@ export default class Edition {
169
172
  return this.chapters.length;
170
173
  }
171
174
  withChapters(chapters) {
172
- 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.active);
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);
173
176
  }
174
177
  withChapterText(text) {
175
178
  return this.withChapters(this.chapters.map((chapter) => {
@@ -196,7 +199,7 @@ export default class Edition {
196
199
  return this.withRef(undefined).withChapters(this.chapters.map((chap) => chap.withoutRef()));
197
200
  }
198
201
  withIncrementedEditionNumber() {
199
- 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.active);
202
+ 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);
200
203
  }
201
204
  withNewChapter() {
202
205
  // Synthesize a chapter ID placeholder that doesn't overlap with existing chapter names
@@ -225,7 +228,7 @@ export default class Edition {
225
228
  const temp = newChapters[index + increment];
226
229
  newChapters[index + increment] = newChapters[index];
227
230
  newChapters[index] = temp;
228
- 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.active);
231
+ 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);
229
232
  }
230
233
  getSymbols() {
231
234
  return this.symbols;
@@ -242,7 +245,7 @@ export default class Edition {
242
245
  : undefined;
243
246
  }
244
247
  withReferences(newReferences) {
245
- 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.active);
248
+ 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);
246
249
  }
247
250
  withNewReferences(references) {
248
251
  const newReferences = Object.assign({}, this.references);
@@ -283,7 +286,7 @@ export default class Edition {
283
286
  return id in this.glossary;
284
287
  }
285
288
  withGlossary(glossary) {
286
- 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.active);
289
+ 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);
287
290
  }
288
291
  withDefinition(id, phrase, definition, synonyms) {
289
292
  const newGlossary = Object.assign({}, this.glossary);
@@ -320,7 +323,7 @@ export default class Edition {
320
323
  return this.theme;
321
324
  }
322
325
  withTheme(theme) {
323
- 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.active);
326
+ 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);
324
327
  }
325
328
  withThemeValue(group, name, value) {
326
329
  if (this.theme === null)
@@ -338,7 +341,7 @@ export default class Edition {
338
341
  return [...this.authors];
339
342
  }
340
343
  withAuthors(authors) {
341
- 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.active);
344
+ 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);
342
345
  }
343
346
  withAuthor(name) {
344
347
  return this.withAuthors([...this.authors, name]);
@@ -361,7 +364,7 @@ export default class Edition {
361
364
  ]);
362
365
  }
363
366
  withEditors(uids) {
364
- 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.active);
367
+ 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);
365
368
  }
366
369
  hasImage(id) {
367
370
  return this.images[id] !== null;
@@ -372,7 +375,7 @@ export default class Edition {
372
375
  withImage(id, embed) {
373
376
  const newImages = Object.assign({}, this.images);
374
377
  newImages[id] = embed;
375
- 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.active);
378
+ 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);
376
379
  }
377
380
  isEditor(uid) {
378
381
  return this.uids.includes(uid);
@@ -395,7 +398,7 @@ export default class Edition {
395
398
  newActive[content] = uid;
396
399
  else
397
400
  delete newActive[content];
398
- 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, newActive);
401
+ 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);
399
402
  }
400
403
  getBookReadingTime() {
401
404
  return this.chapters
@@ -166,6 +166,9 @@ declare const Schema: {
166
166
  theme: {
167
167
  type: string[];
168
168
  };
169
+ base: {
170
+ type: string[];
171
+ };
169
172
  };
170
173
  };
171
174
  export default Schema;
@@ -107,6 +107,9 @@ const Schema = {
107
107
  theme: {
108
108
  type: ['object', 'null'],
109
109
  },
110
+ base: {
111
+ type: ['string'],
112
+ },
110
113
  },
111
114
  };
112
115
  export default Schema;
@@ -382,7 +382,7 @@ export const SketchyTheme = {
382
382
  // Spacing
383
383
  spacing: {
384
384
  paragraphLineHeight: '2.4em',
385
- paragraphLineHeightTight: '1.4em',
385
+ paragraphLineHeightTight: '1.8em',
386
386
  headerLineHeight: '1.3em',
387
387
  paragraphSpacing: '1.8rem',
388
388
  headerSpacing: '2rem',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bookish-press",
3
3
  "private": false,
4
- "version": "0.4.31",
4
+ "version": "0.4.33",
5
5
  "type": "module",
6
6
  "description": "A platform for reading and writing readable, accessible, beautiful online books.",
7
7
  "license": "MIT",