Skip to content

Commit 90a3cee

Browse files
authored
add UnmatchedNode (#154)
- displays an invalid unmatched node to the user, usually a closing marker without a corresponding opening marker
1 parent d2b1767 commit 90a3cee

File tree

6 files changed

+230
-35
lines changed

6 files changed

+230
-35
lines changed

packages/platform/src/editor/adaptors/editor-usj.adaptor.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ import {
1717
SerializedTypedMarkNode,
1818
TypedMarkNode,
1919
} from "shared/nodes/features/TypedMarkNode";
20-
import {
21-
NBSP,
22-
getEditableCallerText,
23-
parseNumberFromMarkerText,
24-
removeEndingZwsp,
25-
removeUndefinedProperties,
26-
} from "shared/nodes/scripture/usj/node.utils";
2720
import { BookNode, SerializedBookNode } from "shared/nodes/scripture/usj/BookNode";
2821
import { ChapterNode, SerializedChapterNode } from "shared/nodes/scripture/usj/ChapterNode";
2922
import { CharNode, SerializedCharNode } from "shared/nodes/scripture/usj/CharNode";
@@ -32,6 +25,11 @@ import {
3225
SerializedImmutableChapterNode,
3326
} from "shared/nodes/scripture/usj/ImmutableChapterNode";
3427
import { ImmutableNoteCallerNode } from "shared-react/nodes/scripture/usj/ImmutableNoteCallerNode";
28+
import {
29+
ImmutableUnmatchedNode,
30+
SerializedImmutableUnmatchedNode,
31+
UNMATCHED_TAG_NAME,
32+
} from "shared/nodes/scripture/usj/ImmutableUnmatchedNode";
3533
import {
3634
ImmutableVerseNode,
3735
SerializedImmutableVerseNode,
@@ -52,6 +50,13 @@ import { NoteNode, SerializedNoteNode } from "shared/nodes/scripture/usj/NoteNod
5250
import { ParaNode, SerializedParaNode } from "shared/nodes/scripture/usj/ParaNode";
5351
import { SerializedUnknownNode, UnknownNode } from "shared/nodes/scripture/usj/UnknownNode";
5452
import { SerializedVerseNode, VerseNode } from "shared/nodes/scripture/usj/VerseNode";
53+
import {
54+
NBSP,
55+
getEditableCallerText,
56+
parseNumberFromMarkerText,
57+
removeEndingZwsp,
58+
removeUndefinedProperties,
59+
} from "shared/nodes/scripture/usj/node.utils";
5560
import { LoggerBasic } from "shared-react/plugins/logger-basic.model";
5661

5762
interface EditorUsjAdaptor {
@@ -222,6 +227,14 @@ function createUnknownMarker(
222227
});
223228
}
224229

230+
function createUnmatchedMarker(node: SerializedImmutableUnmatchedNode): MarkerObject {
231+
const { marker } = node;
232+
return {
233+
type: UNMATCHED_TAG_NAME,
234+
marker,
235+
};
236+
}
237+
225238
/**
226239
* If the last added content is text then combine the new text content to it, otherwise add the new
227240
* text content.
@@ -395,6 +408,9 @@ function recurseNodes(
395408
createUnknownMarker(serializedUnknownNode, recurseNodes(serializedUnknownNode.children)),
396409
);
397410
break;
411+
case ImmutableUnmatchedNode.getType():
412+
markers.push(createUnmatchedMarker(node as SerializedImmutableUnmatchedNode));
413+
break;
398414
default:
399415
_logger?.error(`Unexpected node type '${node.type}'!`);
400416
}

packages/platform/src/editor/adaptors/usj-editor.adaptor.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ import {
2626
BookNode,
2727
SerializedBookNode,
2828
} from "shared/nodes/scripture/usj/BookNode";
29-
import {
30-
SerializedImmutableChapterNode,
31-
IMMUTABLE_CHAPTER_VERSION,
32-
ImmutableChapterNode,
33-
} from "shared/nodes/scripture/usj/ImmutableChapterNode";
3429
import {
3530
SerializedChapterNode,
3631
CHAPTER_VERSION,
@@ -39,6 +34,33 @@ import {
3934
ChapterMarker,
4035
} from "shared/nodes/scripture/usj/ChapterNode";
4136
import { CHAR_VERSION, CharNode, SerializedCharNode } from "shared/nodes/scripture/usj/CharNode";
37+
import {
38+
SerializedImmutableChapterNode,
39+
IMMUTABLE_CHAPTER_VERSION,
40+
ImmutableChapterNode,
41+
} from "shared/nodes/scripture/usj/ImmutableChapterNode";
42+
import {
43+
IMMUTABLE_NOTE_CALLER_VERSION,
44+
ImmutableNoteCallerNode,
45+
OnClick,
46+
SerializedImmutableNoteCallerNode,
47+
immutableNoteCallerNodeName,
48+
} from "shared-react/nodes/scripture/usj/ImmutableNoteCallerNode";
49+
import {
50+
IMMUTABLE_UNMATCHED_VERSION,
51+
ImmutableUnmatchedNode,
52+
SerializedImmutableUnmatchedNode,
53+
} from "shared/nodes/scripture/usj/ImmutableUnmatchedNode";
54+
import {
55+
SerializedImmutableVerseNode,
56+
IMMUTABLE_VERSE_VERSION,
57+
ImmutableVerseNode,
58+
} from "shared/nodes/scripture/usj/ImmutableVerseNode";
59+
import {
60+
IMPLIED_PARA_VERSION,
61+
ImpliedParaNode,
62+
SerializedImpliedParaNode,
63+
} from "shared/nodes/scripture/usj/ImpliedParaNode";
4264
import {
4365
MILESTONE_VERSION,
4466
MilestoneNode,
@@ -48,35 +70,18 @@ import {
4870
ENDING_MS_COMMENT_MARKER,
4971
isMilestoneCommentMarker,
5072
} from "shared/nodes/scripture/usj/MilestoneNode";
51-
import {
52-
IMPLIED_PARA_VERSION,
53-
ImpliedParaNode,
54-
SerializedImpliedParaNode,
55-
} from "shared/nodes/scripture/usj/ImpliedParaNode";
56-
import {
57-
PARA_MARKER_DEFAULT,
58-
PARA_VERSION,
59-
ParaNode,
60-
SerializedParaNode,
61-
} from "shared/nodes/scripture/usj/ParaNode";
6273
import {
6374
NOTE_VERSION,
6475
NoteNode,
6576
NoteMarker,
6677
SerializedNoteNode,
6778
} from "shared/nodes/scripture/usj/NoteNode";
6879
import {
69-
IMMUTABLE_NOTE_CALLER_VERSION,
70-
ImmutableNoteCallerNode,
71-
OnClick,
72-
SerializedImmutableNoteCallerNode,
73-
immutableNoteCallerNodeName,
74-
} from "shared-react/nodes/scripture/usj/ImmutableNoteCallerNode";
75-
import {
76-
SerializedImmutableVerseNode,
77-
IMMUTABLE_VERSE_VERSION,
78-
ImmutableVerseNode,
79-
} from "shared/nodes/scripture/usj/ImmutableVerseNode";
80+
PARA_MARKER_DEFAULT,
81+
PARA_VERSION,
82+
ParaNode,
83+
SerializedParaNode,
84+
} from "shared/nodes/scripture/usj/ParaNode";
8085
import {
8186
SerializedUnknownNode,
8287
UNKNOWN_VERSION,
@@ -500,6 +505,14 @@ function createUnknown(
500505
});
501506
}
502507

508+
function createUnmatched(marker: string): SerializedImmutableUnmatchedNode {
509+
return {
510+
type: ImmutableUnmatchedNode.getType(),
511+
marker,
512+
version: IMMUTABLE_UNMATCHED_VERSION,
513+
};
514+
}
515+
503516
function createMarker(marker: string, isOpening = true): SerializedMarkerNode {
504517
return {
505518
type: MarkerNode.getType(),
@@ -644,6 +657,9 @@ function recurseNodes(markers: MarkerContent[] | undefined): SerializedLexicalNo
644657
}
645658
nodes.push(createMilestone(markerContent));
646659
break;
660+
case ImmutableUnmatchedNode.getType():
661+
nodes.push(createUnmatched(markerContent.marker));
662+
break;
647663
default:
648664
_logger?.warn(`Unknown type-marker '${markerContent.type}-${markerContent.marker}'!`);
649665
nodes.push(createUnknown(markerContent, recurseNodes(markerContent.content)));
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {
2+
type LexicalNode,
3+
type NodeKey,
4+
$applyNodeReplacement,
5+
DecoratorNode,
6+
SerializedLexicalNode,
7+
Spread,
8+
DOMConversionMap,
9+
LexicalEditor,
10+
DOMExportOutput,
11+
isHTMLElement,
12+
DOMConversionOutput,
13+
} from "lexical";
14+
import { INVALID_CLASS_NAME, ZWSP } from "./node.utils";
15+
16+
export const UNMATCHED_TAG_NAME = "unmatched";
17+
export const IMMUTABLE_UNMATCHED_VERSION = 1;
18+
19+
export type SerializedImmutableUnmatchedNode = Spread<
20+
{
21+
marker: string;
22+
},
23+
SerializedLexicalNode
24+
>;
25+
26+
export class ImmutableUnmatchedNode extends DecoratorNode<void> {
27+
__marker: string;
28+
29+
constructor(marker: string, key?: NodeKey) {
30+
super(key);
31+
this.__marker = marker;
32+
}
33+
34+
static getType(): string {
35+
return "unmatched";
36+
}
37+
38+
static clone(node: ImmutableUnmatchedNode): ImmutableUnmatchedNode {
39+
const { __marker, __key } = node;
40+
return new ImmutableUnmatchedNode(__marker, __key);
41+
}
42+
43+
static importJSON(serializedNode: SerializedImmutableUnmatchedNode): ImmutableUnmatchedNode {
44+
const { marker } = serializedNode;
45+
const node = $createImmutableUnmatchedNode(marker);
46+
return node;
47+
}
48+
49+
static importDOM(): DOMConversionMap | null {
50+
return {
51+
[UNMATCHED_TAG_NAME]: (node: HTMLElement) => {
52+
if (!isUnmatchedElement(node)) return null;
53+
54+
return {
55+
conversion: $convertImmutableUnmatchedElement,
56+
priority: 1,
57+
};
58+
},
59+
};
60+
}
61+
62+
setMarker(marker: string): void {
63+
const self = this.getWritable();
64+
self.__marker = marker;
65+
}
66+
67+
getMarker(): string {
68+
const self = this.getLatest();
69+
return self.__marker;
70+
}
71+
72+
createDOM(): HTMLElement {
73+
const dom = document.createElement(UNMATCHED_TAG_NAME);
74+
dom.setAttribute("data-marker", this.__marker);
75+
dom.classList.add(INVALID_CLASS_NAME);
76+
const isClosing = this.__marker.endsWith("*");
77+
dom.title = isClosing
78+
? `This closing marker has no matching opening marker!`
79+
: `This opening marker has no matching closing marker!`;
80+
return dom;
81+
}
82+
83+
updateDOM(): boolean {
84+
// Returning false tells Lexical that this node does not need its
85+
// DOM element replacing with a new copy from createDOM.
86+
return false;
87+
}
88+
89+
exportDOM(editor: LexicalEditor): DOMExportOutput {
90+
const { element } = super.exportDOM(editor);
91+
if (element && isHTMLElement(element)) {
92+
element.setAttribute("data-marker", this.getMarker());
93+
element.classList.add(INVALID_CLASS_NAME);
94+
}
95+
96+
return { element };
97+
}
98+
99+
decorate(): string {
100+
return `\\${this.getMarker()}${ZWSP}`;
101+
}
102+
103+
exportJSON(): SerializedImmutableUnmatchedNode {
104+
return {
105+
type: this.getType(),
106+
marker: this.getMarker(),
107+
version: IMMUTABLE_UNMATCHED_VERSION,
108+
};
109+
}
110+
}
111+
112+
function $convertImmutableUnmatchedElement(element: HTMLElement): DOMConversionOutput {
113+
const marker = element.getAttribute("data-marker") ?? "";
114+
const node = $createImmutableUnmatchedNode(marker);
115+
return { node };
116+
}
117+
118+
export function $createImmutableUnmatchedNode(marker: string): ImmutableUnmatchedNode {
119+
return $applyNodeReplacement(new ImmutableUnmatchedNode(marker));
120+
}
121+
122+
function isUnmatchedElement(node: HTMLElement | null | undefined): boolean {
123+
return node?.tagName === UNMATCHED_TAG_NAME;
124+
}
125+
126+
export function $isImmutableUnmatchedNode(
127+
node: LexicalNode | null | undefined,
128+
): node is ImmutableUnmatchedNode {
129+
return node instanceof ImmutableUnmatchedNode;
130+
}

packages/shared/nodes/scripture/usj/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { MarkerNode } from "./MarkerNode";
1111
import { ImpliedParaNode } from "./ImpliedParaNode";
1212
import { ParaNode } from "./ParaNode";
1313
import { UnknownNode } from "./UnknownNode";
14+
import { ImmutableUnmatchedNode } from "./ImmutableUnmatchedNode";
1415

1516
const scriptureUsjNodes = [
1617
BookNode,
@@ -23,6 +24,7 @@ const scriptureUsjNodes = [
2324
MilestoneNode,
2425
MarkerNode,
2526
UnknownNode,
27+
ImmutableUnmatchedNode,
2628
ImpliedParaNode,
2729
ParaNode,
2830
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const ZWSP = "\u200B";
3636

3737
export const CHAPTER_CLASS_NAME = "chapter";
3838
export const VERSE_CLASS_NAME = "verse";
39+
export const INVALID_CLASS_NAME = "invalid";
3940
export const TEXT_SPACING_CLASS_NAME = "text-spacing";
4041
export const FORMATTED_FONT_CLASS_NAME = "formatted-font";
4142

packages/utilities/src/converters/usj/converter-test.data.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const usxGen1v1 = `
4545
<verse style="v" number="15" sid="GEN 1:15"/>Tell the Israelites that I, the <char style="nd">Lord</char>, the God of their ancestors, the God of Abraham, Isaac, and Jacob,<verse eid="GEN 1:15" />
4646
</para>
4747
<para style="b" />
48-
<para style="q2"><verse style="v" number="16" sid="GEN 1:16"/>“There is no help for him in God.”<note style="f" caller="+"><char style="fr">3:2 </char><char style="ft">The Hebrew word rendered “God” is “אֱלֹהִ֑ים” (Elohim).</char></note> <char style="qs">Selah.</char><verse eid="GEN 1:16" /></para>
48+
<para style="q2"><verse style="v" number="16" sid="GEN 1:16"/>“There is no help for him in God.”<note style="f" caller="+"><char style="fr">3:2 </char><char style="ft">The Hebrew word rendered “God” is “אֱלֹהִ֑ים” (Elohim).</char></note> <unmatched style="f*" /> <char style="qs">Selah.</char><verse eid="GEN 1:16" /></para>
4949
<chapter eid="GEN 1" />
5050
</usx>
5151
`;
@@ -113,6 +113,8 @@ export const usjGen1v1: Usj = {
113113
],
114114
},
115115
" ",
116+
{ type: "unmatched", marker: "f*" },
117+
" ",
116118
{ type: "char", marker: "qs", content: ["Selah."] },
117119
],
118120
},
@@ -302,6 +304,20 @@ export const editorStateGen1v1 = {
302304
style: "",
303305
version: 1,
304306
},
307+
{
308+
type: "unmatched",
309+
marker: "f*",
310+
version: 1,
311+
},
312+
{
313+
type: "text",
314+
text: " ",
315+
detail: 0,
316+
format: 0,
317+
mode: "normal",
318+
style: "",
319+
version: 1,
320+
},
305321
{
306322
type: "char",
307323
marker: "qs",
@@ -677,6 +693,20 @@ export const editorStateGen1v1Editable = {
677693
style: "",
678694
version: 1,
679695
},
696+
{
697+
type: "unmatched",
698+
marker: "f*",
699+
version: 1,
700+
},
701+
{
702+
type: "text",
703+
text: " ",
704+
detail: 0,
705+
format: 0,
706+
mode: "normal",
707+
style: "",
708+
version: 1,
709+
},
680710
{
681711
type: "marker",
682712
marker: "qs",

0 commit comments

Comments
 (0)