@react-x11/components 0.8.0 → 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 (323) hide show
  1. package/README.md +95 -34
  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/embed/client.d.ts +48 -3
  7. package/dist/embed/client.d.ts.map +1 -1
  8. package/dist/embed/client.js +79 -5
  9. package/dist/embed/client.js.map +1 -1
  10. package/dist/embed/index.d.ts +1 -1
  11. package/dist/embed/index.d.ts.map +1 -1
  12. package/dist/embed/index.js +1 -1
  13. package/dist/embed/index.js.map +1 -1
  14. package/dist/html/node.d.ts +3 -1
  15. package/dist/html/node.d.ts.map +1 -1
  16. package/dist/html/node.js +7 -5
  17. package/dist/html/node.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
  23. package/dist/internal/markdown/ast.d.ts.map +1 -0
  24. package/dist/internal/markdown/ast.js +22 -0
  25. package/dist/internal/markdown/ast.js.map +1 -0
  26. package/dist/internal/markdown/parse.d.ts.map +1 -0
  27. package/dist/{markdown → internal/markdown}/parse.js +112 -22
  28. package/dist/internal/markdown/parse.js.map +1 -0
  29. package/dist/internal/markdown/stringify.d.ts +4 -0
  30. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  31. package/dist/internal/markdown/stringify.js +684 -0
  32. package/dist/internal/markdown/stringify.js.map +1 -0
  33. package/dist/internal/markdown/tags.d.ts.map +1 -0
  34. package/dist/internal/markdown/tags.js.map +1 -0
  35. package/dist/internal/scroll.d.ts +3 -2
  36. package/dist/internal/scroll.d.ts.map +1 -1
  37. package/dist/internal/scroll.js +1 -0
  38. package/dist/internal/scroll.js.map +1 -1
  39. package/dist/maps/anchors.d.ts +98 -0
  40. package/dist/maps/anchors.d.ts.map +1 -0
  41. package/dist/maps/anchors.js +531 -0
  42. package/dist/maps/anchors.js.map +1 -0
  43. package/dist/maps/controller.d.ts +283 -0
  44. package/dist/maps/controller.d.ts.map +1 -0
  45. package/dist/maps/controller.js +743 -0
  46. package/dist/maps/controller.js.map +1 -0
  47. package/dist/maps/gl/buckets.d.ts +95 -0
  48. package/dist/maps/gl/buckets.d.ts.map +1 -0
  49. package/dist/maps/gl/buckets.js +362 -0
  50. package/dist/maps/gl/buckets.js.map +1 -0
  51. package/dist/maps/gl/build-worker.d.ts +13 -0
  52. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  53. package/dist/maps/gl/build-worker.js +44 -0
  54. package/dist/maps/gl/build-worker.js.map +1 -0
  55. package/dist/maps/gl/color.d.ts +8 -0
  56. package/dist/maps/gl/color.d.ts.map +1 -0
  57. package/dist/maps/gl/color.js +57 -0
  58. package/dist/maps/gl/color.js.map +1 -0
  59. package/dist/maps/gl/cover.d.ts +51 -0
  60. package/dist/maps/gl/cover.d.ts.map +1 -0
  61. package/dist/maps/gl/cover.js +124 -0
  62. package/dist/maps/gl/cover.js.map +1 -0
  63. package/dist/maps/gl/index.d.ts +3 -0
  64. package/dist/maps/gl/index.d.ts.map +1 -0
  65. package/dist/maps/gl/index.js +20 -0
  66. package/dist/maps/gl/index.js.map +1 -0
  67. package/dist/maps/gl/markers.d.ts +28 -0
  68. package/dist/maps/gl/markers.d.ts.map +1 -0
  69. package/dist/maps/gl/markers.js +60 -0
  70. package/dist/maps/gl/markers.js.map +1 -0
  71. package/dist/maps/gl/overlays.d.ts +68 -0
  72. package/dist/maps/gl/overlays.d.ts.map +1 -0
  73. package/dist/maps/gl/overlays.js +300 -0
  74. package/dist/maps/gl/overlays.js.map +1 -0
  75. package/dist/maps/gl/pane.d.ts +12 -0
  76. package/dist/maps/gl/pane.d.ts.map +1 -0
  77. package/dist/maps/gl/pane.js +37 -0
  78. package/dist/maps/gl/pane.js.map +1 -0
  79. package/dist/maps/gl/placement.d.ts +111 -0
  80. package/dist/maps/gl/placement.d.ts.map +1 -0
  81. package/dist/maps/gl/placement.js +567 -0
  82. package/dist/maps/gl/placement.js.map +1 -0
  83. package/dist/maps/gl/renderer.d.ts +352 -0
  84. package/dist/maps/gl/renderer.d.ts.map +1 -0
  85. package/dist/maps/gl/renderer.js +1127 -0
  86. package/dist/maps/gl/renderer.js.map +1 -0
  87. package/dist/maps/gl/shaders.d.ts +120 -0
  88. package/dist/maps/gl/shaders.d.ts.map +1 -0
  89. package/dist/maps/gl/shaders.js +451 -0
  90. package/dist/maps/gl/shaders.js.map +1 -0
  91. package/dist/maps/gl/store.d.ts +115 -0
  92. package/dist/maps/gl/store.d.ts.map +1 -0
  93. package/dist/maps/gl/store.js +469 -0
  94. package/dist/maps/gl/store.js.map +1 -0
  95. package/dist/maps/gl/text.d.ts +154 -0
  96. package/dist/maps/gl/text.d.ts.map +1 -0
  97. package/dist/maps/gl/text.js +382 -0
  98. package/dist/maps/gl/text.js.map +1 -0
  99. package/dist/maps/gl/view.d.ts +54 -0
  100. package/dist/maps/gl/view.d.ts.map +1 -0
  101. package/dist/maps/gl/view.js +974 -0
  102. package/dist/maps/gl/view.js.map +1 -0
  103. package/dist/maps/index.d.ts +1 -1
  104. package/dist/maps/index.d.ts.map +1 -1
  105. package/dist/maps/index.js +216 -52
  106. package/dist/maps/index.js.map +1 -1
  107. package/dist/maps/labels.d.ts +37 -16
  108. package/dist/maps/labels.d.ts.map +1 -1
  109. package/dist/maps/labels.js +188 -119
  110. package/dist/maps/labels.js.map +1 -1
  111. package/dist/maps/node.d.ts +162 -56
  112. package/dist/maps/node.d.ts.map +1 -1
  113. package/dist/maps/node.js +607 -515
  114. package/dist/maps/node.js.map +1 -1
  115. package/dist/maps/overlay.d.ts +72 -9
  116. package/dist/maps/overlay.d.ts.map +1 -1
  117. package/dist/maps/overlay.js +108 -28
  118. package/dist/maps/overlay.js.map +1 -1
  119. package/dist/maps/paint.d.ts +3 -0
  120. package/dist/maps/paint.d.ts.map +1 -1
  121. package/dist/maps/paint.js.map +1 -1
  122. package/dist/maps/proj.d.ts +14 -0
  123. package/dist/maps/proj.d.ts.map +1 -1
  124. package/dist/maps/proj.js +28 -0
  125. package/dist/maps/proj.js.map +1 -1
  126. package/dist/maps/renderer.d.ts +69 -0
  127. package/dist/maps/renderer.d.ts.map +1 -0
  128. package/dist/maps/renderer.js +66 -0
  129. package/dist/maps/renderer.js.map +1 -0
  130. package/dist/maps/sources.d.ts +25 -3
  131. package/dist/maps/sources.d.ts.map +1 -1
  132. package/dist/maps/sources.js +17 -0
  133. package/dist/maps/sources.js.map +1 -1
  134. package/dist/maps/theme.d.ts +19 -0
  135. package/dist/maps/theme.d.ts.map +1 -0
  136. package/dist/maps/theme.js +44 -0
  137. package/dist/maps/theme.js.map +1 -0
  138. package/dist/maps/tiles.d.ts +117 -17
  139. package/dist/maps/tiles.d.ts.map +1 -1
  140. package/dist/maps/tiles.js +163 -30
  141. package/dist/maps/tiles.js.map +1 -1
  142. package/dist/maps/types.d.ts +188 -20
  143. package/dist/maps/types.d.ts.map +1 -1
  144. package/dist/markdown/expressions.d.ts +1 -1
  145. package/dist/markdown/expressions.d.ts.map +1 -1
  146. package/dist/markdown/expressions.js.map +1 -1
  147. package/dist/markdown/index.d.ts +7 -6
  148. package/dist/markdown/index.d.ts.map +1 -1
  149. package/dist/markdown/index.js +9 -7
  150. package/dist/markdown/index.js.map +1 -1
  151. package/dist/markdown/spans.d.ts +1 -1
  152. package/dist/markdown/spans.d.ts.map +1 -1
  153. package/dist/media-player/index.d.ts +11 -3
  154. package/dist/media-player/index.d.ts.map +1 -1
  155. package/dist/media-player/index.js +13 -2
  156. package/dist/media-player/index.js.map +1 -1
  157. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  158. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  159. package/dist/rich-text-editor/clipboard.js +307 -0
  160. package/dist/rich-text-editor/clipboard.js.map +1 -0
  161. package/dist/rich-text-editor/collab.d.ts +32 -0
  162. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  163. package/dist/rich-text-editor/collab.js +55 -0
  164. package/dist/rich-text-editor/collab.js.map +1 -0
  165. package/dist/rich-text-editor/commands.d.ts +50 -0
  166. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  167. package/dist/rich-text-editor/commands.js +376 -0
  168. package/dist/rich-text-editor/commands.js.map +1 -0
  169. package/dist/rich-text-editor/drag.d.ts +68 -0
  170. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  171. package/dist/rich-text-editor/drag.js +148 -0
  172. package/dist/rich-text-editor/drag.js.map +1 -0
  173. package/dist/rich-text-editor/html.d.ts +145 -0
  174. package/dist/rich-text-editor/html.d.ts.map +1 -0
  175. package/dist/rich-text-editor/html.js +516 -0
  176. package/dist/rich-text-editor/html.js.map +1 -0
  177. package/dist/rich-text-editor/index.d.ts +263 -0
  178. package/dist/rich-text-editor/index.d.ts.map +1 -0
  179. package/dist/rich-text-editor/index.js +822 -0
  180. package/dist/rich-text-editor/index.js.map +1 -0
  181. package/dist/rich-text-editor/inline.d.ts +94 -0
  182. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  183. package/dist/rich-text-editor/inline.js +252 -0
  184. package/dist/rich-text-editor/inline.js.map +1 -0
  185. package/dist/rich-text-editor/keymap.d.ts +71 -0
  186. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  187. package/dist/rich-text-editor/keymap.js +393 -0
  188. package/dist/rich-text-editor/keymap.js.map +1 -0
  189. package/dist/rich-text-editor/keys.d.ts +29 -0
  190. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  191. package/dist/rich-text-editor/keys.js +93 -0
  192. package/dist/rich-text-editor/keys.js.map +1 -0
  193. package/dist/rich-text-editor/look.d.ts +71 -0
  194. package/dist/rich-text-editor/look.d.ts.map +1 -0
  195. package/dist/rich-text-editor/look.js +342 -0
  196. package/dist/rich-text-editor/look.js.map +1 -0
  197. package/dist/rich-text-editor/markdown.d.ts +18 -0
  198. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  199. package/dist/rich-text-editor/markdown.js +496 -0
  200. package/dist/rich-text-editor/markdown.js.map +1 -0
  201. package/dist/rich-text-editor/nodes.d.ts +135 -0
  202. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  203. package/dist/rich-text-editor/nodes.js +397 -0
  204. package/dist/rich-text-editor/nodes.js.map +1 -0
  205. package/dist/rich-text-editor/render.d.ts +68 -0
  206. package/dist/rich-text-editor/render.d.ts.map +1 -0
  207. package/dist/rich-text-editor/render.js +500 -0
  208. package/dist/rich-text-editor/render.js.map +1 -0
  209. package/dist/rich-text-editor/schema.d.ts +16 -0
  210. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  211. package/dist/rich-text-editor/schema.js +386 -0
  212. package/dist/rich-text-editor/schema.js.map +1 -0
  213. package/dist/rich-text-editor/store.d.ts +11 -0
  214. package/dist/rich-text-editor/store.d.ts.map +1 -0
  215. package/dist/rich-text-editor/store.js +43 -0
  216. package/dist/rich-text-editor/store.js.map +1 -0
  217. package/dist/rich-text-editor/suggest.d.ts +127 -0
  218. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  219. package/dist/rich-text-editor/suggest.js +0 -0
  220. package/dist/rich-text-editor/suggest.js.map +1 -0
  221. package/dist/rich-text-editor/tables.d.ts +41 -0
  222. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  223. package/dist/rich-text-editor/tables.js +204 -0
  224. package/dist/rich-text-editor/tables.js.map +1 -0
  225. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  226. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  227. package/dist/rich-text-editor/toolbar.js +548 -0
  228. package/dist/rich-text-editor/toolbar.js.map +1 -0
  229. package/dist/rich-text-editor/view.d.ts +368 -0
  230. package/dist/rich-text-editor/view.d.ts.map +1 -0
  231. package/dist/rich-text-editor/view.js +1968 -0
  232. package/dist/rich-text-editor/view.js.map +1 -0
  233. package/dist/rich-text-editor/virtual.d.ts +54 -0
  234. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  235. package/dist/rich-text-editor/virtual.js +262 -0
  236. package/dist/rich-text-editor/virtual.js.map +1 -0
  237. package/dist/richtext/node.d.ts +25 -2
  238. package/dist/richtext/node.d.ts.map +1 -1
  239. package/dist/richtext/node.js +46 -24
  240. package/dist/richtext/node.js.map +1 -1
  241. package/dist/terminal/backends.d.ts +4 -1
  242. package/dist/terminal/backends.d.ts.map +1 -1
  243. package/dist/terminal/backends.js.map +1 -1
  244. package/dist/terminal/index.d.ts +18 -7
  245. package/dist/terminal/index.d.ts.map +1 -1
  246. package/dist/terminal/index.js +28 -11
  247. package/dist/terminal/index.js.map +1 -1
  248. package/package.json +23 -4
  249. package/src/charts/node.ts +6 -2
  250. package/src/embed/client.ts +102 -8
  251. package/src/embed/index.ts +2 -0
  252. package/src/html/node.ts +7 -5
  253. package/src/index.ts +2 -0
  254. package/src/{markdown → internal/markdown}/ast.ts +24 -7
  255. package/src/{markdown → internal/markdown}/parse.ts +134 -24
  256. package/src/internal/markdown/stringify.ts +772 -0
  257. package/src/internal/scroll.ts +5 -3
  258. package/src/maps/anchors.ts +607 -0
  259. package/src/maps/controller.ts +999 -0
  260. package/src/maps/gl/buckets.ts +457 -0
  261. package/src/maps/gl/build-worker.ts +55 -0
  262. package/src/maps/gl/color.ts +62 -0
  263. package/src/maps/gl/cover.ts +170 -0
  264. package/src/maps/gl/index.ts +23 -0
  265. package/src/maps/gl/markers.ts +83 -0
  266. package/src/maps/gl/overlays.ts +379 -0
  267. package/src/maps/gl/pane.ts +42 -0
  268. package/src/maps/gl/placement.ts +816 -0
  269. package/src/maps/gl/renderer.ts +1576 -0
  270. package/src/maps/gl/shaders.ts +486 -0
  271. package/src/maps/gl/store.ts +566 -0
  272. package/src/maps/gl/text.ts +495 -0
  273. package/src/maps/gl/view.ts +1224 -0
  274. package/src/maps/index.ts +270 -81
  275. package/src/maps/labels.ts +214 -121
  276. package/src/maps/node.ts +763 -629
  277. package/src/maps/overlay.ts +148 -43
  278. package/src/maps/paint.ts +3 -0
  279. package/src/maps/proj.ts +31 -0
  280. package/src/maps/renderer.ts +155 -0
  281. package/src/maps/sources.ts +38 -3
  282. package/src/maps/theme.ts +51 -0
  283. package/src/maps/tiles.ts +191 -33
  284. package/src/maps/types.ts +188 -20
  285. package/src/markdown/expressions.ts +5 -1
  286. package/src/markdown/index.ts +14 -11
  287. package/src/markdown/spans.ts +1 -1
  288. package/src/media-player/index.ts +18 -4
  289. package/src/rich-text-editor/clipboard.ts +439 -0
  290. package/src/rich-text-editor/collab.ts +91 -0
  291. package/src/rich-text-editor/commands.ts +448 -0
  292. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  293. package/src/rich-text-editor/drag.ts +211 -0
  294. package/src/rich-text-editor/html.ts +619 -0
  295. package/src/rich-text-editor/index.ts +1331 -0
  296. package/src/rich-text-editor/inline.ts +345 -0
  297. package/src/rich-text-editor/keymap.ts +510 -0
  298. package/src/rich-text-editor/keys.ts +119 -0
  299. package/src/rich-text-editor/look.ts +427 -0
  300. package/src/rich-text-editor/markdown.ts +653 -0
  301. package/src/rich-text-editor/nodes.ts +497 -0
  302. package/src/rich-text-editor/render.ts +800 -0
  303. package/src/rich-text-editor/schema.ts +431 -0
  304. package/src/rich-text-editor/store.ts +39 -0
  305. package/src/rich-text-editor/suggest.ts +597 -0
  306. package/src/rich-text-editor/tables.ts +272 -0
  307. package/src/rich-text-editor/toolbar.ts +724 -0
  308. package/src/rich-text-editor/view.ts +2287 -0
  309. package/src/rich-text-editor/virtual.ts +347 -0
  310. package/src/richtext/node.ts +59 -30
  311. package/src/terminal/backends.ts +4 -1
  312. package/src/terminal/index.ts +43 -15
  313. package/dist/markdown/ast.d.ts.map +0 -1
  314. package/dist/markdown/ast.js +0 -17
  315. package/dist/markdown/ast.js.map +0 -1
  316. package/dist/markdown/parse.d.ts.map +0 -1
  317. package/dist/markdown/parse.js.map +0 -1
  318. package/dist/markdown/tags.d.ts.map +0 -1
  319. package/dist/markdown/tags.js.map +0 -1
  320. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  321. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  322. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  323. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -12,8 +12,12 @@
12
12
  // `onReady` — because core's `<foreign>` already owns the protocol
13
13
  // (react-x11#269, ntk#246): the reparent, the save set, `_XEMBED_INFO`, the
14
14
  // synthetic ConfigureNotify, and handing the client back on unmount without
15
- // ever destroying it.
15
+ // ever destroying it. The one thing asked of the connection directly is
16
+ // whether it has that protocol at all (`canHostXEmbed`), because on a backend
17
+ // that does not, `onReady` is no evidence either way: the Cocoa backend's
18
+ // `<foreign>` still calls it, with `windowId: undefined`.
16
19
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
20
+ import { useApp } from 'react-x11';
17
21
 
18
22
  import { nodeProcessHost } from './host.js';
19
23
  import type { ExitInfo, ProcessHost, SpawnedProcess } from './host.js';
@@ -25,7 +29,9 @@ import type { ExitInfo, ProcessHost, SpawnedProcess } from './host.js';
25
29
  * with no terminal emulator installed, or no mpv, is an ordinary machine.
26
30
  * Both components render their `fallback` on it, the way
27
31
  * `useDesktopCalendarEvents` reports "no bus, no ical.js" rather than
28
- * throwing.
32
+ * throwing. So is an app on a react-x11 backend that cannot embed another
33
+ * program's window at all — the native macOS one — and `error` says which of
34
+ * the two it is: a `BackendUnavailableError` or an `EmbedUnsupportedError`.
29
35
  */
30
36
  export type EmbedStatus =
31
37
  'idle' | 'starting' | 'running' | 'exited' | 'unavailable';
@@ -51,6 +57,65 @@ export class BackendUnavailableError extends Error {
51
57
  }
52
58
  }
53
59
 
60
+ /**
61
+ * This app cannot host another program's window at all.
62
+ *
63
+ * Embedding is XEmbed, and XEmbed is X protocol — a reparent into a
64
+ * container, the save set, ClientMessages — which only react-x11's X11
65
+ * backend has. The native macOS backend has no cross-process window
66
+ * embedding (react-x11 `docs/macos.md`, "What this is, and is not"), so there
67
+ * is no window to hand a program and nothing to spawn.
68
+ *
69
+ * `useEmbeddedClient` reports it as `status: 'unavailable'`, the status a
70
+ * `BackendUnavailableError` gets, and for the same reason: it is an ordinary
71
+ * state of a healthy app, and the component renders its `fallback`. The class
72
+ * is what tells the two apart, because "install xterm" is the wrong advice
73
+ * here.
74
+ */
75
+ export class EmbedUnsupportedError extends Error {
76
+ constructor() {
77
+ super(
78
+ '@react-x11/components: this react-x11 backend cannot embed another ' +
79
+ "program's window — XEmbed needs the X11 backend, and the native " +
80
+ 'macOS one has no cross-process window embedding at all. Pass ' +
81
+ '`fallback` to render something else.',
82
+ );
83
+ this.name = 'EmbedUnsupportedError';
84
+ }
85
+ }
86
+
87
+ /** The slice of the app `canHostXEmbed` reads. `useApp()` is the escape
88
+ * hatch, so the shape is written out rather than imported — the call
89
+ * `../tray-host/manager.ts` makes for the slice it needs. */
90
+ interface XEmbedHostApp {
91
+ X?: { SetSelectionOwner?: unknown; ReparentWindow?: unknown } | null;
92
+ }
93
+
94
+ /**
95
+ * Can this app host another program's window?
96
+ *
97
+ * Asked of the connection, because `<foreign>` cannot be the one to say: on
98
+ * react-x11's Cocoa backend it still calls `onReady`, with an undefined
99
+ * `windowId`, and on the headless mock mounting one throws from the commit.
100
+ * The test is `<TrayHost>`'s — its
101
+ * manager reports `'unavailable'` when `app.X` has no `SetSelectionOwner`,
102
+ * which is what core's Cocoa `X` lacks (react-x11 `src/cocoa/app.js`: a stub
103
+ * carrying only the few requests core makes there) and what the headless
104
+ * mock's `X` lacks too. `ReparentWindow` is asked as well, because it is the
105
+ * request an embed is actually made of.
106
+ *
107
+ * Takes the app rather than calling `useApp()`, so it can be asked off the
108
+ * render path — of `createRoot()`'s result, before there is a tree.
109
+ */
110
+ export function canHostXEmbed(app: unknown): boolean {
111
+ const X = (app as XEmbedHostApp | null | undefined)?.X;
112
+ return (
113
+ !!X &&
114
+ typeof X.SetSelectionOwner === 'function' &&
115
+ typeof X.ReparentWindow === 'function'
116
+ );
117
+ }
118
+
54
119
  /** What to run, and what to do with it once it is running. */
55
120
  export interface LaunchPlan {
56
121
  /** Absolute path or a name on `PATH`. */
@@ -101,13 +166,15 @@ export interface UseEmbeddedClientOptions {
101
166
  stopSignal?: string;
102
167
  onStart?: (info: { process: SpawnedProcess; backend?: string }) => void;
103
168
  onExit?: (info: ExitInfo) => void;
104
- /** Spawn failures and missing backends alike. */
169
+ /** Spawn failures, missing backends, and an app that cannot host an
170
+ * embedded window at all (`EmbedUnsupportedError`). */
105
171
  onError?: (err: Error) => void;
106
172
  }
107
173
 
108
174
  export interface EmbeddedClient {
109
175
  status: EmbedStatus;
110
- /** The last failure, including a `BackendUnavailableError`. */
176
+ /** The last failure, including a `BackendUnavailableError` or an
177
+ * `EmbedUnsupportedError`. */
111
178
  error: Error | null;
112
179
  pid: number | null;
113
180
  /** The container window, once `<foreign>` has one. */
@@ -134,12 +201,21 @@ export interface EmbeddedClient {
134
201
  * element's props — style, focus, the `onEmbedded`/`onClientGone` handlers a
135
202
  * component may want to compose with — belong to the component, not to the
136
203
  * lifecycle.
204
+ *
205
+ * **On an app that cannot host XEmbed** (`canHostXEmbed` is false — the
206
+ * native macOS backend), `status` is `'unavailable'` from the first render,
207
+ * `error` is an `EmbedUnsupportedError`, and nothing is probed or spawned.
208
+ * The caller should render no `<foreign>` there either: it has no socket to
209
+ * be.
137
210
  */
138
211
  export function useEmbeddedClient(
139
212
  options: UseEmbeddedClientOptions,
140
213
  ): EmbeddedClient {
141
214
  const { plan, enabled = true, stopSignal = 'SIGTERM' } = options;
142
215
  const host = useMemo(() => options.host ?? nodeProcessHost(), [options.host]);
216
+ // Asked every render: two property reads, and an app does not change
217
+ // backend under a mounted tree.
218
+ const embeddable = canHostXEmbed(useApp());
143
219
 
144
220
  const [windowId, setWindowId] = useState<number | null>(null);
145
221
  const [status, setStatus] = useState<EmbedStatus>('idle');
@@ -158,6 +234,24 @@ export function useEmbeddedClient(
158
234
  const handlers = useRef(options);
159
235
  handlers.current = options;
160
236
 
237
+ // No XEmbed on this connection means no window for a program to draw into,
238
+ // whatever `<foreign>` reports: the Cocoa backend's calls `onReady` with an
239
+ // undefined `windowId`, which slipped past a `windowId === null` guard and
240
+ // spawned `xterm -into undefined`. So the app decides, and the lifecycle
241
+ // below starts nothing without it. Decided while rendering rather than in
242
+ // an effect, so the first commit can already be the `fallback`, and
243
+ // reported from an effect keyed on the error itself, so `onError` hears it
244
+ // once rather than once per plan. `enabled` false still means "hold off": a
245
+ // pane nobody can see yet reports nothing.
246
+ const unsupported = enabled && !embeddable;
247
+ const unsupportedError = useMemo(
248
+ () => (unsupported ? new EmbedUnsupportedError() : null),
249
+ [unsupported],
250
+ );
251
+ useEffect(() => {
252
+ if (unsupportedError) handlers.current.onError?.(unsupportedError);
253
+ }, [unsupportedError]);
254
+
161
255
  const handleReady = useCallback((info: { windowId: number }) => {
162
256
  setWindowId(info.windowId);
163
257
  }, []);
@@ -170,7 +264,7 @@ export function useEmbeddedClient(
170
264
  }, []);
171
265
 
172
266
  useEffect(() => {
173
- if (!enabled || windowId === null) {
267
+ if (!enabled || !embeddable || windowId === null) {
174
268
  setStatus('idle');
175
269
  return undefined;
176
270
  }
@@ -271,11 +365,11 @@ export function useEmbeddedClient(
271
365
  void planned?.dispose?.();
272
366
  };
273
367
  // `plan` is the restart signal — see the note on the option.
274
- }, [enabled, windowId, plan, host, stopSignal, generation]);
368
+ }, [enabled, embeddable, windowId, plan, host, stopSignal, generation]);
275
369
 
276
370
  return {
277
- status,
278
- error,
371
+ status: unsupported ? 'unavailable' : status,
372
+ error: unsupportedError ?? error,
279
373
  pid,
280
374
  windowId,
281
375
  backend,
@@ -10,6 +10,8 @@
10
10
  // other `-into WID` program should not have to copy this file to exist.
11
11
  export {
12
12
  BackendUnavailableError,
13
+ EmbedUnsupportedError,
14
+ canHostXEmbed,
13
15
  resolveBackend,
14
16
  useEmbeddedClient,
15
17
  } from './client.js';
package/src/html/node.ts CHANGED
@@ -138,7 +138,9 @@ export class HtmlViewNode extends Node {
138
138
  private _cascade: Cascade | null = null;
139
139
  private _tree: BoxTree | null = null;
140
140
  private _stale: Stale = Stale.Everything;
141
- private _laidOutAt = -1;
141
+ /** The width the document was last laid out at. Not `_laidOutAt`: core's
142
+ * `Node` has a method of that name, which a field would hide. */
143
+ private _laidOutWidth = -1;
142
144
  private _mediaBand = -1;
143
145
  private _documentHeight = 0;
144
146
  private _documentWidth = 0;
@@ -369,7 +371,7 @@ export class HtmlViewNode extends Node {
369
371
  this._sweep();
370
372
  if (this._stale < Stale.Style) this._stale = Stale.Style;
371
373
  }
372
- if (this._stale === Stale.Nothing && this._laidOutAt === target) return;
374
+ if (this._stale === Stale.Nothing && this._laidOutWidth === target) return;
373
375
 
374
376
  if (this._stale >= Stale.Style || !this._cascade) {
375
377
  this._restyle(target);
@@ -398,12 +400,12 @@ export class HtmlViewNode extends Node {
398
400
  });
399
401
  this._textPoints = null;
400
402
  this._pointsAreUnits = null;
401
- this._laidOutAt = -1;
403
+ this._laidOutWidth = -1;
402
404
  }
403
405
 
404
406
  if (
405
407
  this._tree &&
406
- (this._laidOutAt !== target || this._stale >= Stale.Layout)
408
+ (this._laidOutWidth !== target || this._stale >= Stale.Layout)
407
409
  ) {
408
410
  const result = layoutDocument(
409
411
  this._tree,
@@ -413,7 +415,7 @@ export class HtmlViewNode extends Node {
413
415
  );
414
416
  this._documentWidth = result.width;
415
417
  this._documentHeight = result.height;
416
- this._laidOutAt = target;
418
+ this._laidOutWidth = target;
417
419
  this._reportControls();
418
420
  }
419
421
  this._stale = Stale.Nothing;
package/src/index.ts CHANGED
@@ -655,7 +655,9 @@ export type {
655
655
  // a third `-into WID` wrapper should not have to reimplement it.
656
656
  export {
657
657
  BackendUnavailableError,
658
+ EmbedUnsupportedError,
658
659
  IpcConnectError,
660
+ canHostXEmbed,
659
661
  connectWhenReady,
660
662
  nodeProcessHost,
661
663
  resolveBackend,
@@ -1,10 +1,15 @@
1
- // The markdown AST `parse.ts` produces and `index.ts` renders. Deliberately
2
- // small: it is the GFM constructs this component draws, not a general
3
- // mdast — but it is shaped so that growing it stays additive. In particular
4
- // `component` is the MDX direction: `ComponentBlock` is parsed and rendered
5
- // (docs/prd-mdx.md, M1), and `ComponentInline` is still reserved — see the
6
- // note on it for why the inline half needs machinery `<richtext>` does not
7
- // have yet.
1
+ // The markdown AST `parse.ts` produces, `<Markdown>` renders and
2
+ // `<RichTextEditor>` edits (it converts to and from ProseMirror nodes, and
3
+ // `stringify.ts` writes the result back out). Deliberately small: it is the
4
+ // GFM constructs the two components draw, not a general mdast — but it is
5
+ // shaped so that growing it stays additive. In particular `component` is the
6
+ // MDX direction: `ComponentBlock` is parsed and rendered (docs/prd-mdx.md,
7
+ // M1), and `ComponentInline` is still reserved — see the note on it for why
8
+ // the inline half needs machinery `<richtext>` does not have yet.
9
+ //
10
+ // It lives in `src/internal/` because two components read it: shared code no
11
+ // app needs on its own yet, per AGENTS.md's "half-step below a shared
12
+ // module". `<Markdown>` still re-exports the parser and the types.
8
13
 
9
14
  /** Inline content, inside a paragraph, heading, list item or table cell. */
10
15
  export type InlineNode =
@@ -56,6 +61,11 @@ export interface LinkInline {
56
61
  /** True when this was `![alt](src)` — the href is the image source. */
57
62
  image: boolean;
58
63
  children: InlineNode[];
64
+ /**
65
+ * `[text](href "title")`'s title, when there was one. `<Markdown>` renders
66
+ * none; the editor keeps it, so a document it saves still has it.
67
+ */
68
+ title?: string;
59
69
  }
60
70
 
61
71
  /** A hard line break (two trailing spaces, or a trailing backslash). */
@@ -144,6 +154,13 @@ export interface CodeBlock {
144
154
  type: 'code';
145
155
  /** First word of the info string, lowercased — `''` when absent. */
146
156
  lang: string;
157
+ /**
158
+ * The whole info string, trimmed — present only when it says more than
159
+ * `lang` does (```` ```ts title="a.ts" ````, or a language not written in
160
+ * lowercase), so a fence that is just a language parses to the AST it
161
+ * always did. The editor keeps it through a round trip.
162
+ */
163
+ info?: string;
147
164
  text: string;
148
165
  /**
149
166
  * False when the closing fence has not arrived. During streaming that is
@@ -1,5 +1,8 @@
1
- // Markdown → AST, written for this component rather than adapted from a
2
- // DOM-shaped pipeline. GFM-flavoured CommonMark: headings (ATX and setext),
1
+ // Markdown → AST, written for this package rather than adapted from a
2
+ // DOM-shaped pipeline — shared, from `src/internal/`, because two components
3
+ // read it: `<Markdown>` renders what it produces and `<RichTextEditor>` edits
4
+ // it, writing it back out through `./stringify.ts`. GFM-flavoured
5
+ // CommonMark: headings (ATX and setext),
3
6
  // paragraphs, fenced and indented code, blockquotes with lazy continuation,
4
7
  // nested ordered/bullet/task lists with the tight/loose distinction, tables,
5
8
  // thematic breaks; inline emphasis with the real flanking and rule-of-three
@@ -317,7 +320,8 @@ function parseBlocks(
317
320
  const start = i;
318
321
  const fenceIndent = fence[1].length;
319
322
  const marker = fence[2];
320
- const lang = (fence[3].trim().split(/[ \t]/, 1)[0] ?? '').toLowerCase();
323
+ const info = fence[3].trim();
324
+ const lang = (info.split(/[ \t]/, 1)[0] ?? '').toLowerCase();
321
325
  const body: string[] = [];
322
326
  let closed = false;
323
327
  i += 1;
@@ -342,7 +346,19 @@ function parseBlocks(
342
346
  );
343
347
  i += 1;
344
348
  }
345
- commit({ type: 'code', lang, text: body.join('\n'), closed }, start, i);
349
+ commit(
350
+ {
351
+ type: 'code',
352
+ lang,
353
+ // only when it says more than the language does, so a plain
354
+ // ```js fence parses to the AST it always did
355
+ ...(info !== lang ? { info } : null),
356
+ text: body.join('\n'),
357
+ closed,
358
+ },
359
+ start,
360
+ i,
361
+ );
346
362
  continue;
347
363
  }
348
364
 
@@ -607,11 +623,27 @@ function parseList(
607
623
  const contentIndent = indent + m[2].length + pad;
608
624
 
609
625
  const inner: string[] = [rest];
626
+ // The fence open in the item's own content, if one is. A blank line
627
+ // inside a fence is the code's rather than the list's: it neither ends
628
+ // the item nor makes the list loose — and a snippet with an empty line
629
+ // in it, under a numbered step, is the ordinary case.
630
+ // (a task item's box comes off first: `- [ ] ```js` opens a fence too)
631
+ let fence = fenceAfter(null, rest.replace(RE_TASK, ''));
610
632
  i += 1;
611
633
  let pendingBlanks = 0;
612
634
  while (i < n) {
613
635
  const l = lines[i];
614
636
  if (RE_BLANK.test(l)) {
637
+ if (fence !== null && pendingBlanks === 0) {
638
+ // …as long as the item goes on after it: an unclosed fence must
639
+ // not swallow the paragraph the list is followed by
640
+ let k = i + 1;
641
+ while (k < n && RE_BLANK.test(lines[k])) k += 1;
642
+ if (k < n && indentOf(lines[k]) >= contentIndent) {
643
+ for (; i < k; i += 1) inner.push('');
644
+ continue;
645
+ }
646
+ }
615
647
  pendingBlanks += 1;
616
648
  if (pendingBlanks > 1) break;
617
649
  i += 1;
@@ -623,7 +655,9 @@ function parseList(
623
655
  loose = true;
624
656
  pendingBlanks = 0;
625
657
  }
626
- inner.push(l.slice(contentIndent));
658
+ const content = l.slice(contentIndent);
659
+ inner.push(content);
660
+ fence = fenceAfter(fence, content);
627
661
  i += 1;
628
662
  continue;
629
663
  }
@@ -666,6 +700,23 @@ function parseList(
666
700
  };
667
701
  }
668
702
 
703
+ /**
704
+ * The fence still open after `line`, given the one open before it — the
705
+ * opening run (```` ``` ````, `~~~~`) or null. Indentation is ignored, so a
706
+ * fence in a list nested inside this item is seen too.
707
+ */
708
+ function fenceAfter(open: string | null, line: string): string | null {
709
+ const m = RE_FENCE_OPEN.exec(line.trimStart());
710
+ if (open === null) {
711
+ if (!m || (m[2][0] === '`' && m[3].includes('`'))) return null;
712
+ return m[2];
713
+ }
714
+ if (m && m[2][0] === open[0] && m[2].length >= open.length && m[3] === '') {
715
+ return null;
716
+ }
717
+ return open;
718
+ }
719
+
669
720
  // --- tables ----------------------------------------------------------------
670
721
 
671
722
  /** Split a row on unescaped pipes; `\|` stays literal in the cell. */
@@ -763,19 +814,45 @@ const NAMED_ENTITIES: Record<string, string> = {
763
814
  middot: '·',
764
815
  };
765
816
 
817
+ /** The same pattern, anchored — for reading one reference at a position. */
818
+ const RE_ENTITY_AT = new RegExp(RE_ENTITY.source, 'y');
819
+
820
+ function entityText(
821
+ whole: string,
822
+ dec?: string,
823
+ hex?: string,
824
+ named?: string,
825
+ ): string {
826
+ if (dec) {
827
+ const cp = parseInt(dec, 10);
828
+ return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';
829
+ }
830
+ if (hex) {
831
+ const cp = parseInt(hex, 16);
832
+ return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';
833
+ }
834
+ return (named && NAMED_ENTITIES[named]) ?? whole;
835
+ }
836
+
766
837
  function decodeEntities(text: string): string {
767
838
  if (!text.includes('&')) return text;
768
- return text.replace(RE_ENTITY, (whole, dec, hex, named) => {
769
- if (dec) {
770
- const cp = parseInt(dec, 10);
771
- return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';
772
- }
773
- if (hex) {
774
- const cp = parseInt(hex, 16);
775
- return cp > 0 && cp <= 0x10ffff ? String.fromCodePoint(cp) : '�';
776
- }
777
- return NAMED_ENTITIES[named] ?? whole;
778
- });
839
+ return text.replace(RE_ENTITY, entityText);
840
+ }
841
+
842
+ /** The whole character ending at `i`, surrogate pair and all — an emoji is
843
+ * a symbol, and half of one is not. */
844
+ function charBefore(text: string, i: number): string | undefined {
845
+ if (i <= 0) return undefined;
846
+ const unit = text.charCodeAt(i - 1);
847
+ return unit >= 0xdc00 && unit <= 0xdfff && i >= 2
848
+ ? text.slice(i - 2, i)
849
+ : text[i - 1];
850
+ }
851
+
852
+ /** The whole character starting at `i`. */
853
+ function charAt(text: string, i: number): string | undefined {
854
+ const cp = text.codePointAt(i);
855
+ return cp === undefined ? undefined : String.fromCodePoint(cp);
779
856
  }
780
857
 
781
858
  function classify(ch: string | undefined): 'ws' | 'punct' | 'other' {
@@ -797,11 +874,14 @@ export function parseInline(
797
874
  const items: Item[] = [];
798
875
  let buf = '';
799
876
 
877
+ // References are decoded where they are read (below), not on the flushed
878
+ // buffer: `\&amp;` is an escaped ampersand followed by `amp;`, and
879
+ // decoding afterwards turned it back into `&`.
800
880
  const flushText = (): void => {
801
881
  if (buf.length === 0) return;
802
882
  items.push({
803
883
  kind: 'node',
804
- node: { type: 'text', text: decodeEntities(buf) },
884
+ node: { type: 'text', text: buf },
805
885
  });
806
886
  buf = '';
807
887
  };
@@ -957,7 +1037,7 @@ export function parseInline(
957
1037
  const suffix = parseLinkSuffix(text, i + 1);
958
1038
  if (suffix) {
959
1039
  flushText();
960
- closeLink(items, openIdx, suffix.href);
1040
+ closeLink(items, openIdx, suffix.href, suffix.title);
961
1041
  i = suffix.end;
962
1042
  continue;
963
1043
  }
@@ -988,8 +1068,8 @@ export function parseInline(
988
1068
  if (ch === '*' || ch === '_' || ch === '~') {
989
1069
  let run = 1;
990
1070
  while (text[i + run] === ch) run += 1;
991
- const before = classify(text[i - 1]);
992
- const after = classify(text[i + run]);
1071
+ const before = classify(charBefore(text, i));
1072
+ const after = classify(charAt(text, i + run));
993
1073
  const leftFlank =
994
1074
  after !== 'ws' && (after !== 'punct' || before !== 'other');
995
1075
  const rightFlank =
@@ -1023,6 +1103,16 @@ export function parseInline(
1023
1103
  continue;
1024
1104
  }
1025
1105
 
1106
+ if (ch === '&') {
1107
+ RE_ENTITY_AT.lastIndex = i;
1108
+ const ref = RE_ENTITY_AT.exec(text);
1109
+ if (ref) {
1110
+ buf += entityText(ref[0], ref[1], ref[2], ref[3]);
1111
+ i += ref[0].length;
1112
+ continue;
1113
+ }
1114
+ }
1115
+
1026
1116
  buf += ch;
1027
1117
  i += 1;
1028
1118
  }
@@ -1072,10 +1162,12 @@ function lastBracket(items: Item[]): number {
1072
1162
  interface LinkSuffix {
1073
1163
  href: string;
1074
1164
  end: number;
1165
+ title?: string;
1075
1166
  }
1076
1167
 
1077
1168
  /** `](dest "title")` — destination in `<>` or bare with balanced parens.
1078
- * The title is tolerated and dropped: nothing here renders one. */
1169
+ * The title is kept on the node — nothing renders one, but the editor
1170
+ * writes it back out. */
1079
1171
  function parseLinkSuffix(text: string, from: number): LinkSuffix | null {
1080
1172
  if (text[from] !== '(') return null;
1081
1173
  let i = from + 1;
@@ -1110,22 +1202,33 @@ function parseLinkSuffix(text: string, from: number): LinkSuffix | null {
1110
1202
  while (i < len && (text[i] === ' ' || text[i] === '\n' || text[i] === '\t'))
1111
1203
  i += 1;
1112
1204
  // an optional title in any of the three quote styles
1205
+ let title: string | undefined;
1113
1206
  const q = text[i];
1114
1207
  if (q === '"' || q === "'" || q === '(') {
1115
1208
  const closer = q === '(' ? ')' : q;
1116
1209
  let j = i + 1;
1117
1210
  while (j < len && text[j] !== closer) j += 1;
1118
1211
  if (j >= len) return null;
1212
+ title = decodeEntities(text.slice(i + 1, j));
1119
1213
  i = j + 1;
1120
1214
  while (i < len && (text[i] === ' ' || text[i] === '\n' || text[i] === '\t'))
1121
1215
  i += 1;
1122
1216
  }
1123
1217
  if (text[i] !== ')') return null;
1124
- return { href: decodeEntities(href), end: i + 1 };
1218
+ return {
1219
+ href: decodeEntities(href),
1220
+ end: i + 1,
1221
+ ...(title !== undefined ? { title } : null),
1222
+ };
1125
1223
  }
1126
1224
 
1127
1225
  /** Fold everything after the bracket opener at `openIdx` into a link node. */
1128
- function closeLink(items: Item[], openIdx: number, href: string | null): void {
1226
+ function closeLink(
1227
+ items: Item[],
1228
+ openIdx: number,
1229
+ href: string | null,
1230
+ title?: string,
1231
+ ): void {
1129
1232
  const bracket = items[openIdx] as { kind: 'bracket'; image: boolean };
1130
1233
  const inner = items.splice(openIdx);
1131
1234
  inner.shift();
@@ -1133,7 +1236,14 @@ function closeLink(items: Item[], openIdx: number, href: string | null): void {
1133
1236
  processEmphasis(inner, href === null);
1134
1237
  items.push({
1135
1238
  kind: 'node',
1136
- node: { type: 'link', href, image: bracket.image, children: finish(inner) },
1239
+ node: {
1240
+ type: 'link',
1241
+ href,
1242
+ image: bracket.image,
1243
+ children: finish(inner),
1244
+ // only when written, so a link without one keeps its old shape
1245
+ ...(title !== undefined ? { title } : null),
1246
+ },
1137
1247
  });
1138
1248
  }
1139
1249