@react-x11/components 0.7.1 → 0.9.0

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 (354) hide show
  1. package/README.md +295 -51
  2. package/dist/charts/node.d.ts +1 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +6 -2
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/color-picker/ColorPicker.d.ts.map +1 -1
  7. package/dist/color-picker/ColorPicker.js +43 -33
  8. package/dist/color-picker/ColorPicker.js.map +1 -1
  9. package/dist/embed/client.d.ts +48 -3
  10. package/dist/embed/client.d.ts.map +1 -1
  11. package/dist/embed/client.js +79 -5
  12. package/dist/embed/client.js.map +1 -1
  13. package/dist/embed/index.d.ts +1 -1
  14. package/dist/embed/index.d.ts.map +1 -1
  15. package/dist/embed/index.js +1 -1
  16. package/dist/embed/index.js.map +1 -1
  17. package/dist/html/node.d.ts +3 -1
  18. package/dist/html/node.d.ts.map +1 -1
  19. package/dist/html/node.js +7 -5
  20. package/dist/html/node.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
  26. package/dist/internal/markdown/ast.d.ts.map +1 -0
  27. package/dist/internal/markdown/ast.js +22 -0
  28. package/dist/internal/markdown/ast.js.map +1 -0
  29. package/dist/internal/markdown/parse.d.ts.map +1 -0
  30. package/dist/{markdown → internal/markdown}/parse.js +112 -22
  31. package/dist/internal/markdown/parse.js.map +1 -0
  32. package/dist/internal/markdown/stringify.d.ts +4 -0
  33. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  34. package/dist/internal/markdown/stringify.js +684 -0
  35. package/dist/internal/markdown/stringify.js.map +1 -0
  36. package/dist/internal/markdown/tags.d.ts.map +1 -0
  37. package/dist/internal/markdown/tags.js.map +1 -0
  38. package/dist/internal/scroll.d.ts +3 -2
  39. package/dist/internal/scroll.d.ts.map +1 -1
  40. package/dist/internal/scroll.js +33 -15
  41. package/dist/internal/scroll.js.map +1 -1
  42. package/dist/internal/units.d.ts +2 -0
  43. package/dist/internal/units.d.ts.map +1 -0
  44. package/dist/internal/units.js +35 -0
  45. package/dist/internal/units.js.map +1 -0
  46. package/dist/internal/window.d.ts.map +1 -1
  47. package/dist/internal/window.js +4 -2
  48. package/dist/internal/window.js.map +1 -1
  49. package/dist/maps/anchors.d.ts +98 -0
  50. package/dist/maps/anchors.d.ts.map +1 -0
  51. package/dist/maps/anchors.js +531 -0
  52. package/dist/maps/anchors.js.map +1 -0
  53. package/dist/maps/controller.d.ts +283 -0
  54. package/dist/maps/controller.d.ts.map +1 -0
  55. package/dist/maps/controller.js +743 -0
  56. package/dist/maps/controller.js.map +1 -0
  57. package/dist/maps/gl/buckets.d.ts +95 -0
  58. package/dist/maps/gl/buckets.d.ts.map +1 -0
  59. package/dist/maps/gl/buckets.js +362 -0
  60. package/dist/maps/gl/buckets.js.map +1 -0
  61. package/dist/maps/gl/build-worker.d.ts +13 -0
  62. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  63. package/dist/maps/gl/build-worker.js +44 -0
  64. package/dist/maps/gl/build-worker.js.map +1 -0
  65. package/dist/maps/gl/color.d.ts +8 -0
  66. package/dist/maps/gl/color.d.ts.map +1 -0
  67. package/dist/maps/gl/color.js +57 -0
  68. package/dist/maps/gl/color.js.map +1 -0
  69. package/dist/maps/gl/cover.d.ts +51 -0
  70. package/dist/maps/gl/cover.d.ts.map +1 -0
  71. package/dist/maps/gl/cover.js +124 -0
  72. package/dist/maps/gl/cover.js.map +1 -0
  73. package/dist/maps/gl/index.d.ts +3 -0
  74. package/dist/maps/gl/index.d.ts.map +1 -0
  75. package/dist/maps/gl/index.js +20 -0
  76. package/dist/maps/gl/index.js.map +1 -0
  77. package/dist/maps/gl/markers.d.ts +28 -0
  78. package/dist/maps/gl/markers.d.ts.map +1 -0
  79. package/dist/maps/gl/markers.js +60 -0
  80. package/dist/maps/gl/markers.js.map +1 -0
  81. package/dist/maps/gl/overlays.d.ts +68 -0
  82. package/dist/maps/gl/overlays.d.ts.map +1 -0
  83. package/dist/maps/gl/overlays.js +300 -0
  84. package/dist/maps/gl/overlays.js.map +1 -0
  85. package/dist/maps/gl/pane.d.ts +12 -0
  86. package/dist/maps/gl/pane.d.ts.map +1 -0
  87. package/dist/maps/gl/pane.js +37 -0
  88. package/dist/maps/gl/pane.js.map +1 -0
  89. package/dist/maps/gl/placement.d.ts +111 -0
  90. package/dist/maps/gl/placement.d.ts.map +1 -0
  91. package/dist/maps/gl/placement.js +567 -0
  92. package/dist/maps/gl/placement.js.map +1 -0
  93. package/dist/maps/gl/renderer.d.ts +352 -0
  94. package/dist/maps/gl/renderer.d.ts.map +1 -0
  95. package/dist/maps/gl/renderer.js +1127 -0
  96. package/dist/maps/gl/renderer.js.map +1 -0
  97. package/dist/maps/gl/shaders.d.ts +120 -0
  98. package/dist/maps/gl/shaders.d.ts.map +1 -0
  99. package/dist/maps/gl/shaders.js +451 -0
  100. package/dist/maps/gl/shaders.js.map +1 -0
  101. package/dist/maps/gl/store.d.ts +115 -0
  102. package/dist/maps/gl/store.d.ts.map +1 -0
  103. package/dist/maps/gl/store.js +469 -0
  104. package/dist/maps/gl/store.js.map +1 -0
  105. package/dist/maps/gl/text.d.ts +154 -0
  106. package/dist/maps/gl/text.d.ts.map +1 -0
  107. package/dist/maps/gl/text.js +382 -0
  108. package/dist/maps/gl/text.js.map +1 -0
  109. package/dist/maps/gl/view.d.ts +54 -0
  110. package/dist/maps/gl/view.d.ts.map +1 -0
  111. package/dist/maps/gl/view.js +974 -0
  112. package/dist/maps/gl/view.js.map +1 -0
  113. package/dist/maps/index.d.ts +1 -1
  114. package/dist/maps/index.d.ts.map +1 -1
  115. package/dist/maps/index.js +216 -52
  116. package/dist/maps/index.js.map +1 -1
  117. package/dist/maps/labels.d.ts +37 -16
  118. package/dist/maps/labels.d.ts.map +1 -1
  119. package/dist/maps/labels.js +188 -119
  120. package/dist/maps/labels.js.map +1 -1
  121. package/dist/maps/node.d.ts +162 -56
  122. package/dist/maps/node.d.ts.map +1 -1
  123. package/dist/maps/node.js +607 -515
  124. package/dist/maps/node.js.map +1 -1
  125. package/dist/maps/overlay.d.ts +72 -9
  126. package/dist/maps/overlay.d.ts.map +1 -1
  127. package/dist/maps/overlay.js +108 -28
  128. package/dist/maps/overlay.js.map +1 -1
  129. package/dist/maps/paint.d.ts +3 -0
  130. package/dist/maps/paint.d.ts.map +1 -1
  131. package/dist/maps/paint.js.map +1 -1
  132. package/dist/maps/proj.d.ts +14 -0
  133. package/dist/maps/proj.d.ts.map +1 -1
  134. package/dist/maps/proj.js +28 -0
  135. package/dist/maps/proj.js.map +1 -1
  136. package/dist/maps/renderer.d.ts +69 -0
  137. package/dist/maps/renderer.d.ts.map +1 -0
  138. package/dist/maps/renderer.js +66 -0
  139. package/dist/maps/renderer.js.map +1 -0
  140. package/dist/maps/sources.d.ts +25 -3
  141. package/dist/maps/sources.d.ts.map +1 -1
  142. package/dist/maps/sources.js +17 -0
  143. package/dist/maps/sources.js.map +1 -1
  144. package/dist/maps/theme.d.ts +19 -0
  145. package/dist/maps/theme.d.ts.map +1 -0
  146. package/dist/maps/theme.js +44 -0
  147. package/dist/maps/theme.js.map +1 -0
  148. package/dist/maps/tiles.d.ts +117 -17
  149. package/dist/maps/tiles.d.ts.map +1 -1
  150. package/dist/maps/tiles.js +163 -30
  151. package/dist/maps/tiles.js.map +1 -1
  152. package/dist/maps/types.d.ts +188 -20
  153. package/dist/maps/types.d.ts.map +1 -1
  154. package/dist/markdown/expressions.d.ts +1 -1
  155. package/dist/markdown/expressions.d.ts.map +1 -1
  156. package/dist/markdown/expressions.js.map +1 -1
  157. package/dist/markdown/index.d.ts +7 -6
  158. package/dist/markdown/index.d.ts.map +1 -1
  159. package/dist/markdown/index.js +9 -7
  160. package/dist/markdown/index.js.map +1 -1
  161. package/dist/markdown/spans.d.ts +1 -1
  162. package/dist/markdown/spans.d.ts.map +1 -1
  163. package/dist/media-player/index.d.ts +11 -3
  164. package/dist/media-player/index.d.ts.map +1 -1
  165. package/dist/media-player/index.js +13 -2
  166. package/dist/media-player/index.js.map +1 -1
  167. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  168. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  169. package/dist/rich-text-editor/clipboard.js +307 -0
  170. package/dist/rich-text-editor/clipboard.js.map +1 -0
  171. package/dist/rich-text-editor/collab.d.ts +32 -0
  172. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  173. package/dist/rich-text-editor/collab.js +55 -0
  174. package/dist/rich-text-editor/collab.js.map +1 -0
  175. package/dist/rich-text-editor/commands.d.ts +50 -0
  176. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  177. package/dist/rich-text-editor/commands.js +376 -0
  178. package/dist/rich-text-editor/commands.js.map +1 -0
  179. package/dist/rich-text-editor/drag.d.ts +68 -0
  180. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  181. package/dist/rich-text-editor/drag.js +148 -0
  182. package/dist/rich-text-editor/drag.js.map +1 -0
  183. package/dist/rich-text-editor/html.d.ts +145 -0
  184. package/dist/rich-text-editor/html.d.ts.map +1 -0
  185. package/dist/rich-text-editor/html.js +516 -0
  186. package/dist/rich-text-editor/html.js.map +1 -0
  187. package/dist/rich-text-editor/index.d.ts +263 -0
  188. package/dist/rich-text-editor/index.d.ts.map +1 -0
  189. package/dist/rich-text-editor/index.js +822 -0
  190. package/dist/rich-text-editor/index.js.map +1 -0
  191. package/dist/rich-text-editor/inline.d.ts +94 -0
  192. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  193. package/dist/rich-text-editor/inline.js +252 -0
  194. package/dist/rich-text-editor/inline.js.map +1 -0
  195. package/dist/rich-text-editor/keymap.d.ts +71 -0
  196. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  197. package/dist/rich-text-editor/keymap.js +393 -0
  198. package/dist/rich-text-editor/keymap.js.map +1 -0
  199. package/dist/rich-text-editor/keys.d.ts +29 -0
  200. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  201. package/dist/rich-text-editor/keys.js +93 -0
  202. package/dist/rich-text-editor/keys.js.map +1 -0
  203. package/dist/rich-text-editor/look.d.ts +71 -0
  204. package/dist/rich-text-editor/look.d.ts.map +1 -0
  205. package/dist/rich-text-editor/look.js +342 -0
  206. package/dist/rich-text-editor/look.js.map +1 -0
  207. package/dist/rich-text-editor/markdown.d.ts +18 -0
  208. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  209. package/dist/rich-text-editor/markdown.js +496 -0
  210. package/dist/rich-text-editor/markdown.js.map +1 -0
  211. package/dist/rich-text-editor/nodes.d.ts +135 -0
  212. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  213. package/dist/rich-text-editor/nodes.js +397 -0
  214. package/dist/rich-text-editor/nodes.js.map +1 -0
  215. package/dist/rich-text-editor/render.d.ts +68 -0
  216. package/dist/rich-text-editor/render.d.ts.map +1 -0
  217. package/dist/rich-text-editor/render.js +500 -0
  218. package/dist/rich-text-editor/render.js.map +1 -0
  219. package/dist/rich-text-editor/schema.d.ts +16 -0
  220. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  221. package/dist/rich-text-editor/schema.js +386 -0
  222. package/dist/rich-text-editor/schema.js.map +1 -0
  223. package/dist/rich-text-editor/store.d.ts +11 -0
  224. package/dist/rich-text-editor/store.d.ts.map +1 -0
  225. package/dist/rich-text-editor/store.js +43 -0
  226. package/dist/rich-text-editor/store.js.map +1 -0
  227. package/dist/rich-text-editor/suggest.d.ts +127 -0
  228. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  229. package/dist/rich-text-editor/suggest.js +0 -0
  230. package/dist/rich-text-editor/suggest.js.map +1 -0
  231. package/dist/rich-text-editor/tables.d.ts +41 -0
  232. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  233. package/dist/rich-text-editor/tables.js +204 -0
  234. package/dist/rich-text-editor/tables.js.map +1 -0
  235. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  236. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  237. package/dist/rich-text-editor/toolbar.js +548 -0
  238. package/dist/rich-text-editor/toolbar.js.map +1 -0
  239. package/dist/rich-text-editor/view.d.ts +368 -0
  240. package/dist/rich-text-editor/view.d.ts.map +1 -0
  241. package/dist/rich-text-editor/view.js +1968 -0
  242. package/dist/rich-text-editor/view.js.map +1 -0
  243. package/dist/rich-text-editor/virtual.d.ts +54 -0
  244. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  245. package/dist/rich-text-editor/virtual.js +262 -0
  246. package/dist/rich-text-editor/virtual.js.map +1 -0
  247. package/dist/richtext/node.d.ts +25 -2
  248. package/dist/richtext/node.d.ts.map +1 -1
  249. package/dist/richtext/node.js +46 -24
  250. package/dist/richtext/node.js.map +1 -1
  251. package/dist/table/index.d.ts.map +1 -1
  252. package/dist/table/index.js +14 -5
  253. package/dist/table/index.js.map +1 -1
  254. package/dist/terminal/backends.d.ts +4 -1
  255. package/dist/terminal/backends.d.ts.map +1 -1
  256. package/dist/terminal/backends.js.map +1 -1
  257. package/dist/terminal/index.d.ts +18 -7
  258. package/dist/terminal/index.d.ts.map +1 -1
  259. package/dist/terminal/index.js +28 -11
  260. package/dist/terminal/index.js.map +1 -1
  261. package/dist/terminal/vt/node.d.ts +56 -3
  262. package/dist/terminal/vt/node.d.ts.map +1 -1
  263. package/dist/terminal/vt/node.js +106 -26
  264. package/dist/terminal/vt/node.js.map +1 -1
  265. package/dist/terminal/vt/renderer.d.ts +4 -0
  266. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  267. package/dist/terminal/vt/renderer.js +14 -5
  268. package/dist/terminal/vt/renderer.js.map +1 -1
  269. package/dist/tree/index.d.ts.map +1 -1
  270. package/dist/tree/index.js +6 -4
  271. package/dist/tree/index.js.map +1 -1
  272. package/package.json +23 -4
  273. package/src/charts/node.ts +6 -2
  274. package/src/color-picker/ColorPicker.ts +107 -81
  275. package/src/embed/client.ts +102 -8
  276. package/src/embed/index.ts +2 -0
  277. package/src/html/node.ts +7 -5
  278. package/src/index.ts +2 -0
  279. package/src/{markdown → internal/markdown}/ast.ts +24 -7
  280. package/src/{markdown → internal/markdown}/parse.ts +134 -24
  281. package/src/internal/markdown/stringify.ts +772 -0
  282. package/src/internal/scroll.ts +39 -18
  283. package/src/internal/units.ts +34 -0
  284. package/src/internal/window.ts +4 -2
  285. package/src/maps/anchors.ts +607 -0
  286. package/src/maps/controller.ts +999 -0
  287. package/src/maps/gl/buckets.ts +457 -0
  288. package/src/maps/gl/build-worker.ts +55 -0
  289. package/src/maps/gl/color.ts +62 -0
  290. package/src/maps/gl/cover.ts +170 -0
  291. package/src/maps/gl/index.ts +23 -0
  292. package/src/maps/gl/markers.ts +83 -0
  293. package/src/maps/gl/overlays.ts +379 -0
  294. package/src/maps/gl/pane.ts +42 -0
  295. package/src/maps/gl/placement.ts +816 -0
  296. package/src/maps/gl/renderer.ts +1576 -0
  297. package/src/maps/gl/shaders.ts +486 -0
  298. package/src/maps/gl/store.ts +566 -0
  299. package/src/maps/gl/text.ts +495 -0
  300. package/src/maps/gl/view.ts +1224 -0
  301. package/src/maps/index.ts +270 -81
  302. package/src/maps/labels.ts +214 -121
  303. package/src/maps/node.ts +763 -629
  304. package/src/maps/overlay.ts +148 -43
  305. package/src/maps/paint.ts +3 -0
  306. package/src/maps/proj.ts +31 -0
  307. package/src/maps/renderer.ts +155 -0
  308. package/src/maps/sources.ts +38 -3
  309. package/src/maps/theme.ts +51 -0
  310. package/src/maps/tiles.ts +191 -33
  311. package/src/maps/types.ts +188 -20
  312. package/src/markdown/expressions.ts +5 -1
  313. package/src/markdown/index.ts +14 -11
  314. package/src/markdown/spans.ts +1 -1
  315. package/src/media-player/index.ts +18 -4
  316. package/src/rich-text-editor/clipboard.ts +439 -0
  317. package/src/rich-text-editor/collab.ts +91 -0
  318. package/src/rich-text-editor/commands.ts +448 -0
  319. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  320. package/src/rich-text-editor/drag.ts +211 -0
  321. package/src/rich-text-editor/html.ts +619 -0
  322. package/src/rich-text-editor/index.ts +1331 -0
  323. package/src/rich-text-editor/inline.ts +345 -0
  324. package/src/rich-text-editor/keymap.ts +510 -0
  325. package/src/rich-text-editor/keys.ts +119 -0
  326. package/src/rich-text-editor/look.ts +427 -0
  327. package/src/rich-text-editor/markdown.ts +653 -0
  328. package/src/rich-text-editor/nodes.ts +497 -0
  329. package/src/rich-text-editor/render.ts +800 -0
  330. package/src/rich-text-editor/schema.ts +431 -0
  331. package/src/rich-text-editor/store.ts +39 -0
  332. package/src/rich-text-editor/suggest.ts +597 -0
  333. package/src/rich-text-editor/tables.ts +272 -0
  334. package/src/rich-text-editor/toolbar.ts +724 -0
  335. package/src/rich-text-editor/view.ts +2287 -0
  336. package/src/rich-text-editor/virtual.ts +347 -0
  337. package/src/richtext/node.ts +59 -30
  338. package/src/table/index.ts +14 -5
  339. package/src/terminal/backends.ts +4 -1
  340. package/src/terminal/index.ts +43 -15
  341. package/src/terminal/vt/node.ts +128 -29
  342. package/src/terminal/vt/renderer.ts +27 -5
  343. package/src/tree/index.ts +6 -4
  344. package/dist/markdown/ast.d.ts.map +0 -1
  345. package/dist/markdown/ast.js +0 -17
  346. package/dist/markdown/ast.js.map +0 -1
  347. package/dist/markdown/parse.d.ts.map +0 -1
  348. package/dist/markdown/parse.js.map +0 -1
  349. package/dist/markdown/tags.d.ts.map +0 -1
  350. package/dist/markdown/tags.js.map +0 -1
  351. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  352. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  353. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  354. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -0,0 +1,772 @@
1
+ // AST → markdown: the other half of `./parse.ts`.
2
+ //
3
+ // The rich text editor keeps its document as ProseMirror nodes and saves it
4
+ // as markdown through this AST, so this is the serializer the package did not
5
+ // have — and it is written against the parser beside it rather than against
6
+ // CommonMark in the abstract, because that parser is the first reader every
7
+ // document written here meets. What comes out is *normalized* GFM, not the
8
+ // source a document was parsed from: ATX headings, `-` bullets, `1.`
9
+ // numbering, fenced code, `**`/`*`/`~~`, backslash hard breaks. What is
10
+ // promised is the round trip: `parse(stringify(ast), { partial: false })`
11
+ // gives back `ast` for every AST the parser can produce and every one the
12
+ // editor builds, which test/rich-text-editor-markdown.test.ts holds it to
13
+ // over a generated corpus as well as by example.
14
+ //
15
+ // Nearly all of the work is escaping, and nearly all of the escaping is
16
+ // context: `*` is always syntax, `_` only at the edge of a word, `~` only in
17
+ // pairs, and `#`, `>`, `-`, `1.` only at the start of a line — where a line
18
+ // starts after every hard break, not only at the top of a paragraph.
19
+ import type {
20
+ BlockNode,
21
+ CodeBlock,
22
+ Document,
23
+ InlineNode,
24
+ LinkInline,
25
+ ListBlock,
26
+ TableBlock,
27
+ } from './ast.js';
28
+ import { parseInline } from './parse.js';
29
+
30
+ /** Markdown for a document, or for a run of blocks. */
31
+ export function stringify(doc: Document | readonly BlockNode[]): string {
32
+ const blocks = 'blocks' in doc ? doc.blocks : doc;
33
+ return blockSequence(blocks, false).join('\n');
34
+ }
35
+
36
+ // --- blocks ------------------------------------------------------------------
37
+
38
+ interface ListMarker {
39
+ ordered: boolean;
40
+ marker: string;
41
+ }
42
+
43
+ /** A list line with a marker and nothing after it. */
44
+ const EMPTY_ITEM = /^(?:\d{1,9}[.)]|[-+*])(?: \[[ x]\])? *$/;
45
+
46
+ /**
47
+ * The lines for a run of sibling blocks, with a blank line between each two.
48
+ * The exception is a nested list straight after the paragraph of a tight
49
+ * item — "- a\n - b" — which is how a tight list stays tight.
50
+ */
51
+ function blockSequence(
52
+ blocks: readonly BlockNode[],
53
+ tightItem: boolean,
54
+ ): string[] {
55
+ const out: string[] = [];
56
+ let prevList: ListMarker | null = null;
57
+ for (const block of blocks) {
58
+ let lines: string[];
59
+ let list: ListMarker | null = null;
60
+ if (block.type === 'list') {
61
+ // Two lists in a row with the same marker are one list to a reader —
62
+ // a blank line between them only makes it loose — so the second takes
63
+ // its kind's other marker, which is the break CommonMark recognises.
64
+ const base = block.ordered ? '.' : '-';
65
+ const other = block.ordered ? ')' : '*';
66
+ const marker: string =
67
+ prevList &&
68
+ prevList.ordered === block.ordered &&
69
+ prevList.marker === base
70
+ ? other
71
+ : base;
72
+ list = { ordered: block.ordered, marker };
73
+ lines = listLines(block, marker);
74
+ } else {
75
+ lines = blockLines(block);
76
+ }
77
+ // an empty paragraph has no markdown; dropping it is the only honest
78
+ // spelling of "nothing", and it keeps adjacent lists adjacent
79
+ if (lines.length === 0) continue;
80
+ // …and an empty item cannot interrupt a paragraph ("a\n3." is one
81
+ // paragraph), so a list that opens with one keeps its blank line
82
+ const interrupts =
83
+ tightItem && block.type === 'list' && !EMPTY_ITEM.test(lines[0]);
84
+ if (out.length > 0 && !interrupts) out.push('');
85
+ out.push(...lines);
86
+ prevList = list;
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function blockLines(block: BlockNode): string[] {
92
+ switch (block.type) {
93
+ case 'paragraph': {
94
+ const text = inline(block.children, 'block');
95
+ return text === '' ? [] : text.split('\n');
96
+ }
97
+ case 'heading':
98
+ return [headingLine(block.depth, block.children)];
99
+ case 'code':
100
+ return fenceLines(block);
101
+ case 'quote':
102
+ return blockSequence(block.children, false).map((line) =>
103
+ line === '' ? '>' : `> ${line}`,
104
+ );
105
+ case 'list':
106
+ return listLines(block, block.ordered ? '.' : '-');
107
+ case 'table':
108
+ return tableLines(block);
109
+ case 'rule':
110
+ return ['---'];
111
+ case 'component':
112
+ // The editor never builds one; an AST that has one keeps its content.
113
+ return blockSequence(block.children, false);
114
+ }
115
+ }
116
+
117
+ function headingLine(depth: number, children: readonly InlineNode[]): string {
118
+ let text = inline(children, 'heading');
119
+ // `# Title #` loses its closing run to the ATX rule — so a heading that
120
+ // really ends in a `#` escapes the last one
121
+ if (/(^|[ \t])#+$/.test(text)) text = `${text.slice(0, -1)}\\#`;
122
+ const level = Math.max(1, Math.min(6, Math.round(depth)));
123
+ return '#'.repeat(level) + (text ? ` ${text}` : '');
124
+ }
125
+
126
+ function fenceLines(block: CodeBlock): string[] {
127
+ const info = (block.info ?? block.lang).trim();
128
+ // a backtick fence cannot carry a backtick in its info string
129
+ const char = info.includes('`') ? '~' : '`';
130
+ let longest = 0;
131
+ for (const m of block.text.matchAll(char === '`' ? /`+/g : /~+/g)) {
132
+ longest = Math.max(longest, m[0].length);
133
+ }
134
+ const fence = char.repeat(Math.max(3, longest + 1));
135
+ const body = block.text === '' ? [] : block.text.split('\n');
136
+ return [fence + info, ...body, fence];
137
+ }
138
+
139
+ function listLines(list: ListBlock, marker: string): string[] {
140
+ const out: string[] = [];
141
+ list.items.forEach((item, index) => {
142
+ const bullet = list.ordered ? `${list.start + index}${marker}` : marker;
143
+ const pad = ' '.repeat(bullet.length + 1);
144
+ const task = item.checked === null ? '' : item.checked ? '[x] ' : '[ ] ';
145
+ const body = blockSequence(item.children, list.tight);
146
+ if (index > 0 && !list.tight) out.push('');
147
+ if (body.length === 0) {
148
+ // "- [ ] " keeps the space: a task marker needs one after it
149
+ out.push(task ? `${bullet} ${task}` : bullet);
150
+ return;
151
+ }
152
+ out.push(`${bullet} ${task}${body[0]}`);
153
+ for (let i = 1; i < body.length; i++) {
154
+ out.push(body[i] === '' ? '' : pad + body[i]);
155
+ }
156
+ });
157
+ return out;
158
+ }
159
+
160
+ function tableLines(table: TableBlock): string[] {
161
+ const row = (cells: string[]): string =>
162
+ `| ${cells.map((c) => c || ' ').join(' | ')} |`;
163
+ const cell = (nodes: readonly InlineNode[]): string => inline(nodes, 'cell');
164
+ const delimiter = table.align.map((a) =>
165
+ a === 'left'
166
+ ? ':---'
167
+ : a === 'center'
168
+ ? ':---:'
169
+ : a === 'right'
170
+ ? '---:'
171
+ : '---',
172
+ );
173
+ return [
174
+ row(table.header.map(cell)),
175
+ row(delimiter),
176
+ ...table.rows.map((r) => row(r.map(cell))),
177
+ ];
178
+ }
179
+
180
+ // --- inline ------------------------------------------------------------------
181
+
182
+ /**
183
+ * Where inline text is going. A paragraph's lines are `'block'`: each starts
184
+ * a line, so the characters that open a block there are escaped. A heading
185
+ * and a table cell sit on a line that has already started, so they are not —
186
+ * but neither can hold a line break, and a cell cannot hold a bare `|`.
187
+ */
188
+ type InlineMode = 'block' | 'heading' | 'cell';
189
+
190
+ /**
191
+ * Inline content as markdown — checked. The rules above cover what prose
192
+ * does, but the flanking rules have corners no delimiter placement escapes
193
+ * (bold that ends in punctuation, glued to the word after it, next to
194
+ * italic that does the same), and a corner is where a document loses text
195
+ * to literal asterisks. So the output is read back with the parser and
196
+ * compared, as flat runs of marked text, with what was meant; if they
197
+ * differ, the same content is written without its emphasis, and failing
198
+ * that as plain text. Links, code and every character survive either way.
199
+ */
200
+ function inline(nodes: readonly InlineNode[], mode: InlineMode): string {
201
+ const meant = expel(nodes);
202
+ const first = write(meant, mode);
203
+ if (!checkable(meant) || readsAs(first, meant, mode)) return first;
204
+ const plainer = expel(stripEmphasis(meant));
205
+ const second = write(plainer, mode);
206
+ if (readsAs(second, plainer, mode)) return second;
207
+ return write(expel(textOnly(meant)), mode);
208
+ }
209
+
210
+ function write(nodes: readonly InlineNode[], mode: InlineMode): string {
211
+ const w = new InlineWriter(mode);
212
+ w.nodes(nodes);
213
+ return w.finish();
214
+ }
215
+
216
+ /** Only content the parser can say back — no MDX nodes — is checked. */
217
+ function checkable(nodes: readonly InlineNode[]): boolean {
218
+ return nodes.every(
219
+ (n) =>
220
+ n.type !== 'expression' &&
221
+ n.type !== 'component' &&
222
+ (!('children' in n) || checkable(n.children)),
223
+ );
224
+ }
225
+
226
+ /** Whether `text`, read back where it will be read, is `meant`. */
227
+ function readsAs(
228
+ text: string,
229
+ meant: readonly InlineNode[],
230
+ mode: InlineMode,
231
+ ): boolean {
232
+ // a row is split on bare pipes before its cells are read, and a cell is
233
+ // trimmed; the other two modes read their text as it is
234
+ const source = mode === 'cell' ? text.replace(/\\\|/g, '|').trim() : text;
235
+ const back = parseInline(source, false);
236
+ return flatKey(back, mode) === flatKey(meant, mode);
237
+ }
238
+
239
+ /**
240
+ * Inline content as flat runs of marked text — what a ProseMirror document
241
+ * holds, and so what has to survive. Nesting order is not in it (`***x***`
242
+ * is bold and italic either way round), and neither are the breaks that
243
+ * trail a paragraph, which markdown cannot hold.
244
+ */
245
+ function flatKey(nodes: readonly InlineNode[], mode: InlineMode): string {
246
+ const runs: [string, string][] = [];
247
+ const push = (marks: string, text: string): void => {
248
+ const last = runs[runs.length - 1];
249
+ if (last && last[0] === marks) last[1] += text;
250
+ else runs.push([marks, text]);
251
+ };
252
+ const walk = (list: readonly InlineNode[], marks: string): void => {
253
+ for (const node of list) {
254
+ switch (node.type) {
255
+ case 'text':
256
+ for (const part of node.text.split(/(\n)/)) {
257
+ if (part === '\n') push('\n', mode === 'block' ? '\n' : ' ');
258
+ else if (part) push(marks, part);
259
+ }
260
+ break;
261
+ case 'code':
262
+ push(`${marks}|code`, node.text.replace(/\n/g, ' '));
263
+ break;
264
+ case 'break':
265
+ if (mode === 'block') push('\n', '\n');
266
+ else push(marks, ' ');
267
+ break;
268
+ case 'link':
269
+ if (node.image)
270
+ push(
271
+ `${marks}|img:${node.href}:${node.title ?? ''}`,
272
+ plain(node.children) || '',
273
+ );
274
+ else if (node.href === null) walk(node.children, marks);
275
+ else
276
+ walk(node.children, `${marks}|a:${node.href}:${node.title ?? ''}`);
277
+ break;
278
+ case 'strong':
279
+ case 'em':
280
+ case 'del':
281
+ walk(
282
+ node.children,
283
+ [...marks.split('|').filter(Boolean), node.type].sort().join('|'),
284
+ );
285
+ break;
286
+ default:
287
+ break;
288
+ }
289
+ }
290
+ };
291
+ walk(nodes, '');
292
+ while (runs.length && runs[runs.length - 1][0] === '\n') runs.pop();
293
+ return JSON.stringify(runs);
294
+ }
295
+
296
+ function plain(nodes: readonly InlineNode[]): string {
297
+ let out = '';
298
+ for (const node of nodes) {
299
+ if (node.type === 'text' || node.type === 'code') out += node.text;
300
+ else if ('children' in node) out += plain(node.children);
301
+ }
302
+ return out;
303
+ }
304
+
305
+ function stripEmphasis(nodes: readonly InlineNode[]): InlineNode[] {
306
+ const out: InlineNode[] = [];
307
+ for (const node of nodes) {
308
+ if (node.type === 'strong' || node.type === 'em' || node.type === 'del') {
309
+ out.push(...stripEmphasis(node.children));
310
+ } else if (node.type === 'link') {
311
+ out.push({ ...node, children: stripEmphasis(node.children) });
312
+ } else out.push(node);
313
+ }
314
+ return out;
315
+ }
316
+
317
+ function textOnly(nodes: readonly InlineNode[]): InlineNode[] {
318
+ const out: InlineNode[] = [];
319
+ for (const node of nodes) {
320
+ if (node.type === 'text' || node.type === 'break') out.push(node);
321
+ else if (node.type === 'code') out.push({ type: 'text', text: node.text });
322
+ else if ('children' in node) out.push(...textOnly(node.children));
323
+ }
324
+ return out;
325
+ }
326
+
327
+ const WS = /\s/u;
328
+ const WORD = /[\p{L}\p{N}]/u;
329
+ const ENTITY_LIKE =
330
+ /^&(?:#\d{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]{1,31});/;
331
+
332
+ class InlineWriter {
333
+ private out = '';
334
+ /** The next character starts a line — the top of the text, or just after
335
+ * a hard break — which is where block syntax lives. */
336
+ private lineStart = true;
337
+
338
+ constructor(private readonly mode: InlineMode) {}
339
+
340
+ finish(): string {
341
+ let text = this.out;
342
+ if (this.mode === 'block') {
343
+ // A hard break has to be followed by something: at the very end of a
344
+ // paragraph it would read back as a literal backslash, so it goes.
345
+ text = text.replace(/(\\\n)+$/, '');
346
+ } else if (/\s$/u.test(text)) {
347
+ // The ATX rule and a cell's trim both eat trailing whitespace; the last
348
+ // character of it is spelled as a reference so the text keeps it.
349
+ const last = text[text.length - 1];
350
+ text = text.slice(0, -1) + (last === '\t' ? '&#9;' : '&#32;');
351
+ }
352
+ return text;
353
+ }
354
+
355
+ /** `after` is the character the output continues with once these nodes
356
+ * are written — a closing delimiter, a `]`, or nothing at all. */
357
+ nodes(nodes: readonly InlineNode[], after?: string): void {
358
+ for (let i = 0; i < nodes.length; i++) {
359
+ const node = nodes[i];
360
+ const next = i + 1 < nodes.length ? leadOf(nodes[i + 1]) : after;
361
+ switch (node.type) {
362
+ case 'text':
363
+ this.text(node.text, next);
364
+ break;
365
+ case 'code':
366
+ this.code(node.text);
367
+ break;
368
+ case 'strong':
369
+ this.wrap('**', node.children, next);
370
+ break;
371
+ case 'em': {
372
+ // Beside another `*` run — italic at the start of bold, at its
373
+ // end, or right before it — the two delimiters would merge into
374
+ // one run and be read differently (`***x***` is bold inside
375
+ // italic), so this one is `_`, where `_` can stand: not against a
376
+ // word on either side.
377
+ const besideStar = this.out.endsWith('*') || next === '*';
378
+ const underscore =
379
+ besideStar &&
380
+ !WORD.test(lastChar(this.out) ?? '') &&
381
+ !WORD.test(next ?? '');
382
+ this.wrap(underscore ? '_' : '*', node.children, next);
383
+ break;
384
+ }
385
+ case 'del':
386
+ this.wrap('~~', node.children, next);
387
+ break;
388
+ case 'link':
389
+ this.link(node);
390
+ break;
391
+ case 'break':
392
+ this.hardBreak();
393
+ break;
394
+ case 'expression':
395
+ this.raw(`{${node.src}}`);
396
+ break;
397
+ case 'component':
398
+ this.nodes(node.children);
399
+ break;
400
+ }
401
+ }
402
+ }
403
+
404
+ private raw(text: string): void {
405
+ this.out += text;
406
+ if (text) this.lineStart = false;
407
+ }
408
+
409
+ private hardBreak(): void {
410
+ if (this.mode !== 'block') {
411
+ // a heading or a cell is one line: the break is a space, spelled the
412
+ // way any other space at its edge is
413
+ this.text(' ', undefined);
414
+ return;
415
+ }
416
+ this.out += '\\\n';
417
+ this.lineStart = true;
418
+ }
419
+
420
+ /**
421
+ * A delimited run. Whether a delimiter opens or closes depends on the
422
+ * characters either side of it — CommonMark's flanking rules — so the
423
+ * content is written first and the placement checked against what
424
+ * actually surrounds it. Some placements have no spelling at all: `**`
425
+ * cannot close after punctuation when a letter follows (`**"a"**s`). Then
426
+ * the text is kept and the emphasis is not, because a word that stops
427
+ * being bold is a smaller loss than a document that grows literal
428
+ * asterisks every time it is saved.
429
+ *
430
+ * A dropped run is written again rather than unwrapped: what is inside it
431
+ * was judged against this run's delimiters, which are no longer there, so
432
+ * an inner run that stood beside them may not stand beside what replaced
433
+ * them — and a line start it sat after is a line start again.
434
+ */
435
+ private wrap(
436
+ delimiter: string,
437
+ children: readonly InlineNode[],
438
+ after: string | undefined,
439
+ ): void {
440
+ const start = this.out.length;
441
+ const wasLineStart = this.lineStart;
442
+ const before = lastChar(this.out);
443
+ this.raw(delimiter);
444
+ const innerStart = this.out.length;
445
+ this.nodes(children, delimiter[0]);
446
+ const inner = this.out.slice(innerStart);
447
+ const ch = delimiter[0];
448
+ if (
449
+ inner !== '' &&
450
+ canOpen(ch, before, firstChar(inner)) &&
451
+ canClose(ch, lastChar(inner), after)
452
+ ) {
453
+ this.raw(delimiter);
454
+ return;
455
+ }
456
+ this.out = this.out.slice(0, start);
457
+ this.lineStart = wasLineStart;
458
+ this.nodes(children, after);
459
+ }
460
+
461
+ private code(text: string): void {
462
+ const flat = text.replace(/\n/g, ' ');
463
+ let longest = 0;
464
+ for (const m of flat.matchAll(/`+/g))
465
+ longest = Math.max(longest, m[0].length);
466
+ const fence = '`'.repeat(longest + 1);
467
+ // CommonMark shaves one space off each end when both have one, and a
468
+ // span that starts or ends in a backtick would merge with its fence
469
+ const pad =
470
+ flat.startsWith('`') ||
471
+ flat.endsWith('`') ||
472
+ (flat.startsWith(' ') && flat.endsWith(' ') && flat.trim() !== '')
473
+ ? ' '
474
+ : '';
475
+ this.raw(this.cellSafe(fence + pad + flat + pad + fence));
476
+ }
477
+
478
+ /**
479
+ * In a table cell a bare `|` ends the cell — inside a code span or a link
480
+ * destination too, because a table row is split before its cells' inline
481
+ * syntax is read, and `\|` is turned back into `|` as it is split.
482
+ */
483
+ private cellSafe(raw: string): string {
484
+ return this.mode === 'cell' ? raw.replace(/\|/g, '\\|') : raw;
485
+ }
486
+
487
+ private link(node: LinkInline): void {
488
+ if (node.href === null) {
489
+ this.nodes(node.children); // a link still streaming in has nowhere to go
490
+ return;
491
+ }
492
+ const tail = this.cellSafe(
493
+ `](${destination(node.href)}${titlePart(node.title)})`,
494
+ );
495
+ if (node.image) {
496
+ this.raw('![');
497
+ this.nodes(node.children, ']');
498
+ this.raw(tail);
499
+ return;
500
+ }
501
+ const auto = autolink(node);
502
+ if (auto !== null) {
503
+ this.raw(this.cellSafe(auto));
504
+ return;
505
+ }
506
+ this.raw('[');
507
+ this.nodes(node.children, ']');
508
+ this.raw(tail);
509
+ }
510
+
511
+ /** `after` is the character the output continues with past this text, so
512
+ * a character at the text's edge is judged by its real neighbour and not
513
+ * by where one node happened to end. */
514
+ private text(text: string, after: string | undefined): void {
515
+ for (let i = 0; i < text.length; i++) {
516
+ const ch = text[i];
517
+ if (ch === '\n') {
518
+ this.hardBreak(); // a text node the editor built may carry one
519
+ continue;
520
+ }
521
+ if (this.lineStart) {
522
+ this.lineStart = false;
523
+ const lead = this.lineLead(text, i);
524
+ if (lead !== null) {
525
+ this.out += lead.text;
526
+ i += lead.consumed - 1;
527
+ continue;
528
+ }
529
+ }
530
+ this.out += this.escape(text, i, after);
531
+ }
532
+ }
533
+
534
+ /**
535
+ * The first character of a line, when it would open a block: `# ` a
536
+ * heading, `>` a quote, `-`/`+`/`=`/`:`/`|`/`~` a list, a setext underline,
537
+ * a table's delimiter row or a fence, `12.` an ordered list, and leading
538
+ * whitespace an indented code block (or nothing, after a trim). The last is
539
+ * spelled as a character reference, which no block rule looks at.
540
+ */
541
+ private lineLead(
542
+ text: string,
543
+ i: number,
544
+ ): { text: string; consumed: number } | null {
545
+ const ch = text[i];
546
+ if (ch === ' ') return { text: '&#32;', consumed: 1 };
547
+ if (ch === '\t') return { text: '&#9;', consumed: 1 };
548
+ if (this.mode !== 'block') return null;
549
+ if ('#>-+=:|~'.includes(ch)) return { text: `\\${ch}`, consumed: 1 };
550
+ const ordered = /^(\d{1,9})([.)])/.exec(text.slice(i));
551
+ if (ordered) {
552
+ const after = text[i + ordered[0].length];
553
+ if (after === undefined || WS.test(after)) {
554
+ return {
555
+ text: `${ordered[1]}\\${ordered[2]}`,
556
+ consumed: ordered[0].length,
557
+ };
558
+ }
559
+ }
560
+ return null;
561
+ }
562
+
563
+ private escape(text: string, i: number, after: string | undefined): string {
564
+ const ch = text[i];
565
+ const prev = i > 0 ? text[i - 1] : lastChar(this.out);
566
+ const next = i + 1 < text.length ? text[i + 1] : after;
567
+ switch (ch) {
568
+ case '\\':
569
+ case '`':
570
+ case '*':
571
+ case '[':
572
+ case ']':
573
+ return `\\${ch}`;
574
+ case '_':
575
+ // intraword `_` is never emphasis — snake_case needs nothing
576
+ return WORD.test(prev ?? '') && WORD.test(next ?? '') ? ch : `\\${ch}`;
577
+ case '~':
578
+ // strikethrough is `~~` only, so a lone tilde is text — but one
579
+ // beside another, or beside a strikethrough's own delimiter, is not
580
+ return prev === '~' || next === '~' ? `\\${ch}` : ch;
581
+ case '<':
582
+ return /[A-Za-z/!?]/.test(text[i + 1] ?? '') ? `\\${ch}` : ch;
583
+ case '&':
584
+ return ENTITY_LIKE.test(text.slice(i)) ? `\\${ch}` : ch;
585
+ case '!':
586
+ // `!` then a link is an image
587
+ return next === '[' ? `\\${ch}` : ch;
588
+ case '|':
589
+ return this.mode === 'cell' ? `\\${ch}` : ch;
590
+ default:
591
+ return ch;
592
+ }
593
+ }
594
+ }
595
+
596
+ // --- flanking -----------------------------------------------------------------
597
+ //
598
+ // The parser's own rules (`./parse.ts`, the emphasis scan), asked in advance.
599
+
600
+ const PUNCT = /[\p{P}\p{S}]/u;
601
+
602
+ function kind(ch: string | undefined): 'ws' | 'punct' | 'other' {
603
+ if (ch === undefined || ch === '' || WS.test(ch)) return 'ws';
604
+ return PUNCT.test(ch) ? 'punct' : 'other';
605
+ }
606
+
607
+ function flanks(before: string | undefined, after: string | undefined) {
608
+ const b = kind(before);
609
+ const a = kind(after);
610
+ return {
611
+ b,
612
+ a,
613
+ left: a !== 'ws' && (a !== 'punct' || b !== 'other'),
614
+ right: b !== 'ws' && (b !== 'punct' || a !== 'other'),
615
+ };
616
+ }
617
+
618
+ function canOpen(
619
+ ch: string,
620
+ before: string | undefined,
621
+ first: string | undefined,
622
+ ): boolean {
623
+ const f = flanks(before, first);
624
+ return ch === '_' ? f.left && (!f.right || f.b === 'punct') : f.left;
625
+ }
626
+
627
+ function canClose(
628
+ ch: string,
629
+ last: string | undefined,
630
+ after: string | undefined,
631
+ ): boolean {
632
+ const f = flanks(last, after);
633
+ return ch === '_' ? f.right && (!f.left || f.a === 'punct') : f.right;
634
+ }
635
+
636
+ /** The whole last character of `text` — both halves of a surrogate pair. */
637
+ function lastChar(text: string): string | undefined {
638
+ if (text === '') return undefined;
639
+ const unit = text.charCodeAt(text.length - 1);
640
+ return unit >= 0xdc00 && unit <= 0xdfff && text.length >= 2
641
+ ? text.slice(-2)
642
+ : text[text.length - 1];
643
+ }
644
+
645
+ function firstChar(text: string): string | undefined {
646
+ const cp = text.codePointAt(0);
647
+ return cp === undefined ? undefined : String.fromCodePoint(cp);
648
+ }
649
+
650
+ /** The first character a node writes — all its neighbour's flanking can
651
+ * see. Every delimiter and bracket is punctuation, whichever it turns out
652
+ * to be. */
653
+ function leadOf(node: InlineNode): string | undefined {
654
+ switch (node.type) {
655
+ case 'text':
656
+ return firstChar(node.text);
657
+ case 'code':
658
+ return '`';
659
+ case 'strong':
660
+ case 'em':
661
+ return '*';
662
+ case 'del':
663
+ return '~';
664
+ case 'link':
665
+ return node.image ? '!' : '[';
666
+ case 'break':
667
+ return '\\';
668
+ case 'expression':
669
+ return '{';
670
+ case 'component':
671
+ return node.children.length ? leadOf(node.children[0]) : undefined;
672
+ }
673
+ }
674
+
675
+ /** `<https://…>` for a link whose text is its own target. */
676
+ function autolink(node: LinkInline): string | null {
677
+ if (node.title || !node.href || node.children.length !== 1) return null;
678
+ const only = node.children[0];
679
+ if (only.type !== 'text') return null;
680
+ const text = only.text;
681
+ if (
682
+ text === node.href &&
683
+ /^[a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^<>\s]*$/.test(text)
684
+ ) {
685
+ return `<${text}>`;
686
+ }
687
+ if (
688
+ node.href === `mailto:${text}` &&
689
+ /^[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+$/.test(text)
690
+ ) {
691
+ return `<${text}>`;
692
+ }
693
+ return null;
694
+ }
695
+
696
+ /** A link destination: bare with its parentheses escaped, or in `<…>` when
697
+ * it has whitespace in it — the one form a space survives in. */
698
+ function destination(href: string): string {
699
+ if (href === '') return '<>';
700
+ if (/[\s<>]/.test(href)) {
701
+ return `<${href.replace(/[\n\r]/g, '').replace(/[<>]/g, (c) => encodeURIComponent(c))}>`;
702
+ }
703
+ return href.replace(/[\\()]/g, (c) => `\\${c}`);
704
+ }
705
+
706
+ /** ` "title"`, in whichever of the three quote styles the title does not
707
+ * contain — the parser's title scan has no escapes. */
708
+ function titlePart(title: string | undefined): string {
709
+ if (!title) return '';
710
+ if (!title.includes('"')) return ` "${title}"`;
711
+ if (!title.includes("'")) return ` '${title}'`;
712
+ if (!title.includes(')')) return ` (${title})`;
713
+ return ` "${title.replace(/"/g, '&quot;')}"`;
714
+ }
715
+
716
+ /**
717
+ * Emphasis can open only before non-space and close only after it (the
718
+ * flanking rules), so `**bold **` is not bold. Whitespace at the edge of a
719
+ * strong/em/del node moves outside it, and a node that was all whitespace
720
+ * becomes that whitespace.
721
+ */
722
+ function expel(nodes: readonly InlineNode[]): InlineNode[] {
723
+ const out: InlineNode[] = [];
724
+ for (const node of nodes) {
725
+ if (node.type === 'strong' || node.type === 'em' || node.type === 'del') {
726
+ const children = expel(node.children);
727
+ const lead = takeEdge(children, 'start');
728
+ const trail = takeEdge(children, 'end');
729
+ if (lead) out.push({ type: 'text', text: lead });
730
+ if (children.length > 0) out.push({ type: node.type, children });
731
+ if (trail) out.push({ type: 'text', text: trail });
732
+ } else if (node.type === 'link' || node.type === 'component') {
733
+ out.push({ ...node, children: expel(node.children) });
734
+ } else {
735
+ out.push(node);
736
+ }
737
+ }
738
+ return mergeTexts(out);
739
+ }
740
+
741
+ /** Remove, and return, the whitespace at one end of `nodes` (in place). */
742
+ function takeEdge(nodes: InlineNode[], side: 'start' | 'end'): string {
743
+ const at = side === 'start' ? 0 : nodes.length - 1;
744
+ const node = nodes[at];
745
+ if (!node || node.type !== 'text') return '';
746
+ const m =
747
+ side === 'start' ? /^\s+/u.exec(node.text) : /\s+$/u.exec(node.text);
748
+ if (!m) return '';
749
+ const rest =
750
+ side === 'start'
751
+ ? node.text.slice(m[0].length)
752
+ : node.text.slice(0, node.text.length - m[0].length);
753
+ if (rest === '') nodes.splice(at, 1);
754
+ else nodes[at] = { type: 'text', text: rest };
755
+ return m[0];
756
+ }
757
+
758
+ function mergeTexts(nodes: InlineNode[]): InlineNode[] {
759
+ const out: InlineNode[] = [];
760
+ for (const node of nodes) {
761
+ const prev = out[out.length - 1];
762
+ if (node.type === 'text') {
763
+ if (node.text === '') continue;
764
+ if (prev?.type === 'text') {
765
+ out[out.length - 1] = { type: 'text', text: prev.text + node.text };
766
+ continue;
767
+ }
768
+ }
769
+ out.push(node);
770
+ }
771
+ return out;
772
+ }