bookish-press 0.7.0 → 0.7.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.
@@ -38,7 +38,9 @@ afterUpdate(() => $chapter?.layoutMarginals());
38
38
  ><Marginal
39
39
  {node}
40
40
  id={'citation-' + citations.join('-')}
41
- label="citations"
41
+ label="reference {citations
42
+ .map((c) => $root.getCitationNumber(c))
43
+ .join(', ')}"
42
44
  ><svelte:fragment slot="interactor">
43
45
  {#each citations as citationID, index}{@const citationNumber =
44
46
  $root.getCitationNumber(
@@ -61,6 +63,7 @@ afterUpdate(() => $chapter?.layoutMarginals());
61
63
  {@const ref = $edition?.getReference(citationID)}
62
64
  {#if citationNumber && ref && $edition}<span
63
65
  class="bookish-reference"
66
+ aria-hidden="true"
64
67
  >
65
68
  <sup class="bookish-citation-symbol"
66
69
  >{citationNumber}</sup
@@ -41,6 +41,7 @@ let undoPosition = -1;
41
41
  let clipboard = undefined;
42
42
  let ignoredInput = false;
43
43
  let activeEditor = getCaret();
44
+ let accent = undefined;
44
45
  let editedNode;
45
46
  $: {
46
47
  // Note: when the text changes externally, the caret will be lost. We either have
@@ -462,8 +463,16 @@ function getCaretContext() {
462
463
  clipboard: clipboard,
463
464
  handleCopy: handleCopy,
464
465
  handlePaste: handlePaste,
466
+ setAccent,
467
+ getAccent,
465
468
  };
466
469
  }
470
+ function setAccent(newAccent) {
471
+ accent = newAccent;
472
+ }
473
+ function getAccent() {
474
+ return accent;
475
+ }
467
476
  function getUtilities() {
468
477
  return {
469
478
  getCaretOnLine: getCaretOnLine,
@@ -506,7 +515,7 @@ function handleKey(event, press = false) {
506
515
  (command.active instanceof Function &&
507
516
  command.active.call(undefined, context, event.key)))) {
508
517
  // See if the command is handled
509
- if (executeCommand(command, event.key)) {
518
+ if (executeCommand(command, event.key, event.code)) {
510
519
  event.preventDefault();
511
520
  event.stopPropagation();
512
521
  return true;
@@ -560,14 +569,17 @@ function handleKey(event, press = false) {
560
569
  }
561
570
  }
562
571
  }
563
- function executeCommand(command, key) {
572
+ function executeCommand(command, key, code) {
564
573
  // Assume we process it, then flip it if we don't.
565
574
  ignoredInput = false;
566
575
  const context = getCaretContext();
567
576
  if (context && caretRange) {
568
- const results = command.handler.call(undefined, context, getUtilities(), key);
577
+ const results = command.handler.call(undefined, context, getUtilities(), key, code);
569
578
  // If the command invoked produced a new range
570
- if (results !== undefined) {
579
+ if (results === true) {
580
+ return true;
581
+ }
582
+ else if (results !== undefined) {
571
583
  const { root, range } = results;
572
584
  const newRange = { start: range.start, end: range.end };
573
585
  if (root === editedNode && command.mutates)
@@ -845,6 +857,7 @@ setContext('claimeditor', claimActiveEditor);
845
857
  italic={isItalic}
846
858
  disabled={!editorFocused || locked}
847
859
  {locked}
860
+ {accent}
848
861
  />
849
862
  {/if}
850
863
  </div>
@@ -17,6 +17,8 @@ import type Node from '../../models/chapter/Node';
17
17
  export type PasteContent = {
18
18
  plain: string | undefined;
19
19
  };
20
+ export type Accent = '`' | "'" | '^' | '~' | ':';
21
+ export declare const Accents: Record<string, string>;
20
22
  type CaretContext = {
21
23
  chapter: boolean;
22
24
  range: CaretRange;
@@ -46,5 +48,7 @@ type CaretContext = {
46
48
  clipboard: Clipboard;
47
49
  handleCopy: (node: Node) => void;
48
50
  handlePaste: (context: CaretContext, text: PasteContent | Node, save: boolean) => Edit;
51
+ setAccent: (accent: Accent | undefined) => void;
52
+ getAccent: () => Accent | undefined;
49
53
  };
50
54
  export type { CaretContext as default };
@@ -1 +1,52 @@
1
- export {};
1
+ export const Accents = {
2
+ '`a': 'à',
3
+ '`A': 'À',
4
+ '`e': 'è',
5
+ '`E': 'È',
6
+ '`i': 'ì',
7
+ '`I': 'Ì',
8
+ '`o': 'ò',
9
+ '`O': 'Ò',
10
+ '`u': 'ù',
11
+ '`U': 'Ù',
12
+ "'a": 'á',
13
+ "'A": 'Á',
14
+ "'e": 'é',
15
+ "'E": 'É',
16
+ "'i": 'í',
17
+ "'I": 'Í',
18
+ "'o": 'ó',
19
+ "'O": 'Ó',
20
+ "'u": 'ú',
21
+ "'U": 'Ú',
22
+ '^a': 'â',
23
+ '^A': 'Â',
24
+ '^e': 'ê',
25
+ '^E': 'Ê',
26
+ '^i': 'î',
27
+ '^I': 'Î',
28
+ '^o': 'ô',
29
+ '^O': 'Ô',
30
+ '^u': 'û',
31
+ '^U': 'Û',
32
+ '~a': 'ã',
33
+ '~A': 'Ã',
34
+ '~e': 'ẽ',
35
+ '~E': 'Ẽ',
36
+ '~i': 'ĩ',
37
+ '~I': 'Ĩ',
38
+ '~o': 'õ',
39
+ '~O': 'Õ',
40
+ '~u': 'ũ',
41
+ '~U': 'Ũ',
42
+ ':a': 'ä',
43
+ ':A': 'Ä',
44
+ ':e': 'ë',
45
+ ':E': 'Ë',
46
+ ':i': 'ï',
47
+ ':I': 'Ï',
48
+ ':o': 'ö',
49
+ ':O': 'Ö',
50
+ ':u': 'ü',
51
+ ':U': 'Ü',
52
+ };
@@ -13,7 +13,7 @@ type CaretState = {
13
13
  context: CaretContext | undefined;
14
14
  edit: (previous: Node, edited: Node) => void;
15
15
  setCaret: (range: CaretRange) => void;
16
- executor: (command: Command, key: string) => void;
16
+ executor: (command: Command, key: string, code: string) => void;
17
17
  focused: boolean;
18
18
  };
19
19
  export type { CaretState as default };
@@ -9,6 +9,7 @@ export let disabled;
9
9
  export let ignored;
10
10
  export let blink;
11
11
  export let locked;
12
+ export let accent;
12
13
  let element;
13
14
  let headerHeight = 0;
14
15
  // When the caret updates position, scroll the element into view after the render is complete.
@@ -38,7 +39,9 @@ afterUpdate(() => {
38
39
  style:height="{height}px"
39
40
  style:scroll-margin-top="{headerHeight}px"
40
41
  bind:this={element}
41
- >{#if locked}🔒{/if}</div
42
+ >{#if locked}🔒{/if}{#if accent}
43
+ <span class="accent">{accent}</span>
44
+ {/if}</div
42
45
  >
43
46
 
44
47
  <style>
@@ -84,4 +87,12 @@ afterUpdate(() => {
84
87
  outline-width: 1px;
85
88
  outline-color: var(--bookish-link-color);
86
89
  }
90
+
91
+ .accent {
92
+ display: inline-block;
93
+ position: absolute;
94
+ top: -0.5em;
95
+ color: var(--app-muted-color);
96
+ font-size: 200%;
97
+ }
87
98
  </style>
@@ -1,4 +1,5 @@
1
1
  import { SvelteComponent } from "svelte";
2
+ import type { Accent } from './CaretContext';
2
3
  declare const __propDef: {
3
4
  props: {
4
5
  left: number;
@@ -11,6 +12,7 @@ declare const __propDef: {
11
12
  ignored: boolean;
12
13
  blink: boolean;
13
14
  locked: boolean;
15
+ accent: Accent | undefined;
14
16
  };
15
17
  events: {
16
18
  [evt: string]: CustomEvent<any>;
@@ -15,6 +15,6 @@ type Command = {
15
15
  mutates: boolean;
16
16
  visible: boolean | ((context: CaretContext) => boolean);
17
17
  active: boolean | ((context: CaretContext, key?: string) => boolean);
18
- handler: (context: CaretContext, utilities: CaretUtilities, key: string) => Edit;
18
+ handler: (context: CaretContext, utilities: CaretUtilities, key: string, code: string) => Edit | true;
19
19
  };
20
20
  export type { Command as default };
@@ -54,6 +54,7 @@ import SuperscriptIcon from './icons/super.svg?raw';
54
54
  import TableIcon from './icons/table.svg?raw';
55
55
  import UndoIcon from './icons/undo.svg?raw';
56
56
  import UnindentIcon from './icons/unindent.svg?raw';
57
+ import { Accents } from './CaretContext';
57
58
  import LineBreakNode from '../../models/chapter/LineBreakNode';
58
59
  /**
59
60
  * Icons:
@@ -109,6 +110,25 @@ function rootWithNode(context, original, replacement, caret) {
109
110
  : context.range,
110
111
  };
111
112
  }
113
+ function createAccentCommand(accent, code, control, alt, shift, description) {
114
+ return {
115
+ icon: accent,
116
+ description: description,
117
+ category: 'text',
118
+ mutates: false,
119
+ control,
120
+ alt,
121
+ shift,
122
+ key: undefined,
123
+ code,
124
+ visible: false,
125
+ active: true,
126
+ handler: (context) => {
127
+ context.setAccent(accent);
128
+ return true;
129
+ },
130
+ };
131
+ }
112
132
  // An ordered list of command specifications for keyboard and mouse input.
113
133
  const commands = [
114
134
  {
@@ -695,6 +715,8 @@ const commands = [
695
715
  handler: (context) => {
696
716
  if (context.root === undefined)
697
717
  return;
718
+ // Reset the accent.
719
+ context.setAccent(undefined);
698
720
  if (context.isSelection) {
699
721
  const edit = context.root.withRangeFormatted(context.range, undefined);
700
722
  if (edit === undefined)
@@ -1505,6 +1527,26 @@ const commands = [
1505
1527
  return { root: context.root, range: context.range };
1506
1528
  },
1507
1529
  },
1530
+ // Windows grave
1531
+ createAccentCommand('`', 'Backquote', true, false, false, 'insert a grave accent'),
1532
+ // Mac grave
1533
+ createAccentCommand('`', 'Backquote', false, true, false, 'insert a grave accent'),
1534
+ // Windows apostrophe
1535
+ createAccentCommand("'", 'Quote', true, false, false, 'insert an apostrophe accent'),
1536
+ // Mac apostrophe
1537
+ createAccentCommand('`', 'KeyE', false, true, false, 'insert an apostrophe accent'),
1538
+ // Windows caret
1539
+ createAccentCommand('^', 'Digit6', true, false, true, 'insert a caret accent'),
1540
+ // Mac caret
1541
+ createAccentCommand('^', 'KeyI', false, true, false, 'insert a caret accent'),
1542
+ // Windows tilde
1543
+ createAccentCommand('~', 'Backquote', true, false, true, 'insert a tilde accent'),
1544
+ // Mac tilde
1545
+ createAccentCommand('~', 'KeyN', false, true, false, 'insert a tilde accent'),
1546
+ // Windows umlaut
1547
+ createAccentCommand(':', 'Semicolon', true, false, true, 'insert a tilde accent'),
1548
+ // Mac umlaut
1549
+ createAccentCommand(':', 'KeyU', false, true, false, 'insert a grave accent'),
1508
1550
  {
1509
1551
  icon: 'insert symbol',
1510
1552
  description: 'insert character',
@@ -1521,6 +1563,16 @@ const commands = [
1521
1563
  utilities;
1522
1564
  const range = context.range;
1523
1565
  if (key.length === 1) {
1566
+ // See if there's an accent.
1567
+ const accent = context.getAccent();
1568
+ if (accent !== undefined) {
1569
+ // Reset the accent.
1570
+ context.setAccent(undefined);
1571
+ const match = Accents[accent + key];
1572
+ if (match) {
1573
+ key = match;
1574
+ }
1575
+ }
1524
1576
  // Insert at the start.
1525
1577
  let sortedRange = range.start.node.sortRange(range);
1526
1578
  let insertionPoint = sortedRange.start;
@@ -60,23 +60,25 @@ function hasEditor(email) {
60
60
  </form>
61
61
  {/if}
62
62
 
63
- {#each uids as uid, index (uid)}
64
- {@const email = $emails.get(uid)}
65
- <p>
66
- {#if email}{email}{:else}<em>missing email</em>{/if}
67
- {#if writable}
68
- <Button
69
- tooltip="remove {email} editing rights"
70
- command={() =>
71
- change([
72
- ...uids.slice(0, index),
73
- ...uids.slice(index + 1),
74
- ])}
75
- disabled={atleastone && uids.length <= 1}>⨉</Button
76
- >
77
- {/if}
78
- </p>
79
- {/each}
63
+ {#key $emails}
64
+ {#each uids as uid, index (uid)}
65
+ {@const email = $emails.get(uid)}
66
+ <p>
67
+ {#if email}{email}{:else}<em>loading...</em>{/if}
68
+ {#if writable}
69
+ <Button
70
+ tooltip="remove {email} editing rights"
71
+ command={() =>
72
+ change([
73
+ ...uids.slice(0, index),
74
+ ...uids.slice(index + 1),
75
+ ])}
76
+ disabled={atleastone && uids.length <= 1}>⨉</Button
77
+ >
78
+ {/if}
79
+ </p>
80
+ {/each}
81
+ {/key}
80
82
  {#each inheriteduids as uid}
81
83
  <p><em>{$emails.get(uid)}</em> <Note>book editor</Note></p>
82
84
  {/each}
@@ -186,12 +186,7 @@ function handleKeyPress(event) {
186
186
  ' ' +
187
187
  getShortcutDescription(command)}
188
188
  tabIndex="0"
189
- command={() =>
190
- caret?.executor.call(
191
- undefined,
192
- command,
193
- '',
194
- )}
189
+ command={() => caret?.executor(command, '', '')}
195
190
  >
196
191
  <Icon icon={command.icon} />
197
192
  </Button>
@@ -183,21 +183,22 @@ $: chapterStore.set({
183
183
  marginal,
184
184
  layoutMarginals,
185
185
  });
186
- let editable = isBookEditable() ||
187
- isEditionEditable() ||
188
- isChapterEditable() ||
189
- ($auth !== undefined &&
190
- $auth.user !== null &&
191
- chapter.isEditor($auth.user.uid));
186
+ let editable = !print &&
187
+ (isBookEditable() ||
188
+ isEditionEditable() ||
189
+ isChapterEditable() ||
190
+ ($auth !== undefined &&
191
+ $auth.user !== null &&
192
+ chapter.isEditor($auth.user.uid)));
192
193
  </script>
193
194
 
194
195
  {#if $edition}
195
196
  <Page
196
197
  afterLoaded={scrollToLastLocation}
197
- title={`${$edition.getTitle()} - ${chapter.getTitle()}`}
198
+ title={print ? undefined : `${$edition.getTitle()} - ${chapter.getTitle()}`}
198
199
  >
199
200
  <Header
200
- editable={isChapterEditable()}
201
+ {editable}
201
202
  id={chapter.id}
202
203
  header={chapter.getTitle()}
203
204
  label="Chapter title"
@@ -74,7 +74,7 @@ $: syns = definition.synonyms || [];
74
74
  $edition.withEditedDefinition(id, {
75
75
  phrase: text,
76
76
  definition: definition.definition,
77
- synonyms: definition.synonyms,
77
+ synonyms: definition.synonyms ?? [],
78
78
  }),
79
79
  )
80
80
  : undefined}
@@ -106,7 +106,7 @@ $: syns = definition.synonyms || [];
106
106
  $edition.withEditedDefinition(id, {
107
107
  phrase: definition.phrase,
108
108
  definition: node.toBookdown(),
109
- synonyms: definition.synonyms,
109
+ synonyms: definition.synonyms ?? [],
110
110
  }),
111
111
  )
112
112
  : undefined}
@@ -97,7 +97,7 @@ onDestroy(() => {
97
97
  </script>
98
98
 
99
99
  {#if $book === undefined || error}
100
- <Feedback error>Unable to load edition.</Feedback>
100
+ <Feedback error>Unable to load edition: {error}</Feedback>
101
101
  {:else if $edition === undefined}
102
102
  <Loading />
103
103
  {:else}
@@ -13,20 +13,47 @@ let editable = isBookEditable();
13
13
  let user = getUser();
14
14
  $: editions = $book?.getEditions();
15
15
  $: latestPublishedID = $book?.getLatestPublishedEditionID();
16
- async function handleDraftEdition() {
16
+ let publisher = false;
17
+ $: {
18
+ if ($user && $user.user)
19
+ updateUserClaims();
20
+ }
21
+ async function updateUserClaims() {
22
+ if ($user === undefined || $user.user === null)
23
+ return;
24
+ const token = await $user.user.getIdTokenResult();
25
+ publisher =
26
+ 'publisher' in token.claims && token.claims.publisher === true;
27
+ }
28
+ async function createEdition() {
17
29
  if ($book === undefined)
18
30
  return;
19
- book.set(await createNewEdition($book));
31
+ try {
32
+ const revisedBook = await createNewEdition($book);
33
+ book.set(revisedBook);
34
+ }
35
+ catch (err) {
36
+ console.error('Error creating new edition:', err);
37
+ }
20
38
  }
21
39
  </script>
22
40
 
23
41
  {#if $book && editions}
24
42
  {#if editable}
25
43
  <p>
26
- <Button tooltip="create new edition" command={handleDraftEdition}
27
- >+ edition</Button
44
+ <Button
45
+ tooltip="create new edition"
46
+ command={createEdition}
47
+ disabled={!publisher}>+ edition</Button
28
48
  >
29
49
  </p>
50
+ {#if !publisher || !$book.hasPublishedEdition()}
51
+ <Note
52
+ >You must have publisher privileges to create new editions of a
53
+ published book. If you believe this is an error, please contact
54
+ support.</Note
55
+ >
56
+ {/if}
30
57
  {/if}
31
58
  <Instructions {editable}>
32
59
  Each book has one or more editions, allowing you to track revisions and
@@ -36,18 +63,17 @@ async function handleDraftEdition() {
36
63
  and then publish. By default, readers will see the latest published
37
64
  edition, but they can access older editions here.
38
65
  </Instructions>
39
- {#each editions as edition, index (edition.ref)}
40
- {@const editionNumber =
41
- editions === undefined ? -1 : editions.length - index}
66
+ {#each editions.toReversed() as edition (edition.ref)}
67
+ {@const editionNumber = edition.number}
42
68
  {@const editionLabel =
43
69
  editionNumber +
44
70
  (editionNumber === 1
45
71
  ? 'st'
46
72
  : editionNumber === 2
47
- ? 'nd'
48
- : editionNumber === 3
49
- ? 'rd'
50
- : 'th')}
73
+ ? 'nd'
74
+ : editionNumber === 3
75
+ ? 'rd'
76
+ : 'th')}
51
77
 
52
78
  {#if $book && (editable || edition.published || ($user?.user?.uid && edition.editionuids.includes($user.user.uid)))}
53
79
  <section class="edition">
@@ -55,38 +81,37 @@ async function handleDraftEdition() {
55
81
  >{editionLabel}
56
82
  <Note
57
83
  >{#if edition.published}{new Date(
58
- edition.published
84
+ edition.published,
59
85
  ).toLocaleDateString(
60
- 'en-us'
86
+ 'en-us',
61
87
  )}{:else}unpublished{/if}
62
88
  {#if edition.published}&nbsp;
63
89
  <Link
64
90
  to={latestPublishedID === edition.ref.id &&
65
91
  $book.getSubdomain() !== undefined
66
92
  ? `/${$book.getSubdomain()}`
67
- : `/${$book.getID()}/${
68
- editions.length - index
69
- }`}
93
+ : `/${$book.getID()}/${editionNumber}`}
70
94
  external>read</Link
71
95
  >{/if}
72
96
  {#if editable || ($user?.user?.uid && edition.editionuids.includes($user?.user.uid))}
73
97
  &nbsp;
74
98
  <Link
75
- to={`/write/${$book.getID()}/${
76
- editions.length - index
77
- }`}>edit</Link
99
+ to={`/write/${$book.getID()}/${editionNumber}`}
100
+ >edit</Link
78
101
  >
79
102
  {/if}
80
103
  </Note>
81
104
  </h3>
82
- <p
83
- >{#if edition.summary !== ''}<Format
84
- node={Parser.parseFormat(
85
- undefined,
86
- edition.summary
87
- )}
88
- />{:else}&mdash;{/if}</p
89
- >
105
+ {#if edition.summary !== ''}<p
106
+ ><em
107
+ ><Format
108
+ node={Parser.parseFormat(
109
+ undefined,
110
+ edition.summary,
111
+ )}
112
+ /></em
113
+ ></p
114
+ >{/if}
90
115
  </section>
91
116
  {/if}
92
117
  {/each}
@@ -324,6 +324,7 @@ let expandLabel = '\u2630';
324
324
  /* Desktop */
325
325
  @media screen and (min-width: 1200px) {
326
326
  .outline {
327
+ --outline-width: min(12em, calc((100vw - 740px) / 2 - 3 * var(--outline-padding)));
327
328
  width: var(--outline-width);
328
329
  z-index: 0; /* Put it below everything when it's in the margin. */
329
330
  border-right: 1px solid var(--bookish-border-color-light);
@@ -1,6 +1,6 @@
1
1
  <script>import { onMount } from 'svelte';
2
2
  import Loading from './Loading.svelte';
3
- export let title;
3
+ export let title = undefined;
4
4
  export let afterLoaded = undefined;
5
5
  let fontsLoaded = false;
6
6
  let imagesLoaded = false;
@@ -44,8 +44,10 @@ function imagesAreLoaded() {
44
44
  </script>
45
45
 
46
46
  <svelte:head>
47
- <title>{title}</title>
48
- <meta name="description" content={title} />
47
+ {#if title !== undefined}
48
+ <title>{title}</title>
49
+ <meta name="description" content={title} />
50
+ {/if}
49
51
  </svelte:head>
50
52
 
51
53
  <section class={'bookish-page' + (imagesLoaded ? ' loaded' : '')}>
@@ -1,7 +1,7 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
- title: string;
4
+ title?: string | undefined;
5
5
  afterLoaded?: Function | undefined;
6
6
  };
7
7
  events: {
@@ -1,8 +1,22 @@
1
1
  <script>import Chapter from './Chapter.svelte';
2
2
  import { getEdition } from './Contexts';
3
+ import Parser from '../../models/chapter/Parser';
3
4
  let edition = getEdition();
5
+ $: title = $edition
6
+ ? [
7
+ $edition.getTitle(),
8
+ `${$edition.getEditionLabel()} Edition`,
9
+ Parser.parseFormat($edition, $edition.getAuthors().at(0) ?? '').toText() + ($edition.getAuthors().length > 1 ? ' et al.' : ''),
10
+ ]
11
+ .filter(Boolean)
12
+ .join(' - ')
13
+ : undefined;
4
14
  </script>
5
15
 
16
+ <svelte:head>
17
+ {#if title}<title>{title}</title>{/if}
18
+ </svelte:head>
19
+
6
20
  {#if $edition}
7
21
  {#each $edition
8
22
  .getChapters()
@@ -16,6 +30,9 @@ let edition = getEdition();
16
30
  :global(.bookish-chapter-header) {
17
31
  break-before: page;
18
32
  }
33
+ :global(.bookish-page:first-of-type .bookish-chapter-header) {
34
+ break-before: auto;
35
+ }
19
36
 
20
37
  :global(body) {
21
38
  font-size: 16pt;
@@ -132,31 +132,54 @@ export async function createBook(userID) {
132
132
  }
133
133
  export async function createNewEdition(book) {
134
134
  if (!db)
135
- throw Error("Can't publish draft, no Firebase connection.");
135
+ throw Error("Can't create new edition, no Firebase connection.");
136
136
  // Get the latest draft.
137
137
  const latestEditionID = book.getLatestEditionID();
138
138
  if (latestEditionID === undefined)
139
139
  throw 'No editions on this book';
140
140
  const latestDraftDoc = await getDoc(doc(db, 'books', book.id, 'editions', latestEditionID));
141
- const latestDraft = latestDraftDoc.exists()
141
+ const latestDraftData = latestDraftDoc.exists()
142
142
  ? latestDraftDoc.data()
143
143
  : undefined;
144
- if (latestDraft === undefined)
144
+ // No latest draft? Return the book as is.
145
+ if (latestDraftData === undefined)
145
146
  return book;
147
+ const latestDraft = Edition.fromJSON(latestDraftDoc.ref, latestDraftData);
146
148
  // Make a copy without chapter and edition refs, because we want to create new ones,
147
149
  // and with an incremented edition number.
148
- const newEdition = latestDraft.withoutRefs();
150
+ let newEdition = latestDraft.withoutRefs();
149
151
  // Create a copy of the existing draft as a new edition.
150
- const newEditionRef = await addDoc(collection(db, 'editions'), newEdition.toObject());
151
- // Create the edition, creating chapter text as necessary, using the edition sync function.
152
- updateEdition(book, undefined, newEdition.withRef(newEditionRef));
152
+ const newEditionRef = await addDoc(collection(db, 'books', book.id, 'editions'), newEdition.toObject());
153
+ // Create a new edition that has the new ref and an updated edition number.
154
+ newEdition = newEdition
155
+ .withRef(newEditionRef)
156
+ .withIncrementedEditionNumber();
157
+ // Add the chapter text of the existing edition into the new edition's chapters, so it gets copied over. We have to do this because the chapter text is stored in the chapter documents, not the edition document.
158
+ const textByID = new Map();
159
+ for (const chapter of latestDraft.chapters) {
160
+ if (chapter.ref === undefined)
161
+ continue;
162
+ const chapterDoc = await getDoc(chapter.ref);
163
+ if (!chapterDoc.exists())
164
+ continue;
165
+ const chapterData = chapterDoc.data();
166
+ if (!chapterData || typeof chapterData.text !== 'string')
167
+ continue;
168
+ textByID.set(chapter.id, chapterData.text);
169
+ }
170
+ newEdition = newEdition.withChapters(newEdition.chapters.map((c) => {
171
+ const text = textByID.get(c.id) ?? '';
172
+ return c.withText(text);
173
+ }));
174
+ // Save the edition, creating chapter text as necessary, using the edition sync function.
175
+ await updateEdition(book, undefined, newEdition);
153
176
  // Put the new edition in the book.
154
177
  book = book.withEditions([
155
178
  newEdition.getInfo(),
156
179
  ...book.getEditions(),
157
180
  ]);
158
181
  // Update the book.
159
- updateBook(book);
182
+ await updateBook(book);
160
183
  return book;
161
184
  }
162
185
  export async function updateLock(edition, uid, content, lock) {
@@ -49,7 +49,7 @@ export default class Book {
49
49
  withAuthors(authors: string[]): Book;
50
50
  getID(): string;
51
51
  getEditionNumberID(number: number): string | undefined;
52
- getLatestPublishedEditionID(): string;
52
+ getLatestPublishedEditionID(): string | undefined;
53
53
  getLatestEditionID(): string | undefined;
54
54
  getEditions(): EditionInfo[];
55
55
  getPublishedEditionCount(): number;
@@ -18,7 +18,7 @@ export default class Book {
18
18
  this.cover = cover;
19
19
  // The published state is always true if some edition's published flag isn't null.
20
20
  this.published = editions.some((edition) => edition.published !== null);
21
- // We always keep editions in reversed edition number for easy reference.
21
+ // We always keep editions in ascending edition number for easy reference.
22
22
  this.editions = editions.sort((a, b) => a.number - b.number);
23
23
  this.domain = domain;
24
24
  this.uids = uids;
@@ -91,10 +91,10 @@ export default class Book {
91
91
  return edition ? edition.ref.id : undefined;
92
92
  }
93
93
  getLatestPublishedEditionID() {
94
- return this.editions.filter((ed) => ed.published !== null)[0]?.ref.id;
94
+ return this.editions.filter((ed) => ed.published !== null).at(-1)?.ref.id;
95
95
  }
96
96
  getLatestEditionID() {
97
- return this.editions[0]?.ref.id;
97
+ return this.editions.toSorted((a, b) => b.number - a.number)[0]?.ref.id;
98
98
  }
99
99
  getEditions() {
100
100
  return this.editions.slice();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bookish-press",
3
3
  "private": false,
4
- "version": "0.7.0",
4
+ "version": "0.7.4",
5
5
  "type": "module",
6
6
  "description": "A platform for reading and writing readable, accessible, beautiful online books.",
7
7
  "license": "MIT",
@@ -27,8 +27,7 @@
27
27
  "merge": "git checkout main && git merge dev && git push origin main && git checkout dev"
28
28
  },
29
29
  "devDependencies": {
30
- "@sveltejs/adapter-auto": "^3",
31
- "@sveltejs/adapter-static": "^3",
30
+ "@sveltejs/adapter-node": "^5",
32
31
  "@sveltejs/kit": "^2",
33
32
  "@sveltejs/package": "^2",
34
33
  "@sveltejs/vite-plugin-svelte": "^3",
@@ -51,14 +50,15 @@
51
50
  "ajv": "^8",
52
51
  "ajv-formats": "^3",
53
52
  "firebase": "^11",
54
- "firebase-admin": "^13.2.0",
55
- "firebase-functions": "^6.3.2",
53
+ "firebase-admin": "^12.7.0",
54
+ "firebase-functions": "^6",
56
55
  "prismjs": "^1.29",
57
56
  "smoothscroll-polyfill": "^0.4",
58
- "svelte": "^4"
57
+ "svelte": "^4",
58
+ "uuid": "^11"
59
59
  },
60
60
  "engines": {
61
- "node": "20"
61
+ "node": "22"
62
62
  },
63
63
  "types": "./dist/index.d.ts",
64
64
  "main": "./dist/index.js",