@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.
Files changed (219) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +376 -0
  3. package/dist/annotations/anchor-align.d.ts +50 -0
  4. package/dist/annotations/anchor-align.js +74 -0
  5. package/dist/annotations/anchor-model.d.ts +81 -0
  6. package/dist/annotations/anchor-model.js +80 -0
  7. package/dist/annotations/anchor-resolver.d.ts +92 -0
  8. package/dist/annotations/anchor-resolver.js +231 -0
  9. package/dist/annotations/block-structure.d.ts +65 -0
  10. package/dist/annotations/block-structure.js +113 -0
  11. package/dist/annotations/types.d.ts +159 -0
  12. package/dist/annotations/types.js +50 -0
  13. package/dist/annotations.d.ts +25 -0
  14. package/dist/annotations.js +27 -0
  15. package/dist/app/App.d.ts +64 -0
  16. package/dist/app/App.js +180 -0
  17. package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
  18. package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
  19. package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
  20. package/dist/app/editor/AnchorScrollPlugin.js +134 -0
  21. package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
  22. package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
  23. package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
  24. package/dist/app/editor/AnnotationPlugin.js +135 -0
  25. package/dist/app/editor/AnnotationSurface.d.ts +34 -0
  26. package/dist/app/editor/AnnotationSurface.js +115 -0
  27. package/dist/app/editor/AssetContext.d.ts +20 -0
  28. package/dist/app/editor/AssetContext.js +81 -0
  29. package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
  30. package/dist/app/editor/BlockClickPlugin.js +126 -0
  31. package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
  32. package/dist/app/editor/CodeBlockPlugin.js +219 -0
  33. package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
  34. package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
  35. package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
  36. package/dist/app/editor/DragHandlePlugin.js +300 -0
  37. package/dist/app/editor/Editor.d.ts +84 -0
  38. package/dist/app/editor/Editor.js +539 -0
  39. package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
  40. package/dist/app/editor/FrontmatterPlugin.js +301 -0
  41. package/dist/app/editor/ImageModal.d.ts +13 -0
  42. package/dist/app/editor/ImageModal.js +267 -0
  43. package/dist/app/editor/ImagePlugin.d.ts +3 -0
  44. package/dist/app/editor/ImagePlugin.js +187 -0
  45. package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
  46. package/dist/app/editor/LinkClickPlugin.js +90 -0
  47. package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
  48. package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
  49. package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
  50. package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
  51. package/dist/app/editor/SearchPlugin.d.ts +1 -0
  52. package/dist/app/editor/SearchPlugin.js +379 -0
  53. package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
  54. package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
  55. package/dist/app/editor/SlashMenu.d.ts +11 -0
  56. package/dist/app/editor/SlashMenu.js +494 -0
  57. package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
  58. package/dist/app/editor/SlashMenuPlugin.js +168 -0
  59. package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
  60. package/dist/app/editor/TableActionsPlugin.js +398 -0
  61. package/dist/app/editor/TogglePlugin.d.ts +1 -0
  62. package/dist/app/editor/TogglePlugin.js +159 -0
  63. package/dist/app/editor/Toolbar.d.ts +10 -0
  64. package/dist/app/editor/Toolbar.js +263 -0
  65. package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
  66. package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
  67. package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
  68. package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
  69. package/dist/app/editor/annotation-marks.d.ts +177 -0
  70. package/dist/app/editor/annotation-marks.js +750 -0
  71. package/dist/app/editor/annotationCommands.d.ts +13 -0
  72. package/dist/app/editor/annotationCommands.js +11 -0
  73. package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
  74. package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
  75. package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
  76. package/dist/app/editor/c4/edge-clipping.js +139 -0
  77. package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
  78. package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
  79. package/dist/app/editor/c4/layout.d.ts +25 -0
  80. package/dist/app/editor/c4/layout.js +839 -0
  81. package/dist/app/editor/c4/parser.d.ts +19 -0
  82. package/dist/app/editor/c4/parser.js +410 -0
  83. package/dist/app/editor/c4/render-to-string.d.ts +24 -0
  84. package/dist/app/editor/c4/render-to-string.js +34 -0
  85. package/dist/app/editor/c4/renderer.d.ts +64 -0
  86. package/dist/app/editor/c4/renderer.js +569 -0
  87. package/dist/app/editor/c4/types.d.ts +203 -0
  88. package/dist/app/editor/c4/types.js +43 -0
  89. package/dist/app/editor/correction-yaml.d.ts +10 -0
  90. package/dist/app/editor/correction-yaml.js +40 -0
  91. package/dist/app/editor/editorNodes.d.ts +9 -0
  92. package/dist/app/editor/editorNodes.js +36 -0
  93. package/dist/app/editor/index.d.ts +1 -0
  94. package/dist/app/editor/index.js +1 -0
  95. package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
  96. package/dist/app/editor/mark-overlap-resolver.js +45 -0
  97. package/dist/app/editor/nodes/C4Component.d.ts +14 -0
  98. package/dist/app/editor/nodes/C4Component.js +284 -0
  99. package/dist/app/editor/nodes/C4Node.d.ts +32 -0
  100. package/dist/app/editor/nodes/C4Node.js +112 -0
  101. package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
  102. package/dist/app/editor/nodes/CalloutNode.js +94 -0
  103. package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
  104. package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
  105. package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
  106. package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
  107. package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
  108. package/dist/app/editor/nodes/CustomListNode.js +54 -0
  109. package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
  110. package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
  111. package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
  112. package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
  113. package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
  114. package/dist/app/editor/nodes/EquationComponent.js +130 -0
  115. package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
  116. package/dist/app/editor/nodes/EquationNode.js +179 -0
  117. package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
  118. package/dist/app/editor/nodes/FootnoteNode.js +151 -0
  119. package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
  120. package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
  121. package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
  122. package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
  123. package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
  124. package/dist/app/editor/nodes/HtmlNode.js +121 -0
  125. package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
  126. package/dist/app/editor/nodes/ImageComponent.js +228 -0
  127. package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
  128. package/dist/app/editor/nodes/ImageNode.js +183 -0
  129. package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
  130. package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
  131. package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
  132. package/dist/app/editor/nodes/MermaidComponent.js +216 -0
  133. package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
  134. package/dist/app/editor/nodes/MermaidNode.js +96 -0
  135. package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
  136. package/dist/app/editor/nodes/ToggleNode.js +237 -0
  137. package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
  138. package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
  139. package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
  140. package/dist/app/editor/nodes/diagram-utils.js +29 -0
  141. package/dist/app/editor/nodes/index.d.ts +30 -0
  142. package/dist/app/editor/nodes/index.js +17 -0
  143. package/dist/app/editor/utils.d.ts +2 -0
  144. package/dist/app/editor/utils.js +17 -0
  145. package/dist/app/mapper/index.d.ts +3 -0
  146. package/dist/app/mapper/index.js +2 -0
  147. package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
  148. package/dist/app/mapper/lexicalToMdast.js +1937 -0
  149. package/dist/app/mapper/mdastToLexical.d.ts +31 -0
  150. package/dist/app/mapper/mdastToLexical.js +1075 -0
  151. package/dist/contract.d.ts +8 -0
  152. package/dist/contract.js +8 -0
  153. package/dist/headless.d.ts +40 -0
  154. package/dist/headless.js +45 -0
  155. package/dist/host/context.d.ts +20 -0
  156. package/dist/host/context.js +24 -0
  157. package/dist/host/defaults.d.ts +21 -0
  158. package/dist/host/defaults.js +47 -0
  159. package/dist/host/messages.d.ts +26 -0
  160. package/dist/host/messages.js +47 -0
  161. package/dist/host/types.d.ts +75 -0
  162. package/dist/host/types.js +13 -0
  163. package/dist/index.d.ts +35 -0
  164. package/dist/index.js +51 -0
  165. package/dist/markdown/annotate-sentinels.d.ts +42 -0
  166. package/dist/markdown/annotate-sentinels.js +86 -0
  167. package/dist/markdown/parse.d.ts +56 -0
  168. package/dist/markdown/parse.js +516 -0
  169. package/dist/markdown/stringify.d.ts +7 -0
  170. package/dist/markdown/stringify.js +439 -0
  171. package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
  172. package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
  173. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
  174. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
  175. package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
  176. package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
  177. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
  178. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
  179. package/dist/markdown.d.ts +28 -0
  180. package/dist/markdown.js +44 -0
  181. package/dist/mathjax-config.d.ts +89 -0
  182. package/dist/mathjax-config.js +133 -0
  183. package/dist/nodes.d.ts +34 -0
  184. package/dist/nodes.js +34 -0
  185. package/dist/stores/correctionStore.d.ts +15 -0
  186. package/dist/stores/correctionStore.js +12 -0
  187. package/dist/stores/editorStore.d.ts +16 -0
  188. package/dist/stores/editorStore.js +27 -0
  189. package/dist/styles.css +2477 -0
  190. package/dist/types.d.ts +296 -0
  191. package/dist/types.js +91 -0
  192. package/dist/utils/file-types.d.ts +10 -0
  193. package/dist/utils/file-types.js +46 -0
  194. package/docs/annotations.md +134 -0
  195. package/docs/decisions/README.md +72 -0
  196. package/docs/decisions/adr-002.md +27 -0
  197. package/docs/decisions/adr-007.md +27 -0
  198. package/docs/decisions/adr-010.md +26 -0
  199. package/docs/decisions/adr-024.md +124 -0
  200. package/docs/decisions/adr-024b.md +0 -0
  201. package/docs/decisions/adr-025.md +103 -0
  202. package/docs/decisions/adr-027.md +66 -0
  203. package/docs/decisions/adr-057.md +67 -0
  204. package/docs/decisions/adr-070.md +131 -0
  205. package/docs/decisions/adr-075.md +410 -0
  206. package/docs/decisions/adr-076.md +183 -0
  207. package/docs/decisions/adr-077.md +410 -0
  208. package/docs/decisions/adr-078.md +243 -0
  209. package/docs/decisions/adr-079.md +167 -0
  210. package/docs/decisions/adr-080.md +325 -0
  211. package/docs/decisions/adr-081.md +108 -0
  212. package/docs/decisions/adr-082.md +123 -0
  213. package/docs/decisions/adr-083.md +204 -0
  214. package/docs/decisions/adr-084.md +179 -0
  215. package/docs/editor-api.md +106 -0
  216. package/docs/markdown-pipeline.md +282 -0
  217. package/docs/provenance.md +106 -0
  218. package/docs/zusammen-editor-capability-map.md +232 -0
  219. package/package.json +175 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Unified annotation types (ADR-077).
3
+ *
4
+ * An annotation is a range-anchored marker over document text. Comments and
5
+ * corrections are not separate features here — they are two *kinds*, differing
6
+ * only by the {@link AnnotationKindConfig} a host supplies. Everything in this
7
+ * module is DOM-free, Lexical-free and React-free so it can be reached from the
8
+ * `./annotations` subpath and used outside a rendered editor.
9
+ *
10
+ * Per ADR-075 the seam is persistence: the package owns anchor mechanics and
11
+ * marker rendering; identity, storage and lifecycle (resolve/reopen for
12
+ * comments, accept/reject for corrections) stay in the consuming app. The
13
+ * package treats `id` as opaque and `payload` as pass-through.
14
+ */
15
+ import type { Anchor, AnchorOutcome, AnchorRange } from './anchor-model.js';
16
+ /**
17
+ * Which kind of annotation this is. Deliberately a plain string rather than a
18
+ * closed union — a host may configure kinds the package has never heard of,
19
+ * and the two Liminis/Zusammen kinds carry no special status in the mechanism.
20
+ */
21
+ export type AnnotationKind = string;
22
+ /** Presentation hints a host may attach per annotation, overriding kind defaults. */
23
+ export interface AnnotationPresentation {
24
+ /** Extra CSS class applied to this annotation's marker element. */
25
+ className?: string;
26
+ /** Accessible label for the marker; falls back to the kind name. */
27
+ label?: string;
28
+ }
29
+ /**
30
+ * A range-anchored marker over document text. `id` is opaque to the package —
31
+ * hosts mint and persist it. `payload` is carried through untouched and handed
32
+ * back on activation, so a host can round-trip its own domain object without
33
+ * the package understanding it.
34
+ */
35
+ export interface Annotation<TPayload = unknown> {
36
+ id: string;
37
+ kind: AnnotationKind;
38
+ anchor: Anchor;
39
+ /**
40
+ * The resolution outcome currently in effect for this anchor. Hosts compute
41
+ * this (via `resolveAnchors` or their own store) — the package never
42
+ * classifies on the host's behalf. Defaults to `unchanged` when omitted.
43
+ */
44
+ outcome?: AnchorOutcome;
45
+ presentation?: AnnotationPresentation;
46
+ payload?: TPayload;
47
+ }
48
+ /**
49
+ * How a kind's markers paint. `none` places no visible marker at all, which is
50
+ * what corrections use: they adopt the anchor model and the capture primitive
51
+ * but render exactly what they render today — nothing (FR-006 parity).
52
+ */
53
+ export type AnnotationMarkerStyle = 'highlight' | 'squiggle' | 'none';
54
+ /** Where a kind's user-initiated create affordance is offered, if anywhere. */
55
+ export interface AnnotationCreateAffordance {
56
+ surface: 'toolbar' | 'contextMenu';
57
+ label?: string;
58
+ }
59
+ /**
60
+ * The per-kind configuration record. SC-001's claim rests on this type: a
61
+ * reviewer can point at two values of it as the entire difference between the
62
+ * comment feature and the correction feature.
63
+ */
64
+ export interface AnnotationKindConfig<TPayload = unknown> {
65
+ markerStyle: AnnotationMarkerStyle;
66
+ /**
67
+ * User-initiated creation. Omitted or null means host-injected only — the
68
+ * package offers no way for a user to create one.
69
+ *
70
+ * CAUTION: a kind literally named `correction` on the `contextMenu` surface
71
+ * is special-cased internally — `SelectionContextMenuPlugin` opens the
72
+ * package's own legacy correction panel for it in addition to dispatching
73
+ * this create flow. If such a kind's host also supplies
74
+ * `onCreateAnnotation`, both UIs open off the same click with no
75
+ * coordination between them. Name a kind something other than `correction`
76
+ * if you want only the standard create flow.
77
+ */
78
+ createAffordance?: AnnotationCreateAffordance | null;
79
+ /**
80
+ * Whether this annotation gets a live `MarkNode` in the document. Defaults to
81
+ * {@link shouldPlaceLiveMark} over the annotation's outcome when omitted.
82
+ *
83
+ * Consulted only when `markerStyle` is not `none` — see
84
+ * {@link deriveMarkerTargets} for why that precedence exists.
85
+ */
86
+ livemarkPolicy?: (annotation: Annotation<TPayload>) => boolean;
87
+ /**
88
+ * Whether the capture primitive leaves its transient mark in place after
89
+ * capturing an anchor. Comments retain it (the composer highlights the
90
+ * passage); corrections discard it.
91
+ *
92
+ * The discard is *not* part of the wrapping update. `AnnotationPlugin` wraps
93
+ * inside the command handler, then reads the anchor back and removes the
94
+ * mark on a `queueMicrotask` — the read cannot be inline, because a command
95
+ * handler runs inside an active Lexical update and the nested wrap is not
96
+ * observable until that update flushes. The guarantee is therefore "removed
97
+ * before the browser paints" (microtasks drain ahead of paint), not "removed
98
+ * in the same update". Anything that pushes the discard past a task boundary
99
+ * — an `await`, a `setTimeout`, a `requestAnimationFrame` — would let the
100
+ * mark flash visibly first.
101
+ */
102
+ retainMarkOnCreate?: boolean;
103
+ }
104
+ /** A host's full annotation configuration: kind name → its config. */
105
+ export type AnnotationKindConfigs = Record<AnnotationKind, AnnotationKindConfig>;
106
+ /**
107
+ * A resolved instruction to place one marker. Ported from Zusammen's
108
+ * `comment-thread.ts`; its `deriveMarkerTargets` is deliberately *not* ported —
109
+ * that function takes Zusammen's `Thread`/`Comment`, which are app-domain under
110
+ * ADR-075. The package accepts already-derived annotations instead.
111
+ */
112
+ export interface MarkerTarget {
113
+ annotationId: string;
114
+ kind: AnnotationKind;
115
+ anchor: Anchor;
116
+ outcome: AnchorOutcome;
117
+ /**
118
+ * Carried through from the source annotation. The marker renderer sees only
119
+ * targets, so without this the per-annotation className/label overrides
120
+ * {@link AnnotationPresentation} promises would be silently unreachable.
121
+ */
122
+ presentation?: AnnotationPresentation;
123
+ }
124
+ /**
125
+ * Whether `outcome` gets a live in-document `MarkNode` at all. Only
126
+ * `unchanged`/`re-attached` do — `flagged`'s match is uncertain and `orphaned`
127
+ * has nothing left to point at, so both stay panel-only, never placed on text
128
+ * the resolver wasn't confident about.
129
+ */
130
+ export declare function shouldPlaceLiveMark(outcome: AnchorOutcome): boolean;
131
+ /**
132
+ * Derive marker targets from host-supplied annotations, applying each kind's
133
+ * live-mark policy. Annotations whose `kind` has no configuration are dropped;
134
+ * the caller is responsible for warning about them (the logger is injected at
135
+ * the React layer, and this module stays dependency-free).
136
+ *
137
+ * `markerStyle: 'none'` takes precedence over `livemarkPolicy` and yields no
138
+ * target at all. A target is not merely a styling instruction — it is what
139
+ * makes `AnnotationMarkPlacementPlugin` wrap the text in a real `MarkNode`,
140
+ * which reaches the DOM as a `<mark>` element. `AnnotationMarkerPlugin` skips
141
+ * decoration for `none`, so that element would carry no `annotation-mark-*`
142
+ * class, and `styles.css` neutralizes the UA's default yellow `<mark>`
143
+ * background only under those classes — leaving a persistent uncontrolled
144
+ * highlight, the exact opposite of what `none` promises. Gating here rather
145
+ * than at paint time keeps the promise structural instead of leaving it to a
146
+ * host remembering to pair `none` with `livemarkPolicy: () => false`.
147
+ */
148
+ export declare function deriveMarkerTargets(annotations: readonly Annotation[], kinds: AnnotationKindConfigs): MarkerTarget[];
149
+ /**
150
+ * The imperative bridge from a host to the live marks inside a mounted editor —
151
+ * set once the editor mounts, read by the host at delete-time and
152
+ * checkpoint-time. Ported from Zusammen's `comment-editor-handle.ts`.
153
+ */
154
+ export interface AnnotationEditorHandle {
155
+ /** Removes every live mark wrapping `annotationId` (annotation deleted, or composer cancelled). */
156
+ removeMarksForAnnotation: (annotationId: string) => void;
157
+ /** The current live range of every marked passage, keyed by annotation id. */
158
+ collectLiveAnchorSnapshots: (markdownText: string) => Map<string, AnchorRange>;
159
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Whether `outcome` gets a live in-document `MarkNode` at all. Only
3
+ * `unchanged`/`re-attached` do — `flagged`'s match is uncertain and `orphaned`
4
+ * has nothing left to point at, so both stay panel-only, never placed on text
5
+ * the resolver wasn't confident about.
6
+ */
7
+ export function shouldPlaceLiveMark(outcome) {
8
+ return outcome === 'unchanged' || outcome === 're-attached';
9
+ }
10
+ /**
11
+ * Derive marker targets from host-supplied annotations, applying each kind's
12
+ * live-mark policy. Annotations whose `kind` has no configuration are dropped;
13
+ * the caller is responsible for warning about them (the logger is injected at
14
+ * the React layer, and this module stays dependency-free).
15
+ *
16
+ * `markerStyle: 'none'` takes precedence over `livemarkPolicy` and yields no
17
+ * target at all. A target is not merely a styling instruction — it is what
18
+ * makes `AnnotationMarkPlacementPlugin` wrap the text in a real `MarkNode`,
19
+ * which reaches the DOM as a `<mark>` element. `AnnotationMarkerPlugin` skips
20
+ * decoration for `none`, so that element would carry no `annotation-mark-*`
21
+ * class, and `styles.css` neutralizes the UA's default yellow `<mark>`
22
+ * background only under those classes — leaving a persistent uncontrolled
23
+ * highlight, the exact opposite of what `none` promises. Gating here rather
24
+ * than at paint time keeps the promise structural instead of leaving it to a
25
+ * host remembering to pair `none` with `livemarkPolicy: () => false`.
26
+ */
27
+ export function deriveMarkerTargets(annotations, kinds) {
28
+ const targets = [];
29
+ for (const annotation of annotations) {
30
+ const config = kinds[annotation.kind];
31
+ if (!config)
32
+ continue;
33
+ if (config.markerStyle === 'none')
34
+ continue;
35
+ const outcome = annotation.outcome ?? 'unchanged';
36
+ const placeLive = config.livemarkPolicy
37
+ ? config.livemarkPolicy(annotation)
38
+ : shouldPlaceLiveMark(outcome);
39
+ if (!placeLive)
40
+ continue;
41
+ targets.push({
42
+ annotationId: annotation.id,
43
+ kind: annotation.kind,
44
+ anchor: annotation.anchor,
45
+ outcome,
46
+ presentation: annotation.presentation,
47
+ });
48
+ }
49
+ return targets;
50
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@liminis/editor/annotations` — the headless annotation surface (ADR-077).
3
+ *
4
+ * The sixth declared export subpath, added as a *considered* export under
5
+ * ADR-075's five-subpath rule. It exists because this surface has a profile no
6
+ * existing subpath has: it must be callable outside a rendered editor, so it
7
+ * is free of DOM, React and Lexical — but it is not pure-mdast either (the
8
+ * anchor schema imports `zod`), so it does not belong on `./markdown`, and it
9
+ * must not go on `./headless`, whose `mathjax-config` re-export drags ~1.9 MB
10
+ * of side-effectful imports.
11
+ *
12
+ * Everything reachable from here is safe to call from a main process, a worker,
13
+ * or a test with no DOM. The React plugins and the marker-placement code live
14
+ * on the root barrel alongside `<Editor>` instead.
15
+ */
16
+ export { ANCHOR_SCHEMA, ANCHOR_RESOLUTION_SCHEMA, CONTEXT_WINDOW_CHARS, captureAnchor, } from './annotations/anchor-model.js';
17
+ export type { Anchor, AnchorFields, AnchorRange, AnchorOutcome, AnchorResolution, } from './annotations/anchor-model.js';
18
+ export { resolveAnchor, resolveAnchors, similarity, noopProposeSemanticRelocation, REATTACH_THRESHOLD, FLAG_THRESHOLD, } from './annotations/anchor-resolver.js';
19
+ export type { ProposeSemanticRelocation, ResolveAnchorOptions, IdentifiedAnchor, AnchorResolutionResult, } from './annotations/anchor-resolver.js';
20
+ export { parseBlocks, findEnclosingBlock, blockPlainText, sentenceChunks, } from './annotations/block-structure.js';
21
+ export type { Block, BlockType, SentenceChunk } from './annotations/block-structure.js';
22
+ export { locateInSpan } from './annotations/anchor-align.js';
23
+ export type { SpanRange, LocateInSpanOptions } from './annotations/anchor-align.js';
24
+ export { shouldPlaceLiveMark, deriveMarkerTargets } from './annotations/types.js';
25
+ export type { Annotation, AnnotationKind, AnnotationKindConfig, AnnotationKindConfigs, AnnotationCreateAffordance, AnnotationMarkerStyle, AnnotationPresentation, AnnotationEditorHandle, MarkerTarget, } from './annotations/types.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@liminis/editor/annotations` — the headless annotation surface (ADR-077).
3
+ *
4
+ * The sixth declared export subpath, added as a *considered* export under
5
+ * ADR-075's five-subpath rule. It exists because this surface has a profile no
6
+ * existing subpath has: it must be callable outside a rendered editor, so it
7
+ * is free of DOM, React and Lexical — but it is not pure-mdast either (the
8
+ * anchor schema imports `zod`), so it does not belong on `./markdown`, and it
9
+ * must not go on `./headless`, whose `mathjax-config` re-export drags ~1.9 MB
10
+ * of side-effectful imports.
11
+ *
12
+ * Everything reachable from here is safe to call from a main process, a worker,
13
+ * or a test with no DOM. The React plugins and the marker-placement code live
14
+ * on the root barrel alongside `<Editor>` instead.
15
+ */
16
+ // The durable anchor model — capture, schemas, and the outcome vocabulary.
17
+ export { ANCHOR_SCHEMA, ANCHOR_RESOLUTION_SCHEMA, CONTEXT_WINDOW_CHARS, captureAnchor, } from './annotations/anchor-model.js';
18
+ // The sole fuzzy-matching module (FR-003). `resolveAnchors` is the batch entry
19
+ // point hosts call; it is async because the relocation seam may be host-supplied.
20
+ export { resolveAnchor, resolveAnchors, similarity, noopProposeSemanticRelocation, REATTACH_THRESHOLD, FLAG_THRESHOLD, } from './annotations/anchor-resolver.js';
21
+ // Block structure — the structural context anchors are captured against.
22
+ export { parseBlocks, findEnclosingBlock, blockPlainText, sentenceChunks, } from './annotations/block-structure.js';
23
+ // Span alignment, used when mapping an anchor onto a live document offset span.
24
+ export { locateInSpan } from './annotations/anchor-align.js';
25
+ // The annotation primitive and its per-kind configuration — the sole difference
26
+ // between the comment feature and the correction feature (SC-001).
27
+ export { shouldPlaceLiveMark, deriveMarkerTargets } from './annotations/types.js';
@@ -0,0 +1,64 @@
1
+ import { type MutableRefObject } from 'react';
2
+ import type { SelectionContextMenuEvent } from './editor/SelectionContextMenuPlugin.js';
3
+ import type { SweepFn } from './editor/AmbientCorrectionPlugin.js';
4
+ import type { AnnotationCreateEvent } from './editor/AnnotationPlugin.js';
5
+ import type { Annotation, AnnotationEditorHandle, AnnotationKindConfigs } from '../annotations/types.js';
6
+ import type { WikiLinkPromotionMode } from './mapper/lexicalToMdast.js';
7
+ export interface CursorState {
8
+ offset: number;
9
+ contextBefore: string;
10
+ contextAfter: string;
11
+ }
12
+ interface AppProps {
13
+ /** When false, the editor is read-only */
14
+ editable?: boolean;
15
+ /**
16
+ * Whether the editor takes focus shortly after mounting. Defaults to
17
+ * `false`; see `Editor`'s own prop for when to opt in.
18
+ */
19
+ autoFocus?: boolean;
20
+ /** Content to display - when provided, used instead of IPC */
21
+ content?: string;
22
+ /** Called when content changes */
23
+ onChange?: (content: string) => void;
24
+ /** Path to the file being edited (used for file-type-specific UI like .mdc) */
25
+ filePath?: string;
26
+ /** Resolve a workspace-relative file path to a data URL for display */
27
+ resolveLocalAsset?: (relativePath: string) => Promise<string | null>;
28
+ /**
29
+ * Whether an untitled relative link is promoted to wiki-link syntax on
30
+ * export. Defaults to `'promote'`, today's only behavior. Unlike
31
+ * `imagePathResolution`, this is a direct prop rather than sourced from IPC
32
+ * `SlashMDSettings` — `App` also supports a non-IPC "inline" mode (`content`/
33
+ * `onChange` props), where `settings` stays null forever, and a
34
+ * settings-only path would be unreachable there (liminis#951).
35
+ */
36
+ wikiLinkPromotion?: WikiLinkPromotionMode;
37
+ /** Called when user right-clicks a text selection and chooses a context menu action */
38
+ onSelectionContextMenu?: (event: SelectionContextMenuEvent) => void;
39
+ /** Called when a single-word substitution is detected after a debounce window. */
40
+ onSubstitutionDetected?: (oldTerm: string, newTerm: string) => void;
41
+ /** Ref populated with a sweep function by AmbientCorrectionPlugin when active. */
42
+ sweepRef?: MutableRefObject<SweepFn | null>;
43
+ /**
44
+ * Annotation kind configuration (ADR-077), forwarded to <Editor>. Absent
45
+ * means the annotation mechanism stays off entirely.
46
+ */
47
+ annotationKinds?: AnnotationKindConfigs;
48
+ annotations?: Annotation[];
49
+ activeAnnotationId?: string | null;
50
+ scrollToAnnotation?: {
51
+ id: string;
52
+ nonce: number;
53
+ } | null;
54
+ onCreateAnnotation?: (event: AnnotationCreateEvent) => void;
55
+ onActivateAnnotation?: (id: string) => void;
56
+ annotationEditorHandleRef?: MutableRefObject<AnnotationEditorHandle | null>;
57
+ annotationLogger?: {
58
+ warn: (message: string, ...args: unknown[]) => void;
59
+ };
60
+ /** Override className on the root div (default: "min-h-screen p-0"). */
61
+ className?: string;
62
+ }
63
+ export declare function App({ editable, autoFocus, content: propContent, onChange: propOnChange, filePath, resolveLocalAsset, wikiLinkPromotion, onSelectionContextMenu, onSubstitutionDetected, sweepRef, annotationKinds, annotations, activeAnnotationId, scrollToAnnotation, onCreateAnnotation, onActivateAnnotation, annotationEditorHandleRef, annotationLogger, className }: AppProps): import("react").JSX.Element;
64
+ export {};
@@ -0,0 +1,180 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
3
+ import { Editor } from './editor/index.js';
4
+ import { useEditorHost } from '../host/context.js';
5
+ import { useHostMessages } from '../host/messages.js';
6
+ // Simple diff algorithm to find the changed region between two strings
7
+ function computeMinimalEdits(oldText, newText) {
8
+ if (oldText === newText)
9
+ return [];
10
+ // Find common prefix
11
+ let prefixLen = 0;
12
+ const minLen = Math.min(oldText.length, newText.length);
13
+ while (prefixLen < minLen && oldText[prefixLen] === newText[prefixLen]) {
14
+ prefixLen++;
15
+ }
16
+ // Find common suffix (but don't overlap with prefix)
17
+ let suffixLen = 0;
18
+ while (suffixLen < minLen - prefixLen &&
19
+ oldText[oldText.length - 1 - suffixLen] === newText[newText.length - 1 - suffixLen]) {
20
+ suffixLen++;
21
+ }
22
+ // Calculate the changed region
23
+ const start = prefixLen;
24
+ const end = oldText.length - suffixLen;
25
+ const newTextContent = newText.slice(prefixLen, newText.length - suffixLen);
26
+ return [{
27
+ start,
28
+ end,
29
+ newText: newTextContent,
30
+ }];
31
+ }
32
+ export function App({ editable = true, autoFocus = false, content: propContent, onChange: propOnChange, filePath, resolveLocalAsset, wikiLinkPromotion, onSelectionContextMenu, onSubstitutionDetected, sweepRef, annotationKinds, annotations, activeAnnotationId, scrollToAnnotation, onCreateAnnotation, onActivateAnnotation, annotationEditorHandleRef, annotationLogger, className = 'min-h-screen p-0' }) {
33
+ const { bridge, logger } = useEditorHost();
34
+ const log = useMemo(() => logger('slashmd/App'), [logger]);
35
+ const { requestInit, applyTextEdits } = useHostMessages();
36
+ const [internalContent, setInternalContent] = useState(null);
37
+ const [settings, setSettings] = useState(null);
38
+ const [assetBaseUri, setAssetBaseUri] = useState(undefined);
39
+ const [ipcDocumentDirUri, setIpcDocumentDirUri] = useState(undefined);
40
+ // Derive documentDirUri from filePath when not provided via IPC (inline mode).
41
+ // This ensures relative image paths like "image.svg" resolve relative to the document's directory.
42
+ const documentDirUri = useMemo(() => {
43
+ if (ipcDocumentDirUri)
44
+ return ipcDocumentDirUri;
45
+ if (!filePath)
46
+ return undefined;
47
+ const lastSlash = filePath.lastIndexOf('/');
48
+ return lastSlash >= 0 ? filePath.slice(0, lastSlash + 1) : '';
49
+ }, [ipcDocumentDirUri, filePath]);
50
+ const [themeOverrides, setThemeOverrides] = useState(undefined);
51
+ const [error, setError] = useState(null);
52
+ const pendingAssetCallback = useRef(null);
53
+ // Track the last known document content for diff computation
54
+ const lastDocumentContent = useRef('');
55
+ // Cursor state - persists across content reloads
56
+ const cursorStateRef = useRef(null);
57
+ // Counter to trigger cursor restoration after content changes
58
+ const [contentVersion, setContentVersion] = useState(0);
59
+ // Use prop content if provided, otherwise use internal state
60
+ const content = propContent !== undefined ? propContent : internalContent;
61
+ // Track when editor-initiated changes happen to skip cursor restoration
62
+ const lastEditorChangeTime = useRef(0);
63
+ // Track prop content changes to update lastDocumentContent and trigger cursor restoration
64
+ // BUT only increment contentVersion for EXTERNAL changes (file switch, reload)
65
+ // NOT for changes that came from the editor itself (typing)
66
+ const prevPropContent = useRef(propContent);
67
+ useEffect(() => {
68
+ if (propContent !== undefined && propContent !== prevPropContent.current) {
69
+ const timeSinceEditorChange = Date.now() - lastEditorChangeTime.current;
70
+ const isEditorInitiated = timeSinceEditorChange < 500;
71
+ log.info('Content prop changed', {
72
+ chars: propContent.length,
73
+ isEditorInitiated,
74
+ timeSinceEditorChange
75
+ });
76
+ lastDocumentContent.current = propContent;
77
+ // Only increment contentVersion for external changes (triggers cursor restore)
78
+ // Skip for editor-initiated changes (would mess up cursor during typing)
79
+ if (!isEditorInitiated) {
80
+ setContentVersion((v) => v + 1);
81
+ }
82
+ }
83
+ prevPropContent.current = propContent;
84
+ }, [propContent, log]);
85
+ useEffect(() => {
86
+ log.info('Setting up message handler');
87
+ const removeHandler = bridge.addMessageHandler((message) => {
88
+ log.debug('Handling message', message.type);
89
+ switch (message.type) {
90
+ case 'DOC_INIT':
91
+ // Only use IPC content if not controlled by prop
92
+ if (propContent === undefined) {
93
+ log.info('DOC_INIT', { chars: message.text?.length });
94
+ lastDocumentContent.current = message.text;
95
+ setInternalContent(message.text);
96
+ }
97
+ setSettings(message.settings);
98
+ setAssetBaseUri(message.assetBaseUri);
99
+ setIpcDocumentDirUri(message.documentDirUri);
100
+ setThemeOverrides(message.themeOverrides);
101
+ break;
102
+ case 'DOC_CHANGED':
103
+ // Only use IPC content if not controlled by prop
104
+ if (propContent === undefined) {
105
+ log.debug('DOC_CHANGED', { chars: message.text?.length });
106
+ // Skip if content matches what we already have (our own save echoing back)
107
+ if (message.text === lastDocumentContent.current) {
108
+ log.debug('DOC_CHANGED skipped - content unchanged');
109
+ return;
110
+ }
111
+ log.info('DOC_CHANGED - content differs, will restore cursor', {
112
+ cursorOffset: cursorStateRef.current?.offset,
113
+ });
114
+ lastDocumentContent.current = message.text;
115
+ setInternalContent(message.text);
116
+ // Increment version to signal Editor to restore cursor
117
+ setContentVersion((v) => v + 1);
118
+ }
119
+ break;
120
+ case 'SETTINGS_CHANGED':
121
+ setSettings(message.settings);
122
+ setThemeOverrides(message.themeOverrides);
123
+ break;
124
+ case 'ASSET_WRITTEN':
125
+ if (pendingAssetCallback.current) {
126
+ pendingAssetCallback.current(message.relPath);
127
+ pendingAssetCallback.current = null;
128
+ }
129
+ break;
130
+ case 'ERROR':
131
+ log.warn('ERROR', message.message);
132
+ setError(message.message);
133
+ setTimeout(() => setError(null), 5000);
134
+ break;
135
+ }
136
+ });
137
+ // Only request init if not controlled by prop
138
+ if (propContent === undefined) {
139
+ requestInit();
140
+ }
141
+ return removeHandler;
142
+ }, [propContent, bridge, requestInit, log]);
143
+ // Apply theme overrides as CSS variables
144
+ useEffect(() => {
145
+ if (!themeOverrides)
146
+ return;
147
+ const root = document.documentElement;
148
+ for (const [property, value] of Object.entries(themeOverrides)) {
149
+ root.style.setProperty(property, value);
150
+ }
151
+ }, [themeOverrides]);
152
+ const handleChange = useCallback((markdown) => {
153
+ // Calculate diff and send minimal edits
154
+ const edits = computeMinimalEdits(lastDocumentContent.current, markdown);
155
+ if (edits.length > 0) {
156
+ // This can happen even without typing if the editor normalizes/serializes markdown
157
+ // (e.g., whitespace normalization). Logging helps verify what's being changed.
158
+ log.debug('onChange -> applyTextEdits', {
159
+ edits: edits.map((e) => ({ start: e.start, end: e.end, newTextLen: e.newText.length })),
160
+ beforeLen: lastDocumentContent.current.length,
161
+ afterLen: markdown.length,
162
+ });
163
+ lastDocumentContent.current = markdown;
164
+ applyTextEdits(edits, 'typing');
165
+ // Mark this as an editor-initiated change BEFORE calling propOnChange
166
+ // This prevents the prop change from triggering cursor restoration
167
+ lastEditorChangeTime.current = Date.now();
168
+ // Call prop onChange if provided
169
+ propOnChange?.(markdown);
170
+ }
171
+ }, [propOnChange, applyTextEdits, log]);
172
+ // Track cursor position whenever it changes
173
+ const handleCursorChange = useCallback((cursor) => {
174
+ cursorStateRef.current = cursor;
175
+ }, []);
176
+ if (content === null) {
177
+ return (_jsxs("div", { className: "flex items-center justify-center gap-3 min-h-screen text-[var(--vscode-foreground)] opacity-70", children: [_jsx("div", { className: "loading-spinner" }), _jsx("span", { children: "Loading document..." })] }));
178
+ }
179
+ return (_jsxs("div", { className: className, children: [error && (_jsx("div", { className: "fixed top-0 left-0 right-0 px-4 py-2 bg-red-500 text-white text-center z-[1000]", role: "alert", children: error })), _jsx(Editor, { initialContent: content, autoFocus: autoFocus, contentVersion: contentVersion, cursorToRestoreRef: cursorStateRef, onChange: handleChange, onCursorChange: handleCursorChange, assetBaseUri: assetBaseUri, documentDirUri: documentDirUri, imagePathResolution: settings?.imagePathResolution ?? 'document', wikiLinkPromotion: wikiLinkPromotion, resolveLocalAsset: resolveLocalAsset, editable: editable, filePath: filePath, onSelectionContextMenu: onSelectionContextMenu, onSubstitutionDetected: onSubstitutionDetected, sweepRef: sweepRef, annotationKinds: annotationKinds, annotations: annotations, activeAnnotationId: activeAnnotationId, scrollToAnnotation: scrollToAnnotation, onCreateAnnotation: onCreateAnnotation, onActivateAnnotation: onActivateAnnotation, annotationEditorHandleRef: annotationEditorHandleRef, annotationLogger: annotationLogger })] }));
180
+ }
@@ -0,0 +1,18 @@
1
+ export type SweepFn = (oldTerm: string, newTerm: string) => Promise<number>;
2
+ interface AmbientCorrectionPluginProps {
3
+ onSubstitutionDetected: (oldTerm: string, newTerm: string) => void;
4
+ sweepRef?: React.MutableRefObject<SweepFn | null>;
5
+ }
6
+ /**
7
+ * Ambient correction plugin.
8
+ *
9
+ * 1. Detects single-word substitutions (debounced 300 ms) and calls
10
+ * `onSubstitutionDetected` so the parent can show a nudge toast.
11
+ *
12
+ * 2. Populates `sweepRef.current` with a function that performs the confirmed
13
+ * sweep: replaces all remaining occurrences of `oldTerm` with `newTerm`
14
+ * (case-preserving, skipping code blocks and frontmatter) inside a single
15
+ * `editor.update()` call so undo produces one history entry.
16
+ */
17
+ export declare function AmbientCorrectionPlugin({ onSubstitutionDetected, sweepRef, }: AmbientCorrectionPluginProps): null;
18
+ export {};