@1agh/maude 0.42.0 → 0.44.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -375,7 +375,8 @@ export function applyRemove(
|
|
|
375
375
|
export async function editText(
|
|
376
376
|
canvasAbsPath: string,
|
|
377
377
|
id: string,
|
|
378
|
-
text: string
|
|
378
|
+
text: string,
|
|
379
|
+
opts?: DynamicTextOpts
|
|
379
380
|
): Promise<EditResult> {
|
|
380
381
|
return withLock(canvasAbsPath, async () => {
|
|
381
382
|
const file = Bun.file(canvasAbsPath);
|
|
@@ -386,7 +387,16 @@ export async function editText(
|
|
|
386
387
|
});
|
|
387
388
|
}
|
|
388
389
|
const source = await file.text();
|
|
389
|
-
|
|
390
|
+
// Byte cap before the AST walk (ethical-hacker F-C) — a real canvas is well
|
|
391
|
+
// under this; a pathologically large attacker-authored file that would make
|
|
392
|
+
// the resolver's per-candidate walk expensive is refused up front.
|
|
393
|
+
if (source.length > 4_000_000) {
|
|
394
|
+
throw new CanvasEditError(`canvas too large to edit inline (${source.length} bytes)`, {
|
|
395
|
+
canvas: canvasAbsPath,
|
|
396
|
+
id,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const next = applyTextEdit(canvasAbsPath, source, id, text, opts);
|
|
390
400
|
if (next.source === source) return { source, delta: 0, changed: false };
|
|
391
401
|
const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
|
|
392
402
|
await Bun.write(tmp, next.source);
|
|
@@ -455,17 +465,35 @@ export function applyEdit(
|
|
|
455
465
|
return { source: out, delta: out.length - source.length };
|
|
456
466
|
}
|
|
457
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Extra context for editing text that comes from a `{variable}` rather than a
|
|
470
|
+
* literal (unified-text-editing follow-up). `occurrence` = which rendered
|
|
471
|
+
* instance the user edited (index among DOM nodes carrying this same cd-id — a
|
|
472
|
+
* `.map()` renders one source element N×); `before` = the pre-edit rendered
|
|
473
|
+
* text, used both to pick the right `.map()` item when the index drifts (a
|
|
474
|
+
* `.filter().map()` etc.) and to refuse a rewrite we can't confidently target.
|
|
475
|
+
*/
|
|
476
|
+
export interface DynamicTextOpts {
|
|
477
|
+
occurrence?: number;
|
|
478
|
+
before?: string;
|
|
479
|
+
}
|
|
480
|
+
|
|
458
481
|
/**
|
|
459
482
|
* Pure variant of `editText` — parse, locate the JSXText child, overwrite its
|
|
460
483
|
* source span (preserving the original leading/trailing whitespace so JSX
|
|
461
|
-
* indentation survives), escaping the new text.
|
|
462
|
-
*
|
|
484
|
+
* indentation survives), escaping the new text. A single `{'literal'}` child is
|
|
485
|
+
* rewritten in place (DDR-150 P1). A single `{variable}` / `{item.prop}` child
|
|
486
|
+
* is resolved back to its source string (a local `const` or a `.map()`ed array
|
|
487
|
+
* element) when `opts` carries enough to target it unambiguously — otherwise it
|
|
488
|
+
* throws `CanvasEditError` (genuinely dynamic → route to /design:edit), same as
|
|
489
|
+
* mixed content.
|
|
463
490
|
*/
|
|
464
491
|
export function applyTextEdit(
|
|
465
492
|
canvasAbsPath: string,
|
|
466
493
|
source: string,
|
|
467
494
|
id: string,
|
|
468
|
-
text: string
|
|
495
|
+
text: string,
|
|
496
|
+
opts?: DynamicTextOpts
|
|
469
497
|
): EditResult {
|
|
470
498
|
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
471
499
|
if (parsed.errors && parsed.errors.length > 0) {
|
|
@@ -507,16 +535,24 @@ export function applyTextEdit(
|
|
|
507
535
|
// dynamic: refuse and route to /design:edit rather than delete the binding.
|
|
508
536
|
if (meaningful.length === 1 && only?.type === 'JSXExpressionContainer') {
|
|
509
537
|
const expr = (only as AnyNode).expression;
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
(expr.type === 'Literal' || expr.type === 'StringLiteral') &&
|
|
513
|
-
typeof expr.value === 'string'
|
|
514
|
-
) {
|
|
538
|
+
// A single `{'string literal'}` — rewrite the literal in place (DDR-150 P1).
|
|
539
|
+
if (isStringLit(expr)) {
|
|
515
540
|
const s = new MagicString(source);
|
|
516
541
|
s.overwrite(expr.start as number, expr.end as number, JSON.stringify(text));
|
|
517
542
|
const out = s.toString();
|
|
518
543
|
return { source: out, delta: out.length - source.length };
|
|
519
544
|
}
|
|
545
|
+
// A single `{variable}` / `{item.prop}` — trace it back to its source string
|
|
546
|
+
// (a local const, or a `.map()`ed array element) and rewrite THERE. The
|
|
547
|
+
// literal that comes back is a JS string, so it round-trips through
|
|
548
|
+
// JSON.stringify like the `{'literal'}` case above (no JSX-entity surface).
|
|
549
|
+
const span = resolveDynamicTextSpan(parsed.program, hit.element, expr, id, opts);
|
|
550
|
+
if (span) {
|
|
551
|
+
const s = new MagicString(source);
|
|
552
|
+
s.overwrite(span.start as number, span.end as number, JSON.stringify(text));
|
|
553
|
+
const out = s.toString();
|
|
554
|
+
return { source: out, delta: out.length - source.length };
|
|
555
|
+
}
|
|
520
556
|
throw new CanvasEditError(`element "${id}" has dynamic content — edit it via /design:edit`, {
|
|
521
557
|
canvas: canvasAbsPath,
|
|
522
558
|
id,
|
|
@@ -541,6 +577,287 @@ export function applyTextEdit(
|
|
|
541
577
|
return { source: out, delta: out.length - source.length };
|
|
542
578
|
}
|
|
543
579
|
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// Dynamic-text resolution (unified-text-editing follow-up). A `{variable}` /
|
|
582
|
+
// `{item.prop}` text child has no literal to rewrite at the element — the
|
|
583
|
+
// string lives in a `const` or a `.map()`ed data array. These helpers trace it
|
|
584
|
+
// back to that source StringLiteral so inline editing works there too, WITHOUT
|
|
585
|
+
// ever risking the wrong rewrite: the occurrence index picks the `.map()` item,
|
|
586
|
+
// and the pre-edit text (`before`) both verifies that pick and rescues it when
|
|
587
|
+
// the index drifts (`.filter().map()`, reorders). Anything we can't target
|
|
588
|
+
// unambiguously returns null → the caller throws → routes to /design:edit.
|
|
589
|
+
|
|
590
|
+
function isStringLit(n: AnyNode): boolean {
|
|
591
|
+
return !!(
|
|
592
|
+
n &&
|
|
593
|
+
(n.type === 'Literal' || n.type === 'StringLiteral') &&
|
|
594
|
+
typeof n.value === 'string'
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Depth-first visit every AST node (skips location metadata keys). */
|
|
599
|
+
function walkAst(root: AnyNode, fn: (node: AnyNode) => void): void {
|
|
600
|
+
function visit(node: AnyNode): void {
|
|
601
|
+
if (!node || typeof node !== 'object') return;
|
|
602
|
+
if (Array.isArray(node)) {
|
|
603
|
+
for (const c of node) visit(c);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (typeof node.type !== 'string') return;
|
|
607
|
+
fn(node);
|
|
608
|
+
for (const k of Object.keys(node)) {
|
|
609
|
+
if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
|
|
610
|
+
visit(node[k]);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
visit(root);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** The innermost `xs.map((param) => …)` whose callback param is `paramName`
|
|
617
|
+
* and whose body byte-range encloses `element`. Returns the mapped array
|
|
618
|
+
* expression (an Identifier or an inline ArrayExpression), or null. */
|
|
619
|
+
function findEnclosingMapArray(
|
|
620
|
+
program: AnyNode,
|
|
621
|
+
element: AnyNode,
|
|
622
|
+
paramName: string
|
|
623
|
+
): AnyNode | null {
|
|
624
|
+
const es = element.start as number;
|
|
625
|
+
const ee = element.end as number;
|
|
626
|
+
let best: AnyNode | null = null;
|
|
627
|
+
let bestSize = Number.POSITIVE_INFINITY;
|
|
628
|
+
walkAst(program, (node) => {
|
|
629
|
+
if (node.type !== 'CallExpression') return;
|
|
630
|
+
const callee = node.callee;
|
|
631
|
+
if (callee?.type !== 'MemberExpression' || callee.computed) return;
|
|
632
|
+
if (callee.property?.name !== 'map') return;
|
|
633
|
+
const cb = node.arguments?.[0];
|
|
634
|
+
if (cb?.type !== 'ArrowFunctionExpression' && cb?.type !== 'FunctionExpression') return;
|
|
635
|
+
const p0 = cb.params?.[0];
|
|
636
|
+
if (p0?.type !== 'Identifier' || p0.name !== paramName) return;
|
|
637
|
+
const body = cb.body;
|
|
638
|
+
if (typeof body?.start !== 'number' || typeof body?.end !== 'number') return;
|
|
639
|
+
if (!(body.start <= es && ee <= body.end)) return;
|
|
640
|
+
const size = (body.end as number) - (body.start as number);
|
|
641
|
+
if (size < bestSize) {
|
|
642
|
+
bestSize = size;
|
|
643
|
+
best = callee.object;
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
return best;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Resolve an array expression (an inline `[…]` or an Identifier bound to a
|
|
650
|
+
* `const xs = […]`) to its ArrayExpression node. */
|
|
651
|
+
function resolveArrayExpr(program: AnyNode, arrayExpr: AnyNode): AnyNode | null {
|
|
652
|
+
if (arrayExpr?.type === 'ArrayExpression') return arrayExpr;
|
|
653
|
+
if (arrayExpr?.type === 'Identifier') {
|
|
654
|
+
let found: AnyNode | null = null;
|
|
655
|
+
walkAst(program, (node) => {
|
|
656
|
+
if (
|
|
657
|
+
node.type === 'VariableDeclarator' &&
|
|
658
|
+
node.id?.type === 'Identifier' &&
|
|
659
|
+
node.id.name === arrayExpr.name &&
|
|
660
|
+
node.init?.type === 'ArrayExpression'
|
|
661
|
+
) {
|
|
662
|
+
found = node.init;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
return found;
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** The value node of property `propName` on an ObjectExpression, or null. */
|
|
671
|
+
function objPropValue(objExpr: AnyNode, propName: string): AnyNode | null {
|
|
672
|
+
if (objExpr?.type !== 'ObjectExpression') return null;
|
|
673
|
+
for (const p of objExpr.properties ?? []) {
|
|
674
|
+
if (p?.type !== 'Property' || p.computed) continue;
|
|
675
|
+
const keyOk =
|
|
676
|
+
(p.key?.type === 'Identifier' && p.key.name === propName) ||
|
|
677
|
+
(isStringLit(p.key) && p.key.value === propName);
|
|
678
|
+
if (keyOk) return p.value ?? null;
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Bounded expression evaluator — resolve `expr` to a concrete value node
|
|
685
|
+
* (a StringLiteral, ObjectExpression, or ArrayExpression) by following const
|
|
686
|
+
* bindings, numeric array indices (`XS[0]`), and object-field access
|
|
687
|
+
* (`obj.field`). Returns null for anything genuinely computed (calls, template
|
|
688
|
+
* strings, arithmetic). Depth-capped so a cyclic const can't loop.
|
|
689
|
+
*/
|
|
690
|
+
function resolveValueNode(program: AnyNode, expr: AnyNode, depth = 0): AnyNode | null {
|
|
691
|
+
if (!expr || depth > 8) return null;
|
|
692
|
+
if (isStringLit(expr)) return expr;
|
|
693
|
+
if (expr.type === 'ObjectExpression' || expr.type === 'ArrayExpression') return expr;
|
|
694
|
+
if (expr.type === 'Identifier') {
|
|
695
|
+
let init: AnyNode | null = null;
|
|
696
|
+
walkAst(program, (n) => {
|
|
697
|
+
if (
|
|
698
|
+
n.type === 'VariableDeclarator' &&
|
|
699
|
+
n.id?.type === 'Identifier' &&
|
|
700
|
+
n.id.name === expr.name &&
|
|
701
|
+
n.init
|
|
702
|
+
) {
|
|
703
|
+
init = n.init;
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return init ? resolveValueNode(program, init, depth + 1) : null;
|
|
707
|
+
}
|
|
708
|
+
if (expr.type === 'MemberExpression') {
|
|
709
|
+
const obj = resolveValueNode(program, expr.object, depth + 1);
|
|
710
|
+
if (!obj) return null;
|
|
711
|
+
if (expr.computed) {
|
|
712
|
+
const idx = expr.property;
|
|
713
|
+
if (
|
|
714
|
+
(idx?.type === 'Literal' || idx?.type === 'NumericLiteral') &&
|
|
715
|
+
typeof idx.value === 'number' &&
|
|
716
|
+
obj.type === 'ArrayExpression'
|
|
717
|
+
) {
|
|
718
|
+
return resolveValueNode(program, obj.elements?.[idx.value], depth + 1);
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
if (expr.property?.type === 'Identifier') {
|
|
723
|
+
return resolveValueNode(program, objPropValue(obj, expr.property.name), depth + 1);
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Ordered value-expressions of a `<CompName propName={…}>` usage, one per
|
|
731
|
+
* usage in source order (aligns with the DOM occurrence of the element the
|
|
732
|
+
* prop feeds). null slot = a usage that omits the prop. */
|
|
733
|
+
function componentUsageValues(
|
|
734
|
+
program: AnyNode,
|
|
735
|
+
componentName: string,
|
|
736
|
+
propName: string
|
|
737
|
+
): Array<AnyNode | null> | null {
|
|
738
|
+
if (!componentName) return null;
|
|
739
|
+
const out: Array<AnyNode | null> = [];
|
|
740
|
+
walkAst(program, (node) => {
|
|
741
|
+
if (node.type !== 'JSXElement') return;
|
|
742
|
+
const name = node.openingElement?.name;
|
|
743
|
+
if (name?.type !== 'JSXIdentifier' || name.name !== componentName) return;
|
|
744
|
+
let val: AnyNode | null = null;
|
|
745
|
+
for (const a of node.openingElement?.attributes ?? []) {
|
|
746
|
+
if (
|
|
747
|
+
a?.type === 'JSXAttribute' &&
|
|
748
|
+
a.name?.type === 'JSXIdentifier' &&
|
|
749
|
+
a.name.name === propName
|
|
750
|
+
) {
|
|
751
|
+
val =
|
|
752
|
+
a.value?.type === 'JSXExpressionContainer'
|
|
753
|
+
? a.value.expression
|
|
754
|
+
: isStringLit(a.value)
|
|
755
|
+
? a.value
|
|
756
|
+
: null;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
out.push(val);
|
|
761
|
+
});
|
|
762
|
+
return out.length ? out : null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Trace a single `{base}` / `{base.field}` text expression back to the
|
|
767
|
+
* StringLiteral node that holds its text, or null when it can't be targeted
|
|
768
|
+
* confidently. `base` is resolved through three bindings — a `.map()` callback
|
|
769
|
+
* param (array items), a component PROP (each `<Comp prop={…}>` usage), or a
|
|
770
|
+
* local `const` — and each candidate is run through the bounded evaluator
|
|
771
|
+
* (`beat.caption` where `beat={BEATS[0]}` → `BEATS[0].caption`). The occurrence
|
|
772
|
+
* index picks the slot; `before` verifies it and rescues a drifted index; ties
|
|
773
|
+
* are never guessed. `occurrence`/`before` come from the edited DOM instance.
|
|
774
|
+
*/
|
|
775
|
+
function resolveDynamicTextSpan(
|
|
776
|
+
program: AnyNode,
|
|
777
|
+
element: AnyNode,
|
|
778
|
+
expr: AnyNode,
|
|
779
|
+
id: string,
|
|
780
|
+
opts?: DynamicTextOpts
|
|
781
|
+
): AnyNode | null {
|
|
782
|
+
// Decompose into base Identifier + optional single field.
|
|
783
|
+
let baseName: string;
|
|
784
|
+
let field: string | null;
|
|
785
|
+
if (expr?.type === 'Identifier') {
|
|
786
|
+
baseName = expr.name;
|
|
787
|
+
field = null;
|
|
788
|
+
} else if (
|
|
789
|
+
expr?.type === 'MemberExpression' &&
|
|
790
|
+
!expr.computed &&
|
|
791
|
+
expr.object?.type === 'Identifier' &&
|
|
792
|
+
expr.property?.type === 'Identifier'
|
|
793
|
+
) {
|
|
794
|
+
baseName = expr.object.name;
|
|
795
|
+
field = expr.property.name;
|
|
796
|
+
} else {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Candidate value-expressions for `base`, one per rendered slot.
|
|
801
|
+
let candidates: Array<AnyNode | null> | null = null;
|
|
802
|
+
// (1) a `.map()` callback param → the array items.
|
|
803
|
+
const arrExpr = findEnclosingMapArray(program, element, baseName);
|
|
804
|
+
if (arrExpr) {
|
|
805
|
+
const arr = resolveArrayExpr(program, arrExpr);
|
|
806
|
+
candidates = arr ? (arr.elements ?? []) : null;
|
|
807
|
+
}
|
|
808
|
+
// (2) a component prop → each `<Comp base={…}>` usage's value.
|
|
809
|
+
if (!candidates) {
|
|
810
|
+
const componentName =
|
|
811
|
+
collectElementsFull(program).find((e) => e.id === id)?.componentName ?? '';
|
|
812
|
+
candidates = componentUsageValues(program, componentName, baseName);
|
|
813
|
+
}
|
|
814
|
+
// (3) a local const → a single candidate.
|
|
815
|
+
if (!candidates) {
|
|
816
|
+
let init: AnyNode | null = null;
|
|
817
|
+
walkAst(program, (n) => {
|
|
818
|
+
if (
|
|
819
|
+
n.type === 'VariableDeclarator' &&
|
|
820
|
+
n.id?.type === 'Identifier' &&
|
|
821
|
+
n.id.name === baseName
|
|
822
|
+
) {
|
|
823
|
+
init = n.init ?? null;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
candidates = init ? [init] : null;
|
|
827
|
+
}
|
|
828
|
+
if (!candidates) return null;
|
|
829
|
+
// Complexity cap (ethical-hacker F-C): each candidate runs a full-program
|
|
830
|
+
// walkAst per resolution step, so an attacker-authored canvas with a huge
|
|
831
|
+
// array of `{identifier}` items could make this O(N × program). A real
|
|
832
|
+
// `.map()`/usage list is tiny; anything past the cap is refused (a single
|
|
833
|
+
// edit couldn't confidently target one of thousands of slots anyway).
|
|
834
|
+
const MAX_CANDIDATES = 1000;
|
|
835
|
+
if (candidates.length > MAX_CANDIDATES) return null;
|
|
836
|
+
|
|
837
|
+
// Resolve each candidate to a StringLiteral (applying `.field` when present).
|
|
838
|
+
const lits: Array<AnyNode | null> = candidates.map((c) => {
|
|
839
|
+
const base = resolveValueNode(program, c);
|
|
840
|
+
if (!base) return null;
|
|
841
|
+
if (field) {
|
|
842
|
+
const v = resolveValueNode(program, objPropValue(base, field));
|
|
843
|
+
return isStringLit(v) ? v : null;
|
|
844
|
+
}
|
|
845
|
+
return isStringLit(base) ? base : null;
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// Pick: occurrence index (verified by `before`), else the UNIQUE `before`
|
|
849
|
+
// match, else — only with no `before` to verify — the raw index. No guessing.
|
|
850
|
+
const beforeTrim = (opts?.before ?? '').trim();
|
|
851
|
+
const occ = typeof opts?.occurrence === 'number' && opts.occurrence >= 0 ? opts.occurrence : null;
|
|
852
|
+
const at = occ != null ? lits[occ] : null;
|
|
853
|
+
if (at && (!beforeTrim || String(at.value).trim() === beforeTrim)) return at;
|
|
854
|
+
if (beforeTrim) {
|
|
855
|
+
const hits = lits.filter((c): c is AnyNode => !!c && String(c.value).trim() === beforeTrim);
|
|
856
|
+
return hits.length === 1 ? (hits[0] ?? null) : null;
|
|
857
|
+
}
|
|
858
|
+
return at ?? null;
|
|
859
|
+
}
|
|
860
|
+
|
|
544
861
|
// ---------------------------------------------------------------------------
|
|
545
862
|
// Node-move reorder (DDR-138, phase-12.1). Moving a whole JSXElement to a new
|
|
546
863
|
// sibling/parent position — the one structural edit `editAttribute`/`editText`
|
|
@@ -2842,6 +3159,138 @@ export async function insertElement(
|
|
|
2842
3159
|
});
|
|
2843
3160
|
}
|
|
2844
3161
|
|
|
3162
|
+
/**
|
|
3163
|
+
* Insert a synthesized element as a child of the `<DCArtboard id="…">` itself —
|
|
3164
|
+
* the empty-artboard fallback for the tool-palette "+ Element" affordance
|
|
3165
|
+
* (Stage I3 tail addendum). `applyInsertElement` needs a sibling `data-cd-id`
|
|
3166
|
+
* to anchor on; a freshly-scaffolded or fully-cleared artboard has none, so
|
|
3167
|
+
* this variant anchors on the DCArtboard JSX node itself (addressed by its
|
|
3168
|
+
* `id` PROP, same convention as `applyResizeArtboard`/`applyDeleteArtboard` —
|
|
3169
|
+
* an artboard's rendered `<article data-dc-screen>` carries no `data-cd-id`).
|
|
3170
|
+
* Only `inside-start`/`inside-end` make sense here (no sibling to be
|
|
3171
|
+
* before/after). Pure; reparse-gated like `applyInsertElement`.
|
|
3172
|
+
*/
|
|
3173
|
+
export function applyInsertElementIntoArtboard(
|
|
3174
|
+
canvasAbsPath: string,
|
|
3175
|
+
source: string,
|
|
3176
|
+
artboardId: string,
|
|
3177
|
+
position: 'inside-start' | 'inside-end',
|
|
3178
|
+
kind: InsertKind,
|
|
3179
|
+
opts?: { src?: string }
|
|
3180
|
+
): { source: string; newId: string | null } {
|
|
3181
|
+
if (kind === 'image') {
|
|
3182
|
+
if (!opts?.src?.trim()) {
|
|
3183
|
+
throw new CanvasEditError('insert image requires a contained asset src (assets/…)', {
|
|
3184
|
+
canvas: canvasAbsPath,
|
|
3185
|
+
id: artboardId,
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
assertContainedAssetSrc(opts.src, canvasAbsPath);
|
|
3189
|
+
}
|
|
3190
|
+
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
3191
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
3192
|
+
throw new CanvasEditError(
|
|
3193
|
+
`oxc-parser failed on ${canvasAbsPath}: ${parsed.errors[0]?.message ?? 'unknown'}`,
|
|
3194
|
+
{ canvas: canvasAbsPath, id: artboardId }
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
const artboards = collectJsxByTag(parsed.program, 'DCArtboard');
|
|
3198
|
+
const target = artboards.find((a) => getStringAttr(a.openingElement, 'id') === artboardId);
|
|
3199
|
+
if (!target) {
|
|
3200
|
+
throw new CanvasEditError(`<DCArtboard id="${artboardId}"> not found in ${canvasAbsPath}`, {
|
|
3201
|
+
canvas: canvasAbsPath,
|
|
3202
|
+
id: artboardId,
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
if (target.openingElement?.selfClosing || !target.closingElement) {
|
|
3206
|
+
throw new CanvasEditError(
|
|
3207
|
+
`<DCArtboard id="${artboardId}"> is self-closing — cannot nest an element inside it`,
|
|
3208
|
+
{ canvas: canvasAbsPath, id: artboardId }
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
const rStart = target.start as number;
|
|
3213
|
+
const indentUnit = detectIndentUnit(source);
|
|
3214
|
+
const newText = synthInsertElement(kind, opts?.src);
|
|
3215
|
+
const targetIndent = lineStartInfo(source, rStart).indent + indentUnit;
|
|
3216
|
+
|
|
3217
|
+
let anchor: number;
|
|
3218
|
+
let insertText: string;
|
|
3219
|
+
if (position === 'inside-start') {
|
|
3220
|
+
anchor = target.openingElement.end as number;
|
|
3221
|
+
insertText = `\n${targetIndent}${newText}`;
|
|
3222
|
+
} else {
|
|
3223
|
+
const cStart = target.closingElement.start as number;
|
|
3224
|
+
const cLine = lineStartInfo(source, cStart);
|
|
3225
|
+
if (cLine.newlineBefore) {
|
|
3226
|
+
anchor = cLine.indentStart - 1;
|
|
3227
|
+
insertText = `\n${targetIndent}${newText}`;
|
|
3228
|
+
} else {
|
|
3229
|
+
anchor = cStart;
|
|
3230
|
+
insertText = `${newText}`;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
const s = new MagicString(source);
|
|
3235
|
+
s.appendLeft(anchor, insertText);
|
|
3236
|
+
const out = s.toString();
|
|
3237
|
+
const check = parseSync(canvasAbsPath, out, { sourceType: 'module' });
|
|
3238
|
+
if (check.errors && check.errors.length > 0) {
|
|
3239
|
+
throw new CanvasEditError(
|
|
3240
|
+
`insert would produce invalid source (${check.errors[0]?.message ?? 'parse error'}); aborted`,
|
|
3241
|
+
{ canvas: canvasAbsPath, id: artboardId }
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
// Same post-insert id recomputation as applyInsertElement — single append,
|
|
3245
|
+
// no removes, so the new element's `<` lands at this exact offset in `out`.
|
|
3246
|
+
const prefixLen = insertText.indexOf(newText);
|
|
3247
|
+
const elemStart = anchor + prefixLen;
|
|
3248
|
+
let newId: string | null = null;
|
|
3249
|
+
let fallback: string | null = null;
|
|
3250
|
+
for (const { id: eid, node } of collectElements(check.program)) {
|
|
3251
|
+
if ((node.start as number) === elemStart) {
|
|
3252
|
+
newId = eid;
|
|
3253
|
+
break;
|
|
3254
|
+
}
|
|
3255
|
+
if (out.slice(node.start as number, node.end as number) === newText) fallback = eid;
|
|
3256
|
+
}
|
|
3257
|
+
return { source: out, newId: newId ?? fallback };
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
/** Insert an element into an artboard by artboardId (no ref sibling) on disk. */
|
|
3261
|
+
export async function insertElementIntoArtboard(
|
|
3262
|
+
canvasAbsPath: string,
|
|
3263
|
+
artboardId: string,
|
|
3264
|
+
position: 'inside-start' | 'inside-end',
|
|
3265
|
+
kind: InsertKind,
|
|
3266
|
+
opts?: { src?: string }
|
|
3267
|
+
): Promise<{ source: string; newId: string | null }> {
|
|
3268
|
+
return withLock(canvasAbsPath, async () => {
|
|
3269
|
+
const file = Bun.file(canvasAbsPath);
|
|
3270
|
+
if (!(await file.exists())) {
|
|
3271
|
+
throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
|
|
3272
|
+
canvas: canvasAbsPath,
|
|
3273
|
+
id: artboardId,
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
const source = await file.text();
|
|
3277
|
+
const next = applyInsertElementIntoArtboard(
|
|
3278
|
+
canvasAbsPath,
|
|
3279
|
+
source,
|
|
3280
|
+
artboardId,
|
|
3281
|
+
position,
|
|
3282
|
+
kind,
|
|
3283
|
+
opts
|
|
3284
|
+
);
|
|
3285
|
+
if (next.source === source) return next;
|
|
3286
|
+
const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
|
|
3287
|
+
await Bun.write(tmp, next.source);
|
|
3288
|
+
const { rename } = await import('node:fs/promises');
|
|
3289
|
+
await rename(tmp, canvasAbsPath);
|
|
3290
|
+
return next;
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
|
|
2845
3294
|
/**
|
|
2846
3295
|
* Duplicate the element with `data-cd-id === id` — insert a verbatim copy of its
|
|
2847
3296
|
* source as the next sibling (Cmd+D, Task L3). Since the SOURCE carries no
|
|
@@ -2957,8 +3406,14 @@ function collectJsxByTag(program: AnyNode, tagName: string): AnyNode[] {
|
|
|
2957
3406
|
* `width="430"` and break the numeric prop. Overwrites an existing `{expr}` or
|
|
2958
3407
|
* `"literal"`, or inserts the attribute after the tag name if missing.
|
|
2959
3408
|
*/
|
|
2960
|
-
function writeNumericAttr(
|
|
2961
|
-
|
|
3409
|
+
function writeNumericAttr(
|
|
3410
|
+
s: MagicString,
|
|
3411
|
+
opening: AnyNode,
|
|
3412
|
+
name: string,
|
|
3413
|
+
value: number,
|
|
3414
|
+
min = 1
|
|
3415
|
+
): void {
|
|
3416
|
+
const num = Math.max(min, Math.round(value));
|
|
2962
3417
|
const attr = findAttribute(opening, name);
|
|
2963
3418
|
if (attr) {
|
|
2964
3419
|
const v = attr.value;
|
|
@@ -3045,6 +3500,195 @@ export async function resizeArtboard(
|
|
|
3045
3500
|
});
|
|
3046
3501
|
}
|
|
3047
3502
|
|
|
3503
|
+
/**
|
|
3504
|
+
* Write/clear a BARE JSX boolean attribute (`<DCArtboard fixed>`, not
|
|
3505
|
+
* `fixed={true}` or a string). `value=false` reuses `removeStringAttr`'s
|
|
3506
|
+
* generic span-plus-leading-whitespace removal (it doesn't care whether the
|
|
3507
|
+
* attribute it's removing has a value) — only the `true` insert path is new.
|
|
3508
|
+
*/
|
|
3509
|
+
function writeBooleanAttr(
|
|
3510
|
+
s: MagicString,
|
|
3511
|
+
opening: AnyNode,
|
|
3512
|
+
name: string,
|
|
3513
|
+
value: boolean,
|
|
3514
|
+
source: string
|
|
3515
|
+
): void {
|
|
3516
|
+
if (!value) {
|
|
3517
|
+
removeStringAttr(s, opening, name, source);
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
if (findAttribute(opening, name)) return; // already present in some form
|
|
3521
|
+
const insertAt: number | undefined = opening?.name?.end;
|
|
3522
|
+
if (typeof insertAt !== 'number') throw new Error('Opening element has no resolvable name range');
|
|
3523
|
+
s.appendLeft(insertAt, ` ${name}`);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
/**
|
|
3527
|
+
* Toggle an artboard's height sizing mode (Hug ⇄ Fixed CSS-panel control).
|
|
3528
|
+
* `fixed=true` adds the bare `fixed` prop and, when `freezeHeight` is given
|
|
3529
|
+
* (the board's current measured height), also writes it as the exact
|
|
3530
|
+
* `height` — so pinning to Fixed doesn't snap the box back to whatever the
|
|
3531
|
+
* JSX `height` floor happened to be. `fixed=false` removes the `fixed` prop;
|
|
3532
|
+
* `height` is left as-is (it resumes being the hug floor). Same id-prop
|
|
3533
|
+
* addressing as applyResizeArtboard (DDR-027 — an artboard frame carries no
|
|
3534
|
+
* data-cd-id). Pure; reparse-gated.
|
|
3535
|
+
*/
|
|
3536
|
+
export function applySetArtboardHug(
|
|
3537
|
+
canvasAbsPath: string,
|
|
3538
|
+
source: string,
|
|
3539
|
+
artboardId: string,
|
|
3540
|
+
fixed: boolean,
|
|
3541
|
+
freezeHeight?: number
|
|
3542
|
+
): { source: string } {
|
|
3543
|
+
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
3544
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
3545
|
+
throw new CanvasEditError(
|
|
3546
|
+
`oxc-parser failed on ${canvasAbsPath}: ${parsed.errors[0]?.message ?? 'unknown'}`,
|
|
3547
|
+
{ canvas: canvasAbsPath, id: artboardId }
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
const artboards = collectJsxByTag(parsed.program, 'DCArtboard');
|
|
3551
|
+
const target = artboards.find((a) => getStringAttr(a.openingElement, 'id') === artboardId);
|
|
3552
|
+
if (!target) {
|
|
3553
|
+
throw new CanvasEditError(`<DCArtboard id="${artboardId}"> not found in ${canvasAbsPath}`, {
|
|
3554
|
+
canvas: canvasAbsPath,
|
|
3555
|
+
id: artboardId,
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
const s = new MagicString(source);
|
|
3559
|
+
writeBooleanAttr(s, target.openingElement, 'fixed', fixed, source);
|
|
3560
|
+
if (fixed && typeof freezeHeight === 'number') {
|
|
3561
|
+
writeNumericAttr(s, target.openingElement, 'height', freezeHeight);
|
|
3562
|
+
}
|
|
3563
|
+
const out = s.toString();
|
|
3564
|
+
const check = parseSync(canvasAbsPath, out, { sourceType: 'module' });
|
|
3565
|
+
if (check.errors && check.errors.length > 0) {
|
|
3566
|
+
throw new CanvasEditError(
|
|
3567
|
+
`artboard hug-toggle produced invalid source (${check.errors[0]?.message ?? 'parse error'})`,
|
|
3568
|
+
{ canvas: canvasAbsPath, id: artboardId }
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
return { source: out };
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
/** Set an artboard's Hug/Fixed mode on disk (atomic write + cross-process lock). */
|
|
3575
|
+
export async function setArtboardHug(
|
|
3576
|
+
canvasAbsPath: string,
|
|
3577
|
+
artboardId: string,
|
|
3578
|
+
fixed: boolean,
|
|
3579
|
+
freezeHeight?: number
|
|
3580
|
+
): Promise<{ source: string }> {
|
|
3581
|
+
return withLock(canvasAbsPath, async () => {
|
|
3582
|
+
const file = Bun.file(canvasAbsPath);
|
|
3583
|
+
if (!(await file.exists())) {
|
|
3584
|
+
throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
|
|
3585
|
+
canvas: canvasAbsPath,
|
|
3586
|
+
id: artboardId,
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
const source = await file.text();
|
|
3590
|
+
const next = applySetArtboardHug(canvasAbsPath, source, artboardId, fixed, freezeHeight);
|
|
3591
|
+
if (next.source === source) return next;
|
|
3592
|
+
const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
|
|
3593
|
+
await Bun.write(tmp, next.source);
|
|
3594
|
+
const { rename } = await import('node:fs/promises');
|
|
3595
|
+
await rename(tmp, canvasAbsPath);
|
|
3596
|
+
return next;
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
/** Patch shape for {@link applySetArtboardStyle} — `null` resets/removes that
|
|
3601
|
+
* prop back to the engine default; `undefined`/absent leaves it untouched. */
|
|
3602
|
+
export interface ArtboardStylePatch {
|
|
3603
|
+
background?: string | null;
|
|
3604
|
+
padding?: number | null;
|
|
3605
|
+
layout?: string | null;
|
|
3606
|
+
gap?: number | null;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
/**
|
|
3610
|
+
* Write the artboard "more settings" props (background / padding / layout /
|
|
3611
|
+
* gap) — applied to `.dc-artboard-body` by DCArtboard, not the frame chrome.
|
|
3612
|
+
* String props (background/layout) reuse `editStringAttr`/`removeStringAttr`;
|
|
3613
|
+
* numeric props (padding/gap) reuse `writeNumericAttr` with a 0 floor (unlike
|
|
3614
|
+
* width/height, 0 padding/gap is a normal value, not a degenerate one). Same
|
|
3615
|
+
* id-prop addressing + reparse-gate as applyResizeArtboard/applySetArtboardHug.
|
|
3616
|
+
*/
|
|
3617
|
+
export function applySetArtboardStyle(
|
|
3618
|
+
canvasAbsPath: string,
|
|
3619
|
+
source: string,
|
|
3620
|
+
artboardId: string,
|
|
3621
|
+
patch: ArtboardStylePatch
|
|
3622
|
+
): { source: string } {
|
|
3623
|
+
const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
|
|
3624
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
3625
|
+
throw new CanvasEditError(
|
|
3626
|
+
`oxc-parser failed on ${canvasAbsPath}: ${parsed.errors[0]?.message ?? 'unknown'}`,
|
|
3627
|
+
{ canvas: canvasAbsPath, id: artboardId }
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
const artboards = collectJsxByTag(parsed.program, 'DCArtboard');
|
|
3631
|
+
const target = artboards.find((a) => getStringAttr(a.openingElement, 'id') === artboardId);
|
|
3632
|
+
if (!target) {
|
|
3633
|
+
throw new CanvasEditError(`<DCArtboard id="${artboardId}"> not found in ${canvasAbsPath}`, {
|
|
3634
|
+
canvas: canvasAbsPath,
|
|
3635
|
+
id: artboardId,
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
const s = new MagicString(source);
|
|
3639
|
+
const opening = target.openingElement;
|
|
3640
|
+
if ('background' in patch) {
|
|
3641
|
+
if (patch.background == null) removeStringAttr(s, opening, 'background', source);
|
|
3642
|
+
else editStringAttr(s, opening, 'background', patch.background, canvasAbsPath, artboardId);
|
|
3643
|
+
}
|
|
3644
|
+
if ('layout' in patch) {
|
|
3645
|
+
if (patch.layout == null) removeStringAttr(s, opening, 'layout', source);
|
|
3646
|
+
else editStringAttr(s, opening, 'layout', patch.layout, canvasAbsPath, artboardId);
|
|
3647
|
+
}
|
|
3648
|
+
if ('padding' in patch) {
|
|
3649
|
+
if (patch.padding == null) removeStringAttr(s, opening, 'padding', source);
|
|
3650
|
+
else writeNumericAttr(s, opening, 'padding', patch.padding, 0);
|
|
3651
|
+
}
|
|
3652
|
+
if ('gap' in patch) {
|
|
3653
|
+
if (patch.gap == null) removeStringAttr(s, opening, 'gap', source);
|
|
3654
|
+
else writeNumericAttr(s, opening, 'gap', patch.gap, 0);
|
|
3655
|
+
}
|
|
3656
|
+
const out = s.toString();
|
|
3657
|
+
const check = parseSync(canvasAbsPath, out, { sourceType: 'module' });
|
|
3658
|
+
if (check.errors && check.errors.length > 0) {
|
|
3659
|
+
throw new CanvasEditError(
|
|
3660
|
+
`artboard style edit produced invalid source (${check.errors[0]?.message ?? 'parse error'})`,
|
|
3661
|
+
{ canvas: canvasAbsPath, id: artboardId }
|
|
3662
|
+
);
|
|
3663
|
+
}
|
|
3664
|
+
return { source: out };
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
/** Set an artboard's style props on disk (atomic write + cross-process lock). */
|
|
3668
|
+
export async function setArtboardStyle(
|
|
3669
|
+
canvasAbsPath: string,
|
|
3670
|
+
artboardId: string,
|
|
3671
|
+
patch: ArtboardStylePatch
|
|
3672
|
+
): Promise<{ source: string }> {
|
|
3673
|
+
return withLock(canvasAbsPath, async () => {
|
|
3674
|
+
const file = Bun.file(canvasAbsPath);
|
|
3675
|
+
if (!(await file.exists())) {
|
|
3676
|
+
throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
|
|
3677
|
+
canvas: canvasAbsPath,
|
|
3678
|
+
id: artboardId,
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
const source = await file.text();
|
|
3682
|
+
const next = applySetArtboardStyle(canvasAbsPath, source, artboardId, patch);
|
|
3683
|
+
if (next.source === source) return next;
|
|
3684
|
+
const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
|
|
3685
|
+
await Bun.write(tmp, next.source);
|
|
3686
|
+
const { rename } = await import('node:fs/promises');
|
|
3687
|
+
await rename(tmp, canvasAbsPath);
|
|
3688
|
+
return next;
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3048
3692
|
/**
|
|
3049
3693
|
* Delete the `<DCArtboard id="…">` whose `id` prop equals `artboardId` — the
|
|
3050
3694
|
* artboard counterpart of applyDeleteElement (an artboard is addressed by its id
|