bookish-press 0.5.25 → 0.5.26
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/dist/components/chapter/Callout.svelte +0 -1
- package/dist/components/chapter/Embed.svelte +4 -3
- package/dist/components/chapter/Figure.svelte +0 -1
- package/dist/components/chapter/Footnote.svelte +1 -0
- package/dist/components/chapter/Paragraph.svelte +7 -2
- package/dist/components/chapter/Quote.svelte +6 -6
- package/dist/components/editor/BookishEditor.svelte +9 -16
- package/dist/components/editor/ConfirmButton.svelte +1 -1
- package/dist/components/editor/EmbedEditor.svelte +3 -3
- package/dist/components/editor/ImageChooser.svelte +6 -26
- package/dist/components/editor/ImageThumbnail.svelte +61 -0
- package/dist/components/editor/ImageThumbnail.svelte.d.ts +19 -0
- package/dist/components/editor/TextEditor.svelte +5 -2
- package/dist/components/page/DefinitionView.svelte +43 -38
- package/dist/components/page/Edition.svelte +11 -4
- package/dist/components/page/Header.svelte +73 -69
- package/dist/components/page/Media.svelte +5 -4
- package/dist/components/page/MediaPreview.svelte +8 -2
- package/dist/components/page/MediaPreview.svelte.d.ts +1 -0
- package/dist/components/page/Page.svelte +6 -3
- package/dist/components/page/Reference.svelte +1 -1
- package/dist/components/page/Search.svelte +1 -1
- package/dist/components/page/TableOfContents.svelte +1 -4
- package/dist/components/page/TableOfContentsRow.svelte +1 -1
- package/dist/components/page/ThemeEditor.svelte +1 -1
- package/dist/models/book/BookMedia.d.ts +3 -0
- package/dist/models/book/BookMedia.js +22 -5
- package/dist/models/book/Edition.d.ts +5 -1
- package/dist/models/book/Edition.js +11 -6
- package/dist/models/book/Words.js +0 -1
- package/dist/models/chapter/ChapterNode.d.ts +1 -0
- package/dist/models/chapter/ChapterNode.js +3 -0
- package/package.json +7 -5
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<svelte:options immutable={true} />
|
|
2
2
|
|
|
3
3
|
<script>import { storage } from '../../models/Firebase';
|
|
4
|
-
import { getCaret, getBook, getBase } from '../page/Contexts';
|
|
4
|
+
import { getCaret, getBook, getBase, isEditionEditable, } from '../page/Contexts';
|
|
5
5
|
import Figure from './Figure.svelte';
|
|
6
6
|
import { getContext } from 'svelte';
|
|
7
7
|
export let node;
|
|
8
|
-
export let editable =
|
|
8
|
+
export let editable = true;
|
|
9
9
|
export let placeholder = '';
|
|
10
10
|
export let imageOnly = false;
|
|
11
11
|
$: url = node.getURL();
|
|
@@ -13,6 +13,7 @@ $: description = node.getDescription();
|
|
|
13
13
|
let caret = getCaret();
|
|
14
14
|
let book = getBook();
|
|
15
15
|
let base = getBase();
|
|
16
|
+
let editionEditable = isEditionEditable();
|
|
16
17
|
// Bookish editor should have passed down a way to set the active editor.
|
|
17
18
|
let claimCaret = getContext('claimeditor');
|
|
18
19
|
let dragging = false;
|
|
@@ -94,7 +95,7 @@ function handleClick() {
|
|
|
94
95
|
credit={imageOnly ? undefined : node.getCredit()}
|
|
95
96
|
>
|
|
96
97
|
{#if url.trim().length === 0}
|
|
97
|
-
{#if editable}
|
|
98
|
+
{#if editable && editionEditable}
|
|
98
99
|
<p
|
|
99
100
|
class={`bookish-figure-unspecified ${
|
|
100
101
|
dragging ? 'bookish-figure-dragging' : ''
|
|
@@ -19,10 +19,15 @@ $: classes =
|
|
|
19
19
|
($chapter && $chapter.highlightedID === id
|
|
20
20
|
? ' bookish-content-highlight'
|
|
21
21
|
: '');
|
|
22
|
+
$: firstParagraph = $root.getFirstParagraph();
|
|
23
|
+
$: dropcap =
|
|
24
|
+
placeholder === undefined &&
|
|
25
|
+
node === firstParagraph &&
|
|
26
|
+
/^[a-zA-Z]/.test(firstParagraph.getFirstTextNode().getText());
|
|
22
27
|
</script>
|
|
23
28
|
|
|
24
29
|
{#if level === 0}
|
|
25
|
-
<p data-nodeid={node.nodeID} class:placeholder
|
|
30
|
+
<p data-nodeid={node.nodeID} class:placeholder class:dropcap
|
|
26
31
|
><Format node={node.getFormat()} placeholder={placeholder ?? '¶'} /></p
|
|
27
32
|
>
|
|
28
33
|
{:else}
|
|
@@ -54,7 +59,7 @@ $: classes =
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/* This implements a drop cap in the first letter of the first text in the first format of the first paragraph of a chapter (but only chapters). */
|
|
57
|
-
|
|
62
|
+
p.dropcap::first-letter {
|
|
58
63
|
padding: 0 0.25rem;
|
|
59
64
|
margin: 0 0.25rem 0 0;
|
|
60
65
|
font-size: 4rem;
|
|
@@ -37,12 +37,11 @@ $: credit = node.getCredit();
|
|
|
37
37
|
font-family: var(--bookish-header-font-family);
|
|
38
38
|
font-weight: 400;
|
|
39
39
|
font-style: italic;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
position: relative;
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding-top: 0;
|
|
42
|
+
padding-bottom: var(--bookish-paragraph-spacing);
|
|
43
|
+
padding-left: var(--bookish-indent);
|
|
44
|
+
padding-right: var(--bookish-indent);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
.quote > :global(p) {
|
|
@@ -52,6 +51,7 @@ $: credit = node.getCredit();
|
|
|
52
51
|
|
|
53
52
|
.caption {
|
|
54
53
|
margin-top: 0.5em;
|
|
54
|
+
margin-right: var(--bookish-indent);
|
|
55
55
|
text-align: right;
|
|
56
56
|
font-family: var(--bookish-header-font-family);
|
|
57
57
|
font-style: italic;
|
|
@@ -16,9 +16,8 @@ import ErrorNode from '../../models/chapter/ErrorNode';
|
|
|
16
16
|
import { caretRangeToIndexRange, indexRangeToCaretRange, } from '../../models/chapter/Caret';
|
|
17
17
|
import commands from './Commands';
|
|
18
18
|
import { afterUpdate, onMount, setContext } from 'svelte';
|
|
19
|
-
import { getCaret
|
|
19
|
+
import { getCaret } from '../page/Contexts';
|
|
20
20
|
import CaretView from './CaretView.svelte';
|
|
21
|
-
import { writable } from 'svelte/store';
|
|
22
21
|
const IDLE_TIME = 500;
|
|
23
22
|
export let text;
|
|
24
23
|
export let parser;
|
|
@@ -159,25 +158,18 @@ afterUpdate(() => {
|
|
|
159
158
|
const lineHeight = lineHeightString.endsWith('px')
|
|
160
159
|
? parseInt(lineHeightString.substring(0, lineHeightString.length - 2))
|
|
161
160
|
: undefined;
|
|
162
|
-
//
|
|
163
|
-
let
|
|
161
|
+
// Figure out the relative position of this editor.
|
|
162
|
+
let editorAncestor = element;
|
|
164
163
|
let relativeX = 0;
|
|
165
164
|
let relativeY = 0;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
while (caretAncestor != null) {
|
|
169
|
-
const style = window.getComputedStyle(caretAncestor);
|
|
165
|
+
while (editorAncestor != null) {
|
|
166
|
+
const style = window.getComputedStyle(editorAncestor);
|
|
170
167
|
if (style.position === 'relative') {
|
|
171
|
-
relativeX +=
|
|
172
|
-
relativeY +=
|
|
168
|
+
relativeX += editorAncestor.offsetLeft;
|
|
169
|
+
relativeY += editorAncestor.offsetTop;
|
|
173
170
|
}
|
|
174
|
-
|
|
175
|
-
fixedX = caretAncestor.offsetLeft;
|
|
176
|
-
fixedY = caretAncestor.offsetTop;
|
|
177
|
-
}
|
|
178
|
-
caretAncestor = caretAncestor.parentElement;
|
|
171
|
+
editorAncestor = editorAncestor.parentElement;
|
|
179
172
|
}
|
|
180
|
-
// Is the caret in a fixed position marginal? Get it's position.
|
|
181
173
|
// If we're after a new line, calculate the correct position, since selections don't actually render to the next line.
|
|
182
174
|
const left = afterNewLine
|
|
183
175
|
? textRect.left
|
|
@@ -185,6 +177,7 @@ afterUpdate(() => {
|
|
|
185
177
|
const top = afterNewLine && lineHeight
|
|
186
178
|
? rangeRect.top + lineHeight
|
|
187
179
|
: rangeRect.top;
|
|
180
|
+
// Compute the final position of the caret.
|
|
188
181
|
const position = {
|
|
189
182
|
x: left + window.scrollX - relativeX,
|
|
190
183
|
y: top + window.scrollY - relativeY,
|
|
@@ -9,7 +9,7 @@ let confirming = false;
|
|
|
9
9
|
{#if !confirming}
|
|
10
10
|
<Button {tooltip} command={() => (confirming = true)}><slot /></Button>
|
|
11
11
|
{:else}
|
|
12
|
-
<Button tooltip="no" command={() => (confirming = false)}>
|
|
12
|
+
<Button tooltip="no" command={() => (confirming = false)}>cancel</Button
|
|
13
13
|
> <Button tooltip="yes" {command}>{confirm}</Button>
|
|
14
14
|
{/if}
|
|
15
15
|
</div>
|
|
@@ -43,10 +43,10 @@ function handleImageChange(event) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
function handleImageSelection(image) {
|
|
46
|
-
// Toggle the embed
|
|
47
46
|
$caret?.edit(embed, embed.getURL() === image.url
|
|
48
|
-
? embed
|
|
49
|
-
|
|
47
|
+
? // Toggle the embed if choosing the same image
|
|
48
|
+
embed.withURL('').withDescription('')
|
|
49
|
+
: embed.withURLs(image.url, image.thumb));
|
|
50
50
|
}
|
|
51
51
|
</script>
|
|
52
52
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script>import { onMount } from 'svelte';
|
|
2
|
-
import { getBook
|
|
2
|
+
import { getBook } from '../page/Contexts';
|
|
3
|
+
import ImageThumbnail from './ImageThumbnail.svelte';
|
|
3
4
|
export let select;
|
|
4
5
|
export let selection;
|
|
5
|
-
let editor = getCaret();
|
|
6
6
|
let book = getBook();
|
|
7
7
|
let images = undefined;
|
|
8
8
|
let expanded = false;
|
|
@@ -37,18 +37,10 @@ onMount(() => {
|
|
|
37
37
|
<!-- Sort the images by their URL. There's probably a more meaningful sort,
|
|
38
38
|
such as placing unused images at the front of the list. -->
|
|
39
39
|
{#each images.sort((a, b) => a.url.localeCompare(b.url)) as image}
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
src={image.url}
|
|
45
|
-
alt={`Image named ${image.url}`}
|
|
46
|
-
tabIndex="0"
|
|
47
|
-
on:click|stopPropagation={() => select(image)}
|
|
48
|
-
on:keydown={(event) =>
|
|
49
|
-
event.key === 'Enter' || event.key === ' '
|
|
50
|
-
? select(image)
|
|
51
|
-
: undefined}
|
|
40
|
+
<ImageThumbnail
|
|
41
|
+
{image}
|
|
42
|
+
selected={image.url === selection}
|
|
43
|
+
{select}
|
|
52
44
|
/>
|
|
53
45
|
{:else}
|
|
54
46
|
No images uploaded
|
|
@@ -74,16 +66,4 @@ onMount(() => {
|
|
|
74
66
|
height: auto;
|
|
75
67
|
overflow-y: auto;
|
|
76
68
|
}
|
|
77
|
-
|
|
78
|
-
.bookish-image-chooser-image {
|
|
79
|
-
display: inline-block;
|
|
80
|
-
height: 2em;
|
|
81
|
-
cursor: pointer;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.bookish-image-chooser-image.selected {
|
|
85
|
-
outline: var(--app-chrome-border-size) solid
|
|
86
|
-
var(--app-interactive-color);
|
|
87
|
-
outline-offset: calc(-1 * var(--app-chrome-border-size));
|
|
88
|
-
}
|
|
89
69
|
</style>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script>export let image;
|
|
2
|
+
export let selected;
|
|
3
|
+
export let select;
|
|
4
|
+
let retries = 3;
|
|
5
|
+
let error = false;
|
|
6
|
+
let loading = true;
|
|
7
|
+
function loadImage(url) {
|
|
8
|
+
retries--;
|
|
9
|
+
let img = new Image();
|
|
10
|
+
img.onerror = () => {
|
|
11
|
+
if (retries > 0) {
|
|
12
|
+
setTimeout(() => loadImage(url), 1000);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
loading = false;
|
|
16
|
+
error = true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
img.onload = () => {
|
|
20
|
+
loading = false;
|
|
21
|
+
};
|
|
22
|
+
img.src = url;
|
|
23
|
+
}
|
|
24
|
+
loadImage(image.thumb);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if loading}
|
|
28
|
+
<div class="thumbnail loading" />
|
|
29
|
+
{:else}
|
|
30
|
+
<img
|
|
31
|
+
class="thumbnail"
|
|
32
|
+
class:selected
|
|
33
|
+
src={error ? image.url : image.thumb}
|
|
34
|
+
alt={`Image named ${image.url}`}
|
|
35
|
+
tabIndex="0"
|
|
36
|
+
on:click|stopPropagation={() => select(image)}
|
|
37
|
+
on:keydown={(event) =>
|
|
38
|
+
event.key === 'Enter' || event.key === ' '
|
|
39
|
+
? select(image)
|
|
40
|
+
: undefined}
|
|
41
|
+
/>
|
|
42
|
+
{/if}
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.thumbnail {
|
|
46
|
+
display: inline-block;
|
|
47
|
+
height: 2em;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.thumbnail.selected {
|
|
52
|
+
outline: var(--app-chrome-border-size) solid
|
|
53
|
+
var(--app-interactive-color);
|
|
54
|
+
outline-offset: calc(-1 * var(--app-chrome-border-size));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.loading {
|
|
58
|
+
width: 2.5em;
|
|
59
|
+
background: var(--app-chrome-background);
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import type { Image } from '../../models/book/BookMedia';
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: {
|
|
5
|
+
image: Image;
|
|
6
|
+
selected: boolean;
|
|
7
|
+
select: (image: Image) => void;
|
|
8
|
+
};
|
|
9
|
+
events: {
|
|
10
|
+
[evt: string]: CustomEvent<any>;
|
|
11
|
+
};
|
|
12
|
+
slots: {};
|
|
13
|
+
};
|
|
14
|
+
export type ImageThumbnailProps = typeof __propDef.props;
|
|
15
|
+
export type ImageThumbnailEvents = typeof __propDef.events;
|
|
16
|
+
export type ImageThumbnailSlots = typeof __propDef.slots;
|
|
17
|
+
export default class ImageThumbnail extends SvelteComponentTyped<ImageThumbnailProps, ImageThumbnailEvents, ImageThumbnailSlots> {
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -24,7 +24,7 @@ $: {
|
|
|
24
24
|
// them with non-breaking spaces.
|
|
25
25
|
const sizedText = showPlaceholder()
|
|
26
26
|
? placeholder
|
|
27
|
-
: text.replace(/\s
|
|
27
|
+
: text.replace(/\s$/g, '\u00a0');
|
|
28
28
|
// Clip the text to prevent this editor from getting too long when editing. If it goes past one line,
|
|
29
29
|
// the measurements and layout are way off.
|
|
30
30
|
const trimmedText = width !== undefined &&
|
|
@@ -111,7 +111,6 @@ function handleKeyPress(event) {
|
|
|
111
111
|
bind:this={field}
|
|
112
112
|
bind:value={text}
|
|
113
113
|
required
|
|
114
|
-
role="textbox"
|
|
115
114
|
aria-invalid={error !== undefined}
|
|
116
115
|
aria-label={label}
|
|
117
116
|
aria-placeholder={label}
|
|
@@ -136,6 +135,8 @@ function handleKeyPress(event) {
|
|
|
136
135
|
<style>
|
|
137
136
|
.text-editor {
|
|
138
137
|
position: relative;
|
|
138
|
+
display: inline-block;
|
|
139
|
+
max-width: 100%;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
input[type='text'] {
|
|
@@ -175,6 +176,8 @@ function handleKeyPress(event) {
|
|
|
175
176
|
|
|
176
177
|
/* This ensures the error message always appears below empty text editors */
|
|
177
178
|
.sizer {
|
|
179
|
+
display: inline-block;
|
|
180
|
+
max-width: 100%;
|
|
178
181
|
min-height: 1em;
|
|
179
182
|
opacity: 0;
|
|
180
183
|
border: none;
|
|
@@ -5,13 +5,13 @@ import BookishEditor from '../editor/BookishEditor.svelte';
|
|
|
5
5
|
import Format from '../chapter/Format.svelte';
|
|
6
6
|
import Note from '../editor/Note.svelte';
|
|
7
7
|
import { afterUpdate } from 'svelte';
|
|
8
|
-
import { getUser, getEdition, getLeasee, isChapterEditable, lease, } from './Contexts';
|
|
8
|
+
import { getUser, getEdition, getLeasee, isChapterEditable, lease, isEditionEditable, } from './Contexts';
|
|
9
9
|
import Button from '../app/Button.svelte';
|
|
10
10
|
export let id;
|
|
11
11
|
export let definition;
|
|
12
12
|
let auth = getUser();
|
|
13
13
|
let edition = getEdition();
|
|
14
|
-
let editable =
|
|
14
|
+
let editable = isEditionEditable();
|
|
15
15
|
// Focus after adding a new synonym.
|
|
16
16
|
let newSynonym = false;
|
|
17
17
|
let synonymsEditor = null;
|
|
@@ -36,6 +36,23 @@ function addSynonym() {
|
|
|
36
36
|
}
|
|
37
37
|
newSynonym = true;
|
|
38
38
|
}
|
|
39
|
+
function removeSynonym(index) {
|
|
40
|
+
if (definition.synonyms &&
|
|
41
|
+
index >= 0 &&
|
|
42
|
+
index < definition.synonyms.length)
|
|
43
|
+
updateSynonyms([
|
|
44
|
+
...definition.synonyms.slice(0, index),
|
|
45
|
+
...definition.synonyms.slice(index + 1),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
function updateSynonyms(synonyms) {
|
|
49
|
+
if ($edition)
|
|
50
|
+
edition.set($edition.withEditedDefinition(id, {
|
|
51
|
+
phrase: definition.phrase,
|
|
52
|
+
definition: definition.definition,
|
|
53
|
+
synonyms: synonyms,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
39
56
|
$: syns = definition.synonyms || [];
|
|
40
57
|
</script>
|
|
41
58
|
|
|
@@ -67,11 +84,11 @@ $: syns = definition.synonyms || [];
|
|
|
67
84
|
{#if editable}
|
|
68
85
|
<ConfirmButton
|
|
69
86
|
tooltip="delete definition of {definition.phrase}"
|
|
70
|
-
confirm="
|
|
87
|
+
confirm="delete entry"
|
|
71
88
|
command={() =>
|
|
72
89
|
$edition
|
|
73
90
|
? edition.set($edition.withoutDefinition(id))
|
|
74
|
-
: undefined}
|
|
91
|
+
: undefined}>⨉</ConfirmButton
|
|
75
92
|
>
|
|
76
93
|
{/if}
|
|
77
94
|
</dt>
|
|
@@ -111,40 +128,26 @@ $: syns = definition.synonyms || [];
|
|
|
111
128
|
{/if}
|
|
112
129
|
{#if edition && editable}
|
|
113
130
|
<span bind:this={synonymsEditor}>
|
|
114
|
-
{#
|
|
115
|
-
<Note>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
phrase: definition.phrase,
|
|
136
|
-
definition:
|
|
137
|
-
definition.definition,
|
|
138
|
-
synonyms: newSyns,
|
|
139
|
-
}
|
|
140
|
-
)
|
|
141
|
-
);
|
|
142
|
-
}}
|
|
143
|
-
/>
|
|
144
|
-
</Note>
|
|
145
|
-
{#if syns.length > 1 && index < syns.length - 1}, {/if}
|
|
146
|
-
{/each}
|
|
147
|
-
{/if}
|
|
131
|
+
{#each syns as syn, index}
|
|
132
|
+
<Note>
|
|
133
|
+
<TextEditor
|
|
134
|
+
text={syn}
|
|
135
|
+
label={'Synonym editor.'}
|
|
136
|
+
placeholder="synonym"
|
|
137
|
+
valid={() => undefined}
|
|
138
|
+
save={(text) => {
|
|
139
|
+
const newSyns = [...syns];
|
|
140
|
+
newSyns[index] = text;
|
|
141
|
+
updateSynonyms(newSyns);
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
<Button
|
|
145
|
+
tooltip="delete synonym"
|
|
146
|
+
command={() => removeSynonym(index)}>x</Button
|
|
147
|
+
>
|
|
148
|
+
</Note>
|
|
149
|
+
{#if syns.length > 1 && index < syns.length - 1}, {/if}
|
|
150
|
+
{/each}
|
|
148
151
|
</span> <Button tooltip="add synonym" command={addSynonym}
|
|
149
152
|
>+ synonym</Button
|
|
150
153
|
>
|
|
@@ -162,6 +165,8 @@ $: syns = definition.synonyms || [];
|
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
dt {
|
|
168
|
+
font-family: var(--bookish-header-font-family);
|
|
165
169
|
font-weight: bold;
|
|
170
|
+
vertical-align: baseline;
|
|
166
171
|
}
|
|
167
172
|
</style>
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { goto } from '$app/navigation';
|
|
3
3
|
import { onMount, setContext, tick } from 'svelte';
|
|
4
4
|
import { writable } from 'svelte/store';
|
|
5
|
-
import { BASE, DARK_MODE, } from './Contexts';
|
|
5
|
+
import { BASE, DARK_MODE, getBook, } from './Contexts';
|
|
6
6
|
import { BookishTheme } from '../../models/book/Theme';
|
|
7
7
|
import { isDark, setDark } from '../../util/dark';
|
|
8
|
+
import { page } from '$app/stores';
|
|
8
9
|
// Poly fill smooth scrolling for Safari.
|
|
9
10
|
onMount(() => smoothscroll.polyfill());
|
|
10
11
|
// The book edition to render.
|
|
@@ -14,6 +15,7 @@ export let edition;
|
|
|
14
15
|
// but when the book is being viewed or edited in the Bookish app, it needs a prefix for the
|
|
15
16
|
// route in the app.
|
|
16
17
|
export let base = '';
|
|
18
|
+
let book = getBook();
|
|
17
19
|
// When the base changes, update the context.
|
|
18
20
|
let baseStore = writable(base);
|
|
19
21
|
$: setContext(BASE, baseStore);
|
|
@@ -84,10 +86,15 @@ function toRules(set) {
|
|
|
84
86
|
<meta property="og:title" content={edition.getTitle()} />
|
|
85
87
|
<meta property="og:image" content={edition.getImage('cover')} />
|
|
86
88
|
<meta property="og:description" content={edition.getDescription()} />
|
|
87
|
-
|
|
89
|
+
<meta property="og:type" content="book" />
|
|
90
|
+
<meta property="og:author" content={edition.getAuthorsText()} />
|
|
91
|
+
<meta
|
|
88
92
|
property="og:url"
|
|
89
|
-
content=
|
|
90
|
-
|
|
93
|
+
content={edition.base ??
|
|
94
|
+
`https://bookish.press/${
|
|
95
|
+
$book && $book.domain ? $book.domain : $page.params.bookid
|
|
96
|
+
}`}
|
|
97
|
+
/>
|
|
91
98
|
</svelte:head>
|
|
92
99
|
|
|
93
100
|
<main class="bookish {$darkMode ? ' dark' : ''}">
|
|
@@ -54,80 +54,84 @@ let embed;
|
|
|
54
54
|
$: embed = getImage();
|
|
55
55
|
</script>
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
{/if}
|
|
86
|
-
{#if !print}
|
|
87
|
-
<slot name="outline" />
|
|
88
|
-
{/if}
|
|
89
|
-
{#if editable}
|
|
90
|
-
{#if embed === null}
|
|
91
|
-
<Button tooltip="add cover image" command={addCover}
|
|
92
|
-
>+ cover image</Button
|
|
93
|
-
>
|
|
57
|
+
<!-- We key on the chapter ID to avoid laggy updates from image loading -->
|
|
58
|
+
{#key id}
|
|
59
|
+
<header class="bookish-chapter-header">
|
|
60
|
+
{#if embed}
|
|
61
|
+
<div class="bookish-figure-full">
|
|
62
|
+
{#if $edition && editable}
|
|
63
|
+
<BookishEditor
|
|
64
|
+
text={embed}
|
|
65
|
+
parser={(text) => Parser.parseEmbed($edition, text)}
|
|
66
|
+
save={(node) =>
|
|
67
|
+
setImage ? setImage(node.toBookdown()) : undefined}
|
|
68
|
+
chapter={false}
|
|
69
|
+
component={Embed}
|
|
70
|
+
placeholder=""
|
|
71
|
+
leasee={getLeasee(auth, edition, `embed-${id}`)}
|
|
72
|
+
lease={(lock) =>
|
|
73
|
+
lease(auth, edition, `embed-${id}`, lock)}
|
|
74
|
+
/>
|
|
75
|
+
{:else}
|
|
76
|
+
<Embed
|
|
77
|
+
node={Parser.parseEmbed($edition, embed)}
|
|
78
|
+
editable={false}
|
|
79
|
+
/>
|
|
80
|
+
{/if}
|
|
81
|
+
{#if !print && showReminder}
|
|
82
|
+
<div class="bookish-scroll-reminder" />
|
|
83
|
+
{/if}
|
|
84
|
+
</div>
|
|
94
85
|
{:else}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
tooltip="remove cover image"
|
|
98
|
-
command={removeCover}>- cover image</ConfirmButton
|
|
99
|
-
>
|
|
86
|
+
<!-- Add a bit of space to account for the lack of an image. -->
|
|
87
|
+
<p> </p>
|
|
100
88
|
{/if}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{#if
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
placeholder="title"
|
|
110
|
-
valid={(text) =>
|
|
111
|
-
text.length === 0
|
|
112
|
-
? 'Titles have to be at least one character long.'
|
|
113
|
-
: undefined}
|
|
114
|
-
{save}
|
|
115
|
-
/>
|
|
89
|
+
{#if !print}
|
|
90
|
+
<slot name="outline" />
|
|
91
|
+
{/if}
|
|
92
|
+
{#if editable}
|
|
93
|
+
{#if embed === null}
|
|
94
|
+
<Button tooltip="add cover image" command={addCover}
|
|
95
|
+
>+ cover image</Button
|
|
96
|
+
>
|
|
116
97
|
{:else}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
98
|
+
<ConfirmButton
|
|
99
|
+
confirm="delete image"
|
|
100
|
+
tooltip="delete cover image"
|
|
101
|
+
command={removeCover}>- cover image</ConfirmButton
|
|
102
|
+
>
|
|
120
103
|
{/if}
|
|
121
|
-
</Title>
|
|
122
|
-
<slot name="after" />
|
|
123
|
-
{#if tags}
|
|
124
|
-
<div
|
|
125
|
-
>{#each tags as tag}<span class="bookish-tag">{tag}</span
|
|
126
|
-
>{/each}</div
|
|
127
|
-
>
|
|
128
104
|
{/if}
|
|
129
|
-
|
|
130
|
-
|
|
105
|
+
<div bind:this={title} class="bookish-chapter-header-text">
|
|
106
|
+
<slot name="before" />
|
|
107
|
+
<Title>
|
|
108
|
+
{#if editable && save}
|
|
109
|
+
<TextEditor
|
|
110
|
+
{label}
|
|
111
|
+
text={header + (subtitle ? ': ' + subtitle : '')}
|
|
112
|
+
placeholder="title"
|
|
113
|
+
valid={(text) =>
|
|
114
|
+
text.length === 0
|
|
115
|
+
? 'Titles have to be at least one character long.'
|
|
116
|
+
: undefined}
|
|
117
|
+
{save}
|
|
118
|
+
/>
|
|
119
|
+
{:else}
|
|
120
|
+
{header}
|
|
121
|
+
{#if subtitle}<div class="bookish-subtitle">{subtitle}</div
|
|
122
|
+
>{/if}
|
|
123
|
+
{/if}
|
|
124
|
+
</Title>
|
|
125
|
+
<slot name="after" />
|
|
126
|
+
{#if tags}
|
|
127
|
+
<div
|
|
128
|
+
>{#each tags as tag}<span class="bookish-tag">{tag}</span
|
|
129
|
+
>{/each}</div
|
|
130
|
+
>
|
|
131
|
+
{/if}
|
|
132
|
+
</div>
|
|
133
|
+
</header>
|
|
134
|
+
{/key}
|
|
131
135
|
|
|
132
136
|
<style>
|
|
133
137
|
.bookish-subtitle {
|
|
@@ -24,7 +24,7 @@ onMount(() => {
|
|
|
24
24
|
media?.notify(updateImages);
|
|
25
25
|
return () => media?.stopNotifying(updateImages);
|
|
26
26
|
});
|
|
27
|
-
$: unused = images?.filter((image) => embeds.
|
|
27
|
+
$: unused = images?.filter((image) => !embeds.some((embed) => image.url.includes(embed.embed.getURL())));
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
30
|
{#if $edition}
|
|
@@ -59,9 +59,10 @@ $: unused = images?.filter((image) => embeds.find((embed) => embed.getURL() ===
|
|
|
59
59
|
{:else}
|
|
60
60
|
<p>These are the images in the book:</p>
|
|
61
61
|
{/if}
|
|
62
|
-
{#each embeds as embed}
|
|
62
|
+
{#each embeds as { embed, chapterID }}
|
|
63
63
|
{#if !embed.isVideo()}
|
|
64
64
|
<MediaPreview
|
|
65
|
+
{chapterID}
|
|
65
66
|
url={embed.isLocal()
|
|
66
67
|
? `${$base}/${embed.getSmallURL()}`
|
|
67
68
|
: embed.getSmallURL()}
|
|
@@ -89,10 +90,10 @@ $: unused = images?.filter((image) => embeds.find((embed) => embed.getURL() ===
|
|
|
89
90
|
be used in other editions of this book.
|
|
90
91
|
</Instructions>
|
|
91
92
|
{#each unused as image}
|
|
92
|
-
<MediaPreview url={image.url} alt={''}>
|
|
93
|
+
<MediaPreview chapterID={undefined} url={image.url} alt={''}>
|
|
93
94
|
<ConfirmButton
|
|
94
95
|
tooltip="delete unused image"
|
|
95
|
-
confirm="delete image
|
|
96
|
+
confirm="delete image"
|
|
96
97
|
command={() =>
|
|
97
98
|
media
|
|
98
99
|
?.remove(image)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script>import Link from '../Link.svelte';
|
|
2
|
+
import { getBase } from './Contexts';
|
|
3
|
+
export let url;
|
|
2
4
|
export let alt;
|
|
5
|
+
export let chapterID;
|
|
6
|
+
let base = getBase();
|
|
3
7
|
let loaded;
|
|
4
8
|
let urlChecked;
|
|
5
9
|
$: {
|
|
@@ -18,7 +22,9 @@ $: {
|
|
|
18
22
|
|
|
19
23
|
<figure class={'media-preview'}>
|
|
20
24
|
{#if loaded}
|
|
21
|
-
<
|
|
25
|
+
<Link to={`${$base}/${chapterID}`} linked={chapterID !== undefined}
|
|
26
|
+
><img src={url.startsWith('http') ? url : `${url}`} {alt} /></Link
|
|
27
|
+
>
|
|
22
28
|
{:else if loaded === null}
|
|
23
29
|
<div class="missing">missing image</div>
|
|
24
30
|
{/if}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import Loading from './Loading.svelte';
|
|
3
3
|
export let title;
|
|
4
4
|
export let afterLoaded = undefined;
|
|
5
|
-
let
|
|
5
|
+
let fontsLoaded = false;
|
|
6
|
+
let imagesLoaded = false;
|
|
6
7
|
let lastHeight = 0;
|
|
7
8
|
let intervalID = null;
|
|
8
9
|
let mountTime = Date.now();
|
|
10
|
+
$: loaded = fontsLoaded && imagesLoaded;
|
|
9
11
|
function watchLoading() {
|
|
10
12
|
const bodyHeight = document.body.clientHeight;
|
|
11
13
|
// If the height hasn't changed and images are loaded OR it's been 2 seconds, load.
|
|
@@ -14,7 +16,7 @@ function watchLoading() {
|
|
|
14
16
|
// Stop watching the images
|
|
15
17
|
stopWatching();
|
|
16
18
|
// Set to loaded to hide the overlay and notify callback if there is one.
|
|
17
|
-
|
|
19
|
+
imagesLoaded = true;
|
|
18
20
|
if (afterLoaded)
|
|
19
21
|
afterLoaded();
|
|
20
22
|
}
|
|
@@ -29,6 +31,7 @@ onMount(() => {
|
|
|
29
31
|
// Watch the height of the document and wait until it's been stable for a while before scrolling
|
|
30
32
|
// to any targets. We use the data below to monitor the document height over time.
|
|
31
33
|
intervalID = setInterval(() => watchLoading(), 50);
|
|
34
|
+
document.fonts.ready.then(() => (fontsLoaded = true));
|
|
32
35
|
return () => stopWatching();
|
|
33
36
|
});
|
|
34
37
|
function imagesAreLoaded() {
|
|
@@ -45,7 +48,7 @@ function imagesAreLoaded() {
|
|
|
45
48
|
<meta name="description" content={title} />
|
|
46
49
|
</svelte:head>
|
|
47
50
|
|
|
48
|
-
<section class={'bookish-page' + (
|
|
51
|
+
<section class={'bookish-page' + (imagesLoaded ? ' loaded' : '')}>
|
|
49
52
|
<slot />
|
|
50
53
|
</section>
|
|
51
54
|
<!-- Overlay loading feedback if loading, but still render the page. -->
|
|
@@ -249,7 +249,7 @@ function move(field, direction) {
|
|
|
249
249
|
{#if edit && editable && !reference.short}
|
|
250
250
|
<ConfirmButton
|
|
251
251
|
tooltip="delete reference {reference.title}"
|
|
252
|
-
confirm="delete reference
|
|
252
|
+
confirm="delete reference"
|
|
253
253
|
command={() =>
|
|
254
254
|
$edition
|
|
255
255
|
? edition.set($edition.withoutReference(reference.citationID))
|
|
@@ -273,10 +273,7 @@ onMount(() => {
|
|
|
273
273
|
|
|
274
274
|
<!-- Book citation -->
|
|
275
275
|
<PageParagraph>
|
|
276
|
-
{$edition
|
|
277
|
-
.getAuthors()
|
|
278
|
-
.map((author) => Parser.parseFormat($edition, author).toText())
|
|
279
|
-
.join(', ')} ({new Date().getFullYear()}).
|
|
276
|
+
{$edition.getAuthorsText()} ({new Date().getFullYear()}).
|
|
280
277
|
<em>{$edition.getTitle()}</em>. {bookURL},
|
|
281
278
|
<em>retrieved {new Date().toLocaleDateString('en-US')}</em>.
|
|
282
279
|
</PageParagraph>
|
|
@@ -2,6 +2,7 @@ import type { StorageReference } from 'firebase/storage';
|
|
|
2
2
|
import type Book from './Book';
|
|
3
3
|
export type Image = {
|
|
4
4
|
url: string;
|
|
5
|
+
thumb: string;
|
|
5
6
|
ref: StorageReference;
|
|
6
7
|
};
|
|
7
8
|
export default class BookMedia {
|
|
@@ -17,6 +18,8 @@ export default class BookMedia {
|
|
|
17
18
|
/** Get a listing of all of the images for the book. */
|
|
18
19
|
cacheImages(): Promise<Image[]>;
|
|
19
20
|
retrieveImages(): Promise<(Image | undefined)[]>;
|
|
21
|
+
stripTokenFromURL(url: string): string;
|
|
22
|
+
makeThumbnailURL(url: string): string;
|
|
20
23
|
getThumbnailURL(url: string, thumbnailRef: StorageReference, error: (message: string) => void, finished: (url: string, thumbnailURL: string) => void, attempt?: number): void;
|
|
21
24
|
upload(file: File, _: (progress: number) => void, errorHandler: (message: string) => void, finishedHandler: (url: string, thumbnailURL: string) => void): void;
|
|
22
25
|
remove(image: Image): Promise<Image[] | undefined>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { deleteObject, getDownloadURL, listAll, ref, uploadBytes, } from 'firebase/storage';
|
|
2
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
3
2
|
import { storage } from '../Firebase';
|
|
4
3
|
export default class BookMedia {
|
|
5
4
|
book;
|
|
@@ -43,7 +42,8 @@ export default class BookMedia {
|
|
|
43
42
|
// Find all the prefixes and items.
|
|
44
43
|
return listAll(listRef).then((res) => Promise.all(res.items.map((itemRef) => getDownloadURL(itemRef).then((url) => {
|
|
45
44
|
const image = {
|
|
46
|
-
url
|
|
45
|
+
url,
|
|
46
|
+
thumb: this.makeThumbnailURL(url),
|
|
47
47
|
description: '',
|
|
48
48
|
ref: itemRef,
|
|
49
49
|
};
|
|
@@ -53,6 +53,22 @@ export default class BookMedia {
|
|
|
53
53
|
return image;
|
|
54
54
|
}))));
|
|
55
55
|
}
|
|
56
|
+
stripTokenFromURL(url) {
|
|
57
|
+
const tokenIndex = url.indexOf('&token=');
|
|
58
|
+
return tokenIndex >= 0 ? (url = url.substring(0, tokenIndex)) : url;
|
|
59
|
+
}
|
|
60
|
+
// Construct a thumbnail URL from the base URL.
|
|
61
|
+
makeThumbnailURL(url) {
|
|
62
|
+
// The structure of the URL is
|
|
63
|
+
// https://[firebase-domain]/v0/b/[projectid].appspot.com/o/images%2F[bookID]%2F[imagename]?alt=media
|
|
64
|
+
// And we want
|
|
65
|
+
// https://[firebase-domain]/v0/b/[projectid].appspot.com/o/images%2F[bookID]%2Fthumbnails%2F[imagename]?alt=media
|
|
66
|
+
const bookID = this.book.getID();
|
|
67
|
+
const bookIDIndex = url.indexOf(bookID);
|
|
68
|
+
return bookIDIndex >= 0
|
|
69
|
+
? `${url.substring(0, bookIDIndex)}${bookID}%2Fthumbnails${url.substring(bookIDIndex + bookID.length)}`
|
|
70
|
+
: url;
|
|
71
|
+
}
|
|
56
72
|
getThumbnailURL(url, thumbnailRef, error, finished, attempt = 0) {
|
|
57
73
|
getDownloadURL(thumbnailRef)
|
|
58
74
|
.then((thumbnail) => finished(url, thumbnail))
|
|
@@ -64,9 +80,10 @@ export default class BookMedia {
|
|
|
64
80
|
upload(file, _, errorHandler, finishedHandler) {
|
|
65
81
|
if (storage === undefined)
|
|
66
82
|
return;
|
|
83
|
+
// We use the file name given, but append a unique string to avoid collisions.
|
|
84
|
+
const imageName = file.name;
|
|
67
85
|
// The canonical path format for Bookish images in the store is image/{bookid}/{imageid}
|
|
68
|
-
// where {bookid} is the Firestore ID of the book being edited and {imageid} is just a random id.
|
|
69
|
-
const imageName = `image-${uuidv4()}`;
|
|
86
|
+
// where {bookid} is the Firestore document ID of the book being edited and {imageid} is just a random id.
|
|
70
87
|
const imageRef = ref(storage, `${this.getImagePath()}/${imageName}`);
|
|
71
88
|
const thumbnailRef = ref(storage, `${this.getImagePath()}/thumbnails/${imageName}`);
|
|
72
89
|
uploadBytes(imageRef, file)
|
|
@@ -74,7 +91,7 @@ export default class BookMedia {
|
|
|
74
91
|
getDownloadURL(snapshot.ref)
|
|
75
92
|
// After we get the download URL for the uploaded image,
|
|
76
93
|
// try to get the thumbnail URL for the resized image on the server.
|
|
77
|
-
.then((url) => this.
|
|
94
|
+
.then((url) => finishedHandler(this.stripTokenFromURL(url), this.makeThumbnailURL(this.stripTokenFromURL(url))))
|
|
78
95
|
.catch(() => errorHandler('Unable to get image URL.'));
|
|
79
96
|
})
|
|
80
97
|
.then(() => this.cacheImages())
|
|
@@ -107,6 +107,7 @@ export default class Edition {
|
|
|
107
107
|
withTheme(theme: Theme | null): Edition;
|
|
108
108
|
withThemeValue(group: string, name: string, value: string): Edition;
|
|
109
109
|
getAuthors(): string[];
|
|
110
|
+
getAuthorsText(): string;
|
|
110
111
|
withAuthors(authors: string[]): Edition;
|
|
111
112
|
withAuthor(name: string): Edition;
|
|
112
113
|
withAuthorName(index: number, name: string): Edition;
|
|
@@ -130,7 +131,10 @@ export default class Edition {
|
|
|
130
131
|
getBookIndex(): Record<string, Set<string>>;
|
|
131
132
|
getNextChapterID(chapterID: string): string | null;
|
|
132
133
|
getPreviousChapterID(chapterID: string): string | null;
|
|
133
|
-
getEmbeds():
|
|
134
|
+
getEmbeds(): {
|
|
135
|
+
embed: EmbedNode;
|
|
136
|
+
chapterID: string | undefined;
|
|
137
|
+
}[];
|
|
134
138
|
isLatestEdition(book: Book): boolean;
|
|
135
139
|
isLatestPublishedEdition(book: Book): boolean;
|
|
136
140
|
getEditionNumber(): number;
|
|
@@ -59,7 +59,7 @@ export default class Edition {
|
|
|
59
59
|
static fromJSON(editionRef, spec) {
|
|
60
60
|
// Copy all of the specification metadata to fields.
|
|
61
61
|
// Choose suitable defaults if the spec is lacking a field.
|
|
62
|
-
return new Edition(spec.bookRef, editionRef, spec.uids, spec.title, spec.authors, spec.number, spec.summary, spec.published, spec.images, spec.description, spec.chapters.map((chap) => Chapter.fromJSON(chap, '')), spec.license, spec.acknowledgements, spec.tags ?? [], spec.sources ?? {}, spec.references ?? {}, spec.symbols ?? {}, spec.glossary ?? {}, spec.theme, spec.base
|
|
62
|
+
return new Edition(spec.bookRef, editionRef, spec.uids, spec.title, spec.authors, spec.number, spec.summary, spec.published, spec.images, spec.description, spec.chapters.map((chap) => Chapter.fromJSON(chap, '')), spec.license, spec.acknowledgements, spec.tags ?? [], spec.sources ?? {}, spec.references ?? {}, spec.symbols ?? {}, spec.glossary ?? {}, spec.theme, spec.base, spec.active);
|
|
63
63
|
}
|
|
64
64
|
static parseReference(citationID, ref, edition, short = false) {
|
|
65
65
|
if (typeof ref === 'string')
|
|
@@ -99,7 +99,7 @@ export default class Edition {
|
|
|
99
99
|
symbols: Object.assign({}, this.symbols),
|
|
100
100
|
glossary: Object.assign({}, this.glossary),
|
|
101
101
|
theme: this.theme,
|
|
102
|
-
base: this.base,
|
|
102
|
+
base: this.base && this.base.length > 0 ? this.base : null,
|
|
103
103
|
uids: this.uids.slice(),
|
|
104
104
|
chapteruids: this.getChapterUIDS(),
|
|
105
105
|
active: this.active,
|
|
@@ -340,6 +340,11 @@ export default class Edition {
|
|
|
340
340
|
getAuthors() {
|
|
341
341
|
return [...this.authors];
|
|
342
342
|
}
|
|
343
|
+
getAuthorsText() {
|
|
344
|
+
return this.getAuthors()
|
|
345
|
+
.map((author) => Parser.parseFormat(this, author).toText())
|
|
346
|
+
.join(', ');
|
|
347
|
+
}
|
|
343
348
|
withAuthors(authors) {
|
|
344
349
|
return new Edition(this.bookRef, this.editionRef, this.uids, this.title, authors, this.number, this.summary, this.published, this.images, this.description, this.chapters, this.license, this.acknowledgements, this.tags, this.sources, this.references, this.symbols, this.glossary, this.theme, this.base, this.active);
|
|
345
350
|
}
|
|
@@ -563,18 +568,18 @@ export default class Edition {
|
|
|
563
568
|
if (cover) {
|
|
564
569
|
let coverNode = Parser.parseEmbed(this, cover);
|
|
565
570
|
if (coverNode instanceof EmbedNode)
|
|
566
|
-
embeds.push(coverNode);
|
|
571
|
+
embeds.push({ embed: coverNode, chapterID: undefined });
|
|
567
572
|
}
|
|
568
573
|
// Add the cover and images from each chapter.
|
|
569
574
|
this.getChapters().forEach((c) => {
|
|
570
575
|
const image = c.getImage();
|
|
571
576
|
let cover = image === null ? undefined : Parser.parseEmbed(this, image);
|
|
572
577
|
if (cover && cover instanceof EmbedNode)
|
|
573
|
-
embeds.push(cover);
|
|
578
|
+
embeds.push({ embed: cover, chapterID: c.id });
|
|
574
579
|
// Get the chapter body's embeds
|
|
575
580
|
let bodyEmbeds = c?.getAST(this)?.getEmbeds();
|
|
576
581
|
if (bodyEmbeds)
|
|
577
|
-
bodyEmbeds.forEach((embed) => embeds.push(embed));
|
|
582
|
+
bodyEmbeds.forEach((embed) => embeds.push({ embed, chapterID: c.id }));
|
|
578
583
|
});
|
|
579
584
|
// Add the back matter covers
|
|
580
585
|
let backmatter = ['references', 'glossary', 'index', 'search', 'media'];
|
|
@@ -583,7 +588,7 @@ export default class Edition {
|
|
|
583
588
|
if (img) {
|
|
584
589
|
let image = Parser.parseEmbed(this, img);
|
|
585
590
|
if (image instanceof EmbedNode)
|
|
586
|
-
embeds.push(image);
|
|
591
|
+
embeds.push({ embed: image, chapterID: id });
|
|
587
592
|
}
|
|
588
593
|
});
|
|
589
594
|
return embeds;
|
|
@@ -22,6 +22,7 @@ export default class ChapterNode extends BlocksNode {
|
|
|
22
22
|
getEmbeds(): EmbedNode[];
|
|
23
23
|
getComments(): CommentNode[];
|
|
24
24
|
getLabels(): LabelNode[];
|
|
25
|
+
getFirstParagraph(): ParagraphNode | undefined;
|
|
25
26
|
hasLabel(labelID: string): boolean;
|
|
26
27
|
getCitationNumber(citationID: string): number | null;
|
|
27
28
|
toText(): string;
|
|
@@ -45,6 +45,9 @@ export default class ChapterNode extends BlocksNode {
|
|
|
45
45
|
getLabels() {
|
|
46
46
|
return this.getNodes().filter((n) => n instanceof LabelNode);
|
|
47
47
|
}
|
|
48
|
+
getFirstParagraph() {
|
|
49
|
+
return this.getBlocks().find((b) => b instanceof ParagraphNode);
|
|
50
|
+
}
|
|
48
51
|
hasLabel(labelID) {
|
|
49
52
|
return (this.getLabels().find((label) => label.getMeta() === labelID) !==
|
|
50
53
|
undefined);
|
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.26",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A platform for reading and writing readable, accessible, beautiful online books.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
"//stage": "echo 'switch to the dev project, deploy, relying on Firebase's predeploy hook to run the build script above before release'",
|
|
23
23
|
"stage": "firebase use dev && firebase deploy",
|
|
24
24
|
"//release": "echo 'switch to the prod project, deploy, relying on Firebase's predeploy hook to run the build script above before release'",
|
|
25
|
-
"release": "firebase use prod && firebase deploy"
|
|
25
|
+
"release": "firebase use prod && firebase deploy",
|
|
26
|
+
"//merge": "merge dev into main, push to remote",
|
|
27
|
+
"merge": "git checkout main && git merge dev && git push origin main && git checkout dev"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@sveltejs/adapter-static": "^2.0.1",
|
|
29
|
-
"@sveltejs/kit": "^1.
|
|
30
|
-
"@sveltejs/package": "^2.0.
|
|
31
|
+
"@sveltejs/kit": "^1.11",
|
|
32
|
+
"@sveltejs/package": "^2.0.2",
|
|
31
33
|
"@types/prismjs": "^1.26.0",
|
|
32
34
|
"@types/smoothscroll-polyfill": "^0.3.1",
|
|
33
35
|
"@types/uuid": "^9.0.0",
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
"tslib": "^2.5.0",
|
|
40
42
|
"typescript": "^4.9.5",
|
|
41
43
|
"vite": "^4.1.2",
|
|
42
|
-
"vitest": "^0.
|
|
44
|
+
"vitest": "^0.29.2"
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
47
|
"ajv": "^8.12.0",
|