@jxsuite/studio 2.4.3 → 4.0.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/README.md +5 -5
- package/dist/chunks/Overlay.dev-723bv0df.js +23 -0
- package/dist/chunks/cf-settings-45fgyx8y.js +21 -0
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-t88vb2dr.js +49 -0
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-dhw1nrz9.js} +27 -18
- package/dist/chunks/entry-commands-6z6bhxg0.js +46 -0
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-3ydgjbdy.js} +32 -23
- package/dist/chunks/file-ops-rskmhxyq.js +48 -0
- package/dist/chunks/files-0cratsr0.js +92 -0
- package/dist/chunks/{format-host-wewba4nq.js → format-host-tgcmfs4p.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-0yywyk76.js} +21 -15
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8q2g05x1.js} +6 -4
- package/dist/chunks/layers-panel-z3tqe5cd.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-rws7k4br.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-54jvhskq.js +28 -0
- package/dist/chunks/quick-search-cgfmjwq5.js +65 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-cqp8bc4v.js +46 -0
- package/dist/chunks/sp-overlay.dev-yztmtrs1.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-xcvpnqr0.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-f5yyvqxv.js → studio-1hbqnt19.js} +37 -31
- package/dist/chunks/studio-2zkz8evn.js +3163 -0
- package/dist/chunks/{studio-0b6fs05q.js → studio-4pqqxbrt.js} +23 -15
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-ef8t53vc.js → studio-5q69gfag.js} +60 -56
- package/dist/chunks/{studio-v2c5pmjj.js → studio-5rywrkxz.js} +565 -133
- package/dist/chunks/{studio-6kv3apa9.js → studio-68n67094.js} +25 -9
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/{studio-hyn3vhwr.js → studio-8swrjbny.js} +31 -18
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/{studio-28xc8ws4.js → studio-9c4k4vhv.js} +14 -10
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-tr20m8g8.js → studio-9r6j5ev1.js} +37 -2
- package/dist/chunks/{studio-aweyr4v1.js → studio-ahjfdy6e.js} +9 -7
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/{studio-26sv44br.js → studio-cb12yz9w.js} +8543 -8784
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-rqemfhz6.js → studio-eenfkt4e.js} +6 -6
- package/dist/chunks/studio-eesa1myv.js +498 -0
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-ke244pam.js → studio-ex8zyp98.js} +51 -25
- package/dist/chunks/studio-gv8vvshc.js +118 -0
- package/dist/chunks/studio-hbtp0eh1.js +1315 -0
- package/dist/chunks/{studio-cz2j1c0g.js → studio-htsf0w6m.js} +75 -123
- package/dist/chunks/{studio-pyrw8h0w.js → studio-kjjsjak4.js} +528 -212
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/studio-kyh9avvv.js +28 -0
- package/dist/chunks/{studio-jnakhg87.js → studio-kyk2v92a.js} +14 -14
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/{studio-tzx3xmvx.js → studio-mmv72a27.js} +8 -8
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/{studio-sccge2wf.js → studio-p8j86wag.js} +2 -2
- package/dist/chunks/{studio-v1va0vhd.js → studio-pdfjntbk.js} +75 -139
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/studio-r1r7afxx.js +1066 -0
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-sf7zjgv1.js +91 -0
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-wdt9y92a.js → studio-txwkm6p9.js} +2 -2
- package/dist/chunks/{studio-dgj7gq44.js → studio-v38hmvx1.js} +7 -7
- package/dist/chunks/{studio-7ejemhej.js → studio-vadzgkrz.js} +6 -6
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-hb7c46jy.js → studio-wx5609xd.js} +21 -15
- package/dist/chunks/studio-xrapxwwc.js +1539 -0
- package/dist/chunks/{studio-ws6ef6js.js → studio-yxrkyv8a.js} +41 -29
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/workspace-xpres40v.js +89 -0
- package/dist/iframe-entry.js +450 -48
- package/dist/studio.js +2455 -1761
- package/package.json +16 -15
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +59 -5
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +50 -16
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/file-drop-action.ts +2 -1
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -61
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +35 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +333 -160
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +21 -80
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/signals-panel.ts +2 -1
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +34 -3
- package/src/platforms/devserver.ts +72 -34
- package/src/preview/preview-overlay.ts +226 -0
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +494 -0
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-settings.ts +69 -71
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/cf-settings.ts +19 -37
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +89 -0
- package/src/services/document-assistant.ts +49 -7
- package/src/services/idle.ts +6 -1
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +181 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/services/tool-executor.ts +34 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-dialog.ts +18 -41
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/settings/settings-document.ts +65 -9
- package/src/shell.ts +6 -14
- package/src/studio.ts +65 -7
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +164 -8
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/ai-model-picker.ts +162 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +43 -31
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +111 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/cf-settings-g5fbf2zp.js +0 -17
- package/dist/chunks/content-source-k9724vcv.js +0 -39
- package/dist/chunks/files-yew27sn9.js +0 -69
- package/dist/chunks/layers-panel-qnsghwwd.js +0 -59
- package/dist/chunks/publish-panel-a0pdft5w.js +0 -21
- package/dist/chunks/quick-search-9avyc4pc.js +0 -54
- package/dist/chunks/right-panel-kcnhtc5j.js +0 -38
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/dist/chunks/studio-ra7ar50f.js +0 -6682
- package/src/canvas/content-assets.ts +0 -263
- package/src/canvas/site-style-css.ts +0 -121
- package/src/services/settings-store.ts +0 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,19 +52,20 @@
|
|
|
52
52
|
"lint:styles": "bun run scripts/check-styles.ts",
|
|
53
53
|
"test": "bun run lint:styles && bun test --isolate",
|
|
54
54
|
"test:coverage": "bun test --isolate --coverage",
|
|
55
|
-
"upgrade": "
|
|
55
|
+
"upgrade": "bun update --latest && bun install"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
|
|
59
59
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.1.0",
|
|
60
|
-
"@jxsuite/ai": "^0.36.
|
|
61
|
-
"@jxsuite/collab": "^0.8.
|
|
62
|
-
"@jxsuite/create": "^1.3.
|
|
63
|
-
"@jxsuite/formulas": "^0.0.
|
|
64
|
-
"@jxsuite/markup": "^0.4.
|
|
65
|
-
"@jxsuite/protocol": "^
|
|
66
|
-
"@jxsuite/runtime": "^
|
|
67
|
-
"@jxsuite/schema": "^
|
|
60
|
+
"@jxsuite/ai": "^0.36.5",
|
|
61
|
+
"@jxsuite/collab": "^0.8.4",
|
|
62
|
+
"@jxsuite/create": "^1.3.8",
|
|
63
|
+
"@jxsuite/formulas": "^0.0.15",
|
|
64
|
+
"@jxsuite/markup": "^0.4.8",
|
|
65
|
+
"@jxsuite/protocol": "^2.1.0",
|
|
66
|
+
"@jxsuite/runtime": "^3.0.0",
|
|
67
|
+
"@jxsuite/schema": "^2.0.0",
|
|
68
|
+
"@jxsuite/site": "^1.0.0",
|
|
68
69
|
"@spectrum-web-components/accordion": "^1.12.2",
|
|
69
70
|
"@spectrum-web-components/action-bar": "1.12.2",
|
|
70
71
|
"@spectrum-web-components/action-button": "^1.12.2",
|
|
@@ -107,15 +108,15 @@
|
|
|
107
108
|
"lit": "^3.3.3",
|
|
108
109
|
"lit-html": "^3.3.3",
|
|
109
110
|
"monaco-editor": "^0.56.0",
|
|
110
|
-
"tabulator-tables": "^6.5",
|
|
111
|
+
"tabulator-tables": "^6.5.2",
|
|
111
112
|
"yaml": "^2.9.0"
|
|
112
113
|
},
|
|
113
114
|
"devDependencies": {
|
|
114
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
115
|
+
"@happy-dom/global-registrator": "^20.11.8",
|
|
115
116
|
"@types/mdast": "^4.0.4",
|
|
116
|
-
"@webref/css": "^8.7.
|
|
117
|
-
"@webref/elements": "^2.
|
|
118
|
-
"@webref/idl": "^3.
|
|
117
|
+
"@webref/css": "^8.7.3",
|
|
118
|
+
"@webref/elements": "^2.8.0",
|
|
119
|
+
"@webref/idl": "^3.83.0"
|
|
119
120
|
},
|
|
120
121
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
121
122
|
}
|
|
@@ -19,7 +19,7 @@ import { html, nothing } from "lit-html";
|
|
|
19
19
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
20
20
|
import { ref } from "lit-html/directives/ref.js";
|
|
21
21
|
import { isImage } from "../files/media-upload";
|
|
22
|
-
import {
|
|
22
|
+
import { previewFileSrc } from "../files/media-paths";
|
|
23
23
|
import { localeLabel } from "@jxsuite/schema/locale";
|
|
24
24
|
import { groupByCategory, groupByDate } from "./library-model";
|
|
25
25
|
import type { LibraryFile, LibraryLayout } from "./library-model";
|
|
@@ -154,7 +154,7 @@ function previewTpl(file: LibraryFile, ctx: LayoutContext): TemplateResult {
|
|
|
154
154
|
return html`<img
|
|
155
155
|
class="library-thumb"
|
|
156
156
|
loading="lazy"
|
|
157
|
-
src=${
|
|
157
|
+
src=${previewFileSrc(file.path)}
|
|
158
158
|
alt=""
|
|
159
159
|
/>`;
|
|
160
160
|
}
|
|
@@ -268,7 +268,7 @@ export function mediaTpl(files: readonly LibraryFile[], ctx: LayoutContext): Tem
|
|
|
268
268
|
? html`<img
|
|
269
269
|
class="library-thumb"
|
|
270
270
|
loading="lazy"
|
|
271
|
-
src=${
|
|
271
|
+
src=${previewFileSrc(file.path)}
|
|
272
272
|
alt=""
|
|
273
273
|
/>`
|
|
274
274
|
: html`<sp-icon-document size="l"></sp-icon-document>`
|
|
@@ -42,8 +42,9 @@ import { invalidateUsages } from "../services/references";
|
|
|
42
42
|
import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
|
|
43
43
|
import { createFileIn, openFileInTab } from "../files/files";
|
|
44
44
|
import { createEntry } from "../content/entry-commands";
|
|
45
|
+
import { formatByExtension, formatSerialize } from "../format/format-host";
|
|
45
46
|
import { entryCollections } from "../content/entry-model";
|
|
46
|
-
import {
|
|
47
|
+
import { uploadAccept, uploadAssets } from "../files/media-upload";
|
|
47
48
|
import { localeLabel } from "@jxsuite/schema/locale";
|
|
48
49
|
import {
|
|
49
50
|
LIBRARY_CATEGORIES,
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
tableRowsTpl,
|
|
71
72
|
} from "./library-layouts";
|
|
72
73
|
import type { ActivityHandle } from "../panels/activity-panel";
|
|
74
|
+
import type { FormatChoice } from "../files/files";
|
|
73
75
|
import type { EffectScope } from "@vue/reactivity";
|
|
74
76
|
import type { LibraryFile, LibraryLayout } from "./library-model";
|
|
75
77
|
import type { LibrarySource } from "./library-source";
|
|
@@ -602,10 +604,60 @@ export function libraryNewEntries(): LibraryNewEntry[] {
|
|
|
602
604
|
* `createEntry` is called rather than reimplemented; a second seeder here would drift from the one
|
|
603
605
|
* the palette's `content.newEntry` uses within a release.
|
|
604
606
|
*
|
|
605
|
-
* A **document kind**
|
|
606
|
-
* as easily as a `.json`, so the extension
|
|
607
|
-
*
|
|
607
|
+
* A **document kind** gets the format PICKER, which is the same argument the verbatim name field
|
|
608
|
+
* used to make: a page may be a `.md` as easily as a `.json`, so the extension is a real choice —
|
|
609
|
+
* now offered rather than left to be typed from memory. A collection's extension is not a choice;
|
|
610
|
+
* it is the collection's, and `createEntry` supplies it.
|
|
611
|
+
*
|
|
612
|
+
* **A layout is not a choice either, and that is not a policy.** Both readers of a layout parse it
|
|
613
|
+
* as JSON and neither dispatches through the format registry — `site/layout-resolver.ts` throws and
|
|
614
|
+
* `site-context.ts` returns null — so a `.md` layout is a file the build cannot load. There is no
|
|
615
|
+
* `"layout"` document kind for a format to declare, so this is stated here rather than derived.
|
|
616
|
+
*
|
|
617
|
+
* A **component** keeps the picker: a format whose `$studio.documentMode.componentWhen` promotes a
|
|
618
|
+
* frontmatter `tagName` (Markdown's does) can express one perfectly well. What it cannot do is
|
|
619
|
+
* express one from a `newFileTemplate` that carries no `tagName`, so the seed below supplies it —
|
|
620
|
+
* through the format's own serializer, because this module knows no format's syntax.
|
|
621
|
+
*/
|
|
622
|
+
/** How each document kind's extension is settled. Anything else falls back to the full picker. */
|
|
623
|
+
const FORMAT_CHOICE_BY_KIND: Record<string, FormatChoice> = {
|
|
624
|
+
component: { defaultExt: ".json", docKind: "component", kind: "choose" },
|
|
625
|
+
layout: { ext: ".json", kind: "fixed" },
|
|
626
|
+
page: { defaultExt: ".json", docKind: "page", kind: "choose" },
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The body a new component starts as, when the reader picked a format rather than `.json`.
|
|
631
|
+
*
|
|
632
|
+
* `.json` answers `undefined` and takes the shared blank document. A format answers with its OWN
|
|
633
|
+
* serialization of `{ tagName }` — never a hand-written `---\ntagName: …\n---`, which would put one
|
|
634
|
+
* format's syntax in the Library. Without the `tagName` the file satisfies neither
|
|
635
|
+
* `$studio.documentMode.componentWhen` nor the build's tag registration, and is a component nothing
|
|
636
|
+
* loads. A stem that is not a valid custom-element name (no hyphen) cannot carry one, so it falls
|
|
637
|
+
* back to the format's own template and the author names the tag themselves.
|
|
608
638
|
*/
|
|
639
|
+
async function componentSeed(ext: string, fileName: string): Promise<string | undefined> {
|
|
640
|
+
const format = formatByExtension(ext);
|
|
641
|
+
if (!format?.capabilities.serialize) {
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
const tagName = fileName.slice(0, fileName.length - ext.length);
|
|
645
|
+
if (!/^[a-z][\d.a-z]*-[\d.a-z-]*$/.test(tagName)) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
return await formatSerialize(
|
|
650
|
+
format.name,
|
|
651
|
+
{ children: [], tagName },
|
|
652
|
+
{ frontmatter: true, mode: "roundtrip" },
|
|
653
|
+
);
|
|
654
|
+
} catch {
|
|
655
|
+
// The serializer is the project's, so its failure is not a reason to refuse the creation —
|
|
656
|
+
// The format's own template still produces a file the author can finish by hand.
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
609
661
|
export async function createLibraryEntry(key: string): Promise<string | null> {
|
|
610
662
|
const entry = libraryNewEntries().find((row) => row.key === key);
|
|
611
663
|
if (!entry) {
|
|
@@ -614,7 +666,9 @@ export async function createLibraryEntry(key: string): Promise<string | null> {
|
|
|
614
666
|
const created =
|
|
615
667
|
entry.collection === undefined
|
|
616
668
|
? await createFileIn({
|
|
669
|
+
...(entry.key === "component" ? { content: componentSeed } : {}),
|
|
617
670
|
dir: entry.dir,
|
|
671
|
+
format: FORMAT_CHOICE_BY_KIND[entry.key] ?? { defaultExt: ".json", kind: "choose" },
|
|
618
672
|
source: "Library",
|
|
619
673
|
suggestedName: "untitled",
|
|
620
674
|
title: `New ${entry.label}`,
|
|
@@ -760,7 +814,7 @@ function toolbarTpl(panel: ActiveLibraryPane, files: readonly LibraryFile[]) {
|
|
|
760
814
|
<input
|
|
761
815
|
type="file"
|
|
762
816
|
multiple
|
|
763
|
-
accept=${
|
|
817
|
+
accept=${uploadAccept()}
|
|
764
818
|
class="library-upload-input"
|
|
765
819
|
@change=${(e: Event) => {
|
|
766
820
|
const input = e.target as HTMLInputElement;
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
* already knows a preview is in flight without this module declaring an idle source of its own.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
buildScope,
|
|
24
|
+
renderNode,
|
|
25
|
+
setCanvasAssetResolver,
|
|
26
|
+
setSkipServerFunctions,
|
|
27
|
+
} from "@jxsuite/runtime";
|
|
28
|
+
import { assetContextFor, hostAssetDeclarations, resolveAssetRef } from "../canvas/asset-refs";
|
|
23
29
|
import { componentRegistry } from "../files/components";
|
|
24
30
|
import { getPlatform } from "../platform";
|
|
25
31
|
import { loadFormats, formatForPath } from "../format/format-host";
|
|
@@ -115,6 +121,7 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
|
|
|
115
121
|
try {
|
|
116
122
|
const content = await getPlatform().readFile(filePath);
|
|
117
123
|
setSkipServerFunctions(true);
|
|
124
|
+
const assets = assetContextFor(filePath, hostAssetDeclarations());
|
|
118
125
|
await loadFormats();
|
|
119
126
|
let document_: JxDocument;
|
|
120
127
|
if (formatForPath(filePath)) {
|
|
@@ -124,8 +131,17 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
|
|
|
124
131
|
document_ = JSON.parse(content) as JxDocument;
|
|
125
132
|
}
|
|
126
133
|
const scope = await buildScope(document_, {}, location.href);
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
/* This render lands in the PARENT document, so an `<img src="/hero.jpg">` inside it resolves
|
|
135
|
+
against `index.html` — which is the shell, not the site. Same hook the canvas installs, same
|
|
136
|
+
context, restored afterwards because the next preview is a different document and a stale
|
|
137
|
+
`documentDir` would resolve its relative refs against the wrong entry. */
|
|
138
|
+
setCanvasAssetResolver((value) => resolveAssetRef(value, assets));
|
|
139
|
+
try {
|
|
140
|
+
const element = renderNode(document_, scope);
|
|
141
|
+
return element instanceof HTMLElement ? element : null;
|
|
142
|
+
} finally {
|
|
143
|
+
setCanvasAssetResolver(null);
|
|
144
|
+
}
|
|
129
145
|
} catch {
|
|
130
146
|
return null;
|
|
131
147
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset references, resolved for the canvas — the half that asks the running editor.
|
|
3
|
+
*
|
|
4
|
+
* The resolution math itself is {@link file://./asset-resolve.ts}, which is pure and which the
|
|
5
|
+
* canvas iframe imports. This module is everything that has to look something up: the open
|
|
6
|
+
* project's content sections and locales, the active tab, the registered platform's declarations.
|
|
7
|
+
*
|
|
8
|
+
* The split is by REALM. The iframe bundle is deliberately dependency-light, and reaching
|
|
9
|
+
* `projectState` or `getPlatform()` from it drags the whole editor shell in behind it — which is
|
|
10
|
+
* what `check-studio-dist` caught the first time these lived in one file.
|
|
11
|
+
*
|
|
12
|
+
* @docs studio/projects/media
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveI18n } from "@jxsuite/schema/locale";
|
|
16
|
+
import { BUILD_LANES, dirOfPath } from "@jxsuite/schema/asset-paths";
|
|
17
|
+
import { contentMountFor, resolveAssetRef } from "./asset-resolve";
|
|
18
|
+
import { documentBase, loopbackAssetSrc } from "./canvas-origin";
|
|
19
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
20
|
+
import { projectState } from "../store";
|
|
21
|
+
import { activeTab } from "../workspace/workspace";
|
|
22
|
+
import type { AssetContext } from "./asset-resolve";
|
|
23
|
+
import type { ContentSectionEntry } from "../types";
|
|
24
|
+
|
|
25
|
+
/* Re-exported so callers that want both halves import one module. The pure ones are defined next
|
|
26
|
+
door precisely so the iframe can reach them WITHOUT reaching any of the above. */
|
|
27
|
+
export { contentMountFor, mountedRefFor, resolveAssetRef } from "./asset-resolve";
|
|
28
|
+
export type { AssetContext } from "./asset-resolve";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The open project's canonical locale tags, or `[]` when it declares none.
|
|
32
|
+
*
|
|
33
|
+
* Through `resolveI18n` rather than off the raw config, so `EN-us` means the same thing here as it
|
|
34
|
+
* does in the compiler and the content loader — the whole reason that resolver is in
|
|
35
|
+
* `@jxsuite/schema`.
|
|
36
|
+
*/
|
|
37
|
+
export function projectLocales(): string[] {
|
|
38
|
+
return resolveI18n(projectState?.projectConfig ?? {}).i18n?.locales ?? [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The context for a document in the project currently open, or null when nothing applies.
|
|
43
|
+
*
|
|
44
|
+
* @param {string | null | undefined} documentPath - Project-relative path of the open document
|
|
45
|
+
* @param {object} [over] - Host declarations: the asset space and the project-file base URL
|
|
46
|
+
* @returns {AssetContext | null} A context, or null when no resolution is needed
|
|
47
|
+
*/
|
|
48
|
+
export function assetContextFor(
|
|
49
|
+
documentPath: string | null | undefined,
|
|
50
|
+
over?: { space?: "site" | "repo" | undefined; fileBaseUrl?: string | undefined },
|
|
51
|
+
): AssetContext | null {
|
|
52
|
+
const space = over?.space ?? "site";
|
|
53
|
+
const mount = contentMountFor(
|
|
54
|
+
documentPath,
|
|
55
|
+
projectState?.projectConfig?.content as Record<string, ContentSectionEntry> | undefined,
|
|
56
|
+
projectLocales(),
|
|
57
|
+
);
|
|
58
|
+
/* In site space the mount is the ONLY thing that needs doing, so no mount means no work. In repo
|
|
59
|
+
space every reference needs rebasing, mount or not. */
|
|
60
|
+
if (space === "site" && !mount) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
documentDir: dirOfPath(documentPath ?? ""),
|
|
65
|
+
...(over?.fileBaseUrl === undefined ? {} : { fileBaseUrl: over.fileBaseUrl }),
|
|
66
|
+
lanes: BUILD_LANES,
|
|
67
|
+
mounts: mount ? [mount] : [],
|
|
68
|
+
space,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The src a PARENT-REALM preview should load for an AUTHORED value — a media-picker thumbnail, the
|
|
74
|
+
* social card in the SEO modal.
|
|
75
|
+
*
|
|
76
|
+
* The canvas resolves its own references; panel chrome renders in the parent document, where a
|
|
77
|
+
* content-relative value would resolve against `index.html` and 404 exactly as it did in the
|
|
78
|
+
* canvas. Same mapping, same rule — resolved against the ACTIVE tab, which is the entry whose field
|
|
79
|
+
* is being edited.
|
|
80
|
+
*
|
|
81
|
+
* Takes what the author WROTE. For a project FILE PATH — a library thumbnail, a media-browser tile
|
|
82
|
+
* — use `previewFileSrc` in `files/media-paths`; the two are separate because a file path is not an
|
|
83
|
+
* authored reference and confusing them is silent.
|
|
84
|
+
*
|
|
85
|
+
* The loopback absolutization is folded in rather than left to the caller: every one of the four
|
|
86
|
+
* call sites needs both steps, and one that composed them by hand would work everywhere except the
|
|
87
|
+
* desktop shell, where a relative src resolves against `views://`.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} value - The reference as the author wrote it
|
|
90
|
+
* @returns {string} A src the parent realm can load
|
|
91
|
+
*/
|
|
92
|
+
export function previewAssetSrc(value: string): string {
|
|
93
|
+
if (!value) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
const ctx = assetContextFor(activeTab.value?.documentPath, hostAssetDeclarations());
|
|
97
|
+
return loopbackAssetSrc(resolveAssetRef(value, ctx) ?? value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* What the registered host says about media — the platform's `assetSpace`, and the base project
|
|
102
|
+
* files are served under.
|
|
103
|
+
*
|
|
104
|
+
* Read through the platform rather than passed in, because every caller is panel chrome with no
|
|
105
|
+
* access to the render pipeline and because the answer is a property of the HOST, not of the call.
|
|
106
|
+
* The canvas pipeline builds the same two values from the same two places; the difference is only
|
|
107
|
+
* that it already has the document in hand.
|
|
108
|
+
*
|
|
109
|
+
* @returns {{ space?: "site" | "repo"; fileBaseUrl: string }} The host's declarations
|
|
110
|
+
*/
|
|
111
|
+
export function hostAssetDeclarations(): {
|
|
112
|
+
space?: "site" | "repo" | undefined;
|
|
113
|
+
fileBaseUrl: string;
|
|
114
|
+
} {
|
|
115
|
+
return {
|
|
116
|
+
fileBaseUrl: documentBase(projectState?.projectRoot),
|
|
117
|
+
...(hasPlatform() ? { space: getPlatform().assetSpace } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset references, resolved for the canvas — the PURE half.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `asset-refs.ts` for one reason, and it is a hard one: this module runs **inside the
|
|
5
|
+
* canvas iframe**, which is a deliberately dependency-light bundle. `asset-refs.ts` reaches the
|
|
6
|
+
* open project, the active tab and the registered platform, and importing any of that from the
|
|
7
|
+
* iframe drags the entire editor shell in behind it. The build gate caught exactly that.
|
|
8
|
+
*
|
|
9
|
+
* So the split is by REALM, not by taste: everything here is a pure function of its arguments, and
|
|
10
|
+
* everything that has to ask the running editor a question lives next door.
|
|
11
|
+
*
|
|
12
|
+
* A document names media in one of two shapes, and neither is a URL the canvas can use as written.
|
|
13
|
+
*
|
|
14
|
+
* A content entry references media relative to ITSELF — `` — so the file
|
|
15
|
+
* reads correctly in a plain markdown editor and so collections sourced outside the project root
|
|
16
|
+
* still reach their media (specs/site-architecture.md §9.3). On the built site the collection
|
|
17
|
+
* loader rewrites those refs onto the content type's asset mount,
|
|
18
|
+
* `/content/<type>/images/hero.png`. Studio opens an entry as a STANDALONE document, so that loader
|
|
19
|
+
* never runs.
|
|
20
|
+
*
|
|
21
|
+
* A page references media by SITE URL — `/hero.jpg` — which is a file the project tree calls
|
|
22
|
+
* `public/hero.jpg`. That works wherever the canvas is hosted by something that serves the site's
|
|
23
|
+
* own URL space, which is what the dev server and the desktop loopback both are.
|
|
24
|
+
*
|
|
25
|
+
* {@link AssetContext} is the host's answer to "which of those two do you serve", and
|
|
26
|
+
* {@link resolveAssetRef} is the whole of the resolution:
|
|
27
|
+
*
|
|
28
|
+
* - `"site"` — the origin already answers site URLs, so only the content-mount mapping is missing.
|
|
29
|
+
* This is what desktop and `jx dev` have always done, and the default when a host says nothing.
|
|
30
|
+
* - `"repo"` — nothing answers a site URL, and the host serves PROJECT PATHS under a base instead.
|
|
31
|
+
* Every reference resolves to the file it names and is rebased onto that base. The mount detour
|
|
32
|
+
* disappears: a content entry's `./images/hero.png` is already `content/posts/images/hero.png`, a
|
|
33
|
+
* real path the host can serve.
|
|
34
|
+
*
|
|
35
|
+
* The SOURCE document is never touched in either case: the authored ref is what gets serialized
|
|
36
|
+
* back to disk, and the properties panel shows the author what they wrote.
|
|
37
|
+
*
|
|
38
|
+
* One deliberate divergence from the content loader: it declines to rewrite a ref whose target does
|
|
39
|
+
* not exist (`existsSync`), leaving an unresolvable path alone rather than changing its meaning.
|
|
40
|
+
* The browser has no filesystem, so this resolves optimistically. A missing file is broken either
|
|
41
|
+
* way; pointing at the URL the built site would have attempted is the more informative failure.
|
|
42
|
+
*
|
|
43
|
+
* @docs studio/projects/media
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
assetUrlFor,
|
|
48
|
+
encodeProjectPath,
|
|
49
|
+
isNonRelativeRef,
|
|
50
|
+
joinProjectPath,
|
|
51
|
+
projectPathForRef,
|
|
52
|
+
splitRefSuffix,
|
|
53
|
+
} from "@jxsuite/schema/asset-paths";
|
|
54
|
+
import type { AssetLane, AssetMount } from "@jxsuite/schema/asset-paths";
|
|
55
|
+
import type { ContentSectionEntry } from "../types";
|
|
56
|
+
|
|
57
|
+
/** Content type names safe in a URL path segment. Mirrors `SAFE_TYPE_NAME` in the content loader. */
|
|
58
|
+
const SAFE_TYPE_NAME = /^[\w.~-]+$/;
|
|
59
|
+
|
|
60
|
+
/** The project.json section key content types live under; also their URL namespace. */
|
|
61
|
+
const SECTION_KEY = "content";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The placeholder a content type's `source` may carry to say "one directory per locale".
|
|
65
|
+
*
|
|
66
|
+
* Held here rather than imported out of the parser extension's content loader ({@link
|
|
67
|
+
* file://../../../../extensions/parser/src/content-loader.ts}): that module reads the filesystem
|
|
68
|
+
* and this one runs in a browser. It is also an extension, and core may not import one.
|
|
69
|
+
*/
|
|
70
|
+
const LOCALE_PLACEHOLDER = "{locale}";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Everything the canvas needs to resolve one reference, as PLAIN DATA.
|
|
74
|
+
*
|
|
75
|
+
* Plain data because it has to cross a realm. The resolver itself is a function and cannot be
|
|
76
|
+
* posted into the canvas iframe, so the parent posts this beside the render document and the iframe
|
|
77
|
+
* builds the resolver from it locally.
|
|
78
|
+
*/
|
|
79
|
+
export interface AssetContext {
|
|
80
|
+
/** What the canvas ORIGIN answers for a site URL — the platform's `assetSpace`, defaulted. */
|
|
81
|
+
space: "site" | "repo";
|
|
82
|
+
/**
|
|
83
|
+
* Base URL under which the host serves PROJECT PATHS. Required by `"repo"` and inert without it:
|
|
84
|
+
* a host that declares repo space and no base has told us its site URLs are wrong without telling
|
|
85
|
+
* us what is right, and inventing an answer there would be worse than leaving the ref alone.
|
|
86
|
+
*/
|
|
87
|
+
fileBaseUrl?: string | undefined;
|
|
88
|
+
/** Project-relative directory of the document being rendered (`""` at the project root). */
|
|
89
|
+
documentDir: string;
|
|
90
|
+
/** Project-relative asset mounts that apply to this document. */
|
|
91
|
+
mounts: AssetMount[];
|
|
92
|
+
/**
|
|
93
|
+
* The resolution order for a site-absolute URL.
|
|
94
|
+
*
|
|
95
|
+
* `BUILD_LANES` by default, and that is the deliberate answer rather than an omission: with no
|
|
96
|
+
* filesystem to probe, a lane list has to collapse to ONE candidate, and the one that makes the
|
|
97
|
+
* preview agree with the deployed site is the build's. See `DEV_SERVER_LANES` for the order the
|
|
98
|
+
* editing servers actually use, and why the two differ.
|
|
99
|
+
*/
|
|
100
|
+
lanes: readonly AssetLane[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The asset mount for the content type that owns `documentPath`, or null when the document is not a
|
|
105
|
+
* directory-backed content entry.
|
|
106
|
+
*
|
|
107
|
+
* Only in-project sources can match: an entry Studio can address project-relatively is by
|
|
108
|
+
* definition inside the project, and a source pointing elsewhere (`../../docs`) names files the
|
|
109
|
+
* file tree cannot reach in the first place.
|
|
110
|
+
*
|
|
111
|
+
* @param {string | null | undefined} documentPath - Project-relative path of the open document
|
|
112
|
+
* @param {Record<string, ContentSectionEntry> | null | undefined} content - Project.json `content`
|
|
113
|
+
* @param {readonly string[]} [locales] - Canonical locale tags, for `{locale}` sources
|
|
114
|
+
* @returns {AssetMount | null} The mount, project-relative
|
|
115
|
+
*/
|
|
116
|
+
export function contentMountFor(
|
|
117
|
+
documentPath: string | null | undefined,
|
|
118
|
+
content: Record<string, ContentSectionEntry> | null | undefined,
|
|
119
|
+
locales: readonly string[] = [],
|
|
120
|
+
): AssetMount | null {
|
|
121
|
+
if (!documentPath || !content) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const path = documentPath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
125
|
+
let best: AssetMount | null = null;
|
|
126
|
+
const consider = (dir: string, urlPrefix: string): void => {
|
|
127
|
+
if (path.startsWith(`${dir}/`) && (!best || dir.length > best.dir.length)) {
|
|
128
|
+
best = { dir, urlPrefix };
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
for (const [name, def] of Object.entries(content)) {
|
|
132
|
+
if (!def?.source || !SAFE_TYPE_NAME.test(name)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const dir = normalizeSource(def.source);
|
|
136
|
+
// A single-file source is not a collection directory — its siblings are not its assets.
|
|
137
|
+
if (dir === "" || dir.startsWith("..")) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (dir.includes(LOCALE_PLACEHOLDER)) {
|
|
141
|
+
const mount = localeMountFor(path, dir, name, locales);
|
|
142
|
+
if (mount) {
|
|
143
|
+
consider(mount.dir, mount.urlPrefix);
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (/\.[a-z0-9]+$/i.test(dir)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
consider(dir, `/${SECTION_KEY}/${name}`);
|
|
151
|
+
}
|
|
152
|
+
return best;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The mount for ONE locale of a `{locale}` source — the locale being whichever one `path` is in.
|
|
157
|
+
*
|
|
158
|
+
* A localized content type is N directories, not N content types, and each publishes separately at
|
|
159
|
+
* `/content/<type>/<locale>` so a French post's `./hero.png` and its English translation's cannot
|
|
160
|
+
* collide at one URL. The content loader picks the directory by probing the disk (`localeSource`);
|
|
161
|
+
* Studio has no disk, so it reads the locale out of the document path it was given and checks that
|
|
162
|
+
* against the project's declared tags. That is not a weaker test — the path IS the file, so the
|
|
163
|
+
* only question left is whether its directory is a locale the project declares.
|
|
164
|
+
*
|
|
165
|
+
* Matched case-insensitively, for the same reason the loader does: two writers name that directory
|
|
166
|
+
* and they differ. A project that declared `fr-CA` most likely typed `fr-CA/`, while Studio creates
|
|
167
|
+
* `fr-ca/` because a page directory becomes a URL and the site's URLs are lowercase. The mount's
|
|
168
|
+
* `dir` is the spelling on disk and its `urlPrefix` is the CANONICAL tag, exactly as the loader
|
|
169
|
+
* publishes it.
|
|
170
|
+
*/
|
|
171
|
+
function localeMountFor(
|
|
172
|
+
path: string,
|
|
173
|
+
source: string,
|
|
174
|
+
name: string,
|
|
175
|
+
locales: readonly string[],
|
|
176
|
+
): AssetMount | null {
|
|
177
|
+
const cut = source.indexOf(LOCALE_PLACEHOLDER);
|
|
178
|
+
const head = source.slice(0, cut);
|
|
179
|
+
const tail = source.slice(cut + LOCALE_PLACEHOLDER.length);
|
|
180
|
+
if (!path.startsWith(head)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const segment = path.slice(head.length).split("/")[0] ?? "";
|
|
184
|
+
if (segment === "" || !SAFE_TYPE_NAME.test(segment)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const canonical = locales.find((tag) => tag.toLowerCase() === segment.toLowerCase());
|
|
188
|
+
if (canonical === undefined || !SAFE_TYPE_NAME.test(canonical)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const dir = `${head}${segment}${tail}`.replace(/\/+$/, "");
|
|
192
|
+
return { dir, urlPrefix: `/${SECTION_KEY}/${name}/${canonical}` };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Strip `./` and any trailing slash from a configured `source`, the way collectionDirs does. */
|
|
196
|
+
function normalizeSource(source: string): string {
|
|
197
|
+
return source.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The URL the canvas should load for one authored reference, or null to leave it as written.
|
|
202
|
+
*
|
|
203
|
+
* Null is not a failure — it is most of the answers. An absolute URL, a `data:` URI, a `${…}`
|
|
204
|
+
* template that did not resolve to a project file, a `../` escape out of the collection: each one
|
|
205
|
+
* already means what it says, and changing it would be the bug.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} value - The reference as the author wrote it
|
|
208
|
+
* @param {AssetContext | null} ctx - The host's asset context, or null for no resolution at all
|
|
209
|
+
* @returns {string | null} The URL to load, or null to leave `value` untouched
|
|
210
|
+
*/
|
|
211
|
+
export function resolveAssetRef(value: string, ctx: AssetContext | null): string | null {
|
|
212
|
+
if (!ctx || value === "") {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (ctx.space === "repo") {
|
|
216
|
+
if (!ctx.fileBaseUrl) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const path = projectPathForRef(value, ctx.documentDir, ctx.mounts, ctx.lanes);
|
|
220
|
+
if (path === null) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const { suffix } = splitRefSuffix(value);
|
|
224
|
+
return `${ctx.fileBaseUrl}${encodeProjectPath(path)}${suffix}`;
|
|
225
|
+
}
|
|
226
|
+
const [mount] = ctx.mounts;
|
|
227
|
+
return mount ? mountedRefFor(value, ctx.documentDir, mount) : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The mounted URL for one content-relative reference, or null to leave it as written. A ref is
|
|
232
|
+
* rewritten only when it is relative and lands inside the collection directory — anything else (an
|
|
233
|
+
* absolute URL, a project-root path, a bound template, a `../` escape out of the collection) keeps
|
|
234
|
+
* the meaning it already had.
|
|
235
|
+
*/
|
|
236
|
+
export function mountedRefFor(value: string, entryDir: string, mount: AssetMount): string | null {
|
|
237
|
+
if (isNonRelativeRef(value)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const { path: bare, suffix } = splitRefSuffix(value);
|
|
241
|
+
let decoded: string;
|
|
242
|
+
try {
|
|
243
|
+
decoded = decodeURIComponent(bare);
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const target = joinProjectPath(entryDir, decoded);
|
|
248
|
+
if (target === null) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const url = assetUrlFor([mount], target);
|
|
252
|
+
if (!url) {
|
|
253
|
+
return null; // Outside the collection — not the mount's to publish.
|
|
254
|
+
}
|
|
255
|
+
return `${url}${suffix}`;
|
|
256
|
+
}
|