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.
@@ -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 !== undefined) {
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}</div
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}
@@ -97,7 +97,7 @@ onDestroy(() => {
97
97
  </script>
98
98
 
99
99
  {#if $book === undefined || error}
100
- <Feedback error>Unable to load edition.</Feedback>
100
+ <Feedback error>Unable to load edition: {error}</Feedback>
101
101
  {:else if $edition === undefined}
102
102
  <Loading />
103
103
  {:else}
@@ -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 latestDraft = latestDraftDoc.exists()
141
+ const latestDraftData = latestDraftDoc.exists()
142
142
  ? latestDraftDoc.data()
143
143
  : undefined;
144
- if (latestDraft === undefined)
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
- const newEdition = latestDraft.withoutRefs();
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.withRef(newEditionRef));
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.0",
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": "^13.2.0",
55
- "firebase-functions": "^6.3.2",
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"