bookish-press 0.5.62 → 0.5.71
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.
- package/README.md +30 -1
- package/dist/components/app/Options.svelte +1 -0
- package/dist/components/app/Table.svelte +0 -1
- package/dist/components/chapter/CaptionedCode.svelte +21 -19
- package/dist/components/chapter/Figure.svelte +5 -3
- package/dist/components/chapter/Figure.svelte.d.ts +1 -0
- package/dist/components/editor/BookishEditor.svelte +9 -6
- package/dist/components/editor/CaretView.svelte +8 -3
- package/dist/components/editor/Commands.js +1 -1
- package/dist/components/page/BulkReferenceEditor.svelte +1 -1
- package/dist/components/page/Chapter.svelte +4 -5
- package/dist/components/page/EditionLoader.svelte +9 -18
- package/dist/components/page/Editions.svelte +4 -3
- package/dist/components/page/Glossary.svelte +8 -2
- package/dist/components/page/Index.svelte +5 -2
- package/dist/components/page/Media.svelte +5 -2
- package/dist/components/page/Reference.svelte +24 -15
- package/dist/components/page/References.svelte +6 -2
- package/dist/components/page/Search.svelte +5 -3
- package/dist/components/page/SubdomainEditor.svelte +4 -3
- package/dist/components/page/TableOfContents.svelte +9 -3
- package/dist/components/page/TableOfContentsRow.svelte +3 -1
- package/dist/models/CRUD.d.ts +1 -2
- package/dist/models/CRUD.js +3 -5
- package/dist/models/book/BookMedia.js +8 -3
- package/dist/models/book/Chapter.js +2 -3
- package/dist/models/book/Edition.d.ts +5 -3
- package/dist/models/book/Edition.js +10 -15
- package/dist/util/mineReference.js +8 -6
- package/package.json +11 -8
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
|
|
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.
|
|
@@ -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
|
|
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
|
|
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=
|
|
36
|
-
tabindex=
|
|
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()
|
|
@@ -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 from '../../models/chapter/Parser';
|
|
21
22
|
const IDLE_TIME = 500;
|
|
22
23
|
export let text;
|
|
23
24
|
export let parser;
|
|
@@ -698,14 +699,16 @@ 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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
705
|
+
if (text.plain === undefined)
|
|
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);
|
|
709
|
+
const edit = handlePasteNode(context, chapter, save);
|
|
710
|
+
if (edit)
|
|
711
|
+
return edit;
|
|
709
712
|
return undefined;
|
|
710
713
|
}
|
|
711
714
|
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,
|
|
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;
|
|
@@ -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
|
-
|
|
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}
|
|
@@ -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 {
|
|
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
|
-
|
|
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(
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
|
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(
|
|
54
|
-
|
|
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(
|
|
61
|
-
|
|
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(
|
|
47
|
-
|
|
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}>
|
|
@@ -231,7 +231,7 @@ function move(field, direction) {
|
|
|
231
231
|
</tr>
|
|
232
232
|
</Table>
|
|
233
233
|
{:else}
|
|
234
|
-
<!--
|
|
234
|
+
<!-- Otherwise, just render the reference. -->
|
|
235
235
|
<p class="reference">
|
|
236
236
|
{#if reference.authors}{reference.authors}{:else}<em>Authors</em>{/if}
|
|
237
237
|
{#if reference.year}({reference.year}){:else}<em>Year</em>{/if}. {#if reference.url === null || reference.url.length === 0}{#if reference.title}{reference.title}{:else}<em
|
|
@@ -247,26 +247,31 @@ function move(field, direction) {
|
|
|
247
247
|
</p>
|
|
248
248
|
{/if}
|
|
249
249
|
{#if edit && editable && !reference.short}
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
>
|
|
262
|
-
|
|
250
|
+
<div class="controls">
|
|
251
|
+
{#if editable}<Note>{reference.citationID}</Note>{/if}
|
|
252
|
+
<ConfirmButton
|
|
253
|
+
tooltip="delete reference {reference.title}"
|
|
254
|
+
confirm="delete reference"
|
|
255
|
+
command={() =>
|
|
256
|
+
$edition
|
|
257
|
+
? edition.set(
|
|
258
|
+
$edition.withoutReference(reference.citationID)
|
|
259
|
+
)
|
|
260
|
+
: undefined}>⨉</ConfirmButton
|
|
261
|
+
>
|
|
262
|
+
<Button
|
|
263
|
+
tooltip="stop editing reference"
|
|
264
|
+
command={() => (editing ? stopEditing() : startEditing())}
|
|
265
|
+
>{editing ? 'done' : 'edit'}</Button
|
|
266
|
+
>
|
|
267
|
+
</div>
|
|
263
268
|
{/if}
|
|
264
269
|
|
|
265
270
|
<style>
|
|
266
271
|
.reference {
|
|
267
272
|
line-height: var(--bookish-paragraph-line-height-tight);
|
|
268
273
|
margin-top: 0;
|
|
269
|
-
margin-
|
|
274
|
+
margin-top: var(--bookish-block-padding);
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
p {
|
|
@@ -289,4 +294,8 @@ function move(field, direction) {
|
|
|
289
294
|
td {
|
|
290
295
|
line-height: 1;
|
|
291
296
|
}
|
|
297
|
+
|
|
298
|
+
.controls {
|
|
299
|
+
margin-top: calc(var(--bookish-block-padding) / 2);
|
|
300
|
+
}
|
|
292
301
|
</style>
|
|
@@ -36,9 +36,13 @@ if (references !== null) {
|
|
|
36
36
|
<Outline
|
|
37
37
|
slot="outline"
|
|
38
38
|
previous={$edition.getPreviousChapterID(
|
|
39
|
-
ChapterIDs.ReferencesID
|
|
39
|
+
ChapterIDs.ReferencesID,
|
|
40
|
+
editable
|
|
41
|
+
)}
|
|
42
|
+
next={$edition.getNextChapterID(
|
|
43
|
+
ChapterIDs.ReferencesID,
|
|
44
|
+
editable
|
|
40
45
|
)}
|
|
41
|
-
next={$edition.getNextChapterID(ChapterIDs.ReferencesID)}
|
|
42
46
|
/>
|
|
43
47
|
</Header>
|
|
44
48
|
{#if editable}<BulkReferenceEditor />{/if}
|
|
@@ -5,7 +5,6 @@ import Link from '../Link.svelte';
|
|
|
5
5
|
import Chapter from '../../models/book/Chapter';
|
|
6
6
|
import ChapterIDs from '../../models/book/ChapterID';
|
|
7
7
|
import { getBase, getEdition, isEditionEditable } from './Contexts';
|
|
8
|
-
import TextInput from '../app/TextInput.svelte';
|
|
9
8
|
import ChapterTitle from './ChapterTitle.svelte';
|
|
10
9
|
import ChapterNumber from './ChapterNumber.svelte';
|
|
11
10
|
import PageHeader from './PageHeader.svelte';
|
|
@@ -76,8 +75,11 @@ $: {
|
|
|
76
75
|
>
|
|
77
76
|
<Outline
|
|
78
77
|
slot="outline"
|
|
79
|
-
previous={$edition.getPreviousChapterID(
|
|
80
|
-
|
|
78
|
+
previous={$edition.getPreviousChapterID(
|
|
79
|
+
ChapterIDs.SearchID,
|
|
80
|
+
false
|
|
81
|
+
)}
|
|
82
|
+
next={$edition.getNextChapterID(ChapterIDs.SearchID, false)}
|
|
81
83
|
/>
|
|
82
84
|
</Header>
|
|
83
85
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script>import TextEditor from '../editor/TextEditor.svelte';
|
|
2
2
|
import { isSubdomainAvailable } from '../../models/CRUD';
|
|
3
|
-
import
|
|
3
|
+
import { getSubdomain } from '../../util/getSubdomain';
|
|
4
4
|
import { getBook } from './Contexts';
|
|
5
5
|
import Muted from './Muted.svelte';
|
|
6
6
|
let book = getBook();
|
|
@@ -22,9 +22,10 @@ const invalidNames = [
|
|
|
22
22
|
save={// Save the new domain
|
|
23
23
|
async (domain) => {
|
|
24
24
|
if ($book) {
|
|
25
|
-
const available = await isSubdomainAvailable(domain
|
|
25
|
+
const available = await isSubdomainAvailable(domain);
|
|
26
26
|
if (available) book.set($book.withSubdomain(domain));
|
|
27
|
-
else
|
|
27
|
+
else if ($book.getSubdomain() !== domain)
|
|
28
|
+
throw Error("Domain isn't available");
|
|
28
29
|
}
|
|
29
30
|
}}
|
|
30
31
|
placeholder="book name"
|
|
@@ -95,7 +95,7 @@ onMount(() => {
|
|
|
95
95
|
<Outline
|
|
96
96
|
slot="outline"
|
|
97
97
|
previous={null}
|
|
98
|
-
next={$edition.getNextChapterID('')}
|
|
98
|
+
next={$edition.getNextChapterID('', editable)}
|
|
99
99
|
/>
|
|
100
100
|
<div slot="after">
|
|
101
101
|
<Authors
|
|
@@ -126,7 +126,7 @@ onMount(() => {
|
|
|
126
126
|
<Description />
|
|
127
127
|
|
|
128
128
|
<Instructions {editable}>
|
|
129
|
-
This will appear on the <Link to="
|
|
129
|
+
This will appear on the <Link to="/read">book browsing</Link>
|
|
130
130
|
page and in your table of contents. Write an informative description
|
|
131
131
|
of what your book is about.
|
|
132
132
|
</Instructions>
|
|
@@ -192,7 +192,7 @@ onMount(() => {
|
|
|
192
192
|
You can edit this chapter.
|
|
193
193
|
{/if}
|
|
194
194
|
</span>
|
|
195
|
-
<span slot="etc">
|
|
195
|
+
<span class="etc" slot="etc">
|
|
196
196
|
<Muted>
|
|
197
197
|
{#if editable}
|
|
198
198
|
<Toggle
|
|
@@ -310,3 +310,9 @@ onMount(() => {
|
|
|
310
310
|
{/if}
|
|
311
311
|
</Page>
|
|
312
312
|
{/if}
|
|
313
|
+
|
|
314
|
+
<style>
|
|
315
|
+
.etc {
|
|
316
|
+
white-space: nowrap;
|
|
317
|
+
}
|
|
318
|
+
</style>
|
|
@@ -82,7 +82,7 @@ $: chapterURL =
|
|
|
82
82
|
? edition.set(
|
|
83
83
|
$edition.withoutChapter(chapter.getID())
|
|
84
84
|
)
|
|
85
|
-
: undefined}
|
|
85
|
+
: undefined}>⨉</ConfirmButton
|
|
86
86
|
>
|
|
87
87
|
{/if}
|
|
88
88
|
</td>
|
|
@@ -102,6 +102,8 @@ $: chapterURL =
|
|
|
102
102
|
display: flex;
|
|
103
103
|
flex-direction: row;
|
|
104
104
|
flex-wrap: nowrap;
|
|
105
|
+
justify-content: end;
|
|
106
|
+
margin-inline-end: var(--app-chrome-padding);
|
|
105
107
|
gap: var(--app-chrome-padding);
|
|
106
108
|
}
|
|
107
109
|
|
package/dist/models/CRUD.d.ts
CHANGED
|
@@ -20,9 +20,8 @@ export declare function updateLock(edition: Edition, uid: string, content: strin
|
|
|
20
20
|
* returns a map of DocumentReferences for new chapters so they can be persisted client side.
|
|
21
21
|
*/
|
|
22
22
|
export declare function updateEdition(book: Book, previousEdition: Edition | undefined, newEdition: Edition): Promise<Map<string, DocumentReference>>;
|
|
23
|
-
export declare function deleteChapterText(ref: DocumentReference): Promise<void>;
|
|
24
23
|
export declare function updateBook(book: Book): Promise<void>;
|
|
25
|
-
export declare function isSubdomainAvailable(subdomain: string
|
|
24
|
+
export declare function isSubdomainAvailable(subdomain: string): Promise<boolean>;
|
|
26
25
|
export declare function getBookIDWithSubdomain(subdomain: string): Promise<string | null>;
|
|
27
26
|
export declare function publish(edition: Edition): Promise<string | {
|
|
28
27
|
url: string;
|
package/dist/models/CRUD.js
CHANGED
|
@@ -62,7 +62,8 @@ export function listenToChapters(bookID, editionID, react) {
|
|
|
62
62
|
const chapters = [];
|
|
63
63
|
// For each chapter in the book, if the doc exists and it's not a pending local write, send updated chapters to the front end.
|
|
64
64
|
docs.forEach((doc) => {
|
|
65
|
-
|
|
65
|
+
// We propagate pending writes to cycle back local edits to the client.
|
|
66
|
+
if (doc.exists())
|
|
66
67
|
// If the chapter text is somehow null, set it to an empty string.
|
|
67
68
|
// This accounts for previous defects that could lead to null.
|
|
68
69
|
chapters.push([doc.ref, doc.data().text ?? '']);
|
|
@@ -235,15 +236,12 @@ export async function updateEdition(book, previousEdition, newEdition) {
|
|
|
235
236
|
await setDoc(doc(db, editionPath(newEdition.bookRef.id, editionRef.id)), newEdition.toObject());
|
|
236
237
|
return newChapterRefs;
|
|
237
238
|
}
|
|
238
|
-
export async function deleteChapterText(ref) {
|
|
239
|
-
deleteDoc(ref);
|
|
240
|
-
}
|
|
241
239
|
export async function updateBook(book) {
|
|
242
240
|
if (!db)
|
|
243
241
|
throw Error("Can't update book, not connected to Firebase.");
|
|
244
242
|
await setDoc(doc(db, 'books', book.getID()), book.toJSON());
|
|
245
243
|
}
|
|
246
|
-
export async function isSubdomainAvailable(subdomain
|
|
244
|
+
export async function isSubdomainAvailable(subdomain) {
|
|
247
245
|
if (!db)
|
|
248
246
|
throw Error('Not connected to Firebase.');
|
|
249
247
|
return (await getBookIDWithSubdomain(subdomain)) === null;
|
|
@@ -85,17 +85,22 @@ export default class BookMedia {
|
|
|
85
85
|
// The canonical path format for Bookish images in the store is image/{bookid}/{imageid}
|
|
86
86
|
// where {bookid} is the Firestore document ID of the book being edited and {imageid} is just a random id.
|
|
87
87
|
const imageRef = ref(storage, `${this.getImagePath()}/${imageName}`);
|
|
88
|
-
const thumbnailRef = ref(storage, `${this.getImagePath()}/thumbnails/${imageName}`);
|
|
89
88
|
uploadBytes(imageRef, file)
|
|
90
89
|
.then((snapshot) => {
|
|
91
90
|
getDownloadURL(snapshot.ref)
|
|
92
91
|
// After we get the download URL for the uploaded image,
|
|
93
92
|
// try to get the thumbnail URL for the resized image on the server.
|
|
94
93
|
.then((url) => finishedHandler(this.stripTokenFromURL(url), this.makeThumbnailURL(this.stripTokenFromURL(url))))
|
|
95
|
-
.catch(() =>
|
|
94
|
+
.catch((error) => {
|
|
95
|
+
console.error(error);
|
|
96
|
+
errorHandler('Unable to get image URL.');
|
|
97
|
+
});
|
|
96
98
|
})
|
|
97
99
|
.then(() => this.cacheImages())
|
|
98
|
-
.catch(() =>
|
|
100
|
+
.catch((error) => {
|
|
101
|
+
console.error(error);
|
|
102
|
+
errorHandler('Unable to upload image.');
|
|
103
|
+
});
|
|
99
104
|
// This seems to be broken in Firebase right now. Switched above to a no-feedback approach.
|
|
100
105
|
// const uploadTask = uploadBytesResumable(imageRef, file);
|
|
101
106
|
// uploadTask.on("state_changed",
|
|
@@ -156,9 +156,8 @@ export default class Chapter {
|
|
|
156
156
|
}
|
|
157
157
|
getWordCount(edition) {
|
|
158
158
|
if (this._wordCount === undefined)
|
|
159
|
-
this._wordCount = this.getAST(edition)
|
|
160
|
-
|
|
161
|
-
.split(/\s+/).length;
|
|
159
|
+
this._wordCount = this.getAST(edition)?.toText().split(/\s+/)
|
|
160
|
+
.length;
|
|
162
161
|
return this._wordCount;
|
|
163
162
|
}
|
|
164
163
|
// Utility function
|
|
@@ -27,7 +27,9 @@ export type EditionSpecification = {
|
|
|
27
27
|
glossary: Record<string, Definition>;
|
|
28
28
|
theme: Theme | null;
|
|
29
29
|
base: string | null;
|
|
30
|
+
/** People who have edit access to this edition */
|
|
30
31
|
uids: string[];
|
|
32
|
+
/** All uids who have edit access to at least one chapter. Used in firestore rules to give permission to update book. */
|
|
31
33
|
chapteruids: string[];
|
|
32
34
|
active: Record<string, string>;
|
|
33
35
|
};
|
|
@@ -78,7 +80,7 @@ export default class Edition {
|
|
|
78
80
|
getChapterPosition(chapterID: string): number | undefined;
|
|
79
81
|
getChapterCount(): number;
|
|
80
82
|
withChapters(chapters: Chapter[]): Edition;
|
|
81
|
-
withChapterText(
|
|
83
|
+
withChapterText(textByID: Map<string, string>): Edition;
|
|
82
84
|
withRevisedChapter(previous: Chapter, edited: Chapter): Edition;
|
|
83
85
|
withoutRefs(): Edition;
|
|
84
86
|
withIncrementedEditionNumber(): Edition;
|
|
@@ -130,8 +132,8 @@ export default class Edition {
|
|
|
130
132
|
getChapterSection(chapterID: string): string | undefined;
|
|
131
133
|
getFootnoteSymbol(number: number): string;
|
|
132
134
|
getBookIndex(): Record<string, Set<string>>;
|
|
133
|
-
getNextChapterID(chapterID: string): string | null;
|
|
134
|
-
getPreviousChapterID(chapterID: string): string | null;
|
|
135
|
+
getNextChapterID(chapterID: string, editable: boolean): string | null;
|
|
136
|
+
getPreviousChapterID(chapterID: string, editable: boolean): string | null;
|
|
135
137
|
getEmbeds(): {
|
|
136
138
|
embed: EmbedNode;
|
|
137
139
|
chapterID: string | undefined;
|
|
@@ -174,20 +174,15 @@ export default class Edition {
|
|
|
174
174
|
withChapters(chapters) {
|
|
175
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);
|
|
176
176
|
}
|
|
177
|
-
withChapterText(
|
|
178
|
-
// No need to make a new edition if no chapter was changed.
|
|
179
|
-
let modified = false;
|
|
177
|
+
withChapterText(textByID) {
|
|
180
178
|
const newChapters = this.chapters.map((chapter) => {
|
|
181
|
-
if (chapter.ref === undefined)
|
|
179
|
+
if (chapter.ref === undefined) {
|
|
182
180
|
return chapter;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (match)
|
|
187
|
-
modified = true;
|
|
188
|
-
return match ? chapter.withText(match[1]) : chapter;
|
|
181
|
+
}
|
|
182
|
+
const text = textByID.get(chapter.ref.id);
|
|
183
|
+
return text === undefined ? chapter : chapter.withText(text);
|
|
189
184
|
});
|
|
190
|
-
return
|
|
185
|
+
return this.withChapters(newChapters);
|
|
191
186
|
}
|
|
192
187
|
withRevisedChapter(previous, edited) {
|
|
193
188
|
const index = this.chapters.indexOf(previous);
|
|
@@ -482,7 +477,7 @@ export default class Edition {
|
|
|
482
477
|
return bookIndex;
|
|
483
478
|
}
|
|
484
479
|
// Given the current chapter, find the available chapter after it.
|
|
485
|
-
getNextChapterID(chapterID) {
|
|
480
|
+
getNextChapterID(chapterID, editable) {
|
|
486
481
|
// Handle back matter chapters.
|
|
487
482
|
switch (chapterID) {
|
|
488
483
|
case ChapterIDs.ReferencesID:
|
|
@@ -508,7 +503,7 @@ export default class Edition {
|
|
|
508
503
|
if (chapter.getID() === chapterID)
|
|
509
504
|
after = true;
|
|
510
505
|
// If we're after the given chapter and it's not forthcoming.
|
|
511
|
-
else if (after && !chapter.isForthcoming())
|
|
506
|
+
else if (after && (!chapter.isForthcoming() || editable))
|
|
512
507
|
return chapter.getID();
|
|
513
508
|
}
|
|
514
509
|
// If the given ID was the last chapter, go to the next back matter chapter.
|
|
@@ -524,7 +519,7 @@ export default class Edition {
|
|
|
524
519
|
}
|
|
525
520
|
}
|
|
526
521
|
// Given a chapter id, find the available chapter before it.
|
|
527
|
-
getPreviousChapterID(chapterID) {
|
|
522
|
+
getPreviousChapterID(chapterID, editable) {
|
|
528
523
|
switch (chapterID) {
|
|
529
524
|
// Handle back matter chapters.
|
|
530
525
|
case ChapterIDs.ReferencesID:
|
|
@@ -556,7 +551,7 @@ export default class Edition {
|
|
|
556
551
|
if (chapter.getID() === chapterID)
|
|
557
552
|
before = true;
|
|
558
553
|
// If we're before the given chapter and it's not forthcoming.
|
|
559
|
-
else if (before && !chapter.isForthcoming())
|
|
554
|
+
else if (before && (!chapter.isForthcoming() || editable))
|
|
560
555
|
return chapter.getID();
|
|
561
556
|
}
|
|
562
557
|
// If the given ID was the last chapter, go to the next back matter chapter.
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import Reference from '../models/book/Reference';
|
|
2
2
|
export function mineReference(ids, text) {
|
|
3
3
|
// First find the year, since it's the most easily detected. Optional parens.
|
|
4
|
-
let yearMatches = text.match(/(\(?[0-9]{4}
|
|
4
|
+
let yearMatches = text.match(/(\(?[0-9]{4}(,[a-zA-Z ]+)?\)?)/);
|
|
5
5
|
let year = yearMatches !== null && yearMatches.length > 0
|
|
6
6
|
? yearMatches[1].replace('(', '').replace(')', '')
|
|
7
7
|
: undefined;
|
|
8
|
-
if (year && yearMatches)
|
|
8
|
+
if (year && yearMatches) {
|
|
9
|
+
const digits = year.match(/([0-9]{4})/);
|
|
10
|
+
if (digits !== null)
|
|
11
|
+
year = digits[1];
|
|
9
12
|
text = text.replace(yearMatches[1], '');
|
|
13
|
+
}
|
|
10
14
|
// Next, find the URL
|
|
11
15
|
let url = null;
|
|
12
16
|
let urlMatches = text.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
|
|
@@ -38,9 +42,7 @@ export function mineReference(ids, text) {
|
|
|
38
42
|
source = source.trim();
|
|
39
43
|
}
|
|
40
44
|
// If there's anything left, fall back to just a string.
|
|
41
|
-
return
|
|
42
|
-
? new Reference(getUniqueReferenceID(ids), text)
|
|
43
|
-
: new Reference(getUniqueReferenceID(ids, authors, year), authors || '', year || '', title || '', source || '', url || '', '', false);
|
|
45
|
+
return new Reference(getUniqueReferenceID(ids, authors, year), authors || '', year || '', title || '', source || '', url || '', '', false);
|
|
44
46
|
}
|
|
45
47
|
export function splitIntoSentences(text) {
|
|
46
48
|
// Split the remaining text by period-space sequences, assuming chunks of information are segmented by sentences
|
|
@@ -92,7 +94,7 @@ export function getUniqueReferenceID(ids, authors, year) {
|
|
|
92
94
|
return revisedID;
|
|
93
95
|
}
|
|
94
96
|
else {
|
|
95
|
-
let id = '
|
|
97
|
+
let id = '00';
|
|
96
98
|
for (let i = 0; i < 4; i++)
|
|
97
99
|
id = id + Math.round(Math.random() * 9);
|
|
98
100
|
return id;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bookish-press",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.71",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A platform for reading and writing readable, accessible, beautiful online books.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -27,38 +27,41 @@
|
|
|
27
27
|
"merge": "git checkout main && git merge dev && git push origin main && git checkout dev"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@sveltejs/adapter-static": "^2",
|
|
31
|
-
"@sveltejs/
|
|
32
|
-
"@sveltejs/
|
|
30
|
+
"@sveltejs/adapter-static": "^2.0",
|
|
31
|
+
"@sveltejs/adapter-auto": "^2.1.1",
|
|
32
|
+
"@sveltejs/kit": "^1.27",
|
|
33
|
+
"@sveltejs/package": "^2.2",
|
|
33
34
|
"@types/prismjs": "^1.26",
|
|
34
35
|
"@types/smoothscroll-polyfill": "^0",
|
|
35
36
|
"@types/uuid": "^9.0",
|
|
36
37
|
"prettier": "^3",
|
|
37
38
|
"prettier-plugin-svelte": "^3",
|
|
38
39
|
"svelte-adapter-firebase": "^0.15.0",
|
|
39
|
-
"svelte-check": "^3",
|
|
40
|
-
"svelte-preprocess": "^5",
|
|
40
|
+
"svelte-check": "^3.6",
|
|
41
|
+
"svelte-preprocess": "^5.1",
|
|
41
42
|
"tslib": "^2",
|
|
42
43
|
"typescript": "^5",
|
|
43
44
|
"vite": "^4",
|
|
44
45
|
"vitest": "^0"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
48
|
+
"@sveltejs/kit": "^1.27",
|
|
47
49
|
"ajv": "^8",
|
|
48
50
|
"ajv-formats": "^2",
|
|
49
51
|
"firebase": "latest",
|
|
50
52
|
"firebase-functions": "latest",
|
|
51
53
|
"prismjs": "^1.29",
|
|
52
54
|
"smoothscroll-polyfill": "^0.4",
|
|
53
|
-
"svelte": "^4.
|
|
54
|
-
"@sveltejs/kit": "^1.22",
|
|
55
|
+
"svelte": "^4.2",
|
|
55
56
|
"uuid": "^9.0"
|
|
56
57
|
},
|
|
57
58
|
"types": "./dist/index.d.ts",
|
|
59
|
+
"main": "./dist/index.js",
|
|
58
60
|
"exports": {
|
|
59
61
|
"./package.json": "./package.json",
|
|
60
62
|
".": {
|
|
61
63
|
"types": "./dist/index.d.ts",
|
|
64
|
+
"main": "./dist/index.js",
|
|
62
65
|
"svelte": "./dist/index.js",
|
|
63
66
|
"default": "./dist/index.js"
|
|
64
67
|
},
|