@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.
Files changed (58) hide show
  1. package/dist/studio.js +33900 -56705
  2. package/dist/studio.js.map +128 -396
  3. package/package.json +4 -10
  4. package/src/browse/browse.ts +62 -22
  5. package/src/canvas/canvas-live-render.ts +57 -55
  6. package/src/canvas/canvas-render.ts +50 -21
  7. package/src/editor/context-menu.ts +4 -1
  8. package/src/editor/convert-to-component.ts +8 -2
  9. package/src/editor/inline-edit.ts +7 -2
  10. package/src/editor/insertion-helper.ts +12 -3
  11. package/src/editor/slash-menu.ts +5 -1
  12. package/src/files/file-ops.ts +121 -102
  13. package/src/files/files.ts +53 -35
  14. package/src/format/constraints.ts +55 -0
  15. package/src/format/format-host.ts +212 -0
  16. package/src/new-project/new-project-modal.ts +18 -3
  17. package/src/panels/ai-panel.ts +3 -1
  18. package/src/panels/block-action-bar.ts +4 -2
  19. package/src/panels/canvas-dnd.ts +37 -8
  20. package/src/panels/data-explorer.ts +9 -2
  21. package/src/panels/editors.ts +1 -1
  22. package/src/panels/git-panel.ts +18 -8
  23. package/src/panels/head-panel.ts +12 -5
  24. package/src/panels/left-panel.ts +7 -4
  25. package/src/panels/panel-events.ts +3 -1
  26. package/src/panels/properties-panel.ts +9 -3
  27. package/src/panels/quick-search.ts +10 -10
  28. package/src/panels/right-panel.ts +6 -2
  29. package/src/panels/shared.ts +6 -1
  30. package/src/panels/signals-panel.ts +35 -10
  31. package/src/panels/style-inputs.ts +5 -1
  32. package/src/panels/style-panel.ts +6 -2
  33. package/src/panels/stylebook-layers-panel.ts +12 -3
  34. package/src/panels/stylebook-panel.ts +24 -8
  35. package/src/panels/toolbar.ts +16 -3
  36. package/src/platforms/devserver.ts +32 -3
  37. package/src/resize-edges.ts +12 -2
  38. package/src/services/cem-export.ts +9 -3
  39. package/src/settings/content-types-editor.ts +7 -2
  40. package/src/settings/defs-editor.ts +6 -1
  41. package/src/settings/general-settings.ts +3 -1
  42. package/src/settings/schema-field-ui.ts +11 -3
  43. package/src/settings/settings-modal.ts +4 -1
  44. package/src/state.ts +5 -1
  45. package/src/studio.ts +27 -16
  46. package/src/tabs/tab.ts +29 -6
  47. package/src/tabs/transact.ts +4 -1
  48. package/src/types.ts +15 -6
  49. package/src/ui/button-group.ts +6 -1
  50. package/src/ui/expression-editor.ts +14 -3
  51. package/src/ui/layers.ts +5 -1
  52. package/src/ui/unit-selector.ts +6 -1
  53. package/src/utils/canvas-media.ts +1 -1
  54. package/src/utils/google-fonts.ts +4 -1
  55. package/src/utils/studio-utils.ts +7 -1
  56. package/src/view.ts +8 -2
  57. package/src/markdown/md-allowlist.ts +0 -104
  58. package/src/markdown/md-convert.ts +0 -846
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,9 +32,9 @@
32
32
  "dependencies": {
33
33
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
34
34
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.2.0",
35
- "@jxsuite/parser": "^0.27.0",
36
- "@jxsuite/runtime": "^0.27.0",
37
- "@jxsuite/schema": "^0.27.0",
35
+ "@jxsuite/parser": "^0.28.0",
36
+ "@jxsuite/runtime": "^0.28.0",
37
+ "@jxsuite/schema": "^0.28.0",
38
38
  "@spectrum-web-components/accordion": "^1.12.1",
39
39
  "@spectrum-web-components/action-bar": "1.12.1",
40
40
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -75,12 +75,6 @@
75
75
  "lit-html": "^3.3.3",
76
76
  "monaco-editor": "^0.55.1",
77
77
  "quikchat": "^1.2.7",
78
- "remark-directive": "^4.0.0",
79
- "remark-frontmatter": "^5.0.0",
80
- "remark-gfm": "^4.0.1",
81
- "remark-parse": "^11.0.0",
82
- "remark-stringify": "^11.0.0",
83
- "unified": "^11.0.5",
84
78
  "yaml": "^2.9.0"
85
79
  },
86
80
  "devDependencies": {
@@ -20,7 +20,14 @@ import { componentRegistry } from "../files/components";
20
20
  import { showDialog, renderPopover } from "../ui/layers";
21
21
  import { renderComponentPreview } from "../panels/stylebook-panel";
22
22
  import { renderNode, buildScope, setSkipServerFunctions } from "@jxsuite/runtime";
23
- import { loadMarkdown } from "../files/file-ops";
23
+ import { parseSourceForPath } from "../files/file-ops";
24
+ import {
25
+ loadFormats,
26
+ formatForPath,
27
+ formatByName,
28
+ documentExtensions,
29
+ defaultContentFormat,
30
+ } from "../format/format-host";
24
31
 
25
32
  import type { ComponentEntry } from "../files/components";
26
33
  import type { ContentTypeDef } from "@jxsuite/schema/types";
@@ -62,7 +69,13 @@ const MEDIA_EXTENSIONS = new Set([
62
69
  let activeCategory = "all";
63
70
  let searchQuery = "";
64
71
  let viewMode: "grid" | "table" = "grid";
65
- let fileCache: { name: string; path: string; type: string; category: string; ext: string }[] = [];
72
+ let fileCache: {
73
+ name: string;
74
+ path: string;
75
+ type: string;
76
+ category: string;
77
+ ext: string;
78
+ }[] = [];
66
79
  let loading = false;
67
80
  /** Track which projectDirs were used for the last load, so we re-scan when they change. */
68
81
  let lastProjectDirsKey = "";
@@ -117,7 +130,13 @@ async function collectFiles(
117
130
  dir: string,
118
131
  platform: ReturnType<typeof getPlatform>,
119
132
  ): Promise<{ name: string; path: string; type: string; category: string; ext: string }[]> {
120
- const results: { name: string; path: string; type: string; category: string; ext: string }[] = [];
133
+ const results: {
134
+ name: string;
135
+ path: string;
136
+ type: string;
137
+ category: string;
138
+ ext: string;
139
+ }[] = [];
121
140
  try {
122
141
  const entries = await platform.listDirectory(dir);
123
142
  for (const entry of entries) {
@@ -200,12 +219,16 @@ function filteredFiles() {
200
219
 
201
220
  // ─── Entity types for "New +" button ────────────────────────────────────────
202
221
 
203
- const ENTITY_TYPES = [
204
- { key: "page", label: "Page", dir: "pages", ext: ".md" },
205
- { key: "layout", label: "Layout", dir: "layouts", ext: ".json" },
206
- { key: "component", label: "Component", dir: "components", ext: ".json" },
207
- { key: "content", label: "Content", dir: "content", ext: ".md" },
208
- ];
222
+ function getEntityTypes() {
223
+ const pageExt = documentExtensions("page")[0] ?? ".json";
224
+ const contentExt = defaultContentFormat()?.extensions[0] ?? ".json";
225
+ return [
226
+ { key: "page", label: "Page", dir: "pages", ext: pageExt },
227
+ { key: "layout", label: "Layout", dir: "layouts", ext: ".json" },
228
+ { key: "component", label: "Component", dir: "components", ext: ".json" },
229
+ { key: "content", label: "Content", dir: "content", ext: contentExt },
230
+ ];
231
+ }
209
232
 
210
233
  /**
211
234
  * Build frontmatter YAML from a content type's schema properties.
@@ -220,7 +243,7 @@ function buildFrontmatterYaml(contentTypeName: string) {
220
243
 
221
244
  let yaml = "";
222
245
  for (const [field, def] of Object.entries(col.schema.properties)) {
223
- const d = /** @type {{ type?: string; format?: string }} */ (def);
246
+ const d = /** @type {{ type?: string; format?: string }} */ def;
224
247
  yaml += `${field}: ${yamlDefault(d.type || "", d.format || "")}\n`;
225
248
  }
226
249
  return yaml || "title: Untitled\n";
@@ -241,7 +264,12 @@ function getContentTypeTypes() {
241
264
  key: `contentType:${name}`,
242
265
  label: name.charAt(0).toUpperCase() + name.slice(1),
243
266
  dir,
244
- ext: `.${d.format || "md"}`,
267
+ ext:
268
+ d.format === "json"
269
+ ? ".json"
270
+ : (formatByName(d.format)?.extensions[0] ??
271
+ defaultContentFormat()?.extensions[0] ??
272
+ ".json"),
245
273
  contentTypeName: name,
246
274
  };
247
275
  });
@@ -261,7 +289,8 @@ async function handleNewEntity(
261
289
  ) {
262
290
  const isContentType = typeKey.startsWith("contentType:");
263
291
  const contentTypeName = isContentType ? typeKey.slice("contentType:".length) : null;
264
- const allTypes = [...ENTITY_TYPES, ...getContentTypeTypes()];
292
+ await loadFormats();
293
+ const allTypes = [...getEntityTypes(), ...getContentTypeTypes()];
265
294
  const typeInfo = allTypes.find((t) => t.key === typeKey);
266
295
  if (!typeInfo) return;
267
296
 
@@ -275,11 +304,13 @@ async function handleNewEntity(
275
304
  const filePath = `${typeInfo.dir}/${slug}${typeInfo.ext}`;
276
305
 
277
306
  let content;
278
- if (typeInfo.ext === ".md") {
279
- const frontmatter = contentTypeName
280
- ? buildFrontmatterYaml(contentTypeName)
281
- : "title: Untitled\n";
282
- content = `---\n${frontmatter}---\n\n`;
307
+ const entityFormat = formatForPath(filePath);
308
+ if (entityFormat) {
309
+ if (contentTypeName) {
310
+ content = `---\n${buildFrontmatterYaml(contentTypeName)}---\n\n`;
311
+ } else {
312
+ content = entityFormat.studio?.newFileTemplate ?? "";
313
+ }
283
314
  } else {
284
315
  content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
285
316
  }
@@ -589,9 +620,10 @@ async function renderDocPreview(filePath: string) {
589
620
  const platform = getPlatform();
590
621
  const content = await platform.readFile(filePath);
591
622
  setSkipServerFunctions(true);
623
+ await loadFormats();
592
624
  let doc;
593
- if (filePath.endsWith(".md")) {
594
- const result = await loadMarkdown(content);
625
+ if (formatForPath(filePath)) {
626
+ const result = await parseSourceForPath(filePath, content);
595
627
  doc = result.document;
596
628
  } else {
597
629
  doc = JSON.parse(content);
@@ -623,7 +655,7 @@ async function loadPreview(el: Element, file: { path: string; category: string }
623
655
  } else {
624
656
  preview = ((await renderDocPreview(file.path)) as HTMLElement | undefined) || undefined;
625
657
  }
626
- if (preview) _previewCache.set(file.path, /** @type {HTMLElement} */ (preview));
658
+ if (preview) _previewCache.set(file.path, /** @type {HTMLElement} */ preview);
627
659
  } catch {
628
660
  return;
629
661
  }
@@ -639,7 +671,13 @@ async function loadPreview(el: Element, file: { path: string; category: string }
639
671
  * @param {{ openFile: (path: string) => void }} ctx
640
672
  */
641
673
  function renderCard(
642
- file: { name: string; path: string; type: string; category: string; ext: string },
674
+ file: {
675
+ name: string;
676
+ path: string;
677
+ type: string;
678
+ category: string;
679
+ ext: string;
680
+ },
643
681
  container: HTMLElement,
644
682
  ctx: { openFile: (path: string) => void },
645
683
  ) {
@@ -736,7 +774,9 @@ export async function renderBrowse(
736
774
  @change=${(e: Event) =>
737
775
  handleNewEntity((e.target as HTMLSelectElement).value, container, ctx)}
738
776
  >
739
- ${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
777
+ ${getEntityTypes().map(
778
+ (t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
779
+ )}
740
780
  ${contentTypeTypes.length
741
781
  ? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
742
782
  (t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
@@ -131,7 +131,11 @@ export function initCanvasLiveRender(ctx: { getCanvasMode: () => string }) {
131
131
  */
132
132
  export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl: HTMLElement) {
133
133
  const tab = activeTab.value;
134
- const S = { documentPath: tab?.documentPath, mode: tab?.doc.mode, document: tab?.doc.document };
134
+ const S = {
135
+ documentPath: tab?.documentPath,
136
+ mode: tab?.doc.mode,
137
+ document: tab?.doc.document,
138
+ };
135
139
  const canvasMode = _ctx!.getCanvasMode();
136
140
 
137
141
  if (S.documentPath !== _failedElementsDocPath) {
@@ -384,67 +388,65 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
384
388
  const $defs = await buildScope(renderDoc, {}, docBase);
385
389
  // Bail out if a newer render started while buildScope was running
386
390
  if (gen !== view.renderGeneration) return null;
387
- const el = /** @type {HTMLElement} */ (
388
- runtimeRenderNode(renderDoc, $defs, {
389
- onNodeCreated(
390
- el: HTMLElement | Text,
391
- path: (string | number)[],
392
- def: Record<string, unknown>,
393
- ) {
394
- if (!(el instanceof HTMLElement)) return;
395
- // Track layout-originated elements don't store in elToPath to avoid
396
- // path collisions with remapped page content paths
397
- if (layoutWrapped && def?.$__layout) {
398
- layoutElements.add(el);
399
- if (el.setAttribute) el.setAttribute("data-jx-layout", "");
400
- return;
401
- }
391
+ const el = /** @type {HTMLElement} */ runtimeRenderNode(renderDoc, $defs, {
392
+ onNodeCreated(
393
+ el: HTMLElement | Text,
394
+ path: (string | number)[],
395
+ def: Record<string, unknown>,
396
+ ) {
397
+ if (!(el instanceof HTMLElement)) return;
398
+ // Track layout-originated elements — don't store in elToPath to avoid
399
+ // path collisions with remapped page content paths
400
+ if (layoutWrapped && def?.$__layout) {
401
+ layoutElements.add(el);
402
+ if (el.setAttribute) el.setAttribute("data-jx-layout", "");
403
+ return;
404
+ }
402
405
 
403
- // Remap layout-wrapped paths: strip the layout prefix so paths are
404
- // relative to the original page document (which is what S.document holds)
405
- let mappedPath = path;
406
- if (layoutWrapped && pageContentPrefix) {
407
- const pfx = pageContentPrefix;
408
- if (
409
- path.length >= pfx.length &&
410
- pfx.every((seg: string | number, i: number) => path[i] === seg)
411
- ) {
412
- mappedPath = ["children", ...path.slice(pfx.length)];
413
- }
406
+ // Remap layout-wrapped paths: strip the layout prefix so paths are
407
+ // relative to the original page document (which is what S.document holds)
408
+ let mappedPath = path;
409
+ if (layoutWrapped && pageContentPrefix) {
410
+ const pfx = pageContentPrefix;
411
+ if (
412
+ path.length >= pfx.length &&
413
+ pfx.every((seg: string | number, i: number) => path[i] === seg)
414
+ ) {
415
+ mappedPath = ["children", ...path.slice(pfx.length)];
414
416
  }
417
+ }
415
418
 
416
- // Remap $map paths: wrapper and template children → real document paths
417
- // prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
418
- // Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
419
- if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
420
- for (let i = 0; i < mappedPath.length - 1; i++) {
421
- if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
422
- const parentKey = mappedPath.slice(0, i).join("/");
423
- if (mapParentPaths.has(parentKey)) {
424
- if (mappedPath.length === i + 2) {
425
- mappedPath = mappedPath.slice(0, i + 1);
426
- } else if (
427
- mappedPath.length >= i + 4 &&
428
- mappedPath[i + 2] === "children" &&
429
- mappedPath[i + 3] === 0
430
- ) {
431
- mappedPath = [
432
- ...mappedPath.slice(0, i),
433
- "children",
434
- "map",
435
- ...mappedPath.slice(i + 4),
436
- ];
437
- }
438
- break;
419
+ // Remap $map paths: wrapper and template children → real document paths
420
+ // prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
421
+ // Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
422
+ if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
423
+ for (let i = 0; i < mappedPath.length - 1; i++) {
424
+ if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
425
+ const parentKey = mappedPath.slice(0, i).join("/");
426
+ if (mapParentPaths.has(parentKey)) {
427
+ if (mappedPath.length === i + 2) {
428
+ mappedPath = mappedPath.slice(0, i + 1);
429
+ } else if (
430
+ mappedPath.length >= i + 4 &&
431
+ mappedPath[i + 2] === "children" &&
432
+ mappedPath[i + 3] === 0
433
+ ) {
434
+ mappedPath = [
435
+ ...mappedPath.slice(0, i),
436
+ "children",
437
+ "map",
438
+ ...mappedPath.slice(i + 4),
439
+ ];
439
440
  }
441
+ break;
440
442
  }
441
443
  }
442
444
  }
443
- elToPath.set(el, mappedPath);
444
- },
445
- _path: [],
446
- })
447
- );
445
+ }
446
+ elToPath.set(el, mappedPath);
447
+ },
448
+ _path: [],
449
+ });
448
450
  if (canvasMode === "design" || canvasMode === "edit") {
449
451
  // Disable pointer events on all rendered elements for edit mode
450
452
  el.style.pointerEvents = "none";
@@ -11,7 +11,8 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
11
11
  import { canvasWrap, canvasPanels, updateCanvas } from "../store";
12
12
  import { activeTab } from "../workspace/workspace";
13
13
  import { view } from "../view";
14
- import { loadMarkdown, serializeDocument } from "../files/file-ops";
14
+ import { parseSourceForPath, serializeDocument } from "../files/file-ops";
15
+ import { formatByName, formatForPath } from "../format/format-host";
15
16
  import { renderWelcome } from "../panels/welcome-screen";
16
17
  import { projectState } from "../state";
17
18
  import {
@@ -77,16 +78,17 @@ export function initCanvasRender(ctx: CanvasRenderCtx) {
77
78
 
78
79
  /** Monaco language for the source view of a tab's document. */
79
80
  function sourceLang(tab: import("../tabs/tab.js").Tab) {
80
- if (tab.doc.sourceFormat === "md") return "markdown";
81
+ const format = formatByName(tab.doc.sourceFormat);
82
+ if (format) return format.mediaType?.split("/").pop() ?? "plaintext";
81
83
  return (tab.documentPath || "").endsWith(".js") ? "javascript" : "json";
82
84
  }
83
85
 
84
86
  /**
85
- * The full source text for the source view. Markdown files serialize to their on-disk form
86
- * (frontmatter YAML — title, $head, etc. — plus the body), not just the JSON of the body tree.
87
+ * The full source text for the source view. Format-class files serialize to their on-disk form
88
+ * (e.g. frontmatter YAML plus the body for markdown), not just the JSON of the body tree.
87
89
  */
88
- function sourceContent(tab: import("../tabs/tab.js").Tab, lang: string) {
89
- if (lang === "markdown") return serializeDocument(tab);
90
+ async function sourceContent(tab: import("../tabs/tab.js").Tab, lang: string) {
91
+ if (formatByName(tab.doc.sourceFormat)) return serializeDocument(tab);
90
92
  if (lang === "javascript") return tab.doc.document?.toString?.() || "";
91
93
  return JSON.stringify(tab.doc.document, null, 2);
92
94
  }
@@ -102,7 +104,11 @@ export function renderCanvas() {
102
104
  return;
103
105
  }
104
106
  const ctx = _ctx as CanvasRenderCtx;
105
- const S = { document: tab.doc.document, ui: tab.session.ui, mode: tab.doc.mode };
107
+ const S = {
108
+ document: tab.doc.document,
109
+ ui: tab.session.ui,
110
+ mode: tab.doc.mode,
111
+ };
106
112
  const canvasMode = ctx.getCanvasMode();
107
113
 
108
114
  // Advance render generation so stale async renders from the previous cycle bail out
@@ -139,11 +145,17 @@ export function renderCanvas() {
139
145
  // the editing surface here, mirroring the panel draft-state behaviour).
140
146
  if (canvasMode === "source" && view.monacoEditor) {
141
147
  const editor = view.monacoEditor;
142
- const newVal = sourceContent(tab, sourceLang(tab));
143
- if (!editor.hasTextFocus() && editor.getValue() !== newVal) {
144
- editor._ignoreNextChange = true;
145
- editor.setValue(newVal);
146
- }
148
+ sourceContent(tab, sourceLang(tab))
149
+ .then((newVal) => {
150
+ if (view.monacoEditor !== editor) return;
151
+ if (!editor.hasTextFocus() && editor.getValue() !== newVal) {
152
+ editor._ignoreNextChange = true;
153
+ editor.setValue(newVal);
154
+ }
155
+ })
156
+ .catch(() => {
157
+ // Serialization unavailable (e.g. format service unreachable) — keep the current buffer
158
+ });
147
159
  return;
148
160
  }
149
161
 
@@ -251,9 +263,19 @@ export function renderCanvas() {
251
263
 
252
264
  const filePath = tab.documentPath || "document.json";
253
265
  const lang = sourceLang(tab);
254
- const content = sourceContent(tab, lang);
255
266
  const modelUri = monaco.Uri.parse("file:///" + filePath);
256
- const model = monaco.editor.createModel(content, lang, modelUri);
267
+ const model = monaco.editor.createModel("", lang, modelUri);
268
+ sourceContent(tab, lang)
269
+ .then((content) => {
270
+ const editor = view.monacoEditor;
271
+ if (editor && editor.getModel() === model) {
272
+ editor._ignoreNextChange = true;
273
+ model.setValue(content);
274
+ }
275
+ })
276
+ .catch(() => {
277
+ // Serialization unavailable — leave the buffer empty rather than crash the render
278
+ });
257
279
  view.monacoEditor = monaco.editor.create(editorContainer as unknown as HTMLElement, {
258
280
  model,
259
281
  theme: "vs-dark",
@@ -280,15 +302,18 @@ export function renderCanvas() {
280
302
  debounce = setTimeout(async () => {
281
303
  const tab = activeTab.value;
282
304
  if (!tab) return;
283
- if (lang === "markdown") {
305
+ if (formatByName(tab.doc.sourceFormat) && tab.documentPath) {
284
306
  try {
285
- // Parse the full markdown source back into body + frontmatter (title, $head, etc.).
286
- const { document, frontmatter } = await loadMarkdown(editor.getValue());
307
+ // Parse the full source back into body + frontmatter (title, $head, etc.).
308
+ const { document, frontmatter } = await parseSourceForPath(
309
+ tab.documentPath,
310
+ editor.getValue(),
311
+ );
287
312
  tab.doc.document = document as JxMutableNode;
288
313
  tab.doc.content.frontmatter = frontmatter;
289
314
  tab.doc.dirty = true;
290
315
  } catch {
291
- // Unparseable markdown — don't update state
316
+ // Unparseable source — don't update state
292
317
  }
293
318
  } else if (lang === "json") {
294
319
  try {
@@ -354,14 +379,18 @@ export function renderCanvas() {
354
379
 
355
380
  /** @param {string} content */
356
381
  const parseContent = (content: string) => {
357
- if (gitDiffState.isMarkdown) {
358
- return loadMarkdown(content).then((r) => r.document);
382
+ const fmtPath = gitDiffState.filePath ?? "";
383
+ if (formatForPath(fmtPath)) {
384
+ return parseSourceForPath(fmtPath, content).then((r) => r.document);
359
385
  }
360
386
  return Promise.resolve().then(() => {
361
387
  try {
362
388
  return JSON.parse(content);
363
389
  } catch {
364
- return { tagName: "div", children: [{ tagName: "p", textContent: "Failed to parse" }] };
390
+ return {
391
+ tagName: "div",
392
+ children: [{ tagName: "p", textContent: "Failed to parse" }],
393
+ };
365
394
  }
366
395
  });
367
396
  };
@@ -216,7 +216,10 @@ export function dismissContextMenu() {
216
216
  export function showContextMenu(
217
217
  e: MouseEvent,
218
218
  path: JxPath,
219
- opts: { onEditComponent?: (path: string) => void; rerender?: () => void } = {},
219
+ opts: {
220
+ onEditComponent?: (path: string) => void;
221
+ rerender?: () => void;
222
+ } = {},
220
223
  ) {
221
224
  e.preventDefault();
222
225
  dismissContextMenu();
@@ -102,10 +102,16 @@ function extractComponentDef(node: JxMutableNode) {
102
102
  function validateName(val: string) {
103
103
  val = val.trim().toLowerCase();
104
104
  if (!val.includes("-")) {
105
- return { valid: false, error: "Name must contain a hyphen (e.g. my-component)" };
105
+ return {
106
+ valid: false,
107
+ error: "Name must contain a hyphen (e.g. my-component)",
108
+ };
106
109
  }
107
110
  if (!VALID_NAME.test(val)) {
108
- return { valid: false, error: "Lowercase letters, digits, and hyphens only" };
111
+ return {
112
+ valid: false,
113
+ error: "Lowercase letters, digits, and hyphens only",
114
+ };
109
115
  }
110
116
  const exists = componentRegistry.some((c: JxMutableNode) => c.tagName === val);
111
117
  if (exists) {
@@ -526,7 +526,12 @@ function domNodeToJx(node: Node) {
526
526
  const result: JxMutableNode = { tagName: tag };
527
527
 
528
528
  // Map browser execCommand output to our tag conventions
529
- const tagMap: Record<string, string> = { b: "strong", i: "em", s: "del", strike: "del" };
529
+ const tagMap: Record<string, string> = {
530
+ b: "strong",
531
+ i: "em",
532
+ s: "del",
533
+ strike: "del",
534
+ };
530
535
  if (tagMap[tag]) result.tagName = tagMap[tag];
531
536
 
532
537
  // Attributes
@@ -549,7 +554,7 @@ function domNodeToJx(node: Node) {
549
554
  result.children = [];
550
555
  for (const child of childNodes) {
551
556
  const jsx = domNodeToJx(child);
552
- if (jsx) result.children.push(/** @type {JxMutableNode} */ (jsx));
557
+ if (jsx) result.children.push(/** @type {JxMutableNode} */ jsx);
553
558
  }
554
559
  }
555
560
 
@@ -44,7 +44,12 @@ let _helper: HTMLElement | null = null;
44
44
 
45
45
  let _currentAnchor: HTMLElement | null = null;
46
46
 
47
- let _insertionPoint: { edge: string; path: JxPath; parentPath: JxPath; idx: number } | null = null;
47
+ let _insertionPoint: {
48
+ edge: string;
49
+ path: JxPath;
50
+ parentPath: JxPath;
51
+ idx: number;
52
+ } | null = null;
48
53
 
49
54
  let _abort: AbortController | null = null;
50
55
 
@@ -87,8 +92,12 @@ export function mount(ctx: InsertionHelperContext) {
87
92
  viewport.on("mousemove", { signal: _abort.signal }).subscribe({ next: onMouseMove });
88
93
  viewport.on("mouseleave", { signal: _abort.signal }).subscribe({ next: hide });
89
94
  } else {
90
- panel.viewport.addEventListener("mousemove", onMouseMove, { signal: _abort.signal });
91
- panel.viewport.addEventListener("mouseleave", hide, { signal: _abort.signal });
95
+ panel.viewport.addEventListener("mousemove", onMouseMove, {
96
+ signal: _abort.signal,
97
+ });
98
+ panel.viewport.addEventListener("mouseleave", hide, {
99
+ signal: _abort.signal,
100
+ });
92
101
  }
93
102
  }
94
103
 
@@ -79,7 +79,11 @@ export function isSlashMenuOpen() {
79
79
  export function showSlashMenu(
80
80
  anchorEl: HTMLElement,
81
81
  filter: string,
82
- cbs: { onSelect: (cmd: SlashCommand) => void; showFilter?: boolean; commands?: SlashCommand[] },
82
+ cbs: {
83
+ onSelect: (cmd: SlashCommand) => void;
84
+ showFilter?: boolean;
85
+ commands?: SlashCommand[];
86
+ },
83
87
  ) {
84
88
  callbacks = cbs;
85
89
  _anchorEl = anchorEl;