bookish-press 0.5.71 → 0.5.74

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.
@@ -18,6 +18,10 @@ function isInvalidChapterLink() {
18
18
  let parts = chapter.split(':');
19
19
  chapter = parts[0];
20
20
  }
21
+ else if (url.indexOf('#') >= 0) {
22
+ let parts = chapter.split('#');
23
+ chapter = parts[0];
24
+ }
21
25
  if (chapter !== '' && !$edition?.hasChapter(chapter))
22
26
  return true;
23
27
  }
@@ -98,4 +98,22 @@ $: dropcap =
98
98
  font-size: var(--bookish-header-3-font-size);
99
99
  font-style: italic;
100
100
  }
101
+
102
+ h5 {
103
+ font-size: var(--bookish-paragraph-font-size);
104
+ display: inline;
105
+ }
106
+
107
+ h5::after {
108
+ content: '. ';
109
+ }
110
+
111
+ h5 + :global(p) {
112
+ display: inline;
113
+ }
114
+
115
+ h5 + :global(p::after) {
116
+ content: '\0A';
117
+ display: block;
118
+ }
101
119
  </style>
@@ -9,7 +9,7 @@ let editable = isChapterEditable();
9
9
  $: rows = node.getRows();
10
10
  </script>
11
11
 
12
- <Figure {node} caption={node.getCaption()}>
12
+ <Figure {node} caption={node.getCaption()} focusable={false}>
13
13
  <Rows>
14
14
  {#each rows as row, index}
15
15
  <tr class={editable ? 'editable' : null}>
@@ -18,7 +18,7 @@ import commands from './Commands';
18
18
  import { afterUpdate, onMount, setContext } from 'svelte';
19
19
  import { getCaret } from '../page/Contexts';
20
20
  import CaretView from './CaretView.svelte';
21
- import Parser from '../../models/chapter/Parser';
21
+ import Parser, { Reserved } from '../../models/chapter/Parser';
22
22
  const IDLE_TIME = 500;
23
23
  export let text;
24
24
  export let parser;
@@ -704,8 +704,14 @@ function handlePaste(context, text, save) {
704
704
  return handlePasteNode(context, text, save);
705
705
  if (text.plain === undefined)
706
706
  return undefined;
707
- // If we're pasting text, then we parse it as a series of paragraphs.
708
- const chapter = Parser.parseChapter(undefined, text.plain);
707
+ // Escape all reserved Bookdown characters.
708
+ const escaped = text.plain.replaceAll(Reserved, '\\$1');
709
+ const chapter =
710
+ // If we're pasting text into a paragraph node, then we parse it as a series of paragraphs.
711
+ context.block instanceof ParagraphNode
712
+ ? Parser.parseChapter(undefined, escaped)
713
+ : // Otherwise parse as just a format node, replacing all text newlines with a space.
714
+ Parser.parseFormat(undefined, escaped.replaceAll('\n', ' '));
709
715
  const edit = handlePasteNode(context, chapter, save);
710
716
  if (edit)
711
717
  return edit;
@@ -35,6 +35,7 @@ import FootnoteIcon from './icons/footnote.svg?raw';
35
35
  import Header1Icon from './icons/header1.svg?raw';
36
36
  import Header2Icon from './icons/header2.svg?raw';
37
37
  import Header3Icon from './icons/header3.svg?raw';
38
+ import Header4Icon from './icons/header4.svg?raw';
38
39
  import IndentIcon from './icons/indent.svg?raw';
39
40
  import ItalicIcon from './icons/italic.svg?raw';
40
41
  import LabelIcon from './icons/label.svg?raw';
@@ -79,10 +80,10 @@ function deleteTableRowColumn(context, table, format, row) {
79
80
  ? table.withoutRow(location.row)
80
81
  : table.withoutColumn(location.column), (t) => {
81
82
  return (row
82
- ? t.getCell(location.row === table.getRowCount()
83
+ ? t.getCell(location.row === table.getRowCount() - 1
83
84
  ? location.row - 1
84
85
  : location.row, location.column)
85
- : t.getCell(location.row, location.column === table.getColumnCount()
86
+ : t.getCell(location.row, location.column === table.getColumnCount() - 1
86
87
  ? location.column - 1
87
88
  : location.column))?.getFirstCaret();
88
89
  });
@@ -1109,7 +1110,10 @@ const commands = [
1109
1110
  active: (context) => context.chapter &&
1110
1111
  context.atom === undefined &&
1111
1112
  context.meta === undefined,
1112
- handler: (context) => context.root.withSegmentAtSelection(context.range, (text) => new CommentNode(new FormatNode('', [new TextNode(text)]))),
1113
+ handler: (context) => context.root.withSegmentAtSelection({
1114
+ start: context.root.sortRange(context.range).end,
1115
+ end: context.root.sortRange(context.range).end,
1116
+ }, (text) => new CommentNode(new FormatNode('', [new TextNode(text)]))),
1113
1117
  },
1114
1118
  {
1115
1119
  icon: ParagraphIcon,
@@ -1172,6 +1176,20 @@ const commands = [
1172
1176
  context.paragraph.getLevel() !== 3,
1173
1177
  handler: (context) => rootWithNode(context, context.paragraph, context.paragraph?.withLevel(3)),
1174
1178
  },
1179
+ {
1180
+ icon: Header4Icon,
1181
+ description: 'format as 4rd level header',
1182
+ category: 'level',
1183
+ mutates: true,
1184
+ control: true,
1185
+ alt: true,
1186
+ shift: false,
1187
+ code: 'Digit4',
1188
+ visible: (context) => context.paragraph !== undefined,
1189
+ active: (context) => context.paragraph !== undefined &&
1190
+ context.paragraph.getLevel() !== 4,
1191
+ handler: (context) => rootWithNode(context, context.paragraph, context.paragraph?.withLevel(4)),
1192
+ },
1175
1193
  {
1176
1194
  icon: RuleIcon,
1177
1195
  description: 'insert horizontal rule',
@@ -55,12 +55,25 @@ $: {
55
55
  // Add an option for each chapter and it's labels.
56
56
  if ($edition) {
57
57
  for (const chap of $edition?.getChapters() ?? []) {
58
+ // Include the chapter itself
58
59
  options.push([`Chapter: ${chap.getTitle()}`, chap.getID()]);
59
- for (const label of chap.getAST($edition)?.getLabels() ?? [])
60
- options.push([
61
- chap.getTitle() + ': ' + label.getMeta(),
62
- `${chap.getID()}:${label.getMeta()}`,
63
- ]);
60
+ const root = chap.getAST($edition);
61
+ if (root) {
62
+ // Include the chapter's headers
63
+ const headers = root.getHeaders();
64
+ for (let number = 0; number < headers.length; number++) {
65
+ options.push([
66
+ chap.getTitle() + ': ' + headers[number].toText(),
67
+ `${chap.getID()}#header-${number}`,
68
+ ]);
69
+ }
70
+ // Include all of the labels in the chapter
71
+ for (const label of root.getLabels())
72
+ options.push([
73
+ chap.getTitle() + ': ' + label.getMeta(),
74
+ `${chap.getID()}:${label.getMeta()}`,
75
+ ]);
76
+ }
64
77
  }
65
78
  }
66
79
  }
@@ -0,0 +1,7 @@
1
+ <svg role="presentation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14">
2
+ <g transform="scale(0.5 0.5)" transform-origin="7 12">
3
+ <line x1="3" y1="2" x2="3" y2="12" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line>
4
+ <line x1="3" y1="7" x2="11" y2="7" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line>
5
+ <line x1="11" y1="2" x2="11" y2="12" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line>
6
+ </g>
7
+ </svg>
@@ -0,0 +1,21 @@
1
+ <script>/** By default, we use the Bookish.press Google Analytics tag */
2
+ export let gtagid = null;
3
+ $: id = gtagid ?? 'G-BGFDV229BN';
4
+ </script>
5
+
6
+ <svelte:head>
7
+ <script
8
+ async
9
+ src="https://www.googletagmanager.com/gtag/js?id={id}"
10
+ ></script>
11
+ {@html `<script>
12
+ window.dataLayer = window.dataLayer || [];
13
+
14
+ function gtag() {
15
+ dataLayer.push(arguments);
16
+ }
17
+
18
+ gtag('js', new Date());
19
+ gtag('config', "${id}");
20
+ </script>`}
21
+ </svelte:head>
@@ -0,0 +1,16 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ /** By default, we use the Bookish.press Google Analytics tag */ gtagid?: string | null | undefined;
5
+ };
6
+ events: {
7
+ [evt: string]: CustomEvent<any>;
8
+ };
9
+ slots: {};
10
+ };
11
+ export type AnalyticsProps = typeof __propDef.props;
12
+ export type AnalyticsEvents = typeof __propDef.events;
13
+ export type AnalyticsSlots = typeof __propDef.slots;
14
+ export default class Analytics extends SvelteComponent<AnalyticsProps, AnalyticsEvents, AnalyticsSlots> {
15
+ }
16
+ export {};
@@ -62,6 +62,7 @@ $: syns = definition.synonyms || [];
62
62
  {#if editable}
63
63
  <TextEditor
64
64
  text={definition.phrase}
65
+ saveOnExit
65
66
  label={'Glossary phrase editor.'}
66
67
  placeholder="phrase"
67
68
  valid={(text) => {
@@ -74,7 +75,7 @@ $: syns = definition.synonyms || [];
74
75
  phrase: text,
75
76
  definition: definition.definition,
76
77
  synonyms: definition.synonyms,
77
- })
78
+ }),
78
79
  )
79
80
  : undefined}
80
81
  />
@@ -106,7 +107,7 @@ $: syns = definition.synonyms || [];
106
107
  phrase: definition.phrase,
107
108
  definition: node.toBookdown(),
108
109
  synonyms: definition.synonyms,
109
- })
110
+ }),
110
111
  )
111
112
  : undefined}
112
113
  chapter={false}
@@ -122,7 +123,7 @@ $: syns = definition.synonyms || [];
122
123
  <Format
123
124
  node={Parser.parseFormat(
124
125
  undefined,
125
- definition.definition
126
+ definition.definition,
126
127
  ).withTextIfEmpty()}
127
128
  />
128
129
  {/if}
@@ -8,7 +8,7 @@ import { isDark, setDark } from '../../util/dark';
8
8
  import { page } from '$app/stores';
9
9
  // Poly fill smooth scrolling for Safari.
10
10
  onMount(() => smoothscroll.polyfill());
11
- // The book edition to render.
11
+ // The book and edition to render
12
12
  export let edition;
13
13
  // The base path allows links to adjust to different routing contexts in which a book is placed.
14
14
  // For example, when the book is hosted alone, all routes might start with the bare root "/",
@@ -79,8 +79,8 @@ function toRules(set) {
79
79
  }
80
80
  </script>
81
81
 
82
- <!-- Keep the imports up to date -->
83
82
  <svelte:head>
83
+ <!-- Keep the imports up to date -->
84
84
  {@html '<' + `style>${themeCSS}</style>`}
85
85
 
86
86
  <meta property="og:title" content={edition.getTitle()} />
@@ -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';
@@ -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}
@@ -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}>
@@ -113,7 +113,7 @@ export async function createBook(userID) {
113
113
  // Create a model to store it.
114
114
  let book = Book.fromJSON(bookRef.id, newBookData);
115
115
  // Make a new default edition with a single chapter with this user id.
116
- 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();
117
117
  // Add the edition to the editions collection in the database so we can get an edition ref.
118
118
  const editionRef = await addDoc(collection(db, 'books', bookRef.id, 'editions'), newEdition.toObject());
119
119
  // Remember the ref.
@@ -32,6 +32,8 @@ export type EditionSpecification = {
32
32
  /** All uids who have edit access to at least one chapter. Used in firestore rules to give permission to update book. */
33
33
  chapteruids: string[];
34
34
  active: Record<string, string>;
35
+ /** An optional Google Analytics tag ID */
36
+ gtagid: string | null;
35
37
  };
36
38
  export default class Edition {
37
39
  readonly bookRef: DocumentReference;
@@ -55,7 +57,8 @@ export default class Edition {
55
57
  readonly theme: Theme | null;
56
58
  readonly base: string | null;
57
59
  readonly active: Record<string, string>;
58
- 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);
59
62
  static fromJSON(editionRef: DocumentReference | undefined, spec: EditionSpecification): Edition;
60
63
  static parseReference(citationID: string, ref: string | string[], edition: Edition, short?: boolean): FormatNode | Reference;
61
64
  toObject(): EditionSpecification;
@@ -125,6 +128,7 @@ export default class Edition {
125
128
  hasLease(uid: string, content: string): boolean;
126
129
  isLeased(contentID: string): boolean;
127
130
  withLock(uid: string, content: string, lock: boolean): Edition;
131
+ withGTag(gtagid: string | null): Edition;
128
132
  getBookReadingTime(): number | undefined;
129
133
  getChapterNumber(chapterID: string): undefined;
130
134
  getSource(sourceID: string): string | null;
@@ -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,7 +175,7 @@ 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
180
  withChapterText(textByID) {
178
181
  const newChapters = this.chapters.map((chapter) => {
@@ -198,7 +201,7 @@ export default class Edition {
198
201
  return this.withRef(undefined).withChapters(this.chapters.map((chap) => chap.withoutRef()));
199
202
  }
200
203
  withIncrementedEditionNumber() {
201
- 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);
202
205
  }
203
206
  withNewChapter() {
204
207
  // Synthesize a chapter ID placeholder that doesn't overlap with existing chapter names
@@ -230,7 +233,7 @@ export default class Edition {
230
233
  const temp = newChapters[index + increment];
231
234
  newChapters[index + increment] = newChapters[index];
232
235
  newChapters[index] = temp;
233
- 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);
234
237
  }
235
238
  getSymbols() {
236
239
  return this.symbols;
@@ -247,7 +250,7 @@ export default class Edition {
247
250
  : undefined;
248
251
  }
249
252
  withReferences(newReferences) {
250
- 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);
251
254
  }
252
255
  withNewReferences(references) {
253
256
  const newReferences = Object.assign({}, this.references);
@@ -288,7 +291,7 @@ export default class Edition {
288
291
  return id in this.glossary;
289
292
  }
290
293
  withGlossary(glossary) {
291
- 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);
292
295
  }
293
296
  withDefinition(id, phrase, definition, synonyms) {
294
297
  const newGlossary = Object.assign({}, this.glossary);
@@ -325,7 +328,7 @@ export default class Edition {
325
328
  return this.theme;
326
329
  }
327
330
  withTheme(theme) {
328
- 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);
329
332
  }
330
333
  withThemeValue(group, name, value) {
331
334
  if (this.theme === null)
@@ -348,7 +351,7 @@ export default class Edition {
348
351
  .join(', ');
349
352
  }
350
353
  withAuthors(authors) {
351
- 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);
352
355
  }
353
356
  withAuthor(name) {
354
357
  return this.withAuthors([...this.authors, name]);
@@ -371,7 +374,7 @@ export default class Edition {
371
374
  ]);
372
375
  }
373
376
  withEditors(uids) {
374
- 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);
375
378
  }
376
379
  hasImage(id) {
377
380
  return this.images[id] !== null;
@@ -382,7 +385,7 @@ export default class Edition {
382
385
  withImage(id, embed) {
383
386
  const newImages = Object.assign({}, this.images);
384
387
  newImages[id] = embed;
385
- 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);
386
389
  }
387
390
  isEditor(uid) {
388
391
  return this.uids.includes(uid);
@@ -405,7 +408,10 @@ export default class Edition {
405
408
  newActive[content] = uid;
406
409
  else
407
410
  delete newActive[content];
408
- 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);
409
415
  }
410
416
  getBookReadingTime() {
411
417
  return this.chapters
@@ -68,22 +68,12 @@ export default class ParagraphNode extends BlockNode {
68
68
  return this.#format.toText();
69
69
  }
70
70
  toBookdown() {
71
- return ((this.#level === 1
72
- ? '# '
73
- : this.#level === 2
74
- ? '## '
75
- : this.#level === 3
76
- ? '### '
77
- : '') + this.#format.toBookdown());
71
+ return this.#level === 0
72
+ ? this.#format.toBookdown()
73
+ : `${'#'.repeat(this.#level)} ${this.#format.toBookdown()}`;
78
74
  }
79
75
  toHTML() {
80
- const tag = this.#level === 1
81
- ? 'h1'
82
- : this.#level === 2
83
- ? 'h2'
84
- : this.#level === 3
85
- ? 'h2'
86
- : 'p';
76
+ const tag = this.#level === 0 ? 'p' : `h${this.#level}`;
87
77
  return `<${tag}>${this.#format.toHTML()}</${tag}>`;
88
78
  }
89
79
  withLevel(level) {
@@ -22,6 +22,7 @@ import type Position from './Position';
22
22
  import type BlockNode from './BlockNode';
23
23
  import type Bookkeeping from './Bookkeeping';
24
24
  import LineBreakNode from './LineBreakNode';
25
+ export declare const Reserved: RegExp;
25
26
  export default class Parser {
26
27
  book: Edition | undefined;
27
28
  text: string;
@@ -46,6 +46,7 @@ const bulletRE = /^\*+\s+/;
46
46
  // LINK :: [CONTENT|TEXT]
47
47
  // FOOTNOTE :: {TEXT}
48
48
  // TEXT :: (.+)
49
+ export const Reserved = /([_*`@~:^<>{}\[\]])/g;
49
50
  export default class Parser {
50
51
  book;
51
52
  text;
@@ -355,7 +356,7 @@ export default class Parser {
355
356
  // Read any whitespace after the hashes.
356
357
  this.readWhitespace();
357
358
  // Parse some content.
358
- return new ParagraphNode(Math.min(3, count), this.parseFormat());
359
+ return new ParagraphNode(Math.min(4, count), this.parseFormat());
359
360
  }
360
361
  parseRule() {
361
362
  // Read until the end of the line. Ignore all text that follows.
@@ -168,8 +168,10 @@ export default class TableNode extends BlockNode {
168
168
  if (index < 0 || index >= this.getColumnCount())
169
169
  return;
170
170
  const newRows = this.#rows.slice();
171
- for (let r = 0; r < this.#rows.length; r++)
171
+ for (let r = 0; r < this.#rows.length; r++) {
172
+ newRows[r] = newRows[r].slice();
172
173
  newRows[r].splice(index, 1);
174
+ }
173
175
  return new TableNode(newRows, this.#position, this.#caption);
174
176
  }
175
177
  withNewColumn(index) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bookish-press",
3
3
  "private": false,
4
- "version": "0.5.71",
4
+ "version": "0.5.74",
5
5
  "type": "module",
6
6
  "description": "A platform for reading and writing readable, accessible, beautiful online books.",
7
7
  "license": "MIT",
@@ -20,9 +20,9 @@
20
20
  "//env": "echo 'This copies the .env file corresponding to the current Firebase project specified with firebase use, defaulting to local'",
21
21
  "env": "cp .env.${GCLOUD_PROJECT:-example} .env",
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
- "stage": "firebase use dev && firebase deploy",
23
+ "stage": "firebase use dev && cp .env.bookish-dev-21ac3 .env && 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 && cp .env.bookish-prod .env && firebase deploy",
26
26
  "//merge": "merge dev into main, push to remote",
27
27
  "merge": "git checkout main && git merge dev && git push origin main && git checkout dev"
28
28
  },