@bettercms-ai/convert 0.6.0 → 0.8.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/CHANGELOG.md +56 -0
- package/README.md +114 -4
- package/dist/cli.js +1850 -797
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +250 -4
- package/dist/index.js +1859 -801
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -141,195 +141,80 @@ function propsAttribute(props) {
|
|
|
141
141
|
return `data-bcms-props="${value}"`;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
// src/
|
|
145
|
-
|
|
146
|
-
var
|
|
147
|
-
var
|
|
148
|
-
var
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!path) throw new Error(`The ${dialect} dialect reads its values at publish time, not at build time.`);
|
|
165
|
-
const layout = relativeImport(path, `${CONTENT_DIR}/layout.json`);
|
|
166
|
-
return `${HEADER}${HELPER_VERSION}
|
|
167
|
-
// Values come from bcms-content/*.json, written into the tree before the build. The committed
|
|
168
|
-
// stubs are {} \u2014 a build with them renders every fallback below and still succeeds.
|
|
169
|
-
import layoutSnapshot from "${layout}";
|
|
170
|
-
|
|
171
|
-
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
172
|
-
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
173
|
-
|
|
174
|
-
/** Dotted keys and \`[n]\` indices, positionally \u2014 the editor's own path grammar. */
|
|
175
|
-
function readPath(root: unknown, path: string): unknown {
|
|
176
|
-
let cursor: unknown = root;
|
|
177
|
-
for (const segment of path.split(".")) {
|
|
178
|
-
if (!segment) return undefined;
|
|
179
|
-
const key = segment.replace(/\\[\\d+\\]/g, "");
|
|
180
|
-
if (key) {
|
|
181
|
-
if (!isRecord(cursor)) return undefined;
|
|
182
|
-
cursor = cursor[key];
|
|
144
|
+
// src/declarations.ts
|
|
145
|
+
var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
|
|
146
|
+
var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
|
|
147
|
+
var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
|
|
148
|
+
var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{(?:`[^`]*`|[^}])*\}\}/g;
|
|
149
|
+
var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
|
|
150
|
+
const [address, kind] = entry.split("|");
|
|
151
|
+
if (!address) return [];
|
|
152
|
+
const scope = address.startsWith("layout:") ? "layout" : "page";
|
|
153
|
+
return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
|
|
154
|
+
});
|
|
155
|
+
var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
|
|
156
|
+
var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
|
|
157
|
+
var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
|
|
158
|
+
function declarationsIn(roots, content) {
|
|
159
|
+
const out = [];
|
|
160
|
+
const visit = (node) => {
|
|
161
|
+
if (node.type === "expr") {
|
|
162
|
+
for (const child of node.children) visit(child);
|
|
163
|
+
return;
|
|
183
164
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
165
|
+
if (node.type !== "element") return;
|
|
166
|
+
const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
|
|
167
|
+
const at = node.attrInsertAt;
|
|
168
|
+
for (const attr of node.attrs) {
|
|
169
|
+
const name = attr.name.replace(/^:/, "");
|
|
170
|
+
if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
|
|
171
|
+
const scope = name === "data-bcms-layout-field" ? "layout" : "page";
|
|
172
|
+
const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
|
|
173
|
+
const templated = template?.[1] ?? template?.[2];
|
|
174
|
+
if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
|
|
175
|
+
else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
|
|
176
|
+
} else if (attr.name === "data-bcms-props") {
|
|
177
|
+
out.push(...propsDeclarations(attr.value, at));
|
|
178
|
+
} else if (attr.name === "bcmsBindings") {
|
|
179
|
+
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
180
|
+
out.push({ scope: "page", path, kind: "", at, via: "prop" });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
return cursor;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** A stored value as a template renders it: richtext is its html, an image is its url. */
|
|
193
|
-
function render(value: unknown): string | undefined {
|
|
194
|
-
if (typeof value === "string") return value;
|
|
195
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
196
|
-
if (isRecord(value)) {
|
|
197
|
-
if (typeof value.html === "string") return value.html;
|
|
198
|
-
if (typeof value.url === "string") return value.url;
|
|
199
|
-
}
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** One field of one page. \`slug\` selects the entry on a dynamic route. */
|
|
204
|
-
export function bcms(page: unknown, path: string, fallback: string, slug?: string): string {
|
|
205
|
-
const entries = isRecord(page) ? page.entries : undefined;
|
|
206
|
-
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
207
|
-
return render(readPath(root, path)) ?? fallback;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** One field of the shared layout \u2014 the header and footer every route renders. */
|
|
211
|
-
export function bcmsLayout(path: string, fallback: string): string {
|
|
212
|
-
return render(readPath(layoutSnapshot, path)) ?? fallback;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* The ROWS of a repeater. \`fallback\` is every row the template shipped with, so a build against
|
|
217
|
-
* an empty snapshot still renders the same number of cards it always did.
|
|
218
|
-
*/
|
|
219
|
-
export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: string): T[] {
|
|
220
|
-
const entries = isRecord(page) ? page.entries : undefined;
|
|
221
|
-
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
222
|
-
const value = readPath(root, path);
|
|
223
|
-
return Array.isArray(value) ? (value as T[]) : fallback;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* ONE SECTION's copy, for a component extracted by \`--componentize\`.
|
|
228
|
-
*
|
|
229
|
-
* OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
|
|
230
|
-
* (Phase B) carries an override for every prop it declares, so the group underneath it is never
|
|
231
|
-
* read; a placement still bound to a page field group (Phase A) carries none, so the group is all
|
|
232
|
-
* there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
|
|
233
|
-
* instance does not hold has to keep rendering the page's value rather than nothing.
|
|
234
|
-
*
|
|
235
|
-
* \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
|
|
236
|
-
* the page's old copy back into a heading somebody had deliberately cleared.
|
|
237
|
-
*
|
|
238
|
-
* With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
|
|
239
|
-
* the fallback the template shipped with \u2014 which is why a section renders correctly against the
|
|
240
|
-
* committed \`{}\` stub.
|
|
241
|
-
*/
|
|
242
|
-
export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
|
|
243
|
-
const group = bind ? readPath(page, bind) : undefined;
|
|
244
|
-
const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
|
|
245
|
-
if (!isRecord(overrides)) return base;
|
|
246
|
-
for (const key of Object.keys(overrides)) {
|
|
247
|
-
const value = overrides[key];
|
|
248
|
-
if (value === undefined || value === null) continue;
|
|
249
|
-
/**
|
|
250
|
-
* \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
|
|
251
|
-
* dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
|
|
252
|
-
* helper fell back to the rows the template shipped with and the editor's work never showed.
|
|
253
|
-
*/
|
|
254
|
-
base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
|
|
255
|
-
}
|
|
256
|
-
return base;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
|
|
261
|
-
*
|
|
262
|
-
* An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
|
|
263
|
-
* copy lives. A placement that owns its copy declares the BLOCK lane \u2014
|
|
264
|
-
* \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
|
|
265
|
-
* own value to. A placement bound to a page field group declares the PAGE path, which is what the
|
|
266
|
-
* page form and the coverage meter already know.
|
|
267
|
-
*
|
|
268
|
-
* \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
|
|
269
|
-
* page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
|
|
270
|
-
* second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
|
|
271
|
-
* Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
|
|
272
|
-
* \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
|
|
273
|
-
* the meter counted a binding that was never there. The prefix the markup was cut with comes out,
|
|
274
|
-
* the placement's own group goes in.
|
|
275
|
-
*
|
|
276
|
-
* \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
|
|
277
|
-
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
278
|
-
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
279
|
-
*/
|
|
280
|
-
export function bcmsFields(
|
|
281
|
-
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
282
|
-
pathPrefix: string,
|
|
283
|
-
): (path: string) => string {
|
|
284
|
-
const id = placement.blockId;
|
|
285
|
-
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
286
|
-
/**
|
|
287
|
-
* What replaces the prefix the markup was cut with, or null for "leave every path alone".
|
|
288
|
-
*
|
|
289
|
-
* Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
|
|
290
|
-
* nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
|
|
291
|
-
* where the plan said the prop key IS the whole page path: there is no group segment to swap,
|
|
292
|
-
* so prepending one would invent an address rather than correct it.
|
|
293
|
-
*/
|
|
294
|
-
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
295
|
-
return (path: string): string => {
|
|
296
|
-
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
297
|
-
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
298
|
-
// (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
|
|
299
|
-
if (lane === null || !path.startsWith(pathPrefix)) return path;
|
|
300
|
-
return lane + path.slice(pathPrefix.length);
|
|
184
|
+
for (const child of node.children) visit(child);
|
|
301
185
|
};
|
|
186
|
+
for (const root of roots) visit(root);
|
|
187
|
+
return out;
|
|
302
188
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
|
|
189
|
+
var bindingsPaths = (text) => [
|
|
190
|
+
...[...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]),
|
|
191
|
+
...[...text.matchAll(/`([^`]+)`/g)].map((match) => shapeOfPath(match[1]))
|
|
192
|
+
];
|
|
193
|
+
var scopedPath = (scope, path) => `${scope}::${path}`;
|
|
194
|
+
var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
|
|
195
|
+
var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
|
|
196
|
+
var scannedDeclarations = (content) => [...declaredByScan(content)].map((entry) => {
|
|
197
|
+
const [scope, ...rest] = entry.split("::");
|
|
198
|
+
return { scope, path: rest.join("::") };
|
|
199
|
+
});
|
|
200
|
+
function declaredByScan(content) {
|
|
201
|
+
const out = /* @__PURE__ */ new Set();
|
|
202
|
+
const add = (scope, path) => void out.add(scopedPath(scope, path));
|
|
203
|
+
for (const match of content.matchAll(BINDING_ATTR)) {
|
|
204
|
+
add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
|
|
320
205
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
function isKnownHelper(content, dialect) {
|
|
324
|
-
if (content === helperSource(dialect)) return HELPER_VERSION;
|
|
325
|
-
const path = HELPER_PATH[dialect];
|
|
326
|
-
if (!path) return null;
|
|
327
|
-
const digest = sha256(content);
|
|
328
|
-
for (const [version, known] of Object.entries(HISTORICAL[path] ?? {})) {
|
|
329
|
-
if (known === digest) return Number(version);
|
|
206
|
+
for (const match of content.matchAll(TEMPLATE_SCAN)) {
|
|
207
|
+
add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
|
|
330
208
|
}
|
|
331
|
-
|
|
209
|
+
for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
|
|
210
|
+
for (const match of content.matchAll(PROPS_ATTR))
|
|
211
|
+
for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
|
|
212
|
+
return out;
|
|
332
213
|
}
|
|
214
|
+
var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
|
|
215
|
+
|
|
216
|
+
// src/sites/jsx.ts
|
|
217
|
+
import { parse as parseModule } from "@babel/parser";
|
|
333
218
|
|
|
334
219
|
// src/entities.ts
|
|
335
220
|
var LONGEST_ENTITY = 32;
|
|
@@ -2667,202 +2552,6 @@ function decodeStringEscapes(body) {
|
|
|
2667
2552
|
}
|
|
2668
2553
|
var unwrapJsxStrings = (value) => value.replace(JSX_STRING_CHILD, (_whole, _quote, body) => decodeStringEscapes(body));
|
|
2669
2554
|
|
|
2670
|
-
// src/locate.ts
|
|
2671
|
-
var flat = (value) => value.replace(/\s+/g, " ").trim();
|
|
2672
|
-
var stripTags = (value) => value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "");
|
|
2673
|
-
function locate(brief, sources, options) {
|
|
2674
|
-
const flattened = sources.map((s) => {
|
|
2675
|
-
const readable = unwrapJsxStrings(decodeEntities(s.content));
|
|
2676
|
-
return {
|
|
2677
|
-
path: s.path,
|
|
2678
|
-
content: flat(readable),
|
|
2679
|
-
text: options?.stripTags ? flat(stripTags(readable)) : null
|
|
2680
|
-
};
|
|
2681
|
-
});
|
|
2682
|
-
const located = [];
|
|
2683
|
-
const unlocated = [];
|
|
2684
|
-
for (const page of brief.pages) {
|
|
2685
|
-
for (const p of page.paths) {
|
|
2686
|
-
const original = p.original === null ? "" : flat(p.original);
|
|
2687
|
-
if (!original) {
|
|
2688
|
-
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
|
|
2689
|
-
continue;
|
|
2690
|
-
}
|
|
2691
|
-
const files = flattened.filter((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
|
|
2692
|
-
if (files.length === 0) {
|
|
2693
|
-
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
|
|
2694
|
-
continue;
|
|
2695
|
-
}
|
|
2696
|
-
located.push({ route: page.route, scope: p.scope, path: p.path, kind: p.kind, original, files });
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
return { located, unlocated };
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
// src/loops.ts
|
|
2703
|
-
import MagicString from "magic-string";
|
|
2704
|
-
|
|
2705
|
-
// src/declarations.ts
|
|
2706
|
-
var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
|
|
2707
|
-
var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
|
|
2708
|
-
var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
|
|
2709
|
-
var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
|
|
2710
|
-
var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
|
|
2711
|
-
const [address, kind] = entry.split("|");
|
|
2712
|
-
if (!address) return [];
|
|
2713
|
-
const scope = address.startsWith("layout:") ? "layout" : "page";
|
|
2714
|
-
return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
|
|
2715
|
-
});
|
|
2716
|
-
var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
|
|
2717
|
-
var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
|
|
2718
|
-
var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
|
|
2719
|
-
function declarationsIn(roots, content) {
|
|
2720
|
-
const out = [];
|
|
2721
|
-
const visit = (node) => {
|
|
2722
|
-
if (node.type === "expr") {
|
|
2723
|
-
for (const child of node.children) visit(child);
|
|
2724
|
-
return;
|
|
2725
|
-
}
|
|
2726
|
-
if (node.type !== "element") return;
|
|
2727
|
-
const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
|
|
2728
|
-
const at = node.attrInsertAt;
|
|
2729
|
-
for (const attr of node.attrs) {
|
|
2730
|
-
const name = attr.name.replace(/^:/, "");
|
|
2731
|
-
if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
|
|
2732
|
-
const scope = name === "data-bcms-layout-field" ? "layout" : "page";
|
|
2733
|
-
const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
|
|
2734
|
-
const templated = template?.[1] ?? template?.[2];
|
|
2735
|
-
if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
|
|
2736
|
-
else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
|
|
2737
|
-
} else if (attr.name === "data-bcms-props") {
|
|
2738
|
-
out.push(...propsDeclarations(attr.value, at));
|
|
2739
|
-
} else if (attr.name === "bcmsBindings") {
|
|
2740
|
-
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
2741
|
-
out.push({ scope: "page", path, kind: "", at, via: "prop" });
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
for (const child of node.children) visit(child);
|
|
2746
|
-
};
|
|
2747
|
-
for (const root of roots) visit(root);
|
|
2748
|
-
return out;
|
|
2749
|
-
}
|
|
2750
|
-
var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
|
|
2751
|
-
var scopedPath = (scope, path) => `${scope}::${path}`;
|
|
2752
|
-
var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
|
|
2753
|
-
var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
|
|
2754
|
-
var scannedDeclarations = (content) => [...declaredByScan(content)].map((entry) => {
|
|
2755
|
-
const [scope, ...rest] = entry.split("::");
|
|
2756
|
-
return { scope, path: rest.join("::") };
|
|
2757
|
-
});
|
|
2758
|
-
function declaredByScan(content) {
|
|
2759
|
-
const out = /* @__PURE__ */ new Set();
|
|
2760
|
-
const add = (scope, path) => void out.add(scopedPath(scope, path));
|
|
2761
|
-
for (const match of content.matchAll(BINDING_ATTR)) {
|
|
2762
|
-
add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
|
|
2763
|
-
}
|
|
2764
|
-
for (const match of content.matchAll(TEMPLATE_SCAN)) {
|
|
2765
|
-
add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
|
|
2766
|
-
}
|
|
2767
|
-
for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
|
|
2768
|
-
for (const match of content.matchAll(PROPS_ATTR))
|
|
2769
|
-
for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
|
|
2770
|
-
return out;
|
|
2771
|
-
}
|
|
2772
|
-
var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
|
|
2773
|
-
|
|
2774
|
-
// src/scope.ts
|
|
2775
|
-
function patternNames(node, out) {
|
|
2776
|
-
if (!node || typeof node !== "object") return;
|
|
2777
|
-
const typed = node;
|
|
2778
|
-
switch (typed.type) {
|
|
2779
|
-
case "Identifier":
|
|
2780
|
-
out.add(String(typed.name));
|
|
2781
|
-
return;
|
|
2782
|
-
case "ObjectPattern":
|
|
2783
|
-
for (const property of typed.properties ?? [])
|
|
2784
|
-
patternNames(property.type === "RestElement" ? property.argument : property.value, out);
|
|
2785
|
-
return;
|
|
2786
|
-
case "ArrayPattern":
|
|
2787
|
-
for (const element of typed.elements ?? []) patternNames(element, out);
|
|
2788
|
-
return;
|
|
2789
|
-
case "AssignmentPattern":
|
|
2790
|
-
patternNames(typed.left, out);
|
|
2791
|
-
return;
|
|
2792
|
-
case "RestElement":
|
|
2793
|
-
patternNames(typed.argument, out);
|
|
2794
|
-
return;
|
|
2795
|
-
default:
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
function statementNames(node, out) {
|
|
2800
|
-
if (!node || typeof node !== "object") return;
|
|
2801
|
-
const typed = node;
|
|
2802
|
-
switch (typed.type) {
|
|
2803
|
-
case "ExportNamedDeclaration":
|
|
2804
|
-
case "ExportDefaultDeclaration":
|
|
2805
|
-
statementNames(typed.declaration, out);
|
|
2806
|
-
return;
|
|
2807
|
-
case "ImportDeclaration":
|
|
2808
|
-
for (const specifier of typed.specifiers ?? []) patternNames(specifier.local, out);
|
|
2809
|
-
return;
|
|
2810
|
-
case "VariableDeclaration":
|
|
2811
|
-
for (const declarator of typed.declarations ?? []) patternNames(declarator.id, out);
|
|
2812
|
-
return;
|
|
2813
|
-
case "FunctionDeclaration":
|
|
2814
|
-
case "ClassDeclaration":
|
|
2815
|
-
case "TSTypeAliasDeclaration":
|
|
2816
|
-
case "TSInterfaceDeclaration":
|
|
2817
|
-
case "TSEnumDeclaration":
|
|
2818
|
-
case "TSModuleDeclaration":
|
|
2819
|
-
patternNames(typed.id, out);
|
|
2820
|
-
return;
|
|
2821
|
-
default:
|
|
2822
|
-
return;
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
var DECLARED = /\b(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
|
|
2826
|
-
var IMPORTED = /\bimport\s+([^;\n]*?)\s+from\b/g;
|
|
2827
|
-
function scanNames(code, out) {
|
|
2828
|
-
for (const match of code.matchAll(DECLARED)) out.add(match[1]);
|
|
2829
|
-
for (const match of code.matchAll(IMPORTED))
|
|
2830
|
-
for (const name of match[1].match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
2831
|
-
if (name !== "as" && name !== "type") out.add(name);
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
function scriptText(nodes, content, out) {
|
|
2835
|
-
for (const node of nodes) {
|
|
2836
|
-
if (node.type !== "element") continue;
|
|
2837
|
-
if (node.raw && node.tag === "script" && node.inner) out.push(content.slice(node.inner.start, node.inner.end));
|
|
2838
|
-
scriptText(node.children, content, out);
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
function topLevelBindings(dialect, parsed, content) {
|
|
2842
|
-
const out = /* @__PURE__ */ new Set();
|
|
2843
|
-
const program = parsed.ast?.program;
|
|
2844
|
-
if (program) for (const statement of program.body ?? []) statementNames(statement, out);
|
|
2845
|
-
else if (parsed.script) scanNames(parsed.script.code, out);
|
|
2846
|
-
if (dialect === "html") {
|
|
2847
|
-
const scripts = [];
|
|
2848
|
-
scriptText(parsed.roots, content, scripts);
|
|
2849
|
-
for (const code of scripts) scanNames(code, out);
|
|
2850
|
-
}
|
|
2851
|
-
return out;
|
|
2852
|
-
}
|
|
2853
|
-
function allocateNames(taken, wanted) {
|
|
2854
|
-
const used = new Set(taken);
|
|
2855
|
-
const out = /* @__PURE__ */ new Map();
|
|
2856
|
-
for (const { key, name } of wanted) {
|
|
2857
|
-
if (out.has(key)) continue;
|
|
2858
|
-
let candidate = name;
|
|
2859
|
-
for (let n = 1; used.has(candidate); n++) candidate = `${name}_${n}`;
|
|
2860
|
-
used.add(candidate);
|
|
2861
|
-
out.set(key, candidate);
|
|
2862
|
-
}
|
|
2863
|
-
return out;
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
2555
|
// src/sites/shared.ts
|
|
2867
2556
|
var isIntrinsic = (tag) => tag !== "" && !tag.includes(".") && tag[0] === tag[0].toLowerCase();
|
|
2868
2557
|
var IMAGE_SOURCES = {
|
|
@@ -2903,7 +2592,7 @@ function renderedText(node) {
|
|
|
2903
2592
|
var attrsOf = (node) => Object.fromEntries(node.attrs.map((a) => [a.name, a.value]));
|
|
2904
2593
|
function soleTextChild(node) {
|
|
2905
2594
|
const texts = node.children.filter((c) => c.type === "text" && !isBlank(c.value));
|
|
2906
|
-
return texts.length === 1 ? texts[0]
|
|
2595
|
+
return texts.length === 1 ? texts[0] : null;
|
|
2907
2596
|
}
|
|
2908
2597
|
function collectSites(roots, literals) {
|
|
2909
2598
|
const wanted = new Set(literals.map(norm));
|
|
@@ -2963,13 +2652,39 @@ function collectSites(roots, literals) {
|
|
|
2963
2652
|
scope: "page",
|
|
2964
2653
|
existingAttrs: attrs,
|
|
2965
2654
|
mixed: node.children.some((c) => c.type !== "text"),
|
|
2966
|
-
textRange: soleTextChild(node),
|
|
2655
|
+
textRange: soleTextChild(node)?.range ?? null,
|
|
2967
2656
|
literal,
|
|
2968
2657
|
partial: false,
|
|
2969
2658
|
ancestors: walk.ancestors,
|
|
2970
2659
|
siblingIndex: walk.siblingIndex
|
|
2971
2660
|
});
|
|
2972
2661
|
}
|
|
2662
|
+
const own = soleTextChild(node);
|
|
2663
|
+
if (own && node.children.some((c) => c.type !== "text")) {
|
|
2664
|
+
const value = norm(own.value);
|
|
2665
|
+
for (const literal of wanted) {
|
|
2666
|
+
if (matched.has(literal) || value !== literal) continue;
|
|
2667
|
+
matched.add(literal);
|
|
2668
|
+
sites.push({
|
|
2669
|
+
file: "",
|
|
2670
|
+
tag: node.tag,
|
|
2671
|
+
attrInsertAt: node.attrInsertAt,
|
|
2672
|
+
// The TEXT NODE's bytes, not the element's: two values inside one element must not be
|
|
2673
|
+
// read as claiming the same span. @see overlapping
|
|
2674
|
+
range: own.range,
|
|
2675
|
+
inner: node.inner,
|
|
2676
|
+
where: "text",
|
|
2677
|
+
scope: "page",
|
|
2678
|
+
existingAttrs: attrs,
|
|
2679
|
+
mixed: true,
|
|
2680
|
+
textRange: own.range,
|
|
2681
|
+
literal,
|
|
2682
|
+
partial: false,
|
|
2683
|
+
ancestors: walk.ancestors,
|
|
2684
|
+
siblingIndex: walk.siblingIndex
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2973
2688
|
const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
|
|
2974
2689
|
for (const attr of node.attrs) {
|
|
2975
2690
|
if (attr.expression || !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
|
|
@@ -3036,7 +2751,7 @@ function partials(node, literals, walk, out) {
|
|
|
3036
2751
|
scope: "page",
|
|
3037
2752
|
existingAttrs: attrsOf(node),
|
|
3038
2753
|
mixed: node.children.some((c) => c.type !== "text"),
|
|
3039
|
-
textRange: soleTextChild(node),
|
|
2754
|
+
textRange: soleTextChild(node)?.range ?? null,
|
|
3040
2755
|
literal,
|
|
3041
2756
|
partial: true,
|
|
3042
2757
|
ancestors: walk.ancestors,
|
|
@@ -3056,264 +2771,32 @@ function wordBounded(haystack, needle) {
|
|
|
3056
2771
|
return false;
|
|
3057
2772
|
}
|
|
3058
2773
|
|
|
3059
|
-
// src/
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
attrInsertAt: child.attrInsertAt,
|
|
3086
|
-
// `:key` in vue, `key` in jsx — one existing key, whichever way it is spelled.
|
|
3087
|
-
keyAttr: child.attrs.find((a) => a.name.replace(/^:/, "") === "key")?.range ?? null,
|
|
3088
|
-
node: child,
|
|
3089
|
-
members: inside
|
|
3090
|
-
});
|
|
3091
|
-
}
|
|
3092
|
-
if (rows.length === 0) return null;
|
|
3093
|
-
const covered = new Set(rows.flatMap((r) => r.members.map((m) => m.index)));
|
|
3094
|
-
if (covered.size !== wanted.size || rows.length !== wanted.size) return null;
|
|
3095
|
-
if (rows.some((r) => r.members.length !== members.length / wanted.size)) return null;
|
|
3096
|
-
return rows.sort((a, b) => a.range.start - b.range.start);
|
|
3097
|
-
};
|
|
3098
|
-
for (const root of roots) {
|
|
3099
|
-
const found = search(root);
|
|
3100
|
-
if (found) return found;
|
|
3101
|
-
}
|
|
3102
|
-
return null;
|
|
3103
|
-
}
|
|
3104
|
-
var consecutive = (rows, content) => rows.every((row, at) => at === 0 || content.slice(rows[at - 1].range.end, row.range.start).trim() === "");
|
|
3105
|
-
function maskedRow(row, content) {
|
|
3106
|
-
const masked = new Map(row.members.map((member) => [member.site.inner.start, member.leaf]));
|
|
3107
|
-
return canonical(row.node, content, masked);
|
|
3108
|
-
}
|
|
3109
|
-
var TRIVIA = "";
|
|
3110
|
-
function canonical(node, content, masked) {
|
|
3111
|
-
if (node.type === "text") {
|
|
3112
|
-
return node.value.trim() === "" ? TRIVIA : content.slice(node.range.start, node.range.end);
|
|
3113
|
-
}
|
|
3114
|
-
if (node.type === "comment" || node.type === "expr") {
|
|
3115
|
-
return content.slice(node.range.start, node.range.end);
|
|
3116
|
-
}
|
|
3117
|
-
if (node.type !== "element") return "";
|
|
3118
|
-
const open = content.slice(node.start, node.inner ? node.inner.start : node.attrInsertAt);
|
|
3119
|
-
if (node.inner && masked.has(node.inner.start)) {
|
|
3120
|
-
return `${open}\0${masked.get(node.inner.start)}\0</${node.tag}>`;
|
|
3121
|
-
}
|
|
3122
|
-
const inside = node.children.map((child) => canonical(child, content, masked)).join("");
|
|
3123
|
-
return node.inner ? `${open}${inside}</${node.tag}>` : open;
|
|
3124
|
-
}
|
|
3125
|
-
function sameShape(rows, content) {
|
|
3126
|
-
const first = maskedRow(rows[0], content);
|
|
3127
|
-
return rows.every((row) => maskedRow(row, content) === first);
|
|
3128
|
-
}
|
|
3129
|
-
var indented = (body, indent) => body.split("\n").map((line) => line.startsWith(indent) ? `${indent} ${line.slice(indent.length)}` : `${indent} ${line}`).join("\n");
|
|
3130
|
-
function indentOf(content, at) {
|
|
3131
|
-
const lineStart = content.lastIndexOf("\n", at - 1) + 1;
|
|
3132
|
-
const prefix = content.slice(lineStart, at);
|
|
3133
|
-
return prefix.trim() === "" ? prefix : "";
|
|
3134
|
-
}
|
|
3135
|
-
function writeLeaf(source, shift, member, rule, read, row, literal) {
|
|
3136
|
-
const site = member.site;
|
|
3137
|
-
const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
|
|
3138
|
-
if (member.kind === "richtext" && rule.richtext) {
|
|
3139
|
-
source.remove(site.inner.start + shift, site.inner.end + shift);
|
|
3140
|
-
source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
|
|
3141
|
-
} else {
|
|
3142
|
-
source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
|
|
3143
|
-
}
|
|
3144
|
-
const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
|
|
3145
|
-
source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
|
|
3146
|
-
}
|
|
3147
|
-
function emitNested(rule, array, members, prefix, outer, parent, ctx) {
|
|
3148
|
-
if (!rule.loop || !rule.text) return null;
|
|
3149
|
-
const inner = [];
|
|
3150
|
-
for (const member of members) {
|
|
3151
|
-
const split = splitNested(member.leaf);
|
|
3152
|
-
if (!split || splitNested(split.leaf) !== null) return null;
|
|
3153
|
-
inner.push({ ...member, index: split.index, leaf: split.leaf });
|
|
3154
|
-
}
|
|
3155
|
-
const rows = findRows([parent.node], inner);
|
|
3156
|
-
if (!rows || rows.length === 0) return null;
|
|
3157
|
-
if (!consecutive(rows, ctx.content)) return null;
|
|
3158
|
-
if (!sameShape(rows, ctx.content)) return null;
|
|
3159
|
-
const template = rows[0];
|
|
3160
|
-
const used = /* @__PURE__ */ new Set([
|
|
3161
|
-
...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
|
|
3162
|
-
outer.row,
|
|
3163
|
-
outer.index
|
|
3164
|
-
]);
|
|
3165
|
-
const allocated = allocateNames(used, [
|
|
3166
|
-
{ key: "row", name: "item" },
|
|
3167
|
-
{ key: "index", name: "j" }
|
|
3168
|
-
]);
|
|
3169
|
-
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
3170
|
-
const fallback = [];
|
|
3171
|
-
const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
|
|
3172
|
-
if (!writable) return null;
|
|
3173
|
-
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
3174
|
-
const shift = -template.range.start;
|
|
3175
|
-
const declarations = [];
|
|
3176
|
-
for (const member of template.members) {
|
|
3177
|
-
const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
|
|
3178
|
-
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
3179
|
-
declarations.push({ path: shapeOfPath(literal), literal });
|
|
3180
|
-
}
|
|
3181
|
-
const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
|
|
3182
|
-
if (rule.loop.kind === "attr") {
|
|
3183
|
-
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
3184
|
-
}
|
|
3185
|
-
if (rule.loopKey) {
|
|
3186
|
-
const key = rule.loopKey(names.index);
|
|
3187
|
-
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
3188
|
-
else if (template.keyAttr.end > template.keyAttr.start)
|
|
3189
|
-
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
3190
|
-
else return null;
|
|
3191
|
-
}
|
|
3192
|
-
const indent = indentOf(ctx.content, template.range.start);
|
|
3193
|
-
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
3194
|
-
${indented(source.toString(), indent)}
|
|
3195
|
-
${indent}${rule.loop.close}` : source.toString();
|
|
3196
|
-
return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
|
|
3197
|
-
}
|
|
3198
|
-
function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
|
|
3199
|
-
const rule = DIALECT_RULES[dialect];
|
|
3200
|
-
if (!rule.loop || !rule.text || group.length === 0) return null;
|
|
3201
|
-
const rows = findRows(ctx.roots, group);
|
|
3202
|
-
if (!rows || rows.length < 2) return null;
|
|
3203
|
-
if (!consecutive(rows, ctx.content)) return null;
|
|
3204
|
-
if (!sameShape(rows, ctx.content)) return null;
|
|
3205
|
-
if (group.some((m) => m.site.where !== "text" || !isIntrinsic(m.site.tag) || !m.site.inner)) return null;
|
|
3206
|
-
const template = rows[0];
|
|
3207
|
-
const used = new Set(ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
3208
|
-
const allocated = allocateNames(used, [
|
|
3209
|
-
{ key: "row", name: "card" },
|
|
3210
|
-
{ key: "index", name: "i" }
|
|
3211
|
-
]);
|
|
3212
|
-
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
3213
|
-
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
3214
|
-
const shift = -template.range.start;
|
|
3215
|
-
const declarations = [];
|
|
3216
|
-
const nested = /* @__PURE__ */ new Map();
|
|
3217
|
-
const direct = [];
|
|
3218
|
-
for (const member of template.members) {
|
|
3219
|
-
const inner = splitNested(member.leaf);
|
|
3220
|
-
if (inner === null) direct.push(member);
|
|
3221
|
-
else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
|
|
3222
|
-
}
|
|
3223
|
-
for (const [array, members] of nested) {
|
|
3224
|
-
const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
|
|
3225
|
-
if (!plan2) return null;
|
|
3226
|
-
if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
|
|
3227
|
-
source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
|
|
3228
|
-
declarations.push(...plan2.declarations);
|
|
3229
|
-
}
|
|
3230
|
-
for (const member of direct) {
|
|
3231
|
-
const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
|
|
3232
|
-
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
3233
|
-
declarations.push({ path: shapeOfPath(literal), literal });
|
|
3234
|
-
}
|
|
3235
|
-
const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
|
|
3236
|
-
const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
|
|
3237
|
-
if (rule.loop.kind === "attr") {
|
|
3238
|
-
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
3239
|
-
}
|
|
3240
|
-
if (rule.loopKey) {
|
|
3241
|
-
const key = rule.loopKey(names.index);
|
|
3242
|
-
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
3243
|
-
else if (template.keyAttr.end > template.keyAttr.start)
|
|
3244
|
-
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
3245
|
-
else return null;
|
|
3246
|
-
}
|
|
3247
|
-
const indent = indentOf(ctx.content, template.range.start);
|
|
3248
|
-
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
3249
|
-
${indented(source.toString(), indent)}
|
|
3250
|
-
${indent}${rule.loop.close}` : source.toString();
|
|
3251
|
-
return {
|
|
3252
|
-
range: { start: template.range.start, end: rows[rows.length - 1].range.end },
|
|
3253
|
-
text,
|
|
3254
|
-
declarations,
|
|
3255
|
-
bindings: [...new Set(declarations.map((d) => d.path))],
|
|
3256
|
-
names
|
|
3257
|
-
};
|
|
3258
|
-
}
|
|
3259
|
-
function setLeaf(row, leaf, value) {
|
|
3260
|
-
const segments = leaf.split(".").flatMap((segment) => {
|
|
3261
|
-
const name = segment.replace(/\[\d+\]/g, "");
|
|
3262
|
-
const indices = [...segment.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]));
|
|
3263
|
-
return [...name ? [name] : [], ...indices];
|
|
3264
|
-
});
|
|
3265
|
-
if (segments.some((segment) => typeof segment === "string" && FORBIDDEN.has(segment))) return false;
|
|
3266
|
-
let cursor = row;
|
|
3267
|
-
for (const [at, segment] of segments.entries()) {
|
|
3268
|
-
const key = segment;
|
|
3269
|
-
const last = at === segments.length - 1;
|
|
3270
|
-
if (last) {
|
|
3271
|
-
cursor[key] = value;
|
|
3272
|
-
return true;
|
|
3273
|
-
}
|
|
3274
|
-
const held = Object.prototype.hasOwnProperty.call(cursor, key) ? cursor[key] : void 0;
|
|
3275
|
-
if (held === void 0 || typeof held !== "object" || held === null) {
|
|
3276
|
-
cursor[key] = typeof segments[at + 1] === "number" ? [] : emptyRow();
|
|
3277
|
-
}
|
|
3278
|
-
cursor = cursor[key];
|
|
3279
|
-
}
|
|
3280
|
-
return true;
|
|
3281
|
-
}
|
|
3282
|
-
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3283
|
-
var emptyRow = () => /* @__PURE__ */ Object.create(null);
|
|
3284
|
-
function splitIndexed(arrayPath, path) {
|
|
3285
|
-
if (!path.startsWith(`${arrayPath}[`)) return null;
|
|
3286
|
-
const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
|
|
3287
|
-
return match ? { index: Number(match[1]), leaf: match[2] } : null;
|
|
3288
|
-
}
|
|
3289
|
-
|
|
3290
|
-
// src/sites/jsx.ts
|
|
3291
|
-
import { parse as parseModule } from "@babel/parser";
|
|
3292
|
-
var nameOf = (node) => {
|
|
3293
|
-
if (!node) return "";
|
|
3294
|
-
if (node.type === "JSXIdentifier") return String(node.name);
|
|
3295
|
-
if (node.type === "JSXMemberExpression")
|
|
3296
|
-
return `${nameOf(node.object)}.${nameOf(node.property)}`;
|
|
3297
|
-
if (node.type === "JSXNamespacedName")
|
|
3298
|
-
return `${nameOf(node.namespace)}:${nameOf(node.name)}`;
|
|
3299
|
-
return "";
|
|
3300
|
-
};
|
|
3301
|
-
var SPREAD = "{...}";
|
|
3302
|
-
function attrsOf2(open, content) {
|
|
3303
|
-
return (open.attributes ?? []).flatMap((attr) => {
|
|
3304
|
-
if (attr.type === "JSXSpreadAttribute") {
|
|
3305
|
-
const argument = attr.argument;
|
|
3306
|
-
return [
|
|
3307
|
-
{
|
|
3308
|
-
name: SPREAD,
|
|
3309
|
-
value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
|
|
3310
|
-
valueRange: null,
|
|
3311
|
-
expression: true,
|
|
3312
|
-
range: { start: attr.start ?? 0, end: attr.end ?? 0 },
|
|
3313
|
-
// `{...props}` — the braces ARE the attribute, so the expression is its whole span.
|
|
3314
|
-
expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
|
|
3315
|
-
}
|
|
3316
|
-
];
|
|
2774
|
+
// src/sites/jsx.ts
|
|
2775
|
+
var nameOf = (node) => {
|
|
2776
|
+
if (!node) return "";
|
|
2777
|
+
if (node.type === "JSXIdentifier") return String(node.name);
|
|
2778
|
+
if (node.type === "JSXMemberExpression")
|
|
2779
|
+
return `${nameOf(node.object)}.${nameOf(node.property)}`;
|
|
2780
|
+
if (node.type === "JSXNamespacedName")
|
|
2781
|
+
return `${nameOf(node.namespace)}:${nameOf(node.name)}`;
|
|
2782
|
+
return "";
|
|
2783
|
+
};
|
|
2784
|
+
var SPREAD = "{...}";
|
|
2785
|
+
function attrsOf2(open, content) {
|
|
2786
|
+
return (open.attributes ?? []).flatMap((attr) => {
|
|
2787
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
2788
|
+
const argument = attr.argument;
|
|
2789
|
+
return [
|
|
2790
|
+
{
|
|
2791
|
+
name: SPREAD,
|
|
2792
|
+
value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
|
|
2793
|
+
valueRange: null,
|
|
2794
|
+
expression: true,
|
|
2795
|
+
range: { start: attr.start ?? 0, end: attr.end ?? 0 },
|
|
2796
|
+
// `{...props}` — the braces ARE the attribute, so the expression is its whole span.
|
|
2797
|
+
expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
|
|
2798
|
+
}
|
|
2799
|
+
];
|
|
3317
2800
|
}
|
|
3318
2801
|
if (attr.type !== "JSXAttribute") return [];
|
|
3319
2802
|
const value = attr.value;
|
|
@@ -3547,7 +3030,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3547
3030
|
let receiver = null;
|
|
3548
3031
|
let rest = null;
|
|
3549
3032
|
let hasBindings = false;
|
|
3550
|
-
let
|
|
3033
|
+
let destructured2 = false;
|
|
3551
3034
|
for (const node of component ? [component] : []) {
|
|
3552
3035
|
const params = node.params ?? [];
|
|
3553
3036
|
const first = params[0];
|
|
@@ -3573,7 +3056,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3573
3056
|
if (key !== propName) continue;
|
|
3574
3057
|
const value = property.value;
|
|
3575
3058
|
if (value?.type === "Identifier") names.add(String(value.name));
|
|
3576
|
-
|
|
3059
|
+
destructured2 = true;
|
|
3577
3060
|
receivable = true;
|
|
3578
3061
|
receive = patternClose(content, first, offset);
|
|
3579
3062
|
}
|
|
@@ -3583,7 +3066,7 @@ function receptionIn(component, propName, offset, content) {
|
|
|
3583
3066
|
viaProps = true;
|
|
3584
3067
|
}
|
|
3585
3068
|
}
|
|
3586
|
-
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured };
|
|
3069
|
+
return { names, receive, viaProps, typeName, receivable, inline, receiver, rest, hasBindings, destructured: destructured2 };
|
|
3587
3070
|
}
|
|
3588
3071
|
function astroReception(ast, propName, offset, content) {
|
|
3589
3072
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -3757,129 +3240,1493 @@ function tryParse(code) {
|
|
|
3757
3240
|
return void 0;
|
|
3758
3241
|
}
|
|
3759
3242
|
}
|
|
3760
|
-
var bindingsName = (viaProps) => viaProps ? "props.bcmsBindings" : "bcmsBindings";
|
|
3761
|
-
var bindingsRead = (prop, viaProps) => `${bindingsName(viaProps)}?.${prop}`;
|
|
3762
|
-
|
|
3763
|
-
// src/
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3243
|
+
var bindingsName = (viaProps) => viaProps ? "props.bcmsBindings" : "bcmsBindings";
|
|
3244
|
+
var bindingsRead = (prop, viaProps) => `${bindingsName(viaProps)}?.${prop}`;
|
|
3245
|
+
|
|
3246
|
+
// src/data.ts
|
|
3247
|
+
function splitGroup(path) {
|
|
3248
|
+
const match = path.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
|
|
3249
|
+
return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
|
|
3250
|
+
}
|
|
3251
|
+
function moduleOf(parsed) {
|
|
3252
|
+
return { ast: parsed.ast ?? null, offset: parsed.script?.offset ?? 0 };
|
|
3253
|
+
}
|
|
3254
|
+
function arrayLiterals(ast) {
|
|
3255
|
+
const out = /* @__PURE__ */ new Map();
|
|
3256
|
+
walkAst(ast, (node) => {
|
|
3257
|
+
if (node.type !== "VariableDeclarator") return;
|
|
3258
|
+
const id = node.id;
|
|
3259
|
+
const init = node.init;
|
|
3260
|
+
if (id?.type !== "Identifier" || init?.type !== "ArrayExpression") return;
|
|
3261
|
+
out.set(String(id.name), init);
|
|
3262
|
+
});
|
|
3263
|
+
return out;
|
|
3264
|
+
}
|
|
3265
|
+
function exportedNames(ast) {
|
|
3266
|
+
const out = /* @__PURE__ */ new Set();
|
|
3267
|
+
walkAst(ast, (node) => {
|
|
3268
|
+
if (node.type !== "ExportNamedDeclaration") return;
|
|
3269
|
+
const declaration2 = node.declaration;
|
|
3270
|
+
if (declaration2?.type !== "VariableDeclaration") return;
|
|
3271
|
+
for (const declarator of declaration2.declarations ?? []) {
|
|
3272
|
+
const id = declarator.id;
|
|
3273
|
+
if (id?.type === "Identifier") out.add(String(id.name));
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
return out;
|
|
3277
|
+
}
|
|
3278
|
+
function propertyFor(row, literal) {
|
|
3279
|
+
if (row?.type !== "ObjectExpression") return null;
|
|
3280
|
+
for (const property of row.properties ?? []) {
|
|
3281
|
+
if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
|
|
3282
|
+
const key = property.key;
|
|
3283
|
+
const value = property.value;
|
|
3284
|
+
if (value?.type !== "StringLiteral" || key?.type !== "Identifier") continue;
|
|
3285
|
+
if (norm(String(value.value)) === literal) return String(key.name);
|
|
3286
|
+
}
|
|
3287
|
+
return null;
|
|
3288
|
+
}
|
|
3289
|
+
function iterations(roots, identifier2) {
|
|
3290
|
+
const pattern = new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])\\s*\\.\\s*map\\s*\\(`);
|
|
3291
|
+
const out = [];
|
|
3292
|
+
const visit = (node) => {
|
|
3293
|
+
if (node.type === "expr") {
|
|
3294
|
+
if (pattern.test(node.value)) out.push(node);
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
if (node.type === "element" && !node.raw) for (const child of node.children) visit(child);
|
|
3298
|
+
};
|
|
3299
|
+
for (const root of roots) visit(root);
|
|
3300
|
+
return out;
|
|
3301
|
+
}
|
|
3302
|
+
var CALLBACK = String.raw`\s*\.\s*map\s*\(\s*(?:\(\s*(?:([A-Za-z_$][\w$]*)|(\[[^\]]*\]))\s*(,\s*([A-Za-z_$][\w$]*)\s*)?\)|([A-Za-z_$][\w$]*))\s*=>`;
|
|
3303
|
+
function paramsIn(text, pattern, base) {
|
|
3304
|
+
const match = text.match(pattern);
|
|
3305
|
+
if (!match || match.index === void 0) return null;
|
|
3306
|
+
if (match[5] !== void 0) return { row: match[5], names: null, index: null, addIndexAt: -1 };
|
|
3307
|
+
const bound = match[1] ?? match[2];
|
|
3308
|
+
const within = match[0].lastIndexOf(bound);
|
|
3309
|
+
return {
|
|
3310
|
+
// One of the two, never both: a row is either named or taken apart. @see bindLists for the
|
|
3311
|
+
// second shape — `([question, answer]) =>`, which is how a TUPLE reaches the markup.
|
|
3312
|
+
row: match[1] ?? null,
|
|
3313
|
+
names: match[2] === void 0 ? null : destructured(match[2]),
|
|
3314
|
+
index: match[4] ?? null,
|
|
3315
|
+
addIndexAt: base + match.index + within + bound.length
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function callbackParams(expression, identifier2) {
|
|
3319
|
+
return paramsIn(
|
|
3320
|
+
expression.value,
|
|
3321
|
+
new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])${CALLBACK}`),
|
|
3322
|
+
expression.range.start
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
function mapParams(content, at, until) {
|
|
3326
|
+
return paramsIn(content.slice(at, until), new RegExp(`^${CALLBACK}`), at);
|
|
3327
|
+
}
|
|
3328
|
+
function destructured(pattern) {
|
|
3329
|
+
const names = pattern.slice(1, -1).split(",").map((part) => part.trim());
|
|
3330
|
+
return names.every((name) => /^[A-Za-z_$][\w$]*$/.test(name)) ? names : null;
|
|
3331
|
+
}
|
|
3332
|
+
function renders(content, node, row) {
|
|
3333
|
+
if (!node.inner) return null;
|
|
3334
|
+
const inner = content.slice(node.inner.start, node.inner.end).trim();
|
|
3335
|
+
const match = inner.match(/^\{\s*([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\}$/);
|
|
3336
|
+
return match && match[1] === row ? match[2] : null;
|
|
3337
|
+
}
|
|
3338
|
+
function leafElements(content, expression, row) {
|
|
3339
|
+
const out = /* @__PURE__ */ new Map();
|
|
3340
|
+
const visit = (node) => {
|
|
3341
|
+
if (node.type === "expr") {
|
|
3342
|
+
for (const child of node.children) visit(child);
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
if (node.type !== "element" || node.raw) return;
|
|
3346
|
+
const property = isIntrinsic(node.tag) ? renders(content, node, row) : null;
|
|
3347
|
+
if (property) out.set(property, [...out.get(property) ?? [], node]);
|
|
3348
|
+
for (const child of node.children) visit(child);
|
|
3349
|
+
};
|
|
3350
|
+
for (const child of expression.children) visit(child);
|
|
3351
|
+
return out;
|
|
3352
|
+
}
|
|
3353
|
+
function stringLiterals(ast) {
|
|
3354
|
+
const out = /* @__PURE__ */ new Map();
|
|
3355
|
+
walkAst(ast, (node) => {
|
|
3356
|
+
if (node.type !== "VariableDeclarator") return;
|
|
3357
|
+
const id = node.id;
|
|
3358
|
+
const init = node.init;
|
|
3359
|
+
if (id?.type !== "Identifier" || init?.type !== "StringLiteral") return;
|
|
3360
|
+
out.set(String(id.name), init);
|
|
3361
|
+
});
|
|
3362
|
+
return out;
|
|
3363
|
+
}
|
|
3364
|
+
function scalarUses(content, roots, id) {
|
|
3365
|
+
const mention = new RegExp(`(?<![\\w$])${id}(?![\\w$])`, "g");
|
|
3366
|
+
const alone = new RegExp(`^\\{\\s*${id}\\s*\\}$`);
|
|
3367
|
+
const count = (text) => (text.match(mention) ?? []).length;
|
|
3368
|
+
let total = 0;
|
|
3369
|
+
const hosts = [];
|
|
3370
|
+
const visit = (node) => {
|
|
3371
|
+
if (node.type === "expr") {
|
|
3372
|
+
total += count(node.value);
|
|
3373
|
+
for (const child of node.children) visit(child);
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
if (node.type !== "element" || node.raw) return;
|
|
3377
|
+
for (const attr of node.attrs) {
|
|
3378
|
+
if (attr.expressionRange) total += count(content.slice(attr.expressionRange.start, attr.expressionRange.end));
|
|
3379
|
+
}
|
|
3380
|
+
if (node.inner && isIntrinsic(node.tag) && alone.test(content.slice(node.inner.start, node.inner.end).trim())) {
|
|
3381
|
+
hosts.push(node);
|
|
3382
|
+
}
|
|
3383
|
+
for (const child of node.children) visit(child);
|
|
3384
|
+
};
|
|
3385
|
+
for (const root of roots) visit(root);
|
|
3386
|
+
return { total, hosts };
|
|
3387
|
+
}
|
|
3388
|
+
function bindScalar(ctx, member, literals, offset) {
|
|
3389
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3390
|
+
const named = [...literals].filter(([, init2]) => norm(String(init2.value)) === member.literal);
|
|
3391
|
+
if (named.length !== 1) return { splices: [], refusal: null };
|
|
3392
|
+
const [id, init] = named[0];
|
|
3393
|
+
const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
|
|
3394
|
+
if (uses.hosts.length !== 1 || uses.total !== 1) return { splices: [], refusal: "IN_EXPRESSION" };
|
|
3395
|
+
const host = uses.hosts[0];
|
|
3396
|
+
const at = { start: offset + (init.start ?? 0), end: offset + (init.end ?? 0) };
|
|
3397
|
+
const args = [
|
|
3398
|
+
ctx.snapshotName(member.slug),
|
|
3399
|
+
JSON.stringify(member.path),
|
|
3400
|
+
// The literal EXACTLY as the author wrote it — quotes, escapes and all — so the fallback the
|
|
3401
|
+
// build renders is the byte-for-byte copy the repository renders today.
|
|
3402
|
+
ctx.content.slice(at.start, at.end),
|
|
3403
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3404
|
+
];
|
|
3405
|
+
const attributes = [
|
|
3406
|
+
`data-bcms-field="${member.path}"`,
|
|
3407
|
+
...member.kind ? [`data-bcms-kind="${member.kind}"`] : [],
|
|
3408
|
+
...member.kind === "richtext" && rule.richtext ? [rule.richtext(id)] : []
|
|
3409
|
+
];
|
|
3410
|
+
const splices = [
|
|
3411
|
+
{ start: at.start, end: at.end, text: `${ctx.read}(${args.join(", ")})` },
|
|
3412
|
+
{ start: host.attrInsertAt, end: host.attrInsertAt, text: ` ${attributes.join(" ")}` }
|
|
3413
|
+
];
|
|
3414
|
+
if (member.kind === "richtext" && rule.richtext && host.inner) {
|
|
3415
|
+
splices.push({ start: host.inner.start, end: host.inner.end, text: "" });
|
|
3416
|
+
}
|
|
3417
|
+
return { splices, refusal: null };
|
|
3418
|
+
}
|
|
3419
|
+
async function bindDataLiterals(ctx, members) {
|
|
3420
|
+
const out = { splices: [], usesRead: false, usesRows: false, usesList: false, usesTuples: false, notes: [], slugs: [], bound: [], declarations: [], refused: [], componentEdits: [], props: [] };
|
|
3421
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3422
|
+
const module = moduleOf(ctx.parsed);
|
|
3423
|
+
if (!rule.text) return out;
|
|
3424
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3425
|
+
const scalars = [];
|
|
3426
|
+
for (const member of members) {
|
|
3427
|
+
const split = splitGroup(member.path);
|
|
3428
|
+
if (!split) {
|
|
3429
|
+
scalars.push(member);
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
const held = { ...member, array: split.array, index: split.index, leaf: split.leaf };
|
|
3433
|
+
const key = `${member.slug}\0${split.array}`;
|
|
3434
|
+
groups.set(key, [...groups.get(key) ?? [], held]);
|
|
3435
|
+
}
|
|
3436
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
3437
|
+
const strings = stringLiterals(module.ast);
|
|
3438
|
+
for (const member of scalars) {
|
|
3439
|
+
const scalar = bindScalar(ctx, member, strings, module.offset);
|
|
3440
|
+
if (scalar.refusal) out.refused.push({ key: member.key, reason: scalar.refusal });
|
|
3441
|
+
if (scalar.splices.length === 0) continue;
|
|
3442
|
+
out.splices.push(...scalar.splices);
|
|
3443
|
+
out.usesRead = true;
|
|
3444
|
+
out.slugs.push(member.slug);
|
|
3445
|
+
out.bound.push(member.key);
|
|
3446
|
+
}
|
|
3447
|
+
const literals = arrayLiterals(module.ast);
|
|
3448
|
+
for (const all of groups.values()) {
|
|
3449
|
+
const array = all[0].array;
|
|
3450
|
+
const nested = all.filter((member) => /\[\d+\]/.test(member.leaf));
|
|
3451
|
+
for (const member of nested) out.refused.push({ key: member.key, reason: "PROP_DRILLED_DEEP" });
|
|
3452
|
+
const group = all.filter((member) => !nested.includes(member));
|
|
3453
|
+
if (group.length === 0) continue;
|
|
3454
|
+
const refuse = (reason) => {
|
|
3455
|
+
for (const member of group) out.refused.push({ key: member.key, reason });
|
|
3456
|
+
};
|
|
3457
|
+
let identifier2 = null;
|
|
3458
|
+
let keys = null;
|
|
3459
|
+
for (const [name, literal] of literals) {
|
|
3460
|
+
const elements2 = literal.elements ?? [];
|
|
3461
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
3462
|
+
const ok = group.every((member) => {
|
|
3463
|
+
const property = propertyFor(elements2[member.index] ?? void 0, member.literal);
|
|
3464
|
+
if (!property) return false;
|
|
3465
|
+
const held = mapped.get(property);
|
|
3466
|
+
if (held !== void 0 && held !== member.leaf) return false;
|
|
3467
|
+
mapped.set(property, member.leaf);
|
|
3468
|
+
return true;
|
|
3469
|
+
});
|
|
3470
|
+
if (!ok) continue;
|
|
3471
|
+
if (identifier2 !== null) {
|
|
3472
|
+
identifier2 = null;
|
|
3473
|
+
break;
|
|
3474
|
+
}
|
|
3475
|
+
identifier2 = name;
|
|
3476
|
+
keys = mapped;
|
|
3477
|
+
}
|
|
3478
|
+
if (identifier2 === null || keys === null) continue;
|
|
3479
|
+
const loops = iterations(ctx.parsed.roots, identifier2);
|
|
3480
|
+
if (loops.length === 0) continue;
|
|
3481
|
+
if (loops.length > 1) {
|
|
3482
|
+
refuse("REPEATER_AMBIGUOUS");
|
|
3483
|
+
continue;
|
|
3484
|
+
}
|
|
3485
|
+
const loop = loops[0];
|
|
3486
|
+
const params = callbackParams(loop, identifier2);
|
|
3487
|
+
if (!params || params.row === null) {
|
|
3488
|
+
refuse("IN_EXPRESSION");
|
|
3489
|
+
continue;
|
|
3490
|
+
}
|
|
3491
|
+
const rowName = params.row;
|
|
3492
|
+
const elements = leafElements(ctx.content, loop, rowName);
|
|
3493
|
+
const placed = /* @__PURE__ */ new Map();
|
|
3494
|
+
const passed = [];
|
|
3495
|
+
let refusal = null;
|
|
3496
|
+
for (const property of keys.keys()) {
|
|
3497
|
+
const found = elements.get(property) ?? [];
|
|
3498
|
+
if (found.length === 1) {
|
|
3499
|
+
placed.set(property, found[0]);
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
if (found.length > 1) {
|
|
3503
|
+
refusal = "REPEATER_AMBIGUOUS";
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
const calls = propCalls(ctx.content, loop, rowName, property);
|
|
3507
|
+
if (ctx.drill && calls.length === 1) passed.push({ property, ...calls[0] });
|
|
3508
|
+
else if (calls.length > 1) refusal = "REPEATER_AMBIGUOUS";
|
|
3509
|
+
else refusal = leafRefusal(loop.value, rowName, property);
|
|
3510
|
+
}
|
|
3511
|
+
if (refusal !== null) {
|
|
3512
|
+
refuse(refusal);
|
|
3513
|
+
continue;
|
|
3514
|
+
}
|
|
3515
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3516
|
+
indexes.add(index);
|
|
3517
|
+
const splices = [];
|
|
3518
|
+
if (rule.loopKey) {
|
|
3519
|
+
const rows = loop.children.filter((child) => child.type === "element");
|
|
3520
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
3521
|
+
if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
|
|
3522
|
+
splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
if (params.index === null) {
|
|
3526
|
+
if (params.addIndexAt < 0) {
|
|
3527
|
+
refuse("IN_EXPRESSION");
|
|
3528
|
+
continue;
|
|
3529
|
+
}
|
|
3530
|
+
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3531
|
+
}
|
|
3532
|
+
const drilled = [];
|
|
3533
|
+
const edits = [];
|
|
3534
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
3535
|
+
for (const entry of passed) {
|
|
3536
|
+
const leaf = keys.get(entry.property);
|
|
3537
|
+
const literal = `${array}[\${${index}}].${leaf}`;
|
|
3538
|
+
const outcome = entry.call.attrs.some((a) => a.name === "bcmsBindings") ? { ok: false, reason: "PROP_TARGET_NOT_FOUND" } : await ctx.drill(entry.call.tag, entry.prop, group.find((member) => member.leaf === leaf).kind);
|
|
3539
|
+
if (!outcome.ok) {
|
|
3540
|
+
refusal = outcome.reason;
|
|
3541
|
+
break;
|
|
3542
|
+
}
|
|
3543
|
+
edits.push(...outcome.edits);
|
|
3544
|
+
bindings.set(entry.call.attrInsertAt, [
|
|
3545
|
+
...bindings.get(entry.call.attrInsertAt) ?? [],
|
|
3546
|
+
`${entry.prop}: \`${literal}\``
|
|
3547
|
+
]);
|
|
3548
|
+
drilled.push({
|
|
3549
|
+
file: outcome.edits[0].file,
|
|
3550
|
+
prop: entry.prop,
|
|
3551
|
+
path: shapeOfPath(literal),
|
|
3552
|
+
literal: `${entry.prop}: \`${literal}\``
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
if (refusal !== null) {
|
|
3556
|
+
refuse(refusal);
|
|
3557
|
+
continue;
|
|
3558
|
+
}
|
|
3559
|
+
for (const [at2, entries] of bindings) {
|
|
3560
|
+
splices.push({ start: at2, end: at2, text: ` bcmsBindings={{ ${entries.join(", ")} }}` });
|
|
3561
|
+
}
|
|
3562
|
+
for (const [property, element] of placed) {
|
|
3563
|
+
const leaf = keys.get(property);
|
|
3564
|
+
const literal = `${array}[\${${index}}].${leaf}`;
|
|
3565
|
+
const kind = group.find((member) => member.leaf === leaf).kind;
|
|
3566
|
+
const attributes = [
|
|
3567
|
+
rule.attr("data-bcms-field", `\`${literal}\``),
|
|
3568
|
+
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3569
|
+
// A richtext row value is html, so it goes into the dialect's html sink rather than being
|
|
3570
|
+
// printed as characters — the same rule `writeLeaf` follows one level down. @see loops.ts
|
|
3571
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(`${rowName}.${property}`)] : []
|
|
3572
|
+
];
|
|
3573
|
+
splices.push({ start: element.attrInsertAt, end: element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3574
|
+
if (kind === "richtext" && rule.richtext && element.inner) {
|
|
3575
|
+
splices.push({ start: element.inner.start, end: element.inner.end, text: "" });
|
|
3576
|
+
}
|
|
3577
|
+
out.declarations.push({ path: shapeOfPath(literal), literal });
|
|
3578
|
+
}
|
|
3579
|
+
const at = { start: module.offset + (literals.get(identifier2).start ?? 0), end: module.offset + (literals.get(identifier2).end ?? 0) };
|
|
3580
|
+
const map = [...keys].map(([property, leaf]) => `${property}: ${JSON.stringify(leaf)}`).join(", ");
|
|
3581
|
+
const args = [
|
|
3582
|
+
ctx.snapshotName(group[0].slug),
|
|
3583
|
+
JSON.stringify(array),
|
|
3584
|
+
ctx.content.slice(at.start, at.end),
|
|
3585
|
+
`{ ${map} }`,
|
|
3586
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3587
|
+
];
|
|
3588
|
+
splices.push({ start: at.start, end: at.end, text: `${ctx.rowsAs}(${args.join(", ")})` });
|
|
3589
|
+
out.splices.push(...splices);
|
|
3590
|
+
out.usesRows = true;
|
|
3591
|
+
out.componentEdits.push(...edits);
|
|
3592
|
+
out.props.push(...drilled);
|
|
3593
|
+
out.slugs.push(...group.map((member) => member.slug));
|
|
3594
|
+
out.bound.push(...group.map((member) => member.key));
|
|
3595
|
+
}
|
|
3596
|
+
const settled = /* @__PURE__ */ new Set([...out.bound, ...out.refused.map((refusal) => refusal.key)]);
|
|
3597
|
+
bindLists(ctx, members.filter((member) => !settled.has(member.key)), module, out, indexes);
|
|
3598
|
+
return out;
|
|
3599
|
+
}
|
|
3600
|
+
function literalTable(init) {
|
|
3601
|
+
const elements = (init.elements ?? []).filter((e) => e !== null);
|
|
3602
|
+
const first = elements[0];
|
|
3603
|
+
if (!first || elements.length !== (init.elements ?? []).length) return null;
|
|
3604
|
+
if (elements.some((element) => element.type !== first.type)) return null;
|
|
3605
|
+
if (first.type === "StringLiteral") {
|
|
3606
|
+
return { tuple: false, rows: elements.map((element) => [norm(String(element.value))]) };
|
|
3607
|
+
}
|
|
3608
|
+
if (first.type !== "ArrayExpression") return null;
|
|
3609
|
+
const rows = [];
|
|
3610
|
+
for (const element of elements) {
|
|
3611
|
+
const inner = (element.elements ?? []).filter((x) => x !== null);
|
|
3612
|
+
if (inner.length === 0 || inner.some((x) => x.type !== "StringLiteral")) return null;
|
|
3613
|
+
if (inner.length !== (element.elements ?? []).length) return null;
|
|
3614
|
+
rows.push(inner.map((x) => norm(String(x.value))));
|
|
3615
|
+
}
|
|
3616
|
+
if (new Set(rows.map((row) => row.length)).size !== 1) return null;
|
|
3617
|
+
return { tuple: true, rows };
|
|
3618
|
+
}
|
|
3619
|
+
function openingBracket(text, closeAt) {
|
|
3620
|
+
let depth = 0;
|
|
3621
|
+
for (let at = closeAt; at >= 0; at--) {
|
|
3622
|
+
if (text[at] === "]") depth++;
|
|
3623
|
+
else if (text[at] === "[") {
|
|
3624
|
+
depth--;
|
|
3625
|
+
if (depth === 0) return at;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
return -1;
|
|
3629
|
+
}
|
|
3630
|
+
function arrayLiteral(source) {
|
|
3631
|
+
try {
|
|
3632
|
+
const ast = parseProgram(`(${source})`);
|
|
3633
|
+
const expression = ast.program.body[0]?.expression;
|
|
3634
|
+
if (!expression || expression.type !== "ArrayExpression") return null;
|
|
3635
|
+
return (expression.start ?? -1) === 1 && (expression.end ?? -1) === source.length + 1 ? expression : null;
|
|
3636
|
+
} catch {
|
|
3637
|
+
return null;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
function inlineTables(ctx) {
|
|
3641
|
+
const out = /* @__PURE__ */ new Map();
|
|
3642
|
+
const visit = (node) => {
|
|
3643
|
+
if (node.type === "element") {
|
|
3644
|
+
if (!node.raw) for (const child of node.children) visit(child);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if (node.type !== "expr") return;
|
|
3648
|
+
for (const match of node.value.matchAll(/\]\s*\.\s*map\s*\(/g)) {
|
|
3649
|
+
const closeAt = match.index;
|
|
3650
|
+
const openAt = openingBracket(node.value, closeAt);
|
|
3651
|
+
if (openAt <= 0 || /[\w$)\]]/.test(node.value[openAt - 1] ?? "")) continue;
|
|
3652
|
+
const init = arrayLiteral(node.value.slice(openAt, closeAt + 1));
|
|
3653
|
+
const table = init ? literalTable(init) : null;
|
|
3654
|
+
if (!table) continue;
|
|
3655
|
+
const at = { start: node.range.start + openAt, end: node.range.start + closeAt + 1 };
|
|
3656
|
+
out.set(at.start, {
|
|
3657
|
+
...table,
|
|
3658
|
+
at,
|
|
3659
|
+
label: "an inline array literal",
|
|
3660
|
+
render: () => ({ loop: node, params: mapParams(ctx.content, at.end, node.range.end) })
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
for (const child of node.children) visit(child);
|
|
3664
|
+
};
|
|
3665
|
+
for (const root of ctx.parsed.roots) visit(root);
|
|
3666
|
+
return [...out.values()];
|
|
3667
|
+
}
|
|
3668
|
+
function readerElements(content, expression, reads) {
|
|
3669
|
+
const wanted = new Set(reads);
|
|
3670
|
+
const out = /* @__PURE__ */ new Map();
|
|
3671
|
+
const visit = (node) => {
|
|
3672
|
+
if (node.type === "expr") {
|
|
3673
|
+
for (const child of node.children) visit(child);
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
if (node.type !== "element" || node.raw) return;
|
|
3677
|
+
const inner = isIntrinsic(node.tag) && node.inner ? content.slice(node.inner.start, node.inner.end).trim() : "";
|
|
3678
|
+
const read = inner.match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3679
|
+
if (read && wanted.has(read)) out.set(read, [...out.get(read) ?? [], node]);
|
|
3680
|
+
for (const child of node.children) visit(child);
|
|
3681
|
+
};
|
|
3682
|
+
for (const child of expression.children) visit(child);
|
|
3683
|
+
return out;
|
|
3684
|
+
}
|
|
3685
|
+
function readerWraps(expression, reads) {
|
|
3686
|
+
const wanted = new Set(reads);
|
|
3687
|
+
const out = /* @__PURE__ */ new Map();
|
|
3688
|
+
const visit = (node, parent) => {
|
|
3689
|
+
if (node.type === "expr") {
|
|
3690
|
+
const read = node.value.trim().match(/^\{\s*([A-Za-z_$][\w$]*(?:\[\d+\])?)\s*\}$/)?.[1];
|
|
3691
|
+
const beside = parent !== null && isIntrinsic(parent.tag) && parent.children.some((child) => child !== node && !(child.type === "text" && child.value.trim() === ""));
|
|
3692
|
+
if (read && wanted.has(read) && beside) out.set(read, [...out.get(read) ?? [], node.range]);
|
|
3693
|
+
for (const child of node.children) visit(child, parent);
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
if (node.type !== "element" || node.raw) return;
|
|
3697
|
+
for (const child of node.children) visit(child, node);
|
|
3698
|
+
};
|
|
3699
|
+
for (const child of expression.children) visit(child, null);
|
|
3700
|
+
return out;
|
|
3701
|
+
}
|
|
3702
|
+
function pathTemplates(paths, index) {
|
|
3703
|
+
const out = [];
|
|
3704
|
+
for (let position = 0; position < paths[0].length; position++) {
|
|
3705
|
+
const split = paths.map((row) => splitGroup(row[position]));
|
|
3706
|
+
const first = split[0];
|
|
3707
|
+
if (!first) return null;
|
|
3708
|
+
if (split.some((held, row) => !held || held.array !== first.array || held.leaf !== first.leaf || held.index !== row)) {
|
|
3709
|
+
return null;
|
|
3710
|
+
}
|
|
3711
|
+
out.push(`${first.array}[\${${index}}].${first.leaf}`);
|
|
3712
|
+
}
|
|
3713
|
+
return out;
|
|
3714
|
+
}
|
|
3715
|
+
function bindLists(ctx, members, module, out, indexes) {
|
|
3716
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3717
|
+
const escapes = exportedNames(module.ast);
|
|
3718
|
+
const candidates = [];
|
|
3719
|
+
for (const [name, init] of arrayLiterals(module.ast)) {
|
|
3720
|
+
const table = literalTable(init);
|
|
3721
|
+
if (!table) continue;
|
|
3722
|
+
candidates.push({
|
|
3723
|
+
...table,
|
|
3724
|
+
at: { start: module.offset + (init.start ?? 0), end: module.offset + (init.end ?? 0) },
|
|
3725
|
+
label: `\`${name}\``,
|
|
3726
|
+
render: () => {
|
|
3727
|
+
const loops = iterations(ctx.parsed.roots, name);
|
|
3728
|
+
if (loops.length > 1) return { refuse: "REPEATER_AMBIGUOUS" };
|
|
3729
|
+
if (loops.length === 0) return null;
|
|
3730
|
+
const mentions2 = ctx.content.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
|
|
3731
|
+
if (mentions2.length !== 2 || escapes.has(name)) return { refuse: "IN_EXPRESSION" };
|
|
3732
|
+
return { loop: loops[0], params: callbackParams(loops[0], name) };
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
candidates.push(...inlineTables(ctx));
|
|
3737
|
+
const siblings = /* @__PURE__ */ new Map();
|
|
3738
|
+
const groupOf2 = (member) => `${member.slug}\0${splitGroup(member.path)?.array ?? member.path}`;
|
|
3739
|
+
for (const member of members) {
|
|
3740
|
+
siblings.set(groupOf2(member), [...siblings.get(groupOf2(member)) ?? [], member]);
|
|
3741
|
+
}
|
|
3742
|
+
const fit = (candidate, member) => (siblings.get(groupOf2(member)) ?? [member]).filter(
|
|
3743
|
+
(leaf) => candidate.rows.some((row) => row.includes(leaf.literal))
|
|
3744
|
+
).length;
|
|
3745
|
+
const best = /* @__PURE__ */ new Map();
|
|
3746
|
+
const bestFit = (member) => {
|
|
3747
|
+
const key = groupOf2(member);
|
|
3748
|
+
if (!best.has(key)) best.set(key, Math.max(0, ...candidates.map((candidate) => fit(candidate, member))));
|
|
3749
|
+
return best.get(key);
|
|
3750
|
+
};
|
|
3751
|
+
const claims = (candidate, member) => candidate.rows.some((row) => row.includes(member.literal)) && fit(candidate, member) === bestFit(member);
|
|
3752
|
+
const holders = (member) => candidates.filter((candidate) => fit(candidate, member) === bestFit(member)).length;
|
|
3753
|
+
for (const candidate of candidates) {
|
|
3754
|
+
const mine = members.filter((member) => claims(candidate, member));
|
|
3755
|
+
if (mine.length === 0 || mine.some((member) => holders(member) > 1)) continue;
|
|
3756
|
+
if (new Set(mine.map((member) => member.slug)).size !== 1) continue;
|
|
3757
|
+
const refuse = (reason) => {
|
|
3758
|
+
for (const member of mine) out.refused.push({ key: member.key, reason });
|
|
3759
|
+
};
|
|
3760
|
+
const paths = candidate.rows.map((row) => row.map(() => ""));
|
|
3761
|
+
const kinds = candidate.rows.map((row) => row.map(() => ""));
|
|
3762
|
+
let clash = false;
|
|
3763
|
+
for (const member of mine) {
|
|
3764
|
+
const spots = candidate.rows.flatMap(
|
|
3765
|
+
(row, r) => row.flatMap((value, p) => value === member.literal ? [[r, p]] : [])
|
|
3766
|
+
);
|
|
3767
|
+
if (spots.length !== 1 || paths[spots[0][0]][spots[0][1]] !== "") clash = true;
|
|
3768
|
+
else {
|
|
3769
|
+
paths[spots[0][0]][spots[0][1]] = member.path;
|
|
3770
|
+
kinds[spots[0][0]][spots[0][1]] = member.kind;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
if (clash || paths.some((row) => row.some((path) => path === ""))) {
|
|
3774
|
+
refuse("IN_EXPRESSION");
|
|
3775
|
+
continue;
|
|
3776
|
+
}
|
|
3777
|
+
const kindAt = kinds[0];
|
|
3778
|
+
if (kinds.some((row) => row.some((kind, position) => kind !== kindAt[position]))) {
|
|
3779
|
+
refuse("IN_EXPRESSION");
|
|
3780
|
+
continue;
|
|
3781
|
+
}
|
|
3782
|
+
const rendered = candidate.render();
|
|
3783
|
+
if (rendered === null) continue;
|
|
3784
|
+
if ("refuse" in rendered) {
|
|
3785
|
+
refuse(rendered.refuse);
|
|
3786
|
+
continue;
|
|
3787
|
+
}
|
|
3788
|
+
const { loop, params } = rendered;
|
|
3789
|
+
const reads = !params ? null : candidate.tuple ? params.names ?? (params.row === null ? null : candidate.rows[0].map((_, p) => `${params.row}[${p}]`)) : params.row === null ? null : [params.row];
|
|
3790
|
+
if (!params || !reads || reads.length !== candidate.rows[0].length) {
|
|
3791
|
+
refuse("IN_EXPRESSION");
|
|
3792
|
+
continue;
|
|
3793
|
+
}
|
|
3794
|
+
const elements = readerElements(ctx.content, loop, reads);
|
|
3795
|
+
const wraps = readerWraps(loop, reads);
|
|
3796
|
+
const hosts = [];
|
|
3797
|
+
let refusal = null;
|
|
3798
|
+
for (const read of reads) {
|
|
3799
|
+
const found = elements.get(read) ?? [];
|
|
3800
|
+
const beside = elements.has(read) ? [] : wraps.get(read) ?? [];
|
|
3801
|
+
if (found.length === 1) hosts.push({ element: found[0] });
|
|
3802
|
+
else if (found.length === 0 && beside.length === 1) hosts.push({ wrap: beside[0] });
|
|
3803
|
+
else refusal = found.length + beside.length > 1 ? "REPEATER_AMBIGUOUS" : "IN_EXPRESSION";
|
|
3804
|
+
}
|
|
3805
|
+
if (refusal !== null) {
|
|
3806
|
+
refuse(refusal);
|
|
3807
|
+
continue;
|
|
3808
|
+
}
|
|
3809
|
+
const index = params.index ?? allocateIndex(loop.value, indexes);
|
|
3810
|
+
indexes.add(index);
|
|
3811
|
+
const splices = [];
|
|
3812
|
+
if (rule.loopKey) {
|
|
3813
|
+
const rows = loop.children.filter((child) => child.type === "element");
|
|
3814
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
3815
|
+
if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
|
|
3816
|
+
splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
if (params.index === null) {
|
|
3820
|
+
if (params.addIndexAt < 0) {
|
|
3821
|
+
refuse("IN_EXPRESSION");
|
|
3822
|
+
continue;
|
|
3823
|
+
}
|
|
3824
|
+
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3825
|
+
}
|
|
3826
|
+
const templates = pathTemplates(paths, index);
|
|
3827
|
+
if (!templates) {
|
|
3828
|
+
refuse("IN_EXPRESSION");
|
|
3829
|
+
continue;
|
|
3830
|
+
}
|
|
3831
|
+
const list = JSON.stringify(candidate.tuple ? paths : paths.map((row) => row[0]));
|
|
3832
|
+
for (const [position, host] of hosts.entries()) {
|
|
3833
|
+
const kind = kindAt[position];
|
|
3834
|
+
const attributes = [
|
|
3835
|
+
rule.attr("data-bcms-field", `\`${templates[position]}\``),
|
|
3836
|
+
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3837
|
+
// A richtext value is html, so it goes into the dialect's html sink rather than being
|
|
3838
|
+
// printed as characters — the same rule one level up. @see bindDataLiterals
|
|
3839
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(reads[position])] : []
|
|
3840
|
+
];
|
|
3841
|
+
if ("element" in host) {
|
|
3842
|
+
splices.push({ start: host.element.attrInsertAt, end: host.element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3843
|
+
if (kind === "richtext" && rule.richtext && host.element.inner) {
|
|
3844
|
+
splices.push({ start: host.element.inner.start, end: host.element.inner.end, text: "" });
|
|
3845
|
+
}
|
|
3846
|
+
} else {
|
|
3847
|
+
const inner = kind === "richtext" && rule.richtext ? "" : ctx.content.slice(host.wrap.start, host.wrap.end);
|
|
3848
|
+
splices.push({ start: host.wrap.start, end: host.wrap.end, text: `<span ${attributes.join(" ")}>${inner}</span>` });
|
|
3849
|
+
}
|
|
3850
|
+
out.declarations.push({ path: shapeOfPath(templates[position]), literal: templates[position] });
|
|
3851
|
+
}
|
|
3852
|
+
const args = [
|
|
3853
|
+
ctx.snapshotName(mine[0].slug),
|
|
3854
|
+
list,
|
|
3855
|
+
ctx.content.slice(candidate.at.start, candidate.at.end),
|
|
3856
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3857
|
+
];
|
|
3858
|
+
splices.push({
|
|
3859
|
+
start: candidate.at.start,
|
|
3860
|
+
end: candidate.at.end,
|
|
3861
|
+
text: `${candidate.tuple ? ctx.tuples : ctx.list}(${args.join(", ")})`
|
|
3862
|
+
});
|
|
3863
|
+
out.splices.push(...splices);
|
|
3864
|
+
if (candidate.tuple) out.usesTuples = true;
|
|
3865
|
+
else out.usesList = true;
|
|
3866
|
+
out.slugs.push(...mine.map((member) => member.slug));
|
|
3867
|
+
out.bound.push(...mine.map((member) => member.key));
|
|
3868
|
+
out.notes.push(
|
|
3869
|
+
`${candidate.label} renders exactly the ${paths.length} row(s) this run wrote paths for: a row ADDED in the CMS does not appear, and a row DELETED falls back to the copy the template shipped with.`
|
|
3870
|
+
);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
function propCalls(content, expression, row, property) {
|
|
3874
|
+
const named = new RegExp(`^\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}$`);
|
|
3875
|
+
const spread = new RegExp(`^\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}$`);
|
|
3876
|
+
const out = [];
|
|
3877
|
+
const visit = (node) => {
|
|
3878
|
+
if (node.type === "expr") {
|
|
3879
|
+
for (const child of node.children) visit(child);
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
if (node.type !== "element" || node.raw) return;
|
|
3883
|
+
if (node.tag !== "" && !isIntrinsic(node.tag)) {
|
|
3884
|
+
for (const attr of node.attrs) {
|
|
3885
|
+
if (!attr.expressionRange) continue;
|
|
3886
|
+
const text = content.slice(attr.expressionRange.start, attr.expressionRange.end).trim();
|
|
3887
|
+
if (spread.test(text)) out.push({ call: node, prop: property });
|
|
3888
|
+
else if (named.test(text)) out.push({ call: node, prop: attr.name });
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
for (const child of node.children) visit(child);
|
|
3892
|
+
};
|
|
3893
|
+
for (const child of expression.children) visit(child);
|
|
3894
|
+
return out;
|
|
3895
|
+
}
|
|
3896
|
+
function leafRefusal(loop, row, property) {
|
|
3897
|
+
const prop = new RegExp(`=\\s*\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}|\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}`);
|
|
3898
|
+
return prop.test(loop) ? "PROP_TARGET_NOT_FOUND" : "IN_EXPRESSION";
|
|
3899
|
+
}
|
|
3900
|
+
function allocateIndex(loop, taken) {
|
|
3901
|
+
const used = new Set(loop.match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
3902
|
+
if (!used.has("i") && !taken.has("i")) return "i";
|
|
3903
|
+
for (let n = 1; ; n++) if (!used.has(`i_${n}`) && !taken.has(`i_${n}`)) return `i_${n}`;
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
// src/resolve.ts
|
|
3907
|
+
var EXTENSIONS = [".astro", ".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".mjs", ".cjs"];
|
|
3908
|
+
function join(fromFile, specifier) {
|
|
3909
|
+
const parts = fromFile.split("/").slice(0, -1);
|
|
3910
|
+
for (const segment of specifier.split("/")) {
|
|
3911
|
+
if (segment === "." || segment === "") continue;
|
|
3912
|
+
if (segment === "..") parts.pop();
|
|
3913
|
+
else parts.push(segment);
|
|
3914
|
+
}
|
|
3915
|
+
return parts.join("/");
|
|
3916
|
+
}
|
|
3917
|
+
var firstPresent = (base, files) => [base, ...EXTENSIONS.map((ext) => `${base}${ext}`), ...EXTENSIONS.map((ext) => `${base}/index${ext}`)].find(
|
|
3918
|
+
(candidate) => files.has(candidate)
|
|
3919
|
+
) ?? null;
|
|
3920
|
+
function aliasesFrom(sources) {
|
|
3921
|
+
const out = /* @__PURE__ */ new Map();
|
|
3922
|
+
for (const source of sources) {
|
|
3923
|
+
if (!isProjectConfig(source.path)) continue;
|
|
3924
|
+
const parsed = readJsonc(source.content);
|
|
3925
|
+
if (parsed === null) continue;
|
|
3926
|
+
const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
|
|
3927
|
+
const root = source.path.split("/").slice(0, -1).join("/");
|
|
3928
|
+
for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
|
|
3929
|
+
out.set(
|
|
3930
|
+
pattern,
|
|
3931
|
+
targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
return out;
|
|
3936
|
+
}
|
|
3937
|
+
var isProjectConfig = (path) => path.endsWith("tsconfig.json") || path.endsWith("jsconfig.json");
|
|
3938
|
+
function readJsonc(content) {
|
|
3939
|
+
let out = "";
|
|
3940
|
+
let at = 0;
|
|
3941
|
+
while (at < content.length) {
|
|
3942
|
+
const char = content[at];
|
|
3943
|
+
if (char === '"') {
|
|
3944
|
+
const start = at++;
|
|
3945
|
+
while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
|
|
3946
|
+
out += content.slice(start, Math.min(at + 1, content.length));
|
|
3947
|
+
at++;
|
|
3948
|
+
continue;
|
|
3949
|
+
}
|
|
3950
|
+
if (char === "/" && content[at + 1] === "/") {
|
|
3951
|
+
while (at < content.length && content[at] !== "\n") at++;
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3954
|
+
if (char === "/" && content[at + 1] === "*") {
|
|
3955
|
+
const close = content.indexOf("*/", at + 2);
|
|
3956
|
+
at = close < 0 ? content.length : close + 2;
|
|
3957
|
+
continue;
|
|
3958
|
+
}
|
|
3959
|
+
out += char;
|
|
3960
|
+
at++;
|
|
3961
|
+
}
|
|
3962
|
+
try {
|
|
3963
|
+
const parsed = JSON.parse(out.replace(/,(?=\s*[}\]])/g, ""));
|
|
3964
|
+
return isAliasShaped(parsed) ? parsed : null;
|
|
3965
|
+
} catch {
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3970
|
+
function isAliasShaped(value) {
|
|
3971
|
+
if (!isRecord(value)) return false;
|
|
3972
|
+
const options = value.compilerOptions;
|
|
3973
|
+
if (options === void 0) return true;
|
|
3974
|
+
if (!isRecord(options)) return false;
|
|
3975
|
+
if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
|
|
3976
|
+
if (options.paths === void 0) return true;
|
|
3977
|
+
if (!isRecord(options.paths)) return false;
|
|
3978
|
+
return Object.values(options.paths).every(
|
|
3979
|
+
(targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
|
|
3980
|
+
);
|
|
3981
|
+
}
|
|
3982
|
+
function unreadableConfigs(sources) {
|
|
3983
|
+
return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
|
|
3984
|
+
}
|
|
3985
|
+
function extendedConfigs(sources) {
|
|
3986
|
+
return sources.filter((source) => {
|
|
3987
|
+
if (!isProjectConfig(source.path)) return false;
|
|
3988
|
+
const parsed = readJsonc(source.content);
|
|
3989
|
+
return parsed !== null && parsed.extends !== void 0 && parsed.compilerOptions?.paths === void 0;
|
|
3990
|
+
}).map((source) => source.path);
|
|
3991
|
+
}
|
|
3992
|
+
function expand(specifier, aliases) {
|
|
3993
|
+
const out = [];
|
|
3994
|
+
for (const [pattern, targets] of aliases) {
|
|
3995
|
+
const star = pattern.indexOf("*");
|
|
3996
|
+
if (star < 0) {
|
|
3997
|
+
if (specifier === pattern) out.push(...targets);
|
|
3998
|
+
continue;
|
|
3999
|
+
}
|
|
4000
|
+
const head = pattern.slice(0, star);
|
|
4001
|
+
const tail = pattern.slice(star + 1);
|
|
4002
|
+
if (!specifier.startsWith(head) || !specifier.endsWith(tail)) continue;
|
|
4003
|
+
const middle = specifier.slice(head.length, specifier.length - tail.length);
|
|
4004
|
+
out.push(...targets.map((target) => target.replace("*", middle)));
|
|
4005
|
+
}
|
|
4006
|
+
return out;
|
|
4007
|
+
}
|
|
4008
|
+
function resolveSpecifier(fromFile, specifier, files, aliases) {
|
|
4009
|
+
if (specifier.startsWith(".")) return firstPresent(join(fromFile, specifier), files);
|
|
4010
|
+
for (const candidate of expand(specifier, aliases)) {
|
|
4011
|
+
const found = firstPresent(candidate.replace(/\/$/, ""), files);
|
|
4012
|
+
if (found) return found;
|
|
4013
|
+
}
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
4016
|
+
function importsIn(parsed, _dialect) {
|
|
4017
|
+
const out = /* @__PURE__ */ new Map();
|
|
4018
|
+
walkAst(parsed.ast, (node) => {
|
|
4019
|
+
if (node.type !== "ImportDeclaration") return;
|
|
4020
|
+
if (node.importKind === "type") return;
|
|
4021
|
+
const specifier = node.source?.value;
|
|
4022
|
+
if (typeof specifier !== "string") return;
|
|
4023
|
+
for (const entry of node.specifiers ?? []) {
|
|
4024
|
+
if (entry.importKind === "type") continue;
|
|
4025
|
+
const local = entry.local?.name;
|
|
4026
|
+
if (typeof local !== "string") continue;
|
|
4027
|
+
const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
|
|
4028
|
+
const imported = kind === "named" ? entry.imported?.name ?? local : null;
|
|
4029
|
+
out.set(local, { specifier, imported, kind });
|
|
4030
|
+
}
|
|
4031
|
+
});
|
|
4032
|
+
return out;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
// src/images.ts
|
|
4036
|
+
var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".svg"];
|
|
4037
|
+
var ASSET_COMPONENTS = /* @__PURE__ */ new Set(["Image", "Picture"]);
|
|
4038
|
+
var ASSET_MODULE = "astro:assets";
|
|
4039
|
+
function baseName(url) {
|
|
4040
|
+
const file = url.split(/[?#]/)[0].split("/").pop() ?? "";
|
|
4041
|
+
const base = file.split(".")[0] ?? "";
|
|
4042
|
+
return base === "" ? null : base.toLowerCase();
|
|
4043
|
+
}
|
|
4044
|
+
function assetCandidates(parsed, dialect, original) {
|
|
4045
|
+
const wanted = baseName(original);
|
|
4046
|
+
if (wanted === null || parsed.error) return [];
|
|
4047
|
+
const found = [];
|
|
4048
|
+
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
4049
|
+
const specifier = from.specifier.toLowerCase();
|
|
4050
|
+
if (from.kind !== "default" || !IMAGE_EXTENSIONS.some((ext) => specifier.endsWith(ext))) continue;
|
|
4051
|
+
if (baseName(from.specifier) === wanted) found.push(local);
|
|
4052
|
+
}
|
|
4053
|
+
return found;
|
|
4054
|
+
}
|
|
4055
|
+
function assetImport(parsed, dialect, original) {
|
|
4056
|
+
const found = assetCandidates(parsed, dialect, original);
|
|
4057
|
+
return found.length === 1 ? found[0] : null;
|
|
4058
|
+
}
|
|
4059
|
+
function renderedBy(content, parsed, dialect, binding) {
|
|
4060
|
+
const assets = new Set(
|
|
4061
|
+
[...importsIn(parsed, dialect)].filter(([, from]) => from.specifier === ASSET_MODULE).map(([local]) => local)
|
|
4062
|
+
);
|
|
4063
|
+
const out = [];
|
|
4064
|
+
const visit = (node) => {
|
|
4065
|
+
if (node.type === "expr") {
|
|
4066
|
+
for (const child of node.children) visit(child);
|
|
4067
|
+
return;
|
|
4068
|
+
}
|
|
4069
|
+
if (node.type !== "element" || node.raw) return;
|
|
4070
|
+
const sources = IMAGE_SOURCES[node.tag.toLowerCase()] ?? (assets.has(node.tag) && ASSET_COMPONENTS.has(node.tag) ? ["src"] : []);
|
|
4071
|
+
for (const attr of node.attrs) {
|
|
4072
|
+
if (!sources.includes(attr.name) || !attr.expressionRange) continue;
|
|
4073
|
+
const inner = content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/^\{|\}$/g, "").trim();
|
|
4074
|
+
if (inner === binding) out.push({ node, attr: attr.name });
|
|
4075
|
+
}
|
|
4076
|
+
for (const child of node.children) visit(child);
|
|
4077
|
+
};
|
|
4078
|
+
for (const root of parsed.roots) visit(root);
|
|
4079
|
+
return out;
|
|
4080
|
+
}
|
|
4081
|
+
function bindImages(ctx, targets) {
|
|
4082
|
+
const out = { splices: [], slugs: [], bound: [], refused: [] };
|
|
4083
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
4084
|
+
if (!rule.attr || !rule.text || ctx.parsed.error) return out;
|
|
4085
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
4086
|
+
for (const target of targets) {
|
|
4087
|
+
const binding = assetImport(ctx.parsed, ctx.dialect, target.original);
|
|
4088
|
+
const rendered = binding === null ? [] : renderedBy(ctx.content, ctx.parsed, ctx.dialect, binding);
|
|
4089
|
+
const free = rendered.filter((hit2) => !claimed.has(hit2.node.attrInsertAt));
|
|
4090
|
+
if (free.length !== 1) {
|
|
4091
|
+
out.refused.push({ key: target.key, reason: "IMAGE_ASSET_UNRESOLVED" });
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
const hit = free[0];
|
|
4095
|
+
claimed.add(hit.node.attrInsertAt);
|
|
4096
|
+
const attr = hit.node.attrs.find((a) => a.name === hit.attr);
|
|
4097
|
+
const args = [
|
|
4098
|
+
ctx.snapshotName(target.slug),
|
|
4099
|
+
JSON.stringify(target.path),
|
|
4100
|
+
binding,
|
|
4101
|
+
...ctx.slug ? [ctx.slug] : []
|
|
4102
|
+
];
|
|
4103
|
+
out.splices.push({
|
|
4104
|
+
start: attr.range.start,
|
|
4105
|
+
end: attr.range.end,
|
|
4106
|
+
text: rule.attr(hit.attr, `${ctx.image}(${args.join(", ")})`)
|
|
4107
|
+
});
|
|
4108
|
+
out.splices.push({
|
|
4109
|
+
start: hit.node.attrInsertAt,
|
|
4110
|
+
end: hit.node.attrInsertAt,
|
|
4111
|
+
text: ` data-bcms-field="${target.path}" data-bcms-kind="${target.kind || "image"}"`
|
|
4112
|
+
});
|
|
4113
|
+
out.slugs.push(target.slug);
|
|
4114
|
+
out.bound.push(target.key);
|
|
4115
|
+
}
|
|
4116
|
+
return out;
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// src/helper.ts
|
|
4120
|
+
import { createHash as createHash2 } from "crypto";
|
|
4121
|
+
var HELPER_VERSION = 6;
|
|
4122
|
+
var HEADER = "// @bettercms-ai/convert helper v";
|
|
4123
|
+
var HELPER_PATH = Object.fromEntries(
|
|
4124
|
+
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
4125
|
+
);
|
|
4126
|
+
var CONTENT_DIR = "bcms-content";
|
|
4127
|
+
var STUB_CONTENT = "{}";
|
|
4128
|
+
function relativeImport(fromFile, toFile) {
|
|
4129
|
+
const from = fromFile.split("/").slice(0, -1);
|
|
4130
|
+
const to = toFile.split("/");
|
|
4131
|
+
let shared = 0;
|
|
4132
|
+
while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared]) shared++;
|
|
4133
|
+
const up = from.length - shared;
|
|
4134
|
+
const path = [...Array(up).fill(".."), ...to.slice(shared)].join("/");
|
|
4135
|
+
return up === 0 ? `./${path}` : path;
|
|
4136
|
+
}
|
|
4137
|
+
function helperSource(dialect) {
|
|
4138
|
+
const path = HELPER_PATH[dialect];
|
|
4139
|
+
if (!path) throw new Error(`The ${dialect} dialect reads its values at publish time, not at build time.`);
|
|
4140
|
+
const layout = relativeImport(path, `${CONTENT_DIR}/layout.json`);
|
|
4141
|
+
return `${HEADER}${HELPER_VERSION}
|
|
4142
|
+
// Values come from bcms-content/*.json, written into the tree before the build. The committed
|
|
4143
|
+
// stubs are {} \u2014 a build with them renders every fallback below and still succeeds.
|
|
4144
|
+
import layoutSnapshot from "${layout}";
|
|
4145
|
+
|
|
4146
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
4147
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4148
|
+
|
|
4149
|
+
/** Dotted keys and \`[n]\` indices, positionally \u2014 the editor's own path grammar. */
|
|
4150
|
+
function readPath(root: unknown, path: string): unknown {
|
|
4151
|
+
let cursor: unknown = root;
|
|
4152
|
+
for (const segment of path.split(".")) {
|
|
4153
|
+
if (!segment) return undefined;
|
|
4154
|
+
const key = segment.replace(/\\[\\d+\\]/g, "");
|
|
4155
|
+
if (key) {
|
|
4156
|
+
if (!isRecord(cursor)) return undefined;
|
|
4157
|
+
cursor = cursor[key];
|
|
4158
|
+
}
|
|
4159
|
+
for (const index of segment.matchAll(/\\[(\\d+)\\]/g)) {
|
|
4160
|
+
if (!Array.isArray(cursor)) return undefined;
|
|
4161
|
+
cursor = cursor[Number(index[1])];
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
return cursor;
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
/** A stored value as a template renders it: richtext is its html, an image is its url. */
|
|
4168
|
+
function render(value: unknown): string | undefined {
|
|
4169
|
+
if (typeof value === "string") return value;
|
|
4170
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4171
|
+
if (isRecord(value)) {
|
|
4172
|
+
if (typeof value.html === "string") return value.html;
|
|
4173
|
+
if (typeof value.url === "string") return value.url;
|
|
4174
|
+
}
|
|
4175
|
+
return undefined;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
/** One field of one page. \`slug\` selects the entry on a dynamic route. */
|
|
4179
|
+
export function bcms(page: unknown, path: string, fallback: string, slug?: string): string {
|
|
4180
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
4181
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4182
|
+
return render(readPath(root, path)) ?? fallback;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
/** One field of the shared layout \u2014 the header and footer every route renders. */
|
|
4186
|
+
export function bcmsLayout(path: string, fallback: string): string {
|
|
4187
|
+
return render(readPath(layoutSnapshot, path)) ?? fallback;
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
/**
|
|
4191
|
+
* The ROWS of a repeater. \`fallback\` is every row the template shipped with, so a build against
|
|
4192
|
+
* an empty snapshot still renders the same number of cards it always did.
|
|
4193
|
+
*/
|
|
4194
|
+
export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: string): T[] {
|
|
4195
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
4196
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4197
|
+
const value = readPath(root, path);
|
|
4198
|
+
return Array.isArray(value) ? (value as T[]) : fallback;
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
/**
|
|
4202
|
+
* The rows of a repeater whose template renders its OWN property names.
|
|
4203
|
+
*
|
|
4204
|
+
* \u{1F534} A ROW IS MORE THAN ITS COPY. The template's array carries \`tone\`, \`link\`, an icon \u2014 styling
|
|
4205
|
+
* the brief never heard of, because the CMS holds the four sentences and not the class names. So a
|
|
4206
|
+
* CMS row is merged OVER the row the template shipped with rather than replacing it, and \`keys\`
|
|
4207
|
+
* says which template property each CMS leaf lands on. A row an editor ADDS has no counterpart, so
|
|
4208
|
+
* it inherits row 0's styling \u2014 the only answer that renders at all.
|
|
4209
|
+
*/
|
|
4210
|
+
export function bcmsRowsAs<T>(
|
|
4211
|
+
page: unknown,
|
|
4212
|
+
path: string,
|
|
4213
|
+
fallback: T[],
|
|
4214
|
+
keys: Record<string, string>,
|
|
4215
|
+
slug?: string,
|
|
4216
|
+
): T[] {
|
|
4217
|
+
const rows = bcmsRows<unknown>(page, path, [], slug);
|
|
4218
|
+
if (rows.length === 0) return fallback;
|
|
4219
|
+
return rows.map((row, index) => {
|
|
4220
|
+
const base = { ...(fallback[index] ?? fallback[0] ?? {}) } as Record<string, unknown>;
|
|
4221
|
+
for (const property of Object.keys(keys)) {
|
|
4222
|
+
const value = render(readPath(row, keys[property] as string));
|
|
4223
|
+
if (value !== undefined) base[property] = value;
|
|
4224
|
+
}
|
|
4225
|
+
return base as T;
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
/**
|
|
4230
|
+
* A list of PRIMITIVES \u2014 \`const tags = ["\u2026", "\u2026"]\` \u2014 with one CMS field per ELEMENT.
|
|
4231
|
+
*
|
|
4232
|
+
* \u{1F534} THE LENGTH IS \`paths\`, NOT THE CMS. Unlike \`bcmsRows\`, which reads the array itself and
|
|
4233
|
+
* renders as many rows as the CMS holds, this maps over the path list the codemod wrote \u2014 so a row
|
|
4234
|
+
* an editor ADDS has no path here and never renders, and a row an editor DELETES leaves its path
|
|
4235
|
+
* resolving to nothing, which falls back to the copy the template shipped with. Every row's TEXT is
|
|
4236
|
+
* editable; the number of rows is the template's. @see the PRIMITIVE_LIST receipt note, which says
|
|
4237
|
+
* this out loud rather than leaving an editor to discover it.
|
|
4238
|
+
*/
|
|
4239
|
+
export function bcmsList(page: unknown, paths: string[], fallback: string[], slug?: string): string[] {
|
|
4240
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
4241
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4242
|
+
return paths.map((path, index) => render(readPath(root, path)) ?? fallback[index] ?? "");
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
/**
|
|
4246
|
+
* The same for a list of TUPLES \u2014 \`[["q", "a"], \u2026]\` \u2014 with one field per tuple POSITION, and the
|
|
4247
|
+
* same ceiling: \`paths\` decides how many rows render, and an added or deleted CMS row does not.
|
|
4248
|
+
*/
|
|
4249
|
+
export function bcmsTuples(page: unknown, paths: string[][], fallback: string[][], slug?: string): string[][] {
|
|
4250
|
+
return paths.map((row, index) => bcmsList(page, row, fallback[index] ?? [], slug));
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
/** An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED */
|
|
4254
|
+
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
4255
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
4256
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
4257
|
+
return render(readPath(root, path)) ?? fallback;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
/**
|
|
4261
|
+
* ONE SECTION's copy, for a component extracted by \`--componentize\`.
|
|
4262
|
+
*
|
|
4263
|
+
* OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
|
|
4264
|
+
* (Phase B) carries an override for every prop it declares, so the group underneath it is never
|
|
4265
|
+
* read; a placement still bound to a page field group (Phase A) carries none, so the group is all
|
|
4266
|
+
* there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
|
|
4267
|
+
* instance does not hold has to keep rendering the page's value rather than nothing.
|
|
4268
|
+
*
|
|
4269
|
+
* \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
|
|
4270
|
+
* the page's old copy back into a heading somebody had deliberately cleared.
|
|
4271
|
+
*
|
|
4272
|
+
* With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
|
|
4273
|
+
* the fallback the template shipped with \u2014 which is why a section renders correctly against the
|
|
4274
|
+
* committed \`{}\` stub.
|
|
4275
|
+
*/
|
|
4276
|
+
export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
|
|
4277
|
+
const group = bind ? readPath(page, bind) : undefined;
|
|
4278
|
+
const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
|
|
4279
|
+
if (!isRecord(overrides)) return base;
|
|
4280
|
+
for (const key of Object.keys(overrides)) {
|
|
4281
|
+
const value = overrides[key];
|
|
4282
|
+
if (value === undefined || value === null) continue;
|
|
4283
|
+
/**
|
|
4284
|
+
* \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
|
|
4285
|
+
* dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
|
|
4286
|
+
* helper fell back to the rows the template shipped with and the editor's work never showed.
|
|
4287
|
+
*/
|
|
4288
|
+
base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
|
|
4289
|
+
}
|
|
4290
|
+
return base;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
/**
|
|
4294
|
+
* WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
|
|
4295
|
+
*
|
|
4296
|
+
* An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
|
|
4297
|
+
* copy lives. A placement that owns its copy declares the BLOCK lane \u2014
|
|
4298
|
+
* \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
|
|
4299
|
+
* own value to. A placement bound to a page field group declares the PAGE path, which is what the
|
|
4300
|
+
* page form and the coverage meter already know.
|
|
4301
|
+
*
|
|
4302
|
+
* \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
|
|
4303
|
+
* page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
|
|
4304
|
+
* second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
|
|
4305
|
+
* Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
|
|
4306
|
+
* \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
|
|
4307
|
+
* the meter counted a binding that was never there. The prefix the markup was cut with comes out,
|
|
4308
|
+
* the placement's own group goes in.
|
|
4309
|
+
*
|
|
4310
|
+
* \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
|
|
4311
|
+
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
4312
|
+
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
4313
|
+
*/
|
|
4314
|
+
export function bcmsFields(
|
|
4315
|
+
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
4316
|
+
pathPrefix: string,
|
|
4317
|
+
): (path: string) => string {
|
|
4318
|
+
const id = placement.blockId;
|
|
4319
|
+
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
4320
|
+
/**
|
|
4321
|
+
* What replaces the prefix the markup was cut with, or null for "leave every path alone".
|
|
4322
|
+
*
|
|
4323
|
+
* Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
|
|
4324
|
+
* nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
|
|
4325
|
+
* where the plan said the prop key IS the whole page path: there is no group segment to swap,
|
|
4326
|
+
* so prepending one would invent an address rather than correct it.
|
|
4327
|
+
*/
|
|
4328
|
+
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
4329
|
+
return (path: string): string => {
|
|
4330
|
+
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
4331
|
+
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
4332
|
+
// (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
|
|
4333
|
+
if (lane === null || !path.startsWith(pathPrefix)) return path;
|
|
4334
|
+
return lane + path.slice(pathPrefix.length);
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
`;
|
|
4338
|
+
}
|
|
4339
|
+
var HISTORICAL = {
|
|
4340
|
+
"src/bcms-content.ts": {
|
|
4341
|
+
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
4342
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
4343
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4344
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4345
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
4346
|
+
},
|
|
4347
|
+
"src/lib/bcms-content.ts": {
|
|
4348
|
+
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
4349
|
+
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
4350
|
+
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
4351
|
+
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af",
|
|
4352
|
+
5: "c8c93655320fa65664611ba84541f3fa7bfa292cc9a2fd655373440b99049fba"
|
|
4353
|
+
},
|
|
4354
|
+
"composables/bcms-content.ts": {
|
|
4355
|
+
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
4356
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
4357
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
4358
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595",
|
|
4359
|
+
5: "0472fe3ffdcfa49e719c7b9da9cb63b6eba637cc22783bb00c3c2cadd3d800da"
|
|
4360
|
+
}
|
|
4361
|
+
};
|
|
4362
|
+
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
4363
|
+
function isKnownHelper(content, dialect) {
|
|
4364
|
+
if (content === helperSource(dialect)) return HELPER_VERSION;
|
|
4365
|
+
const path = HELPER_PATH[dialect];
|
|
4366
|
+
if (!path) return null;
|
|
4367
|
+
const digest = sha256(content);
|
|
4368
|
+
for (const [version, known] of Object.entries(HISTORICAL[path] ?? {})) {
|
|
4369
|
+
if (known === digest) return Number(version);
|
|
4370
|
+
}
|
|
4371
|
+
return null;
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// src/locate.ts
|
|
4375
|
+
var flat = (value) => value.replace(/\s+/g, " ").trim();
|
|
4376
|
+
var stripTags = (value) => value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "");
|
|
4377
|
+
function locate(brief, sources, options) {
|
|
4378
|
+
const flattened = sources.map((s) => {
|
|
4379
|
+
const readable = unwrapJsxStrings(decodeEntities(s.content));
|
|
4380
|
+
return {
|
|
4381
|
+
path: s.path,
|
|
4382
|
+
content: flat(readable),
|
|
4383
|
+
text: options?.stripTags ? flat(stripTags(readable)) : null
|
|
4384
|
+
};
|
|
4385
|
+
});
|
|
4386
|
+
const located = [];
|
|
4387
|
+
const unlocated = [];
|
|
4388
|
+
for (const page of brief.pages) {
|
|
4389
|
+
for (const p of page.paths) {
|
|
4390
|
+
const original = p.original === null ? "" : flat(p.original);
|
|
4391
|
+
if (!original) {
|
|
4392
|
+
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
|
|
4393
|
+
continue;
|
|
4394
|
+
}
|
|
4395
|
+
const files = flattened.filter((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
|
|
4396
|
+
if (files.length === 0) {
|
|
4397
|
+
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
|
|
4398
|
+
continue;
|
|
4399
|
+
}
|
|
4400
|
+
located.push({ route: page.route, scope: p.scope, path: p.path, kind: p.kind, original, files });
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
return { located, unlocated };
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
// src/loops.ts
|
|
4407
|
+
import MagicString from "magic-string";
|
|
4408
|
+
|
|
4409
|
+
// src/scope.ts
|
|
4410
|
+
function patternNames(node, out) {
|
|
4411
|
+
if (!node || typeof node !== "object") return;
|
|
4412
|
+
const typed = node;
|
|
4413
|
+
switch (typed.type) {
|
|
4414
|
+
case "Identifier":
|
|
4415
|
+
out.add(String(typed.name));
|
|
4416
|
+
return;
|
|
4417
|
+
case "ObjectPattern":
|
|
4418
|
+
for (const property of typed.properties ?? [])
|
|
4419
|
+
patternNames(property.type === "RestElement" ? property.argument : property.value, out);
|
|
4420
|
+
return;
|
|
4421
|
+
case "ArrayPattern":
|
|
4422
|
+
for (const element of typed.elements ?? []) patternNames(element, out);
|
|
4423
|
+
return;
|
|
4424
|
+
case "AssignmentPattern":
|
|
4425
|
+
patternNames(typed.left, out);
|
|
4426
|
+
return;
|
|
4427
|
+
case "RestElement":
|
|
4428
|
+
patternNames(typed.argument, out);
|
|
4429
|
+
return;
|
|
4430
|
+
default:
|
|
4431
|
+
return;
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
function statementNames(node, out) {
|
|
4435
|
+
if (!node || typeof node !== "object") return;
|
|
4436
|
+
const typed = node;
|
|
4437
|
+
switch (typed.type) {
|
|
4438
|
+
case "ExportNamedDeclaration":
|
|
4439
|
+
case "ExportDefaultDeclaration":
|
|
4440
|
+
statementNames(typed.declaration, out);
|
|
4441
|
+
return;
|
|
4442
|
+
case "ImportDeclaration":
|
|
4443
|
+
for (const specifier of typed.specifiers ?? []) patternNames(specifier.local, out);
|
|
4444
|
+
return;
|
|
4445
|
+
case "VariableDeclaration":
|
|
4446
|
+
for (const declarator of typed.declarations ?? []) patternNames(declarator.id, out);
|
|
4447
|
+
return;
|
|
4448
|
+
case "FunctionDeclaration":
|
|
4449
|
+
case "ClassDeclaration":
|
|
4450
|
+
case "TSTypeAliasDeclaration":
|
|
4451
|
+
case "TSInterfaceDeclaration":
|
|
4452
|
+
case "TSEnumDeclaration":
|
|
4453
|
+
case "TSModuleDeclaration":
|
|
4454
|
+
patternNames(typed.id, out);
|
|
4455
|
+
return;
|
|
4456
|
+
default:
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
var DECLARED = /\b(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
|
|
4461
|
+
var IMPORTED = /\bimport\s+([^;\n]*?)\s+from\b/g;
|
|
4462
|
+
function scanNames(code, out) {
|
|
4463
|
+
for (const match of code.matchAll(DECLARED)) out.add(match[1]);
|
|
4464
|
+
for (const match of code.matchAll(IMPORTED))
|
|
4465
|
+
for (const name of match[1].match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
4466
|
+
if (name !== "as" && name !== "type") out.add(name);
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
function scriptText(nodes, content, out) {
|
|
4470
|
+
for (const node of nodes) {
|
|
4471
|
+
if (node.type !== "element") continue;
|
|
4472
|
+
if (node.raw && node.tag === "script" && node.inner) out.push(content.slice(node.inner.start, node.inner.end));
|
|
4473
|
+
scriptText(node.children, content, out);
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
function topLevelBindings(dialect, parsed, content) {
|
|
4477
|
+
const out = /* @__PURE__ */ new Set();
|
|
4478
|
+
const program = parsed.ast?.program;
|
|
4479
|
+
if (program) for (const statement of program.body ?? []) statementNames(statement, out);
|
|
4480
|
+
else if (parsed.script) scanNames(parsed.script.code, out);
|
|
4481
|
+
if (dialect === "html") {
|
|
4482
|
+
const scripts = [];
|
|
4483
|
+
scriptText(parsed.roots, content, scripts);
|
|
4484
|
+
for (const code of scripts) scanNames(code, out);
|
|
4485
|
+
}
|
|
4486
|
+
return out;
|
|
4487
|
+
}
|
|
4488
|
+
function allocateNames(taken, wanted) {
|
|
4489
|
+
const used = new Set(taken);
|
|
4490
|
+
const out = /* @__PURE__ */ new Map();
|
|
4491
|
+
for (const { key, name } of wanted) {
|
|
4492
|
+
if (out.has(key)) continue;
|
|
4493
|
+
let candidate = name;
|
|
4494
|
+
for (let n = 1; used.has(candidate); n++) candidate = `${name}_${n}`;
|
|
4495
|
+
used.add(candidate);
|
|
4496
|
+
out.set(key, candidate);
|
|
4497
|
+
}
|
|
4498
|
+
return out;
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
// src/loops.ts
|
|
4502
|
+
function splitNested(leaf) {
|
|
4503
|
+
const match = leaf.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
|
|
4504
|
+
return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
|
|
4505
|
+
}
|
|
4506
|
+
var elementEnd = (node) => node.inner ? node.inner.end + node.tag.length + 3 : null;
|
|
4507
|
+
function findRows(roots, members) {
|
|
4508
|
+
const wanted = new Set(members.map((m) => m.index));
|
|
4509
|
+
const search = (node) => {
|
|
4510
|
+
if (node.type !== "element") return null;
|
|
4511
|
+
for (const child of node.children) {
|
|
4512
|
+
const deeper = search(child);
|
|
4513
|
+
if (deeper) return deeper;
|
|
4514
|
+
}
|
|
4515
|
+
const rows = [];
|
|
4516
|
+
for (const child of node.children) {
|
|
4517
|
+
if (child.type !== "element") continue;
|
|
4518
|
+
const end = elementEnd(child);
|
|
4519
|
+
if (end === null) continue;
|
|
4520
|
+
const inside = members.filter((m) => m.site.range.start >= child.start && m.site.range.end <= end);
|
|
4521
|
+
if (inside.length === 0) continue;
|
|
4522
|
+
const indices = new Set(inside.map((m) => m.index));
|
|
4523
|
+
if (indices.size !== 1) return null;
|
|
4524
|
+
rows.push({
|
|
4525
|
+
index: inside[0].index,
|
|
4526
|
+
range: { start: child.start, end },
|
|
4527
|
+
attrInsertAt: child.attrInsertAt,
|
|
4528
|
+
// `:key` in vue, `key` in jsx — one existing key, whichever way it is spelled.
|
|
4529
|
+
keyAttr: child.attrs.find((a) => a.name.replace(/^:/, "") === "key")?.range ?? null,
|
|
4530
|
+
node: child,
|
|
4531
|
+
members: inside
|
|
4532
|
+
});
|
|
4533
|
+
}
|
|
4534
|
+
if (rows.length === 0) return null;
|
|
4535
|
+
const covered = new Set(rows.flatMap((r) => r.members.map((m) => m.index)));
|
|
4536
|
+
if (covered.size !== wanted.size || rows.length !== wanted.size) return null;
|
|
4537
|
+
if (rows.some((r) => r.members.length !== members.length / wanted.size)) return null;
|
|
4538
|
+
return rows.sort((a, b) => a.range.start - b.range.start);
|
|
4539
|
+
};
|
|
4540
|
+
for (const root of roots) {
|
|
4541
|
+
const found = search(root);
|
|
4542
|
+
if (found) return found;
|
|
4543
|
+
}
|
|
4544
|
+
return null;
|
|
4545
|
+
}
|
|
4546
|
+
var consecutive = (rows, content) => rows.every((row, at) => at === 0 || content.slice(rows[at - 1].range.end, row.range.start).trim() === "");
|
|
4547
|
+
function maskedRow(row, content) {
|
|
4548
|
+
const masked = new Map(row.members.map((member) => [member.site.inner.start, member.leaf]));
|
|
4549
|
+
return canonical(row.node, content, masked);
|
|
4550
|
+
}
|
|
4551
|
+
var TRIVIA = "";
|
|
4552
|
+
function canonical(node, content, masked) {
|
|
4553
|
+
if (node.type === "text") {
|
|
4554
|
+
return node.value.trim() === "" ? TRIVIA : content.slice(node.range.start, node.range.end);
|
|
4555
|
+
}
|
|
4556
|
+
if (node.type === "comment" || node.type === "expr") {
|
|
4557
|
+
return content.slice(node.range.start, node.range.end);
|
|
4558
|
+
}
|
|
4559
|
+
if (node.type !== "element") return "";
|
|
4560
|
+
const open = content.slice(node.start, node.inner ? node.inner.start : node.attrInsertAt);
|
|
4561
|
+
if (node.inner && masked.has(node.inner.start)) {
|
|
4562
|
+
return `${open}\0${masked.get(node.inner.start)}\0</${node.tag}>`;
|
|
4563
|
+
}
|
|
4564
|
+
const inside = node.children.map((child) => canonical(child, content, masked)).join("");
|
|
4565
|
+
return node.inner ? `${open}${inside}</${node.tag}>` : open;
|
|
4566
|
+
}
|
|
4567
|
+
function sameShape(rows, content) {
|
|
4568
|
+
const first = maskedRow(rows[0], content);
|
|
4569
|
+
return rows.every((row) => maskedRow(row, content) === first);
|
|
4570
|
+
}
|
|
4571
|
+
var indented = (body, indent) => body.split("\n").map((line) => line.startsWith(indent) ? `${indent} ${line.slice(indent.length)}` : `${indent} ${line}`).join("\n");
|
|
4572
|
+
function indentOf(content, at) {
|
|
4573
|
+
const lineStart = content.lastIndexOf("\n", at - 1) + 1;
|
|
4574
|
+
const prefix = content.slice(lineStart, at);
|
|
4575
|
+
return prefix.trim() === "" ? prefix : "";
|
|
4576
|
+
}
|
|
4577
|
+
function writeLeaf(source, shift, member, rule, read, row, literal) {
|
|
4578
|
+
const site = member.site;
|
|
4579
|
+
const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
|
|
4580
|
+
if (member.kind === "richtext" && rule.richtext) {
|
|
4581
|
+
source.remove(site.inner.start + shift, site.inner.end + shift);
|
|
4582
|
+
source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
|
|
4583
|
+
} else {
|
|
4584
|
+
source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
|
|
4585
|
+
}
|
|
4586
|
+
const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
|
|
4587
|
+
source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
|
|
4588
|
+
}
|
|
4589
|
+
function emitNested(rule, array, members, prefix, outer, parent, ctx) {
|
|
4590
|
+
if (!rule.loop || !rule.text) return null;
|
|
4591
|
+
const inner = [];
|
|
4592
|
+
for (const member of members) {
|
|
4593
|
+
const split = splitNested(member.leaf);
|
|
4594
|
+
if (!split || splitNested(split.leaf) !== null) return null;
|
|
4595
|
+
inner.push({ ...member, index: split.index, leaf: split.leaf });
|
|
4596
|
+
}
|
|
4597
|
+
const rows = findRows([parent.node], inner);
|
|
4598
|
+
if (!rows || rows.length === 0) return null;
|
|
4599
|
+
if (!consecutive(rows, ctx.content)) return null;
|
|
4600
|
+
if (!sameShape(rows, ctx.content)) return null;
|
|
4601
|
+
const template = rows[0];
|
|
4602
|
+
const used = /* @__PURE__ */ new Set([
|
|
4603
|
+
...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
|
|
4604
|
+
outer.row,
|
|
4605
|
+
outer.index
|
|
4606
|
+
]);
|
|
4607
|
+
const allocated = allocateNames(used, [
|
|
4608
|
+
{ key: "row", name: "item" },
|
|
4609
|
+
{ key: "index", name: "j" }
|
|
4610
|
+
]);
|
|
4611
|
+
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
4612
|
+
const fallback = [];
|
|
4613
|
+
const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
|
|
4614
|
+
if (!writable) return null;
|
|
4615
|
+
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
4616
|
+
const shift = -template.range.start;
|
|
4617
|
+
const declarations = [];
|
|
4618
|
+
for (const member of template.members) {
|
|
4619
|
+
const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
|
|
4620
|
+
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
4621
|
+
declarations.push({ path: shapeOfPath(literal), literal });
|
|
4622
|
+
}
|
|
4623
|
+
const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
|
|
4624
|
+
if (rule.loop.kind === "attr") {
|
|
4625
|
+
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
4626
|
+
}
|
|
4627
|
+
if (rule.loopKey) {
|
|
4628
|
+
const key = rule.loopKey(names.index);
|
|
4629
|
+
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
4630
|
+
else if (template.keyAttr.end > template.keyAttr.start)
|
|
4631
|
+
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
4632
|
+
else return null;
|
|
4633
|
+
}
|
|
4634
|
+
const indent = indentOf(ctx.content, template.range.start);
|
|
4635
|
+
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
4636
|
+
${indented(source.toString(), indent)}
|
|
4637
|
+
${indent}${rule.loop.close}` : source.toString();
|
|
4638
|
+
return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
|
|
4639
|
+
}
|
|
4640
|
+
function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
|
|
4641
|
+
const rule = DIALECT_RULES[dialect];
|
|
4642
|
+
if (!rule.loop || !rule.text || group.length === 0) return null;
|
|
4643
|
+
const rows = findRows(ctx.roots, group);
|
|
4644
|
+
if (!rows || rows.length < 2) return null;
|
|
4645
|
+
if (!consecutive(rows, ctx.content)) return null;
|
|
4646
|
+
if (!sameShape(rows, ctx.content)) return null;
|
|
4647
|
+
if (group.some((m) => m.site.where !== "text" || !isIntrinsic(m.site.tag) || !m.site.inner)) return null;
|
|
4648
|
+
const template = rows[0];
|
|
4649
|
+
const used = new Set(ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
4650
|
+
const allocated = allocateNames(used, [
|
|
4651
|
+
{ key: "row", name: "card" },
|
|
4652
|
+
{ key: "index", name: "i" }
|
|
4653
|
+
]);
|
|
4654
|
+
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
4655
|
+
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
4656
|
+
const shift = -template.range.start;
|
|
4657
|
+
const declarations = [];
|
|
4658
|
+
const nested = /* @__PURE__ */ new Map();
|
|
4659
|
+
const direct = [];
|
|
4660
|
+
for (const member of template.members) {
|
|
4661
|
+
const inner = splitNested(member.leaf);
|
|
4662
|
+
if (inner === null) direct.push(member);
|
|
4663
|
+
else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
|
|
3771
4664
|
}
|
|
3772
|
-
|
|
3773
|
-
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
const out = /* @__PURE__ */ new Map();
|
|
3779
|
-
for (const source of sources) {
|
|
3780
|
-
if (!isProjectConfig(source.path)) continue;
|
|
3781
|
-
const parsed = readJsonc(source.content);
|
|
3782
|
-
if (parsed === null) continue;
|
|
3783
|
-
const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
|
|
3784
|
-
const root = source.path.split("/").slice(0, -1).join("/");
|
|
3785
|
-
for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
|
|
3786
|
-
out.set(
|
|
3787
|
-
pattern,
|
|
3788
|
-
targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
|
|
3789
|
-
);
|
|
3790
|
-
}
|
|
4665
|
+
for (const [array, members] of nested) {
|
|
4666
|
+
const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
|
|
4667
|
+
if (!plan2) return null;
|
|
4668
|
+
if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
|
|
4669
|
+
source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
|
|
4670
|
+
declarations.push(...plan2.declarations);
|
|
3791
4671
|
}
|
|
3792
|
-
|
|
3793
|
-
}
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
let out = "";
|
|
3797
|
-
let at = 0;
|
|
3798
|
-
while (at < content.length) {
|
|
3799
|
-
const char = content[at];
|
|
3800
|
-
if (char === '"') {
|
|
3801
|
-
const start = at++;
|
|
3802
|
-
while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
|
|
3803
|
-
out += content.slice(start, Math.min(at + 1, content.length));
|
|
3804
|
-
at++;
|
|
3805
|
-
continue;
|
|
3806
|
-
}
|
|
3807
|
-
if (char === "/" && content[at + 1] === "/") {
|
|
3808
|
-
while (at < content.length && content[at] !== "\n") at++;
|
|
3809
|
-
continue;
|
|
3810
|
-
}
|
|
3811
|
-
if (char === "/" && content[at + 1] === "*") {
|
|
3812
|
-
const close = content.indexOf("*/", at + 2);
|
|
3813
|
-
at = close < 0 ? content.length : close + 2;
|
|
3814
|
-
continue;
|
|
3815
|
-
}
|
|
3816
|
-
out += char;
|
|
3817
|
-
at++;
|
|
4672
|
+
for (const member of direct) {
|
|
4673
|
+
const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
|
|
4674
|
+
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
4675
|
+
declarations.push({ path: shapeOfPath(literal), literal });
|
|
3818
4676
|
}
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
return null;
|
|
4677
|
+
const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
|
|
4678
|
+
const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
|
|
4679
|
+
if (rule.loop.kind === "attr") {
|
|
4680
|
+
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
3824
4681
|
}
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
if (!isRecord(options)) return false;
|
|
3832
|
-
if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
|
|
3833
|
-
if (options.paths === void 0) return true;
|
|
3834
|
-
if (!isRecord(options.paths)) return false;
|
|
3835
|
-
return Object.values(options.paths).every(
|
|
3836
|
-
(targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
|
|
3837
|
-
);
|
|
3838
|
-
}
|
|
3839
|
-
function unreadableConfigs(sources) {
|
|
3840
|
-
return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
|
|
3841
|
-
}
|
|
3842
|
-
function expand(specifier, aliases) {
|
|
3843
|
-
const out = [];
|
|
3844
|
-
for (const [pattern, targets] of aliases) {
|
|
3845
|
-
const star = pattern.indexOf("*");
|
|
3846
|
-
if (star < 0) {
|
|
3847
|
-
if (specifier === pattern) out.push(...targets);
|
|
3848
|
-
continue;
|
|
3849
|
-
}
|
|
3850
|
-
const head = pattern.slice(0, star);
|
|
3851
|
-
const tail = pattern.slice(star + 1);
|
|
3852
|
-
if (!specifier.startsWith(head) || !specifier.endsWith(tail)) continue;
|
|
3853
|
-
const middle = specifier.slice(head.length, specifier.length - tail.length);
|
|
3854
|
-
out.push(...targets.map((target) => target.replace("*", middle)));
|
|
4682
|
+
if (rule.loopKey) {
|
|
4683
|
+
const key = rule.loopKey(names.index);
|
|
4684
|
+
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
4685
|
+
else if (template.keyAttr.end > template.keyAttr.start)
|
|
4686
|
+
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
4687
|
+
else return null;
|
|
3855
4688
|
}
|
|
3856
|
-
|
|
4689
|
+
const indent = indentOf(ctx.content, template.range.start);
|
|
4690
|
+
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
4691
|
+
${indented(source.toString(), indent)}
|
|
4692
|
+
${indent}${rule.loop.close}` : source.toString();
|
|
4693
|
+
return {
|
|
4694
|
+
range: { start: template.range.start, end: rows[rows.length - 1].range.end },
|
|
4695
|
+
text,
|
|
4696
|
+
declarations,
|
|
4697
|
+
bindings: [...new Set(declarations.map((d) => d.path))],
|
|
4698
|
+
names
|
|
4699
|
+
};
|
|
3857
4700
|
}
|
|
3858
|
-
function
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
const
|
|
3862
|
-
|
|
4701
|
+
function setLeaf(row, leaf, value) {
|
|
4702
|
+
const segments = leaf.split(".").flatMap((segment) => {
|
|
4703
|
+
const name = segment.replace(/\[\d+\]/g, "");
|
|
4704
|
+
const indices = [...segment.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]));
|
|
4705
|
+
return [...name ? [name] : [], ...indices];
|
|
4706
|
+
});
|
|
4707
|
+
if (segments.some((segment) => typeof segment === "string" && FORBIDDEN.has(segment))) return false;
|
|
4708
|
+
let cursor = row;
|
|
4709
|
+
for (const [at, segment] of segments.entries()) {
|
|
4710
|
+
const key = segment;
|
|
4711
|
+
const last = at === segments.length - 1;
|
|
4712
|
+
if (last) {
|
|
4713
|
+
cursor[key] = value;
|
|
4714
|
+
return true;
|
|
4715
|
+
}
|
|
4716
|
+
const held = Object.prototype.hasOwnProperty.call(cursor, key) ? cursor[key] : void 0;
|
|
4717
|
+
if (held === void 0 || typeof held !== "object" || held === null) {
|
|
4718
|
+
cursor[key] = typeof segments[at + 1] === "number" ? [] : emptyRow();
|
|
4719
|
+
}
|
|
4720
|
+
cursor = cursor[key];
|
|
3863
4721
|
}
|
|
3864
|
-
return
|
|
4722
|
+
return true;
|
|
3865
4723
|
}
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
if (typeof specifier !== "string") return;
|
|
3873
|
-
for (const entry of node.specifiers ?? []) {
|
|
3874
|
-
if (entry.importKind === "type") continue;
|
|
3875
|
-
const local = entry.local?.name;
|
|
3876
|
-
if (typeof local !== "string") continue;
|
|
3877
|
-
const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
|
|
3878
|
-
const imported = kind === "named" ? entry.imported?.name ?? local : null;
|
|
3879
|
-
out.set(local, { specifier, imported, kind });
|
|
3880
|
-
}
|
|
3881
|
-
});
|
|
3882
|
-
return out;
|
|
4724
|
+
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
4725
|
+
var emptyRow = () => /* @__PURE__ */ Object.create(null);
|
|
4726
|
+
function splitIndexed(arrayPath, path) {
|
|
4727
|
+
if (!path.startsWith(`${arrayPath}[`)) return null;
|
|
4728
|
+
const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
|
|
4729
|
+
return match ? { index: Number(match[1]), leaf: match[2] } : null;
|
|
3883
4730
|
}
|
|
3884
4731
|
|
|
3885
4732
|
// src/routes.ts
|
|
@@ -4044,8 +4891,8 @@ function declaringElements(roots, content) {
|
|
|
4044
4891
|
const name = attr.name.replace(/^:/, "");
|
|
4045
4892
|
if (name === "bcmsBindings") {
|
|
4046
4893
|
const own = ownBytes(node, content);
|
|
4047
|
-
for (const
|
|
4048
|
-
out.push({ scope: "page", path
|
|
4894
|
+
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
4895
|
+
out.push({ scope: "page", path, kind, own });
|
|
4049
4896
|
}
|
|
4050
4897
|
continue;
|
|
4051
4898
|
}
|
|
@@ -4075,14 +4922,18 @@ function declaringElements(roots, content) {
|
|
|
4075
4922
|
return out;
|
|
4076
4923
|
}
|
|
4077
4924
|
function helperLocals(parsed, dialect, file) {
|
|
4078
|
-
const out = { bcms: [], bcmsRows: [], bcmsLayout: [] };
|
|
4925
|
+
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [], bcmsList: [], bcmsTuples: [] };
|
|
4079
4926
|
const helper = HELPER_PATH[dialect];
|
|
4080
4927
|
if (!helper) return out;
|
|
4081
4928
|
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
4082
4929
|
if (!namesFile(file, from.specifier, helper)) continue;
|
|
4083
4930
|
if (from.imported === "bcms") out.bcms.push(local);
|
|
4084
4931
|
if (from.imported === "bcmsRows") out.bcmsRows.push(local);
|
|
4932
|
+
if (from.imported === "bcmsRowsAs") out.bcmsRowsAs.push(local);
|
|
4933
|
+
if (from.imported === "bcmsImage") out.bcmsImage.push(local);
|
|
4085
4934
|
if (from.imported === "bcmsLayout") out.bcmsLayout.push(local);
|
|
4935
|
+
if (from.imported === "bcmsList") out.bcmsList.push(local);
|
|
4936
|
+
if (from.imported === "bcmsTuples") out.bcmsTuples.push(local);
|
|
4086
4937
|
}
|
|
4087
4938
|
return out;
|
|
4088
4939
|
}
|
|
@@ -4113,19 +4964,29 @@ function snapshotLocal(parsed, dialect, slug, file) {
|
|
|
4113
4964
|
}
|
|
4114
4965
|
return null;
|
|
4115
4966
|
}
|
|
4967
|
+
function moduleBytes(roots, content) {
|
|
4968
|
+
let out = "";
|
|
4969
|
+
let at = 0;
|
|
4970
|
+
for (const root of roots) {
|
|
4971
|
+
if (root.type !== "element" || root.start < at) continue;
|
|
4972
|
+
out += content.slice(at, root.start);
|
|
4973
|
+
at = Math.max(at, root.end);
|
|
4974
|
+
}
|
|
4975
|
+
return out + content.slice(at);
|
|
4976
|
+
}
|
|
4116
4977
|
var identifier = (name) => `(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`;
|
|
4117
|
-
function elementReads(own, target, locals, snapshot) {
|
|
4978
|
+
function elementReads(own, target, locals, snapshot, viaRow) {
|
|
4118
4979
|
const quoted = escapeRegExp(JSON.stringify(target.path));
|
|
4119
4980
|
if (target.scope === "layout") {
|
|
4120
4981
|
return locals.bcmsLayout.some((name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${quoted}`).test(own));
|
|
4121
4982
|
}
|
|
4122
4983
|
const indexed = splitArrayPath(target.path);
|
|
4123
|
-
if (indexed) {
|
|
4984
|
+
if (viaRow && indexed) {
|
|
4124
4985
|
const leaf = escapeRegExp(JSON.stringify(indexed.leaf));
|
|
4125
4986
|
return locals.bcms.some((name) => new RegExp(`${identifier(name)}\\s*\\([^)]*,\\s*${leaf}`).test(own));
|
|
4126
4987
|
}
|
|
4127
4988
|
if (!snapshot) return false;
|
|
4128
|
-
return locals.bcms.some(
|
|
4989
|
+
return [...locals.bcms, ...locals.bcmsImage].some(
|
|
4129
4990
|
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${quoted}`).test(own)
|
|
4130
4991
|
);
|
|
4131
4992
|
}
|
|
@@ -4135,16 +4996,36 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4135
4996
|
const snapshot = snapshotLocal(parsed, dialect, target.slug, file);
|
|
4136
4997
|
const shape = shapeOfPath(target.path);
|
|
4137
4998
|
const indexed = splitArrayPath(target.path);
|
|
4999
|
+
let keyed = false;
|
|
5000
|
+
let rows = false;
|
|
4138
5001
|
if (indexed) {
|
|
4139
5002
|
const array = escapeRegExp(JSON.stringify(indexed.array));
|
|
4140
|
-
const
|
|
5003
|
+
const reads = (names) => names.some(
|
|
4141
5004
|
(name) => snapshot !== null && new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${array}`).test(content)
|
|
4142
5005
|
);
|
|
4143
|
-
|
|
5006
|
+
keyed = reads(locals.bcmsRowsAs);
|
|
5007
|
+
rows = keyed || reads(locals.bcmsRows);
|
|
4144
5008
|
}
|
|
4145
|
-
|
|
4146
|
-
(
|
|
5009
|
+
const listed = indexed !== null && snapshot !== null && content.includes(JSON.stringify(target.path)) && [...locals.bcmsList, ...locals.bcmsTuples].some(
|
|
5010
|
+
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,`).test(content)
|
|
4147
5011
|
);
|
|
5012
|
+
let cached = null;
|
|
5013
|
+
const moduleRead = () => {
|
|
5014
|
+
if (indexed || snapshot === null) return false;
|
|
5015
|
+
cached ??= moduleBytes(parsed.roots, content);
|
|
5016
|
+
const quoted = escapeRegExp(JSON.stringify(target.path));
|
|
5017
|
+
return locals.bcms.some(
|
|
5018
|
+
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${quoted}`).test(cached)
|
|
5019
|
+
);
|
|
5020
|
+
};
|
|
5021
|
+
return declaringElements(parsed.roots, content).some((element) => {
|
|
5022
|
+
if (element.scope !== target.scope) return false;
|
|
5023
|
+
if (element.path === target.path) {
|
|
5024
|
+
return elementReads(element.own, target, locals, snapshot, false) || moduleRead();
|
|
5025
|
+
}
|
|
5026
|
+
if (element.path !== shape) return false;
|
|
5027
|
+
return listed || rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
5028
|
+
});
|
|
4148
5029
|
}
|
|
4149
5030
|
|
|
4150
5031
|
// src/rewrite.ts
|
|
@@ -4210,7 +5091,7 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
4210
5091
|
addDeclaration(site.attrInsertAt, declaration(rewrite));
|
|
4211
5092
|
continue;
|
|
4212
5093
|
}
|
|
4213
|
-
if (kind === "richtext") {
|
|
5094
|
+
if (kind === "richtext" && !(site.mixed && site.textRange)) {
|
|
4214
5095
|
if (!isIntrinsic(site.tag)) continue;
|
|
4215
5096
|
if (rule.richtext && site.inner) {
|
|
4216
5097
|
source.remove(site.inner.start, site.inner.end);
|
|
@@ -4221,11 +5102,13 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
4221
5102
|
continue;
|
|
4222
5103
|
}
|
|
4223
5104
|
if (site.mixed && site.textRange) {
|
|
5105
|
+
const html = kind === "richtext" && rule.richtext ? rule.richtext(expression) : null;
|
|
4224
5106
|
const inner = rule.text ? rule.text(expression) : content.slice(site.textRange.start, site.textRange.end).trim();
|
|
5107
|
+
const attributes2 = attrsFor(path, kind, void 0, rewrite.scope);
|
|
4225
5108
|
source.overwrite(
|
|
4226
5109
|
site.textRange.start,
|
|
4227
5110
|
site.textRange.end,
|
|
4228
|
-
`<span ${
|
|
5111
|
+
html ? `<span ${attributes2} ${html}></span>` : `<span ${attributes2}>${inner}</span>`
|
|
4229
5112
|
);
|
|
4230
5113
|
continue;
|
|
4231
5114
|
}
|
|
@@ -4481,6 +5364,7 @@ function convert3(node, content) {
|
|
|
4481
5364
|
}
|
|
4482
5365
|
];
|
|
4483
5366
|
}
|
|
5367
|
+
if (node.type === "fragment") return (node.children ?? []).flatMap((child) => convert3(child, content));
|
|
4484
5368
|
if (node.type !== "element" && node.type !== "component" && node.type !== "custom-element") return [];
|
|
4485
5369
|
const tag = node.name ?? "";
|
|
4486
5370
|
const children = (node.children ?? []).flatMap((child) => convert3(child, content));
|
|
@@ -4855,16 +5739,16 @@ function functionBindings(fn) {
|
|
|
4855
5739
|
if (body?.type === "BlockStatement") statementNames2(body.body, out);
|
|
4856
5740
|
return out;
|
|
4857
5741
|
}
|
|
4858
|
-
function
|
|
5742
|
+
function stringLiterals2(node, out) {
|
|
4859
5743
|
if (!isNode(node)) {
|
|
4860
|
-
if (Array.isArray(node)) for (const child of node)
|
|
5744
|
+
if (Array.isArray(node)) for (const child of node) stringLiterals2(child, out);
|
|
4861
5745
|
return;
|
|
4862
5746
|
}
|
|
4863
5747
|
if (FUNCTIONS3.has(node.type)) return;
|
|
4864
5748
|
if (node.type === "StringLiteral" && typeof node.value === "string") out.add(node.value);
|
|
4865
5749
|
for (const key of Object.keys(node)) {
|
|
4866
5750
|
if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
|
|
4867
|
-
|
|
5751
|
+
stringLiterals2(node[key], out);
|
|
4868
5752
|
}
|
|
4869
5753
|
}
|
|
4870
5754
|
function usages(ast, local) {
|
|
@@ -4881,7 +5765,7 @@ function usages(ast, local) {
|
|
|
4881
5765
|
if (FUNCTIONS3.has(node.type)) {
|
|
4882
5766
|
nextScope = /* @__PURE__ */ new Set([...scope, ...functionBindings(node)]);
|
|
4883
5767
|
nextLiterals = /* @__PURE__ */ new Set();
|
|
4884
|
-
|
|
5768
|
+
stringLiterals2(node.body, nextLiterals);
|
|
4885
5769
|
}
|
|
4886
5770
|
if (node.type === "JSXElement") {
|
|
4887
5771
|
const name = node.openingElement?.name;
|
|
@@ -5211,12 +6095,29 @@ var SOURCE_EXTENSIONS = [
|
|
|
5211
6095
|
".erb",
|
|
5212
6096
|
".twig"
|
|
5213
6097
|
];
|
|
6098
|
+
var PROSE_EXTENSIONS = [".md", ".mdx", ".njk", ".hbs", ".ejs", ".liquid", ".erb", ".twig"];
|
|
6099
|
+
var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
|
|
6100
|
+
"pages",
|
|
6101
|
+
"content",
|
|
6102
|
+
"src",
|
|
6103
|
+
"app",
|
|
6104
|
+
"routes",
|
|
6105
|
+
"layouts",
|
|
6106
|
+
"components",
|
|
6107
|
+
"templates",
|
|
6108
|
+
"views",
|
|
6109
|
+
"_includes",
|
|
6110
|
+
"_layouts"
|
|
6111
|
+
]);
|
|
5214
6112
|
function isSourceCandidate(path) {
|
|
5215
6113
|
const lower = path.toLowerCase();
|
|
5216
6114
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
5217
6115
|
return false;
|
|
5218
6116
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
5219
|
-
|
|
6117
|
+
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
6118
|
+
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
6119
|
+
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
6120
|
+
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
5220
6121
|
}
|
|
5221
6122
|
|
|
5222
6123
|
// src/declared.ts
|
|
@@ -7042,6 +7943,7 @@ var ConvertError = class extends Error {
|
|
|
7042
7943
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
7043
7944
|
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7044
7945
|
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
7946
|
+
var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
|
|
7045
7947
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
7046
7948
|
const miss = { ok: false, reason: "PROP_TARGET_NOT_FOUND", edits: [] };
|
|
7047
7949
|
if (hop > MAX_HOPS) return { ok: false, reason: "PROP_DRILLED_DEEP", edits: [] };
|
|
@@ -7138,7 +8040,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7138
8040
|
const parsed = await parseOf(source.path);
|
|
7139
8041
|
declarations.set(
|
|
7140
8042
|
source.path,
|
|
7141
|
-
parsed === null ? /* @__PURE__ */ new Set() : parsed.error ? declaredByScan(source.content) : declaredInTree(parsed.roots, source.content)
|
|
8043
|
+
parsed === null ? isTolerantDialect(source.path) ? declaredByScan(source.content) : /* @__PURE__ */ new Set() : parsed.error ? declaredByScan(source.content) : declaredInTree(parsed.roots, source.content)
|
|
7142
8044
|
);
|
|
7143
8045
|
}
|
|
7144
8046
|
let declaredOnly = 0;
|
|
@@ -7170,6 +8072,36 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7170
8072
|
const key = keyOf(hit.route, hit.scope ?? "page", hit.path);
|
|
7171
8073
|
if (!locatedByKey.has(key)) locatedByKey.set(key, hit);
|
|
7172
8074
|
}
|
|
8075
|
+
const imageUnresolved = /* @__PURE__ */ new Map();
|
|
8076
|
+
for (const miss of unlocated) {
|
|
8077
|
+
const key = keyOf(miss.route, miss.scope ?? "page", miss.path);
|
|
8078
|
+
const target = targets.get(key);
|
|
8079
|
+
if (!target || target.kind !== "image" || !target.fallback || locatedByKey.has(key)) continue;
|
|
8080
|
+
const hosts = [];
|
|
8081
|
+
for (const file of pageFilesFor(target.route, contents.keys())) {
|
|
8082
|
+
const parsed = await parseOf(file);
|
|
8083
|
+
const dialect = dialectOf(file, contents.get(file) ?? "");
|
|
8084
|
+
const bindings = parsed && dialect ? assetCandidates(parsed, dialect, target.fallback) : [];
|
|
8085
|
+
if (bindings.length > 0) hosts.push({ file, bindings });
|
|
8086
|
+
}
|
|
8087
|
+
const only = hosts.length === 1 && hosts[0].bindings.length === 1 ? hosts[0] : null;
|
|
8088
|
+
if (only) {
|
|
8089
|
+
locatedByKey.set(key, {
|
|
8090
|
+
route: target.route,
|
|
8091
|
+
scope: target.scope,
|
|
8092
|
+
path: target.path,
|
|
8093
|
+
kind: target.kind,
|
|
8094
|
+
original: target.literal,
|
|
8095
|
+
files: [only.file]
|
|
8096
|
+
});
|
|
8097
|
+
continue;
|
|
8098
|
+
}
|
|
8099
|
+
const named = hosts.flatMap((host) => host.bindings.map((binding) => `${host.file}:${binding}`));
|
|
8100
|
+
imageUnresolved.set(
|
|
8101
|
+
key,
|
|
8102
|
+
named.length === 0 ? `No asset imported by this route's page has the base name of ${target.fallback}.` : `${named.join(", ")} all match the base name of ${target.fallback}, and nothing says which.`
|
|
8103
|
+
);
|
|
8104
|
+
}
|
|
7173
8105
|
const sharingPath = /* @__PURE__ */ new Map();
|
|
7174
8106
|
for (const [key, hit] of locatedByKey) {
|
|
7175
8107
|
const held = targets.get(key);
|
|
@@ -7213,6 +8145,11 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7213
8145
|
declaredRow(target, false, byHand);
|
|
7214
8146
|
continue;
|
|
7215
8147
|
}
|
|
8148
|
+
const unresolved = imageUnresolved.get(key);
|
|
8149
|
+
if (unresolved !== void 0) {
|
|
8150
|
+
pend2(target, "IMAGE_ASSET_UNRESOLVED", void 0, unresolved);
|
|
8151
|
+
continue;
|
|
8152
|
+
}
|
|
7216
8153
|
pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
|
|
7217
8154
|
}
|
|
7218
8155
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -7234,6 +8171,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7234
8171
|
const receiptFiles = [];
|
|
7235
8172
|
const dialects = /* @__PURE__ */ new Set();
|
|
7236
8173
|
const dynamicBindings = [];
|
|
8174
|
+
const laneNotes = [];
|
|
7237
8175
|
const snapshotSlugs = /* @__PURE__ */ new Set();
|
|
7238
8176
|
let occurrencesLocated = 0;
|
|
7239
8177
|
let occurrencesRewritten = 0;
|
|
@@ -7259,6 +8197,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7259
8197
|
outFiles.push({ path: file, operation: "modify", content: result.content });
|
|
7260
8198
|
receiptFiles.push({ path: file, bindings: result.bindings, tier: result.tier });
|
|
7261
8199
|
dynamicBindings.push(...result.dynamic);
|
|
8200
|
+
laneNotes.push(...result.notes);
|
|
7262
8201
|
for (const slug of result.snapshots) snapshotSlugs.add(slug);
|
|
7263
8202
|
}
|
|
7264
8203
|
}
|
|
@@ -7270,7 +8209,14 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7270
8209
|
const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
|
|
7271
8210
|
if (worst) {
|
|
7272
8211
|
const file = hit.files[outcomes.indexOf(worst)];
|
|
7273
|
-
pend2(
|
|
8212
|
+
pend2(
|
|
8213
|
+
target,
|
|
8214
|
+
worst,
|
|
8215
|
+
file,
|
|
8216
|
+
// The reason says the source does not render it and the file name says where it was
|
|
8217
|
+
// found; without the second half the two read as a contradiction. @see unplacedReason
|
|
8218
|
+
worst === "NOT_IN_SOURCE" && file ? `${file} contains this copy, but no element renders it on its own.` : void 0
|
|
8219
|
+
);
|
|
7274
8220
|
} else if (outcomes.every((o) => o === "declared")) {
|
|
7275
8221
|
const witness = await witnessed(target);
|
|
7276
8222
|
const byHand = witness === null ? declaredByHand(target, hit.files) : null;
|
|
@@ -7348,13 +8294,25 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7348
8294
|
helperUpgraded,
|
|
7349
8295
|
dynamicBindings,
|
|
7350
8296
|
canvas: { lane: canvas.lane, ...canvas.files.length > 0 ? { files: canvas.files } : {} },
|
|
7351
|
-
notes:
|
|
7352
|
-
(
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
8297
|
+
notes: [
|
|
8298
|
+
...unreadableConfigs(sources).map(
|
|
8299
|
+
(file) => ({
|
|
8300
|
+
code: "TSCONFIG_UNREADABLE",
|
|
8301
|
+
file,
|
|
8302
|
+
message: `${file} is not JSON this package can read, so its \`paths\` aliases resolved to nothing.`
|
|
8303
|
+
})
|
|
8304
|
+
),
|
|
8305
|
+
// An absent alias table looks exactly like a project that has none, and the two have
|
|
8306
|
+
// opposite consequences for every component reached through one. @see extendedConfigs
|
|
8307
|
+
...extendedConfigs(sources).map(
|
|
8308
|
+
(file) => ({
|
|
8309
|
+
code: "TSCONFIG_EXTENDS_ONLY",
|
|
8310
|
+
file,
|
|
8311
|
+
message: `${file} declares no \`compilerOptions.paths\` of its own and extends another config, which this package does not follow \u2014 so an import through a path alias resolved to nothing and any prop drilled through one is PROP_TARGET_NOT_FOUND.`
|
|
8312
|
+
})
|
|
8313
|
+
),
|
|
8314
|
+
...laneNotes
|
|
8315
|
+
]
|
|
7358
8316
|
});
|
|
7359
8317
|
return { files: outFiles.sort((a, b) => a.path.localeCompare(b.path)), receipt };
|
|
7360
8318
|
}
|
|
@@ -7376,12 +8334,17 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7376
8334
|
dialect: dialectOf(file, content),
|
|
7377
8335
|
dynamic: [],
|
|
7378
8336
|
componentEdits: [],
|
|
7379
|
-
snapshots: []
|
|
8337
|
+
snapshots: [],
|
|
8338
|
+
notes: []
|
|
7380
8339
|
};
|
|
7381
8340
|
if (pendingTargets.length === 0) return empty;
|
|
7382
8341
|
if (!dialect) {
|
|
7383
8342
|
if (isTolerantDialect(file)) {
|
|
7384
|
-
const
|
|
8343
|
+
const already = declaredByScan(content);
|
|
8344
|
+
const remaining = pendingTargets.filter((target) => !covers(already, target));
|
|
8345
|
+
for (const target of pendingTargets) if (covers(already, target)) outcomes.set(target.key, "declared");
|
|
8346
|
+
if (remaining.length === 0) return { ...empty, dialect: null, outcomes };
|
|
8347
|
+
const tolerantLiterals = [...new Set(remaining.map((t) => t.literal))];
|
|
7385
8348
|
const tolerantSites = findSitesTolerant(content, tolerantLiterals);
|
|
7386
8349
|
if (tolerantSites.length > 0) {
|
|
7387
8350
|
return plan(
|
|
@@ -7391,7 +8354,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7391
8354
|
// argument is behind `!parsed.error` or `tier !== 2`. Passing the writing dialect keeps
|
|
7392
8355
|
// the parameter honest rather than widening its type for one caller.
|
|
7393
8356
|
"html",
|
|
7394
|
-
|
|
8357
|
+
remaining,
|
|
7395
8358
|
tolerantSites,
|
|
7396
8359
|
void 0,
|
|
7397
8360
|
// no parse, no import point: tier 2 declares, it does not read
|
|
@@ -7404,7 +8367,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7404
8367
|
);
|
|
7405
8368
|
}
|
|
7406
8369
|
}
|
|
7407
|
-
for (const target of pendingTargets) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
|
|
8370
|
+
for (const target of pendingTargets) if (!outcomes.has(target.key)) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
|
|
7408
8371
|
return { ...empty, dialect: null, outcomes };
|
|
7409
8372
|
}
|
|
7410
8373
|
const literals = [...new Set(pendingTargets.map((t) => t.literal))];
|
|
@@ -7433,7 +8396,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7433
8396
|
dialect: null,
|
|
7434
8397
|
dynamic: [],
|
|
7435
8398
|
componentEdits: [],
|
|
7436
|
-
snapshots: []
|
|
8399
|
+
snapshots: [],
|
|
8400
|
+
notes: []
|
|
7437
8401
|
};
|
|
7438
8402
|
}
|
|
7439
8403
|
if (rescued.reason === "unverifiable") {
|
|
@@ -7441,8 +8405,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7441
8405
|
return { ...empty, tier: 3, outcomes, dialect: null };
|
|
7442
8406
|
}
|
|
7443
8407
|
}
|
|
7444
|
-
if (sites.length === 0) {
|
|
7445
|
-
const reason = tier1.error
|
|
8408
|
+
if (sites.length === 0 && tier1.error) {
|
|
8409
|
+
const reason = tier1.error.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
|
|
7446
8410
|
for (const target of pendingTargets) outcomes.set(target.key, reason);
|
|
7447
8411
|
return { ...empty, tier, outcomes, dialect };
|
|
7448
8412
|
}
|
|
@@ -7460,9 +8424,24 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7460
8424
|
tier1.error ? [] : declarationsIn(tier1.roots, content)
|
|
7461
8425
|
);
|
|
7462
8426
|
}
|
|
8427
|
+
function unplacedReason(parsed, literal) {
|
|
8428
|
+
if (!parsed || parsed.error) return "NOT_IN_SOURCE";
|
|
8429
|
+
if (parsed.script && norm(parsed.script.code).includes(literal)) return "IN_EXPRESSION";
|
|
8430
|
+
let found = false;
|
|
8431
|
+
const visit = (node) => {
|
|
8432
|
+
if (found) return;
|
|
8433
|
+
if (node.type === "expr") {
|
|
8434
|
+
if (norm(node.value).includes(literal)) found = true;
|
|
8435
|
+
return;
|
|
8436
|
+
}
|
|
8437
|
+
if (node.type === "element" && !node.raw) for (const child of node.children) visit(child);
|
|
8438
|
+
};
|
|
8439
|
+
for (const root of parsed.roots) visit(root);
|
|
8440
|
+
return found ? "IN_EXPRESSION" : "NOT_IN_SOURCE";
|
|
8441
|
+
}
|
|
7463
8442
|
async function plan(file, content, dialect, targets, sites, importAt, outcomes, tier, parsed, drill, declarations) {
|
|
7464
8443
|
const writing = tier === 2 ? "html" : dialect;
|
|
7465
|
-
const missing = tier !== 2 ?
|
|
8444
|
+
const missing = (literal) => tier !== 2 ? unplacedReason(parsed, literal) : parsed.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
|
|
7466
8445
|
const taken = parsed.error ? /* @__PURE__ */ new Set() : topLevelBindings(dialect, parsed, content);
|
|
7467
8446
|
const slugs = [...new Set(targets.map((t) => t.slug))].sort();
|
|
7468
8447
|
const preset = /* @__PURE__ */ new Map();
|
|
@@ -7471,7 +8450,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7471
8450
|
for (const [key, held] of [
|
|
7472
8451
|
["helper:bcms", locals.bcms],
|
|
7473
8452
|
["helper:bcmsRows", locals.bcmsRows],
|
|
7474
|
-
["helper:bcmsLayout", locals.bcmsLayout]
|
|
8453
|
+
["helper:bcmsLayout", locals.bcmsLayout],
|
|
8454
|
+
["helper:bcmsRowsAs", locals.bcmsRowsAs],
|
|
8455
|
+
["helper:bcmsImage", locals.bcmsImage]
|
|
7475
8456
|
]) {
|
|
7476
8457
|
if (held[0]) preset.set(key, held[0]);
|
|
7477
8458
|
}
|
|
@@ -7484,6 +8465,10 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7484
8465
|
{ key: "helper:bcms", name: "bcms" },
|
|
7485
8466
|
{ key: "helper:bcmsRows", name: "bcmsRows" },
|
|
7486
8467
|
{ key: "helper:bcmsLayout", name: "bcmsLayout" },
|
|
8468
|
+
{ key: "helper:bcmsRowsAs", name: "bcmsRowsAs" },
|
|
8469
|
+
{ key: "helper:bcmsImage", name: "bcmsImage" },
|
|
8470
|
+
{ key: "helper:bcmsList", name: "bcmsList" },
|
|
8471
|
+
{ key: "helper:bcmsTuples", name: "bcmsTuples" },
|
|
7487
8472
|
// Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
|
|
7488
8473
|
...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
|
|
7489
8474
|
].filter((entry) => !preset.has(entry.key));
|
|
@@ -7501,7 +8486,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7501
8486
|
for (const [literal, sharing] of byLiteral) {
|
|
7502
8487
|
const forLiteral = sites.filter((s) => s.literal === literal);
|
|
7503
8488
|
if (forLiteral.length === 0) {
|
|
7504
|
-
for (const target of sharing) outcomes.set(target.key, missing);
|
|
8489
|
+
for (const target of sharing) outcomes.set(target.key, missing(literal));
|
|
7505
8490
|
continue;
|
|
7506
8491
|
}
|
|
7507
8492
|
const assigned = sharing.length === 1 ? /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]) : resolvePositionally(sharing, forLiteral);
|
|
@@ -7515,7 +8500,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7515
8500
|
if (rewritable.length === 0) {
|
|
7516
8501
|
outcomes.set(
|
|
7517
8502
|
target.key,
|
|
7518
|
-
mine.some((s) => s.partial) ? "SUBSTRING_ONLY" : mine.some((s) => s.where === "script" || s.where === "comment") ? "IN_SCRIPT_OR_COMMENT" : missing
|
|
8503
|
+
mine.some((s) => s.partial) ? "SUBSTRING_ONLY" : mine.some((s) => s.where === "script" || s.where === "comment") ? "IN_SCRIPT_OR_COMMENT" : missing(literal)
|
|
7519
8504
|
);
|
|
7520
8505
|
continue;
|
|
7521
8506
|
}
|
|
@@ -7590,7 +8575,11 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7590
8575
|
// `original` has been through `firstBlockText`, so it is the text a reader sees with
|
|
7591
8576
|
// the markup already stripped. Keeping that as the in-code fallback would silently
|
|
7592
8577
|
// flatten every bold and link the moment a build ran against a stub snapshot.
|
|
7593
|
-
|
|
8578
|
+
//
|
|
8579
|
+
// 🔴 AND ONLY WHEN THE ELEMENT'S SUBTREE IS THE VALUE. A value that is one TEXT NODE of
|
|
8580
|
+
// a mixed element gets a wrapper of its own, so taking the element's inner range as its
|
|
8581
|
+
// fallback swallowed the `<em>` beside it — the wrapper then rendered the sibling twice.
|
|
8582
|
+
fallback: target.kind === "richtext" && site.inner && !(site.mixed && site.textRange) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
|
|
7594
8583
|
snapshot: snapshotName(target.slug),
|
|
7595
8584
|
slug: target.slug,
|
|
7596
8585
|
scope: target.scope,
|
|
@@ -7671,10 +8660,65 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7671
8660
|
const collided = new Set(overlapping(kept).map((r) => r.key));
|
|
7672
8661
|
const applied = kept.filter((r) => !collided.has(r.key));
|
|
7673
8662
|
for (const key of collided) outcomes.set(key, "AMBIGUOUS_LITERAL");
|
|
7674
|
-
|
|
7675
|
-
|
|
8663
|
+
const data = await bindDataLiterals(
|
|
8664
|
+
{
|
|
8665
|
+
content,
|
|
8666
|
+
parsed,
|
|
8667
|
+
dialect: writing,
|
|
8668
|
+
rowsAs: helperName("bcmsRowsAs"),
|
|
8669
|
+
list: helperName("bcmsList"),
|
|
8670
|
+
tuples: helperName("bcmsTuples"),
|
|
8671
|
+
read: helperName("bcms"),
|
|
8672
|
+
snapshotName,
|
|
8673
|
+
slug: params?.slug ?? null,
|
|
8674
|
+
// The row's leaf travels to another file exactly as a scalar prop does, and by the same
|
|
8675
|
+
// machinery — the lane decides WHICH props, this decides what may be touched. @see props.ts
|
|
8676
|
+
drill: async (component, prop, kind) => {
|
|
8677
|
+
const drilled = await drillProp(drill, file, component, prop, kind, 1);
|
|
8678
|
+
return drilled.ok ? { ok: true, edits: drilled.edits } : { ok: false, reason: drilled.reason ?? "PROP_TARGET_NOT_FOUND" };
|
|
8679
|
+
}
|
|
8680
|
+
},
|
|
8681
|
+
targets.filter((t) => {
|
|
8682
|
+
const outcome = outcomes.get(t.key);
|
|
8683
|
+
return t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
|
|
8684
|
+
}).map((t) => ({
|
|
8685
|
+
key: t.key,
|
|
8686
|
+
path: t.path,
|
|
8687
|
+
kind: t.kind,
|
|
8688
|
+
literal: t.literal,
|
|
8689
|
+
slug: t.slug
|
|
8690
|
+
}))
|
|
8691
|
+
);
|
|
8692
|
+
for (const key of data.bound) outcomes.set(key, "rewritten");
|
|
8693
|
+
for (const refusal of data.refused) outcomes.set(refusal.key, refusal.reason);
|
|
8694
|
+
for (const declaration2 of data.declarations) {
|
|
8695
|
+
dynamic.push({ file, prop: null, path: declaration2.path, callSites: [{ file, literal: declaration2.literal }] });
|
|
8696
|
+
}
|
|
8697
|
+
componentEdits.push(...data.componentEdits);
|
|
8698
|
+
for (const drilledProp of data.props) {
|
|
8699
|
+
dynamic.push({
|
|
8700
|
+
file: drilledProp.file,
|
|
8701
|
+
prop: drilledProp.prop,
|
|
8702
|
+
path: drilledProp.path,
|
|
8703
|
+
callSites: [{ file, literal: drilledProp.literal }]
|
|
8704
|
+
});
|
|
7676
8705
|
}
|
|
7677
|
-
const
|
|
8706
|
+
const images = bindImages(
|
|
8707
|
+
{ content, parsed, dialect: writing, image: helperName("bcmsImage"), snapshotName, slug: params?.slug ?? null },
|
|
8708
|
+
targets.filter((t) => {
|
|
8709
|
+
const outcome = outcomes.get(t.key);
|
|
8710
|
+
return t.kind === "image" && t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
|
|
8711
|
+
}).map((t) => ({ key: t.key, path: t.path, kind: t.kind, original: t.fallback, slug: t.slug }))
|
|
8712
|
+
);
|
|
8713
|
+
for (const key of images.bound) outcomes.set(key, "rewritten");
|
|
8714
|
+
for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
|
|
8715
|
+
const lanes = [...data.splices, ...images.splices];
|
|
8716
|
+
if (applied.length === 0 && lanes.length === 0) {
|
|
8717
|
+
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [], notes: [] };
|
|
8718
|
+
}
|
|
8719
|
+
const readSlugs = [
|
|
8720
|
+
.../* @__PURE__ */ new Set([...applied.filter((r) => r.scope === "page").map((r) => r.slug), ...data.slugs, ...images.slugs])
|
|
8721
|
+
].sort();
|
|
7678
8722
|
const helper = HELPER_PATH[writing];
|
|
7679
8723
|
const rewritten = rewriteFile(content, writing, applied, {
|
|
7680
8724
|
// html reads nothing at build time, so it imports nothing: `injectFields` writes its values
|
|
@@ -7695,10 +8739,17 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7695
8739
|
{ name: "bcms", alias: helperName("bcms") },
|
|
7696
8740
|
{ name: "bcmsRows", alias: helperName("bcmsRows") }
|
|
7697
8741
|
] : [],
|
|
7698
|
-
...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : []
|
|
8742
|
+
...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : [],
|
|
8743
|
+
...data.usesRows ? [{ name: "bcmsRowsAs", alias: helperName("bcmsRowsAs") }] : [],
|
|
8744
|
+
...data.usesList ? [{ name: "bcmsList", alias: helperName("bcmsList") }] : [],
|
|
8745
|
+
// `bcmsTuples` reads each row through `bcmsList`, but the emitted helper exports both and the
|
|
8746
|
+
// page imports only the one it calls — an unused import is dead code in someone else's repo.
|
|
8747
|
+
...data.usesTuples ? [{ name: "bcmsTuples", alias: helperName("bcmsTuples") }] : [],
|
|
8748
|
+
...data.usesRead ? [{ name: "bcms", alias: helperName("bcms") }] : [],
|
|
8749
|
+
...images.splices.length > 0 ? [{ name: "bcmsImage", alias: helperName("bcmsImage") }] : []
|
|
7699
8750
|
].filter((helper2, at, all) => all.findIndex((h) => h.name === helper2.name) === at).filter((helper2) => !preset.has(`helper:${helper2.name}`)),
|
|
7700
8751
|
at: importAt
|
|
7701
|
-
}, params?.splices ?? []);
|
|
8752
|
+
}, [...params?.splices ?? [], ...lanes]);
|
|
7702
8753
|
return {
|
|
7703
8754
|
tier,
|
|
7704
8755
|
content: rewritten,
|
|
@@ -7708,7 +8759,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7708
8759
|
dialect: writing,
|
|
7709
8760
|
dynamic,
|
|
7710
8761
|
componentEdits,
|
|
7711
|
-
snapshots: rule.text ? readSlugs : []
|
|
8762
|
+
snapshots: rule.text ? readSlugs : [],
|
|
8763
|
+
// The lane reports one sentence per list it bound; the FILE is added here, where it is known.
|
|
8764
|
+
notes: data.notes.map((message) => ({ code: "PRIMITIVE_LIST", file, message }))
|
|
7712
8765
|
};
|
|
7713
8766
|
}
|
|
7714
8767
|
function mergeInto(content, site) {
|
|
@@ -7906,7 +8959,11 @@ export {
|
|
|
7906
8959
|
aliasesFrom,
|
|
7907
8960
|
assertReceipt,
|
|
7908
8961
|
assertSections,
|
|
8962
|
+
assetCandidates,
|
|
8963
|
+
assetImport,
|
|
7909
8964
|
attrsFor,
|
|
8965
|
+
bindDataLiterals,
|
|
8966
|
+
bindImages,
|
|
7910
8967
|
briefDigest,
|
|
7911
8968
|
canvasBridge,
|
|
7912
8969
|
carryFor,
|
|
@@ -7917,6 +8974,7 @@ export {
|
|
|
7917
8974
|
declaringElements,
|
|
7918
8975
|
dialectOf,
|
|
7919
8976
|
exportedFunction,
|
|
8977
|
+
extendedConfigs,
|
|
7920
8978
|
findSites,
|
|
7921
8979
|
findSitesTolerant,
|
|
7922
8980
|
flat,
|