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.
- package/dist/components/chapter/Citations.svelte +4 -1
- package/dist/components/editor/BookishEditor.svelte +17 -4
- package/dist/components/editor/CaretContext.d.ts +4 -0
- package/dist/components/editor/CaretContext.js +52 -1
- package/dist/components/editor/CaretState.d.ts +1 -1
- package/dist/components/editor/CaretView.svelte +12 -1
- package/dist/components/editor/CaretView.svelte.d.ts +2 -0
- package/dist/components/editor/Command.d.ts +1 -1
- package/dist/components/editor/Commands.js +52 -0
- package/dist/components/editor/Permissions.svelte +19 -17
- package/dist/components/editor/Toolbar.svelte +1 -6
- package/dist/components/page/Chapter.svelte +9 -8
- package/dist/components/page/DefinitionView.svelte +2 -2
- package/dist/components/page/EditionLoader.svelte +1 -1
- package/dist/components/page/Editions.svelte +52 -27
- package/dist/components/page/Outline.svelte +1 -0
- package/dist/components/page/Page.svelte +5 -3
- package/dist/components/page/Page.svelte.d.ts +1 -1
- package/dist/components/page/Print.svelte +17 -0
- package/dist/models/CRUD.js +31 -8
- package/dist/models/book/Book.d.ts +1 -1
- package/dist/models/book/Book.js +3 -3
- package/package.json +7 -7
|
@@ -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
|
|
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}
|
|
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
|
-
{#
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
$auth
|
|
191
|
-
|
|
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
|
|
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}
|
|
@@ -13,20 +13,47 @@ let editable = isBookEditable();
|
|
|
13
13
|
let user = getUser();
|
|
14
14
|
$: editions = $book?.getEditions();
|
|
15
15
|
$: latestPublishedID = $book?.getLatestPublishedEditionID();
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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}
|
|
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
|
|
|
74
98
|
<Link
|
|
75
|
-
to={`/write/${$book.getID()}/${
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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,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;
|
package/dist/models/CRUD.js
CHANGED
|
@@ -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
|
|
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
|
|
141
|
+
const latestDraftData = latestDraftDoc.exists()
|
|
142
142
|
? latestDraftDoc.data()
|
|
143
143
|
: undefined;
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
|
|
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;
|
package/dist/models/book/Book.js
CHANGED
|
@@ -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
|
|
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)
|
|
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.
|
|
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-
|
|
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": "^
|
|
55
|
-
"firebase-functions": "^6
|
|
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": "
|
|
61
|
+
"node": "22"
|
|
62
62
|
},
|
|
63
63
|
"types": "./dist/index.d.ts",
|
|
64
64
|
"main": "./dist/index.js",
|