@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,439 @@
1
+ import { toMarkdown } from 'mdast-util-to-markdown';
2
+ import { gfmToMarkdown } from 'mdast-util-gfm';
3
+ import { mathToMarkdown } from 'mdast-util-math';
4
+ import { defListToMarkdown } from 'mdast-util-definition-list';
5
+ import { gfmFootnoteToMarkdown } from 'mdast-util-gfm-footnote';
6
+ import { frontmatterToMarkdown } from 'mdast-util-frontmatter';
7
+ import { stripAnnotateSentinels, splitOnSentinelTokens } from './annotate-sentinels.js';
8
+ // Wiki-link options: use | as the alias divider (Obsidian/Foam style)
9
+ const wikiLinkOptions = { aliasDivider: '|' };
10
+ /**
11
+ * Compute the mdast `spread` property on lists and list items, which
12
+ * controls whether blank lines are added between items (and, for a list
13
+ * item, between that item's own children).
14
+ *
15
+ * The incoming `node.spread` (already set by lexicalToMdast.ts's
16
+ * convertListNode/convertListItemNode from the list's CustomListNode, which
17
+ * carries the source document's original loose/tight state through the
18
+ * round trip) is the baseline and is never downgraded to false here — that
19
+ * would silently re-introduce the loose-list-forced-tight defect. It can
20
+ * only be upgraded to true: an item needs its own children ("spread")
21
+ * blank-line-separated when it holds more than one non-nested-list child
22
+ * (multiple paragraphs, or a paragraph alongside a code block/table/
23
+ * blockquote): abutting them with no blank line is ambiguous or misparses
24
+ * on the way back in. A nested list doesn't count towards this — it already
25
+ * reads unambiguously right after preceding content with no blank line.
26
+ * Per CommonMark, if any item in a list is "loose", the whole list must
27
+ * render loose too, or re-parsing collapses the blank lines and misreads
28
+ * item boundaries — so a list's spread is the OR of its own baseline and its
29
+ * items' spread.
30
+ */
31
+ function computeListSpread(node) {
32
+ if (!node || typeof node !== 'object') {
33
+ return node;
34
+ }
35
+ if (node.type === 'listItem') {
36
+ const children = (node.children || []).map(computeListSpread);
37
+ const nonListChildCount = children.filter((c) => c.type !== 'list').length;
38
+ return {
39
+ ...node,
40
+ spread: node.spread === true || nonListChildCount > 1,
41
+ children,
42
+ };
43
+ }
44
+ if (node.type === 'list') {
45
+ const children = (node.children || []).map(computeListSpread);
46
+ return {
47
+ ...node,
48
+ spread: node.spread === true || children.some((item) => item.spread === true),
49
+ children,
50
+ };
51
+ }
52
+ // Recursively process children
53
+ if (node.children && Array.isArray(node.children)) {
54
+ return {
55
+ ...node,
56
+ children: node.children.map(computeListSpread),
57
+ };
58
+ }
59
+ return node;
60
+ }
61
+ /**
62
+ * Add checkbox text to ordered list items that have checked property.
63
+ *
64
+ * GFM only outputs checkboxes ([ ] or [x]) for unordered lists.
65
+ * For ordered lists with checkboxes (common in Foam/Obsidian), we need to
66
+ * manually add the checkbox text to the first paragraph.
67
+ */
68
+ function addCheckboxTextToOrderedLists(root) {
69
+ function processNode(node) {
70
+ if (node.type === 'list' && node.ordered === true) {
71
+ return {
72
+ ...node,
73
+ children: node.children.map((listItem) => {
74
+ // Only process list items that have a checked property
75
+ if (listItem.checked === true || listItem.checked === false) {
76
+ const checkbox = listItem.checked ? '[x] ' : '[ ] ';
77
+ // Find the first paragraph and prepend the checkbox text
78
+ if (listItem.children && listItem.children.length > 0) {
79
+ const firstChild = listItem.children[0];
80
+ if (firstChild.type === 'paragraph' && firstChild.children && firstChild.children.length > 0) {
81
+ const firstInline = firstChild.children[0];
82
+ if (firstInline.type === 'text') {
83
+ // If text already starts with a checkbox marker, don't duplicate it
84
+ if (firstInline.value.match(/^\[( |x|X)\]\s+/)) {
85
+ return listItem;
86
+ }
87
+ // Prepend checkbox to existing text
88
+ return {
89
+ ...listItem,
90
+ checked: null, // Clear checked so gfm doesn't output it again
91
+ children: [
92
+ {
93
+ ...firstChild,
94
+ children: [
95
+ { ...firstInline, value: checkbox + firstInline.value },
96
+ ...firstChild.children.slice(1),
97
+ ],
98
+ },
99
+ ...listItem.children.slice(1),
100
+ ],
101
+ };
102
+ }
103
+ else {
104
+ // First inline is not text, insert checkbox text before it
105
+ return {
106
+ ...listItem,
107
+ checked: null,
108
+ children: [
109
+ {
110
+ ...firstChild,
111
+ children: [
112
+ { type: 'text', value: checkbox },
113
+ ...firstChild.children,
114
+ ],
115
+ },
116
+ ...listItem.children.slice(1),
117
+ ],
118
+ };
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return listItem;
124
+ }),
125
+ };
126
+ }
127
+ // Recursively process children
128
+ if (node.children && Array.isArray(node.children)) {
129
+ return {
130
+ ...node,
131
+ children: node.children.map(processNode),
132
+ };
133
+ }
134
+ return node;
135
+ }
136
+ return processNode(root);
137
+ }
138
+ // Transient placeholder codepoint wrapping a force-escaped character during
139
+ // stringification (#17). Distinct from `annotate-sentinels.ts`'s E000-E003
140
+ // range, module-local to this file, and never exported: it exists only
141
+ // between `convertForceEscapeTextNodes` (below) and the final
142
+ // placeholder-restoration post-process at the end of `stringifyMarkdown`, on
143
+ // a mdast tree reconstructed fresh from Lexical at export time and discarded
144
+ // immediately after — it never touches Lexical or live document state. Being
145
+ // backslash-free, it is provably invisible to the existing intraword-
146
+ // underscore and bracket-preservation post-processes below, so neither one
147
+ // needs to change to account for it.
148
+ const FORCE_ESCAPE_PLACEHOLDER = '\u{E004}';
149
+ // The character class the placeholder-restoration post-process (below) is
150
+ // allowed to wrap in a backslash. Kept narrow — rather than matching any
151
+ // character between two placeholder codepoints — so that if the placeholder
152
+ // codepoint itself ever showed up in real content (e.g. pasted from
153
+ // somewhere, or inside a code span the pre-process didn't anticipate), this
154
+ // step can't misfire and inject a backslash in front of arbitrary text (#17).
155
+ const FORCE_ESCAPE_RESTORE_CHAR_CLASS = '*_`\\[\\]#\\\\';
156
+ /**
157
+ * Convert any `{type: 'text', data: {_forceEscape: true}}` node (produced by
158
+ * `splitEscapedPunctuation` in `parse.ts` and carried through Lexical via the
159
+ * `--md-force-escape` style hint) into a dedicated `escapedChar` node, so its
160
+ * default `toMarkdown` handler can emit a backslash-free placeholder instead
161
+ * of running through the normal text-escaping logic that the two blind-strip
162
+ * post-processes below (intraword underscore, bracket preservation) operate
163
+ * on (#17).
164
+ */
165
+ function convertForceEscapeTextNodes(node) {
166
+ if (!node || typeof node !== 'object') {
167
+ return node;
168
+ }
169
+ if (node.children && Array.isArray(node.children)) {
170
+ const children = [];
171
+ for (const child of node.children) {
172
+ if (child?.type === 'text' && child.data?._forceEscape === true) {
173
+ // Lexical's own reconciliation merges adjacent TextNodes that share
174
+ // identical format/style (see mdastToLexical.ts's setForceEscape),
175
+ // so two force-escaped characters that were adjacent in the source
176
+ // (e.g. "\*\*") can arrive here fused into one multi-character node.
177
+ // Only single-character force-escaped nodes are ever produced
178
+ // upstream, so every character of a fused node was independently
179
+ // force-escaped — expand back into one escapedChar node per
180
+ // character rather than assuming the node is already length 1.
181
+ //
182
+ // An annotate-serialize sentinel token (see annotate-sentinels.ts)
183
+ // can also be spliced onto this exact node's text by
184
+ // `sentinelAugmentedText` in lexicalToMdast.ts before this data is
185
+ // read, so the value isn't always pure escaped content — split those
186
+ // parts out first and leave them as plain, unescaped text (#970).
187
+ for (const part of splitOnSentinelTokens(child.value)) {
188
+ if (part.isSentinel) {
189
+ children.push({ type: 'text', value: part.text });
190
+ continue;
191
+ }
192
+ for (const ch of part.text) {
193
+ children.push({ type: 'escapedChar', value: ch });
194
+ }
195
+ }
196
+ }
197
+ else {
198
+ children.push(convertForceEscapeTextNodes(child));
199
+ }
200
+ }
201
+ return { ...node, children };
202
+ }
203
+ return node;
204
+ }
205
+ /**
206
+ * Normalize wiki-link nodes for stringification
207
+ *
208
+ * mdast-util-wiki-link expects `data` to exist and reads `data.alias` directly.
209
+ * We keep `data` as an object to avoid runtime errors.
210
+ *
211
+ * We keep data as an object if present, but do not force alias state here.
212
+ */
213
+ function normalizeWikiLinkNodes(node) {
214
+ if (!node || typeof node !== 'object') {
215
+ return node;
216
+ }
217
+ if (node.type === 'wikiLink') {
218
+ const data = node.data && typeof node.data === 'object' ? { ...node.data } : {};
219
+ return {
220
+ ...node,
221
+ data,
222
+ };
223
+ }
224
+ // Recursively normalize children
225
+ if (node.children && Array.isArray(node.children)) {
226
+ return {
227
+ ...node,
228
+ children: node.children.map(normalizeWikiLinkNodes),
229
+ };
230
+ }
231
+ return node;
232
+ }
233
+ export function stringifyMarkdown(root, options = {}) {
234
+ // Pre-process: add checkbox text to ordered list items (GFM only outputs for unordered)
235
+ let processedRoot = addCheckboxTextToOrderedLists(root);
236
+ // Normalize wiki-link nodes before stringifying
237
+ processedRoot = normalizeWikiLinkNodes(processedRoot);
238
+ // Pre-process: isolate force-escaped characters into their own node type
239
+ // so the existing blind-strip post-processes below never see them (#17)
240
+ processedRoot = convertForceEscapeTextNodes(processedRoot);
241
+ // Compute real spread values for lists/list items (loose vs. tight)
242
+ processedRoot = computeListSpread(processedRoot);
243
+ let result = toMarkdown(processedRoot, {
244
+ extensions: [
245
+ gfmToMarkdown({
246
+ // Keep a single space padding around cell content, but don't align columns.
247
+ // This preserves the common "| a | b |" style without spacing churn.
248
+ tableCellPadding: true,
249
+ tablePipeAlign: false,
250
+ }),
251
+ mathToMarkdown(),
252
+ defListToMarkdown,
253
+ gfmFootnoteToMarkdown(),
254
+ frontmatterToMarkdown(['yaml']),
255
+ {
256
+ // Custom handlers for emphasis markers and wiki links
257
+ // Cast to allow custom wikiLink handler (not in base Handlers type)
258
+ handlers: {
259
+ emphasis: (node, _parent, state) => {
260
+ const marker = node?.data?._emphasisMarker === '_' ? '_' : '*';
261
+ const content = state.containerPhrasing(node, { before: marker, after: marker });
262
+ return marker + content + marker;
263
+ },
264
+ strong: (node, _parent, state) => {
265
+ const markerChar = node?.data?._strongMarker === '_' ? '_' : '*';
266
+ const marker = markerChar + markerChar;
267
+ const content = state.containerPhrasing(node, { before: marker, after: marker });
268
+ return marker + content + marker;
269
+ },
270
+ escapedChar: (node) => `${FORCE_ESCAPE_PLACEHOLDER}${node.value}${FORCE_ESCAPE_PLACEHOLDER}`,
271
+ wikiLink: (node) => {
272
+ const value = node.value ?? '';
273
+ const data = node.data && typeof node.data === 'object' ? node.data : {};
274
+ const alias = typeof data.alias === 'string' ? data.alias : '';
275
+ const hasAlias = alias.length > 0 && alias !== value;
276
+ const emptyAlias = data._emptyAlias === true;
277
+ const aliasPart = hasAlias ? `${wikiLinkOptions.aliasDivider}${alias}` : emptyAlias ? `${wikiLinkOptions.aliasDivider}` : '';
278
+ return `[[${value}${aliasPart}]]`;
279
+ },
280
+ // Override mdast-util-definition-list's default (`:` + 3 spaces, i.e. a
281
+ // 4-char marker matching a 4-space continuation indent) with the
282
+ // single-space `: ` marker convention used throughout PHP Markdown
283
+ // Extra source and this codebase's own fixtures/spec examples, so a
284
+ // `: Definition` input round-trips byte-identical instead of being
285
+ // reformatted to `: Definition`.
286
+ defListDescription: (node, _parent, state, info) => {
287
+ const exit = state.enter('defListDescription');
288
+ const value = state.indentLines(state.containerFlow(node, info), (line, index, blank) => {
289
+ if (index) {
290
+ return blank ? '' : ' ' + line;
291
+ }
292
+ return blank ? ':' : ': ' + line;
293
+ });
294
+ exit();
295
+ return value;
296
+ },
297
+ },
298
+ },
299
+ ],
300
+ bullet: options.bulletStyle || '-',
301
+ fence: options.fenceStyle || '`',
302
+ listItemIndent: 'one',
303
+ rule: '-',
304
+ // Custom join function to prevent extra blank lines
305
+ join: [
306
+ // Don't add blank line between paragraph and block when paragraph ends with ':'
307
+ // This preserves the "label + block" pattern (e.g., "Fenced code:", "Block math:").
308
+ // Excludes 'list': unlike table/code/math, a list following a colon-ending
309
+ // paragraph must still get the standard one-blank-line separation every other
310
+ // preceding-block type already gets before a list (see #943) — the zero-blank
311
+ // convention here is specific to the "label:" + verbatim/tabular block pattern.
312
+ // Doesn't apply inside a list item: a table/code fence there needs blank-line
313
+ // separation from the preceding paragraph to parse back correctly, regardless
314
+ // of how that paragraph ends.
315
+ // The colon test reads the sentinel-*free* text: in annotated-serialize
316
+ // mode (see `annotate-sentinels.ts`) the paragraph's last value can carry
317
+ // a trailing close token, so matching the raw value made the paragraph
318
+ // stop "ending with a colon" and re-inserted the blank line before the
319
+ // fence — annotate mode changing output outside its own tokens, which is
320
+ // exactly what `locateLiveMarkdownRange`'s offset math forbids (#970).
321
+ (left, right, parent) => {
322
+ if (left.type === 'paragraph' && parent?.type !== 'listItem') {
323
+ const lastChild = left.children?.[left.children.length - 1];
324
+ const endsWithColon = lastChild?.type === 'text' && stripAnnotateSentinels(lastChild.value ?? '').trimEnd().endsWith(':');
325
+ if (endsWithColon && ['table', 'code', 'math'].includes(right.type)) {
326
+ return 0; // No blank line
327
+ }
328
+ }
329
+ return undefined; // Use default
330
+ },
331
+ ],
332
+ });
333
+ // Post-process: convert escaped spaces back to regular spaces
334
+ // mdast-util-to-markdown escapes leading/trailing spaces at paragraph
335
+ // boundaries as   which looks ugly in raw markdown
336
+ result = result.replace(/ /g, ' ');
337
+ // Post-process: prefer two-space hard breaks over backslash hard breaks
338
+ // Convert a trailing backslash at end of line to two spaces.
339
+ result = result.replace(/\\\n/g, ' \n');
340
+ // Post-process: unescape callout markers in blockquotes
341
+ // Convert "> \[!NOTE]" -> "> [!NOTE]"
342
+ result = result.replace(/^> \\\[!([A-Z]+)\]/gm, '> [!$1]');
343
+ // Post-process: unescape ordered task list markers
344
+ // Convert "1. \[ ]" -> "1. [ ]"
345
+ result = result.replace(/^(\s*\d+\.\s*)\\\[( |x|X)\]/gm, '$1[$2]');
346
+ // Post-process: unescape wiki-link brackets that were escaped
347
+ // mdast-util-to-markdown escapes [ to \[ to prevent link interpretation
348
+ // Pattern: \[\[content]] or \[\[content\]\] → [[content]]
349
+ // NOTE: must run before the bracket preservation step below
350
+ result = result.replace(/\\\[\\\[([^\]\n]+?)\\\]\\\]/g, '[[$1]]');
351
+ result = result.replace(/\\\[\\\[([^\]\n]+?)\]\]/g, '[[$1]]');
352
+ // Post-process: unescape balanced bracket pairs inside image alt text
353
+ // mdast-util-to-markdown always escapes both '[' and ']' within an image's
354
+ // alt-text label (unlike plain phrasing, where only '[' is unsafe), so a
355
+ // literal bracket pair like "see [note] here" serializes as
356
+ // "see \[note\] here" regardless of whether the source was already escaped
357
+ // (the AST only stores the plain alt string; escaped and unescaped sources
358
+ // are indistinguishable once parsed). The bracket-preservation regex below
359
+ // was written for the plain-phrasing case (escaped opener, bare closer) and
360
+ // mis-fires on this already-fully-escaped span, corrupting it (see #903).
361
+ // Strip the escapes back to literal brackets whenever an image's alt-text
362
+ // span contains a properly nested run of escaped brackets — every ']'
363
+ // closes a preceding '[' and depth returns to 0 by the end of the span. An
364
+ // equal open/close *count* alone isn't sufficient (e.g. "a\]b\[c" has one of
365
+ // each but the ']' closes nothing): unescaping that would leave a bare ']'
366
+ // in the middle of the image label and truncate it early, so any span with
367
+ // an out-of-order or otherwise unbalanced bracket is left untouched and
368
+ // deferred to the existing behavior below. `(?:\\.|[^\]\n])*` consumes
369
+ // escaped characters as a unit so an escaped closing bracket in the alt text
370
+ // isn't confused with the image label's own (unescaped) terminating ']'.
371
+ result = result.replace(/!\[((?:\\.|[^\]\n])*)\]\(/g, (match, alt) => {
372
+ if (!/\\[[\]]/.test(alt)) {
373
+ return match;
374
+ }
375
+ const unescaped = alt.replace(/\\([[\]])/g, '$1');
376
+ let depth = 0;
377
+ for (const ch of unescaped) {
378
+ if (ch === '[') {
379
+ depth++;
380
+ }
381
+ else if (ch === ']') {
382
+ depth--;
383
+ if (depth < 0) {
384
+ return match;
385
+ }
386
+ }
387
+ }
388
+ if (depth !== 0) {
389
+ return match;
390
+ }
391
+ return `![${unescaped}](`;
392
+ });
393
+ // Post-process: strip mdast-util-to-markdown's conservative '[' escape in
394
+ // plain phrasing. mdast-util-to-markdown always escapes a literal '[' in
395
+ // phrasing content (in case a same-named link reference definition exists
396
+ // elsewhere in the document) but leaves a lone ']' bare, since ']' is only
397
+ // ever unsafe inside an actual link/image label — and label content is
398
+ // always escaped in full by the link/image handlers themselves (see #918
399
+ // research: no code path produces a half-escaped label). So an escaped '\['
400
+ // followed eventually by a bare ']' can only originate from genuine
401
+ // top-level phrasing, never from an under-escaped label, and the '[' escape
402
+ // there is unnecessary: strip it rather than "completing" it by escaping
403
+ // the ']' too (which corrupted plain prose brackets, see #918).
404
+ // Image alt-text spans are excluded (tried first, left untouched) so this
405
+ // doesn't undo the deliberate escape-preservation of the block above, which
406
+ // already resolved (or intentionally preserved) unbalanced/pre-escaped
407
+ // bracket content inside `![...](`.
408
+ // Accepted trade-off: if the document also has a reference-link definition
409
+ // matching this bracket text elsewhere (e.g. `[label]: url`), stripping the
410
+ // escape means the round-tripped prose will now parse as a real reference
411
+ // link instead of staying inert — a semantic change. This mirrors
412
+ // mdast-util-to-markdown's own conservative-by-default behavior and is out
413
+ // of scope for this fix (see #918 "Out of Scope").
414
+ // Known limitation: unlike the underscore step below, this (and the #903
415
+ // block above it) operate on the fully serialized string without excluding
416
+ // fenced/inline code or math spans, so literal `\[...]` text inside one of
417
+ // those verbatim regions would also be stripped. Pre-existing gap (not
418
+ // introduced by this fix); out of scope per #918 "Out of Scope" (brackets
419
+ // inside code spans).
420
+ result = result.replace(/!\[(?:\\.|[^\\\]\n])*\]\(|\\\[([^\]\n]+)\](?!\])/g, (match, plainPhrasingContent) => (plainPhrasingContent === undefined ? match : `[${plainPhrasingContent}]`));
421
+ // Post-process: unescape intraword underscores, outside of code and math.
422
+ // mdast-util-to-markdown escapes every `_` in text conservatively, but CommonMark
423
+ // never lets an intraword underscore (flanked by alphanumerics on both sides) open
424
+ // or close emphasis — so escaping it is unnecessary. An underscore adjacent to
425
+ // whitespace/punctuation (e.g. `_word_`) can still form emphasis and must stay escaped.
426
+ // Code (fenced blocks and inline spans) and math ($...$ / $$...$$) are skipped: their
427
+ // content is emitted verbatim, so a literal `\_` there (e.g. a regex/shell escape, or
428
+ // a LaTeX subscript marker) is the user's own text and must not be rewritten. The fenced
429
+ // code alternative backreferences the opening fence run so a closing line using a
430
+ // different fence character/length (or a same-character run that's just code content)
431
+ // doesn't end the protected region early.
432
+ result = result.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1[ \t]*$|(`+)[^\n]*?\2|\$\$[\s\S]*?\$\$|\$[^\n$]*?\$|((?<=[\p{L}\p{N}])\\_(?=[\p{L}\p{N}]))/gmu, (match, _fenceRun, _inlineOpen, underscore) => (underscore === undefined ? match : '_'));
433
+ // Post-process: restore the real backslash for every force-escaped
434
+ // character (#17), now that every other post-process above — which only
435
+ // ever strips backslashes, never adds them — has already run and had no
436
+ // chance to see (or strip) a backslash that wasn't there yet.
437
+ result = result.replace(new RegExp(`${FORCE_ESCAPE_PLACEHOLDER}([${FORCE_ESCAPE_RESTORE_CHAR_CLASS}])${FORCE_ESCAPE_PLACEHOLDER}`, 'gu'), '\\$1');
438
+ return result;
439
+ }
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Mark Hudnall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ # Vendored `mdast-util-wiki-link`
2
+
3
+ Source: [`landakram/mdast-util-wiki-link`](https://github.com/landakram/mdast-util-wiki-link) v0.1.2,
4
+ MIT-licensed (see `LICENSE`). Copyright (c) 2020 Mark Hudnall.
5
+
6
+ ## Why this is vendored
7
+
8
+ The behaviour this package needs was previously obtained by patching the npm
9
+ dependency through pnpm's `patchedDependencies`, in
10
+ `liminis-app/patches/mdast-util-wiki-link@0.1.2.patch`. A pnpm patch is a
11
+ property of *this workspace*, not of the published package: an external consumer
12
+ of `@liminis/editor` installing from a registry would get the unpatched
13
+ upstream, and would silently get different — and wrong — round-trip behaviour for
14
+ wiki-links inside markdown tables.
15
+
16
+ Vendoring makes `@liminis/editor` self-contained (#940 / FR-004), and removes
17
+ the `mdast-util-to-markdown@0.6.5` + `@babel/runtime` duplicate dependency graph
18
+ upstream pulled in.
19
+
20
+ ## Divergences from upstream
21
+
22
+ 1. **Trailing-backslash strip in `from-markdown.ts`.** When a wiki-link has an
23
+ alias and its target ends with a backslash, the backslash is dropped before
24
+ the target is resolved. This is the fix for
25
+ [#347](https://github.com/verveguy/liminis/issues/347) — `parseMarkdown`
26
+ escapes the alias divider (`\|`) so that GFM table parsing does not split a
27
+ wiki-link at its pipe, and the escape must not survive into the target.
28
+ Removing this behaviour re-breaks whole markdown tables that contain an
29
+ aliased wiki-link, not just the link. Do not re-remove it.
30
+
31
+ Because the strip happens *before* `pageResolver` runs, `data.permalink` and
32
+ `data.exists` are derived from the stripped value, exactly as under the patch.
33
+
34
+ 2. **`to-markdown.ts` uses `mdast-util-to-markdown` v2's `state.safe()`** rather
35
+ than upstream's deep import of `mdast-util-to-markdown/lib/util/safe` from
36
+ v0.6.5. Same escaping, no duplicate dependency.
37
+
38
+ 3. **Types.** The `any`-typed option and node shapes are given real types.
39
+
40
+ Parse and serialize behaviour is otherwise unchanged. Parity is covered by
41
+ `src/markdown/__tests__/vendor-wiki-link.test.ts`.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Vendored from `mdast-util-wiki-link@0.1.2` (MIT, Mark Hudnall — see LICENSE
3
+ * in this directory), with the Liminis trailing-backslash fix applied inline.
4
+ *
5
+ * Why vendored rather than depended-upon-and-patched: the upstream behaviour we
6
+ * need was only reachable through a `pnpm` `patchedDependencies` entry, which
7
+ * does not travel outside the workspace that declares it. An external consumer of the published
8
+ * package would silently get different — and wrong — wiki-link behaviour inside
9
+ * markdown tables (#347). Vendoring makes the package self-contained.
10
+ *
11
+ * Four deliberate divergences from upstream:
12
+ * 1. The trailing-backslash strip (see `exitWikiLink`), previously carried as
13
+ * `liminis-app/patches/mdast-util-wiki-link@0.1.2.patch` in `verveguy/liminis`.
14
+ * 2. Real types instead of `any` on the public option and node shapes.
15
+ * 3. `exitWikiLink` reads the node off `this.stack` rather than off a
16
+ * closure variable shared by every handler in one `fromMarkdown()` call.
17
+ * 4. `top()` asserts the stack frame is actually a wiki-link, so the
18
+ * cannot-nest assumption fails loudly rather than corrupting a sibling node.
19
+ *
20
+ * Nothing else about the parse behaviour changes: `value`, `data.alias`,
21
+ * `data.permalink`, `data.exists`, `data.hName`, `data.hProperties` and
22
+ * `data.hChildren` are computed exactly as upstream computes them.
23
+ */
24
+ export interface WikiLinkFromMarkdownOptions {
25
+ permalinks?: string[];
26
+ pageResolver?: (name: string) => string[];
27
+ newClassName?: string;
28
+ wikiLinkClassName?: string;
29
+ hrefTemplate?: (permalink: string) => string;
30
+ /** Accepted and ignored here; consumed by `toMarkdown` and the micromark syntax extension. */
31
+ aliasDivider?: string;
32
+ }
33
+ interface WikiLinkNode {
34
+ type: 'wikiLink';
35
+ value: string;
36
+ data: {
37
+ alias: string | null;
38
+ permalink: string | null;
39
+ exists: boolean | null;
40
+ hName?: string;
41
+ hProperties?: {
42
+ className: string;
43
+ href: string;
44
+ };
45
+ hChildren?: {
46
+ type: 'text';
47
+ value: string;
48
+ }[];
49
+ };
50
+ }
51
+ /**
52
+ * The subset of `mdast-util-from-markdown`'s `CompileContext` this handler uses.
53
+ * Typed structurally so the vendored module does not depend on that package's
54
+ * (unexported, version-specific) context type.
55
+ */
56
+ interface WikiLinkCompileContext {
57
+ stack: unknown[];
58
+ enter: (node: WikiLinkNode, token: unknown) => void;
59
+ exit: (token: unknown) => void;
60
+ sliceSerialize: (token: unknown) => string;
61
+ }
62
+ export declare function fromMarkdown(opts?: WikiLinkFromMarkdownOptions): {
63
+ enter: {
64
+ wikiLink: (this: WikiLinkCompileContext, token: unknown) => void;
65
+ };
66
+ exit: {
67
+ wikiLinkTarget: (this: WikiLinkCompileContext, token: unknown) => void;
68
+ wikiLinkAlias: (this: WikiLinkCompileContext, token: unknown) => void;
69
+ wikiLink: (this: WikiLinkCompileContext, token: unknown) => void;
70
+ };
71
+ };
72
+ export {};