bookish-press 0.7.0 → 0.7.1
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/Toolbar.svelte +1 -6
- package/dist/components/page/DefinitionView.svelte +2 -2
- package/dist/components/page/EditionLoader.svelte +1 -1
- package/dist/models/CRUD.js +9 -5
- package/package.json +5 -4
|
@@ -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;
|
|
@@ -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>
|
|
@@ -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}
|
package/dist/models/CRUD.js
CHANGED
|
@@ -138,18 +138,22 @@ export async function createNewEdition(book) {
|
|
|
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
|
-
if (
|
|
144
|
+
if (latestDraftData === undefined)
|
|
145
145
|
return book;
|
|
146
|
+
const latestDraft = Edition.fromJSON(latestDraftDoc.ref, latestDraftData);
|
|
146
147
|
// Make a copy without chapter and edition refs, because we want to create new ones,
|
|
147
148
|
// and with an incremented edition number.
|
|
148
|
-
|
|
149
|
+
let newEdition = latestDraft.withoutRefs();
|
|
149
150
|
// Create a copy of the existing draft as a new edition.
|
|
150
|
-
const newEditionRef = await addDoc(collection(db, 'editions'), newEdition.toObject());
|
|
151
|
+
const newEditionRef = await addDoc(collection(db, 'books', book.id, 'editions'), newEdition.toObject());
|
|
152
|
+
newEdition = newEdition
|
|
153
|
+
.withRef(newEditionRef)
|
|
154
|
+
.withIncrementedEditionNumber();
|
|
151
155
|
// Create the edition, creating chapter text as necessary, using the edition sync function.
|
|
152
|
-
updateEdition(book, undefined, newEdition
|
|
156
|
+
updateEdition(book, undefined, newEdition);
|
|
153
157
|
// Put the new edition in the book.
|
|
154
158
|
book = book.withEditions([
|
|
155
159
|
newEdition.getInfo(),
|
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.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A platform for reading and writing readable, accessible, beautiful online books.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
"ajv": "^8",
|
|
52
52
|
"ajv-formats": "^3",
|
|
53
53
|
"firebase": "^11",
|
|
54
|
-
"firebase-admin": "^
|
|
55
|
-
"firebase-functions": "^6
|
|
54
|
+
"firebase-admin": "^12.7.0",
|
|
55
|
+
"firebase-functions": "^6",
|
|
56
56
|
"prismjs": "^1.29",
|
|
57
57
|
"smoothscroll-polyfill": "^0.4",
|
|
58
|
-
"svelte": "^4"
|
|
58
|
+
"svelte": "^4",
|
|
59
|
+
"uuid": "^11"
|
|
59
60
|
},
|
|
60
61
|
"engines": {
|
|
61
62
|
"node": "20"
|