bookish-press 0.7.1 → 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.
@@ -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}
@@ -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"
@@ -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,7 +132,7 @@ 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)
@@ -141,6 +141,7 @@ export async function createNewEdition(book) {
141
141
  const latestDraftData = latestDraftDoc.exists()
142
142
  ? latestDraftDoc.data()
143
143
  : undefined;
144
+ // No latest draft? Return the book as is.
144
145
  if (latestDraftData === undefined)
145
146
  return book;
146
147
  const latestDraft = Edition.fromJSON(latestDraftDoc.ref, latestDraftData);
@@ -149,18 +150,36 @@ export async function createNewEdition(book) {
149
150
  let newEdition = latestDraft.withoutRefs();
150
151
  // Create a copy of the existing draft as a new edition.
151
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.
152
154
  newEdition = newEdition
153
155
  .withRef(newEditionRef)
154
156
  .withIncrementedEditionNumber();
155
- // Create the edition, creating chapter text as necessary, using the edition sync function.
156
- updateEdition(book, undefined, newEdition);
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);
157
176
  // Put the new edition in the book.
158
177
  book = book.withEditions([
159
178
  newEdition.getInfo(),
160
179
  ...book.getEditions(),
161
180
  ]);
162
181
  // Update the book.
163
- updateBook(book);
182
+ await updateBook(book);
164
183
  return book;
165
184
  }
166
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.1",
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",
@@ -59,7 +58,7 @@
59
58
  "uuid": "^11"
60
59
  },
61
60
  "engines": {
62
- "node": "20"
61
+ "node": "22"
63
62
  },
64
63
  "types": "./dist/index.d.ts",
65
64
  "main": "./dist/index.js",