@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,123 @@
1
+ # ADR-082: `examples/demo` Is the Public Pages Site, and Its Documentation Is Generated from `README.md`
2
+
3
+ **Date:** 2026-08-15
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #3 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ This package's `README.md` is adequate for someone who has already decided to
12
+ adopt it; it does nothing for someone deciding *whether* to. Because
13
+ `@liminis/editor`'s distinguishing claim — that what you type round-trips
14
+ byte-for-byte to markdown on disk — cannot be shown in prose, this issue adds
15
+ a public GitHub Pages site built around a live editor.
16
+
17
+ verveguy/liminis-editor#2 had already grown `examples/demo` into exactly that
18
+ shape: a fixture picker over the shared round-trip corpus, live-editing with
19
+ `onChange` markdown state, importing only the package's public entry points.
20
+ Two questions followed from reusing it:
21
+
22
+ 1. Does the Pages site wrap `examples/demo` as a new top-level app (e.g.
23
+ `site/`), or does `examples/demo` become the deployed artifact directly?
24
+ 2. FR-005–FR-009 require the site to document the seven subpath exports,
25
+ fourteen peer dependencies, the `sideEffects` landmine (ADR-075 §4), the
26
+ `0.x` versioning policy, and the annotation mechanism (ADR-077) — where
27
+ does that content live, given `README.md` already states five of those six
28
+ points accurately in prose?
29
+
30
+ ## Decision
31
+
32
+ ### 1. `examples/demo` *is* the Pages site, not a wrapper around it
33
+
34
+ `scripts/build-site.mjs` builds `examples/demo` and only `examples/demo` —
35
+ the same directory `pnpm demo` and `scripts/build-examples.mjs` already
36
+ build — and its output (`examples/demo/dist`) is uploaded directly as the
37
+ Pages artifact by `.github/workflows/pages.yml`.
38
+
39
+ A new `site/` app importing the demo shell as a component was considered and
40
+ rejected: it would either duplicate the fixture-picker UI or add an
41
+ indirection layer for no functional gain, and it would turn FR-004's "one
42
+ shared artifact, not a duplicated implementation" into something to police
43
+ by discipline rather than something structurally true. With this decision, a
44
+ contributor editing "the demo" is, by construction, also editing the public
45
+ site — there is no second file tree that can silently drift from it.
46
+
47
+ Two additions were made directly to `examples/demo` to carry the site's own
48
+ requirements: a version badge (`App.jsx`, sourced from
49
+ `VITE_LIMINIS_EDITOR_VERSION`, set by the Pages workflow from the triggering
50
+ release's `tag_name` rather than `package.json`'s checked-in, pre-publish
51
+ version) and a Demo/Documentation view toggle.
52
+
53
+ ### 2. The Documentation view renders `README.md`, generated, not re-authored
54
+
55
+ `scripts/generate-demo-docs.mjs` reads the root `README.md` at build time and
56
+ writes a gitignored `examples/demo/src/docs-content.generated.js`, which
57
+ `examples/demo/src/Docs.jsx` renders with `react-markdown` + `remark-gfm`.
58
+ This mirrors the pattern `scripts/generate-demo-fixtures.mjs` established for
59
+ the fixture corpus (ADR-081): a build-time codegen step, not a runtime import
60
+ reaching outside `examples/demo`, and a gitignored output regenerated by
61
+ `pnpm demo`, `pnpm build:examples`, and `pnpm build:site` alike so no code
62
+ path can consume a stale copy.
63
+
64
+ A hand-written `site/docs/*.md` was considered and rejected. Five of the six
65
+ required documentation points (exports, peer dependencies, annotations) were
66
+ already accurate prose in `README.md`; re-deriving them by hand in a second
67
+ location would have been exactly the duplication ADR-081 avoided for
68
+ fixtures, with the same drift risk. The two genuine content gaps — a
69
+ Lexical-free column on the exports table, and the `sideEffects` landmine and
70
+ versioning-policy sections — were instead written into `README.md` itself,
71
+ so the published npm tarball's own shipped documentation improves along with
72
+ the site, rather than the site accumulating content the tarball doesn't
73
+ carry.
74
+
75
+ `tests/adr-citations.test.ts`'s scan of `README.md` + `docs/**` (excluding
76
+ `docs/decisions/`) is not extended to also cover
77
+ `docs-content.generated.js`. Because the generated file is a verbatim copy of
78
+ `README.md`, a dangling `ADR-NNN` citation is caught at the source before it
79
+ ever reaches the generated copy; scanning the copy too would just be a second
80
+ check of the same string.
81
+
82
+ ## Consequences
83
+
84
+ **Good:**
85
+
86
+ - FR-004/SC-004 ("one shared artifact") holds by construction: there is one
87
+ `App.jsx`, one `package.json`, one build output, used by `pnpm demo`,
88
+ `pnpm build:examples`, and the Pages deploy alike.
89
+ - FR-005–FR-009's six documentation points have exactly one copy each, in
90
+ `README.md`, already covered by the existing ADR-citation guard.
91
+ - Editing `README.md` to keep it accurate for npm/GitHub browsing
92
+ automatically keeps the Pages site's Documentation tab accurate too, with
93
+ no second edit to remember.
94
+
95
+ **Bad / accepted:**
96
+
97
+ - `examples/demo`'s `README.md` and source now serve two audiences (a local
98
+ package-demo reader and a public site visitor) rather than one; a change
99
+ made for one has to be considered against the other, though in practice
100
+ the two have stayed aligned so far.
101
+ - Rendering all of `README.md` on the Documentation tab means any section
102
+ that assumes an npm/GitHub reading context (for example, the "This package
103
+ is not published" install-warning language, current as of this decision)
104
+ reads on the public site exactly as written — no site-specific editing
105
+ layer exists to soften it. This self-corrects whenever `README.md` itself
106
+ is updated for the eventual publish flip; it is not something this
107
+ decision defers solving, since no site-only content divergence was wanted
108
+ in the first place.
109
+ - `docs-content.generated.js`, like `fixtures.generated.js`, is gitignored,
110
+ so a reviewer checking "does the Documentation tab still render correctly"
111
+ after a `README.md` edit has to run the generator locally rather than
112
+ reading it in the PR diff.
113
+
114
+ ## References
115
+
116
+ - Issue #3 (this decision), verveguy/liminis-editor#2 (the web shell this
117
+ decision reuses as the site)
118
+ - `docs/decisions/adr-081.md` (the codegen pattern this decision applies a
119
+ second time, to `README.md` instead of the fixture corpus)
120
+ - `docs/decisions/adr-075.md` §4 (the `sideEffects` landmine now documented
121
+ in `README.md` and, by extension, on the site)
122
+ - `docs/decisions/adr-077.md` (the annotation mechanism now documented in
123
+ `README.md` and, by extension, on the site)
@@ -0,0 +1,204 @@
1
+ # ADR-083: Backslash-Escape Provenance Is Captured at Parse Time and Carried Through as a Per-Character Style Hint
2
+
3
+ **Date:** 2026-08-16
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #17 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ A backslash-escaped underscore lost its escape on a no-edit round trip
12
+ (`A backslash\_underscore` → `A backslash_underscore`), while a backslash-escaped
13
+ asterisk on the same line did not. This is not cosmetic: the escape exists to stop
14
+ `_` from being read as an emphasis delimiter, and dropping it silently, on a save
15
+ where the user changed nothing, is the same failure class as #973 (markdown
16
+ serializer non-idempotence).
17
+
18
+ The root cause is structural, not a stray regex bug. `fromMarkdown` (standard
19
+ CommonMark parsing) discards escape provenance at parse time: `\_underscore` and
20
+ `_underscore` both decode to the identical mdast `text` value
21
+ (`"_underscore"`) — there is no flag anywhere in mdast or Lexical recording "this
22
+ character had a backslash in front of it in the original file." Two existing
23
+ `stringify.ts` post-processes (intraword-underscore-unescape, added for #901;
24
+ bracket-preservation, added for #921) strip mdast-util-to-markdown's own
25
+ conservative default escaping of `_`/`[`/`]` back off, because CommonMark doesn't
26
+ always require escaping those characters and the library over-escapes them by
27
+ default. Both post-processes operate purely on the shape of the final string, so
28
+ they cannot distinguish "the library added this escape and it's unneeded" from
29
+ "the user wrote this escape and meant it" — the two cases are byte-identical once
30
+ parsed. `\*` never had this problem only because CommonMark's own rule requires
31
+ escaping `*` unconditionally in the position it appears, so mdast-util-to-markdown
32
+ re-escapes it regardless, and no Liminis post-process ever touches it.
33
+
34
+ Two designs were available for recovering the lost provenance:
35
+
36
+ 1. Embed an invisible Private-Use-Area marker character directly in Lexical's
37
+ live text content at parse/import time, stripped back off at stringify time —
38
+ mirroring `annotate-sentinels.ts`'s existing PUA-sentinel mechanism.
39
+ 2. Capture provenance at parse time, before anything else touches the tree, and
40
+ carry it through Lexical as metadata attached to the `TextNode` rather than
41
+ embedded in its text content — restoring the real backslash only inside a
42
+ single, transient `stringifyMarkdown` call.
43
+
44
+ ## Decision
45
+
46
+ ### 1. Capture at parse time; never re-derive downstream
47
+
48
+ `splitTextNodeEscapes` (called by `splitEscapedPunctuation`, the last step of
49
+ `parseMarkdown`, after `annotateEmphasisMarkers`) replay-decodes each `text`
50
+ node's raw source span character-by-character — consuming a backslash plus one
51
+ of CommonMark's escapable ASCII punctuation characters as a single decoded
52
+ character, exactly as `fromMarkdown` itself does — and compares the result
53
+ against `node.value`. Whenever they don't match exactly (an HTML entity or other
54
+ untracked transform sharing the span), the node is left untouched rather than
55
+ split at a possibly-wrong offset: a missed protection reproduces today's
56
+ pre-existing defect, but a wrong split would corrupt text. This mirrors
57
+ `recordOffsetSpan`'s own width-mismatch bail-out in `mdastToLexical.ts`, an
58
+ existing precedent for the same "decline rather than guess" trade-off.
59
+
60
+ Otherwise, the node is split into siblings at every backslash-escaped instance
61
+ of the seven characters this issue scopes (`* _ \` [ ] # \`, per the issue's own
62
+ named set): untouched runs stay ordinary `text` nodes, and each protected
63
+ character becomes its own single-character `text` node carrying
64
+ `data._forceEscape = true`. This is deliberately uniform across all seven
65
+ characters, not special-cased to the two or three Research suspected were
66
+ actually broken — a character the library already escapes correctly (`*`,
67
+ backtick, line-start `#`, a lone `\`) simply flows through the same mechanism
68
+ with no behavior change, since it only ever *preserves* an escape the user
69
+ actually wrote and never invents one. That by-construction property is what
70
+ satisfies the "must not over-escape" requirement, rather than a per-character
71
+ heuristic that could be wrong for an untested case.
72
+
73
+ ### 2. Carry as a per-character `TextNode` style hint, not a live-text sentinel
74
+
75
+ Design (1) above — a PUA marker spliced into editable Lexical text — was
76
+ rejected. Unlike `annotate-sentinels.ts`'s existing use, which is scoped to a
77
+ single, transient export call, this marker would persist in the actual
78
+ *editable* document for the whole session whenever the user opens and edits the
79
+ file, not just a one-shot round trip: risk of a stray PUA character surviving
80
+ into cursor placement, selection, copy/paste, spellcheck, or a newly-typed
81
+ document abutting it.
82
+
83
+ Instead, `setForceEscape` (`mdastToLexical.ts`'s `convertText`) sets a
84
+ `--md-force-escape:1` style hint on the `TextNode`, mirroring the established
85
+ `--md-emphasis-marker`/`--md-strong-marker` precedent that already carries the
86
+ source's original `_`/`*` marker character through the same round trip. This has
87
+ zero footprint on editable content. Both `convertTextNode` (the single-node
88
+ export path) and `convertFormattedRun` (the merged-run path taken when a node
89
+ sits inside emphasis/strong, or when a live annotation mark forces adjacent
90
+ same-format siblings to merge) read the hint in `lexicalToMdast.ts` — an escaped
91
+ character inside emphasis/strong (e.g. `*text \_x\_*`) always takes the
92
+ merged-run path even on a pure no-edit round trip, so reading the hint in only
93
+ one of the two would leave that adjacency case, called out explicitly in the
94
+ issue's edge cases, unfixed.
95
+
96
+ ### 3. Restore via a transient placeholder and a dedicated node type, not by editing the existing regexes
97
+
98
+ Making the two existing blind-strip post-processes provenance-aware — teaching
99
+ them to recognize a real backslash as "leave alone" — was considered and
100
+ rejected. Their patterns have no per-node knowledge; they operate on the final
101
+ concatenated string. Testing confirmed that simply restoring the real backslash
102
+ before those regexes run still lets the bracket-preservation regex (which has no
103
+ adjacency/flanking guard, unlike the underscore one) misfire and strip a
104
+ genuinely user-escaped `\[...\]` pair — stacking two independent escape
105
+ decisions that can disagree.
106
+
107
+ Instead, `convertForceEscapeTextNodes` (a `stringify.ts` pre-process) converts
108
+ every `{type: 'text', data: {_forceEscape: true}}` node into a dedicated
109
+ `escapedChar` node, whose custom `toMarkdown` handler emits a transient,
110
+ backslash-free placeholder (`\u{E004}<char>\u{E004}`, distinct from
111
+ `annotate-sentinels.ts`'s `E000`-`E003` range, module-local to `stringify.ts`
112
+ and never exported) instead of running the character through the normal
113
+ text-escaping path. Being backslash-free, the placeholder is provably invisible
114
+ to both existing post-processes during their pass, so **neither one needed to
115
+ change**. Only after every other post-process has already run does a final step
116
+ replace each placeholder-wrapped character with a real `\<char>`, unconditionally
117
+ restoring the backslash regardless of whatever the default handler emitted
118
+ underneath. The placeholder never touches Lexical or live document state — like
119
+ `annotate-sentinels.ts`'s own tokens, it exists only inside a single
120
+ `stringifyMarkdown` call, on a mdast tree reconstructed fresh from Lexical at
121
+ export time and discarded immediately after.
122
+
123
+ Two adjacent force-escaped characters in the source (e.g. `\*\*`) can reach this
124
+ pre-process already fused into one multi-character node, because Lexical's own
125
+ reconciliation merges adjacent `TextNode`s sharing identical format and style,
126
+ and two independent single-character force-escape nodes share the same
127
+ `--md-force-escape:1` style. Since only single-character force-escaped nodes are
128
+ ever produced upstream, every character of a fused node was independently
129
+ force-escaped, so the pre-process expands such a node back into one `escapedChar`
130
+ node per character rather than assuming length 1.
131
+
132
+ An annotate-serialize sentinel token (see `annotate-sentinels.ts`, ADR-077's
133
+ underlying mechanism) can also land inside a force-escaped node's text, if that
134
+ exact `TextNode` instance happens to be a live annotation mark's boundary leaf.
135
+ The pre-process uses a new `splitOnSentinelTokens` helper (added to
136
+ `annotate-sentinels.ts`, alongside its existing `stripAnnotateSentinels`) to
137
+ split those parts out first and leave them as plain, unescaped text — otherwise
138
+ the token's own id characters would themselves come back out individually
139
+ backslash-escaped, breaking annotate mode's "differs from a plain export only by
140
+ its sentinel tokens" invariant (#970) for any fixture containing a force-escaped
141
+ character.
142
+
143
+ ## Consequences
144
+
145
+ **Good:**
146
+
147
+ - `\*`, `\_`, `` \` ``, `\[`, `\]`, `\#`, and `\\` all round-trip byte-identically
148
+ in a meaningful context, and the fix is a fixed point on a second pass (ADR-076)
149
+ — see the `17-*` fixture group in
150
+ `src/app/mapper/__tests__/fixtures/roundtrip/README.md`.
151
+ - The fix is provably non-over-escaping by construction: it only ever restores a
152
+ backslash the source actually had, never adds one, so `898-underscore-link.md`
153
+ (`mac_onboarding.sh`) and the bracket-related `known-defects/` fixtures needed
154
+ no changes.
155
+ - Neither existing blind-strip post-process was modified, eliminating the risk
156
+ (flagged during planning) of two independent escape-decision mechanisms
157
+ disagreeing on the same character.
158
+ - `recordOffsetSpan`'s width-mismatch bail-out in `mdastToLexical.ts` needed no
159
+ code change — splitting a text node at escape boundaries is exactly the kind
160
+ of source-width/decoded-length mismatch it was already designed to decline
161
+ gracefully on, one sub-span at a time instead of one whole node at a time. This
162
+ is a net improvement to `offset-spans.test.ts`'s existing coverage: a plain
163
+ run of text beside an escape now gets a valid, usable span where previously the
164
+ entire containing node was dropped.
165
+
166
+ **Bad / accepted:**
167
+
168
+ - The escaped-backslash-then-bare-underscore edge case (`\\_`) is not
169
+ byte-identical on the first pass: the restored literal backslash is not
170
+ alphanumeric, so the pipeline's own pre-existing, pre-#17 conservative default
171
+ (intraword-underscore-unescape only fires when *both* flanking characters are
172
+ alphanumeric) leaves the following bare underscore escaped too. This is not a
173
+ new over-escape introduced by this fix — `898-underscore-link.md` already
174
+ accepts the same conservative policy for other non-alphanumeric-flanked cases
175
+ — so `17-escaped-backslash-then-underscore.md` carries an `.expected.md`
176
+ sidecar rather than asserting byte-identity, per ADR-076's normalization
177
+ allowance. The second pass is still a fixed point.
178
+ - A character reference (e.g. `&amp;`) anywhere in the same `fromMarkdown`-merged
179
+ text run as an escaped target character suppresses protection for the whole
180
+ run, not just the entity reference's own span, because `fromMarkdown` merges
181
+ entity references and surrounding literal text into one `text` node before
182
+ `splitTextNodeEscapes` ever sees it. This is a narrow, defect-shaped
183
+ limitation — not a regression, since no protection existed there before this
184
+ fix either — and is documented at the bail-out site in `parse.ts` and covered
185
+ by a dedicated test in `parse.test.ts`.
186
+ - `convertTextNode` and `convertFormattedRun` in `lexicalToMdast.ts` now
187
+ duplicate a third piece of marker-reading logic (alongside the existing
188
+ `--md-strong-marker`/`--md-emphasis-marker` reads), increasing the surface
189
+ area that could drift out of sync in a future change to one but not the other.
190
+ Accepted because it follows an established, already-reviewed pattern in this
191
+ codebase rather than inventing a new one.
192
+
193
+ ## References
194
+
195
+ - Issue #17 (this decision)
196
+ - `docs/decisions/adr-076.md` (the fixed-point round-trip guarantee this fix
197
+ upholds, and the `.expected.md`/normalization allowance used for the `\\_`
198
+ edge case)
199
+ - `docs/decisions/adr-077.md` (the annotation mechanism whose sentinel tokens
200
+ this fix's placeholder must stay distinguishable from)
201
+ - `src/app/mapper/__tests__/fixtures/roundtrip/README.md`, `## The 973-* fixture
202
+ set` section (the `--md-emphasis-marker`/`--md-strong-marker` style-hint
203
+ precedent this fix's `--md-force-escape` hint mirrors) and `## The 17-*
204
+ fixture set` section (this fix's own as-built documentation)
@@ -0,0 +1,179 @@
1
+ # ADR-084: Ordered Task-Item Checkboxes Are Rendered by `CustomListItemNode` Itself, Never by Lexical's `listType === 'check'` Gate
2
+
3
+ **Date:** 2026-08-16
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #12 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ An ordered task item (`1. [ ] Task one`) used to render its checkbox marker
12
+ as literal `[ ] `/`[x] ` text prepended to the item's first paragraph by
13
+ `convertListItem` (`src/app/mapper/mdastToLexical.ts`), because
14
+ `@lexical/list`'s checklist rendering — DOM attributes/classes, click
15
+ handling (`@lexical/react`'s `CheckListPlugin`), and keyboard handling — is
16
+ entirely gated on the *parent list's* `listType` being `'check'`, and
17
+ Lexical has no `'check'`-typed list that is also numbered. The per-item
18
+ checked state was already stored correctly and independently on
19
+ `CustomListItemNode.__taskChecked` (added for #905/#914's mixed-list
20
+ problem); only the *rendering* declined to use it.
21
+
22
+ Reading the pinned `@lexical/list@0.44.0`/`@lexical/react@0.44.0` source
23
+ directly (rather than trusting its public API surface) confirmed every
24
+ mechanism a checkbox needs is private and individually keyed off
25
+ `listType === 'check'`:
26
+
27
+ - `ListItemNode.updateListItemDOM`/`updateListItemChecked`/
28
+ `$setListItemThemeClassNames` (DOM `role`/`aria-checked`/theme classes) —
29
+ all unexported.
30
+ - `registerCheckList`'s `handleCheckItemEvent`/`handleClick`/
31
+ `getActiveCheckListItem` (click and keyboard handling) — gated on the DOM
32
+ list element's `__lexicalListType === 'check'` property, unexported.
33
+ - `ListNode`'s own `$config().$transform` (`updateChildrenListItemValue`)
34
+ actively **resets** any list item's raw `__checked` back to `undefined`
35
+ whenever the parent list isn't `'check'`-typed — a live transform, not a
36
+ one-time check.
37
+
38
+ ## Decision
39
+
40
+ ### 1. `CustomListItemNode` renders its own ordered-task checkbox, from `__taskChecked`
41
+
42
+ `CustomListItemNode.createDOM`/`updateDOM` call `super.createDOM`/
43
+ `super.updateDOM` first (stock behavior already strips `role`/`tabIndex`/
44
+ `aria-checked` for a non-`'check'` parent), then layer on a checkbox
45
+ affordance — `role="checkbox"`, `aria-checked`, and new
46
+ `editor-listitem-ordered-checked`/`-unchecked` CSS classes (applied
47
+ directly via `classList`, not wired through Lexical's `editorTheme` object
48
+ the way stock's own `listitemChecked`/`listitemUnchecked` are) — driven
49
+ directly by `this.__taskChecked`, gated on the parent list's `listType`
50
+ being `'number'` and the item being a leaf (mirroring stock's own
51
+ `!$isListNode(getFirstChild())` guard). This reuses the "dedicated
52
+ node-owned mechanism over a pattern hack" precedent already established by
53
+ ADR-070, applied here to a private-library gate rather than a markdown
54
+ pattern.
55
+
56
+ The alternative — finding some way to make Lexical's own `'check'`-typed
57
+ rendering also apply to a `'number'`-typed list — was not pursued: every
58
+ mechanism above is private, unexported, and would need forking, not
59
+ extending, and `ListNode`'s reset transform would still need to be defeated
60
+ regardless (see below).
61
+
62
+ New CSS classes (`editor-listitem-ordered-checked`/`-unchecked` in
63
+ `styles.css`) were required rather than reusing the existing
64
+ `editor-listitem-checked`/`-unchecked`: those set `list-style-type: none`
65
+ and pull the marker box back with a negative `margin-left`, which would
66
+ suppress `<ol>`'s native decimal numbering. The new classes instead claim
67
+ space via the item's own `padding-left`, leaving the native marker
68
+ undisturbed in the `<ol>`'s own padding area.
69
+
70
+ ### 2. Toggling an ordered item's checkbox must call `setTaskChecked()` exclusively, never `setChecked()`/`toggleChecked()`
71
+
72
+ `OrderedTaskListPlugin` (`src/app/editor/OrderedTaskListPlugin.tsx`) — a new
73
+ root-element click listener, since `CheckListPlugin`'s own listener never
74
+ fires for a `'number'`-typed list — replicates `@lexical/list`'s own
75
+ `handleCheckItemEvent` hit-box geometry (`clientX` vs. the rendered
76
+ `::before` pseudo-element's width, via `calculateZoomLevel` from
77
+ `@lexical/utils`) so clicking anywhere across the visible checkbox toggles
78
+ it, matching the feel of the unordered checklist. On a hit, it calls
79
+ `node.setTaskChecked(!node.getTaskChecked())` inside `editor.update()`.
80
+
81
+ This is a hard rule, not a style preference. `ListNode`'s own transform
82
+ (`updateChildrenListItemValue`) resets any list item's *raw* `__checked`
83
+ field back to `undefined` on every reconciliation pass, for any list whose
84
+ `listType !== 'check'` — which every ordered list is. `CustomListItemNode`
85
+ overrides `setChecked()` to sync `__taskChecked` from the same argument (to
86
+ support the #905/#914 live-promotion case, where clicking an *unordered*
87
+ checklist's checkbox on a previously-plain item must promote it to a real
88
+ task). If an ordered item's toggle handler called `setChecked()` or
89
+ `toggleChecked()`, it would look correct on the triggering render and then
90
+ be silently reset — `__checked` back to `undefined`, cascading through the
91
+ override into `__taskChecked` too — on the very next reconciliation. The
92
+ importer (`convertListItem`) already avoided this by never writing raw
93
+ `__checked` for ordered items (`useChecked` stays `undefined`); the click
94
+ handler must preserve that invariant symmetrically. Because `__checked`
95
+ stays permanently `undefined` for ordered items under this rule, the reset
96
+ transform's own guard (`__checked != null`) never fires — no other code
97
+ path needed to change to avoid it.
98
+
99
+ ### 3. `stringify.ts`'s `addCheckboxTextToOrderedLists` is left in place, unmodified, despite being redundant
100
+
101
+ The export side (`src/app/mapper/lexicalToMdast.ts`) already read
102
+ `getTaskChecked()` rather than the gated `getChecked()`, so once the
103
+ importer stopped prefixing marker text, `checkedForOutput` simply became
104
+ the item's real per-item state with no export-side code change needed.
105
+
106
+ Separately, `src/markdown/stringify.ts` contains its own workaround,
107
+ `addCheckboxTextToOrderedLists`, which manually rewrites an ordered list's
108
+ mdast tree to prepend literal `[ ]`/`[x] ` text before `toMarkdown()` runs,
109
+ on the documented premise that "GFM only outputs checkboxes for unordered
110
+ lists." That premise does not hold for this repo's pinned
111
+ `mdast-util-gfm-task-list-item@2.0.0` — its `listItemWithTaskListItem`
112
+ handler already matches `(?:[*+-]|\d+\.)` and emits a marker after either
113
+ kind of list marker natively. The function is therefore redundant, but not
114
+ incorrect: tracing a concrete example through the full pipeline confirmed
115
+ it still fires exactly once per checked/unchecked item and still produces
116
+ byte-identical output to the pre-fix behavior. Removing it was considered
117
+ and rejected for this issue — it has no required benefit within this
118
+ issue's scope (a rendering defect, not a serialization one) and would add
119
+ verification surface against a part of the pipeline this issue's spec
120
+ explicitly states does not need to change. It is left as a documented,
121
+ deliberate trap: a future contributor who finds it and "cleans it up"
122
+ without reading this ADR should not be surprised that doing so is safe on
123
+ its own (the GFM dependency already covers it) but was out of scope here.
124
+
125
+ ## Consequences
126
+
127
+ **Good:**
128
+
129
+ - FR-001–FR-003 hold: an ordered task item renders a real checkbox with no
130
+ editable marker text, sourced from its own stored state, independent of
131
+ the parent list's rendering mode.
132
+ - FR-004/FR-005 hold: toggling works via a dedicated plugin and round-trips
133
+ to exactly one marker in the exported Markdown, with FR-006's fixed-point
134
+ guarantee unaffected — the state model and markdown layers needed no
135
+ changes.
136
+ - FR-007/FR-008 hold structurally, not by extra guarding: a plain item in a
137
+ mixed ordered list has `__taskChecked === null`, so
138
+ `CustomListItemNode`'s own gate already excludes it from checkbox
139
+ rendering, and unordered checklists are entirely untouched by any of this
140
+ (different classes, different plugin, same `CheckListPlugin` as before).
141
+ - The reset-transform trap is now a one-time, well-understood constraint
142
+ documented in one place (this ADR, plus inline comments at both
143
+ `CustomListItemNode.setChecked()` and `OrderedTaskListPlugin`) rather than
144
+ a bug waiting to be rediscovered by a future toggle-handling change.
145
+
146
+ **Bad / accepted:**
147
+
148
+ - Two independent checkbox rendering/interaction code paths now exist —
149
+ Lexical's own for unordered lists, this package's own for ordered lists —
150
+ rather than one. This is inherent to the constraint (Lexical's mechanisms
151
+ are private and gate-locked to `'check'`), not a choice that could be
152
+ designed away within this issue's scope.
153
+ - `OrderedTaskListPlugin`'s hit-box geometry duplicates `@lexical/list`'s
154
+ private `handleCheckItemEvent` formula by copying it, not importing it
155
+ (it isn't exported). A future `@lexical/list` upgrade that changes that
156
+ formula would need this plugin updated in step, silently, with no type
157
+ error to catch the drift.
158
+ - `addCheckboxTextToOrderedLists` remains dead-but-correct code with a
159
+ now-inaccurate docstring premise ("GFM only outputs checkboxes for
160
+ unordered lists"), left in place deliberately rather than fixed, which
161
+ future readers of that file alone (not this ADR) may find confusing.
162
+
163
+ ## References
164
+
165
+ - Issue #12 (this decision)
166
+ - `docs/decisions/adr-070.md` ("Dedicated Lexical Node Types vs.
167
+ Paragraph-Pattern Detection") — the general precedent this decision
168
+ applies to a private-library rendering gate rather than a markdown
169
+ pattern.
170
+ - `docs/decisions/adr-076.md` ("Markdown Serialization Is a Fixed Point") —
171
+ the fixed-point guarantee FR-006 restates for this issue; no new
172
+ infrastructure was needed to satisfy it.
173
+ - `src/app/editor/nodes/CustomListItemNode.tsx` — `__taskChecked`, the
174
+ `createDOM`/`updateDOM` override, and the `setChecked()` override this
175
+ decision requires ordered-item toggling to route around.
176
+ - `src/app/editor/OrderedTaskListPlugin.tsx` — the click-to-toggle
177
+ implementation.
178
+ - `src/markdown/stringify.ts` — `addCheckboxTextToOrderedLists`, left
179
+ unmodified per this decision.
@@ -0,0 +1,106 @@
1
+ # `<Editor>` and the host seam
2
+
3
+ ## `<Editor>`
4
+
5
+ ```tsx
6
+ import { Editor } from '@liminis/editor'
7
+ ```
8
+
9
+ The editor takes markdown in and hands markdown back. Lexical state is an
10
+ implementation detail you never have to touch.
11
+
12
+ ### Content
13
+
14
+ | Prop | Type | Notes |
15
+ |---|---|---|
16
+ | `initialContent` | `string` | Markdown text. **Required.** |
17
+ | `onChange` | `(markdown: string) => void` | Fires on edit with the serialized markdown. **Required.** |
18
+ | `contentVersion` | `number` | Bump to force a re-import of `initialContent`. Without it, changing `initialContent` alone does not reload the document — that is deliberate, so a controlled `onChange` loop does not fight the user's cursor. |
19
+ | `editable` | `boolean` | Default `true`. `false` renders read-only. A `toolbar`-surfaced annotation create affordance (see the Annotations section of the package README) remains reachable via selection either way; formatting controls do not render when read-only. |
20
+ | `filePath` | `string` | The path of the file being edited. Drives file-type-specific UI (e.g. `.mdc`). |
21
+
22
+ ### Cursor and selection
23
+
24
+ | Prop | Type | Notes |
25
+ |---|---|---|
26
+ | `cursorToRestoreRef` | `RefObject<CursorState \| null>` | A ref, not a value — it is read inside effects, not during render, so restoring a cursor never triggers a render pass. |
27
+ | `onCursorChange` | `(cursor: CursorState) => void` | Fires as the caret moves. Pair with `cursorToRestoreRef` to persist cursor position across document switches. |
28
+ | `onSelectionContextMenu` | `(event: SelectionContextMenuEvent) => void` | Fires on context menu over a selection. Your app renders the menu. |
29
+
30
+ ### Assets and links
31
+
32
+ | Prop | Type | Notes |
33
+ |---|---|---|
34
+ | `assetBaseUri` | `string` | Base URI images resolve against. |
35
+ | `documentDirUri` | `string` | URI of the directory containing the document, for relative-path resolution. |
36
+ | `imagePathResolution` | `ImagePathResolution` | How relative image paths are interpreted. |
37
+ | `resolveLocalAsset` | `(relativePath: string) => Promise<string \| null>` | Resolve a workspace-relative path to a data URL. Return `null` for "not found" — the editor renders a broken-image placeholder rather than throwing. |
38
+
39
+ ### Corrections
40
+
41
+ | Prop | Type | Notes |
42
+ |---|---|---|
43
+ | `onSubstitutionDetected` | `(oldTerm: string, newTerm: string) => void` | Fires after a debounce when a single-word substitution is detected. |
44
+ | `sweepRef` | `MutableRefObject<SweepFn \| null>` | Populated with a sweep function while the ambient-correction plugin is active. |
45
+
46
+ ### Annotations
47
+
48
+ All optional; supplying `annotationKinds` is what turns the mechanism on. See
49
+ [`annotations.md`](./annotations.md).
50
+
51
+ | Prop | Type | Notes |
52
+ |---|---|---|
53
+ | `annotationKinds` | `AnnotationKindConfigs` | Per-kind configuration. Omit it and no annotation module is loaded. |
54
+ | `annotations` | `Annotation[]` | Host-supplied annotations to render markers for, already resolved. |
55
+ | `activeAnnotationId` | `string \| null` | Which annotation is active, for marker styling. |
56
+ | `scrollToAnnotation` | `{ id: string; nonce: number } \| null` | Host-driven scroll signal. Bump `nonce` to re-scroll to the same id. |
57
+ | `onCreateAnnotation` | `(event: AnnotationCreateEvent) => void` | Fires when a user creates one via a kind's create affordance. |
58
+ | `onActivateAnnotation` | `(id: string) => void` | Fires when a marker is activated. Your app opens its own panel. |
59
+ | `annotationEditorHandleRef` | `MutableRefObject<AnnotationEditorHandle \| null>` | Imperative handle for the mounted editor's live marks. |
60
+ | `annotationLogger` | `{ warn(message, ...args): void }` | Injected, so the package never imports a host logger. |
61
+
62
+ ## The host seam
63
+
64
+ The package boundary is drawn at **persistence**: text ranges, marks, rendering
65
+ and in-document UX are the package's; storage, lifecycle, identity and
66
+ higher-level panels are yours. Everything the package needs from you crosses
67
+ that seam through `EditorHostServices`.
68
+
69
+ Every member is optional and has a safe default, so `<Editor>` works with no
70
+ provider at all. Wrap it when you want to supply real services:
71
+
72
+ ```tsx
73
+ import { EditorHostProvider, Editor } from '@liminis/editor'
74
+
75
+ <EditorHostProvider services={{ logger, bridge, resolveWikiLinks, notifyError }}>
76
+ <Editor initialContent={md} onChange={setMd} />
77
+ </EditorHostProvider>
78
+ ```
79
+
80
+ ### `EditorHostServices`
81
+
82
+ | Member | Type | Purpose |
83
+ |---|---|---|
84
+ | `bridge` | `EditorHostBridge` | The channel to your environment. Two methods only: `postMessage(message)` and `addMessageHandler(handler)` returning an unsubscribe. Every higher-level helper (`requestInit`, `applyTextEdits`, `writeAsset`, `openLink`) is built on `postMessage` inside the package, so every host adapter emits byte-identical payloads. Defaults to a no-op bridge. |
85
+ | `logger` | `(namespace: string) => EditorLogger` | Namespaced logger factory. `EditorLogger` is `{ debug, info, warn, error }`, structurally compatible with most app loggers. |
86
+ | `resolveWikiLinks` | `(targets: string[]) => Promise<Record<string, string \| null>>` | Map wiki-link targets to existing paths; `null` for unresolved. Drives the "this page does not exist yet" styling. |
87
+ | `onScrollToAnchor` | `(cb: (anchor: string) => void) => () => void` | Subscribe to host-driven scroll requests. Returns an unsubscribe. |
88
+ | `notifyError` | `(message: string, description?: string) => void` | Surface a user-visible error. Defaults to a console warning. |
89
+ | `corrections` | `CorrectionHostServices` | Persistence and knowledge-graph services backing the correction feature: `readCorrections`, `writeCorrections`, `suggestEntities`, `suggestPassages`, `applyCorrections`. The in-editor correction UI is package-side; the file and the graph are yours. |
90
+
91
+ Read the resolved set inside your own components with `useEditorHost()`.
92
+ `resolveHostServices()` applies the defaults if you need them directly.
93
+
94
+ ### The message contract
95
+
96
+ `@liminis/editor/contract` exports the `HostToUIMessage` / `UIToHostMessage`
97
+ shapes and nothing else. It exists so a boundary that must not pull renderer
98
+ code into its bundle — an Electron preload script is the canonical case — can
99
+ name the message types.
100
+
101
+ ```ts
102
+ import type { HostToUIMessage, UIToHostMessage } from '@liminis/editor/contract'
103
+ ```
104
+
105
+ Use `import type`. A value import from this entry pulls in `zod`, which is
106
+ exactly what the entry exists to avoid.