@liminis/editor 0.1.0-rc.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/LICENSE +34 -0
- package/README.md +376 -0
- package/dist/annotations/anchor-align.d.ts +50 -0
- package/dist/annotations/anchor-align.js +74 -0
- package/dist/annotations/anchor-model.d.ts +81 -0
- package/dist/annotations/anchor-model.js +80 -0
- package/dist/annotations/anchor-resolver.d.ts +92 -0
- package/dist/annotations/anchor-resolver.js +231 -0
- package/dist/annotations/block-structure.d.ts +65 -0
- package/dist/annotations/block-structure.js +113 -0
- package/dist/annotations/types.d.ts +159 -0
- package/dist/annotations/types.js +50 -0
- package/dist/annotations.d.ts +25 -0
- package/dist/annotations.js +27 -0
- package/dist/app/App.d.ts +64 -0
- package/dist/app/App.js +180 -0
- package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
- package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
- package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
- package/dist/app/editor/AnchorScrollPlugin.js +134 -0
- package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
- package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
- package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
- package/dist/app/editor/AnnotationPlugin.js +135 -0
- package/dist/app/editor/AnnotationSurface.d.ts +34 -0
- package/dist/app/editor/AnnotationSurface.js +115 -0
- package/dist/app/editor/AssetContext.d.ts +20 -0
- package/dist/app/editor/AssetContext.js +81 -0
- package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
- package/dist/app/editor/BlockClickPlugin.js +126 -0
- package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
- package/dist/app/editor/CodeBlockPlugin.js +219 -0
- package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
- package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
- package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
- package/dist/app/editor/DragHandlePlugin.js +300 -0
- package/dist/app/editor/Editor.d.ts +84 -0
- package/dist/app/editor/Editor.js +539 -0
- package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
- package/dist/app/editor/FrontmatterPlugin.js +301 -0
- package/dist/app/editor/ImageModal.d.ts +13 -0
- package/dist/app/editor/ImageModal.js +267 -0
- package/dist/app/editor/ImagePlugin.d.ts +3 -0
- package/dist/app/editor/ImagePlugin.js +187 -0
- package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
- package/dist/app/editor/LinkClickPlugin.js +90 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
- package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
- package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
- package/dist/app/editor/SearchPlugin.d.ts +1 -0
- package/dist/app/editor/SearchPlugin.js +379 -0
- package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
- package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
- package/dist/app/editor/SlashMenu.d.ts +11 -0
- package/dist/app/editor/SlashMenu.js +494 -0
- package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
- package/dist/app/editor/SlashMenuPlugin.js +168 -0
- package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
- package/dist/app/editor/TableActionsPlugin.js +398 -0
- package/dist/app/editor/TogglePlugin.d.ts +1 -0
- package/dist/app/editor/TogglePlugin.js +159 -0
- package/dist/app/editor/Toolbar.d.ts +10 -0
- package/dist/app/editor/Toolbar.js +263 -0
- package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
- package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
- package/dist/app/editor/annotation-marks.d.ts +177 -0
- package/dist/app/editor/annotation-marks.js +750 -0
- package/dist/app/editor/annotationCommands.d.ts +13 -0
- package/dist/app/editor/annotationCommands.js +11 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
- package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
- package/dist/app/editor/c4/edge-clipping.js +139 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
- package/dist/app/editor/c4/layout.d.ts +25 -0
- package/dist/app/editor/c4/layout.js +839 -0
- package/dist/app/editor/c4/parser.d.ts +19 -0
- package/dist/app/editor/c4/parser.js +410 -0
- package/dist/app/editor/c4/render-to-string.d.ts +24 -0
- package/dist/app/editor/c4/render-to-string.js +34 -0
- package/dist/app/editor/c4/renderer.d.ts +64 -0
- package/dist/app/editor/c4/renderer.js +569 -0
- package/dist/app/editor/c4/types.d.ts +203 -0
- package/dist/app/editor/c4/types.js +43 -0
- package/dist/app/editor/correction-yaml.d.ts +10 -0
- package/dist/app/editor/correction-yaml.js +40 -0
- package/dist/app/editor/editorNodes.d.ts +9 -0
- package/dist/app/editor/editorNodes.js +36 -0
- package/dist/app/editor/index.d.ts +1 -0
- package/dist/app/editor/index.js +1 -0
- package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
- package/dist/app/editor/mark-overlap-resolver.js +45 -0
- package/dist/app/editor/nodes/C4Component.d.ts +14 -0
- package/dist/app/editor/nodes/C4Component.js +284 -0
- package/dist/app/editor/nodes/C4Node.d.ts +32 -0
- package/dist/app/editor/nodes/C4Node.js +112 -0
- package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
- package/dist/app/editor/nodes/CalloutNode.js +94 -0
- package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
- package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
- package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
- package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
- package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
- package/dist/app/editor/nodes/CustomListNode.js +54 -0
- package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
- package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
- package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
- package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
- package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
- package/dist/app/editor/nodes/EquationComponent.js +130 -0
- package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
- package/dist/app/editor/nodes/EquationNode.js +179 -0
- package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
- package/dist/app/editor/nodes/FootnoteNode.js +151 -0
- package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
- package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
- package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
- package/dist/app/editor/nodes/HtmlNode.js +121 -0
- package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
- package/dist/app/editor/nodes/ImageComponent.js +228 -0
- package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
- package/dist/app/editor/nodes/ImageNode.js +183 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
- package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
- package/dist/app/editor/nodes/MermaidComponent.js +216 -0
- package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
- package/dist/app/editor/nodes/MermaidNode.js +96 -0
- package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
- package/dist/app/editor/nodes/ToggleNode.js +237 -0
- package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
- package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
- package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
- package/dist/app/editor/nodes/diagram-utils.js +29 -0
- package/dist/app/editor/nodes/index.d.ts +30 -0
- package/dist/app/editor/nodes/index.js +17 -0
- package/dist/app/editor/utils.d.ts +2 -0
- package/dist/app/editor/utils.js +17 -0
- package/dist/app/mapper/index.d.ts +3 -0
- package/dist/app/mapper/index.js +2 -0
- package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
- package/dist/app/mapper/lexicalToMdast.js +1937 -0
- package/dist/app/mapper/mdastToLexical.d.ts +31 -0
- package/dist/app/mapper/mdastToLexical.js +1075 -0
- package/dist/contract.d.ts +8 -0
- package/dist/contract.js +8 -0
- package/dist/headless.d.ts +40 -0
- package/dist/headless.js +45 -0
- package/dist/host/context.d.ts +20 -0
- package/dist/host/context.js +24 -0
- package/dist/host/defaults.d.ts +21 -0
- package/dist/host/defaults.js +47 -0
- package/dist/host/messages.d.ts +26 -0
- package/dist/host/messages.js +47 -0
- package/dist/host/types.d.ts +75 -0
- package/dist/host/types.js +13 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +51 -0
- package/dist/markdown/annotate-sentinels.d.ts +42 -0
- package/dist/markdown/annotate-sentinels.js +86 -0
- package/dist/markdown/parse.d.ts +56 -0
- package/dist/markdown/parse.js +516 -0
- package/dist/markdown/stringify.d.ts +7 -0
- package/dist/markdown/stringify.js +439 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
- package/dist/markdown.d.ts +28 -0
- package/dist/markdown.js +44 -0
- package/dist/mathjax-config.d.ts +89 -0
- package/dist/mathjax-config.js +133 -0
- package/dist/nodes.d.ts +34 -0
- package/dist/nodes.js +34 -0
- package/dist/stores/correctionStore.d.ts +15 -0
- package/dist/stores/correctionStore.js +12 -0
- package/dist/stores/editorStore.d.ts +16 -0
- package/dist/stores/editorStore.js +27 -0
- package/dist/styles.css +2477 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.js +91 -0
- package/dist/utils/file-types.d.ts +10 -0
- package/dist/utils/file-types.js +46 -0
- package/docs/annotations.md +134 -0
- package/docs/decisions/README.md +72 -0
- package/docs/decisions/adr-002.md +27 -0
- package/docs/decisions/adr-007.md +27 -0
- package/docs/decisions/adr-010.md +26 -0
- package/docs/decisions/adr-024.md +124 -0
- package/docs/decisions/adr-024b.md +0 -0
- package/docs/decisions/adr-025.md +103 -0
- package/docs/decisions/adr-027.md +66 -0
- package/docs/decisions/adr-057.md +67 -0
- package/docs/decisions/adr-070.md +131 -0
- package/docs/decisions/adr-075.md +410 -0
- package/docs/decisions/adr-076.md +183 -0
- package/docs/decisions/adr-077.md +410 -0
- package/docs/decisions/adr-078.md +243 -0
- package/docs/decisions/adr-079.md +167 -0
- package/docs/decisions/adr-080.md +325 -0
- package/docs/decisions/adr-081.md +108 -0
- package/docs/decisions/adr-082.md +123 -0
- package/docs/decisions/adr-083.md +204 -0
- package/docs/decisions/adr-084.md +179 -0
- package/docs/editor-api.md +106 -0
- package/docs/markdown-pipeline.md +282 -0
- package/docs/provenance.md +106 -0
- package/docs/zusammen-editor-capability-map.md +232 -0
- package/package.json +175 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import { COMMAND_PRIORITY_CRITICAL } from 'lexical';
|
|
4
|
+
import { readAnchorFields, removeMarksForAnnotation, removeMarksForAnnotations, wrapNativeRangeInMark, } from './annotation-marks.js';
|
|
5
|
+
import { OPEN_ANNOTATION_COMPOSER_COMMAND } from './annotationCommands.js';
|
|
6
|
+
/**
|
|
7
|
+
* Turns a user selection into a captured anchor for a configured kind.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately headless: it renders no composer of its own. Whether a comment
|
|
10
|
+
* gets a textarea or a correction gets a panel is host UX, so this plugin's
|
|
11
|
+
* whole job is to listen for the create command, run the shared capture
|
|
12
|
+
* primitive, and hand the anchor out. Both kinds take the same path here —
|
|
13
|
+
* only `retainMarkOnCreate` differs, which is what SC-001 rests on.
|
|
14
|
+
*
|
|
15
|
+
* Not gated on `editable`: annotating is decoupled from editing, and a
|
|
16
|
+
* read-only document is a legitimate place to comment.
|
|
17
|
+
*/
|
|
18
|
+
export function AnnotationPlugin({ kinds, onCreateAnnotation, logger, wikiLinkPromotion }) {
|
|
19
|
+
const [editor] = useLexicalComposerContext();
|
|
20
|
+
const mintId = useCallback(() => {
|
|
21
|
+
// crypto.randomUUID is unavailable in some non-secure contexts (an
|
|
22
|
+
// unpackaged Electron or file:// window); the id only has to be unique
|
|
23
|
+
// within this document's live marks, so a fallback is fine.
|
|
24
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
25
|
+
return crypto.randomUUID();
|
|
26
|
+
}
|
|
27
|
+
// A timestamp alone is not enough: `performance.now()` is coarsened for
|
|
28
|
+
// fingerprinting protection, so two creates in quick succession can land on
|
|
29
|
+
// the same value. Ids gate mark placement and removal via `hasID`/`deleteID`,
|
|
30
|
+
// so a collision would merge two annotations onto one live mark and let
|
|
31
|
+
// removing either delete the other's anchor. The random suffix makes that
|
|
32
|
+
// effectively impossible without needing a real UUID source.
|
|
33
|
+
const stamp = Math.floor(performance.now() * 1000).toString(36);
|
|
34
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
35
|
+
return `anno-${stamp}-${random}-${globalThis.isSecureContext ? 's' : 'u'}`;
|
|
36
|
+
}, []);
|
|
37
|
+
// Marks placed by a create whose anchor read hasn't run yet. The wrap is
|
|
38
|
+
// synchronous but the read-back is deferred, so between the two the mark
|
|
39
|
+
// exists in the document while its id exists nowhere else — the host has not
|
|
40
|
+
// been told, and `AnnotationMarkerPlugin` walks only host-supplied
|
|
41
|
+
// annotations. Dropping one there would strand a bare `<mark>` the user
|
|
42
|
+
// cannot dismiss until the document is reparsed (review finding,
|
|
43
|
+
// @handarbeit-pruefer). The teardown effect below retracts whatever is left.
|
|
44
|
+
//
|
|
45
|
+
// Held in a ref, not an effect-local, so it survives a re-run of the
|
|
46
|
+
// command-registration effect: a create in flight belongs to the plugin, not
|
|
47
|
+
// to the effect run that happened to register the handler.
|
|
48
|
+
const pendingIdsRef = useRef(new Set());
|
|
49
|
+
// Cancellation is a property of the plugin's *lifetime*, not of one effect
|
|
50
|
+
// run. Scoping it per-run would make correctness depend on React flushing the
|
|
51
|
+
// cleanup after the pending microtask: that holds on the default async render
|
|
52
|
+
// path (verified by test), but a synchronous re-render — `flushSync`, or a
|
|
53
|
+
// discrete input forcing one — runs the cleanup first, and a create in flight
|
|
54
|
+
// would be silently dropped with the host never told (review finding,
|
|
55
|
+
// CodeRabbit). Tying the flag to unmount makes the ordering irrelevant.
|
|
56
|
+
const unmountedRef = useRef(false);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
unmountedRef.current = false;
|
|
59
|
+
const pendingIds = pendingIdsRef.current;
|
|
60
|
+
return () => {
|
|
61
|
+
// Runs only on unmount (`editor` is stable for the composer's lifetime),
|
|
62
|
+
// which is exactly the case where a deferred read must not proceed: it
|
|
63
|
+
// would touch a torn-down editor and call the host back with a stale rect
|
|
64
|
+
// and a stale closure.
|
|
65
|
+
unmountedRef.current = true;
|
|
66
|
+
// Retract regardless of the kind's `retainMarkOnCreate`: a retained mark
|
|
67
|
+
// is only safe to leave behind because the host owns it from the moment
|
|
68
|
+
// `onCreateAnnotation` fires, and that never happened for these.
|
|
69
|
+
//
|
|
70
|
+
// Safe to touch the editor here: this cleanup runs as ordinary React
|
|
71
|
+
// work, not inside the Lexical update the command handler ran in, so the
|
|
72
|
+
// queued wrap has already been applied by the time we get here.
|
|
73
|
+
if (pendingIds.size > 0) {
|
|
74
|
+
removeMarksForAnnotations(editor, [...pendingIds]);
|
|
75
|
+
pendingIds.clear();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}, [editor]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const pendingIds = pendingIdsRef.current;
|
|
81
|
+
const unregister = editor.registerCommand(OPEN_ANNOTATION_COMPOSER_COMMAND, ({ kind }) => {
|
|
82
|
+
const config = kinds[kind];
|
|
83
|
+
if (!config) {
|
|
84
|
+
// An unconfigured kind is a host wiring mistake, not a crash (edge
|
|
85
|
+
// case in the spec): warn through the injected logger and decline.
|
|
86
|
+
logger?.warn(`[annotations] ignoring create for unconfigured kind "${kind}"`);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (!config.createAffordance) {
|
|
90
|
+
logger?.warn(`[annotations] kind "${kind}" is host-injected only; no create affordance`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Read the on-screen rect while the selection is still live and before
|
|
94
|
+
// wrapping mutates the DOM around it.
|
|
95
|
+
const nativeSelection = window.getSelection();
|
|
96
|
+
if (!nativeSelection || nativeSelection.rangeCount === 0 || nativeSelection.isCollapsed)
|
|
97
|
+
return false;
|
|
98
|
+
const nativeRange = nativeSelection.getRangeAt(0);
|
|
99
|
+
const rect = nativeRange.getBoundingClientRect();
|
|
100
|
+
const id = mintId();
|
|
101
|
+
const retainMark = config.retainMarkOnCreate ?? false;
|
|
102
|
+
// Wrap now, while the native range still points at live DOM — but read
|
|
103
|
+
// the anchor back on a microtask rather than inline.
|
|
104
|
+
//
|
|
105
|
+
// A command handler runs *inside* an active Lexical update, so the
|
|
106
|
+
// update `wrapNativeRangeInMark` performs is nested: Lexical queues it
|
|
107
|
+
// and applies it when the outer update completes, which means its
|
|
108
|
+
// result is not observable synchronously here. Reading the anchor
|
|
109
|
+
// inline would read the pre-mark state and always come back null. The
|
|
110
|
+
// microtask runs after the outer update has flushed, so the mark is
|
|
111
|
+
// really in the tree by then.
|
|
112
|
+
wrapNativeRangeInMark(editor, nativeRange, id);
|
|
113
|
+
// The mark is now in the tree but its id has not been reported to the
|
|
114
|
+
// host yet, so nothing outside this plugin can clean it up. Tracked
|
|
115
|
+
// until the microtask hands it over (or the cleanup retracts it).
|
|
116
|
+
pendingIds.add(id);
|
|
117
|
+
queueMicrotask(() => {
|
|
118
|
+
if (unmountedRef.current)
|
|
119
|
+
return;
|
|
120
|
+
pendingIds.delete(id);
|
|
121
|
+
const anchor = readAnchorFields(editor, id, { wikiLinkPromotion });
|
|
122
|
+
// Comments keep the mark (it is their live anchor and the composer's
|
|
123
|
+
// highlight); corrections discard it so nothing ever paints.
|
|
124
|
+
if (!retainMark)
|
|
125
|
+
removeMarksForAnnotation(editor, id);
|
|
126
|
+
onCreateAnnotation({ kind, id, anchor, rect });
|
|
127
|
+
});
|
|
128
|
+
return true;
|
|
129
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
130
|
+
// Only the registration is scoped to this effect run; cancellation and the
|
|
131
|
+
// pending-mark retraction live in the teardown effect above.
|
|
132
|
+
return unregister;
|
|
133
|
+
}, [editor, kinds, onCreateAnnotation, logger, mintId, wikiLinkPromotion]);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MutableRefObject } from 'react';
|
|
2
|
+
import { type Annotation, type AnnotationEditorHandle, type AnnotationKindConfigs } from '../../annotations/types.js';
|
|
3
|
+
import type { OffsetSpan } from '../mapper/mdastToLexical.js';
|
|
4
|
+
import type { WikiLinkPromotionMode } from '../mapper/lexicalToMdast.js';
|
|
5
|
+
import { type AnnotationCreateEvent } from './AnnotationPlugin.js';
|
|
6
|
+
export interface AnnotationSurfaceProps {
|
|
7
|
+
kinds: AnnotationKindConfigs;
|
|
8
|
+
annotations: Annotation[];
|
|
9
|
+
activeAnnotationId: string | null;
|
|
10
|
+
scrollToAnnotation?: {
|
|
11
|
+
id: string;
|
|
12
|
+
nonce: number;
|
|
13
|
+
} | null;
|
|
14
|
+
onCreateAnnotation?: (event: AnnotationCreateEvent) => void;
|
|
15
|
+
onActivateAnnotation?: (id: string) => void;
|
|
16
|
+
editorHandleRef?: MutableRefObject<AnnotationEditorHandle | null>;
|
|
17
|
+
offsetSpansRef: MutableRefObject<OffsetSpan[]>;
|
|
18
|
+
markdownTextRef: MutableRefObject<string>;
|
|
19
|
+
offsetsVersion: number;
|
|
20
|
+
logger?: {
|
|
21
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
22
|
+
};
|
|
23
|
+
/** Forwarded to the anchor-capture export pass — see `Editor`'s own prop of the same name. */
|
|
24
|
+
wikiLinkPromotion?: WikiLinkPromotionMode;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The entire annotation feature surface, behind one lazily-imported boundary.
|
|
28
|
+
*
|
|
29
|
+
* Nothing here is reachable from `Editor.tsx` by static import: the editor
|
|
30
|
+
* loads this module only when a host actually configures annotation kinds
|
|
31
|
+
* (FR-004/SC-004). That is why the create plugin, marker plugin, placement and
|
|
32
|
+
* editor handle are assembled here rather than mounted individually.
|
|
33
|
+
*/
|
|
34
|
+
export default function AnnotationSurface({ kinds, annotations, activeAnnotationId, scrollToAnnotation, onCreateAnnotation, onActivateAnnotation, editorHandleRef, offsetSpansRef, markdownTextRef, offsetsVersion, logger, wikiLinkPromotion, }: AnnotationSurfaceProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
+
import { deriveMarkerTargets, } from '../../annotations/types.js';
|
|
5
|
+
import { collectLiveAnchorSnapshots, placeMarksForAnchors, removeMarksForAnnotation, removeMarksForAnnotations, } from './annotation-marks.js';
|
|
6
|
+
import { AnnotationPlugin } from './AnnotationPlugin.js';
|
|
7
|
+
import { AnnotationMarkerPlugin } from './AnnotationMarkerPlugin.js';
|
|
8
|
+
import { registerMarkOverlapResolver } from './mark-overlap-resolver.js';
|
|
9
|
+
/**
|
|
10
|
+
* Places a live `MarkNode` for every marker target — the set `deriveMarkerTargets`
|
|
11
|
+
* already narrowed by each kind's live-mark policy.
|
|
12
|
+
*
|
|
13
|
+
* This is also where the spec's "document edited mid-resolution" edge case is
|
|
14
|
+
* handled, and it needs no dedicated guard: `placeMarkForAnchor` re-verifies
|
|
15
|
+
* the anchor's target text against the *current* offset spans and returns
|
|
16
|
+
* false rather than placing on unrelated text. A stale anchor therefore
|
|
17
|
+
* no-ops, and the next parse bumps `offsetsVersion`, which re-runs this pass
|
|
18
|
+
* against fresh spans. Placement is never applied on the strength of an
|
|
19
|
+
* out-of-date resolution.
|
|
20
|
+
*/
|
|
21
|
+
function AnnotationMarkPlacementPlugin({ offsetSpansRef, markdownTextRef, targets, offsetsVersion, }) {
|
|
22
|
+
const [editor] = useLexicalComposerContext();
|
|
23
|
+
// Ids this plugin has placed a mark for. Retracting a mark is keyed off this
|
|
24
|
+
// rather than off "every mark in the document" on purpose: a comment created
|
|
25
|
+
// through `AnnotationPlugin` with `retainMarkOnCreate` keeps its mark while
|
|
26
|
+
// the host is still deciding whether to persist it, and that mark is not
|
|
27
|
+
// ours to remove.
|
|
28
|
+
const placedRef = useRef(new Set());
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// No outcome filter here: `deriveMarkerTargets` has already applied each
|
|
31
|
+
// kind's `livemarkPolicy` (defaulting to `shouldPlaceLiveMark` over the
|
|
32
|
+
// outcome). Re-testing the outcome would let the policy only ever subtract
|
|
33
|
+
// — a host that deliberately opts a `flagged` annotation into a live mark
|
|
34
|
+
// would get a marker target the marker plugin then decorates but nothing
|
|
35
|
+
// ever places, which is the one inconsistency the single policy exists to
|
|
36
|
+
// prevent.
|
|
37
|
+
const eligible = new Set(targets.map((target) => target.annotationId));
|
|
38
|
+
// Retract first: an annotation the host removed, or whose outcome or
|
|
39
|
+
// live-mark policy now declines it, must lose the mark it was given.
|
|
40
|
+
// Without this the MarkNode survives every later pass — still in the
|
|
41
|
+
// document, still rendering as a bare <mark>, and no longer decorated by
|
|
42
|
+
// the marker plugin (which only walks current targets), so it becomes an
|
|
43
|
+
// undismissable highlight with nothing behind it.
|
|
44
|
+
// Batched for the same reason placement is: a host dropping a set of live
|
|
45
|
+
// annotations at once would otherwise pay one synchronous Lexical
|
|
46
|
+
// reconciliation per id (review finding, @handarbeit-pruefer).
|
|
47
|
+
const retract = [...placedRef.current].filter((id) => !eligible.has(id));
|
|
48
|
+
if (retract.length > 0) {
|
|
49
|
+
removeMarksForAnnotations(editor, retract);
|
|
50
|
+
for (const id of retract)
|
|
51
|
+
placedRef.current.delete(id);
|
|
52
|
+
}
|
|
53
|
+
// One batched update for the whole set, not one per annotation: an
|
|
54
|
+
// `offsetsVersion` bump re-runs this pass over every target, and a
|
|
55
|
+
// per-target `editor.update()` would force a synchronous Lexical
|
|
56
|
+
// reconciliation each time (review finding, @handarbeit-pruefer).
|
|
57
|
+
const placed = placeMarksForAnchors(editor, offsetSpansRef.current, markdownTextRef.current, targets.map((target) => ({ anchor: target.anchor, id: target.annotationId })));
|
|
58
|
+
for (const id of placed)
|
|
59
|
+
placedRef.current.add(id);
|
|
60
|
+
// The two refs are read fresh on every run; offsetsVersion (bumped whenever
|
|
61
|
+
// a (re)parse produces new spans) is what should actually re-trigger this.
|
|
62
|
+
}, [editor, targets, offsetsVersion, offsetSpansRef, markdownTextRef]);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Keeps overlapping annotations on *shared* MarkNodes rather than nested ones
|
|
67
|
+
* — see `mark-overlap-resolver.ts`. Mounted here because it is only meaningful
|
|
68
|
+
* once annotation kinds are configured, and this whole module is lazily loaded
|
|
69
|
+
* on exactly that condition.
|
|
70
|
+
*/
|
|
71
|
+
function MarkOverlapResolverPlugin() {
|
|
72
|
+
const [editor] = useLexicalComposerContext();
|
|
73
|
+
useEffect(() => registerMarkOverlapResolver(editor), [editor]);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Publishes the live-mark mechanism to the host via an imperative ref — the
|
|
78
|
+
* host lives outside this LexicalComposer tree and has no other way to reach
|
|
79
|
+
* the editor instance.
|
|
80
|
+
*/
|
|
81
|
+
function AnnotationEditorHandlePlugin({ handleRef, wikiLinkPromotion, }) {
|
|
82
|
+
const [editor] = useLexicalComposerContext();
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
handleRef.current = {
|
|
85
|
+
removeMarksForAnnotation: (id) => removeMarksForAnnotation(editor, id),
|
|
86
|
+
collectLiveAnchorSnapshots: (markdownText) => collectLiveAnchorSnapshots(editor, markdownText, { wikiLinkPromotion }),
|
|
87
|
+
};
|
|
88
|
+
return () => {
|
|
89
|
+
handleRef.current = null;
|
|
90
|
+
};
|
|
91
|
+
}, [editor, handleRef, wikiLinkPromotion]);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The entire annotation feature surface, behind one lazily-imported boundary.
|
|
96
|
+
*
|
|
97
|
+
* Nothing here is reachable from `Editor.tsx` by static import: the editor
|
|
98
|
+
* loads this module only when a host actually configures annotation kinds
|
|
99
|
+
* (FR-004/SC-004). That is why the create plugin, marker plugin, placement and
|
|
100
|
+
* editor handle are assembled here rather than mounted individually.
|
|
101
|
+
*/
|
|
102
|
+
export default function AnnotationSurface({ kinds, annotations, activeAnnotationId, scrollToAnnotation, onCreateAnnotation, onActivateAnnotation, editorHandleRef, offsetSpansRef, markdownTextRef, offsetsVersion, logger, wikiLinkPromotion, }) {
|
|
103
|
+
const targets = useMemo(() => deriveMarkerTargets(annotations, kinds), [annotations, kinds]);
|
|
104
|
+
// An annotation whose kind has no configuration is dropped by
|
|
105
|
+
// deriveMarkerTargets; surface that as a warning rather than silently
|
|
106
|
+
// ignoring the host's data.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
for (const annotation of annotations) {
|
|
109
|
+
if (!kinds[annotation.kind]) {
|
|
110
|
+
logger?.warn(`[annotations] annotation ${annotation.id} has unconfigured kind "${annotation.kind}"; ignoring`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}, [annotations, kinds, logger]);
|
|
114
|
+
return (_jsxs(_Fragment, { children: [_jsx(MarkOverlapResolverPlugin, {}), onCreateAnnotation && (_jsx(AnnotationPlugin, { kinds: kinds, onCreateAnnotation: onCreateAnnotation, logger: logger, wikiLinkPromotion: wikiLinkPromotion })), _jsx(AnnotationMarkPlacementPlugin, { offsetSpansRef: offsetSpansRef, markdownTextRef: markdownTextRef, targets: targets, offsetsVersion: offsetsVersion }), _jsx(AnnotationMarkerPlugin, { targets: targets, kinds: kinds, activeAnnotationId: activeAnnotationId, onActivateAnnotation: onActivateAnnotation ?? (() => undefined), scrollToAnnotation: scrollToAnnotation }), editorHandleRef && (_jsx(AnnotationEditorHandlePlugin, { handleRef: editorHandleRef, wikiLinkPromotion: wikiLinkPromotion }))] }));
|
|
115
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ImagePathResolution } from '../../types.js';
|
|
2
|
+
interface AssetContextValue {
|
|
3
|
+
assetBaseUri: string | undefined;
|
|
4
|
+
documentDirUri: string | undefined;
|
|
5
|
+
imagePathResolution: ImagePathResolution;
|
|
6
|
+
resolveAssetPath: (path: string) => string;
|
|
7
|
+
/** Resolve a workspace-relative file path to a data URL for display */
|
|
8
|
+
resolveLocalAsset?: (relativePath: string) => Promise<string | null>;
|
|
9
|
+
}
|
|
10
|
+
export declare const AssetContext: import("react").Context<AssetContextValue>;
|
|
11
|
+
export declare function useAssetContext(): AssetContextValue;
|
|
12
|
+
export interface AssetContextOptions {
|
|
13
|
+
assetBaseUri?: string;
|
|
14
|
+
documentDirUri?: string;
|
|
15
|
+
imagePathResolution?: ImagePathResolution;
|
|
16
|
+
/** Resolve a workspace-relative file path to a data URL for display */
|
|
17
|
+
resolveLocalAsset?: (relativePath: string) => Promise<string | null>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createAssetContextValue(options: AssetContextOptions): AssetContextValue;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const AssetContext = createContext({
|
|
3
|
+
assetBaseUri: undefined,
|
|
4
|
+
documentDirUri: undefined,
|
|
5
|
+
imagePathResolution: 'document',
|
|
6
|
+
resolveAssetPath: (path) => path,
|
|
7
|
+
});
|
|
8
|
+
export function useAssetContext() {
|
|
9
|
+
return useContext(AssetContext);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a relative path by resolving . and .. segments
|
|
13
|
+
* Examples:
|
|
14
|
+
* "./images/photo.png" -> "images/photo.png"
|
|
15
|
+
* "../assets/image.png" -> "../assets/image.png" (kept as-is, resolved by browser)
|
|
16
|
+
* "assets/../images/photo.png" -> "images/photo.png"
|
|
17
|
+
*/
|
|
18
|
+
function normalizePath(path) {
|
|
19
|
+
// Remove leading ./ (current directory reference)
|
|
20
|
+
const normalized = path.replace(/^\.\//, '');
|
|
21
|
+
// Split into segments and resolve . and ..
|
|
22
|
+
const segments = normalized.split('/');
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const segment of segments) {
|
|
25
|
+
if (segment === '.' || segment === '') {
|
|
26
|
+
// Skip current directory references and empty segments
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
else if (segment === '..') {
|
|
30
|
+
// Go up one directory if possible
|
|
31
|
+
if (result.length > 0 && result[result.length - 1] !== '..') {
|
|
32
|
+
result.pop();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Keep leading .. segments (they go above the base)
|
|
36
|
+
result.push(segment);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
result.push(segment);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result.join('/');
|
|
44
|
+
}
|
|
45
|
+
export function createAssetContextValue(options) {
|
|
46
|
+
const { assetBaseUri, documentDirUri, imagePathResolution = 'document', resolveLocalAsset, } = options;
|
|
47
|
+
return {
|
|
48
|
+
assetBaseUri,
|
|
49
|
+
documentDirUri,
|
|
50
|
+
imagePathResolution,
|
|
51
|
+
resolveLocalAsset,
|
|
52
|
+
resolveAssetPath: (path) => {
|
|
53
|
+
// If it's already an absolute URL (http, https, vscode-webview-resource, data), return as-is
|
|
54
|
+
if (path.startsWith('http://') ||
|
|
55
|
+
path.startsWith('https://') ||
|
|
56
|
+
path.startsWith('vscode-webview-resource:') ||
|
|
57
|
+
path.startsWith('data:')) {
|
|
58
|
+
return path;
|
|
59
|
+
}
|
|
60
|
+
// Paths starting with / are absolute from workspace root, regardless of
|
|
61
|
+
// imagePathResolution setting. Strip the leading / so the path is
|
|
62
|
+
// workspace-relative (readBinaryFile joins with workspace root).
|
|
63
|
+
// Check BEFORE normalizePath which strips the leading /.
|
|
64
|
+
if (path.startsWith('/')) {
|
|
65
|
+
return normalizePath(path.slice(1));
|
|
66
|
+
}
|
|
67
|
+
// Normalize the path to handle ./ and ../ correctly
|
|
68
|
+
const normalizedPath = normalizePath(path);
|
|
69
|
+
// Choose the base URI based on settings
|
|
70
|
+
const baseUri = imagePathResolution === 'document' ? documentDirUri : assetBaseUri;
|
|
71
|
+
// If we have a base URI, resolve relative paths and normalize the
|
|
72
|
+
// combined result so that "../" segments are resolved (e.g.
|
|
73
|
+
// "notes/sub/" + "../assets/img.svg" → "notes/assets/img.svg")
|
|
74
|
+
if (baseUri) {
|
|
75
|
+
return normalizePath(baseUri + normalizedPath);
|
|
76
|
+
}
|
|
77
|
+
// Fallback to original path
|
|
78
|
+
return path;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlockClickPlugin - Enables clicking to the right of blocks to place cursor
|
|
3
|
+
*
|
|
4
|
+
* When clicking in empty space to the right of a block's content,
|
|
5
|
+
* this selects that block. For non-editable blocks (images, HRs),
|
|
6
|
+
* pressing Enter will then create a new paragraph below.
|
|
7
|
+
*/
|
|
8
|
+
export declare function BlockClickPlugin(): null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import { $getNodeByKey, $isDecoratorNode, $createParagraphNode, $getRoot, $getNearestNodeFromDOMNode, COMMAND_PRIORITY_LOW, KEY_ENTER_COMMAND, } from 'lexical';
|
|
4
|
+
/**
|
|
5
|
+
* BlockClickPlugin - Enables clicking to the right of blocks to place cursor
|
|
6
|
+
*
|
|
7
|
+
* When clicking in empty space to the right of a block's content,
|
|
8
|
+
* this selects that block. For non-editable blocks (images, HRs),
|
|
9
|
+
* pressing Enter will then create a new paragraph below.
|
|
10
|
+
*/
|
|
11
|
+
export function BlockClickPlugin() {
|
|
12
|
+
const [editor] = useLexicalComposerContext();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const rootElement = editor.getRootElement();
|
|
15
|
+
if (!rootElement)
|
|
16
|
+
return;
|
|
17
|
+
const handleClick = (event) => {
|
|
18
|
+
// Only handle left clicks
|
|
19
|
+
if (event.button !== 0)
|
|
20
|
+
return;
|
|
21
|
+
const clickX = event.clientX;
|
|
22
|
+
const clickY = event.clientY;
|
|
23
|
+
// Get the content editable bounds
|
|
24
|
+
const rootRect = rootElement.getBoundingClientRect();
|
|
25
|
+
// Must be within editor bounds
|
|
26
|
+
if (clickY < rootRect.top || clickY > rootRect.bottom)
|
|
27
|
+
return;
|
|
28
|
+
if (clickX < rootRect.left || clickX > rootRect.right)
|
|
29
|
+
return;
|
|
30
|
+
// Find which block element the click Y coordinate corresponds to
|
|
31
|
+
const targetBlock = findBlockAtY(rootElement, clickY);
|
|
32
|
+
if (!targetBlock) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const blockRect = targetBlock.getBoundingClientRect();
|
|
36
|
+
// Check if click is to the RIGHT of the block's actual content
|
|
37
|
+
// (not inside the block itself)
|
|
38
|
+
if (clickX <= blockRect.right) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// We clicked in the empty space to the right of the block
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
event.stopPropagation();
|
|
44
|
+
// Use Lexical's method to find the node from DOM element
|
|
45
|
+
editor.update(() => {
|
|
46
|
+
const node = $getNearestNodeFromDOMNode(targetBlock);
|
|
47
|
+
if (!node) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Get the top-level block (direct child of root)
|
|
51
|
+
let topLevelNode = node;
|
|
52
|
+
const root = $getRoot();
|
|
53
|
+
while (topLevelNode && topLevelNode.getParent() !== root) {
|
|
54
|
+
topLevelNode = topLevelNode.getParent();
|
|
55
|
+
}
|
|
56
|
+
if (topLevelNode) {
|
|
57
|
+
selectAfterNode(topLevelNode);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Focus the editor
|
|
61
|
+
editor.focus();
|
|
62
|
+
};
|
|
63
|
+
// Use mousedown instead of click to capture before other handlers
|
|
64
|
+
rootElement.addEventListener('mousedown', handleClick);
|
|
65
|
+
return () => {
|
|
66
|
+
rootElement.removeEventListener('mousedown', handleClick);
|
|
67
|
+
};
|
|
68
|
+
}, [editor]);
|
|
69
|
+
// Handle Enter key when a decorator node is selected to insert paragraph after
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
return editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
|
|
72
|
+
// Check if we have an element selection on a decorator node
|
|
73
|
+
return editor.getEditorState().read(() => {
|
|
74
|
+
const sel = editor.getEditorState()._selection;
|
|
75
|
+
if (sel?.getNodes().length !== 1)
|
|
76
|
+
return false;
|
|
77
|
+
const node = sel.getNodes()[0];
|
|
78
|
+
if (!$isDecoratorNode(node))
|
|
79
|
+
return false;
|
|
80
|
+
// Insert a new paragraph after the decorator node
|
|
81
|
+
editor.update(() => {
|
|
82
|
+
const decoratorNode = $getNodeByKey(node.getKey());
|
|
83
|
+
if (!decoratorNode)
|
|
84
|
+
return;
|
|
85
|
+
const paragraph = $createParagraphNode();
|
|
86
|
+
decoratorNode.insertAfter(paragraph);
|
|
87
|
+
paragraph.select();
|
|
88
|
+
});
|
|
89
|
+
event?.preventDefault();
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
}, COMMAND_PRIORITY_LOW);
|
|
93
|
+
}, [editor]);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Find the top-level block element at a given Y coordinate
|
|
98
|
+
*/
|
|
99
|
+
function findBlockAtY(rootElement, y) {
|
|
100
|
+
const children = Array.from(rootElement.children);
|
|
101
|
+
for (const child of children) {
|
|
102
|
+
const rect = child.getBoundingClientRect();
|
|
103
|
+
if (y >= rect.top && y <= rect.bottom) {
|
|
104
|
+
return child;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// If click is below all blocks, return the last block
|
|
108
|
+
if (children.length > 0) {
|
|
109
|
+
const lastChild = children[children.length - 1];
|
|
110
|
+
const lastRect = lastChild.getBoundingClientRect();
|
|
111
|
+
if (y > lastRect.bottom) {
|
|
112
|
+
return lastChild;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Select after a node - inserts a new paragraph after the block and selects it
|
|
119
|
+
*/
|
|
120
|
+
function selectAfterNode(node) {
|
|
121
|
+
// For all block-level nodes, insert a new paragraph after and select it
|
|
122
|
+
// This gives the user a clear place to type
|
|
123
|
+
const paragraph = $createParagraphNode();
|
|
124
|
+
node.insertAfter(paragraph);
|
|
125
|
+
paragraph.select();
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function CodeBlockPlugin(): import("react").JSX.Element;
|