@jxsuite/studio 0.27.0 → 0.28.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/dist/studio.js +33900 -56705
- package/dist/studio.js.map +128 -396
- package/package.json +4 -10
- package/src/browse/browse.ts +62 -22
- package/src/canvas/canvas-live-render.ts +57 -55
- package/src/canvas/canvas-render.ts +50 -21
- package/src/editor/context-menu.ts +4 -1
- package/src/editor/convert-to-component.ts +8 -2
- package/src/editor/inline-edit.ts +7 -2
- package/src/editor/insertion-helper.ts +12 -3
- package/src/editor/slash-menu.ts +5 -1
- package/src/files/file-ops.ts +121 -102
- package/src/files/files.ts +53 -35
- package/src/format/constraints.ts +55 -0
- package/src/format/format-host.ts +212 -0
- package/src/new-project/new-project-modal.ts +18 -3
- package/src/panels/ai-panel.ts +3 -1
- package/src/panels/block-action-bar.ts +4 -2
- package/src/panels/canvas-dnd.ts +37 -8
- package/src/panels/data-explorer.ts +9 -2
- package/src/panels/editors.ts +1 -1
- package/src/panels/git-panel.ts +18 -8
- package/src/panels/head-panel.ts +12 -5
- package/src/panels/left-panel.ts +7 -4
- package/src/panels/panel-events.ts +3 -1
- package/src/panels/properties-panel.ts +9 -3
- package/src/panels/quick-search.ts +10 -10
- package/src/panels/right-panel.ts +6 -2
- package/src/panels/shared.ts +6 -1
- package/src/panels/signals-panel.ts +35 -10
- package/src/panels/style-inputs.ts +5 -1
- package/src/panels/style-panel.ts +6 -2
- package/src/panels/stylebook-layers-panel.ts +12 -3
- package/src/panels/stylebook-panel.ts +24 -8
- package/src/panels/toolbar.ts +16 -3
- package/src/platforms/devserver.ts +32 -3
- package/src/resize-edges.ts +12 -2
- package/src/services/cem-export.ts +9 -3
- package/src/settings/content-types-editor.ts +7 -2
- package/src/settings/defs-editor.ts +6 -1
- package/src/settings/general-settings.ts +3 -1
- package/src/settings/schema-field-ui.ts +11 -3
- package/src/settings/settings-modal.ts +4 -1
- package/src/state.ts +5 -1
- package/src/studio.ts +27 -16
- package/src/tabs/tab.ts +29 -6
- package/src/tabs/transact.ts +4 -1
- package/src/types.ts +15 -6
- package/src/ui/button-group.ts +6 -1
- package/src/ui/expression-editor.ts +14 -3
- package/src/ui/layers.ts +5 -1
- package/src/ui/unit-selector.ts +6 -1
- package/src/utils/canvas-media.ts +1 -1
- package/src/utils/google-fonts.ts +4 -1
- package/src/utils/studio-utils.ts +7 -1
- package/src/view.ts +8 -2
- package/src/markdown/md-allowlist.ts +0 -104
- package/src/markdown/md-convert.ts +0 -846
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
/**
|
|
3
|
-
* Md-convert.js — Bidirectional mdast ↔ Jx conversion
|
|
4
|
-
*
|
|
5
|
-
* MdToJx(mdast) → Jx element tree (for loading into the canvas) jxToMd(jx) → mdast (for saving back
|
|
6
|
-
* to markdown)
|
|
7
|
-
*
|
|
8
|
-
* JxDocToMd(doc) → Jx Markdown string (for saving Jx component documents back to .md)
|
|
9
|
-
*
|
|
10
|
-
* Both are pure tree transformations. The remark ecosystem handles all actual parsing and
|
|
11
|
-
* serialization.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { unified } from "unified";
|
|
15
|
-
import remarkStringify from "remark-stringify";
|
|
16
|
-
import remarkDirective from "remark-directive";
|
|
17
|
-
import remarkGfm from "remark-gfm";
|
|
18
|
-
import { MD_ALL } from "./md-allowlist";
|
|
19
|
-
import { htmlToJx } from "@jxsuite/parser/transpile";
|
|
20
|
-
|
|
21
|
-
import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
|
|
22
|
-
import type { MdastNode } from "@jxsuite/parser/types";
|
|
23
|
-
|
|
24
|
-
// ─── mdast → Jx ──────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Mdast node-type → Jx tagName mapping
|
|
28
|
-
*
|
|
29
|
-
* @type {Record<string, (n: MdastNode) => string>}
|
|
30
|
-
*/
|
|
31
|
-
const MDAST_TAG_MAP: Record<string, (n: MdastNode) => string> = {
|
|
32
|
-
heading: (n: MdastNode) => `h${n.depth}`,
|
|
33
|
-
paragraph: () => "p",
|
|
34
|
-
text: () => "span",
|
|
35
|
-
emphasis: () => "em",
|
|
36
|
-
strong: () => "strong",
|
|
37
|
-
delete: () => "del",
|
|
38
|
-
inlineCode: () => "code",
|
|
39
|
-
link: () => "a",
|
|
40
|
-
image: () => "img",
|
|
41
|
-
blockquote: () => "blockquote",
|
|
42
|
-
list: (n: MdastNode) => (n.ordered ? "ol" : "ul"),
|
|
43
|
-
listItem: () => "li",
|
|
44
|
-
code: () => "pre",
|
|
45
|
-
thematicBreak: () => "hr",
|
|
46
|
-
table: () => "table",
|
|
47
|
-
tableRow: () => "tr",
|
|
48
|
-
tableCell: (n: MdastNode) => (n.isHeader ? "th" : "td"),
|
|
49
|
-
break: () => "br",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Convert an mdast tree to a Jx element tree.
|
|
54
|
-
*
|
|
55
|
-
* @param {MdastNode} mdast - Root mdast node (type: 'root')
|
|
56
|
-
* @returns {JxElement} Jx element tree
|
|
57
|
-
*/
|
|
58
|
-
export function mdToJx(mdast: MdastNode) {
|
|
59
|
-
if (mdast.type === "root") {
|
|
60
|
-
return {
|
|
61
|
-
children: (mdast.children ?? [])
|
|
62
|
-
.filter((n: MdastNode) => n.type !== "yaml" && n.type !== "toml")
|
|
63
|
-
.flatMap(convertMdastNode)
|
|
64
|
-
.filter(Boolean) as (JxElement | string)[],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
return convertMdastNode(mdast) as JxElement;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @param {MdastNode} node
|
|
72
|
-
* @returns {JxElement | null}
|
|
73
|
-
*/
|
|
74
|
-
function convertMdastNode(node: MdastNode) {
|
|
75
|
-
if (!node) return null;
|
|
76
|
-
|
|
77
|
-
// Directive nodes → custom elements
|
|
78
|
-
if (
|
|
79
|
-
node.type === "containerDirective" ||
|
|
80
|
-
node.type === "leafDirective" ||
|
|
81
|
-
node.type === "textDirective"
|
|
82
|
-
) {
|
|
83
|
-
return convertDirective(node);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (node.type === "html") {
|
|
87
|
-
if (!node.value) return null;
|
|
88
|
-
const nodes = htmlToJx(node.value);
|
|
89
|
-
return nodes.length === 1 ? (nodes[0] as JxElement) : { tagName: "div", children: nodes };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const tagFn = MDAST_TAG_MAP[node.type];
|
|
93
|
-
if (!tagFn) return null;
|
|
94
|
-
|
|
95
|
-
const tag = tagFn(node);
|
|
96
|
-
const el: JxElement = { tagName: tag };
|
|
97
|
-
|
|
98
|
-
switch (node.type) {
|
|
99
|
-
case "heading":
|
|
100
|
-
case "paragraph": {
|
|
101
|
-
// If contains only a single text child, flatten to textContent
|
|
102
|
-
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
103
|
-
el.textContent = node.children[0].value ?? null;
|
|
104
|
-
} else if (node.children?.length) {
|
|
105
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
106
|
-
| JxElement
|
|
107
|
-
| string
|
|
108
|
-
)[];
|
|
109
|
-
}
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
case "text":
|
|
114
|
-
el.textContent = node.value ?? null;
|
|
115
|
-
break;
|
|
116
|
-
|
|
117
|
-
case "emphasis":
|
|
118
|
-
case "strong":
|
|
119
|
-
case "delete": {
|
|
120
|
-
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
121
|
-
el.textContent = node.children[0].value ?? null;
|
|
122
|
-
} else if (node.children?.length) {
|
|
123
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
124
|
-
| JxElement
|
|
125
|
-
| string
|
|
126
|
-
)[];
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
case "inlineCode":
|
|
132
|
-
el.textContent = node.value ?? null;
|
|
133
|
-
break;
|
|
134
|
-
|
|
135
|
-
case "link":
|
|
136
|
-
el.attributes = { href: node.url ?? "" };
|
|
137
|
-
if (node.title) el.attributes.title = node.title;
|
|
138
|
-
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
139
|
-
el.textContent = node.children[0].value ?? null;
|
|
140
|
-
} else if (node.children?.length) {
|
|
141
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
142
|
-
| JxElement
|
|
143
|
-
| string
|
|
144
|
-
)[];
|
|
145
|
-
}
|
|
146
|
-
break;
|
|
147
|
-
|
|
148
|
-
case "image":
|
|
149
|
-
el.attributes = { src: node.url ?? "", alt: node.alt ?? "" };
|
|
150
|
-
if (node.title) el.attributes.title = node.title;
|
|
151
|
-
break;
|
|
152
|
-
|
|
153
|
-
case "blockquote":
|
|
154
|
-
case "listItem":
|
|
155
|
-
if (node.children?.length) {
|
|
156
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
157
|
-
| JxElement
|
|
158
|
-
| string
|
|
159
|
-
)[];
|
|
160
|
-
}
|
|
161
|
-
break;
|
|
162
|
-
|
|
163
|
-
case "list":
|
|
164
|
-
if (node.children?.length) {
|
|
165
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
166
|
-
| JxElement
|
|
167
|
-
| string
|
|
168
|
-
)[];
|
|
169
|
-
}
|
|
170
|
-
if (node.start != null && node.start !== 1) {
|
|
171
|
-
el.attributes = { start: String(node.start) };
|
|
172
|
-
}
|
|
173
|
-
break;
|
|
174
|
-
|
|
175
|
-
case "code":
|
|
176
|
-
// Fenced code → pre > code
|
|
177
|
-
el.children = [
|
|
178
|
-
{
|
|
179
|
-
tagName: "code",
|
|
180
|
-
textContent: node.value ?? null,
|
|
181
|
-
...(node.lang ? { attributes: { class: `language-${node.lang}` } } : {}),
|
|
182
|
-
},
|
|
183
|
-
];
|
|
184
|
-
break;
|
|
185
|
-
|
|
186
|
-
case "thematicBreak":
|
|
187
|
-
case "break":
|
|
188
|
-
// Void elements — no content
|
|
189
|
-
break;
|
|
190
|
-
|
|
191
|
-
case "table": {
|
|
192
|
-
// Mdast tables have rows directly; split into thead/tbody
|
|
193
|
-
const rows = (node.children ?? []).flatMap(convertMdastNode).filter(Boolean) as JxElement[];
|
|
194
|
-
const thead =
|
|
195
|
-
rows.length > 0
|
|
196
|
-
? { tagName: "thead", children: [rows[0]] as (JxElement | string)[] }
|
|
197
|
-
: null;
|
|
198
|
-
const tbody =
|
|
199
|
-
rows.length > 1
|
|
200
|
-
? { tagName: "tbody", children: rows.slice(1) as (JxElement | string)[] }
|
|
201
|
-
: null;
|
|
202
|
-
el.children = [thead, tbody].filter(Boolean) as (JxElement | string)[];
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
case "tableRow":
|
|
207
|
-
if (node.children?.length) {
|
|
208
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
209
|
-
| JxElement
|
|
210
|
-
| string
|
|
211
|
-
)[];
|
|
212
|
-
}
|
|
213
|
-
break;
|
|
214
|
-
|
|
215
|
-
case "tableCell":
|
|
216
|
-
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
217
|
-
el.textContent = node.children[0].value ?? null;
|
|
218
|
-
} else if (node.children?.length) {
|
|
219
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
220
|
-
| JxElement
|
|
221
|
-
| string
|
|
222
|
-
)[];
|
|
223
|
-
}
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return el;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* @param {MdastNode} node
|
|
232
|
-
* @returns {JxElement | null}
|
|
233
|
-
*/
|
|
234
|
-
function convertDirective(node: MdastNode) {
|
|
235
|
-
const el: JxElement = { tagName: node.name ?? "div" };
|
|
236
|
-
if (node.attributes && Object.keys(node.attributes).length > 0) {
|
|
237
|
-
el.attributes = { ...node.attributes };
|
|
238
|
-
}
|
|
239
|
-
if (node.type === "textDirective") {
|
|
240
|
-
// Text directives place label as textContent
|
|
241
|
-
if (node.children?.length === 1 && node.children[0].type === "text") {
|
|
242
|
-
el.textContent = node.children[0].value ?? null;
|
|
243
|
-
} else if (node.children?.length) {
|
|
244
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (
|
|
245
|
-
| JxElement
|
|
246
|
-
| string
|
|
247
|
-
)[];
|
|
248
|
-
}
|
|
249
|
-
} else if (node.type === "containerDirective" && node.children?.length) {
|
|
250
|
-
el.children = node.children.flatMap(convertMdastNode).filter(Boolean) as (JxElement | string)[];
|
|
251
|
-
}
|
|
252
|
-
return el;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ─── Jx → mdast ──────────────────────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Jx tagName → mdast node-type mapping (inverse of MDAST_TAG_MAP)
|
|
259
|
-
*
|
|
260
|
-
* @type {Record<string, string>}
|
|
261
|
-
*/
|
|
262
|
-
/** Tags whose content model is inline (phrasing content). */
|
|
263
|
-
const INLINE_CONTENT_TAGS = new Set([
|
|
264
|
-
"p",
|
|
265
|
-
"h1",
|
|
266
|
-
"h2",
|
|
267
|
-
"h3",
|
|
268
|
-
"h4",
|
|
269
|
-
"h5",
|
|
270
|
-
"h6",
|
|
271
|
-
"em",
|
|
272
|
-
"strong",
|
|
273
|
-
"del",
|
|
274
|
-
"a",
|
|
275
|
-
"td",
|
|
276
|
-
"th",
|
|
277
|
-
]);
|
|
278
|
-
|
|
279
|
-
const TAG_MDAST_MAP = {
|
|
280
|
-
h1: "heading",
|
|
281
|
-
h2: "heading",
|
|
282
|
-
h3: "heading",
|
|
283
|
-
h4: "heading",
|
|
284
|
-
h5: "heading",
|
|
285
|
-
h6: "heading",
|
|
286
|
-
p: "paragraph",
|
|
287
|
-
span: "text",
|
|
288
|
-
em: "emphasis",
|
|
289
|
-
strong: "strong",
|
|
290
|
-
del: "delete",
|
|
291
|
-
code: "inlineCode",
|
|
292
|
-
a: "link",
|
|
293
|
-
img: "image",
|
|
294
|
-
blockquote: "blockquote",
|
|
295
|
-
ul: "list",
|
|
296
|
-
ol: "list",
|
|
297
|
-
li: "listItem",
|
|
298
|
-
pre: "code",
|
|
299
|
-
hr: "thematicBreak",
|
|
300
|
-
table: "table",
|
|
301
|
-
tr: "tableRow",
|
|
302
|
-
th: "tableCell",
|
|
303
|
-
td: "tableCell",
|
|
304
|
-
br: "break",
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Convert a Jx element tree to an mdast tree.
|
|
309
|
-
*
|
|
310
|
-
* @param {JxElement} jx - Jx element tree (root content div)
|
|
311
|
-
* @returns {MdastNode} Mdast root node
|
|
312
|
-
*/
|
|
313
|
-
export function jxToMd(jx: JxElement) {
|
|
314
|
-
const childArray = Array.isArray(jx.children) ? jx.children : ([] as (JxElement | string)[]);
|
|
315
|
-
const children = childArray
|
|
316
|
-
.map((child: JxElement | string) => convertJxNode(child, true))
|
|
317
|
-
.filter(Boolean) as MdastNode[];
|
|
318
|
-
|
|
319
|
-
return { type: "root", children };
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Check if a Jx element has extra properties beyond the standard mdast-compatible ones. Elements
|
|
324
|
-
* with style, event handlers, state bindings, etc. need directive syntax.
|
|
325
|
-
*
|
|
326
|
-
* @param {JxElement} el
|
|
327
|
-
* @returns {boolean}
|
|
328
|
-
*/
|
|
329
|
-
function hasJxProps(el: JxElement) {
|
|
330
|
-
for (const key of Object.keys(el)) {
|
|
331
|
-
if (
|
|
332
|
-
key === "tagName" ||
|
|
333
|
-
key === "children" ||
|
|
334
|
-
key === "textContent" ||
|
|
335
|
-
key === "innerHTML" ||
|
|
336
|
-
key === "attributes"
|
|
337
|
-
)
|
|
338
|
-
continue;
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Convert a single Jx element to an mdast node.
|
|
346
|
-
*
|
|
347
|
-
* @param {JxElement | string | number} el - Jx element
|
|
348
|
-
* @param {boolean} isBlock - Whether this element is in a block context
|
|
349
|
-
* @returns {MdastNode | null} Mdast node
|
|
350
|
-
*/
|
|
351
|
-
function convertJxNode(el: JxElement | string | number, isBlock: boolean): MdastNode | null {
|
|
352
|
-
// Bare string/number text nodes → mdast text nodes
|
|
353
|
-
if (typeof el === "string" || typeof el === "number") {
|
|
354
|
-
return { type: "text", value: String(el) };
|
|
355
|
-
}
|
|
356
|
-
if (!el || typeof el !== "object") return null;
|
|
357
|
-
|
|
358
|
-
const tag = el.tagName ?? "div";
|
|
359
|
-
|
|
360
|
-
// If not in the markdown allowlist or has Jx-specific props, convert to directive
|
|
361
|
-
if (!MD_ALL.has(tag) || hasJxProps(el)) {
|
|
362
|
-
return convertToDirective(el, isBlock);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const mdastType = (TAG_MDAST_MAP as Record<string, string>)[tag];
|
|
366
|
-
if (!mdastType) return null;
|
|
367
|
-
|
|
368
|
-
switch (mdastType) {
|
|
369
|
-
case "heading":
|
|
370
|
-
return {
|
|
371
|
-
type: "heading",
|
|
372
|
-
depth: parseInt(tag.slice(1), 10),
|
|
373
|
-
children: inlineChildren(el),
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
case "paragraph":
|
|
377
|
-
return {
|
|
378
|
-
type: "paragraph",
|
|
379
|
-
children: inlineChildren(el),
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
case "text":
|
|
383
|
-
return { type: "text", value: el.textContent ?? "" };
|
|
384
|
-
|
|
385
|
-
case "emphasis":
|
|
386
|
-
case "strong":
|
|
387
|
-
case "delete":
|
|
388
|
-
return {
|
|
389
|
-
type: mdastType,
|
|
390
|
-
children: inlineChildren(el),
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
case "inlineCode":
|
|
394
|
-
return { type: "inlineCode", value: el.textContent ?? "" };
|
|
395
|
-
|
|
396
|
-
case "link":
|
|
397
|
-
return {
|
|
398
|
-
type: "link",
|
|
399
|
-
url: (el.attributes?.href as string) ?? "",
|
|
400
|
-
title: (el.attributes?.title as string | null) ?? null,
|
|
401
|
-
children: inlineChildren(el),
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
case "image":
|
|
405
|
-
return {
|
|
406
|
-
type: "image",
|
|
407
|
-
url: (el.attributes?.src as string) ?? "",
|
|
408
|
-
alt: (el.attributes?.alt as string) ?? "",
|
|
409
|
-
title: (el.attributes?.title as string | null) ?? null,
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
case "blockquote":
|
|
413
|
-
return {
|
|
414
|
-
type: "blockquote",
|
|
415
|
-
children: blockChildren(el),
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
case "list":
|
|
419
|
-
return {
|
|
420
|
-
type: "list",
|
|
421
|
-
ordered: tag === "ol",
|
|
422
|
-
start: tag === "ol" ? parseInt(el.attributes?.start as string, 10) || 1 : null,
|
|
423
|
-
spread: false,
|
|
424
|
-
children: ((el.children ?? []) as (JxElement | string)[])
|
|
425
|
-
.map((c: JxElement | string) => convertJxNode(c, true))
|
|
426
|
-
.filter(Boolean) as MdastNode[],
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
case "listItem":
|
|
430
|
-
return {
|
|
431
|
-
type: "listItem",
|
|
432
|
-
spread: false,
|
|
433
|
-
children: blockChildren(el),
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
case "code": {
|
|
437
|
-
// pre > code → fenced code block
|
|
438
|
-
const codeChild = Array.isArray(el.children)
|
|
439
|
-
? (el.children[0] as JxElement | undefined)
|
|
440
|
-
: undefined;
|
|
441
|
-
const langClass = (codeChild?.attributes?.class as string) ?? "";
|
|
442
|
-
const lang = langClass.replace("language-", "") || null;
|
|
443
|
-
return {
|
|
444
|
-
type: "code",
|
|
445
|
-
lang,
|
|
446
|
-
value: codeChild?.textContent ?? el.textContent ?? "",
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
case "thematicBreak":
|
|
451
|
-
return { type: "thematicBreak" };
|
|
452
|
-
|
|
453
|
-
case "break":
|
|
454
|
-
return { type: "break" };
|
|
455
|
-
|
|
456
|
-
case "table": {
|
|
457
|
-
// Flatten thead/tbody back to rows
|
|
458
|
-
const rows: MdastNode[] = [];
|
|
459
|
-
for (const section of (el.children ?? []) as (JxElement | string)[]) {
|
|
460
|
-
if (typeof section === "string") continue;
|
|
461
|
-
if (section.tagName === "thead" || section.tagName === "tbody") {
|
|
462
|
-
for (const row of (section.children ?? []) as (JxElement | string)[]) {
|
|
463
|
-
const mdRow = convertJxNode(row, true);
|
|
464
|
-
if (mdRow) {
|
|
465
|
-
// Mark header cells
|
|
466
|
-
if (section.tagName === "thead") {
|
|
467
|
-
for (const cell of mdRow.children ?? []) {
|
|
468
|
-
cell.isHeader = true;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
rows.push(mdRow);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return {
|
|
477
|
-
type: "table",
|
|
478
|
-
children: rows,
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
case "tableRow":
|
|
483
|
-
return {
|
|
484
|
-
type: "tableRow",
|
|
485
|
-
children: ((el.children ?? []) as (JxElement | string)[])
|
|
486
|
-
.map((c: JxElement | string) => convertJxNode(c, false))
|
|
487
|
-
.filter(Boolean) as MdastNode[],
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
case "tableCell":
|
|
491
|
-
return {
|
|
492
|
-
type: "tableCell",
|
|
493
|
-
children: inlineChildren(el),
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return null;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Get inline children from a Jx element as mdast nodes. Handles both textContent shorthand and
|
|
502
|
-
* explicit children array.
|
|
503
|
-
*
|
|
504
|
-
* @param {JxElement} el
|
|
505
|
-
* @returns {MdastNode[]}
|
|
506
|
-
*/
|
|
507
|
-
function inlineChildren(el: JxElement): MdastNode[] {
|
|
508
|
-
if (el.textContent != null) {
|
|
509
|
-
return [{ type: "text", value: String(el.textContent) }];
|
|
510
|
-
}
|
|
511
|
-
return ((el.children ?? []) as (JxElement | string)[])
|
|
512
|
-
.map((c: JxElement | string) => convertJxNode(c, false))
|
|
513
|
-
.filter(Boolean) as MdastNode[];
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Get block children from a Jx element as mdast nodes.
|
|
518
|
-
*
|
|
519
|
-
* @param {JxElement} el
|
|
520
|
-
* @returns {MdastNode[]}
|
|
521
|
-
*/
|
|
522
|
-
function blockChildren(el: JxElement): MdastNode[] {
|
|
523
|
-
if (el.textContent != null) {
|
|
524
|
-
// Wrap bare text in a paragraph
|
|
525
|
-
return [{ type: "paragraph", children: [{ type: "text", value: String(el.textContent) }] }];
|
|
526
|
-
}
|
|
527
|
-
return ((el.children ?? []) as (JxElement | string)[])
|
|
528
|
-
.map((c: JxElement | string) => convertJxNode(c, true))
|
|
529
|
-
.filter(Boolean) as MdastNode[];
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Collect all directive attributes from a Jx element. Merges Jx-specific properties (style, event
|
|
534
|
-
* handlers, etc.) and HTML attributes into a flat dot-path attribute map suitable for
|
|
535
|
-
* remark-directive.
|
|
536
|
-
*
|
|
537
|
-
* @param {JxElement} el
|
|
538
|
-
* @returns {Record<string, string>}
|
|
539
|
-
*/
|
|
540
|
-
function collectDirectiveAttrs(el: JxElement) {
|
|
541
|
-
const propsObj: Record<string, unknown> = {};
|
|
542
|
-
|
|
543
|
-
for (const [key, value] of Object.entries(el)) {
|
|
544
|
-
if (key === "tagName" || key === "textContent" || key === "innerHTML" || key === "attributes")
|
|
545
|
-
continue;
|
|
546
|
-
if (key === "children") {
|
|
547
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
548
|
-
propsObj.children = value;
|
|
549
|
-
}
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
propsObj[key] = value;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// Merge HTML attributes
|
|
556
|
-
if (el.attributes) {
|
|
557
|
-
for (const [key, value] of Object.entries(el.attributes as Record<string, unknown>)) {
|
|
558
|
-
propsObj[key] = value;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return collapsePropsToAttrMap(propsObj);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Convert a Jx element to a directive node, preserving all Jx-specific properties as collapsed
|
|
567
|
-
* dot-path directive attributes.
|
|
568
|
-
*
|
|
569
|
-
* @param {JxElement} el
|
|
570
|
-
* @param {boolean} isBlock
|
|
571
|
-
* @returns {MdastNode}
|
|
572
|
-
*/
|
|
573
|
-
function convertToDirective(el: JxElement, isBlock: boolean): MdastNode {
|
|
574
|
-
const tag = (el.tagName as string) ?? "div";
|
|
575
|
-
const attrs = collectDirectiveAttrs(el);
|
|
576
|
-
|
|
577
|
-
if (!isBlock) {
|
|
578
|
-
// Inline → textDirective
|
|
579
|
-
return {
|
|
580
|
-
type: "textDirective",
|
|
581
|
-
name: tag,
|
|
582
|
-
attributes: attrs,
|
|
583
|
-
children:
|
|
584
|
-
el.textContent != null
|
|
585
|
-
? [{ type: "text", value: String(el.textContent) }]
|
|
586
|
-
: (((el.children ?? []) as (JxElement | string)[])
|
|
587
|
-
.map((c: JxElement | string) => convertJxNode(c, false))
|
|
588
|
-
.filter(Boolean) as MdastNode[]),
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Block without children → leafDirective
|
|
593
|
-
const rawChildren = el.children;
|
|
594
|
-
const childrenIsObject =
|
|
595
|
-
rawChildren && typeof rawChildren === "object" && !Array.isArray(rawChildren);
|
|
596
|
-
const childArray = childrenIsObject
|
|
597
|
-
? undefined
|
|
598
|
-
: (rawChildren as (JxElement | string)[] | undefined);
|
|
599
|
-
if (!childArray?.length && el.textContent == null) {
|
|
600
|
-
return {
|
|
601
|
-
type: "leafDirective",
|
|
602
|
-
name: tag,
|
|
603
|
-
attributes: attrs,
|
|
604
|
-
children: [],
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Block with children → containerDirective
|
|
609
|
-
/** @type {MdastNode[]} */
|
|
610
|
-
let directiveChildren;
|
|
611
|
-
if (el.textContent != null) {
|
|
612
|
-
directiveChildren = [
|
|
613
|
-
{ type: "paragraph", children: [{ type: "text", value: String(el.textContent) }] },
|
|
614
|
-
];
|
|
615
|
-
} else if (INLINE_CONTENT_TAGS.has(tag)) {
|
|
616
|
-
// Tags with inline content model: wrap all children in a single paragraph
|
|
617
|
-
// so remark serializes them as one continuous inline flow
|
|
618
|
-
const inlineNodes = ((el.children ?? []) as (JxElement | string)[])
|
|
619
|
-
.map((c: JxElement | string) => convertJxNode(c, false))
|
|
620
|
-
.filter(Boolean) as MdastNode[];
|
|
621
|
-
directiveChildren =
|
|
622
|
-
inlineNodes.length > 0 ? [{ type: "paragraph", children: inlineNodes }] : [];
|
|
623
|
-
} else {
|
|
624
|
-
directiveChildren = ((el.children ?? []) as (JxElement | string)[])
|
|
625
|
-
.map((c: JxElement | string) => convertJxNode(c, true))
|
|
626
|
-
.filter(Boolean) as MdastNode[];
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
return {
|
|
630
|
-
type: "containerDirective",
|
|
631
|
-
name: tag,
|
|
632
|
-
attributes: attrs,
|
|
633
|
-
children: directiveChildren,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// ─── Jx Document → Jx Markdown ─────────────────────────────────────────────
|
|
638
|
-
|
|
639
|
-
/** CSS pseudo-class names that need `:` stripped for markdown attributes. */
|
|
640
|
-
const CSS_PSEUDO_NAMES = new Set([
|
|
641
|
-
"hover",
|
|
642
|
-
"focus",
|
|
643
|
-
"active",
|
|
644
|
-
"visited",
|
|
645
|
-
"disabled",
|
|
646
|
-
"checked",
|
|
647
|
-
"valid",
|
|
648
|
-
"invalid",
|
|
649
|
-
"required",
|
|
650
|
-
"empty",
|
|
651
|
-
"first-child",
|
|
652
|
-
"last-child",
|
|
653
|
-
"focus-within",
|
|
654
|
-
"focus-visible",
|
|
655
|
-
"placeholder",
|
|
656
|
-
"selection",
|
|
657
|
-
"before",
|
|
658
|
-
"after",
|
|
659
|
-
]);
|
|
660
|
-
|
|
661
|
-
/** Jx `$`-prefixed keys that become unprefixed in directive attributes. */
|
|
662
|
-
const JX_DOLLAR_KEYS = new Set([
|
|
663
|
-
"$prototype",
|
|
664
|
-
"$ref",
|
|
665
|
-
"$component",
|
|
666
|
-
"$props",
|
|
667
|
-
"$switch",
|
|
668
|
-
"$elements",
|
|
669
|
-
]);
|
|
670
|
-
|
|
671
|
-
const JX_ANNOTATION_KEYS = new Set(["$title", "$description"]);
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Convert a Jx JSON document back to Jx Markdown source string.
|
|
675
|
-
*
|
|
676
|
-
* Inverse of `transpileJxMarkdown()` from @jxsuite/parser/transpile. Emits YAML frontmatter from
|
|
677
|
-
* top-level props and uses remark-stringify with remark-directive for the body — standard markdown
|
|
678
|
-
* elements emit as native syntax, Jx-decorated elements emit as directives.
|
|
679
|
-
*
|
|
680
|
-
* @param {JxMutableNode} doc - Jx JSON document
|
|
681
|
-
* @returns {string} Jx Markdown source
|
|
682
|
-
*/
|
|
683
|
-
export function jxDocToMd(doc: JxMutableNode) {
|
|
684
|
-
const { stringify: stringifyYaml } = yamlImport();
|
|
685
|
-
|
|
686
|
-
const lines: string[] = [];
|
|
687
|
-
|
|
688
|
-
// Emit YAML frontmatter
|
|
689
|
-
const frontmatter: Record<string, unknown> = {};
|
|
690
|
-
for (const [key, value] of Object.entries(doc)) {
|
|
691
|
-
if (key === "children") continue;
|
|
692
|
-
frontmatter[key] = value;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (Object.keys(frontmatter).length > 0) {
|
|
696
|
-
lines.push("---");
|
|
697
|
-
lines.push(stringifyYaml(frontmatter).trim());
|
|
698
|
-
lines.push("---");
|
|
699
|
-
lines.push("");
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Convert children to mdast and stringify with remark
|
|
703
|
-
if (Array.isArray(doc.children) && doc.children.length > 0) {
|
|
704
|
-
const mdastChildren = doc.children
|
|
705
|
-
.map((child: JxMutableNode | string) => convertJxNode(child as JxElement | string, true))
|
|
706
|
-
.filter(Boolean) as MdastNode[];
|
|
707
|
-
|
|
708
|
-
const mdast = { type: "root", children: mdastChildren };
|
|
709
|
-
const md = unified()
|
|
710
|
-
.use(remarkGfm)
|
|
711
|
-
.use(remarkDirective)
|
|
712
|
-
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
713
|
-
.stringify(mdast as unknown as import("mdast").Root);
|
|
714
|
-
|
|
715
|
-
lines.push(/** @type {string} */ (md));
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
return (
|
|
719
|
-
lines
|
|
720
|
-
.join("\n")
|
|
721
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
722
|
-
.trim() + "\n"
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Lazy import of yaml stringify — avoids importing at module load.
|
|
728
|
-
*
|
|
729
|
-
* @returns {{ stringify: (v: unknown) => string }}
|
|
730
|
-
*/
|
|
731
|
-
let _yaml: { stringify: (v: unknown) => string } | null = null;
|
|
732
|
-
function yamlImport() {
|
|
733
|
-
if (!_yaml) {
|
|
734
|
-
// Dynamic require avoided; use the yaml package already available in studio
|
|
735
|
-
_yaml = { stringify: yamlStringifySimple };
|
|
736
|
-
}
|
|
737
|
-
return _yaml;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Simple YAML stringifier for frontmatter. Handles the subset of YAML needed for Jx frontmatter
|
|
742
|
-
* (scalars, arrays, nested objects).
|
|
743
|
-
*
|
|
744
|
-
* @param {unknown} value
|
|
745
|
-
* @param {number} indent
|
|
746
|
-
* @returns {string}
|
|
747
|
-
*/
|
|
748
|
-
function yamlStringifySimple(value: unknown, indent: number = 0): string {
|
|
749
|
-
if (value === null || value === undefined) return "null";
|
|
750
|
-
if (typeof value === "boolean") return String(value);
|
|
751
|
-
if (typeof value === "number") return String(value);
|
|
752
|
-
if (typeof value === "string") {
|
|
753
|
-
// Quote if it contains special characters
|
|
754
|
-
if (/[:#[\]{}&*!|>'"%@`\n]/.test(value) || value === "" || value.trim() !== value) {
|
|
755
|
-
return JSON.stringify(value);
|
|
756
|
-
}
|
|
757
|
-
return value;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const prefix = " ".repeat(indent);
|
|
761
|
-
|
|
762
|
-
if (Array.isArray(value)) {
|
|
763
|
-
if (value.length === 0) return "[]";
|
|
764
|
-
return value
|
|
765
|
-
.map((item) => {
|
|
766
|
-
const itemStr: string = yamlStringifySimple(item, indent + 1);
|
|
767
|
-
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
768
|
-
// Object items: first key on same line as -, rest indented
|
|
769
|
-
const objLines: string[] = itemStr.split("\n");
|
|
770
|
-
return `${prefix}- ${objLines[0]}\n${objLines
|
|
771
|
-
.slice(1)
|
|
772
|
-
.map((l: string) => `${prefix} ${l}`)
|
|
773
|
-
.join("\n")}`;
|
|
774
|
-
}
|
|
775
|
-
return `${prefix}- ${itemStr}`;
|
|
776
|
-
})
|
|
777
|
-
.join("\n");
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (typeof value === "object") {
|
|
781
|
-
const entries = Object.entries(value as Record<string, unknown>);
|
|
782
|
-
if (entries.length === 0) return "{}";
|
|
783
|
-
return entries
|
|
784
|
-
.map(([k, v]) => {
|
|
785
|
-
const valStr: string = yamlStringifySimple(v, indent + 1);
|
|
786
|
-
if (typeof v === "object" && v !== null) {
|
|
787
|
-
return `${prefix}${k}:\n${valStr}`;
|
|
788
|
-
}
|
|
789
|
-
return `${prefix}${k}: ${valStr}`;
|
|
790
|
-
})
|
|
791
|
-
.join("\n");
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return String(value);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Collapse a Jx props object to a flat directive attribute map. Applies key mapping: strips `$`
|
|
799
|
-
* from Jx keywords, `:` from pseudo-classes, `@` from media queries.
|
|
800
|
-
*
|
|
801
|
-
* @param {Record<string, unknown>} propsObj
|
|
802
|
-
* @returns {Record<string, string>}
|
|
803
|
-
*/
|
|
804
|
-
function collapsePropsToAttrMap(propsObj: Record<string, unknown>) {
|
|
805
|
-
const result: Record<string, string> = {};
|
|
806
|
-
|
|
807
|
-
function walk(obj: Record<string, unknown>, prefix: string) {
|
|
808
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
809
|
-
let mdAttrKey = key;
|
|
810
|
-
// Strip $ prefix for Jx keywords
|
|
811
|
-
if (JX_DOLLAR_KEYS.has(key)) {
|
|
812
|
-
mdAttrKey = key.slice(1);
|
|
813
|
-
}
|
|
814
|
-
// Convert $title/$description to --title/--description
|
|
815
|
-
if (JX_ANNOTATION_KEYS.has(key)) {
|
|
816
|
-
mdAttrKey = `--${key.slice(1)}`;
|
|
817
|
-
}
|
|
818
|
-
// Strip : prefix for CSS pseudo-classes (inside style.* paths)
|
|
819
|
-
if (key.startsWith(":") && CSS_PSEUDO_NAMES.has(key.slice(1))) {
|
|
820
|
-
mdAttrKey = key.slice(1);
|
|
821
|
-
}
|
|
822
|
-
// Strip @ prefix for media queries (inside style.* paths)
|
|
823
|
-
if (key === "@") {
|
|
824
|
-
// Bare "@" (no media name) — treat contents as base-level style props
|
|
825
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
826
|
-
walk(value as Record<string, unknown>, prefix);
|
|
827
|
-
}
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
if (key.startsWith("@--")) {
|
|
831
|
-
mdAttrKey = key.slice(1);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
const fullKey = prefix ? `${prefix}.${mdAttrKey}` : mdAttrKey;
|
|
835
|
-
|
|
836
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
837
|
-
walk(value as Record<string, unknown>, fullKey);
|
|
838
|
-
} else {
|
|
839
|
-
result[fullKey] = String(value);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
walk(propsObj, "");
|
|
845
|
-
return result;
|
|
846
|
-
}
|