@jxsuite/studio 0.6.2 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
33
- "@jxsuite/runtime": "^0.5.5",
33
+ "@jxsuite/runtime": "^0.7.0",
34
34
  "@spectrum-web-components/accordion": "^1.12.0",
35
35
  "@spectrum-web-components/action-bar": "1.12.0",
36
36
  "@spectrum-web-components/action-button": "^1.12.0",
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Insertion-helper.js — Single floating "+" button for element insertion on the canvas.
3
+ *
4
+ * Uses CSS Anchor Positioning to attach to sibling boundaries and empty containers. Uses Native
5
+ * Observables (Chrome 135+) for declarative event handling.
6
+ */
7
+
8
+ import { showSlashMenu } from "./slash-menu.js";
9
+
10
+ // ─── Types ────────────────────────────────────────────────────────────────────
11
+
12
+ /**
13
+ * @typedef {Object} ObservableSubscription
14
+ * @property {(obj: { next: Function }) => void} subscribe - Subscribes to the observable stream.
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} ObservableElement
19
+ * @property {(event: string, options?: Object) => ObservableSubscription} on - Creates an
20
+ * observable for the given event.
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} CanvasPanel
25
+ * @property {HTMLElement} canvas - The canvas content element.
26
+ * @property {HTMLElement & ObservableElement} overlayClk - The overlay click-capture layer.
27
+ * @property {HTMLElement} overlay - The overlay rendering layer.
28
+ * @property {HTMLElement} viewport - The viewport container (containing block for anchor
29
+ * positioning).
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} InsertionHelperContext
34
+ * @property {() => any} getState - Returns the current editor state.
35
+ * @property {(state: any) => void} update - Commits a new state.
36
+ * @property {() => string} getCanvasMode - Returns the active canvas mode.
37
+ * @property {(fn: Function) => any} withPanelPointerEvents - Executes fn with pointer-events
38
+ * temporarily enabled on the canvas.
39
+ * @property {() => number} effectiveZoom - Returns the current zoom scale factor.
40
+ * @property {(tag: string) => Object} defaultDef - Creates a default element definition for a tag.
41
+ * @property {(s: any, path: any[], idx: number, def: Object) => any} insertNode - Inserts a node
42
+ * into the document tree.
43
+ * @property {(s: any, path: any[]) => any} selectNode - Sets the selection to the given path.
44
+ * @property {(path: any[]) => any[] | null} parentElementPath - Returns the parent element path, or
45
+ * null for root.
46
+ * @property {(path: any[]) => string | number} childIndex - Returns the child index within the
47
+ * parent.
48
+ * @property {(doc: any, path: any[]) => any} getNodeAtPath - Retrieves the node at a document path.
49
+ * @property {WeakMap<any, any[]>} elToPath - Maps rendered DOM elements to their document paths.
50
+ * @property {CanvasPanel} panel - The active canvas panel.
51
+ */
52
+
53
+ // ─── State ───────────────────────────────────────────────────────────────────
54
+
55
+ /** @type {InsertionHelperContext | null} */
56
+ let _ctx = null;
57
+
58
+ /** @type {HTMLElement | null} */
59
+ let _helper = null;
60
+
61
+ /** @type {HTMLElement | null} */
62
+ let _currentAnchor = null;
63
+
64
+ /** @type {{ edge: string; path: any[]; parentPath: any[]; idx: number } | null} */
65
+ let _insertionPoint = null;
66
+
67
+ /** @type {AbortController | null} */
68
+ let _abort = null;
69
+
70
+ /** @type {ReturnType<typeof setTimeout> | null} */
71
+ let _hideTimer = null;
72
+
73
+ // Edge detection threshold in pixels
74
+ const EDGE_THRESHOLD = 14;
75
+
76
+ // Delay before hiding to allow cursor to reach the button
77
+ const HIDE_DELAY = 300;
78
+
79
+ // ─── Public API ──────────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Mount the insertion helper system.
83
+ *
84
+ * @param {InsertionHelperContext} ctx
85
+ */
86
+ export function mount(ctx) {
87
+ _ctx = ctx;
88
+ const { panel } = ctx;
89
+
90
+ _helper = document.createElement("button");
91
+ _helper.className = "insertion-helper";
92
+ _helper.textContent = "+";
93
+ _helper.addEventListener("click", onHelperClick);
94
+ _helper.addEventListener("mouseenter", () => {
95
+ cancelHide();
96
+ });
97
+ _helper.addEventListener("mouseleave", () => {
98
+ scheduleHide();
99
+ });
100
+ panel.viewport.appendChild(_helper);
101
+
102
+ _abort = new AbortController();
103
+
104
+ // Listen on viewport — overlayClk gets pointer-events:none during editing/selection
105
+ const viewport = /** @type {HTMLElement & ObservableElement} */ (panel.viewport);
106
+ if (typeof viewport.on === "function") {
107
+ viewport.on("mousemove", { signal: _abort.signal }).subscribe({ next: onMouseMove });
108
+ viewport.on("mouseleave", { signal: _abort.signal }).subscribe({ next: hide });
109
+ } else {
110
+ panel.viewport.addEventListener("mousemove", onMouseMove, { signal: _abort.signal });
111
+ panel.viewport.addEventListener("mouseleave", hide, { signal: _abort.signal });
112
+ }
113
+ }
114
+
115
+ export function unmount() {
116
+ _abort?.abort();
117
+ _abort = null;
118
+ cancelHide();
119
+ if (_helper?.parentElement) _helper.remove();
120
+ clearAnchor();
121
+ _helper = null;
122
+ _ctx = null;
123
+ _insertionPoint = null;
124
+ }
125
+
126
+ // ─── Detection ───────────────────────────────────────────────────────────────
127
+
128
+ /** @param {MouseEvent} e */
129
+ function onMouseMove(e) {
130
+ if (!_ctx || !_helper) return;
131
+
132
+ const { getCanvasMode } = _ctx;
133
+ if (getCanvasMode() !== "design") {
134
+ hide();
135
+ return;
136
+ }
137
+
138
+ const { panel, withPanelPointerEvents, elToPath } = _ctx;
139
+ const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
140
+
141
+ if (!el || !panel.canvas.contains(el)) {
142
+ hide();
143
+ return;
144
+ }
145
+
146
+ const path = elToPath.get(el);
147
+ if (!path) {
148
+ hide();
149
+ return;
150
+ }
151
+
152
+ // Empty container: show centered "+"
153
+ if (el.classList.contains("empty-container-placeholder")) {
154
+ showAt(el, "center", path, path, 0);
155
+ return;
156
+ }
157
+
158
+ // Root element — can't insert siblings above/below root
159
+ if (path.length === 0) {
160
+ hide();
161
+ return;
162
+ }
163
+
164
+ // Determine layout direction of parent container
165
+ const parent = el.parentElement;
166
+ if (!parent) {
167
+ hide();
168
+ return;
169
+ }
170
+
171
+ const parentStyle = getComputedStyle(parent);
172
+ const display = parentStyle.display;
173
+ const isFlex = display === "flex" || display === "inline-flex";
174
+ const isGrid = display === "grid" || display === "inline-grid";
175
+ const isRow =
176
+ (isFlex && parentStyle.flexDirection.startsWith("row")) ||
177
+ (isGrid && parentStyle.gridAutoFlow?.startsWith("column"));
178
+
179
+ // Calculate relative position within element
180
+ const rect = el.getBoundingClientRect();
181
+ const parentPath = _ctx.parentElementPath(path);
182
+ if (!parentPath) {
183
+ hide();
184
+ return;
185
+ }
186
+ const childIdx = /** @type {number} */ (_ctx.childIndex(path));
187
+
188
+ if (isRow) {
189
+ const relX = e.clientX - rect.left;
190
+ if (relX < EDGE_THRESHOLD) {
191
+ showAt(el, "left", path, parentPath, childIdx);
192
+ } else if (rect.width - relX < EDGE_THRESHOLD) {
193
+ showAt(el, "right", path, parentPath, childIdx + 1);
194
+ } else {
195
+ hide();
196
+ }
197
+ } else {
198
+ const relY = e.clientY - rect.top;
199
+ if (relY < EDGE_THRESHOLD) {
200
+ showAt(el, "top", path, parentPath, childIdx);
201
+ } else if (rect.height - relY < EDGE_THRESHOLD) {
202
+ showAt(el, "bottom", path, parentPath, childIdx + 1);
203
+ } else {
204
+ hide();
205
+ }
206
+ }
207
+ }
208
+
209
+ // ─── Show / Hide ─────────────────────────────────────────────────────────────
210
+
211
+ /**
212
+ * @param {HTMLElement} el
213
+ * @param {string} edge
214
+ * @param {any[]} path
215
+ * @param {any[]} parentPath
216
+ * @param {number} idx
217
+ */
218
+ function showAt(el, edge, path, parentPath, idx) {
219
+ if (!_helper) return;
220
+
221
+ // Set CSS anchor on target element
222
+ if (_currentAnchor !== el) {
223
+ clearAnchor();
224
+ el.style.anchorName = "--jx-insert";
225
+ _currentAnchor = el;
226
+ }
227
+
228
+ _helper.dataset.edge = edge;
229
+ _helper.classList.add("visible");
230
+ _insertionPoint = { edge, path, parentPath, idx };
231
+ cancelHide();
232
+ }
233
+
234
+ function scheduleHide() {
235
+ cancelHide();
236
+ _hideTimer = setTimeout(hideNow, HIDE_DELAY);
237
+ }
238
+
239
+ function cancelHide() {
240
+ if (_hideTimer !== null) {
241
+ clearTimeout(_hideTimer);
242
+ _hideTimer = null;
243
+ }
244
+ }
245
+
246
+ function hide() {
247
+ scheduleHide();
248
+ }
249
+
250
+ function hideNow() {
251
+ _hideTimer = null;
252
+ if (!_helper) return;
253
+ _helper.classList.remove("visible");
254
+ clearAnchor();
255
+ _insertionPoint = null;
256
+ }
257
+
258
+ function clearAnchor() {
259
+ if (_currentAnchor) {
260
+ _currentAnchor.style.anchorName = "";
261
+ _currentAnchor = null;
262
+ }
263
+ }
264
+
265
+ // ─── Insertion ───────────────────────────────────────────────────────────────
266
+
267
+ function onHelperClick(/** @type {MouseEvent} */ e) {
268
+ e.stopPropagation();
269
+ e.preventDefault();
270
+
271
+ if (!_ctx || !_helper || !_insertionPoint) return;
272
+
273
+ const captured = _insertionPoint;
274
+ showSlashMenu(_helper, "", {
275
+ showFilter: true,
276
+ onSelect: (cmd) => onSlashSelect(cmd, captured),
277
+ });
278
+ }
279
+
280
+ /**
281
+ * @param {any} cmd
282
+ * @param {{ edge: string; path: any[]; parentPath: any[]; idx: number }} point
283
+ */
284
+ function onSlashSelect(cmd, point) {
285
+ if (!_ctx) return;
286
+
287
+ const { getState, update, defaultDef, insertNode, selectNode } = _ctx;
288
+ const S = getState();
289
+ const { parentPath, idx, edge } = point;
290
+
291
+ const newDef = defaultDef(cmd.tag);
292
+ const insertPath = edge === "center" ? point.path : parentPath;
293
+ const insertIdx = edge === "center" ? 0 : idx;
294
+
295
+ let s = insertNode(S, insertPath, insertIdx, newDef);
296
+ const newPath = [...insertPath, "children", insertIdx];
297
+ s = selectNode(s, newPath);
298
+ update(s);
299
+
300
+ hide();
301
+ }
@@ -34,12 +34,16 @@ const SLASH_COMMANDS = [
34
34
  const host = document.createElement("div");
35
35
  host.style.display = "contents";
36
36
 
37
- /** @type {{ onSelect: (cmd: any) => void } | null} */
37
+ /** @type {{ onSelect: (cmd: any) => void; showFilter?: boolean } | null} */
38
38
  let callbacks = null;
39
39
  let activeIdx = 0;
40
40
  /** @type {any[]} */
41
41
  let filteredItems = [];
42
42
  let open = false;
43
+ /** @type {HTMLElement | null} */
44
+ let _anchorEl = null;
45
+ /** @type {DOMRect | null} */
46
+ let _anchorRect = null;
43
47
 
44
48
  // ─── Public API ───────────────────────────────────────────────────────────────
45
49
 
@@ -53,7 +57,7 @@ export function isSlashMenuOpen() {
53
57
  *
54
58
  * @param {HTMLElement} anchorEl — the element being edited (for positioning)
55
59
  * @param {string} filter — current typed filter text (after the "/")
56
- * @param {{ onSelect: (cmd: any) => void }} cbs
60
+ * @param {{ onSelect: (cmd: any) => void; showFilter?: boolean }} cbs
57
61
  */
58
62
  export function showSlashMenu(anchorEl, filter, cbs) {
59
63
  // Lazily attach host to sp-theme
@@ -62,6 +66,8 @@ export function showSlashMenu(anchorEl, filter, cbs) {
62
66
  }
63
67
 
64
68
  callbacks = cbs;
69
+ _anchorEl = anchorEl;
70
+ _anchorRect = anchorEl.getBoundingClientRect();
65
71
 
66
72
  filteredItems = filter
67
73
  ? SLASH_COMMANDS.filter(
@@ -69,49 +75,28 @@ export function showSlashMenu(anchorEl, filter, cbs) {
69
75
  )
70
76
  : SLASH_COMMANDS;
71
77
 
72
- if (!filteredItems.length) {
78
+ if (!filteredItems.length && !cbs.showFilter) {
73
79
  dismissSlashMenu();
74
80
  return;
75
81
  }
76
82
 
77
83
  activeIdx = 0;
78
84
 
79
- const rect = anchorEl.getBoundingClientRect();
80
-
81
- litRender(
82
- html`
83
- <sp-popover
84
- open
85
- style="position:fixed;left:${rect.left}px;top:${rect.bottom +
86
- 4}px;z-index:9999;max-height:280px;overflow-y:auto"
87
- >
88
- <sp-menu style="min-width:220px">
89
- ${filteredItems.map(
90
- (cmd, i) => html`
91
- <sp-menu-item
92
- ?focused=${i === 0}
93
- @click=${(/** @type {Event} */ e) => {
94
- e.preventDefault();
95
- e.stopPropagation();
96
- select(cmd);
97
- }}
98
- >
99
- ${cmd.label}
100
- ${cmd.description
101
- ? html`<span slot="description">${cmd.description}</span>`
102
- : nothing}
103
- </sp-menu-item>
104
- `,
105
- )}
106
- </sp-menu>
107
- </sp-popover>
108
- `,
109
- host,
110
- );
85
+ render(anchorEl, cbs.showFilter || false);
111
86
 
112
87
  if (!open) {
113
88
  open = true;
114
89
  document.addEventListener("keydown", onKeydown, true); // capture phase
90
+ requestAnimationFrame(() => {
91
+ document.addEventListener("mousedown", onOutsideClick, true);
92
+ });
93
+ }
94
+
95
+ if (cbs.showFilter) {
96
+ requestAnimationFrame(() => {
97
+ const input = /** @type {HTMLInputElement | null} */ (host.querySelector(".slash-filter"));
98
+ if (input) input.focus();
99
+ });
115
100
  }
116
101
  }
117
102
 
@@ -119,13 +104,75 @@ export function dismissSlashMenu() {
119
104
  if (!open) return;
120
105
  open = false;
121
106
  callbacks = null;
107
+ _anchorEl = null;
108
+ _anchorRect = null;
122
109
  filteredItems = [];
123
110
  document.removeEventListener("keydown", onKeydown, true);
111
+ document.removeEventListener("mousedown", onOutsideClick, true);
124
112
  litRender(nothing, host);
125
113
  }
126
114
 
127
115
  // ─── Internal ─────────────────────────────────────────────────────────────────
128
116
 
117
+ /**
118
+ * @param {HTMLElement} anchorEl
119
+ * @param {boolean} showFilter
120
+ */
121
+ function render(anchorEl, showFilter) {
122
+ const rect = _anchorRect || anchorEl.getBoundingClientRect();
123
+
124
+ litRender(
125
+ html`
126
+ <sp-popover
127
+ open
128
+ style="position:fixed;left:${rect.left}px;top:${rect.bottom +
129
+ 4}px;z-index:9999;max-height:320px;overflow-y:auto"
130
+ >
131
+ ${showFilter
132
+ ? html`<input
133
+ class="slash-filter"
134
+ type="text"
135
+ placeholder="Filter…"
136
+ autocomplete="off"
137
+ style="display:block;width:100%;box-sizing:border-box;padding:6px 10px;border:none;border-bottom:1px solid var(--border, #444);outline:none;font-size:13px;background:transparent;color:inherit"
138
+ @input=${onFilterInput}
139
+ />`
140
+ : nothing}
141
+ <sp-menu style="min-width:220px">
142
+ ${filteredItems.length
143
+ ? filteredItems.map(
144
+ (cmd, i) => html`
145
+ <sp-menu-item
146
+ ?focused=${i === 0}
147
+ @click=${(/** @type {Event} */ e) => {
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ select(cmd);
151
+ }}
152
+ >
153
+ ${cmd.label}
154
+ ${cmd.description
155
+ ? html`<span slot="description">${cmd.description}</span>`
156
+ : nothing}
157
+ </sp-menu-item>
158
+ `,
159
+ )
160
+ : html`<sp-menu-item disabled>No matches</sp-menu-item>`}
161
+ </sp-menu>
162
+ </sp-popover>
163
+ `,
164
+ host,
165
+ );
166
+ }
167
+
168
+ /** @param {MouseEvent} e */
169
+ function onOutsideClick(e) {
170
+ const popover = host.querySelector("sp-popover");
171
+ if (popover && !popover.contains(/** @type {Node} */ (e.target))) {
172
+ dismissSlashMenu();
173
+ }
174
+ }
175
+
129
176
  /** @param {any} cmd */
130
177
  function select(cmd) {
131
178
  const cbs = callbacks;
@@ -133,16 +180,42 @@ function select(cmd) {
133
180
  cbs?.onSelect(cmd);
134
181
  }
135
182
 
183
+ /** @param {Event} e */
184
+ function onFilterInput(e) {
185
+ const input = /** @type {HTMLInputElement} */ (e.target);
186
+ const filter = input.value.toLowerCase();
187
+
188
+ filteredItems = filter
189
+ ? SLASH_COMMANDS.filter(
190
+ (c) => c.label.toLowerCase().includes(filter) || c.tag.toLowerCase().includes(filter),
191
+ )
192
+ : SLASH_COMMANDS;
193
+
194
+ activeIdx = 0;
195
+ if (_anchorEl) render(_anchorEl, true);
196
+
197
+ // Re-focus input after re-render
198
+ requestAnimationFrame(() => {
199
+ const el = /** @type {HTMLInputElement | null} */ (host.querySelector(".slash-filter"));
200
+ if (el && el !== document.activeElement) {
201
+ el.focus();
202
+ el.selectionStart = el.selectionEnd = el.value.length;
203
+ }
204
+ });
205
+ }
206
+
136
207
  /** @param {KeyboardEvent} e */
137
208
  function onKeydown(e) {
138
209
  if (!open) return;
139
210
 
140
- const items = /** @type {NodeListOf<Element>} */ (host.querySelectorAll("sp-menu-item"));
141
- if (!items.length) return;
211
+ const items = /** @type {NodeListOf<Element>} */ (
212
+ host.querySelectorAll("sp-menu-item:not([disabled])")
213
+ );
142
214
 
143
215
  if (e.key === "ArrowDown") {
144
216
  e.preventDefault();
145
217
  e.stopPropagation();
218
+ if (!items.length) return;
146
219
  items[activeIdx]?.removeAttribute("focused");
147
220
  activeIdx = (activeIdx + 1) % items.length;
148
221
  items[activeIdx]?.setAttribute("focused", "");
@@ -150,6 +223,7 @@ function onKeydown(e) {
150
223
  } else if (e.key === "ArrowUp") {
151
224
  e.preventDefault();
152
225
  e.stopPropagation();
226
+ if (!items.length) return;
153
227
  items[activeIdx]?.removeAttribute("focused");
154
228
  activeIdx = (activeIdx - 1 + items.length) % items.length;
155
229
  items[activeIdx]?.setAttribute("focused", "");
@@ -14,6 +14,7 @@ import { unified } from "unified";
14
14
  import remarkStringify from "remark-stringify";
15
15
  import remarkDirective from "remark-directive";
16
16
  import { MD_ALL } from "./md-allowlist.js";
17
+ import { htmlToJx } from "@jxsuite/parser/transpile";
17
18
 
18
19
  // ─── mdast → Jx ──────────────────────────────────────────────────────────
19
20
 
@@ -40,7 +41,6 @@ const MDAST_TAG_MAP = {
40
41
  table: () => "table",
41
42
  tableRow: () => "tr",
42
43
  tableCell: (/** @type {any} */ n) => (n.isHeader ? "th" : "td"),
43
- html: () => "div",
44
44
  break: () => "br",
45
45
  };
46
46
 
@@ -55,7 +55,7 @@ export function mdToJx(mdast) {
55
55
  return {
56
56
  children: (mdast.children ?? [])
57
57
  .filter((/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml")
58
- .map(convertMdastNode)
58
+ .flatMap(convertMdastNode)
59
59
  .filter(Boolean),
60
60
  };
61
61
  }
@@ -78,6 +78,12 @@ function convertMdastNode(node) {
78
78
  return convertDirective(node);
79
79
  }
80
80
 
81
+ if (node.type === "html") {
82
+ if (!node.value) return null;
83
+ const nodes = htmlToJx(node.value);
84
+ return nodes.length === 1 ? nodes[0] : { tagName: "div", children: nodes };
85
+ }
86
+
81
87
  const tagFn = MDAST_TAG_MAP[node.type];
82
88
  if (!tagFn) return null;
83
89
 
@@ -92,7 +98,7 @@ function convertMdastNode(node) {
92
98
  if (node.children?.length === 1 && node.children[0].type === "text") {
93
99
  el.textContent = node.children[0].value;
94
100
  } else if (node.children?.length > 0) {
95
- el.children = node.children.map(convertMdastNode).filter(Boolean);
101
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
96
102
  }
97
103
  break;
98
104
  }
@@ -107,7 +113,7 @@ function convertMdastNode(node) {
107
113
  if (node.children?.length === 1 && node.children[0].type === "text") {
108
114
  el.textContent = node.children[0].value;
109
115
  } else if (node.children?.length > 0) {
110
- el.children = node.children.map(convertMdastNode).filter(Boolean);
116
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
111
117
  }
112
118
  break;
113
119
  }
@@ -122,7 +128,7 @@ function convertMdastNode(node) {
122
128
  if (node.children?.length === 1 && node.children[0].type === "text") {
123
129
  el.textContent = node.children[0].value;
124
130
  } else if (node.children?.length > 0) {
125
- el.children = node.children.map(convertMdastNode).filter(Boolean);
131
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
126
132
  }
127
133
  break;
128
134
 
@@ -134,13 +140,13 @@ function convertMdastNode(node) {
134
140
  case "blockquote":
135
141
  case "listItem":
136
142
  if (node.children?.length > 0) {
137
- el.children = node.children.map(convertMdastNode).filter(Boolean);
143
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
138
144
  }
139
145
  break;
140
146
 
141
147
  case "list":
142
148
  if (node.children?.length > 0) {
143
- el.children = node.children.map(convertMdastNode).filter(Boolean);
149
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
144
150
  }
145
151
  if (node.start != null && node.start !== 1) {
146
152
  el.attributes = { start: String(node.start) };
@@ -165,7 +171,7 @@ function convertMdastNode(node) {
165
171
 
166
172
  case "table": {
167
173
  // Mdast tables have rows directly; split into thead/tbody
168
- const rows = (node.children ?? []).map(convertMdastNode).filter(Boolean);
174
+ const rows = (node.children ?? []).flatMap(convertMdastNode).filter(Boolean);
169
175
  const thead = rows.length > 0 ? { tagName: "thead", children: [rows[0]] } : null;
170
176
  const tbody = rows.length > 1 ? { tagName: "tbody", children: rows.slice(1) } : null;
171
177
  el.children = [thead, tbody].filter(Boolean);
@@ -174,7 +180,7 @@ function convertMdastNode(node) {
174
180
 
175
181
  case "tableRow":
176
182
  if (node.children?.length > 0) {
177
- el.children = node.children.map(convertMdastNode).filter(Boolean);
183
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
178
184
  }
179
185
  break;
180
186
 
@@ -182,13 +188,9 @@ function convertMdastNode(node) {
182
188
  if (node.children?.length === 1 && node.children[0].type === "text") {
183
189
  el.textContent = node.children[0].value;
184
190
  } else if (node.children?.length > 0) {
185
- el.children = node.children.map(convertMdastNode).filter(Boolean);
191
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
186
192
  }
187
193
  break;
188
-
189
- case "html":
190
- el.innerHTML = node.value;
191
- break;
192
194
  }
193
195
 
194
196
  return el;
@@ -209,10 +211,10 @@ function convertDirective(node) {
209
211
  if (node.children?.length === 1 && node.children[0].type === "text") {
210
212
  el.textContent = node.children[0].value;
211
213
  } else if (node.children?.length > 0) {
212
- el.children = node.children.map(convertMdastNode).filter(Boolean);
214
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
213
215
  }
214
216
  } else if (node.type === "containerDirective" && node.children?.length > 0) {
215
- el.children = node.children.map(convertMdastNode).filter(Boolean);
217
+ el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
216
218
  }
217
219
  return el;
218
220
  }