bookish-press 0.5.62 → 0.5.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +30 -1
  2. package/dist/components/app/Options.svelte +1 -0
  3. package/dist/components/app/Table.svelte +0 -1
  4. package/dist/components/chapter/CaptionedCode.svelte +21 -19
  5. package/dist/components/chapter/Figure.svelte +5 -3
  6. package/dist/components/chapter/Figure.svelte.d.ts +1 -0
  7. package/dist/components/chapter/Link.svelte +4 -0
  8. package/dist/components/chapter/Paragraph.svelte +18 -0
  9. package/dist/components/chapter/Table.svelte +1 -1
  10. package/dist/components/editor/BookishEditor.svelte +15 -6
  11. package/dist/components/editor/CaretView.svelte +8 -3
  12. package/dist/components/editor/Commands.js +22 -4
  13. package/dist/components/editor/LinkEditor.svelte +18 -5
  14. package/dist/components/editor/icons/header4.svg +7 -0
  15. package/dist/components/page/Analytics.svelte +19 -0
  16. package/dist/components/page/Analytics.svelte.d.ts +16 -0
  17. package/dist/components/page/BulkReferenceEditor.svelte +1 -1
  18. package/dist/components/page/Chapter.svelte +4 -5
  19. package/dist/components/page/DefinitionView.svelte +4 -3
  20. package/dist/components/page/Edition.svelte +2 -2
  21. package/dist/components/page/EditionLoader.svelte +9 -18
  22. package/dist/components/page/Editions.svelte +4 -3
  23. package/dist/components/page/Glossary.svelte +8 -2
  24. package/dist/components/page/Index.svelte +5 -2
  25. package/dist/components/page/Media.svelte +5 -2
  26. package/dist/components/page/Reference.svelte +24 -15
  27. package/dist/components/page/References.svelte +6 -2
  28. package/dist/components/page/Search.svelte +5 -3
  29. package/dist/components/page/SubdomainEditor.svelte +4 -3
  30. package/dist/components/page/TableOfContents.svelte +42 -11
  31. package/dist/components/page/TableOfContentsRow.svelte +3 -1
  32. package/dist/models/CRUD.d.ts +1 -2
  33. package/dist/models/CRUD.js +4 -6
  34. package/dist/models/book/BookMedia.js +8 -3
  35. package/dist/models/book/Chapter.js +2 -3
  36. package/dist/models/book/Edition.d.ts +10 -4
  37. package/dist/models/book/Edition.js +35 -34
  38. package/dist/models/chapter/ParagraphNode.js +4 -14
  39. package/dist/models/chapter/Parser.d.ts +1 -0
  40. package/dist/models/chapter/Parser.js +2 -1
  41. package/dist/models/chapter/TableNode.js +3 -1
  42. package/dist/util/mineReference.js +8 -6
  43. package/package.json +13 -10
package/README.md CHANGED
@@ -4,4 +4,33 @@ Bookish is a framework and platform for writing and publishing web-based books.
4
4
 
5
5
  ## Status
6
6
 
7
- I'm working on a beta launch of the platform at [bookish.press](https://bookish.press) and expect to maintain the platform for the foreseeable future. Let me know if you'd like to help!
7
+ I'm working on a 1.0 of the platform at [bookish.press](https://bookish.press) and expect to maintain the platform for the foreseeable future. Let me know if you'd like to help!
8
+
9
+ ## Deployment notes
10
+
11
+ There are two major components to Bookish: the authoring platform and the reading front end. Changes to the authoring platform that do not affect reading can be verified and deployed to Firebase without any other coordination. However, changes to the reading experience have downstream dependencies that need to be managed. Here's the general deployment workflow, which I'm currently using as reminders for myself:
12
+
13
+ 0. Ensure you're working in the `dev` branch
14
+ 1. Fix bugs, add features
15
+ 2. Ensure there are no type errors (`npm run check`) or failed tests (`npm run test`).
16
+ 2. Test with emulator (`npm run emu`), if possible. (Some features aren't possible to perfectly emulate, such as authentication, hosting configurations, permissions.)
17
+ 3. Test on the [staging server](https://bookish-dev-21ac3.web.app/`) (`npm run stage`).
18
+ 4. Bump the version in `package.json`
19
+ 5. Describe the changes in `CHANGELOG.md`, linking to any issues closed
20
+ 6. Commit changes to `dev`, referring to any issues addressed
21
+ 7. Release to production (`npm run release`)
22
+ 8. Verify the change in production on [bookish.press](https://bookish.press)
23
+ 9. Merge dev to main (`npm run merge`)
24
+ 10. If any of the changes affected the reading experience (essentially if any `.svelte` file or its dependencies in `/src/lib/components` changed), then we need to update the reader package for authors who pre-build their books manually:
25
+ 1. Update the bookish package with the Sveltekit package tooling (`npm run package`)
26
+ 2. Publish the package to npm (`npm publish`)
27
+ 3. Open the `bookish-reader` repository
28
+ 4. Pull the revisions (`npm update`), ensuring you update the repository's `package.json` dependencies, in case there was a major version change
29
+ 5. Update reader's `CHANGELOG.md` to describe any changes.
30
+ 6. Bump the reader's version in `package.json`
31
+ 7. Commit the changes directly to `main`, and sync them
32
+ 8. For any of these changes to make it to pre-built books, authors need ot use the updated `bookish-reader` repo to rebuild their books. That generally involves:
33
+ 1. Cloning the reader repo: `git clone https://github.com/amyjko/bookish-reader`
34
+ 2. Entering the repo directory: `cd bookish-reader`
35
+ 3. Binding the book: `bind.sh`
36
+ 4. Deploying the bound book in the newly created `build` folder to wherever it's hosted.
@@ -20,5 +20,6 @@ export let label;
20
20
  padding: var(--app-chrome-padding);
21
21
  font-family: var(--app-font);
22
22
  font-size: var(--app-chrome-font-size);
23
+ max-width: 10em;
23
24
  }
24
25
  </style>
@@ -9,7 +9,6 @@
9
9
  width: 100%;
10
10
  overflow-x: auto;
11
11
  margin-top: var(--app-text-spacing);
12
- margin-bottom: var(--app-text-spacing);
13
12
  border-top: var(--app-chrome-border-size) solid var(--app-border-color);
14
13
  border-bottom: var(--app-chrome-border-size) solid
15
14
  var(--app-border-color);
@@ -5,35 +5,22 @@ import Python from './Python.svelte';
5
5
  import Text from './Text.svelte';
6
6
  import { isChapterEditable } from '../page/Contexts';
7
7
  import Figure from './Figure.svelte';
8
- import { getCaret } from '../page/Contexts';
8
+ import Button from '../app/Button.svelte';
9
9
  export let node;
10
10
  $: language = node.getLanguage();
11
11
  let editable = isChapterEditable();
12
12
  // const languages = [ "C", "C++", "CSS", "Go", "HTML", "Java", "JavaScript", "Markdown", "Plaintext", "Python", "TypeScript" ];
13
- let caret = getCaret();
14
- $: inside =
15
- $caret?.range?.start.node === node.getCodeNode() ||
16
- $caret?.range?.end.node === node.getCodeNode();
13
+ let editing = true;
17
14
  </script>
18
15
 
19
- <Figure {node} caption={node.getCaption()}>
20
- {#if editable}
21
- <div class="container">
16
+ <Figure {node} caption={node.getCaption()} focusable={false}>
17
+ {#if editable && editing}
18
+ <div class="container" class:editing>
22
19
  <code
23
- class={`bookish-code bookish-code-block language-${language} ${
24
- !inside ? 'hidden' : ''
25
- }`}
20
+ class={`bookish-code bookish-code-block language-${language}`}
26
21
  >
27
22
  <Text node={node.getCodeNode()} />
28
23
  </code>
29
- <div class="code" class:inside>
30
- <Code
31
- editable={false}
32
- inline={false}
33
- language={node.getLanguage()}
34
- nodeID={node.getCodeNode().nodeID}>{node.getCode()}</Code
35
- >
36
- </div>
37
24
  </div>
38
25
  {:else if node.getLanguage() === 'python' && node.isExecutable()}
39
26
  <Python {node} startCode={node.getCode()} />
@@ -45,6 +32,15 @@ $: inside =
45
32
  nodeID={node.getCodeNode().nodeID}>{node.getCode()}</Code
46
33
  >
47
34
  {/if}
35
+ {#if editable}
36
+ <Button
37
+ tooltip={editing ? 'stop editing' : 'start editing'}
38
+ command={() => {
39
+ editing = !editing;
40
+ }}
41
+ >{#if editing}done{:else}edit{/if}</Button
42
+ >
43
+ {/if}
48
44
  {#if node.getLanguage() !== 'plaintext'}<aside class="language"
49
45
  >{node.getLanguage()}</aside
50
46
  >{/if}
@@ -66,6 +62,12 @@ $: inside =
66
62
  .container {
67
63
  position: relative;
68
64
  }
65
+
66
+ .container.editing {
67
+ border: 1px solid var(--app-interactive-color);
68
+ border-radius: var(--app-chrome-roundedness);
69
+ }
70
+
69
71
  .code {
70
72
  position: absolute;
71
73
  top: 0;
@@ -6,6 +6,7 @@ import { getCaret, isChapterEditable } from '../page/Contexts';
6
6
  export let node;
7
7
  export let caption;
8
8
  export let credit = undefined;
9
+ export let focusable = true;
9
10
  let editable = isChapterEditable();
10
11
  let caret = getCaret();
11
12
  $: selected = $caret?.range?.start.node === node;
@@ -30,11 +31,12 @@ function focusFigure() {
30
31
 
31
32
  <Positioned position={node.getPosition()}>
32
33
  <figure class="bookish-figure" data-nodeid={node.nodeID}>
34
+ <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
33
35
  <div
34
36
  class:selected
35
- role="button"
36
- tabindex="0"
37
- on:mousedown={focusFigure}
37
+ role={focusable ? 'button' : undefined}
38
+ tabindex={focusable ? 0 : undefined}
39
+ on:mousedown={focusable ? focusFigure : null}
38
40
  on:keydown={(event) =>
39
41
  event.key === ' ' || event.key === 'Enter'
40
42
  ? focusFigure()
@@ -8,6 +8,7 @@ declare const __propDef: {
8
8
  node: TableNode | EmbedNode | CodeNode;
9
9
  caption: FormatNode | undefined;
10
10
  credit?: FormatNode | undefined;
11
+ focusable?: boolean | undefined;
11
12
  };
12
13
  events: {
13
14
  [evt: string]: CustomEvent<any>;
@@ -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,6 +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, { Reserved } from '../../models/chapter/Parser';
21
22
  const IDLE_TIME = 500;
22
23
  export let text;
23
24
  export let parser;
@@ -698,14 +699,22 @@ function handleCopy(node) {
698
699
  window.alert("Your browser doesn't support copying to the clipboard :(");
699
700
  }
700
701
  function handlePaste(context, text, save) {
702
+ // If we're pasting a node, see what will accept it.
701
703
  if (text instanceof BookishNode)
702
704
  return handlePasteNode(context, text, save);
703
- const plain = text.plain === undefined ? undefined : new TextNode(text.plain);
704
- if (plain) {
705
- const edit = handlePasteNode(context, plain, save);
706
- if (edit)
707
- return edit;
708
- }
705
+ if (text.plain === undefined)
706
+ return undefined;
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', ' '));
715
+ const edit = handlePasteNode(context, chapter, save);
716
+ if (edit)
717
+ return edit;
709
718
  return undefined;
710
719
  }
711
720
  function handlePasteNode(context, node, save) {
@@ -1,4 +1,4 @@
1
- <script>import { afterUpdate } from 'svelte';
1
+ <script>import { afterUpdate, tick } from 'svelte';
2
2
  export let left;
3
3
  export let top;
4
4
  export let height;
@@ -11,9 +11,14 @@ export let blink;
11
11
  export let locked;
12
12
  let element;
13
13
  let headerHeight = 0;
14
- // When the caret updates, make sure it's in view.
14
+ // When the caret updates position, scroll the element into view after the render is complete.
15
+ $: {
16
+ left;
17
+ top;
18
+ tick().then(() => element ? element.scrollIntoView({ block: 'nearest' }) : undefined);
19
+ }
20
+ /** Keep track of the header height, so we can snap to the caret position. */
15
21
  afterUpdate(() => {
16
- element?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
17
22
  headerHeight =
18
23
  document.querySelector('.bookish-app > .header')
19
24
  ?.offsetHeight ?? 0;
@@ -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',
@@ -1471,7 +1489,7 @@ const commands = [
1471
1489
  const text = await blob.text();
1472
1490
  contents.plain = text;
1473
1491
  }
1474
- context.handlePaste(context, contents, true);
1492
+ return context.handlePaste(context, contents, true);
1475
1493
  }
1476
1494
  catch (err) { }
1477
1495
  }
@@ -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,19 @@
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 async src="https://www.googletagmanager.com/gtag/js?id={id}">
8
+ </script>
9
+ <script>
10
+ window.dataLayer = window.dataLayer || [];
11
+
12
+ function gtag() {
13
+ dataLayer.push(arguments);
14
+ }
15
+
16
+ gtag('js', new Date());
17
+ gtag('config', id);
18
+ </script>
19
+ </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 {};
@@ -39,7 +39,7 @@ function handleEmptyAdd() {
39
39
  command={handleBulkAdd}>+ bulk references</Button
40
40
  >
41
41
  <Button tooltip="add empty reference" command={handleEmptyAdd}
42
- >+ reference</Button
42
+ >+ empty reference</Button
43
43
  >
44
44
 
45
45
  <style>
@@ -213,8 +213,8 @@ let editable = isBookEditable() ||
213
213
  <Outline
214
214
  slot="outline"
215
215
  collapse={$marginal !== undefined}
216
- previous={$edition.getPreviousChapterID(chapterID)}
217
- next={$edition.getNextChapterID(chapterID)}
216
+ previous={$edition.getPreviousChapterID(chapterID, editable)}
217
+ next={$edition.getNextChapterID(chapterID, editable)}
218
218
  listener={(expanded) => {
219
219
  // If the outline is being expanded, hide the marginal, otherwise leave it alone.
220
220
  if (expanded) marginal.set(undefined);
@@ -331,9 +331,8 @@ let editable = isBookEditable() ||
331
331
  {:else}
332
332
  <ChapterBody node={chapterAST} />
333
333
  {/if}
334
- {:else}
335
- <span>Loading...</span>
336
- {/if}
334
+ {:else if $edition === undefined}Loading...{:else}Unable to load
335
+ chapter text{/if}
337
336
  </section>
338
337
 
339
338
  {#if citations && citations.size > 0}
@@ -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()} />
@@ -4,12 +4,13 @@ import Feedback from '../app/Feedback.svelte';
4
4
  import { getBook, getEdition, getUser, getChapterText, } from './Contexts';
5
5
  import { onDestroy } from 'svelte';
6
6
  import { page } from '$app/stores';
7
- import { deleteChapterText, listenToChapters, listenToEdition, } from '../../models/CRUD';
7
+ import { listenToChapters, listenToEdition } from '../../models/CRUD';
8
8
  export let write;
9
9
  let auth = getUser();
10
10
  let book = getBook();
11
11
  let edition = getEdition();
12
- let chapterText = getChapterText();
12
+ /** Mapping from Firestore docuemnt IDs to chapter text */
13
+ let chapterText = new Map();
13
14
  let editionUnsub = undefined;
14
15
  let chaptersUnsub = undefined;
15
16
  let error = undefined;
@@ -54,9 +55,7 @@ function listen() {
54
55
  editionUnsub = listenToEdition($book.getID(), editionID, (ed) => {
55
56
  if (ed) {
56
57
  // Before setting the new edition, augmented it with the chapter text.
57
- edition.set($chapterText
58
- ? ed.withChapterText($chapterText)
59
- : ed);
58
+ edition.set(ed.withChapterText(chapterText));
60
59
  }
61
60
  else {
62
61
  edition.set(undefined);
@@ -64,20 +63,12 @@ function listen() {
64
63
  }
65
64
  });
66
65
  chaptersUnsub = listenToChapters($book.getID(), editionID, (chapters) => {
67
- // Update the chapter text store.
68
- chapterText.set(chapters);
66
+ // Update the text of the chapters we received.
67
+ for (const [ref, text] of chapters)
68
+ chapterText.set(ref.id, text);
69
69
  // Update the chapter text in the current edition.
70
- if ($edition) {
71
- const newEdition = $edition.withChapterText(chapters);
72
- if (newEdition !== $edition)
73
- edition.set(newEdition);
74
- // Get all the chapters in the updated chapter list and
75
- // delete the ones not referenced by the edition.
76
- chapters
77
- .filter(([ref]) => $edition &&
78
- !$edition.hasChapterRefID(ref.id))
79
- .forEach(([ref]) => deleteChapterText(ref));
80
- }
70
+ if ($edition)
71
+ edition.set($edition.withChapterText(chapterText));
81
72
  });
82
73
  }
83
74
  }
@@ -1,7 +1,7 @@
1
1
  <script>import { createNewEdition } from '../../models/CRUD';
2
2
  import Instructions from './Instructions.svelte';
3
3
  import Note from '../editor/Note.svelte';
4
- import { getBook, isBookEditable } from './Contexts';
4
+ import { getBook, getUser, isBookEditable } from './Contexts';
5
5
  import Button from '../app/Button.svelte';
6
6
  import Link from '../app/Link.svelte';
7
7
  import Format from '../chapter/Format.svelte';
@@ -10,6 +10,7 @@ import Permissions from '../editor/Permissions.svelte';
10
10
  export let editors = true;
11
11
  let book = getBook();
12
12
  let editable = isBookEditable();
13
+ let user = getUser();
13
14
  $: editions = $book?.getEditions();
14
15
  $: latestPublishedID = $book?.getLatestPublishedEditionID();
15
16
  async function handleDraftEdition() {
@@ -48,7 +49,7 @@ async function handleDraftEdition() {
48
49
  ? 'rd'
49
50
  : 'th')}
50
51
 
51
- {#if $book && (editable || edition.published)}
52
+ {#if $book && (editable || edition.published || ($user?.user?.uid && edition.editionuids.includes($user.user.uid)))}
52
53
  <section class="edition">
53
54
  <h3
54
55
  >{editionLabel}
@@ -68,7 +69,7 @@ async function handleDraftEdition() {
68
69
  }`}
69
70
  external>read</Link
70
71
  >{/if}
71
- {#if editable}
72
+ {#if editable || ($user?.user?.uid && edition.editionuids.includes($user?.user.uid))}
72
73
  &nbsp;
73
74
  <Link
74
75
  to={`/write/${$book.getID()}/${
@@ -50,8 +50,14 @@ function addEmptyDefinition() {
50
50
  >
51
51
  <Outline
52
52
  slot="outline"
53
- previous={$edition.getPreviousChapterID(ChapterIDs.GlossaryID)}
54
- next={$edition.getNextChapterID(ChapterIDs.GlossaryID)}
53
+ previous={$edition.getPreviousChapterID(
54
+ ChapterIDs.GlossaryID,
55
+ editable
56
+ )}
57
+ next={$edition.getNextChapterID(
58
+ ChapterIDs.GlossaryID,
59
+ editable
60
+ )}
55
61
  />
56
62
  </Header>
57
63
  {#if editable}
@@ -57,8 +57,11 @@ $: {
57
57
  >
58
58
  <Outline
59
59
  slot="outline"
60
- previous={$edition.getPreviousChapterID(ChapterIDs.IndexID)}
61
- next={$edition.getNextChapterID(ChapterIDs.IndexID)}
60
+ previous={$edition.getPreviousChapterID(
61
+ ChapterIDs.IndexID,
62
+ editable
63
+ )}
64
+ next={$edition.getNextChapterID(ChapterIDs.IndexID, editable)}
62
65
  />
63
66
  </Header>
64
67
 
@@ -43,8 +43,11 @@ $: unused = images?.filter((image) => !embeds.some((embed) => image.url.includes
43
43
  >
44
44
  <Outline
45
45
  slot="outline"
46
- previous={$edition.getPreviousChapterID(ChapterIDs.MediaID)}
47
- next={$edition.getNextChapterID(ChapterIDs.MediaID)}
46
+ previous={$edition.getPreviousChapterID(
47
+ ChapterIDs.MediaID,
48
+ editable
49
+ )}
50
+ next={$edition.getNextChapterID(ChapterIDs.MediaID, editable)}
48
51
  />
49
52
  </Header>
50
53
  <Instructions {editable}>