@trojanbox-vcp-test/site-edit-engine 0.1.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 +85 -0
- package/dist/execute-integration/execute-fixture-harness.d.ts +25 -0
- package/dist/execute-integration/execute-fixture-harness.js +37 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/internal/ast/diagnostics/index.d.ts +5 -0
- package/dist/internal/ast/diagnostics/index.js +25 -0
- package/dist/internal/ast/history/index.d.ts +15 -0
- package/dist/internal/ast/history/index.js +62 -0
- package/dist/internal/ast/index.d.ts +8 -0
- package/dist/internal/ast/index.js +5 -0
- package/dist/internal/ast/locators/index.d.ts +1 -0
- package/dist/internal/ast/locators/index.js +1 -0
- package/dist/internal/ast/locators/resolve-locator.d.ts +16 -0
- package/dist/internal/ast/locators/resolve-locator.js +920 -0
- package/dist/internal/ast/parser/SourceParser.d.ts +30 -0
- package/dist/internal/ast/parser/SourceParser.js +49 -0
- package/dist/internal/ast/parser/index.d.ts +21 -0
- package/dist/internal/ast/parser/index.js +64 -0
- package/dist/internal/ast/primitives/conditional/conditional-primitives.d.ts +18 -0
- package/dist/internal/ast/primitives/conditional/conditional-primitives.js +237 -0
- package/dist/internal/ast/primitives/conditional/index.d.ts +1 -0
- package/dist/internal/ast/primitives/conditional/index.js +1 -0
- package/dist/internal/ast/primitives/imports/add-import.d.ts +18 -0
- package/dist/internal/ast/primitives/imports/add-import.js +111 -0
- package/dist/internal/ast/primitives/imports/index.d.ts +2 -0
- package/dist/internal/ast/primitives/imports/index.js +2 -0
- package/dist/internal/ast/primitives/imports/remove-import.d.ts +15 -0
- package/dist/internal/ast/primitives/imports/remove-import.js +72 -0
- package/dist/internal/ast/primitives/index.d.ts +10 -0
- package/dist/internal/ast/primitives/index.js +10 -0
- package/dist/internal/ast/primitives/jsx/index.d.ts +4 -0
- package/dist/internal/ast/primitives/jsx/index.js +4 -0
- package/dist/internal/ast/primitives/jsx/insert-child.d.ts +11 -0
- package/dist/internal/ast/primitives/jsx/insert-child.js +69 -0
- package/dist/internal/ast/primitives/jsx/move-node.d.ts +9 -0
- package/dist/internal/ast/primitives/jsx/move-node.js +76 -0
- package/dist/internal/ast/primitives/jsx/remove-node.d.ts +7 -0
- package/dist/internal/ast/primitives/jsx/remove-node.js +36 -0
- package/dist/internal/ast/primitives/jsx/update-text.d.ts +8 -0
- package/dist/internal/ast/primitives/jsx/update-text.js +81 -0
- package/dist/internal/ast/primitives/next/index.d.ts +1 -0
- package/dist/internal/ast/primitives/next/index.js +1 -0
- package/dist/internal/ast/primitives/next/next-primitives.d.ts +43 -0
- package/dist/internal/ast/primitives/next/next-primitives.js +211 -0
- package/dist/internal/ast/primitives/shared.d.ts +60 -0
- package/dist/internal/ast/primitives/shared.js +176 -0
- package/dist/internal/ast/primitives/style/class-expression.d.ts +23 -0
- package/dist/internal/ast/primitives/style/class-expression.js +174 -0
- package/dist/internal/ast/primitives/style/index.d.ts +1 -0
- package/dist/internal/ast/primitives/style/index.js +1 -0
- package/dist/internal/ast/primitives/style/style-primitives.d.ts +49 -0
- package/dist/internal/ast/primitives/style/style-primitives.js +555 -0
- package/dist/internal/ast/primitives/values/index.d.ts +1 -0
- package/dist/internal/ast/primitives/values/index.js +1 -0
- package/dist/internal/ast/primitives/values/value-primitives.d.ts +42 -0
- package/dist/internal/ast/primitives/values/value-primitives.js +158 -0
- package/dist/internal/ast/printer/SourcePrinter.d.ts +21 -0
- package/dist/internal/ast/printer/SourcePrinter.js +76 -0
- package/dist/internal/ast/printer/index.d.ts +6 -0
- package/dist/internal/ast/printer/index.js +126 -0
- package/dist/internal/ast/types.d.ts +190 -0
- package/dist/internal/ast/types.js +1 -0
- package/dist/internal/capability/capability-resolver.d.ts +16 -0
- package/dist/internal/capability/capability-resolver.js +127 -0
- package/dist/internal/classname-source.d.ts +24 -0
- package/dist/internal/classname-source.js +220 -0
- package/dist/internal/contracts/IEditEngineRuntime.d.ts +18 -0
- package/dist/internal/contracts/IEditEngineRuntime.js +1 -0
- package/dist/internal/domain/EditDiagnostic.d.ts +38 -0
- package/dist/internal/domain/EditDiagnostic.js +43 -0
- package/dist/internal/events/event-bus.d.ts +14 -0
- package/dist/internal/events/event-bus.js +21 -0
- package/dist/internal/graph/graph-builder.d.ts +12 -0
- package/dist/internal/graph/graph-builder.js +1371 -0
- package/dist/internal/graph/import-resolver.d.ts +31 -0
- package/dist/internal/graph/import-resolver.js +109 -0
- package/dist/internal/graph/project-graph-builder.d.ts +32 -0
- package/dist/internal/graph/project-graph-builder.js +133 -0
- package/dist/internal/graph/types.d.ts +114 -0
- package/dist/internal/graph/types.js +6 -0
- package/dist/internal/history/undo-redo.d.ts +28 -0
- package/dist/internal/history/undo-redo.js +42 -0
- package/dist/internal/index.d.ts +2 -0
- package/dist/internal/index.js +1 -0
- package/dist/internal/planner/planner.d.ts +104 -0
- package/dist/internal/planner/planner.js +2533 -0
- package/dist/internal/planner/types.d.ts +275 -0
- package/dist/internal/planner/types.js +6 -0
- package/dist/internal/protocol/boundary.d.ts +10 -0
- package/dist/internal/protocol/boundary.js +3 -0
- package/dist/internal/protocol/capability.d.ts +47 -0
- package/dist/internal/protocol/capability.js +8 -0
- package/dist/internal/protocol/error.d.ts +43 -0
- package/dist/internal/protocol/error.js +38 -0
- package/dist/internal/protocol/event.d.ts +39 -0
- package/dist/internal/protocol/event.js +3 -0
- package/dist/internal/protocol/identity.d.ts +26 -0
- package/dist/internal/protocol/identity.js +30 -0
- package/dist/internal/protocol/operation.d.ts +224 -0
- package/dist/internal/protocol/operation.js +8 -0
- package/dist/internal/protocol/render.d.ts +212 -0
- package/dist/internal/protocol/render.js +3 -0
- package/dist/internal/protocol.d.ts +9 -0
- package/dist/internal/protocol.js +2 -0
- package/dist/internal/provenance/binding-graph.d.ts +39 -0
- package/dist/internal/provenance/binding-graph.js +184 -0
- package/dist/internal/provenance/capability-policy.d.ts +15 -0
- package/dist/internal/provenance/capability-policy.js +96 -0
- package/dist/internal/provenance/data-source-classifier.d.ts +14 -0
- package/dist/internal/provenance/data-source-classifier.js +281 -0
- package/dist/internal/provenance/resolve-text-provenance.d.ts +45 -0
- package/dist/internal/provenance/resolve-text-provenance.js +3090 -0
- package/dist/internal/provenance/types.d.ts +89 -0
- package/dist/internal/provenance/types.js +1 -0
- package/dist/internal/render/component-semantic.d.ts +11 -0
- package/dist/internal/render/component-semantic.js +141 -0
- package/dist/internal/render/content-model.d.ts +3 -0
- package/dist/internal/render/content-model.js +89 -0
- package/dist/internal/render/media-model.d.ts +3 -0
- package/dist/internal/render/media-model.js +45 -0
- package/dist/internal/render/provenance-types.d.ts +33 -0
- package/dist/internal/render/provenance-types.js +1 -0
- package/dist/internal/render/render-projection.d.ts +24 -0
- package/dist/internal/render/render-projection.js +281 -0
- package/dist/internal/render/tailwind-style-model.d.ts +19 -0
- package/dist/internal/render/tailwind-style-model.js +1187 -0
- package/dist/internal/runtime/EditEngineRuntime.d.ts +25 -0
- package/dist/internal/runtime/EditEngineRuntime.js +89 -0
- package/dist/internal/runtime/EditEngineRuntimeSnapshot.d.ts +31 -0
- package/dist/internal/runtime/EditEngineRuntimeSnapshot.js +15 -0
- package/dist/internal/runtime/InternalEditEngine.d.ts +44 -0
- package/dist/internal/runtime/InternalEditEngine.js +1391 -0
- package/dist/internal/runtime.d.ts +3 -0
- package/dist/internal/runtime.js +1 -0
- package/dist/internal/topology/topology.d.ts +6 -0
- package/dist/internal/topology/topology.js +98 -0
- package/dist/internal/topology/types.d.ts +35 -0
- package/dist/internal/topology/types.js +5 -0
- package/dist/internal/types.d.ts +1 -0
- package/dist/internal/types.js +1 -0
- package/dist/internal/writeback/in-memory-fs.d.ts +7 -0
- package/dist/internal/writeback/in-memory-fs.js +44 -0
- package/dist/internal/writeback/types.d.ts +45 -0
- package/dist/internal/writeback/types.js +7 -0
- package/dist/internal/writeback/writeback-service.d.ts +7 -0
- package/dist/internal/writeback/writeback-service.js +568 -0
- package/dist/internal-adapter.d.ts +18 -0
- package/dist/internal-adapter.js +350 -0
- package/dist/next-app-router-fs.d.ts +2 -0
- package/dist/next-app-router-fs.js +64 -0
- package/dist/next-app-router.d.ts +11 -0
- package/dist/next-app-router.js +140 -0
- package/dist/preview-runtime.d.ts +394 -0
- package/dist/preview-runtime.js +102 -0
- package/dist/public-file-system.d.ts +7 -0
- package/dist/public-file-system.js +1 -0
- package/dist/runtime-sync.d.ts +95 -0
- package/dist/runtime-sync.js +321 -0
- package/dist/runtime.d.ts +340 -0
- package/dist/runtime.js +134 -0
- package/dist/site-edit-instrumentation.d.ts +19 -0
- package/dist/site-edit-instrumentation.js +322 -0
- package/dist/snapshot-file-system.d.ts +19 -0
- package/dist/snapshot-file-system.js +49 -0
- package/dist/source-watcher.d.ts +20 -0
- package/dist/source-watcher.js +150 -0
- package/dist/source-writeback-test-harness.d.ts +244 -0
- package/dist/source-writeback-test-harness.js +119 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/dist/webpack-loader.cjs +592 -0
- package/dist/webpack-loader.d.ts +27 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
// ─── Designer Engine Facade ──────────────────────────
|
|
2
|
+
// 来源: docs/04-module-api-and-call-graph.md §2
|
|
3
|
+
//
|
|
4
|
+
// DesignerEngine 是唯一语义中枢。
|
|
5
|
+
// 所有 "can-edit / how-to-edit / what-happens-after" 决策只在 engine 中发生。
|
|
6
|
+
import { OperationErrorCode } from "../protocol.js";
|
|
7
|
+
import { buildMultiFileRenderGraph } from "../graph/project-graph-builder.js";
|
|
8
|
+
import { buildEditableTopology } from "../topology/topology.js";
|
|
9
|
+
import { resolveAllCapabilities } from "../capability/capability-resolver.js";
|
|
10
|
+
import { projectDetailWriteTarget, projectRenderDocument, projectRenderObjectDetail, } from "../render/render-projection.js";
|
|
11
|
+
import { buildComponentSemanticDescriptor } from "../render/component-semantic.js";
|
|
12
|
+
import { buildContentModel } from "../render/content-model.js";
|
|
13
|
+
import { buildMediaModel } from "../render/media-model.js";
|
|
14
|
+
import { buildStyleSource } from "../render/tailwind-style-model.js";
|
|
15
|
+
import { applyClassNameSourceWrite, extractClassNameSource, } from "../classname-source.js";
|
|
16
|
+
import { OperationPlanner } from "../planner/planner.js";
|
|
17
|
+
import { resolveProvenance, resolveTextProvenance, } from "../provenance/resolve-text-provenance.js";
|
|
18
|
+
import { createWritebackService } from "../writeback/writeback-service.js";
|
|
19
|
+
import { createEventBus } from "../events/event-bus.js";
|
|
20
|
+
const nextRouteEntryPattern = /^(?:src\/)?app\/.*\/?page\.[cm]?[jt]sx?$/;
|
|
21
|
+
const nextLayoutBasenames = [
|
|
22
|
+
"layout.tsx",
|
|
23
|
+
"layout.ts",
|
|
24
|
+
"layout.jsx",
|
|
25
|
+
"layout.js",
|
|
26
|
+
"layout.mjs",
|
|
27
|
+
"layout.cjs",
|
|
28
|
+
];
|
|
29
|
+
const repeatItemInstanceKeyMarker = "::repeat-item#";
|
|
30
|
+
const richTextBlockKeyMarker = "::rich-text-block#";
|
|
31
|
+
// ── Default in-memory file system ──
|
|
32
|
+
function createDefaultFileSystem() {
|
|
33
|
+
const versions = new Map();
|
|
34
|
+
return {
|
|
35
|
+
async readFile(path) {
|
|
36
|
+
const fs = await import("node:fs/promises");
|
|
37
|
+
return fs.readFile(path, "utf-8");
|
|
38
|
+
},
|
|
39
|
+
async writeFile(path, content) {
|
|
40
|
+
const fs = await import("node:fs/promises");
|
|
41
|
+
await fs.writeFile(path, content, "utf-8");
|
|
42
|
+
},
|
|
43
|
+
async exists(path) {
|
|
44
|
+
const fs = await import("node:fs/promises");
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(path);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
getVersion(file) {
|
|
54
|
+
return versions.get(file) ?? 0;
|
|
55
|
+
},
|
|
56
|
+
bumpVersion(file) {
|
|
57
|
+
const v = (versions.get(file) ?? 0) + 1;
|
|
58
|
+
versions.set(file, v);
|
|
59
|
+
return v;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function toTextSourceView(result) {
|
|
64
|
+
if (!result || result.kind !== "value-binding") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const path = "path" in result ? result.path : undefined;
|
|
68
|
+
const root = result.root;
|
|
69
|
+
const bindingName = root && (root.kind === "binding" || root.kind === "named-export")
|
|
70
|
+
? root.name
|
|
71
|
+
: undefined;
|
|
72
|
+
return {
|
|
73
|
+
kind: result.sourceKind,
|
|
74
|
+
file: result.file,
|
|
75
|
+
expression: result.displayExpression,
|
|
76
|
+
bindingName,
|
|
77
|
+
path: path?.map((segment) => segment.kind === "property" ? segment.name : segment.index),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function toWriteTargetView(writeTarget) {
|
|
81
|
+
return {
|
|
82
|
+
kind: writeTarget.isProxy ? "component-proxy" : "jsx-node",
|
|
83
|
+
file: writeTarget.file,
|
|
84
|
+
componentName: writeTarget.componentName,
|
|
85
|
+
isProxy: writeTarget.isProxy,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 创建 DesignerEngine 实例。
|
|
90
|
+
*/
|
|
91
|
+
export function createInternalEditEngine(config) {
|
|
92
|
+
let _status = "idle";
|
|
93
|
+
const _fileSystem = config.fileSystem ?? createDefaultFileSystem();
|
|
94
|
+
const _eventBus = createEventBus();
|
|
95
|
+
const _routes = new Map();
|
|
96
|
+
let _activeRouteId = null;
|
|
97
|
+
const _pendingRebuildFiles = new Set();
|
|
98
|
+
let _rebuildLoop = null;
|
|
99
|
+
const _writebackService = createWritebackService(_fileSystem, {
|
|
100
|
+
externalAdapters: config.externalAdapters,
|
|
101
|
+
});
|
|
102
|
+
const _fileContentCache = new Map();
|
|
103
|
+
let _documentVersion = 0;
|
|
104
|
+
let _mutationsInFlight = 0;
|
|
105
|
+
// Register onEvent callback
|
|
106
|
+
if (config.onEvent) {
|
|
107
|
+
_eventBus.subscribe(config.onEvent);
|
|
108
|
+
}
|
|
109
|
+
// Register initial routes
|
|
110
|
+
if (config.routes) {
|
|
111
|
+
for (const route of config.routes) {
|
|
112
|
+
_routes.set(route.routeId, {
|
|
113
|
+
routeId: route.routeId,
|
|
114
|
+
entryFile: route.entryFile,
|
|
115
|
+
graph: null,
|
|
116
|
+
topology: null,
|
|
117
|
+
capabilities: null,
|
|
118
|
+
document: null,
|
|
119
|
+
detailProjections: null,
|
|
120
|
+
fileVersions: new Map(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── Build route graph (async, multi-file aware) ──
|
|
125
|
+
async function buildDetailProjections(graph, topology, allSources) {
|
|
126
|
+
const detailProjections = new Map();
|
|
127
|
+
const provenanceFileCache = new Map();
|
|
128
|
+
const exists = typeof _fileSystem.exists === "function"
|
|
129
|
+
? (path) => _fileSystem.exists(path)
|
|
130
|
+
: async () => false;
|
|
131
|
+
for (const [key, element] of graph.elements) {
|
|
132
|
+
const topNode = topology.nodes.get(key);
|
|
133
|
+
if (!topNode) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const source = allSources.get(element.sourceFile) ??
|
|
137
|
+
_fileContentCache.get(element.sourceFile);
|
|
138
|
+
const textSources = new Map();
|
|
139
|
+
const textTargets = new Map();
|
|
140
|
+
const provenances = new Map();
|
|
141
|
+
const segmentResolutions = [];
|
|
142
|
+
for (const segment of element.textSegments) {
|
|
143
|
+
if (!source) {
|
|
144
|
+
textSources.set(segment.index, null);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const resolved = await resolveTextProvenance({
|
|
148
|
+
file: element.sourceFile,
|
|
149
|
+
source,
|
|
150
|
+
segment,
|
|
151
|
+
readFile: (path) => _fileSystem.readFile(path),
|
|
152
|
+
exists,
|
|
153
|
+
graph,
|
|
154
|
+
externalSourceResolvers: config.externalSourceResolvers,
|
|
155
|
+
externalAdapters: config.externalAdapters?.map(({ sourceType, adapterId, capabilities }) => ({
|
|
156
|
+
sourceType,
|
|
157
|
+
adapterId,
|
|
158
|
+
capabilities,
|
|
159
|
+
})),
|
|
160
|
+
functionCallContracts: config.functionCallContracts,
|
|
161
|
+
fileCache: provenanceFileCache,
|
|
162
|
+
});
|
|
163
|
+
const provenance = await resolveProvenance({
|
|
164
|
+
file: element.sourceFile,
|
|
165
|
+
source,
|
|
166
|
+
target: { kind: "text-segment", segment },
|
|
167
|
+
readFile: (path) => _fileSystem.readFile(path),
|
|
168
|
+
exists,
|
|
169
|
+
graph,
|
|
170
|
+
externalSourceResolvers: config.externalSourceResolvers,
|
|
171
|
+
externalAdapters: config.externalAdapters?.map(({ sourceType, adapterId, capabilities }) => ({
|
|
172
|
+
sourceType,
|
|
173
|
+
adapterId,
|
|
174
|
+
capabilities,
|
|
175
|
+
})),
|
|
176
|
+
functionCallContracts: config.functionCallContracts,
|
|
177
|
+
fileCache: provenanceFileCache,
|
|
178
|
+
});
|
|
179
|
+
const textSource = toTextSourceView(resolved);
|
|
180
|
+
textSources.set(segment.index, textSource);
|
|
181
|
+
textTargets.set(segment.index, resolved);
|
|
182
|
+
provenances.set(segment.index, provenance);
|
|
183
|
+
segmentResolutions.push({
|
|
184
|
+
editable: segment.editable,
|
|
185
|
+
source: textSource,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const writeTarget = projectDetailWriteTarget(toWriteTargetView(topNode.writeTarget), segmentResolutions);
|
|
189
|
+
const componentSemantic = buildComponentSemanticDescriptor({
|
|
190
|
+
key,
|
|
191
|
+
element,
|
|
192
|
+
graph,
|
|
193
|
+
provenances,
|
|
194
|
+
});
|
|
195
|
+
const contentModel = buildContentModel(element);
|
|
196
|
+
const mediaModel = buildMediaModel(element);
|
|
197
|
+
const styleSource = buildStyleSource(element);
|
|
198
|
+
const conditionalRegion = graph.regions.find((region) => region.kind === "conditional-region" &&
|
|
199
|
+
region.childKeys.includes(key));
|
|
200
|
+
let conditional;
|
|
201
|
+
const candidate = conditionalRegion?.sourceCandidates?.[0];
|
|
202
|
+
if (source &&
|
|
203
|
+
conditionalRegion &&
|
|
204
|
+
candidate?.kind === "conditional-branch") {
|
|
205
|
+
const provenance = await resolveProvenance({
|
|
206
|
+
file: element.sourceFile,
|
|
207
|
+
source,
|
|
208
|
+
target: { kind: "conditional-branch", candidate },
|
|
209
|
+
readFile: (path) => _fileSystem.readFile(path),
|
|
210
|
+
exists,
|
|
211
|
+
graph,
|
|
212
|
+
externalSourceResolvers: config.externalSourceResolvers,
|
|
213
|
+
externalAdapters: config.externalAdapters?.map(({ sourceType, adapterId, capabilities }) => ({
|
|
214
|
+
sourceType,
|
|
215
|
+
adapterId,
|
|
216
|
+
capabilities,
|
|
217
|
+
})),
|
|
218
|
+
functionCallContracts: config.functionCallContracts,
|
|
219
|
+
fileCache: provenanceFileCache,
|
|
220
|
+
});
|
|
221
|
+
const branchIndex = conditionalRegion.childKeys.indexOf(key);
|
|
222
|
+
conditional = {
|
|
223
|
+
currentBranch: branchIndex <= 0 ? "then" : "else",
|
|
224
|
+
availableBranches: conditionalRegion.childKeys.length > 1
|
|
225
|
+
? ["then", "else"]
|
|
226
|
+
: ["then"],
|
|
227
|
+
conditionExpression: candidate.conditionExpression,
|
|
228
|
+
provenance: {
|
|
229
|
+
finalSource: provenance.finalSource
|
|
230
|
+
? {
|
|
231
|
+
kind: provenance.finalSource.kind,
|
|
232
|
+
file: provenance.finalSource.file,
|
|
233
|
+
displayPath: provenance.finalSource.displayPath,
|
|
234
|
+
}
|
|
235
|
+
: null,
|
|
236
|
+
chain: provenance.chain.map((hop) => ({
|
|
237
|
+
kind: hop.kind,
|
|
238
|
+
file: hop.file,
|
|
239
|
+
displayName: hop.displayName,
|
|
240
|
+
canEditHere: hop.canEditHere,
|
|
241
|
+
})),
|
|
242
|
+
confidence: provenance.confidence,
|
|
243
|
+
editMode: provenance.editMode,
|
|
244
|
+
diagnostics: provenance.diagnostics.map((diagnostic) => ({
|
|
245
|
+
code: diagnostic.code,
|
|
246
|
+
message: diagnostic.message,
|
|
247
|
+
})),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
detailProjections.set(key, {
|
|
252
|
+
textSources,
|
|
253
|
+
textTargets,
|
|
254
|
+
provenances,
|
|
255
|
+
writeTarget,
|
|
256
|
+
componentSemantic,
|
|
257
|
+
contentModel,
|
|
258
|
+
mediaModel,
|
|
259
|
+
styleSource,
|
|
260
|
+
conditional,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return detailProjections;
|
|
264
|
+
}
|
|
265
|
+
function getTrackedFileVersion(file) {
|
|
266
|
+
return typeof _fileSystem.getVersion === "function"
|
|
267
|
+
? _fileSystem.getVersion(file)
|
|
268
|
+
: 0;
|
|
269
|
+
}
|
|
270
|
+
function getRouteVersionFloor(routeState, fileVersions) {
|
|
271
|
+
let versionFloor = 1;
|
|
272
|
+
for (const version of fileVersions.values()) {
|
|
273
|
+
versionFloor = Math.max(versionFloor, version);
|
|
274
|
+
}
|
|
275
|
+
return Math.max(versionFloor, routeState.graph?.version ?? 0);
|
|
276
|
+
}
|
|
277
|
+
async function buildRoute(routeId, source, options = {}) {
|
|
278
|
+
const routeState = _routes.get(routeId);
|
|
279
|
+
if (!routeState)
|
|
280
|
+
return;
|
|
281
|
+
const additionalEntries = await loadNextRouteLayoutEntries(routeState.entryFile);
|
|
282
|
+
// Use multi-file builder to load + merge all locally-imported files
|
|
283
|
+
const { graph, allSources } = await buildMultiFileRenderGraph(routeState.entryFile, source, (path) => _fileSystem.readFile(path), typeof _fileSystem.exists === "function"
|
|
284
|
+
? (path) => _fileSystem.exists(path)
|
|
285
|
+
: async () => false, {
|
|
286
|
+
additionalEntries,
|
|
287
|
+
version: routeState.graph?.version ?? 1,
|
|
288
|
+
});
|
|
289
|
+
const fileVersions = new Map();
|
|
290
|
+
for (const file of allSources.keys()) {
|
|
291
|
+
fileVersions.set(file, getTrackedFileVersion(file));
|
|
292
|
+
}
|
|
293
|
+
let routeVersion = Math.max(getRouteVersionFloor(routeState, fileVersions), options.minimumVersion ?? 0);
|
|
294
|
+
if (options.changed && routeVersion <= (routeState.graph?.version ?? 0)) {
|
|
295
|
+
routeVersion = (routeState.graph?.version ?? 0) + 1;
|
|
296
|
+
}
|
|
297
|
+
graph.version = routeVersion;
|
|
298
|
+
_documentVersion = Math.max(_documentVersion, routeVersion);
|
|
299
|
+
// Cache all loaded source files for planner use
|
|
300
|
+
for (const [file, src] of allSources) {
|
|
301
|
+
_fileContentCache.set(file, src);
|
|
302
|
+
}
|
|
303
|
+
graph.routeId = routeId;
|
|
304
|
+
const topology = buildEditableTopology(graph);
|
|
305
|
+
const capabilities = resolveAllCapabilities(topology, graph);
|
|
306
|
+
const detailProjections = await buildDetailProjections(graph, topology, allSources);
|
|
307
|
+
const canUpdateTextByKey = resolveEffectiveUpdateTextCapabilities(graph, capabilities, detailProjections);
|
|
308
|
+
const document = projectRichTextBlockEntries(projectRepeatConcreteEntries(projectRenderDocument(graph, topology, capabilities, {
|
|
309
|
+
canUpdateTextByKey,
|
|
310
|
+
}), graph, detailProjections), graph, detailProjections);
|
|
311
|
+
routeState.graph = graph;
|
|
312
|
+
routeState.topology = topology;
|
|
313
|
+
routeState.capabilities = capabilities;
|
|
314
|
+
routeState.document = document;
|
|
315
|
+
routeState.detailProjections = detailProjections;
|
|
316
|
+
routeState.fileVersions = fileVersions;
|
|
317
|
+
}
|
|
318
|
+
async function loadNextRouteLayoutEntries(entryFile) {
|
|
319
|
+
const entries = [];
|
|
320
|
+
for (const file of getNextRouteLayoutCandidates(entryFile)) {
|
|
321
|
+
const exists = typeof _fileSystem.exists === "function"
|
|
322
|
+
? await _fileSystem.exists(file)
|
|
323
|
+
: false;
|
|
324
|
+
if (!exists) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
entries.push({
|
|
328
|
+
file,
|
|
329
|
+
source: await _fileSystem.readFile(file),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return entries;
|
|
333
|
+
}
|
|
334
|
+
function resolveEffectiveUpdateTextCapabilities(graph, capabilities, detailProjections) {
|
|
335
|
+
const result = new Map();
|
|
336
|
+
for (const [key, element] of graph.elements) {
|
|
337
|
+
const capability = capabilities.get(key);
|
|
338
|
+
if (!capability?.canUpdateText) {
|
|
339
|
+
result.set(key, false);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const detail = detailProjections.get(key);
|
|
343
|
+
const hasWritableSegment = element.textSegments.some((segment) => {
|
|
344
|
+
if (!segment.editable) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
const provenance = detail?.provenances.get(segment.index) ?? null;
|
|
348
|
+
return !provenance || isWritableUpdateTextProvenance(provenance);
|
|
349
|
+
});
|
|
350
|
+
result.set(key, hasWritableSegment);
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
function isWritableUpdateTextProvenance(provenance) {
|
|
355
|
+
return (provenance.editMode === "direct" ||
|
|
356
|
+
provenance.editMode === "proxy" ||
|
|
357
|
+
provenance.editMode === "upstream");
|
|
358
|
+
}
|
|
359
|
+
async function rebuildRoutesForFiles(files, options = {}) {
|
|
360
|
+
const uniqueFiles = new Set(files);
|
|
361
|
+
const result = {
|
|
362
|
+
rebuiltRoutes: [],
|
|
363
|
+
failedRoutes: [],
|
|
364
|
+
};
|
|
365
|
+
for (const routeState of _routes.values()) {
|
|
366
|
+
const isAffected = [...uniqueFiles].some((file) => routeState.entryFile === file ||
|
|
367
|
+
routeState.graph?.fileIndex.has(file));
|
|
368
|
+
if (!isAffected) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const entrySource = await _fileSystem.readFile(routeState.entryFile);
|
|
373
|
+
await buildRoute(routeState.routeId, entrySource, options);
|
|
374
|
+
if (routeState.graph) {
|
|
375
|
+
result.rebuiltRoutes.push({
|
|
376
|
+
routeId: routeState.routeId,
|
|
377
|
+
version: routeState.graph.version,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
result.failedRoutes.push({
|
|
383
|
+
routeId: routeState.routeId,
|
|
384
|
+
message: getErrorMessage(error),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
function getAffectedRouteVersion(files, fallbackVersion = 0) {
|
|
391
|
+
const uniqueFiles = new Set(files);
|
|
392
|
+
let version = fallbackVersion;
|
|
393
|
+
for (const routeState of _routes.values()) {
|
|
394
|
+
const isAffected = [...uniqueFiles].some((file) => routeState.entryFile === file ||
|
|
395
|
+
routeState.graph?.fileIndex.has(file));
|
|
396
|
+
if (isAffected && routeState.graph) {
|
|
397
|
+
version = Math.max(version, routeState.graph.version);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return version;
|
|
401
|
+
}
|
|
402
|
+
function getAffectedRouteIds(files) {
|
|
403
|
+
const uniqueFiles = new Set(files);
|
|
404
|
+
const routeIds = new Set();
|
|
405
|
+
for (const routeState of _routes.values()) {
|
|
406
|
+
const isAffected = [...uniqueFiles].some((file) => routeState.entryFile === file ||
|
|
407
|
+
routeState.graph?.fileIndex.has(file));
|
|
408
|
+
if (isAffected) {
|
|
409
|
+
routeIds.add(routeState.routeId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return [...routeIds];
|
|
413
|
+
}
|
|
414
|
+
function scheduleRouteRebuild(files) {
|
|
415
|
+
for (const file of files) {
|
|
416
|
+
_pendingRebuildFiles.add(file);
|
|
417
|
+
}
|
|
418
|
+
if (_rebuildLoop || _status === "disposed") {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
_rebuildLoop = (async () => {
|
|
422
|
+
_status = "building";
|
|
423
|
+
try {
|
|
424
|
+
while (_pendingRebuildFiles.size > 0) {
|
|
425
|
+
await waitForMutationRebuilds();
|
|
426
|
+
const filesToRebuild = [..._pendingRebuildFiles];
|
|
427
|
+
_pendingRebuildFiles.clear();
|
|
428
|
+
const changedFiles = await filterChangedSourceFiles(filesToRebuild);
|
|
429
|
+
if (changedFiles.length === 0) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const affectedRouteIds = getAffectedRouteIds(changedFiles);
|
|
433
|
+
for (const routeId of affectedRouteIds) {
|
|
434
|
+
_eventBus.emit({ type: "graph-invalidated", routeId });
|
|
435
|
+
}
|
|
436
|
+
const { rebuiltRoutes, failedRoutes } = await rebuildRoutesForFiles(changedFiles, { changed: true });
|
|
437
|
+
for (const { routeId, version } of rebuiltRoutes) {
|
|
438
|
+
_eventBus.emit({ type: "graph-ready", routeId, version });
|
|
439
|
+
}
|
|
440
|
+
for (const { routeId, message } of failedRoutes) {
|
|
441
|
+
_eventBus.emit({ type: "graph-error", routeId, message });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
_rebuildLoop = null;
|
|
447
|
+
if (_status !== "disposed") {
|
|
448
|
+
_status = "ready";
|
|
449
|
+
}
|
|
450
|
+
if (_pendingRebuildFiles.size > 0) {
|
|
451
|
+
scheduleRouteRebuild([]);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
})();
|
|
455
|
+
}
|
|
456
|
+
async function waitForMutationRebuilds() {
|
|
457
|
+
while (_mutationsInFlight > 0) {
|
|
458
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function filterChangedSourceFiles(files) {
|
|
462
|
+
const changedFiles = [];
|
|
463
|
+
for (const file of new Set(files)) {
|
|
464
|
+
const cachedSource = _fileContentCache.get(file);
|
|
465
|
+
if (cachedSource === undefined) {
|
|
466
|
+
changedFiles.push(file);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const currentSource = await _fileSystem.readFile(file);
|
|
471
|
+
if (currentSource !== cachedSource) {
|
|
472
|
+
changedFiles.push(file);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
changedFiles.push(file);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return changedFiles;
|
|
480
|
+
}
|
|
481
|
+
// ── Ensure route is built ──
|
|
482
|
+
async function ensureRouteBuilt(routeId) {
|
|
483
|
+
const routeState = _routes.get(routeId);
|
|
484
|
+
if (!routeState)
|
|
485
|
+
throw new Error(`Route not found: ${routeId}`);
|
|
486
|
+
if (routeState.graph)
|
|
487
|
+
return; // already built
|
|
488
|
+
const source = await _fileSystem.readFile(routeState.entryFile);
|
|
489
|
+
await buildRoute(routeId, source);
|
|
490
|
+
}
|
|
491
|
+
function getRouteStateForKey(key, routeId) {
|
|
492
|
+
if (routeId) {
|
|
493
|
+
const routeState = _routes.get(routeId) ?? null;
|
|
494
|
+
if (!routeState || !routeStateHasKey(routeState, key)) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
return routeState;
|
|
498
|
+
}
|
|
499
|
+
for (const routeState of _routes.values()) {
|
|
500
|
+
if (routeStateHasKey(routeState, key)) {
|
|
501
|
+
return routeState;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
function routeStateHasKey(routeState, key) {
|
|
507
|
+
if (routeState.graph?.elements.has(key)) {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
const repeatInstance = parseRepeatItemInstanceKey(key);
|
|
511
|
+
if (repeatInstance) {
|
|
512
|
+
return routeState.graph?.elements.has(repeatInstance.baseKey) === true;
|
|
513
|
+
}
|
|
514
|
+
const richTextBlock = parseRichTextBlockKey(key);
|
|
515
|
+
return richTextBlock
|
|
516
|
+
? routeState.graph?.elements.has(richTextBlock.ownerKey) === true
|
|
517
|
+
: false;
|
|
518
|
+
}
|
|
519
|
+
const engine = {
|
|
520
|
+
get status() {
|
|
521
|
+
return _status;
|
|
522
|
+
},
|
|
523
|
+
dispose() {
|
|
524
|
+
_status = "disposed";
|
|
525
|
+
},
|
|
526
|
+
registerRoute(routeId, entryFile) {
|
|
527
|
+
_routes.set(routeId, {
|
|
528
|
+
routeId,
|
|
529
|
+
entryFile,
|
|
530
|
+
graph: null,
|
|
531
|
+
topology: null,
|
|
532
|
+
capabilities: null,
|
|
533
|
+
document: null,
|
|
534
|
+
detailProjections: null,
|
|
535
|
+
fileVersions: new Map(),
|
|
536
|
+
});
|
|
537
|
+
},
|
|
538
|
+
async setActiveRoute(routeId) {
|
|
539
|
+
const routeState = _routes.get(routeId);
|
|
540
|
+
if (!routeState)
|
|
541
|
+
throw new Error(`Route not found: ${routeId}`);
|
|
542
|
+
const wasAlreadyActive = _activeRouteId === routeId;
|
|
543
|
+
_activeRouteId = routeId;
|
|
544
|
+
if (!routeState.graph) {
|
|
545
|
+
_status = "building";
|
|
546
|
+
try {
|
|
547
|
+
const source = await _fileSystem.readFile(routeState.entryFile);
|
|
548
|
+
await buildRoute(routeId, source);
|
|
549
|
+
const nextVersion = _routes.get(routeId)?.graph?.version ?? 1;
|
|
550
|
+
_status = "ready";
|
|
551
|
+
_eventBus.emit({
|
|
552
|
+
type: "graph-ready",
|
|
553
|
+
routeId,
|
|
554
|
+
version: nextVersion,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
_status = "ready"; // degrade gracefully with empty state
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
_status = "ready";
|
|
563
|
+
if (!wasAlreadyActive) {
|
|
564
|
+
_eventBus.emit({
|
|
565
|
+
type: "graph-ready",
|
|
566
|
+
routeId,
|
|
567
|
+
version: routeState.graph.version,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
getDocument(routeId) {
|
|
572
|
+
const routeState = _routes.get(routeId);
|
|
573
|
+
if (!routeState)
|
|
574
|
+
throw new Error(`Route not found: ${routeId}`);
|
|
575
|
+
if (!routeState.document)
|
|
576
|
+
throw new Error(`Route not built: ${routeId}`);
|
|
577
|
+
return routeState.document;
|
|
578
|
+
},
|
|
579
|
+
getObjectCapabilities(key, routeId) {
|
|
580
|
+
const routeState = getRouteStateForKey(key, routeId);
|
|
581
|
+
const richTextBlock = parseRichTextBlockKey(key);
|
|
582
|
+
if (richTextBlock) {
|
|
583
|
+
return {
|
|
584
|
+
key,
|
|
585
|
+
canUpdateText: false,
|
|
586
|
+
canInsertChild: false,
|
|
587
|
+
canMove: true,
|
|
588
|
+
canRemove: true,
|
|
589
|
+
boundaryKind: null,
|
|
590
|
+
isOpaque: false,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const baseKey = parseRepeatItemInstanceKey(key)?.baseKey ?? key;
|
|
594
|
+
const element = routeState?.graph?.elements.get(baseKey);
|
|
595
|
+
const capability = routeState?.capabilities?.get(baseKey);
|
|
596
|
+
if (element && capability) {
|
|
597
|
+
return {
|
|
598
|
+
key,
|
|
599
|
+
canUpdateText: capability.canUpdateText,
|
|
600
|
+
canInsertChild: capability.canInsertChild,
|
|
601
|
+
canMove: capability.canMove,
|
|
602
|
+
canRemove: capability.canRemove,
|
|
603
|
+
boundaryKind: element.boundaryKind,
|
|
604
|
+
isOpaque: element.isOpaque,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
throw new Error(`Key not found: ${key}`);
|
|
608
|
+
},
|
|
609
|
+
getRenderDetail(key, routeId) {
|
|
610
|
+
const routeState = getRouteStateForKey(key, routeId);
|
|
611
|
+
const richTextBlock = parseRichTextBlockKey(key);
|
|
612
|
+
const baseKey = richTextBlock?.ownerKey ??
|
|
613
|
+
parseRepeatItemInstanceKey(key)?.baseKey ??
|
|
614
|
+
key;
|
|
615
|
+
if (routeState?.graph &&
|
|
616
|
+
routeState.topology &&
|
|
617
|
+
routeState.capabilities &&
|
|
618
|
+
routeState.detailProjections) {
|
|
619
|
+
const detailState = routeState.detailProjections.get(baseKey);
|
|
620
|
+
if (detailState) {
|
|
621
|
+
const detail = projectRenderObjectDetail(baseKey, routeState.graph, routeState.topology, routeState.capabilities, detailState.textSources, detailState.writeTarget, detailState.provenances, detailState.conditional, detailState.componentSemantic, detailState.contentModel, detailState.mediaModel, detailState.styleSource, detailState.styleModel);
|
|
622
|
+
return key === baseKey ? detail : { ...detail, key };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
throw new Error(`Key not found: ${key}`);
|
|
626
|
+
},
|
|
627
|
+
getClassNameSource(key, routeId) {
|
|
628
|
+
const routeState = getRouteStateForKey(key, routeId);
|
|
629
|
+
const baseKey = parseRepeatItemInstanceKey(key)?.baseKey ?? key;
|
|
630
|
+
if (!routeState?.graph) {
|
|
631
|
+
throw new Error(`Key not found: ${key}`);
|
|
632
|
+
}
|
|
633
|
+
const element = routeState.graph.elements.get(baseKey);
|
|
634
|
+
if (!element) {
|
|
635
|
+
throw new Error(`Key not found: ${key}`);
|
|
636
|
+
}
|
|
637
|
+
const source = _fileContentCache.get(element.sourceFile);
|
|
638
|
+
if (source === undefined) {
|
|
639
|
+
throw new Error(`Source not loaded: ${element.sourceFile}`);
|
|
640
|
+
}
|
|
641
|
+
return extractClassNameSource({
|
|
642
|
+
key: baseKey,
|
|
643
|
+
route: {
|
|
644
|
+
routeId: routeState.routeId,
|
|
645
|
+
graph: routeState.graph,
|
|
646
|
+
source,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
},
|
|
650
|
+
async setClassNameSource(request) {
|
|
651
|
+
const routeState = getRouteStateForKey(request.key, request.routeId);
|
|
652
|
+
const baseKey = parseRepeatItemInstanceKey(request.key)?.baseKey ?? request.key;
|
|
653
|
+
if (!routeState?.graph) {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
reason: "node_not_found",
|
|
657
|
+
diagnostics: [
|
|
658
|
+
{
|
|
659
|
+
code: "source-locator-warning",
|
|
660
|
+
message: `Key not found: ${request.key}`,
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const element = routeState.graph.elements.get(baseKey);
|
|
666
|
+
if (!element) {
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
reason: "node_not_found",
|
|
670
|
+
diagnostics: [
|
|
671
|
+
{
|
|
672
|
+
code: "source-locator-warning",
|
|
673
|
+
message: `Key not found: ${request.key}`,
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const source = _fileContentCache.get(element.sourceFile) ??
|
|
679
|
+
(await _fileSystem.readFile(element.sourceFile));
|
|
680
|
+
const current = extractClassNameSource({
|
|
681
|
+
key: baseKey,
|
|
682
|
+
route: {
|
|
683
|
+
routeId: routeState.routeId,
|
|
684
|
+
graph: routeState.graph,
|
|
685
|
+
source,
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
const result = await applyClassNameSourceWrite({
|
|
689
|
+
request,
|
|
690
|
+
current,
|
|
691
|
+
source,
|
|
692
|
+
writeSource: async (nextSource) => {
|
|
693
|
+
await _fileSystem.writeFile(element.sourceFile, nextSource);
|
|
694
|
+
_fileSystem.bumpVersion?.(element.sourceFile);
|
|
695
|
+
_fileContentCache.set(element.sourceFile, nextSource);
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
if (!result.ok) {
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
await rebuildRoutesForFiles([element.sourceFile], {
|
|
702
|
+
changed: true,
|
|
703
|
+
minimumVersion: request.expectedVersion.documentVersion + 1,
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
ok: true,
|
|
707
|
+
source: this.getClassNameSource(baseKey, routeState.routeId),
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
async execute(request) {
|
|
711
|
+
// 1. Find route
|
|
712
|
+
let routeState = null;
|
|
713
|
+
if (request.target.kind === "node") {
|
|
714
|
+
routeState = getRouteStateForKey(request.target.key, request.routeId);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
routeState = _routes.get(request.target.routeId) ?? null;
|
|
718
|
+
if (routeState && !routeState.graph) {
|
|
719
|
+
await ensureRouteBuilt(routeState.routeId);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (!routeState) {
|
|
723
|
+
return {
|
|
724
|
+
requestId: request.id,
|
|
725
|
+
kind: request.kind,
|
|
726
|
+
target: request.target,
|
|
727
|
+
ok: false,
|
|
728
|
+
resultVersion: _documentVersion,
|
|
729
|
+
error: {
|
|
730
|
+
code: request.target.kind === "route"
|
|
731
|
+
? OperationErrorCode.ROUTE_NOT_FOUND
|
|
732
|
+
: OperationErrorCode.NODE_NOT_FOUND,
|
|
733
|
+
message: request.target.kind === "route"
|
|
734
|
+
? `Route not found: ${request.target.routeId}`
|
|
735
|
+
: `Target key not found: ${request.target.key}`,
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (!routeState.graph ||
|
|
740
|
+
!routeState.topology ||
|
|
741
|
+
!routeState.capabilities) {
|
|
742
|
+
return {
|
|
743
|
+
requestId: request.id,
|
|
744
|
+
kind: request.kind,
|
|
745
|
+
target: request.target,
|
|
746
|
+
ok: false,
|
|
747
|
+
resultVersion: _documentVersion,
|
|
748
|
+
error: {
|
|
749
|
+
code: request.target.kind === "route"
|
|
750
|
+
? OperationErrorCode.ROUTE_NOT_FOUND
|
|
751
|
+
: OperationErrorCode.NODE_NOT_FOUND,
|
|
752
|
+
message: request.target.kind === "route"
|
|
753
|
+
? `Route not ready: ${request.target.routeId}`
|
|
754
|
+
: `Target key not found: ${request.target.key}`,
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
// 2. Plan via OperationPlanner
|
|
759
|
+
const graphAccess = createPlannerGraphAccess(routeState, routeState.graph, routeState.capabilities, _fileContentCache, _fileSystem);
|
|
760
|
+
const planResult = await new OperationPlanner(graphAccess).plan(request);
|
|
761
|
+
if (!planResult.ok) {
|
|
762
|
+
return {
|
|
763
|
+
requestId: request.id,
|
|
764
|
+
kind: request.kind,
|
|
765
|
+
target: request.target,
|
|
766
|
+
ok: false,
|
|
767
|
+
resultVersion: _documentVersion,
|
|
768
|
+
error: {
|
|
769
|
+
code: mapInternalErrorCode(planResult.errorCode),
|
|
770
|
+
message: planResult.reason,
|
|
771
|
+
},
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const rewritePlan = planResult.plan;
|
|
775
|
+
if (rewritePlan.steps.length === 0) {
|
|
776
|
+
return {
|
|
777
|
+
requestId: request.id,
|
|
778
|
+
kind: request.kind,
|
|
779
|
+
target: request.target,
|
|
780
|
+
ok: false,
|
|
781
|
+
resultVersion: _documentVersion,
|
|
782
|
+
error: {
|
|
783
|
+
code: OperationErrorCode.PLAN_FAILED,
|
|
784
|
+
message: "Planner could not generate a valid execution plan",
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
// 3. Delegate execute + snapshot + undo-stack to writeback-service
|
|
789
|
+
_mutationsInFlight += 1;
|
|
790
|
+
const internal = await _writebackService.execute(rewritePlan);
|
|
791
|
+
if (!internal.ok) {
|
|
792
|
+
_mutationsInFlight -= 1;
|
|
793
|
+
return {
|
|
794
|
+
requestId: request.id,
|
|
795
|
+
kind: request.kind,
|
|
796
|
+
target: request.target,
|
|
797
|
+
ok: false,
|
|
798
|
+
resultVersion: _documentVersion,
|
|
799
|
+
error: {
|
|
800
|
+
code: mapInternalErrorCode(internal.errorCode),
|
|
801
|
+
message: internal.reason,
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
// 4. Rebuild graph for affected files
|
|
806
|
+
try {
|
|
807
|
+
await rebuildRoutesForFiles(internal.affectedFiles, {
|
|
808
|
+
changed: true,
|
|
809
|
+
minimumVersion: internal.resultVersion,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
finally {
|
|
813
|
+
_mutationsInFlight -= 1;
|
|
814
|
+
}
|
|
815
|
+
const resultVersion = getAffectedRouteVersion(internal.affectedFiles, internal.resultVersion);
|
|
816
|
+
_documentVersion = Math.max(_documentVersion, resultVersion);
|
|
817
|
+
// 5. Emit events
|
|
818
|
+
const result = {
|
|
819
|
+
requestId: request.id,
|
|
820
|
+
kind: request.kind,
|
|
821
|
+
target: request.target,
|
|
822
|
+
ok: true,
|
|
823
|
+
resultVersion,
|
|
824
|
+
};
|
|
825
|
+
_eventBus.emit({ type: "operation-completed", result });
|
|
826
|
+
if (routeState.routeId) {
|
|
827
|
+
_eventBus.emit({
|
|
828
|
+
type: "document-changed",
|
|
829
|
+
routeId: routeState.routeId,
|
|
830
|
+
version: resultVersion,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return result;
|
|
834
|
+
},
|
|
835
|
+
async undo() {
|
|
836
|
+
_mutationsInFlight += 1;
|
|
837
|
+
const internal = await _writebackService.undo();
|
|
838
|
+
if (!internal.ok) {
|
|
839
|
+
_mutationsInFlight -= 1;
|
|
840
|
+
return {
|
|
841
|
+
requestId: "undo",
|
|
842
|
+
kind: "update-text",
|
|
843
|
+
target: { kind: "node", key: "" },
|
|
844
|
+
ok: false,
|
|
845
|
+
resultVersion: _documentVersion,
|
|
846
|
+
error: {
|
|
847
|
+
code: OperationErrorCode.INTERNAL_ERROR,
|
|
848
|
+
message: internal.reason,
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
await rebuildRoutesForFiles(internal.affectedFiles, {
|
|
854
|
+
changed: true,
|
|
855
|
+
minimumVersion: internal.resultVersion,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
finally {
|
|
859
|
+
_mutationsInFlight -= 1;
|
|
860
|
+
}
|
|
861
|
+
const resultVersion = getAffectedRouteVersion(internal.affectedFiles, internal.resultVersion);
|
|
862
|
+
_documentVersion = Math.max(_documentVersion, resultVersion);
|
|
863
|
+
return {
|
|
864
|
+
requestId: "undo",
|
|
865
|
+
kind: "update-text",
|
|
866
|
+
target: { kind: "node", key: "" },
|
|
867
|
+
ok: true,
|
|
868
|
+
resultVersion,
|
|
869
|
+
};
|
|
870
|
+
},
|
|
871
|
+
async redo() {
|
|
872
|
+
_mutationsInFlight += 1;
|
|
873
|
+
const internal = await _writebackService.redo();
|
|
874
|
+
if (!internal.ok) {
|
|
875
|
+
_mutationsInFlight -= 1;
|
|
876
|
+
return {
|
|
877
|
+
requestId: "redo",
|
|
878
|
+
kind: "update-text",
|
|
879
|
+
target: { kind: "node", key: "" },
|
|
880
|
+
ok: false,
|
|
881
|
+
resultVersion: _documentVersion,
|
|
882
|
+
error: {
|
|
883
|
+
code: OperationErrorCode.INTERNAL_ERROR,
|
|
884
|
+
message: internal.reason,
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
await rebuildRoutesForFiles(internal.affectedFiles, {
|
|
890
|
+
changed: true,
|
|
891
|
+
minimumVersion: internal.resultVersion,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
finally {
|
|
895
|
+
_mutationsInFlight -= 1;
|
|
896
|
+
}
|
|
897
|
+
const resultVersion = getAffectedRouteVersion(internal.affectedFiles, internal.resultVersion);
|
|
898
|
+
_documentVersion = Math.max(_documentVersion, resultVersion);
|
|
899
|
+
return {
|
|
900
|
+
requestId: "redo",
|
|
901
|
+
kind: "update-text",
|
|
902
|
+
target: { kind: "node", key: "" },
|
|
903
|
+
ok: true,
|
|
904
|
+
resultVersion,
|
|
905
|
+
};
|
|
906
|
+
},
|
|
907
|
+
notifyFileChanged(file) {
|
|
908
|
+
const affectedFiles = new Set();
|
|
909
|
+
// Find routes affected by this file change
|
|
910
|
+
for (const routeState of _routes.values()) {
|
|
911
|
+
if (routeState.entryFile === file ||
|
|
912
|
+
routeState.graph?.fileIndex.has(file)) {
|
|
913
|
+
affectedFiles.add(file);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
scheduleRouteRebuild(affectedFiles);
|
|
917
|
+
},
|
|
918
|
+
subscribe(listener) {
|
|
919
|
+
return _eventBus.subscribe(listener);
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
return engine;
|
|
923
|
+
}
|
|
924
|
+
// ── PlannerGraphAccess adapter ──────────────────────────────────────
|
|
925
|
+
/**
|
|
926
|
+
* Creates a PlannerGraphAccess from the engine's route state + file system.
|
|
927
|
+
* Maps protocol/engine types to the shapes planner.ts expects.
|
|
928
|
+
*/
|
|
929
|
+
function createPlannerGraphAccess(routeState, graph, capabilities, fileContentCache, fs) {
|
|
930
|
+
const repeatRootOwners = new Map();
|
|
931
|
+
const repeatTemplateOwners = new Map();
|
|
932
|
+
const visitRepeatTemplateNode = (key, ownerKey) => {
|
|
933
|
+
if (repeatTemplateOwners.has(key)) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
repeatTemplateOwners.set(key, ownerKey);
|
|
937
|
+
const element = graph.elements.get(key);
|
|
938
|
+
for (const childKey of element?.childKeys ?? []) {
|
|
939
|
+
visitRepeatTemplateNode(childKey, ownerKey);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
for (const region of graph.regions) {
|
|
943
|
+
if (region.kind !== "repeat-region")
|
|
944
|
+
continue;
|
|
945
|
+
for (const childKey of region.childKeys) {
|
|
946
|
+
repeatRootOwners.set(childKey, region.ownerKey);
|
|
947
|
+
visitRepeatTemplateNode(childKey, region.ownerKey);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
getNode(key) {
|
|
952
|
+
const resolved = resolvePlannerKey(key);
|
|
953
|
+
const element = graph.elements.get(resolved.baseKey);
|
|
954
|
+
if (!element)
|
|
955
|
+
return null;
|
|
956
|
+
return toNodeInfo(key, element, resolved.repeatIndex, resolved.richTextBlock);
|
|
957
|
+
},
|
|
958
|
+
getChildNodes(key) {
|
|
959
|
+
const resolved = resolvePlannerKey(key);
|
|
960
|
+
if (resolved.richTextBlock) {
|
|
961
|
+
return [];
|
|
962
|
+
}
|
|
963
|
+
const element = graph.elements.get(resolved.baseKey);
|
|
964
|
+
if (!element)
|
|
965
|
+
return [];
|
|
966
|
+
return element.childKeys
|
|
967
|
+
.map((childKey) => {
|
|
968
|
+
const child = graph.elements.get(childKey);
|
|
969
|
+
return child ? toNodeInfo(childKey, child) : null;
|
|
970
|
+
})
|
|
971
|
+
.filter((child) => child !== null);
|
|
972
|
+
},
|
|
973
|
+
getCapability(key) {
|
|
974
|
+
const resolved = resolvePlannerKey(key);
|
|
975
|
+
if (resolved.richTextBlock) {
|
|
976
|
+
return {
|
|
977
|
+
key,
|
|
978
|
+
canUpdateText: false,
|
|
979
|
+
canRemove: true,
|
|
980
|
+
canInsertChild: false,
|
|
981
|
+
canMove: true,
|
|
982
|
+
constraints: [],
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const cap = capabilities.get(resolved.baseKey);
|
|
986
|
+
if (!cap) {
|
|
987
|
+
return {
|
|
988
|
+
key,
|
|
989
|
+
canUpdateText: false,
|
|
990
|
+
canRemove: false,
|
|
991
|
+
canInsertChild: false,
|
|
992
|
+
canMove: false,
|
|
993
|
+
constraints: [],
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
key,
|
|
998
|
+
canUpdateText: cap.canUpdateText,
|
|
999
|
+
canRemove: cap.canRemove,
|
|
1000
|
+
canInsertChild: cap.canInsertChild,
|
|
1001
|
+
canMove: cap.canMove,
|
|
1002
|
+
constraints: cap.constraints.map((c) => c.message),
|
|
1003
|
+
};
|
|
1004
|
+
},
|
|
1005
|
+
getFileContent(file) {
|
|
1006
|
+
return fileContentCache.get(file) ?? "";
|
|
1007
|
+
},
|
|
1008
|
+
getFileVersion(file) {
|
|
1009
|
+
return Math.max(routeState.fileVersions.get(file) ?? 0, graph.version);
|
|
1010
|
+
},
|
|
1011
|
+
getRouteEntryFile(routeId) {
|
|
1012
|
+
return routeState.routeId === routeId ? routeState.entryFile : null;
|
|
1013
|
+
},
|
|
1014
|
+
getComponentSemantic(key) {
|
|
1015
|
+
const resolved = resolvePlannerKey(key);
|
|
1016
|
+
return (routeState.detailProjections?.get(resolved.baseKey)
|
|
1017
|
+
?.componentSemantic ?? null);
|
|
1018
|
+
},
|
|
1019
|
+
getEditableTextSegmentIndex(key) {
|
|
1020
|
+
const resolved = resolvePlannerKey(key);
|
|
1021
|
+
const iterator = routeState.detailProjections
|
|
1022
|
+
?.get(resolved.baseKey)
|
|
1023
|
+
?.textTargets.keys();
|
|
1024
|
+
const first = iterator?.next();
|
|
1025
|
+
return first && !first.done ? first.value : null;
|
|
1026
|
+
},
|
|
1027
|
+
async getTextWriteTarget(key, segmentIndex) {
|
|
1028
|
+
const resolved = resolvePlannerKey(key);
|
|
1029
|
+
return retargetRepeatTextWriteTarget(routeState.detailProjections
|
|
1030
|
+
?.get(resolved.baseKey)
|
|
1031
|
+
?.textTargets.get(segmentIndex) ?? null, resolved.repeatIndex);
|
|
1032
|
+
},
|
|
1033
|
+
getTextProvenance(key, segmentIndex) {
|
|
1034
|
+
const resolved = resolvePlannerKey(key);
|
|
1035
|
+
return (routeState.detailProjections
|
|
1036
|
+
?.get(resolved.baseKey)
|
|
1037
|
+
?.provenances.get(segmentIndex) ?? null);
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
function resolvePlannerKey(key) {
|
|
1041
|
+
const richTextBlock = parseRichTextBlockKey(key);
|
|
1042
|
+
if (richTextBlock) {
|
|
1043
|
+
return {
|
|
1044
|
+
baseKey: richTextBlock.ownerKey,
|
|
1045
|
+
repeatIndex: null,
|
|
1046
|
+
richTextBlock,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
const repeatInstance = parseRepeatItemInstanceKey(key);
|
|
1050
|
+
if (repeatInstance) {
|
|
1051
|
+
return {
|
|
1052
|
+
baseKey: repeatInstance.baseKey,
|
|
1053
|
+
repeatIndex: repeatInstance.repeatIndex,
|
|
1054
|
+
richTextBlock: null,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
return { baseKey: key, repeatIndex: null, richTextBlock: null };
|
|
1058
|
+
}
|
|
1059
|
+
function toNodeInfo(key, element, repeatIndex = null, richTextBlock = null) {
|
|
1060
|
+
const baseKey = richTextBlock?.ownerKey ??
|
|
1061
|
+
parseRepeatItemInstanceKey(key)?.baseKey ??
|
|
1062
|
+
key;
|
|
1063
|
+
const detail = routeState.detailProjections?.get(baseKey);
|
|
1064
|
+
return {
|
|
1065
|
+
key,
|
|
1066
|
+
file: element.sourceFile,
|
|
1067
|
+
component: element.componentName,
|
|
1068
|
+
tag: element.tag,
|
|
1069
|
+
structuralPath: element.identity.structuralPath,
|
|
1070
|
+
sourceRange: element.sourceRange,
|
|
1071
|
+
parentKey: element.parentKey ?? undefined,
|
|
1072
|
+
isRepeatRegion: element.boundaryKind === "repeat-region" ||
|
|
1073
|
+
repeatTemplateOwners.has(baseKey),
|
|
1074
|
+
isBoundary: false,
|
|
1075
|
+
importedFrom: element.importedFrom,
|
|
1076
|
+
exportName: element.exportName,
|
|
1077
|
+
isRepeatItemRoot: repeatIndex !== null || repeatRootOwners.has(baseKey),
|
|
1078
|
+
repeatOwnerKey: repeatRootOwners.get(baseKey) ?? repeatTemplateOwners.get(baseKey),
|
|
1079
|
+
contentModel: detail?.contentModel,
|
|
1080
|
+
richTextBlock: richTextBlock
|
|
1081
|
+
? {
|
|
1082
|
+
field: richTextBlock.field,
|
|
1083
|
+
index: richTextBlock.index,
|
|
1084
|
+
}
|
|
1085
|
+
: undefined,
|
|
1086
|
+
attributes: element.attributes?.map((attribute) => ({
|
|
1087
|
+
name: attribute.name,
|
|
1088
|
+
value: attribute.value,
|
|
1089
|
+
source: attribute.source
|
|
1090
|
+
? {
|
|
1091
|
+
expression: attribute.source.expression,
|
|
1092
|
+
base: attribute.source.base,
|
|
1093
|
+
path: attribute.source.path.map((segment) => ({ ...segment })),
|
|
1094
|
+
}
|
|
1095
|
+
: null,
|
|
1096
|
+
resolvedValue: attribute.resolvedValue,
|
|
1097
|
+
})),
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/** Maps writeback-service internal error codes to protocol OperationErrorCode. */
|
|
1102
|
+
function mapInternalErrorCode(code) {
|
|
1103
|
+
switch (code) {
|
|
1104
|
+
case "stale-version":
|
|
1105
|
+
return OperationErrorCode.VERSION_STALE;
|
|
1106
|
+
case "capability-denied":
|
|
1107
|
+
return OperationErrorCode.CAPABILITY_REJECTED;
|
|
1108
|
+
case "route-not-found":
|
|
1109
|
+
return OperationErrorCode.ROUTE_NOT_FOUND;
|
|
1110
|
+
case "unsupported-operation":
|
|
1111
|
+
return OperationErrorCode.UNSUPPORTED_OPERATION;
|
|
1112
|
+
case "invalid-target":
|
|
1113
|
+
return OperationErrorCode.INVALID_TARGET;
|
|
1114
|
+
case "invalid-params":
|
|
1115
|
+
return OperationErrorCode.INVALID_PARAMS;
|
|
1116
|
+
case "locator-miss":
|
|
1117
|
+
return OperationErrorCode.LOCATOR_FAILED;
|
|
1118
|
+
case "ast-error":
|
|
1119
|
+
return OperationErrorCode.AST_PARSE_ERROR;
|
|
1120
|
+
case "writeback-failed":
|
|
1121
|
+
return OperationErrorCode.WRITE_IO_ERROR;
|
|
1122
|
+
case "rollback-failed":
|
|
1123
|
+
return OperationErrorCode.ROLLBACK_FAILED;
|
|
1124
|
+
default:
|
|
1125
|
+
return OperationErrorCode.INTERNAL_ERROR;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function getErrorMessage(error) {
|
|
1129
|
+
if (error instanceof Error && error.message) {
|
|
1130
|
+
return error.message;
|
|
1131
|
+
}
|
|
1132
|
+
return String(error);
|
|
1133
|
+
}
|
|
1134
|
+
function getNextRouteLayoutCandidates(entryFile) {
|
|
1135
|
+
const normalized = normalizeRouteFile(entryFile);
|
|
1136
|
+
if (!nextRouteEntryPattern.test(normalized)) {
|
|
1137
|
+
return [];
|
|
1138
|
+
}
|
|
1139
|
+
const appRoot = normalized.startsWith("src/app/") ? "src/app" : "app";
|
|
1140
|
+
const routeDirectory = normalized.slice(0, normalized.lastIndexOf("/"));
|
|
1141
|
+
const directories = [];
|
|
1142
|
+
let current = routeDirectory;
|
|
1143
|
+
while (current === appRoot || current.startsWith(`${appRoot}/`)) {
|
|
1144
|
+
directories.unshift(current);
|
|
1145
|
+
if (current === appRoot) {
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
current = current.slice(0, current.lastIndexOf("/"));
|
|
1149
|
+
}
|
|
1150
|
+
return directories.flatMap((directory) => nextLayoutBasenames.map((basename) => `${directory}/${basename}`));
|
|
1151
|
+
}
|
|
1152
|
+
function normalizeRouteFile(file) {
|
|
1153
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1154
|
+
}
|
|
1155
|
+
function makeRepeatItemInstanceKey(baseKey, index) {
|
|
1156
|
+
return `${baseKey}${repeatItemInstanceKeyMarker}${index}`;
|
|
1157
|
+
}
|
|
1158
|
+
function makeRichTextBlockKey(ownerKey, field, index) {
|
|
1159
|
+
return `${ownerKey}${richTextBlockKeyMarker}${encodeURIComponent(field)}#${index}`;
|
|
1160
|
+
}
|
|
1161
|
+
function parseRichTextBlockKey(key) {
|
|
1162
|
+
if (typeof key !== "string") {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
const markerIndex = key.lastIndexOf(richTextBlockKeyMarker);
|
|
1166
|
+
if (markerIndex < 0) {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
const ownerKey = key.slice(0, markerIndex);
|
|
1170
|
+
const rest = key.slice(markerIndex + richTextBlockKeyMarker.length);
|
|
1171
|
+
const separatorIndex = rest.lastIndexOf("#");
|
|
1172
|
+
if (!ownerKey || separatorIndex <= 0) {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
const field = decodeURIComponent(rest.slice(0, separatorIndex));
|
|
1176
|
+
const index = Number(rest.slice(separatorIndex + 1));
|
|
1177
|
+
if (!field || !Number.isInteger(index) || index < 0) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
return { ownerKey, field, index };
|
|
1181
|
+
}
|
|
1182
|
+
function parseRepeatItemInstanceKey(key) {
|
|
1183
|
+
if (typeof key !== "string") {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
const markerIndex = key.lastIndexOf(repeatItemInstanceKeyMarker);
|
|
1187
|
+
if (markerIndex < 0) {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
const baseKey = key.slice(0, markerIndex);
|
|
1191
|
+
const repeatIndex = Number(key.slice(markerIndex + repeatItemInstanceKeyMarker.length));
|
|
1192
|
+
if (!baseKey || !Number.isInteger(repeatIndex) || repeatIndex < 0) {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
return { baseKey, repeatIndex };
|
|
1196
|
+
}
|
|
1197
|
+
function getRepeatItemCount(detail) {
|
|
1198
|
+
let count = 1;
|
|
1199
|
+
for (const target of detail?.textTargets.values() ?? []) {
|
|
1200
|
+
if (target?.kind !== "value-binding") {
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
if (target.sourceKind !== "repeat-template") {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
count = Math.max(count, target.collectionIndexMap?.length ?? 0, target.collectionPathMap?.length ?? 0);
|
|
1207
|
+
}
|
|
1208
|
+
return count;
|
|
1209
|
+
}
|
|
1210
|
+
function getRichTextBlocks(value) {
|
|
1211
|
+
if (typeof value === "string") {
|
|
1212
|
+
return value === "" ? [] : value.split(/\n{2,}/);
|
|
1213
|
+
}
|
|
1214
|
+
if (Array.isArray(value)) {
|
|
1215
|
+
return value.filter(isOperationValue);
|
|
1216
|
+
}
|
|
1217
|
+
if (isOperationObject(value) &&
|
|
1218
|
+
Array.isArray(value.content) &&
|
|
1219
|
+
value.content.every(isOperationValue)) {
|
|
1220
|
+
return value.content;
|
|
1221
|
+
}
|
|
1222
|
+
return [];
|
|
1223
|
+
}
|
|
1224
|
+
function isOperationObject(value) {
|
|
1225
|
+
return !Array.isArray(value) && typeof value === "object" && value !== null;
|
|
1226
|
+
}
|
|
1227
|
+
function isOperationValue(value) {
|
|
1228
|
+
if (value === null ||
|
|
1229
|
+
typeof value === "string" ||
|
|
1230
|
+
typeof value === "number" ||
|
|
1231
|
+
typeof value === "boolean") {
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
if (Array.isArray(value)) {
|
|
1235
|
+
return value.every(isOperationValue);
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof value === "object" && value !== null) {
|
|
1238
|
+
return Object.values(value).every(isOperationValue);
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
function getAttributeValue(element, field) {
|
|
1243
|
+
const attribute = element.attributes?.find((candidate) => candidate.name === field);
|
|
1244
|
+
if (!attribute) {
|
|
1245
|
+
return undefined;
|
|
1246
|
+
}
|
|
1247
|
+
if (attribute.resolvedValue !== undefined) {
|
|
1248
|
+
return attribute.resolvedValue;
|
|
1249
|
+
}
|
|
1250
|
+
return isOperationValue(attribute.value) ? attribute.value : undefined;
|
|
1251
|
+
}
|
|
1252
|
+
function cloneValuePath(path) {
|
|
1253
|
+
return path?.map((segment) => ({ ...segment }));
|
|
1254
|
+
}
|
|
1255
|
+
function retargetRepeatTextWriteTarget(target, repeatIndex) {
|
|
1256
|
+
if (repeatIndex === null ||
|
|
1257
|
+
target?.kind !== "value-binding" ||
|
|
1258
|
+
target.sourceKind !== "repeat-template") {
|
|
1259
|
+
return target;
|
|
1260
|
+
}
|
|
1261
|
+
const mappedIndex = target.collectionIndexMap?.[repeatIndex] ?? repeatIndex;
|
|
1262
|
+
const mappedPath = cloneValuePath(target.collectionPathMap?.[repeatIndex]) ??
|
|
1263
|
+
cloneValuePath(target.locator?.path);
|
|
1264
|
+
if (mappedPath) {
|
|
1265
|
+
for (let index = mappedPath.length - 1; index >= 0; index -= 1) {
|
|
1266
|
+
const segment = mappedPath[index];
|
|
1267
|
+
if (segment.kind === "index") {
|
|
1268
|
+
segment.index = mappedIndex;
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
...target,
|
|
1275
|
+
path: cloneValuePath(target.path),
|
|
1276
|
+
locator: target.locator
|
|
1277
|
+
? {
|
|
1278
|
+
...target.locator,
|
|
1279
|
+
path: mappedPath,
|
|
1280
|
+
}
|
|
1281
|
+
: target.locator,
|
|
1282
|
+
collectionIndexMap: target.collectionIndexMap
|
|
1283
|
+
? [...target.collectionIndexMap]
|
|
1284
|
+
: undefined,
|
|
1285
|
+
collectionPathMap: target.collectionPathMap?.map((path) => cloneValuePath(path) ?? []),
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function projectRepeatConcreteEntries(document, graph, detailProjections) {
|
|
1289
|
+
const repeatItemRoots = new Set();
|
|
1290
|
+
for (const region of graph.regions) {
|
|
1291
|
+
if (region.kind !== "repeat-region") {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
for (const childKey of region.childKeys) {
|
|
1295
|
+
repeatItemRoots.add(childKey);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (repeatItemRoots.size === 0) {
|
|
1299
|
+
return document;
|
|
1300
|
+
}
|
|
1301
|
+
const entryByKey = new Map(document.entries.map((entry) => [
|
|
1302
|
+
entry.key,
|
|
1303
|
+
{ ...entry, childKeys: [...entry.childKeys] },
|
|
1304
|
+
]));
|
|
1305
|
+
const entries = [];
|
|
1306
|
+
for (const originalEntry of document.entries) {
|
|
1307
|
+
const entry = entryByKey.get(originalEntry.key) ?? {
|
|
1308
|
+
...originalEntry,
|
|
1309
|
+
childKeys: [...originalEntry.childKeys],
|
|
1310
|
+
};
|
|
1311
|
+
entries.push(entry);
|
|
1312
|
+
if (!repeatItemRoots.has(entry.key)) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const repeatItemCount = getRepeatItemCount(detailProjections.get(entry.key));
|
|
1316
|
+
if (repeatItemCount <= 1) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const parentEntry = entry.parentKey
|
|
1320
|
+
? entryByKey.get(entry.parentKey)
|
|
1321
|
+
: null;
|
|
1322
|
+
const baseChildIndex = parentEntry?.childKeys.indexOf(entry.key) ?? -1;
|
|
1323
|
+
const virtualKeys = [];
|
|
1324
|
+
for (let index = 1; index < repeatItemCount; index += 1) {
|
|
1325
|
+
const virtualKey = makeRepeatItemInstanceKey(entry.key, index);
|
|
1326
|
+
virtualKeys.push(virtualKey);
|
|
1327
|
+
entries.push({
|
|
1328
|
+
...entry,
|
|
1329
|
+
key: virtualKey,
|
|
1330
|
+
parentKey: entry.parentKey,
|
|
1331
|
+
childKeys: [...entry.childKeys],
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
if (parentEntry && baseChildIndex >= 0) {
|
|
1335
|
+
parentEntry.childKeys.splice(baseChildIndex + 1, 0, ...virtualKeys);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return {
|
|
1339
|
+
...document,
|
|
1340
|
+
rootKeys: [...document.rootKeys],
|
|
1341
|
+
entries,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function projectRichTextBlockEntries(document, graph, detailProjections) {
|
|
1345
|
+
const entryByKey = new Map(document.entries.map((entry) => [
|
|
1346
|
+
entry.key,
|
|
1347
|
+
{ ...entry, childKeys: [...entry.childKeys] },
|
|
1348
|
+
]));
|
|
1349
|
+
const entries = [];
|
|
1350
|
+
let hasRichTextBlocks = false;
|
|
1351
|
+
for (const originalEntry of document.entries) {
|
|
1352
|
+
const entry = entryByKey.get(originalEntry.key) ?? {
|
|
1353
|
+
...originalEntry,
|
|
1354
|
+
childKeys: [...originalEntry.childKeys],
|
|
1355
|
+
};
|
|
1356
|
+
entries.push(entry);
|
|
1357
|
+
const contentModel = detailProjections.get(entry.key)?.contentModel;
|
|
1358
|
+
if (!contentModel?.operationBoundary.includes("remove-rich-text-block")) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const element = graph.elements.get(entry.key);
|
|
1362
|
+
if (!element) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const blocks = getRichTextBlocks(getAttributeValue(element, contentModel.field));
|
|
1366
|
+
if (blocks.length === 0) {
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const blockKeys = blocks.map((_, index) => makeRichTextBlockKey(entry.key, contentModel.field, index));
|
|
1370
|
+
entry.childKeys.push(...blockKeys);
|
|
1371
|
+
for (let index = 0; index < blockKeys.length; index += 1) {
|
|
1372
|
+
entries.push({
|
|
1373
|
+
key: blockKeys[index],
|
|
1374
|
+
tag: "rich-text-block",
|
|
1375
|
+
label: `${contentModel.field} block ${index + 1}`,
|
|
1376
|
+
parentKey: entry.key,
|
|
1377
|
+
childKeys: [],
|
|
1378
|
+
operationSummary: {
|
|
1379
|
+
canUpdateText: false,
|
|
1380
|
+
canInsertChild: false,
|
|
1381
|
+
canMove: true,
|
|
1382
|
+
canRemove: true,
|
|
1383
|
+
},
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
hasRichTextBlocks = true;
|
|
1387
|
+
}
|
|
1388
|
+
return hasRichTextBlocks
|
|
1389
|
+
? { ...document, rootKeys: [...document.rootKeys], entries }
|
|
1390
|
+
: document;
|
|
1391
|
+
}
|