Skip to content

Commit acdbcf9

Browse files
committed
platform: fix verse renumbering
- fix renumbering if only one chapter or is last chapter - don't include `sid` for chapter or verse - add a space before a verse if needed - don't renumber if new USJ is loaded - refactor to use node transform (remove bad cascading update) - add tests and framework - also add `utilities` develop-in-app instructions - also remove redundant `await`s from tests
1 parent f8d8243 commit acdbcf9

File tree

8 files changed

+481
-107
lines changed

8 files changed

+481
-107
lines changed

packages/platform/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ To develop an editor in a target application you can use [yalc](https://www.npmj
298298
```bash
299299
yalc link @biblionexus-foundation/platform-editor
300300
```
301-
3. In this monorepo, make changes and re-publish the editor (see step 2).
301+
3. In this monorepo, make changes and re-publish the editor (see step 1).
302302
4. When you have finished developing in the target application repo, unlink from `yalc`:
303303
```bash
304304
yalc remove @biblionexus-foundation/platform-editor && npm i

packages/platform/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export default function App() {
171171
const timeoutId = setTimeout(() => {
172172
marginalRef.current?.setComments?.(comments as Comments);
173173
marginalRef.current?.setUsj(usxStringToUsj(usx));
174-
}, 1000);
174+
}, 0);
175175
return () => clearTimeout(timeoutId);
176176
}, []);
177177

packages/platform/src/editor/adaptors/usj-marker-action.utils.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MarkerContent } from "@biblionexus-foundation/scripture-utilities";
22
import { SerializedVerseRef } from "@sillsdev/scripture";
33
import {
4+
$createTextNode,
45
$getSelection,
56
$isElementNode,
67
$isRangeSelection,
@@ -19,6 +20,7 @@ import { ParaNode } from "shared/nodes/scripture/usj/ParaNode";
1920
import { MarkerAction } from "shared/utils/get-marker-action.model";
2021
import { Marker } from "shared/utils/usfm/usfmTypes";
2122
import { createLexicalUsjNode } from "shared/utils/usj/contentToLexicalNode";
23+
import { $isSomeVerseNode } from "shared-react/nodes/scripture/usj/node-react.utils";
2224
import { ViewOptions } from "./view-options.utils";
2325
import usjEditorAdaptor from "./usj-editor.adaptor";
2426

@@ -33,26 +35,24 @@ const markerActions: {
3335
} = {
3436
c: {
3537
action: (currentEditor) => {
36-
const { book, chapterNum } = currentEditor.reference;
38+
const { chapterNum } = currentEditor.reference;
3739
const nextChapter = chapterNum + 1;
3840
const content: MarkerContent = {
3941
type: "chapter",
4042
marker: "c",
4143
number: `${nextChapter}`,
42-
sid: `${book} ${nextChapter}`,
4344
};
4445
return [content];
4546
},
4647
},
4748
v: {
4849
action: (currentEditor) => {
49-
const { book, chapterNum, verseNum, verse } = currentEditor.reference;
50+
const { verseNum, verse } = currentEditor.reference;
5051
const nextVerse = getNextVerse(verseNum, verse);
5152
const content: MarkerContent = {
5253
type: "verse",
5354
marker: "v",
5455
number: `${nextVerse}`,
55-
sid: `${book} ${chapterNum}:${nextVerse}`,
5656
};
5757
return [content];
5858
},
@@ -110,7 +110,7 @@ export function getUsjMarkerAction(
110110
if (!content) return;
111111

112112
const serializedLexicalNode = createLexicalUsjNode(content, usjEditorAdaptor, viewOptions);
113-
const usjNode = $createNodeFromSerializedNode(serializedLexicalNode);
113+
const nodeToInsert = $createNodeFromSerializedNode(serializedLexicalNode);
114114

115115
const selection = $getSelection();
116116
if ($isRangeSelection(selection)) {
@@ -119,22 +119,23 @@ export function getUsjMarkerAction(
119119
$wrapTextSelectionInInlineNode(selection, () =>
120120
$createNodeFromSerializedNode(serializedLexicalNode),
121121
);
122-
} else if ($isElementNode(usjNode) && !usjNode.isInline()) {
122+
} else if ($isElementNode(nodeToInsert) && !nodeToInsert.isInline()) {
123123
// If the selection is empty, insert a new paragraph and replace it with the USJ node
124124
const paragraph = selection.insertParagraph();
125125
if (paragraph) {
126126
// Transfer the content of the paragraph to the USJ node
127127
const paragraphContent = paragraph.getChildren();
128-
usjNode.append(...paragraphContent);
129-
paragraph.replace(usjNode);
130-
usjNode.selectStart();
128+
nodeToInsert.append(...paragraphContent);
129+
paragraph.replace(nodeToInsert);
130+
nodeToInsert.selectStart();
131131
}
132132
} else {
133-
selection.insertNodes([usjNode]);
133+
const nodes = $addVerseLeadingSpaceIfNeeded(selection, nodeToInsert);
134+
selection.insertNodes(nodes);
134135
}
135136
} else {
136-
// Insert the USJ node directly
137-
selection?.insertNodes([usjNode]);
137+
// Insert the node directly
138+
selection?.insertNodes([nodeToInsert]);
138139
}
139140
});
140141
};
@@ -193,7 +194,7 @@ function $wrapTextSelectionInInlineNode(
193194
}
194195

195196
// Get the target node to wrap
196-
const targetNode = getTargetNode(
197+
const targetNode = $getTargetNode(
197198
node,
198199
index === 0,
199200
index === nodes.length - 1,
@@ -213,7 +214,7 @@ function $wrapTextSelectionInInlineNode(
213214
}
214215

215216
// Wrap the target node
216-
wrapNode(targetNode, currentWrapper);
217+
$wrapNode(targetNode, currentWrapper);
217218
});
218219

219220
// Update selection
@@ -235,7 +236,7 @@ function getSelectionOffsets(selection: RangeSelection): [number, number] {
235236
return selection.isBackward() ? [focusOffset, anchorOffset] : [anchorOffset, focusOffset];
236237
}
237238

238-
function getTargetNode(
239+
function $getTargetNode(
239240
node: LexicalNode,
240241
isFirst: boolean,
241242
isLast: boolean,
@@ -282,7 +283,7 @@ function handleTextNode(
282283
return splitNodes.length === 3 || isFirst || end === textLength ? splitNodes[1] : splitNodes[0];
283284
}
284285

285-
function wrapNode(node: LexicalNode, wrapper: LexicalNode): void {
286+
function $wrapNode(node: LexicalNode, wrapper: LexicalNode): void {
286287
if ($isTextNode(wrapper)) {
287288
wrapper.setTextContent(node.getTextContent());
288289
node.remove();
@@ -293,3 +294,33 @@ function wrapNode(node: LexicalNode, wrapper: LexicalNode): void {
293294
}
294295

295296
// #endregion
297+
298+
/**
299+
* Adds a leading space before a verse node if needed.
300+
*
301+
* This function checks if the given selection is collapsed and if the node to be inserted is a
302+
* verse node or an immutable verse node. If both conditions are met, it further checks if the
303+
* anchor node of the selection is a text node and if there is no leading space before the insertion
304+
* point. If there is no leading space, it prepends a space to the node to be inserted.
305+
*
306+
* @param selection - The current selection range in the editor.
307+
* @param nodeToInsert - The node that is to be inserted into the editor.
308+
* @returns An array containing the nodes to be inserted, potentially with a leading space node.
309+
*/
310+
function $addVerseLeadingSpaceIfNeeded(
311+
selection: RangeSelection,
312+
nodeToInsert: LexicalNode,
313+
): LexicalNode[] {
314+
if (!selection.isCollapsed()) return [nodeToInsert];
315+
if (!$isSomeVerseNode(nodeToInsert)) return [nodeToInsert];
316+
317+
const anchorNode = selection.anchor.getNode();
318+
if (!$isTextNode(anchorNode)) return [nodeToInsert];
319+
320+
const offset = selection.anchor.offset;
321+
const textContent = anchorNode.getTextContent();
322+
const hasLeadingSpace = textContent[offset - 1] === " ";
323+
if (hasLeadingSpace) return [nodeToInsert];
324+
325+
return [$createTextNode(" "), nodeToInsert];
326+
}

packages/shared-react/nodes/scripture/usj/node-react.utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import { $isVerseNode, VerseNode } from "shared/nodes/scripture/usj/VerseNode";
1111
import { $isImmutableNoteCallerNode, ImmutableNoteCallerNode } from "./ImmutableNoteCallerNode";
1212
import { $isImmutableVerseNode, ImmutableVerseNode } from "./ImmutableVerseNode";
1313

14-
/** Caller count is in an object so it can be manipulated by passing the the object. */
14+
/** Caller count is in an object so it can be manipulated by passing the object. */
1515
export type CallerData = {
1616
count: number;
1717
};
1818

1919
// If you want use these utils with your own verse node, add it to this list of types, then modify
20-
// all the functions where this type is used.
21-
type SomeVerseNode = VerseNode | ImmutableVerseNode;
20+
// all the functions where this type is used in this file.
21+
export type SomeVerseNode = VerseNode | ImmutableVerseNode;
2222

2323
/**
2424
* Generate the note caller to use. E.g. for '+' replace with a-z, aa-zz.

0 commit comments

Comments
 (0)