@bettercms-ai/convert 0.5.0 → 0.7.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 +18 -4
- package/README.md +59 -4
- package/dist/cli.js +1383 -784
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +181 -3
- package/dist/index.js +1391 -788
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -147,195 +147,73 @@ function propsAttribute(props) {
|
|
|
147
147
|
return `data-bcms-props="${value}"`;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// src/
|
|
151
|
-
|
|
152
|
-
var
|
|
153
|
-
var
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!path) throw new Error(`The ${dialect} dialect reads its values at publish time, not at build time.`);
|
|
171
|
-
const layout = relativeImport(path, `${CONTENT_DIR}/layout.json`);
|
|
172
|
-
return `${HEADER}${HELPER_VERSION}
|
|
173
|
-
// Values come from bcms-content/*.json, written into the tree before the build. The committed
|
|
174
|
-
// stubs are {} \u2014 a build with them renders every fallback below and still succeeds.
|
|
175
|
-
import layoutSnapshot from "${layout}";
|
|
176
|
-
|
|
177
|
-
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
178
|
-
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
179
|
-
|
|
180
|
-
/** Dotted keys and \`[n]\` indices, positionally \u2014 the editor's own path grammar. */
|
|
181
|
-
function readPath(root: unknown, path: string): unknown {
|
|
182
|
-
let cursor: unknown = root;
|
|
183
|
-
for (const segment of path.split(".")) {
|
|
184
|
-
if (!segment) return undefined;
|
|
185
|
-
const key = segment.replace(/\\[\\d+\\]/g, "");
|
|
186
|
-
if (key) {
|
|
187
|
-
if (!isRecord(cursor)) return undefined;
|
|
188
|
-
cursor = cursor[key];
|
|
150
|
+
// src/declarations.ts
|
|
151
|
+
var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
|
|
152
|
+
var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
|
|
153
|
+
var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
|
|
154
|
+
var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
|
|
155
|
+
var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
|
|
156
|
+
const [address, kind] = entry.split("|");
|
|
157
|
+
if (!address) return [];
|
|
158
|
+
const scope = address.startsWith("layout:") ? "layout" : "page";
|
|
159
|
+
return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
|
|
160
|
+
});
|
|
161
|
+
var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
|
|
162
|
+
var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
|
|
163
|
+
var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
|
|
164
|
+
function declarationsIn(roots, content) {
|
|
165
|
+
const out = [];
|
|
166
|
+
const visit = (node) => {
|
|
167
|
+
if (node.type === "expr") {
|
|
168
|
+
for (const child of node.children) visit(child);
|
|
169
|
+
return;
|
|
189
170
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
171
|
+
if (node.type !== "element") return;
|
|
172
|
+
const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
|
|
173
|
+
const at = node.attrInsertAt;
|
|
174
|
+
for (const attr of node.attrs) {
|
|
175
|
+
const name = attr.name.replace(/^:/, "");
|
|
176
|
+
if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
|
|
177
|
+
const scope = name === "data-bcms-layout-field" ? "layout" : "page";
|
|
178
|
+
const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
|
|
179
|
+
const templated = template?.[1] ?? template?.[2];
|
|
180
|
+
if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
|
|
181
|
+
else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
|
|
182
|
+
} else if (attr.name === "data-bcms-props") {
|
|
183
|
+
out.push(...propsDeclarations(attr.value, at));
|
|
184
|
+
} else if (attr.name === "bcmsBindings") {
|
|
185
|
+
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
186
|
+
out.push({ scope: "page", path, kind: "", at, via: "prop" });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
193
189
|
}
|
|
194
|
-
|
|
195
|
-
return cursor;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** A stored value as a template renders it: richtext is its html, an image is its url. */
|
|
199
|
-
function render(value: unknown): string | undefined {
|
|
200
|
-
if (typeof value === "string") return value;
|
|
201
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
202
|
-
if (isRecord(value)) {
|
|
203
|
-
if (typeof value.html === "string") return value.html;
|
|
204
|
-
if (typeof value.url === "string") return value.url;
|
|
205
|
-
}
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** One field of one page. \`slug\` selects the entry on a dynamic route. */
|
|
210
|
-
export function bcms(page: unknown, path: string, fallback: string, slug?: string): string {
|
|
211
|
-
const entries = isRecord(page) ? page.entries : undefined;
|
|
212
|
-
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
213
|
-
return render(readPath(root, path)) ?? fallback;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** One field of the shared layout \u2014 the header and footer every route renders. */
|
|
217
|
-
export function bcmsLayout(path: string, fallback: string): string {
|
|
218
|
-
return render(readPath(layoutSnapshot, path)) ?? fallback;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* The ROWS of a repeater. \`fallback\` is every row the template shipped with, so a build against
|
|
223
|
-
* an empty snapshot still renders the same number of cards it always did.
|
|
224
|
-
*/
|
|
225
|
-
export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: string): T[] {
|
|
226
|
-
const entries = isRecord(page) ? page.entries : undefined;
|
|
227
|
-
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
228
|
-
const value = readPath(root, path);
|
|
229
|
-
return Array.isArray(value) ? (value as T[]) : fallback;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* ONE SECTION's copy, for a component extracted by \`--componentize\`.
|
|
234
|
-
*
|
|
235
|
-
* OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
|
|
236
|
-
* (Phase B) carries an override for every prop it declares, so the group underneath it is never
|
|
237
|
-
* read; a placement still bound to a page field group (Phase A) carries none, so the group is all
|
|
238
|
-
* there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
|
|
239
|
-
* instance does not hold has to keep rendering the page's value rather than nothing.
|
|
240
|
-
*
|
|
241
|
-
* \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
|
|
242
|
-
* the page's old copy back into a heading somebody had deliberately cleared.
|
|
243
|
-
*
|
|
244
|
-
* With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
|
|
245
|
-
* the fallback the template shipped with \u2014 which is why a section renders correctly against the
|
|
246
|
-
* committed \`{}\` stub.
|
|
247
|
-
*/
|
|
248
|
-
export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
|
|
249
|
-
const group = bind ? readPath(page, bind) : undefined;
|
|
250
|
-
const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
|
|
251
|
-
if (!isRecord(overrides)) return base;
|
|
252
|
-
for (const key of Object.keys(overrides)) {
|
|
253
|
-
const value = overrides[key];
|
|
254
|
-
if (value === undefined || value === null) continue;
|
|
255
|
-
/**
|
|
256
|
-
* \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
|
|
257
|
-
* dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
|
|
258
|
-
* helper fell back to the rows the template shipped with and the editor's work never showed.
|
|
259
|
-
*/
|
|
260
|
-
base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
|
|
261
|
-
}
|
|
262
|
-
return base;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
|
|
267
|
-
*
|
|
268
|
-
* An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
|
|
269
|
-
* copy lives. A placement that owns its copy declares the BLOCK lane \u2014
|
|
270
|
-
* \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
|
|
271
|
-
* own value to. A placement bound to a page field group declares the PAGE path, which is what the
|
|
272
|
-
* page form and the coverage meter already know.
|
|
273
|
-
*
|
|
274
|
-
* \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
|
|
275
|
-
* page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
|
|
276
|
-
* second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
|
|
277
|
-
* Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
|
|
278
|
-
* \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
|
|
279
|
-
* the meter counted a binding that was never there. The prefix the markup was cut with comes out,
|
|
280
|
-
* the placement's own group goes in.
|
|
281
|
-
*
|
|
282
|
-
* \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
|
|
283
|
-
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
284
|
-
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
285
|
-
*/
|
|
286
|
-
export function bcmsFields(
|
|
287
|
-
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
288
|
-
pathPrefix: string,
|
|
289
|
-
): (path: string) => string {
|
|
290
|
-
const id = placement.blockId;
|
|
291
|
-
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
292
|
-
/**
|
|
293
|
-
* What replaces the prefix the markup was cut with, or null for "leave every path alone".
|
|
294
|
-
*
|
|
295
|
-
* Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
|
|
296
|
-
* nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
|
|
297
|
-
* where the plan said the prop key IS the whole page path: there is no group segment to swap,
|
|
298
|
-
* so prepending one would invent an address rather than correct it.
|
|
299
|
-
*/
|
|
300
|
-
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
301
|
-
return (path: string): string => {
|
|
302
|
-
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
303
|
-
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
304
|
-
// (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
|
|
305
|
-
if (lane === null || !path.startsWith(pathPrefix)) return path;
|
|
306
|
-
return lane + path.slice(pathPrefix.length);
|
|
190
|
+
for (const child of node.children) visit(child);
|
|
307
191
|
};
|
|
192
|
+
for (const root of roots) visit(root);
|
|
193
|
+
return out;
|
|
308
194
|
}
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
319
|
-
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
320
|
-
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e"
|
|
321
|
-
},
|
|
322
|
-
"composables/bcms-content.ts": {
|
|
323
|
-
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
324
|
-
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
325
|
-
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227"
|
|
195
|
+
var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
|
|
196
|
+
var scopedPath = (scope, path) => `${scope}::${path}`;
|
|
197
|
+
var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
|
|
198
|
+
var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
|
|
199
|
+
function declaredByScan(content) {
|
|
200
|
+
const out = /* @__PURE__ */ new Set();
|
|
201
|
+
const add = (scope, path) => void out.add(scopedPath(scope, path));
|
|
202
|
+
for (const match of content.matchAll(BINDING_ATTR)) {
|
|
203
|
+
add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
|
|
326
204
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
function isKnownHelper(content, dialect) {
|
|
330
|
-
if (content === helperSource(dialect)) return HELPER_VERSION;
|
|
331
|
-
const path = HELPER_PATH[dialect];
|
|
332
|
-
if (!path) return null;
|
|
333
|
-
const digest = sha256(content);
|
|
334
|
-
for (const [version, known] of Object.entries(HISTORICAL[path] ?? {})) {
|
|
335
|
-
if (known === digest) return Number(version);
|
|
205
|
+
for (const match of content.matchAll(TEMPLATE_SCAN)) {
|
|
206
|
+
add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
|
|
336
207
|
}
|
|
337
|
-
|
|
208
|
+
for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
|
|
209
|
+
for (const match of content.matchAll(PROPS_ATTR))
|
|
210
|
+
for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
|
|
211
|
+
return out;
|
|
338
212
|
}
|
|
213
|
+
var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
|
|
214
|
+
|
|
215
|
+
// src/sites/jsx.ts
|
|
216
|
+
import { parse as parseModule } from "@babel/parser";
|
|
339
217
|
|
|
340
218
|
// src/entities.ts
|
|
341
219
|
var LONGEST_ENTITY = 32;
|
|
@@ -2673,198 +2551,6 @@ function decodeStringEscapes(body) {
|
|
|
2673
2551
|
}
|
|
2674
2552
|
var unwrapJsxStrings = (value) => value.replace(JSX_STRING_CHILD, (_whole, _quote, body) => decodeStringEscapes(body));
|
|
2675
2553
|
|
|
2676
|
-
// src/locate.ts
|
|
2677
|
-
var flat = (value) => value.replace(/\s+/g, " ").trim();
|
|
2678
|
-
var stripTags = (value) => value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "");
|
|
2679
|
-
function locate(brief, sources, options) {
|
|
2680
|
-
const flattened = sources.map((s) => {
|
|
2681
|
-
const readable = unwrapJsxStrings(decodeEntities(s.content));
|
|
2682
|
-
return {
|
|
2683
|
-
path: s.path,
|
|
2684
|
-
content: flat(readable),
|
|
2685
|
-
text: options?.stripTags ? flat(stripTags(readable)) : null
|
|
2686
|
-
};
|
|
2687
|
-
});
|
|
2688
|
-
const located = [];
|
|
2689
|
-
const unlocated = [];
|
|
2690
|
-
for (const page of brief.pages) {
|
|
2691
|
-
for (const p of page.paths) {
|
|
2692
|
-
const original = p.original === null ? "" : flat(p.original);
|
|
2693
|
-
if (!original) {
|
|
2694
|
-
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
|
|
2695
|
-
continue;
|
|
2696
|
-
}
|
|
2697
|
-
const files = flattened.filter((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
|
|
2698
|
-
if (files.length === 0) {
|
|
2699
|
-
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
|
|
2700
|
-
continue;
|
|
2701
|
-
}
|
|
2702
|
-
located.push({ route: page.route, scope: p.scope, path: p.path, kind: p.kind, original, files });
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
return { located, unlocated };
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
// src/loops.ts
|
|
2709
|
-
import MagicString from "magic-string";
|
|
2710
|
-
|
|
2711
|
-
// src/declarations.ts
|
|
2712
|
-
var BINDING_ATTR = /data-bcms-(?:layout-)?field\s*=\s*["']([^"']+)["']/g;
|
|
2713
|
-
var PROPS_ATTR = /data-bcms-props\s*=\s*["']([^"']+)["']/g;
|
|
2714
|
-
var TEMPLATE_SCAN = /:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")/g;
|
|
2715
|
-
var BINDINGS_SCAN = /bcmsBindings\s*=\s*\{\{[^}]*\}\}/g;
|
|
2716
|
-
var propsDeclarations = (value, at) => value.split(";").flatMap((entry) => {
|
|
2717
|
-
const [address, kind] = entry.split("|");
|
|
2718
|
-
if (!address) return [];
|
|
2719
|
-
const scope = address.startsWith("layout:") ? "layout" : "page";
|
|
2720
|
-
return [{ scope, path: stripLayoutPrefix(address), kind: kind ?? "", at, via: "props" }];
|
|
2721
|
-
});
|
|
2722
|
-
var propsPaths = (value) => propsDeclarations(value, 0).map((d) => ({ scope: d.scope, path: d.path }));
|
|
2723
|
-
var TEMPLATE_BINDING = /^:?data-bcms-(?:layout-)?field\s*=\s*(?:\{\s*`([^`]+)`\s*\}|"\s*`([^`]+)`\s*")$/;
|
|
2724
|
-
var shapeOfPath = (path) => path.replace(/\[(?:\d+|\$\{[^}]*\})\]/g, "[*]");
|
|
2725
|
-
function declarationsIn(roots, content) {
|
|
2726
|
-
const out = [];
|
|
2727
|
-
const visit = (node) => {
|
|
2728
|
-
if (node.type === "expr") {
|
|
2729
|
-
for (const child of node.children) visit(child);
|
|
2730
|
-
return;
|
|
2731
|
-
}
|
|
2732
|
-
if (node.type !== "element") return;
|
|
2733
|
-
const kind = node.attrs.find((a) => a.name === "data-bcms-kind")?.value ?? "";
|
|
2734
|
-
const at = node.attrInsertAt;
|
|
2735
|
-
for (const attr of node.attrs) {
|
|
2736
|
-
const name = attr.name.replace(/^:/, "");
|
|
2737
|
-
if (name === "data-bcms-field" || name === "data-bcms-layout-field") {
|
|
2738
|
-
const scope = name === "data-bcms-layout-field" ? "layout" : "page";
|
|
2739
|
-
const template = content.slice(attr.range.start, attr.range.end).match(TEMPLATE_BINDING);
|
|
2740
|
-
const templated = template?.[1] ?? template?.[2];
|
|
2741
|
-
if (templated) out.push({ scope, path: shapeOfPath(stripLayoutPrefix(templated)), kind, at, via: "field" });
|
|
2742
|
-
else if (attr.value) out.push({ scope, path: stripLayoutPrefix(attr.value), kind, at, via: "field" });
|
|
2743
|
-
} else if (attr.name === "data-bcms-props") {
|
|
2744
|
-
out.push(...propsDeclarations(attr.value, at));
|
|
2745
|
-
} else if (attr.name === "bcmsBindings") {
|
|
2746
|
-
for (const path of bindingsPaths(content.slice(attr.range.start, attr.range.end))) {
|
|
2747
|
-
out.push({ scope: "page", path, kind: "", at, via: "prop" });
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
for (const child of node.children) visit(child);
|
|
2752
|
-
};
|
|
2753
|
-
for (const root of roots) visit(root);
|
|
2754
|
-
return out;
|
|
2755
|
-
}
|
|
2756
|
-
var bindingsPaths = (text) => [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
|
|
2757
|
-
var scopedPath = (scope, path) => `${scope}::${path}`;
|
|
2758
|
-
var covers = (declared, target) => declared !== void 0 && (declared.has(scopedPath(target.scope, target.path)) || declared.has(scopedPath(target.scope, shapeOfPath(target.path))));
|
|
2759
|
-
var declaredInTree = (roots, content) => new Set(declarationsIn(roots, content).map((d) => scopedPath(d.scope, d.path)));
|
|
2760
|
-
function declaredByScan(content) {
|
|
2761
|
-
const out = /* @__PURE__ */ new Set();
|
|
2762
|
-
const add = (scope, path) => void out.add(scopedPath(scope, path));
|
|
2763
|
-
for (const match of content.matchAll(BINDING_ATTR)) {
|
|
2764
|
-
add(match[0].includes("layout-field") ? "layout" : "page", stripLayoutPrefix(match[1]));
|
|
2765
|
-
}
|
|
2766
|
-
for (const match of content.matchAll(TEMPLATE_SCAN)) {
|
|
2767
|
-
add(match[0].includes("layout-field") ? "layout" : "page", shapeOfPath(stripLayoutPrefix(match[1] ?? match[2])));
|
|
2768
|
-
}
|
|
2769
|
-
for (const match of content.matchAll(BINDINGS_SCAN)) for (const path of bindingsPaths(match[0])) add("page", path);
|
|
2770
|
-
for (const match of content.matchAll(PROPS_ATTR))
|
|
2771
|
-
for (const entry of propsPaths(match[1])) add(entry.scope, entry.path);
|
|
2772
|
-
return out;
|
|
2773
|
-
}
|
|
2774
|
-
var stripLayoutPrefix = (value) => value.startsWith("layout:") ? value.slice("layout:".length) : value;
|
|
2775
|
-
|
|
2776
|
-
// src/scope.ts
|
|
2777
|
-
function patternNames(node, out) {
|
|
2778
|
-
if (!node || typeof node !== "object") return;
|
|
2779
|
-
const typed = node;
|
|
2780
|
-
switch (typed.type) {
|
|
2781
|
-
case "Identifier":
|
|
2782
|
-
out.add(String(typed.name));
|
|
2783
|
-
return;
|
|
2784
|
-
case "ObjectPattern":
|
|
2785
|
-
for (const property of typed.properties ?? [])
|
|
2786
|
-
patternNames(property.type === "RestElement" ? property.argument : property.value, out);
|
|
2787
|
-
return;
|
|
2788
|
-
case "ArrayPattern":
|
|
2789
|
-
for (const element of typed.elements ?? []) patternNames(element, out);
|
|
2790
|
-
return;
|
|
2791
|
-
case "AssignmentPattern":
|
|
2792
|
-
patternNames(typed.left, out);
|
|
2793
|
-
return;
|
|
2794
|
-
case "RestElement":
|
|
2795
|
-
patternNames(typed.argument, out);
|
|
2796
|
-
return;
|
|
2797
|
-
default:
|
|
2798
|
-
return;
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
function statementNames(node, out) {
|
|
2802
|
-
if (!node || typeof node !== "object") return;
|
|
2803
|
-
const typed = node;
|
|
2804
|
-
switch (typed.type) {
|
|
2805
|
-
case "ExportNamedDeclaration":
|
|
2806
|
-
case "ExportDefaultDeclaration":
|
|
2807
|
-
statementNames(typed.declaration, out);
|
|
2808
|
-
return;
|
|
2809
|
-
case "ImportDeclaration":
|
|
2810
|
-
for (const specifier of typed.specifiers ?? []) patternNames(specifier.local, out);
|
|
2811
|
-
return;
|
|
2812
|
-
case "VariableDeclaration":
|
|
2813
|
-
for (const declarator of typed.declarations ?? []) patternNames(declarator.id, out);
|
|
2814
|
-
return;
|
|
2815
|
-
case "FunctionDeclaration":
|
|
2816
|
-
case "ClassDeclaration":
|
|
2817
|
-
case "TSTypeAliasDeclaration":
|
|
2818
|
-
case "TSInterfaceDeclaration":
|
|
2819
|
-
case "TSEnumDeclaration":
|
|
2820
|
-
case "TSModuleDeclaration":
|
|
2821
|
-
patternNames(typed.id, out);
|
|
2822
|
-
return;
|
|
2823
|
-
default:
|
|
2824
|
-
return;
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
var DECLARED = /\b(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
|
|
2828
|
-
var IMPORTED = /\bimport\s+([^;\n]*?)\s+from\b/g;
|
|
2829
|
-
function scanNames(code, out) {
|
|
2830
|
-
for (const match of code.matchAll(DECLARED)) out.add(match[1]);
|
|
2831
|
-
for (const match of code.matchAll(IMPORTED))
|
|
2832
|
-
for (const name of match[1].match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
2833
|
-
if (name !== "as" && name !== "type") out.add(name);
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
function scriptText(nodes, content, out) {
|
|
2837
|
-
for (const node of nodes) {
|
|
2838
|
-
if (node.type !== "element") continue;
|
|
2839
|
-
if (node.raw && node.tag === "script" && node.inner) out.push(content.slice(node.inner.start, node.inner.end));
|
|
2840
|
-
scriptText(node.children, content, out);
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
function topLevelBindings(dialect, parsed, content) {
|
|
2844
|
-
const out = /* @__PURE__ */ new Set();
|
|
2845
|
-
const program = parsed.ast?.program;
|
|
2846
|
-
if (program) for (const statement of program.body ?? []) statementNames(statement, out);
|
|
2847
|
-
else if (parsed.script) scanNames(parsed.script.code, out);
|
|
2848
|
-
if (dialect === "html") {
|
|
2849
|
-
const scripts = [];
|
|
2850
|
-
scriptText(parsed.roots, content, scripts);
|
|
2851
|
-
for (const code of scripts) scanNames(code, out);
|
|
2852
|
-
}
|
|
2853
|
-
return out;
|
|
2854
|
-
}
|
|
2855
|
-
function allocateNames(taken, wanted) {
|
|
2856
|
-
const used = new Set(taken);
|
|
2857
|
-
const out = /* @__PURE__ */ new Map();
|
|
2858
|
-
for (const { key, name } of wanted) {
|
|
2859
|
-
if (out.has(key)) continue;
|
|
2860
|
-
let candidate = name;
|
|
2861
|
-
for (let n = 1; used.has(candidate); n++) candidate = `${name}_${n}`;
|
|
2862
|
-
used.add(candidate);
|
|
2863
|
-
out.set(key, candidate);
|
|
2864
|
-
}
|
|
2865
|
-
return out;
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
2554
|
// src/sites/shared.ts
|
|
2869
2555
|
var isIntrinsic = (tag) => tag !== "" && !tag.includes(".") && tag[0] === tag[0].toLowerCase();
|
|
2870
2556
|
var IMAGE_SOURCES = {
|
|
@@ -2905,7 +2591,7 @@ function renderedText(node) {
|
|
|
2905
2591
|
var attrsOf = (node) => Object.fromEntries(node.attrs.map((a) => [a.name, a.value]));
|
|
2906
2592
|
function soleTextChild(node) {
|
|
2907
2593
|
const texts = node.children.filter((c) => c.type === "text" && !isBlank(c.value));
|
|
2908
|
-
return texts.length === 1 ? texts[0]
|
|
2594
|
+
return texts.length === 1 ? texts[0] : null;
|
|
2909
2595
|
}
|
|
2910
2596
|
function collectSites(roots, literals) {
|
|
2911
2597
|
const wanted = new Set(literals.map(norm));
|
|
@@ -2965,13 +2651,39 @@ function collectSites(roots, literals) {
|
|
|
2965
2651
|
scope: "page",
|
|
2966
2652
|
existingAttrs: attrs,
|
|
2967
2653
|
mixed: node.children.some((c) => c.type !== "text"),
|
|
2968
|
-
textRange: soleTextChild(node),
|
|
2654
|
+
textRange: soleTextChild(node)?.range ?? null,
|
|
2969
2655
|
literal,
|
|
2970
2656
|
partial: false,
|
|
2971
2657
|
ancestors: walk.ancestors,
|
|
2972
2658
|
siblingIndex: walk.siblingIndex
|
|
2973
2659
|
});
|
|
2974
2660
|
}
|
|
2661
|
+
const own = soleTextChild(node);
|
|
2662
|
+
if (own && node.children.some((c) => c.type !== "text")) {
|
|
2663
|
+
const value = norm(own.value);
|
|
2664
|
+
for (const literal of wanted) {
|
|
2665
|
+
if (matched.has(literal) || value !== literal) continue;
|
|
2666
|
+
matched.add(literal);
|
|
2667
|
+
sites.push({
|
|
2668
|
+
file: "",
|
|
2669
|
+
tag: node.tag,
|
|
2670
|
+
attrInsertAt: node.attrInsertAt,
|
|
2671
|
+
// The TEXT NODE's bytes, not the element's: two values inside one element must not be
|
|
2672
|
+
// read as claiming the same span. @see overlapping
|
|
2673
|
+
range: own.range,
|
|
2674
|
+
inner: node.inner,
|
|
2675
|
+
where: "text",
|
|
2676
|
+
scope: "page",
|
|
2677
|
+
existingAttrs: attrs,
|
|
2678
|
+
mixed: true,
|
|
2679
|
+
textRange: own.range,
|
|
2680
|
+
literal,
|
|
2681
|
+
partial: false,
|
|
2682
|
+
ancestors: walk.ancestors,
|
|
2683
|
+
siblingIndex: walk.siblingIndex
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2975
2687
|
const bindingsAttr = node.attrs.find((a) => a.name === "bcmsBindings")?.range;
|
|
2976
2688
|
for (const attr of node.attrs) {
|
|
2977
2689
|
if (attr.expression || !attr.valueRange || BCMS_ATTRS.includes(attr.name)) continue;
|
|
@@ -3038,7 +2750,7 @@ function partials(node, literals, walk, out) {
|
|
|
3038
2750
|
scope: "page",
|
|
3039
2751
|
existingAttrs: attrsOf(node),
|
|
3040
2752
|
mixed: node.children.some((c) => c.type !== "text"),
|
|
3041
|
-
textRange: soleTextChild(node),
|
|
2753
|
+
textRange: soleTextChild(node)?.range ?? null,
|
|
3042
2754
|
literal,
|
|
3043
2755
|
partial: true,
|
|
3044
2756
|
ancestors: walk.ancestors,
|
|
@@ -3058,264 +2770,32 @@ function wordBounded(haystack, needle) {
|
|
|
3058
2770
|
return false;
|
|
3059
2771
|
}
|
|
3060
2772
|
|
|
3061
|
-
// src/
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
attrInsertAt: child.attrInsertAt,
|
|
3088
|
-
// `:key` in vue, `key` in jsx — one existing key, whichever way it is spelled.
|
|
3089
|
-
keyAttr: child.attrs.find((a) => a.name.replace(/^:/, "") === "key")?.range ?? null,
|
|
3090
|
-
node: child,
|
|
3091
|
-
members: inside
|
|
3092
|
-
});
|
|
3093
|
-
}
|
|
3094
|
-
if (rows.length === 0) return null;
|
|
3095
|
-
const covered = new Set(rows.flatMap((r) => r.members.map((m) => m.index)));
|
|
3096
|
-
if (covered.size !== wanted.size || rows.length !== wanted.size) return null;
|
|
3097
|
-
if (rows.some((r) => r.members.length !== members.length / wanted.size)) return null;
|
|
3098
|
-
return rows.sort((a, b) => a.range.start - b.range.start);
|
|
3099
|
-
};
|
|
3100
|
-
for (const root of roots) {
|
|
3101
|
-
const found = search(root);
|
|
3102
|
-
if (found) return found;
|
|
3103
|
-
}
|
|
3104
|
-
return null;
|
|
3105
|
-
}
|
|
3106
|
-
var consecutive = (rows, content) => rows.every((row, at) => at === 0 || content.slice(rows[at - 1].range.end, row.range.start).trim() === "");
|
|
3107
|
-
function maskedRow(row, content) {
|
|
3108
|
-
const masked = new Map(row.members.map((member) => [member.site.inner.start, member.leaf]));
|
|
3109
|
-
return canonical(row.node, content, masked);
|
|
3110
|
-
}
|
|
3111
|
-
var TRIVIA = "";
|
|
3112
|
-
function canonical(node, content, masked) {
|
|
3113
|
-
if (node.type === "text") {
|
|
3114
|
-
return node.value.trim() === "" ? TRIVIA : content.slice(node.range.start, node.range.end);
|
|
3115
|
-
}
|
|
3116
|
-
if (node.type === "comment" || node.type === "expr") {
|
|
3117
|
-
return content.slice(node.range.start, node.range.end);
|
|
3118
|
-
}
|
|
3119
|
-
if (node.type !== "element") return "";
|
|
3120
|
-
const open = content.slice(node.start, node.inner ? node.inner.start : node.attrInsertAt);
|
|
3121
|
-
if (node.inner && masked.has(node.inner.start)) {
|
|
3122
|
-
return `${open}\0${masked.get(node.inner.start)}\0</${node.tag}>`;
|
|
3123
|
-
}
|
|
3124
|
-
const inside = node.children.map((child) => canonical(child, content, masked)).join("");
|
|
3125
|
-
return node.inner ? `${open}${inside}</${node.tag}>` : open;
|
|
3126
|
-
}
|
|
3127
|
-
function sameShape(rows, content) {
|
|
3128
|
-
const first = maskedRow(rows[0], content);
|
|
3129
|
-
return rows.every((row) => maskedRow(row, content) === first);
|
|
3130
|
-
}
|
|
3131
|
-
var indented = (body, indent) => body.split("\n").map((line) => line.startsWith(indent) ? `${indent} ${line.slice(indent.length)}` : `${indent} ${line}`).join("\n");
|
|
3132
|
-
function indentOf(content, at) {
|
|
3133
|
-
const lineStart = content.lastIndexOf("\n", at - 1) + 1;
|
|
3134
|
-
const prefix = content.slice(lineStart, at);
|
|
3135
|
-
return prefix.trim() === "" ? prefix : "";
|
|
3136
|
-
}
|
|
3137
|
-
function writeLeaf(source, shift, member, rule, read, row, literal) {
|
|
3138
|
-
const site = member.site;
|
|
3139
|
-
const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
|
|
3140
|
-
if (member.kind === "richtext" && rule.richtext) {
|
|
3141
|
-
source.remove(site.inner.start + shift, site.inner.end + shift);
|
|
3142
|
-
source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
|
|
3143
|
-
} else {
|
|
3144
|
-
source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
|
|
3145
|
-
}
|
|
3146
|
-
const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
|
|
3147
|
-
source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
|
|
3148
|
-
}
|
|
3149
|
-
function emitNested(rule, array, members, prefix, outer, parent, ctx) {
|
|
3150
|
-
if (!rule.loop || !rule.text) return null;
|
|
3151
|
-
const inner = [];
|
|
3152
|
-
for (const member of members) {
|
|
3153
|
-
const split = splitNested(member.leaf);
|
|
3154
|
-
if (!split || splitNested(split.leaf) !== null) return null;
|
|
3155
|
-
inner.push({ ...member, index: split.index, leaf: split.leaf });
|
|
3156
|
-
}
|
|
3157
|
-
const rows = findRows([parent.node], inner);
|
|
3158
|
-
if (!rows || rows.length === 0) return null;
|
|
3159
|
-
if (!consecutive(rows, ctx.content)) return null;
|
|
3160
|
-
if (!sameShape(rows, ctx.content)) return null;
|
|
3161
|
-
const template = rows[0];
|
|
3162
|
-
const used = /* @__PURE__ */ new Set([
|
|
3163
|
-
...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
|
|
3164
|
-
outer.row,
|
|
3165
|
-
outer.index
|
|
3166
|
-
]);
|
|
3167
|
-
const allocated = allocateNames(used, [
|
|
3168
|
-
{ key: "row", name: "item" },
|
|
3169
|
-
{ key: "index", name: "j" }
|
|
3170
|
-
]);
|
|
3171
|
-
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
3172
|
-
const fallback = [];
|
|
3173
|
-
const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
|
|
3174
|
-
if (!writable) return null;
|
|
3175
|
-
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
3176
|
-
const shift = -template.range.start;
|
|
3177
|
-
const declarations = [];
|
|
3178
|
-
for (const member of template.members) {
|
|
3179
|
-
const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
|
|
3180
|
-
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
3181
|
-
declarations.push({ path: shapeOfPath(literal), literal });
|
|
3182
|
-
}
|
|
3183
|
-
const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
|
|
3184
|
-
if (rule.loop.kind === "attr") {
|
|
3185
|
-
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
3186
|
-
}
|
|
3187
|
-
if (rule.loopKey) {
|
|
3188
|
-
const key = rule.loopKey(names.index);
|
|
3189
|
-
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
3190
|
-
else if (template.keyAttr.end > template.keyAttr.start)
|
|
3191
|
-
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
3192
|
-
else return null;
|
|
3193
|
-
}
|
|
3194
|
-
const indent = indentOf(ctx.content, template.range.start);
|
|
3195
|
-
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
3196
|
-
${indented(source.toString(), indent)}
|
|
3197
|
-
${indent}${rule.loop.close}` : source.toString();
|
|
3198
|
-
return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
|
|
3199
|
-
}
|
|
3200
|
-
function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
|
|
3201
|
-
const rule = DIALECT_RULES[dialect];
|
|
3202
|
-
if (!rule.loop || !rule.text || group.length === 0) return null;
|
|
3203
|
-
const rows = findRows(ctx.roots, group);
|
|
3204
|
-
if (!rows || rows.length < 2) return null;
|
|
3205
|
-
if (!consecutive(rows, ctx.content)) return null;
|
|
3206
|
-
if (!sameShape(rows, ctx.content)) return null;
|
|
3207
|
-
if (group.some((m) => m.site.where !== "text" || !isIntrinsic(m.site.tag) || !m.site.inner)) return null;
|
|
3208
|
-
const template = rows[0];
|
|
3209
|
-
const used = new Set(ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
3210
|
-
const allocated = allocateNames(used, [
|
|
3211
|
-
{ key: "row", name: "card" },
|
|
3212
|
-
{ key: "index", name: "i" }
|
|
3213
|
-
]);
|
|
3214
|
-
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
3215
|
-
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
3216
|
-
const shift = -template.range.start;
|
|
3217
|
-
const declarations = [];
|
|
3218
|
-
const nested = /* @__PURE__ */ new Map();
|
|
3219
|
-
const direct = [];
|
|
3220
|
-
for (const member of template.members) {
|
|
3221
|
-
const inner = splitNested(member.leaf);
|
|
3222
|
-
if (inner === null) direct.push(member);
|
|
3223
|
-
else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
|
|
3224
|
-
}
|
|
3225
|
-
for (const [array, members] of nested) {
|
|
3226
|
-
const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
|
|
3227
|
-
if (!plan2) return null;
|
|
3228
|
-
if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
|
|
3229
|
-
source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
|
|
3230
|
-
declarations.push(...plan2.declarations);
|
|
3231
|
-
}
|
|
3232
|
-
for (const member of direct) {
|
|
3233
|
-
const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
|
|
3234
|
-
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
3235
|
-
declarations.push({ path: shapeOfPath(literal), literal });
|
|
3236
|
-
}
|
|
3237
|
-
const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
|
|
3238
|
-
const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
|
|
3239
|
-
if (rule.loop.kind === "attr") {
|
|
3240
|
-
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
3241
|
-
}
|
|
3242
|
-
if (rule.loopKey) {
|
|
3243
|
-
const key = rule.loopKey(names.index);
|
|
3244
|
-
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
3245
|
-
else if (template.keyAttr.end > template.keyAttr.start)
|
|
3246
|
-
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
3247
|
-
else return null;
|
|
3248
|
-
}
|
|
3249
|
-
const indent = indentOf(ctx.content, template.range.start);
|
|
3250
|
-
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
3251
|
-
${indented(source.toString(), indent)}
|
|
3252
|
-
${indent}${rule.loop.close}` : source.toString();
|
|
3253
|
-
return {
|
|
3254
|
-
range: { start: template.range.start, end: rows[rows.length - 1].range.end },
|
|
3255
|
-
text,
|
|
3256
|
-
declarations,
|
|
3257
|
-
bindings: [...new Set(declarations.map((d) => d.path))],
|
|
3258
|
-
names
|
|
3259
|
-
};
|
|
3260
|
-
}
|
|
3261
|
-
function setLeaf(row, leaf, value) {
|
|
3262
|
-
const segments = leaf.split(".").flatMap((segment) => {
|
|
3263
|
-
const name = segment.replace(/\[\d+\]/g, "");
|
|
3264
|
-
const indices = [...segment.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]));
|
|
3265
|
-
return [...name ? [name] : [], ...indices];
|
|
3266
|
-
});
|
|
3267
|
-
if (segments.some((segment) => typeof segment === "string" && FORBIDDEN.has(segment))) return false;
|
|
3268
|
-
let cursor = row;
|
|
3269
|
-
for (const [at, segment] of segments.entries()) {
|
|
3270
|
-
const key = segment;
|
|
3271
|
-
const last = at === segments.length - 1;
|
|
3272
|
-
if (last) {
|
|
3273
|
-
cursor[key] = value;
|
|
3274
|
-
return true;
|
|
3275
|
-
}
|
|
3276
|
-
const held = Object.prototype.hasOwnProperty.call(cursor, key) ? cursor[key] : void 0;
|
|
3277
|
-
if (held === void 0 || typeof held !== "object" || held === null) {
|
|
3278
|
-
cursor[key] = typeof segments[at + 1] === "number" ? [] : emptyRow();
|
|
3279
|
-
}
|
|
3280
|
-
cursor = cursor[key];
|
|
3281
|
-
}
|
|
3282
|
-
return true;
|
|
3283
|
-
}
|
|
3284
|
-
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3285
|
-
var emptyRow = () => /* @__PURE__ */ Object.create(null);
|
|
3286
|
-
function splitIndexed(arrayPath, path) {
|
|
3287
|
-
if (!path.startsWith(`${arrayPath}[`)) return null;
|
|
3288
|
-
const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
|
|
3289
|
-
return match ? { index: Number(match[1]), leaf: match[2] } : null;
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
// src/sites/jsx.ts
|
|
3293
|
-
import { parse as parseModule } from "@babel/parser";
|
|
3294
|
-
var nameOf = (node) => {
|
|
3295
|
-
if (!node) return "";
|
|
3296
|
-
if (node.type === "JSXIdentifier") return String(node.name);
|
|
3297
|
-
if (node.type === "JSXMemberExpression")
|
|
3298
|
-
return `${nameOf(node.object)}.${nameOf(node.property)}`;
|
|
3299
|
-
if (node.type === "JSXNamespacedName")
|
|
3300
|
-
return `${nameOf(node.namespace)}:${nameOf(node.name)}`;
|
|
3301
|
-
return "";
|
|
3302
|
-
};
|
|
3303
|
-
var SPREAD = "{...}";
|
|
3304
|
-
function attrsOf2(open, content) {
|
|
3305
|
-
return (open.attributes ?? []).flatMap((attr) => {
|
|
3306
|
-
if (attr.type === "JSXSpreadAttribute") {
|
|
3307
|
-
const argument = attr.argument;
|
|
3308
|
-
return [
|
|
3309
|
-
{
|
|
3310
|
-
name: SPREAD,
|
|
3311
|
-
value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
|
|
3312
|
-
valueRange: null,
|
|
3313
|
-
expression: true,
|
|
3314
|
-
range: { start: attr.start ?? 0, end: attr.end ?? 0 },
|
|
3315
|
-
// `{...props}` — the braces ARE the attribute, so the expression is its whole span.
|
|
3316
|
-
expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
|
|
3317
|
-
}
|
|
3318
|
-
];
|
|
2773
|
+
// src/sites/jsx.ts
|
|
2774
|
+
var nameOf = (node) => {
|
|
2775
|
+
if (!node) return "";
|
|
2776
|
+
if (node.type === "JSXIdentifier") return String(node.name);
|
|
2777
|
+
if (node.type === "JSXMemberExpression")
|
|
2778
|
+
return `${nameOf(node.object)}.${nameOf(node.property)}`;
|
|
2779
|
+
if (node.type === "JSXNamespacedName")
|
|
2780
|
+
return `${nameOf(node.namespace)}:${nameOf(node.name)}`;
|
|
2781
|
+
return "";
|
|
2782
|
+
};
|
|
2783
|
+
var SPREAD = "{...}";
|
|
2784
|
+
function attrsOf2(open, content) {
|
|
2785
|
+
return (open.attributes ?? []).flatMap((attr) => {
|
|
2786
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
2787
|
+
const argument = attr.argument;
|
|
2788
|
+
return [
|
|
2789
|
+
{
|
|
2790
|
+
name: SPREAD,
|
|
2791
|
+
value: content.slice(argument?.start ?? 0, argument?.end ?? 0),
|
|
2792
|
+
valueRange: null,
|
|
2793
|
+
expression: true,
|
|
2794
|
+
range: { start: attr.start ?? 0, end: attr.end ?? 0 },
|
|
2795
|
+
// `{...props}` — the braces ARE the attribute, so the expression is its whole span.
|
|
2796
|
+
expressionRange: { start: attr.start ?? 0, end: attr.end ?? 0 }
|
|
2797
|
+
}
|
|
2798
|
+
];
|
|
3319
2799
|
}
|
|
3320
2800
|
if (attr.type !== "JSXAttribute") return [];
|
|
3321
2801
|
const value = attr.value;
|
|
@@ -3758,130 +3238,1090 @@ function tryParse(code) {
|
|
|
3758
3238
|
} catch {
|
|
3759
3239
|
return void 0;
|
|
3760
3240
|
}
|
|
3761
|
-
}
|
|
3762
|
-
var bindingsName = (viaProps) => viaProps ? "props.bcmsBindings" : "bcmsBindings";
|
|
3763
|
-
var bindingsRead = (prop, viaProps) => `${bindingsName(viaProps)}?.${prop}`;
|
|
3764
|
-
|
|
3765
|
-
// src/
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3241
|
+
}
|
|
3242
|
+
var bindingsName = (viaProps) => viaProps ? "props.bcmsBindings" : "bcmsBindings";
|
|
3243
|
+
var bindingsRead = (prop, viaProps) => `${bindingsName(viaProps)}?.${prop}`;
|
|
3244
|
+
|
|
3245
|
+
// src/data.ts
|
|
3246
|
+
function splitGroup(path) {
|
|
3247
|
+
const match = path.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
|
|
3248
|
+
return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
|
|
3249
|
+
}
|
|
3250
|
+
function moduleOf(parsed) {
|
|
3251
|
+
if (!parsed.ast) return null;
|
|
3252
|
+
return { ast: parsed.ast, 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 propertyFor(row, literal) {
|
|
3266
|
+
if (row?.type !== "ObjectExpression") return null;
|
|
3267
|
+
for (const property of row.properties ?? []) {
|
|
3268
|
+
if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
|
|
3269
|
+
const key = property.key;
|
|
3270
|
+
const value = property.value;
|
|
3271
|
+
if (value?.type !== "StringLiteral" || key?.type !== "Identifier") continue;
|
|
3272
|
+
if (norm(String(value.value)) === literal) return String(key.name);
|
|
3273
|
+
}
|
|
3274
|
+
return null;
|
|
3275
|
+
}
|
|
3276
|
+
function iterations(roots, identifier2) {
|
|
3277
|
+
const pattern = new RegExp(`(?<![\\w$])${identifier2}(?![\\w$])\\s*\\.\\s*map\\s*\\(`);
|
|
3278
|
+
const out = [];
|
|
3279
|
+
const visit = (node) => {
|
|
3280
|
+
if (node.type === "expr") {
|
|
3281
|
+
if (pattern.test(node.value)) out.push(node);
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
if (node.type === "element" && !node.raw) for (const child of node.children) visit(child);
|
|
3285
|
+
};
|
|
3286
|
+
for (const root of roots) visit(root);
|
|
3287
|
+
return out;
|
|
3288
|
+
}
|
|
3289
|
+
function callbackParams(expression, identifier2) {
|
|
3290
|
+
const pattern = new RegExp(
|
|
3291
|
+
`(?<![\\w$])${identifier2}(?![\\w$])\\s*\\.\\s*map\\s*\\(\\s*(?:\\(\\s*([A-Za-z_$][\\w$]*)\\s*(,\\s*([A-Za-z_$][\\w$]*)\\s*)?\\)|([A-Za-z_$][\\w$]*))\\s*=>`
|
|
3292
|
+
);
|
|
3293
|
+
const match = expression.value.match(pattern);
|
|
3294
|
+
if (!match || match.index === void 0) return null;
|
|
3295
|
+
const row = match[1] ?? match[4];
|
|
3296
|
+
if (match[4] !== void 0) return { row, index: null, addIndexAt: -1 };
|
|
3297
|
+
const within = match[0].lastIndexOf(row);
|
|
3298
|
+
return {
|
|
3299
|
+
row,
|
|
3300
|
+
index: match[3] ?? null,
|
|
3301
|
+
addIndexAt: expression.range.start + match.index + within + row.length
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
function renders(content, node, row) {
|
|
3305
|
+
if (!node.inner) return null;
|
|
3306
|
+
const inner = content.slice(node.inner.start, node.inner.end).trim();
|
|
3307
|
+
const match = inner.match(/^\{\s*([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\}$/);
|
|
3308
|
+
return match && match[1] === row ? match[2] : null;
|
|
3309
|
+
}
|
|
3310
|
+
function leafElements(content, expression, row) {
|
|
3311
|
+
const out = /* @__PURE__ */ new Map();
|
|
3312
|
+
const visit = (node) => {
|
|
3313
|
+
if (node.type === "expr") {
|
|
3314
|
+
for (const child of node.children) visit(child);
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
if (node.type !== "element" || node.raw) return;
|
|
3318
|
+
const property = isIntrinsic(node.tag) ? renders(content, node, row) : null;
|
|
3319
|
+
if (property) out.set(property, [...out.get(property) ?? [], node]);
|
|
3320
|
+
for (const child of node.children) visit(child);
|
|
3321
|
+
};
|
|
3322
|
+
for (const child of expression.children) visit(child);
|
|
3323
|
+
return out;
|
|
3324
|
+
}
|
|
3325
|
+
function stringLiterals(ast) {
|
|
3326
|
+
const out = /* @__PURE__ */ new Map();
|
|
3327
|
+
walkAst(ast, (node) => {
|
|
3328
|
+
if (node.type !== "VariableDeclarator") return;
|
|
3329
|
+
const id = node.id;
|
|
3330
|
+
const init = node.init;
|
|
3331
|
+
if (id?.type !== "Identifier" || init?.type !== "StringLiteral") return;
|
|
3332
|
+
out.set(String(id.name), init);
|
|
3333
|
+
});
|
|
3334
|
+
return out;
|
|
3335
|
+
}
|
|
3336
|
+
function scalarUses(content, roots, id) {
|
|
3337
|
+
const mention = new RegExp(`(?<![\\w$])${id}(?![\\w$])`, "g");
|
|
3338
|
+
const alone = new RegExp(`^\\{\\s*${id}\\s*\\}$`);
|
|
3339
|
+
const count = (text) => (text.match(mention) ?? []).length;
|
|
3340
|
+
let total = 0;
|
|
3341
|
+
const hosts = [];
|
|
3342
|
+
const visit = (node) => {
|
|
3343
|
+
if (node.type === "expr") {
|
|
3344
|
+
total += count(node.value);
|
|
3345
|
+
for (const child of node.children) visit(child);
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
if (node.type !== "element" || node.raw) return;
|
|
3349
|
+
for (const attr of node.attrs) {
|
|
3350
|
+
if (attr.expressionRange) total += count(content.slice(attr.expressionRange.start, attr.expressionRange.end));
|
|
3351
|
+
}
|
|
3352
|
+
if (node.inner && isIntrinsic(node.tag) && alone.test(content.slice(node.inner.start, node.inner.end).trim())) {
|
|
3353
|
+
hosts.push(node);
|
|
3354
|
+
}
|
|
3355
|
+
for (const child of node.children) visit(child);
|
|
3356
|
+
};
|
|
3357
|
+
for (const root of roots) visit(root);
|
|
3358
|
+
return { total, hosts };
|
|
3359
|
+
}
|
|
3360
|
+
function bindScalar(ctx, member, literals, offset) {
|
|
3361
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3362
|
+
const named = [...literals].filter(([, init2]) => norm(String(init2.value)) === member.literal);
|
|
3363
|
+
if (named.length !== 1) return { splices: [], refusal: null };
|
|
3364
|
+
const [id, init] = named[0];
|
|
3365
|
+
const uses = scalarUses(ctx.content, ctx.parsed.roots, id);
|
|
3366
|
+
if (uses.hosts.length !== 1 || uses.total !== 1) return { splices: [], refusal: "IN_EXPRESSION" };
|
|
3367
|
+
const host = uses.hosts[0];
|
|
3368
|
+
const at = { start: offset + (init.start ?? 0), end: offset + (init.end ?? 0) };
|
|
3369
|
+
const args = [
|
|
3370
|
+
ctx.snapshotName(member.slug),
|
|
3371
|
+
JSON.stringify(member.path),
|
|
3372
|
+
// The literal EXACTLY as the author wrote it — quotes, escapes and all — so the fallback the
|
|
3373
|
+
// build renders is the byte-for-byte copy the repository renders today.
|
|
3374
|
+
ctx.content.slice(at.start, at.end),
|
|
3375
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3376
|
+
];
|
|
3377
|
+
const attributes = [
|
|
3378
|
+
`data-bcms-field="${member.path}"`,
|
|
3379
|
+
...member.kind ? [`data-bcms-kind="${member.kind}"`] : [],
|
|
3380
|
+
...member.kind === "richtext" && rule.richtext ? [rule.richtext(id)] : []
|
|
3381
|
+
];
|
|
3382
|
+
const splices = [
|
|
3383
|
+
{ start: at.start, end: at.end, text: `${ctx.read}(${args.join(", ")})` },
|
|
3384
|
+
{ start: host.attrInsertAt, end: host.attrInsertAt, text: ` ${attributes.join(" ")}` }
|
|
3385
|
+
];
|
|
3386
|
+
if (member.kind === "richtext" && rule.richtext && host.inner) {
|
|
3387
|
+
splices.push({ start: host.inner.start, end: host.inner.end, text: "" });
|
|
3388
|
+
}
|
|
3389
|
+
return { splices, refusal: null };
|
|
3390
|
+
}
|
|
3391
|
+
function bindDataLiterals(ctx, members) {
|
|
3392
|
+
const out = { splices: [], usesRead: false, usesRows: false, slugs: [], bound: [], declarations: [], refused: [] };
|
|
3393
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3394
|
+
const module = moduleOf(ctx.parsed);
|
|
3395
|
+
if (!module || !rule.text) return out;
|
|
3396
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3397
|
+
const scalars = [];
|
|
3398
|
+
for (const member of members) {
|
|
3399
|
+
const split = splitGroup(member.path);
|
|
3400
|
+
if (!split) {
|
|
3401
|
+
scalars.push(member);
|
|
3402
|
+
continue;
|
|
3403
|
+
}
|
|
3404
|
+
const held = { ...member, array: split.array, index: split.index, leaf: split.leaf };
|
|
3405
|
+
const key = `${member.slug}\0${split.array}`;
|
|
3406
|
+
groups.set(key, [...groups.get(key) ?? [], held]);
|
|
3407
|
+
}
|
|
3408
|
+
const strings = stringLiterals(module.ast);
|
|
3409
|
+
for (const member of scalars) {
|
|
3410
|
+
const scalar = bindScalar(ctx, member, strings, module.offset);
|
|
3411
|
+
if (scalar.refusal) out.refused.push({ key: member.key, reason: scalar.refusal });
|
|
3412
|
+
if (scalar.splices.length === 0) continue;
|
|
3413
|
+
out.splices.push(...scalar.splices);
|
|
3414
|
+
out.usesRead = true;
|
|
3415
|
+
out.slugs.push(member.slug);
|
|
3416
|
+
out.bound.push(member.key);
|
|
3417
|
+
}
|
|
3418
|
+
if (groups.size === 0) return out;
|
|
3419
|
+
const literals = arrayLiterals(module.ast);
|
|
3420
|
+
for (const all of groups.values()) {
|
|
3421
|
+
const array = all[0].array;
|
|
3422
|
+
const nested = all.filter((member) => /\[\d+\]/.test(member.leaf));
|
|
3423
|
+
for (const member of nested) out.refused.push({ key: member.key, reason: "PROP_DRILLED_DEEP" });
|
|
3424
|
+
const group = all.filter((member) => !nested.includes(member));
|
|
3425
|
+
if (group.length === 0) continue;
|
|
3426
|
+
const refuse = (reason) => {
|
|
3427
|
+
for (const member of group) out.refused.push({ key: member.key, reason });
|
|
3428
|
+
};
|
|
3429
|
+
let identifier2 = null;
|
|
3430
|
+
let keys = null;
|
|
3431
|
+
for (const [name, literal] of literals) {
|
|
3432
|
+
const elements2 = literal.elements ?? [];
|
|
3433
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
3434
|
+
const ok = group.every((member) => {
|
|
3435
|
+
const property = propertyFor(elements2[member.index] ?? void 0, member.literal);
|
|
3436
|
+
if (!property) return false;
|
|
3437
|
+
const held = mapped.get(property);
|
|
3438
|
+
if (held !== void 0 && held !== member.leaf) return false;
|
|
3439
|
+
mapped.set(property, member.leaf);
|
|
3440
|
+
return true;
|
|
3441
|
+
});
|
|
3442
|
+
if (!ok) continue;
|
|
3443
|
+
if (identifier2 !== null) {
|
|
3444
|
+
identifier2 = null;
|
|
3445
|
+
break;
|
|
3446
|
+
}
|
|
3447
|
+
identifier2 = name;
|
|
3448
|
+
keys = mapped;
|
|
3449
|
+
}
|
|
3450
|
+
if (identifier2 === null || keys === null) continue;
|
|
3451
|
+
const loops = iterations(ctx.parsed.roots, identifier2);
|
|
3452
|
+
if (loops.length === 0) continue;
|
|
3453
|
+
if (loops.length > 1) {
|
|
3454
|
+
refuse("REPEATER_AMBIGUOUS");
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
const loop = loops[0];
|
|
3458
|
+
const params = callbackParams(loop, identifier2);
|
|
3459
|
+
if (!params) {
|
|
3460
|
+
refuse("IN_EXPRESSION");
|
|
3461
|
+
continue;
|
|
3462
|
+
}
|
|
3463
|
+
const elements = leafElements(ctx.content, loop, params.row);
|
|
3464
|
+
const placed = /* @__PURE__ */ new Map();
|
|
3465
|
+
let refusal = null;
|
|
3466
|
+
for (const property of keys.keys()) {
|
|
3467
|
+
const found = elements.get(property) ?? [];
|
|
3468
|
+
if (found.length === 1) placed.set(property, found[0]);
|
|
3469
|
+
else refusal = found.length > 1 ? "REPEATER_AMBIGUOUS" : leafRefusal(loop.value, params.row, property);
|
|
3470
|
+
}
|
|
3471
|
+
if (refusal !== null) {
|
|
3472
|
+
refuse(refusal);
|
|
3473
|
+
continue;
|
|
3474
|
+
}
|
|
3475
|
+
const index = params.index ?? allocateIndex(loop.value);
|
|
3476
|
+
const splices = [];
|
|
3477
|
+
if (rule.loopKey) {
|
|
3478
|
+
const rows = loop.children.filter((child) => child.type === "element");
|
|
3479
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
3480
|
+
if (row && row.tag !== "" && !row.attrs.some((a) => a.name.replace(/^:/, "") === "key")) {
|
|
3481
|
+
splices.push({ start: row.attrInsertAt, end: row.attrInsertAt, text: ` ${rule.loopKey(index)}` });
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
if (params.index === null) {
|
|
3485
|
+
if (params.addIndexAt < 0) {
|
|
3486
|
+
refuse("IN_EXPRESSION");
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
splices.push({ start: params.addIndexAt, end: params.addIndexAt, text: `, ${index}` });
|
|
3490
|
+
}
|
|
3491
|
+
for (const [property, element] of placed) {
|
|
3492
|
+
const leaf = keys.get(property);
|
|
3493
|
+
const literal = `${array}[\${${index}}].${leaf}`;
|
|
3494
|
+
const kind = group.find((member) => member.leaf === leaf).kind;
|
|
3495
|
+
const attributes = [
|
|
3496
|
+
rule.attr("data-bcms-field", `\`${literal}\``),
|
|
3497
|
+
...kind ? [`data-bcms-kind="${kind}"`] : [],
|
|
3498
|
+
// A richtext row value is html, so it goes into the dialect's html sink rather than being
|
|
3499
|
+
// printed as characters — the same rule `writeLeaf` follows one level down. @see loops.ts
|
|
3500
|
+
...kind === "richtext" && rule.richtext ? [rule.richtext(`${params.row}.${property}`)] : []
|
|
3501
|
+
];
|
|
3502
|
+
splices.push({ start: element.attrInsertAt, end: element.attrInsertAt, text: ` ${attributes.join(" ")}` });
|
|
3503
|
+
if (kind === "richtext" && rule.richtext && element.inner) {
|
|
3504
|
+
splices.push({ start: element.inner.start, end: element.inner.end, text: "" });
|
|
3505
|
+
}
|
|
3506
|
+
out.declarations.push({ path: shapeOfPath(literal), literal });
|
|
3507
|
+
}
|
|
3508
|
+
const at = { start: module.offset + (literals.get(identifier2).start ?? 0), end: module.offset + (literals.get(identifier2).end ?? 0) };
|
|
3509
|
+
const map = [...keys].map(([property, leaf]) => `${property}: ${JSON.stringify(leaf)}`).join(", ");
|
|
3510
|
+
const args = [
|
|
3511
|
+
ctx.snapshotName(group[0].slug),
|
|
3512
|
+
JSON.stringify(array),
|
|
3513
|
+
ctx.content.slice(at.start, at.end),
|
|
3514
|
+
`{ ${map} }`,
|
|
3515
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3516
|
+
];
|
|
3517
|
+
splices.push({ start: at.start, end: at.end, text: `${ctx.rowsAs}(${args.join(", ")})` });
|
|
3518
|
+
out.splices.push(...splices);
|
|
3519
|
+
out.usesRows = true;
|
|
3520
|
+
out.slugs.push(...group.map((member) => member.slug));
|
|
3521
|
+
out.bound.push(...group.map((member) => member.key));
|
|
3522
|
+
}
|
|
3523
|
+
return out;
|
|
3524
|
+
}
|
|
3525
|
+
function leafRefusal(loop, row, property) {
|
|
3526
|
+
const prop = new RegExp(`=\\s*\\{\\s*${row}\\s*\\.\\s*${property}\\s*\\}|\\{\\s*\\.\\.\\.\\s*${row}\\s*\\}`);
|
|
3527
|
+
return prop.test(loop) ? "PROP_TARGET_NOT_FOUND" : "IN_EXPRESSION";
|
|
3528
|
+
}
|
|
3529
|
+
function allocateIndex(loop) {
|
|
3530
|
+
const used = new Set(loop.match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
3531
|
+
if (!used.has("i")) return "i";
|
|
3532
|
+
for (let n = 1; ; n++) if (!used.has(`i_${n}`)) return `i_${n}`;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
// src/resolve.ts
|
|
3536
|
+
var EXTENSIONS = [".astro", ".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".mjs", ".cjs"];
|
|
3537
|
+
function join(fromFile, specifier) {
|
|
3538
|
+
const parts = fromFile.split("/").slice(0, -1);
|
|
3539
|
+
for (const segment of specifier.split("/")) {
|
|
3540
|
+
if (segment === "." || segment === "") continue;
|
|
3541
|
+
if (segment === "..") parts.pop();
|
|
3542
|
+
else parts.push(segment);
|
|
3543
|
+
}
|
|
3544
|
+
return parts.join("/");
|
|
3545
|
+
}
|
|
3546
|
+
var firstPresent = (base, files) => [base, ...EXTENSIONS.map((ext) => `${base}${ext}`), ...EXTENSIONS.map((ext) => `${base}/index${ext}`)].find(
|
|
3547
|
+
(candidate) => files.has(candidate)
|
|
3548
|
+
) ?? null;
|
|
3549
|
+
function aliasesFrom(sources) {
|
|
3550
|
+
const out = /* @__PURE__ */ new Map();
|
|
3551
|
+
for (const source of sources) {
|
|
3552
|
+
if (!isProjectConfig(source.path)) continue;
|
|
3553
|
+
const parsed = readJsonc(source.content);
|
|
3554
|
+
if (parsed === null) continue;
|
|
3555
|
+
const base = parsed.compilerOptions?.baseUrl?.replace(/^\.\/?/, "").replace(/\/$/, "") ?? "";
|
|
3556
|
+
const root = source.path.split("/").slice(0, -1).join("/");
|
|
3557
|
+
for (const [pattern, targets] of Object.entries(parsed.compilerOptions?.paths ?? {})) {
|
|
3558
|
+
out.set(
|
|
3559
|
+
pattern,
|
|
3560
|
+
targets.map((target) => [root, base, target.replace(/^\.\/?/, "")].filter(Boolean).join("/"))
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
return out;
|
|
3565
|
+
}
|
|
3566
|
+
var isProjectConfig = (path) => path.endsWith("tsconfig.json") || path.endsWith("jsconfig.json");
|
|
3567
|
+
function readJsonc(content) {
|
|
3568
|
+
let out = "";
|
|
3569
|
+
let at = 0;
|
|
3570
|
+
while (at < content.length) {
|
|
3571
|
+
const char = content[at];
|
|
3572
|
+
if (char === '"') {
|
|
3573
|
+
const start = at++;
|
|
3574
|
+
while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
|
|
3575
|
+
out += content.slice(start, Math.min(at + 1, content.length));
|
|
3576
|
+
at++;
|
|
3577
|
+
continue;
|
|
3578
|
+
}
|
|
3579
|
+
if (char === "/" && content[at + 1] === "/") {
|
|
3580
|
+
while (at < content.length && content[at] !== "\n") at++;
|
|
3581
|
+
continue;
|
|
3582
|
+
}
|
|
3583
|
+
if (char === "/" && content[at + 1] === "*") {
|
|
3584
|
+
const close = content.indexOf("*/", at + 2);
|
|
3585
|
+
at = close < 0 ? content.length : close + 2;
|
|
3586
|
+
continue;
|
|
3587
|
+
}
|
|
3588
|
+
out += char;
|
|
3589
|
+
at++;
|
|
3590
|
+
}
|
|
3591
|
+
try {
|
|
3592
|
+
const parsed = JSON.parse(out.replace(/,(?=\s*[}\]])/g, ""));
|
|
3593
|
+
return isAliasShaped(parsed) ? parsed : null;
|
|
3594
|
+
} catch {
|
|
3595
|
+
return null;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3599
|
+
function isAliasShaped(value) {
|
|
3600
|
+
if (!isRecord(value)) return false;
|
|
3601
|
+
const options = value.compilerOptions;
|
|
3602
|
+
if (options === void 0) return true;
|
|
3603
|
+
if (!isRecord(options)) return false;
|
|
3604
|
+
if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
|
|
3605
|
+
if (options.paths === void 0) return true;
|
|
3606
|
+
if (!isRecord(options.paths)) return false;
|
|
3607
|
+
return Object.values(options.paths).every(
|
|
3608
|
+
(targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
3611
|
+
function unreadableConfigs(sources) {
|
|
3612
|
+
return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
|
|
3613
|
+
}
|
|
3614
|
+
function expand(specifier, aliases) {
|
|
3615
|
+
const out = [];
|
|
3616
|
+
for (const [pattern, targets] of aliases) {
|
|
3617
|
+
const star = pattern.indexOf("*");
|
|
3618
|
+
if (star < 0) {
|
|
3619
|
+
if (specifier === pattern) out.push(...targets);
|
|
3620
|
+
continue;
|
|
3621
|
+
}
|
|
3622
|
+
const head = pattern.slice(0, star);
|
|
3623
|
+
const tail = pattern.slice(star + 1);
|
|
3624
|
+
if (!specifier.startsWith(head) || !specifier.endsWith(tail)) continue;
|
|
3625
|
+
const middle = specifier.slice(head.length, specifier.length - tail.length);
|
|
3626
|
+
out.push(...targets.map((target) => target.replace("*", middle)));
|
|
3627
|
+
}
|
|
3628
|
+
return out;
|
|
3629
|
+
}
|
|
3630
|
+
function resolveSpecifier(fromFile, specifier, files, aliases) {
|
|
3631
|
+
if (specifier.startsWith(".")) return firstPresent(join(fromFile, specifier), files);
|
|
3632
|
+
for (const candidate of expand(specifier, aliases)) {
|
|
3633
|
+
const found = firstPresent(candidate.replace(/\/$/, ""), files);
|
|
3634
|
+
if (found) return found;
|
|
3635
|
+
}
|
|
3636
|
+
return null;
|
|
3637
|
+
}
|
|
3638
|
+
function importsIn(parsed, _dialect) {
|
|
3639
|
+
const out = /* @__PURE__ */ new Map();
|
|
3640
|
+
walkAst(parsed.ast, (node) => {
|
|
3641
|
+
if (node.type !== "ImportDeclaration") return;
|
|
3642
|
+
if (node.importKind === "type") return;
|
|
3643
|
+
const specifier = node.source?.value;
|
|
3644
|
+
if (typeof specifier !== "string") return;
|
|
3645
|
+
for (const entry of node.specifiers ?? []) {
|
|
3646
|
+
if (entry.importKind === "type") continue;
|
|
3647
|
+
const local = entry.local?.name;
|
|
3648
|
+
if (typeof local !== "string") continue;
|
|
3649
|
+
const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
|
|
3650
|
+
const imported = kind === "named" ? entry.imported?.name ?? local : null;
|
|
3651
|
+
out.set(local, { specifier, imported, kind });
|
|
3652
|
+
}
|
|
3653
|
+
});
|
|
3654
|
+
return out;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
// src/images.ts
|
|
3658
|
+
var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif", ".svg"];
|
|
3659
|
+
var ASSET_COMPONENTS = /* @__PURE__ */ new Set(["Image", "Picture"]);
|
|
3660
|
+
var ASSET_MODULE = "astro:assets";
|
|
3661
|
+
function baseName(url) {
|
|
3662
|
+
const file = url.split(/[?#]/)[0].split("/").pop() ?? "";
|
|
3663
|
+
const base = file.split(".")[0] ?? "";
|
|
3664
|
+
return base === "" ? null : base.toLowerCase();
|
|
3665
|
+
}
|
|
3666
|
+
function assetCandidates(parsed, dialect, original) {
|
|
3667
|
+
const wanted = baseName(original);
|
|
3668
|
+
if (wanted === null || parsed.error) return [];
|
|
3669
|
+
const found = [];
|
|
3670
|
+
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
3671
|
+
const specifier = from.specifier.toLowerCase();
|
|
3672
|
+
if (from.kind !== "default" || !IMAGE_EXTENSIONS.some((ext) => specifier.endsWith(ext))) continue;
|
|
3673
|
+
if (baseName(from.specifier) === wanted) found.push(local);
|
|
3674
|
+
}
|
|
3675
|
+
return found;
|
|
3676
|
+
}
|
|
3677
|
+
function assetImport(parsed, dialect, original) {
|
|
3678
|
+
const found = assetCandidates(parsed, dialect, original);
|
|
3679
|
+
return found.length === 1 ? found[0] : null;
|
|
3680
|
+
}
|
|
3681
|
+
function renderedBy(content, parsed, dialect, binding) {
|
|
3682
|
+
const assets = new Set(
|
|
3683
|
+
[...importsIn(parsed, dialect)].filter(([, from]) => from.specifier === ASSET_MODULE).map(([local]) => local)
|
|
3684
|
+
);
|
|
3685
|
+
const out = [];
|
|
3686
|
+
const visit = (node) => {
|
|
3687
|
+
if (node.type === "expr") {
|
|
3688
|
+
for (const child of node.children) visit(child);
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
if (node.type !== "element" || node.raw) return;
|
|
3692
|
+
const sources = IMAGE_SOURCES[node.tag.toLowerCase()] ?? (assets.has(node.tag) && ASSET_COMPONENTS.has(node.tag) ? ["src"] : []);
|
|
3693
|
+
for (const attr of node.attrs) {
|
|
3694
|
+
if (!sources.includes(attr.name) || !attr.expressionRange) continue;
|
|
3695
|
+
const inner = content.slice(attr.expressionRange.start, attr.expressionRange.end).replace(/^\{|\}$/g, "").trim();
|
|
3696
|
+
if (inner === binding) out.push({ node, attr: attr.name });
|
|
3697
|
+
}
|
|
3698
|
+
for (const child of node.children) visit(child);
|
|
3699
|
+
};
|
|
3700
|
+
for (const root of parsed.roots) visit(root);
|
|
3701
|
+
return out;
|
|
3702
|
+
}
|
|
3703
|
+
function bindImages(ctx, targets) {
|
|
3704
|
+
const out = { splices: [], slugs: [], bound: [], refused: [] };
|
|
3705
|
+
const rule = DIALECT_RULES[ctx.dialect];
|
|
3706
|
+
if (!rule.attr || !rule.text || ctx.parsed.error) return out;
|
|
3707
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
3708
|
+
for (const target of targets) {
|
|
3709
|
+
const binding = assetImport(ctx.parsed, ctx.dialect, target.original);
|
|
3710
|
+
const rendered = binding === null ? [] : renderedBy(ctx.content, ctx.parsed, ctx.dialect, binding);
|
|
3711
|
+
const free = rendered.filter((hit2) => !claimed.has(hit2.node.attrInsertAt));
|
|
3712
|
+
if (free.length !== 1) {
|
|
3713
|
+
out.refused.push({ key: target.key, reason: "IMAGE_ASSET_UNRESOLVED" });
|
|
3714
|
+
continue;
|
|
3715
|
+
}
|
|
3716
|
+
const hit = free[0];
|
|
3717
|
+
claimed.add(hit.node.attrInsertAt);
|
|
3718
|
+
const attr = hit.node.attrs.find((a) => a.name === hit.attr);
|
|
3719
|
+
const args = [
|
|
3720
|
+
ctx.snapshotName(target.slug),
|
|
3721
|
+
JSON.stringify(target.path),
|
|
3722
|
+
binding,
|
|
3723
|
+
...ctx.slug ? [ctx.slug] : []
|
|
3724
|
+
];
|
|
3725
|
+
out.splices.push({
|
|
3726
|
+
start: attr.range.start,
|
|
3727
|
+
end: attr.range.end,
|
|
3728
|
+
text: rule.attr(hit.attr, `${ctx.image}(${args.join(", ")})`)
|
|
3729
|
+
});
|
|
3730
|
+
out.splices.push({
|
|
3731
|
+
start: hit.node.attrInsertAt,
|
|
3732
|
+
end: hit.node.attrInsertAt,
|
|
3733
|
+
text: ` data-bcms-field="${target.path}" data-bcms-kind="${target.kind || "image"}"`
|
|
3734
|
+
});
|
|
3735
|
+
out.slugs.push(target.slug);
|
|
3736
|
+
out.bound.push(target.key);
|
|
3737
|
+
}
|
|
3738
|
+
return out;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
// src/helper.ts
|
|
3742
|
+
import { createHash as createHash2 } from "crypto";
|
|
3743
|
+
var HELPER_VERSION = 5;
|
|
3744
|
+
var HEADER = "// @bettercms-ai/convert helper v";
|
|
3745
|
+
var HELPER_PATH = Object.fromEntries(
|
|
3746
|
+
Object.keys(DIALECT_RULES).map((d) => [d, DIALECT_RULES[d].helper])
|
|
3747
|
+
);
|
|
3748
|
+
var CONTENT_DIR = "bcms-content";
|
|
3749
|
+
var STUB_CONTENT = "{}";
|
|
3750
|
+
function relativeImport(fromFile, toFile) {
|
|
3751
|
+
const from = fromFile.split("/").slice(0, -1);
|
|
3752
|
+
const to = toFile.split("/");
|
|
3753
|
+
let shared = 0;
|
|
3754
|
+
while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared]) shared++;
|
|
3755
|
+
const up = from.length - shared;
|
|
3756
|
+
const path = [...Array(up).fill(".."), ...to.slice(shared)].join("/");
|
|
3757
|
+
return up === 0 ? `./${path}` : path;
|
|
3758
|
+
}
|
|
3759
|
+
function helperSource(dialect) {
|
|
3760
|
+
const path = HELPER_PATH[dialect];
|
|
3761
|
+
if (!path) throw new Error(`The ${dialect} dialect reads its values at publish time, not at build time.`);
|
|
3762
|
+
const layout = relativeImport(path, `${CONTENT_DIR}/layout.json`);
|
|
3763
|
+
return `${HEADER}${HELPER_VERSION}
|
|
3764
|
+
// Values come from bcms-content/*.json, written into the tree before the build. The committed
|
|
3765
|
+
// stubs are {} \u2014 a build with them renders every fallback below and still succeeds.
|
|
3766
|
+
import layoutSnapshot from "${layout}";
|
|
3767
|
+
|
|
3768
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
3769
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3770
|
+
|
|
3771
|
+
/** Dotted keys and \`[n]\` indices, positionally \u2014 the editor's own path grammar. */
|
|
3772
|
+
function readPath(root: unknown, path: string): unknown {
|
|
3773
|
+
let cursor: unknown = root;
|
|
3774
|
+
for (const segment of path.split(".")) {
|
|
3775
|
+
if (!segment) return undefined;
|
|
3776
|
+
const key = segment.replace(/\\[\\d+\\]/g, "");
|
|
3777
|
+
if (key) {
|
|
3778
|
+
if (!isRecord(cursor)) return undefined;
|
|
3779
|
+
cursor = cursor[key];
|
|
3780
|
+
}
|
|
3781
|
+
for (const index of segment.matchAll(/\\[(\\d+)\\]/g)) {
|
|
3782
|
+
if (!Array.isArray(cursor)) return undefined;
|
|
3783
|
+
cursor = cursor[Number(index[1])];
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
return cursor;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
/** A stored value as a template renders it: richtext is its html, an image is its url. */
|
|
3790
|
+
function render(value: unknown): string | undefined {
|
|
3791
|
+
if (typeof value === "string") return value;
|
|
3792
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
3793
|
+
if (isRecord(value)) {
|
|
3794
|
+
if (typeof value.html === "string") return value.html;
|
|
3795
|
+
if (typeof value.url === "string") return value.url;
|
|
3796
|
+
}
|
|
3797
|
+
return undefined;
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
/** One field of one page. \`slug\` selects the entry on a dynamic route. */
|
|
3801
|
+
export function bcms(page: unknown, path: string, fallback: string, slug?: string): string {
|
|
3802
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
3803
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
3804
|
+
return render(readPath(root, path)) ?? fallback;
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
/** One field of the shared layout \u2014 the header and footer every route renders. */
|
|
3808
|
+
export function bcmsLayout(path: string, fallback: string): string {
|
|
3809
|
+
return render(readPath(layoutSnapshot, path)) ?? fallback;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
/**
|
|
3813
|
+
* The ROWS of a repeater. \`fallback\` is every row the template shipped with, so a build against
|
|
3814
|
+
* an empty snapshot still renders the same number of cards it always did.
|
|
3815
|
+
*/
|
|
3816
|
+
export function bcmsRows<T>(page: unknown, path: string, fallback: T[], slug?: string): T[] {
|
|
3817
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
3818
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
3819
|
+
const value = readPath(root, path);
|
|
3820
|
+
return Array.isArray(value) ? (value as T[]) : fallback;
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
/**
|
|
3824
|
+
* The rows of a repeater whose template renders its OWN property names.
|
|
3825
|
+
*
|
|
3826
|
+
* \u{1F534} A ROW IS MORE THAN ITS COPY. The template's array carries \`tone\`, \`link\`, an icon \u2014 styling
|
|
3827
|
+
* the brief never heard of, because the CMS holds the four sentences and not the class names. So a
|
|
3828
|
+
* CMS row is merged OVER the row the template shipped with rather than replacing it, and \`keys\`
|
|
3829
|
+
* says which template property each CMS leaf lands on. A row an editor ADDS has no counterpart, so
|
|
3830
|
+
* it inherits row 0's styling \u2014 the only answer that renders at all.
|
|
3831
|
+
*/
|
|
3832
|
+
export function bcmsRowsAs<T>(
|
|
3833
|
+
page: unknown,
|
|
3834
|
+
path: string,
|
|
3835
|
+
fallback: T[],
|
|
3836
|
+
keys: Record<string, string>,
|
|
3837
|
+
slug?: string,
|
|
3838
|
+
): T[] {
|
|
3839
|
+
const rows = bcmsRows<unknown>(page, path, [], slug);
|
|
3840
|
+
if (rows.length === 0) return fallback;
|
|
3841
|
+
return rows.map((row, index) => {
|
|
3842
|
+
const base = { ...(fallback[index] ?? fallback[0] ?? {}) } as Record<string, unknown>;
|
|
3843
|
+
for (const property of Object.keys(keys)) {
|
|
3844
|
+
const value = render(readPath(row, keys[property] as string));
|
|
3845
|
+
if (value !== undefined) base[property] = value;
|
|
3846
|
+
}
|
|
3847
|
+
return base as T;
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
/** An image field's url, or the asset the template already imports. @see IMAGE_ASSET_UNRESOLVED */
|
|
3852
|
+
export function bcmsImage<T>(page: unknown, path: string, fallback: T, slug?: string): T | string {
|
|
3853
|
+
const entries = isRecord(page) ? page.entries : undefined;
|
|
3854
|
+
const root = slug !== undefined && isRecord(entries) ? entries[slug] : page;
|
|
3855
|
+
return render(readPath(root, path)) ?? fallback;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
/**
|
|
3859
|
+
* ONE SECTION's copy, for a component extracted by \`--componentize\`.
|
|
3860
|
+
*
|
|
3861
|
+
* OVERRIDES FIRST, PER KEY: \`overrides[key] ?? group[key]\`. An instance that owns its copy
|
|
3862
|
+
* (Phase B) carries an override for every prop it declares, so the group underneath it is never
|
|
3863
|
+
* read; a placement still bound to a page field group (Phase A) carries none, so the group is all
|
|
3864
|
+
* there is. The interesting case is the one in between \u2014 a section half moved \u2014 where a key the
|
|
3865
|
+
* instance does not hold has to keep rendering the page's value rather than nothing.
|
|
3866
|
+
*
|
|
3867
|
+
* \u{1F534} \`??\`, NOT \`||\`. An empty string is a value an editor typed, and treating it as absent put
|
|
3868
|
+
* the page's old copy back into a heading somebody had deliberately cleared.
|
|
3869
|
+
*
|
|
3870
|
+
* With neither an override nor a group, the answer is \`{}\` and every \`bcms(...)\` below renders
|
|
3871
|
+
* the fallback the template shipped with \u2014 which is why a section renders correctly against the
|
|
3872
|
+
* committed \`{}\` stub.
|
|
3873
|
+
*/
|
|
3874
|
+
export function bcmsSection(page: unknown, bind?: string, overrides?: unknown): Record<string, unknown> {
|
|
3875
|
+
const group = bind ? readPath(page, bind) : undefined;
|
|
3876
|
+
const base: Record<string, unknown> = isRecord(group) ? { ...group } : {};
|
|
3877
|
+
if (!isRecord(overrides)) return base;
|
|
3878
|
+
for (const key of Object.keys(overrides)) {
|
|
3879
|
+
const value = overrides[key];
|
|
3880
|
+
if (value === undefined || value === null) continue;
|
|
3881
|
+
/**
|
|
3882
|
+
* \u{1F534} A TABLE PROP IS STORED AS \`{ rows: [...] }\` AND READ AS A LIST. That is the shape the
|
|
3883
|
+
* dashboard edits a repeater in; handing it to \`bcmsRows\` unchanged is not an array, so the
|
|
3884
|
+
* helper fell back to the rows the template shipped with and the editor's work never showed.
|
|
3885
|
+
*/
|
|
3886
|
+
base[key] = isRecord(value) && Array.isArray(value.rows) ? value.rows : value;
|
|
3887
|
+
}
|
|
3888
|
+
return base;
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
/**
|
|
3892
|
+
* WHICH ADDRESS one bound element declares \u2014 decided at RENDER time, from the placement.
|
|
3893
|
+
*
|
|
3894
|
+
* An element carries exactly ONE \`data-bcms-field\`, and which lane it names depends on where its
|
|
3895
|
+
* copy lives. A placement that owns its copy declares the BLOCK lane \u2014
|
|
3896
|
+
* \`<blockId>__overrides.<propKey>\` \u2014 because that is the address the editor commits an instance's
|
|
3897
|
+
* own value to. A placement bound to a page field group declares the PAGE path, which is what the
|
|
3898
|
+
* page form and the coverage meter already know.
|
|
3899
|
+
*
|
|
3900
|
+
* \u{1F534} THE BOUND ADDRESS IS RE-BASED ONTO **THIS** PLACEMENT'S GROUP. The markup was cut from ONE
|
|
3901
|
+
* page and carries that page's paths (\`hero.title\`); the whole point of \`shapeHash\` is that a
|
|
3902
|
+
* second page renders the same component bound to a group of its own (\`bind="landingHero"\`).
|
|
3903
|
+
* Handing the embedded path through unchanged declared \`hero.title\` on a page whose field is
|
|
3904
|
+
* \`landingHero.title\` \u2014 an address that page does not have, so the editor wrote into nothing and
|
|
3905
|
+
* the meter counted a binding that was never there. The prefix the markup was cut with comes out,
|
|
3906
|
+
* the placement's own group goes in.
|
|
3907
|
+
*
|
|
3908
|
+
* \u{1F534} THE SAME EXTRACTED FILE SERVES BOTH LANES. The choice is a function of the props, never of
|
|
3909
|
+
* how the codemod ran: one component renders a Phase A placement on one page and a Phase B
|
|
3910
|
+
* instance on the next, and a repository does not have to be re-cut when its copy moves.
|
|
3911
|
+
*/
|
|
3912
|
+
export function bcmsFields(
|
|
3913
|
+
placement: { blockId?: string; bind?: string; overrides?: unknown },
|
|
3914
|
+
pathPrefix: string,
|
|
3915
|
+
): (path: string) => string {
|
|
3916
|
+
const id = placement.blockId;
|
|
3917
|
+
const own = typeof id === "string" && id !== "" && !placement.bind && isRecord(placement.overrides);
|
|
3918
|
+
/**
|
|
3919
|
+
* What replaces the prefix the markup was cut with, or null for "leave every path alone".
|
|
3920
|
+
*
|
|
3921
|
+
* Null covers a placement with neither a bind nor an instance \u2014 a shipped fallback, or a page
|
|
3922
|
+
* nobody has componentized yet \u2014 and a bound placement in a component whose prefix is EMPTY,
|
|
3923
|
+
* where the plan said the prop key IS the whole page path: there is no group segment to swap,
|
|
3924
|
+
* so prepending one would invent an address rather than correct it.
|
|
3925
|
+
*/
|
|
3926
|
+
const lane = own ? \`\${id}__overrides.\` : placement.bind && pathPrefix ? \`\${placement.bind}.\` : null;
|
|
3927
|
+
return (path: string): string => {
|
|
3928
|
+
// A path outside the group this section was cut from is not one this placement re-addresses:
|
|
3929
|
+
// it names a key the instance does not hold and a group this placement is not bound to.
|
|
3930
|
+
// (An EMPTY prefix is a plan saying the prop key IS the page path, and matches everything.)
|
|
3931
|
+
if (lane === null || !path.startsWith(pathPrefix)) return path;
|
|
3932
|
+
return lane + path.slice(pathPrefix.length);
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
`;
|
|
3936
|
+
}
|
|
3937
|
+
var HISTORICAL = {
|
|
3938
|
+
"src/bcms-content.ts": {
|
|
3939
|
+
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3940
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3941
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3942
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
3943
|
+
},
|
|
3944
|
+
"src/lib/bcms-content.ts": {
|
|
3945
|
+
1: "ae72ae14d706943d481c880d70da5789b4816ba1ba319a10fbc63ded4d0b42f2",
|
|
3946
|
+
2: "a18731eb42f7db55dac0591c45f873758cde8e103fefcd6e5caf67936ae6a002",
|
|
3947
|
+
3: "0be9c73160361882a038552bc02673c14ed532287823dabaafe36179bc55ed7e",
|
|
3948
|
+
4: "0146d027a727ecd3c9e3fc336fcf1299b59ddf75b04091c05703f3f983ec28af"
|
|
3949
|
+
},
|
|
3950
|
+
"composables/bcms-content.ts": {
|
|
3951
|
+
1: "c272412532efed7db64b6cf313b2bed4291f9951430412289c8a588b7f565407",
|
|
3952
|
+
2: "51e2ae65ddd86bf43d14e29b3865e3f16e45f72069692d1fea57e1f986a894f0",
|
|
3953
|
+
3: "d4305b2983adcb456ef782d35ea9d083a04465c2b68baa0d44a084dfa01a9227",
|
|
3954
|
+
4: "a5742f64dacb1b26ee6dd5e1469a54871487934c71bc8267df19e3a80e417595"
|
|
3955
|
+
}
|
|
3956
|
+
};
|
|
3957
|
+
var sha256 = (value) => createHash2("sha256").update(value, "utf8").digest("hex");
|
|
3958
|
+
function isKnownHelper(content, dialect) {
|
|
3959
|
+
if (content === helperSource(dialect)) return HELPER_VERSION;
|
|
3960
|
+
const path = HELPER_PATH[dialect];
|
|
3961
|
+
if (!path) return null;
|
|
3962
|
+
const digest = sha256(content);
|
|
3963
|
+
for (const [version, known] of Object.entries(HISTORICAL[path] ?? {})) {
|
|
3964
|
+
if (known === digest) return Number(version);
|
|
3965
|
+
}
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// src/locate.ts
|
|
3970
|
+
var flat = (value) => value.replace(/\s+/g, " ").trim();
|
|
3971
|
+
var stripTags = (value) => value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "");
|
|
3972
|
+
function locate(brief, sources, options) {
|
|
3973
|
+
const flattened = sources.map((s) => {
|
|
3974
|
+
const readable = unwrapJsxStrings(decodeEntities(s.content));
|
|
3975
|
+
return {
|
|
3976
|
+
path: s.path,
|
|
3977
|
+
content: flat(readable),
|
|
3978
|
+
text: options?.stripTags ? flat(stripTags(readable)) : null
|
|
3979
|
+
};
|
|
3980
|
+
});
|
|
3981
|
+
const located = [];
|
|
3982
|
+
const unlocated = [];
|
|
3983
|
+
for (const page of brief.pages) {
|
|
3984
|
+
for (const p of page.paths) {
|
|
3985
|
+
const original = p.original === null ? "" : flat(p.original);
|
|
3986
|
+
if (!original) {
|
|
3987
|
+
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "no-original" });
|
|
3988
|
+
continue;
|
|
3989
|
+
}
|
|
3990
|
+
const files = flattened.filter((f) => f.content.includes(original) || f.text !== null && f.text.includes(original)).map((f) => f.path);
|
|
3991
|
+
if (files.length === 0) {
|
|
3992
|
+
unlocated.push({ route: page.route, scope: p.scope, path: p.path, reason: "not-in-source" });
|
|
3993
|
+
continue;
|
|
3994
|
+
}
|
|
3995
|
+
located.push({ route: page.route, scope: p.scope, path: p.path, kind: p.kind, original, files });
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
return { located, unlocated };
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// src/loops.ts
|
|
4002
|
+
import MagicString from "magic-string";
|
|
4003
|
+
|
|
4004
|
+
// src/scope.ts
|
|
4005
|
+
function patternNames(node, out) {
|
|
4006
|
+
if (!node || typeof node !== "object") return;
|
|
4007
|
+
const typed = node;
|
|
4008
|
+
switch (typed.type) {
|
|
4009
|
+
case "Identifier":
|
|
4010
|
+
out.add(String(typed.name));
|
|
4011
|
+
return;
|
|
4012
|
+
case "ObjectPattern":
|
|
4013
|
+
for (const property of typed.properties ?? [])
|
|
4014
|
+
patternNames(property.type === "RestElement" ? property.argument : property.value, out);
|
|
4015
|
+
return;
|
|
4016
|
+
case "ArrayPattern":
|
|
4017
|
+
for (const element of typed.elements ?? []) patternNames(element, out);
|
|
4018
|
+
return;
|
|
4019
|
+
case "AssignmentPattern":
|
|
4020
|
+
patternNames(typed.left, out);
|
|
4021
|
+
return;
|
|
4022
|
+
case "RestElement":
|
|
4023
|
+
patternNames(typed.argument, out);
|
|
4024
|
+
return;
|
|
4025
|
+
default:
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
function statementNames(node, out) {
|
|
4030
|
+
if (!node || typeof node !== "object") return;
|
|
4031
|
+
const typed = node;
|
|
4032
|
+
switch (typed.type) {
|
|
4033
|
+
case "ExportNamedDeclaration":
|
|
4034
|
+
case "ExportDefaultDeclaration":
|
|
4035
|
+
statementNames(typed.declaration, out);
|
|
4036
|
+
return;
|
|
4037
|
+
case "ImportDeclaration":
|
|
4038
|
+
for (const specifier of typed.specifiers ?? []) patternNames(specifier.local, out);
|
|
4039
|
+
return;
|
|
4040
|
+
case "VariableDeclaration":
|
|
4041
|
+
for (const declarator of typed.declarations ?? []) patternNames(declarator.id, out);
|
|
4042
|
+
return;
|
|
4043
|
+
case "FunctionDeclaration":
|
|
4044
|
+
case "ClassDeclaration":
|
|
4045
|
+
case "TSTypeAliasDeclaration":
|
|
4046
|
+
case "TSInterfaceDeclaration":
|
|
4047
|
+
case "TSEnumDeclaration":
|
|
4048
|
+
case "TSModuleDeclaration":
|
|
4049
|
+
patternNames(typed.id, out);
|
|
4050
|
+
return;
|
|
4051
|
+
default:
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
var DECLARED = /\b(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
|
|
4056
|
+
var IMPORTED = /\bimport\s+([^;\n]*?)\s+from\b/g;
|
|
4057
|
+
function scanNames(code, out) {
|
|
4058
|
+
for (const match of code.matchAll(DECLARED)) out.add(match[1]);
|
|
4059
|
+
for (const match of code.matchAll(IMPORTED))
|
|
4060
|
+
for (const name of match[1].match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
4061
|
+
if (name !== "as" && name !== "type") out.add(name);
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
function scriptText(nodes, content, out) {
|
|
4065
|
+
for (const node of nodes) {
|
|
4066
|
+
if (node.type !== "element") continue;
|
|
4067
|
+
if (node.raw && node.tag === "script" && node.inner) out.push(content.slice(node.inner.start, node.inner.end));
|
|
4068
|
+
scriptText(node.children, content, out);
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
function topLevelBindings(dialect, parsed, content) {
|
|
4072
|
+
const out = /* @__PURE__ */ new Set();
|
|
4073
|
+
const program = parsed.ast?.program;
|
|
4074
|
+
if (program) for (const statement of program.body ?? []) statementNames(statement, out);
|
|
4075
|
+
else if (parsed.script) scanNames(parsed.script.code, out);
|
|
4076
|
+
if (dialect === "html") {
|
|
4077
|
+
const scripts = [];
|
|
4078
|
+
scriptText(parsed.roots, content, scripts);
|
|
4079
|
+
for (const code of scripts) scanNames(code, out);
|
|
4080
|
+
}
|
|
4081
|
+
return out;
|
|
4082
|
+
}
|
|
4083
|
+
function allocateNames(taken, wanted) {
|
|
4084
|
+
const used = new Set(taken);
|
|
4085
|
+
const out = /* @__PURE__ */ new Map();
|
|
4086
|
+
for (const { key, name } of wanted) {
|
|
4087
|
+
if (out.has(key)) continue;
|
|
4088
|
+
let candidate = name;
|
|
4089
|
+
for (let n = 1; used.has(candidate); n++) candidate = `${name}_${n}`;
|
|
4090
|
+
used.add(candidate);
|
|
4091
|
+
out.set(key, candidate);
|
|
4092
|
+
}
|
|
4093
|
+
return out;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
// src/loops.ts
|
|
4097
|
+
function splitNested(leaf) {
|
|
4098
|
+
const match = leaf.match(/^([^[\]]+)\[(\d+)\]\.(.+)$/);
|
|
4099
|
+
return match ? { array: match[1], index: Number(match[2]), leaf: match[3] } : null;
|
|
4100
|
+
}
|
|
4101
|
+
var elementEnd = (node) => node.inner ? node.inner.end + node.tag.length + 3 : null;
|
|
4102
|
+
function findRows(roots, members) {
|
|
4103
|
+
const wanted = new Set(members.map((m) => m.index));
|
|
4104
|
+
const search = (node) => {
|
|
4105
|
+
if (node.type !== "element") return null;
|
|
4106
|
+
for (const child of node.children) {
|
|
4107
|
+
const deeper = search(child);
|
|
4108
|
+
if (deeper) return deeper;
|
|
4109
|
+
}
|
|
4110
|
+
const rows = [];
|
|
4111
|
+
for (const child of node.children) {
|
|
4112
|
+
if (child.type !== "element") continue;
|
|
4113
|
+
const end = elementEnd(child);
|
|
4114
|
+
if (end === null) continue;
|
|
4115
|
+
const inside = members.filter((m) => m.site.range.start >= child.start && m.site.range.end <= end);
|
|
4116
|
+
if (inside.length === 0) continue;
|
|
4117
|
+
const indices = new Set(inside.map((m) => m.index));
|
|
4118
|
+
if (indices.size !== 1) return null;
|
|
4119
|
+
rows.push({
|
|
4120
|
+
index: inside[0].index,
|
|
4121
|
+
range: { start: child.start, end },
|
|
4122
|
+
attrInsertAt: child.attrInsertAt,
|
|
4123
|
+
// `:key` in vue, `key` in jsx — one existing key, whichever way it is spelled.
|
|
4124
|
+
keyAttr: child.attrs.find((a) => a.name.replace(/^:/, "") === "key")?.range ?? null,
|
|
4125
|
+
node: child,
|
|
4126
|
+
members: inside
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
if (rows.length === 0) return null;
|
|
4130
|
+
const covered = new Set(rows.flatMap((r) => r.members.map((m) => m.index)));
|
|
4131
|
+
if (covered.size !== wanted.size || rows.length !== wanted.size) return null;
|
|
4132
|
+
if (rows.some((r) => r.members.length !== members.length / wanted.size)) return null;
|
|
4133
|
+
return rows.sort((a, b) => a.range.start - b.range.start);
|
|
4134
|
+
};
|
|
4135
|
+
for (const root of roots) {
|
|
4136
|
+
const found = search(root);
|
|
4137
|
+
if (found) return found;
|
|
4138
|
+
}
|
|
4139
|
+
return null;
|
|
4140
|
+
}
|
|
4141
|
+
var consecutive = (rows, content) => rows.every((row, at) => at === 0 || content.slice(rows[at - 1].range.end, row.range.start).trim() === "");
|
|
4142
|
+
function maskedRow(row, content) {
|
|
4143
|
+
const masked = new Map(row.members.map((member) => [member.site.inner.start, member.leaf]));
|
|
4144
|
+
return canonical(row.node, content, masked);
|
|
4145
|
+
}
|
|
4146
|
+
var TRIVIA = "";
|
|
4147
|
+
function canonical(node, content, masked) {
|
|
4148
|
+
if (node.type === "text") {
|
|
4149
|
+
return node.value.trim() === "" ? TRIVIA : content.slice(node.range.start, node.range.end);
|
|
4150
|
+
}
|
|
4151
|
+
if (node.type === "comment" || node.type === "expr") {
|
|
4152
|
+
return content.slice(node.range.start, node.range.end);
|
|
4153
|
+
}
|
|
4154
|
+
if (node.type !== "element") return "";
|
|
4155
|
+
const open = content.slice(node.start, node.inner ? node.inner.start : node.attrInsertAt);
|
|
4156
|
+
if (node.inner && masked.has(node.inner.start)) {
|
|
4157
|
+
return `${open}\0${masked.get(node.inner.start)}\0</${node.tag}>`;
|
|
4158
|
+
}
|
|
4159
|
+
const inside = node.children.map((child) => canonical(child, content, masked)).join("");
|
|
4160
|
+
return node.inner ? `${open}${inside}</${node.tag}>` : open;
|
|
4161
|
+
}
|
|
4162
|
+
function sameShape(rows, content) {
|
|
4163
|
+
const first = maskedRow(rows[0], content);
|
|
4164
|
+
return rows.every((row) => maskedRow(row, content) === first);
|
|
4165
|
+
}
|
|
4166
|
+
var indented = (body, indent) => body.split("\n").map((line) => line.startsWith(indent) ? `${indent} ${line.slice(indent.length)}` : `${indent} ${line}`).join("\n");
|
|
4167
|
+
function indentOf(content, at) {
|
|
4168
|
+
const lineStart = content.lastIndexOf("\n", at - 1) + 1;
|
|
4169
|
+
const prefix = content.slice(lineStart, at);
|
|
4170
|
+
return prefix.trim() === "" ? prefix : "";
|
|
4171
|
+
}
|
|
4172
|
+
function writeLeaf(source, shift, member, rule, read, row, literal) {
|
|
4173
|
+
const site = member.site;
|
|
4174
|
+
const expression = `${read}(${row}, ${JSON.stringify(member.leaf)}, "")`;
|
|
4175
|
+
if (member.kind === "richtext" && rule.richtext) {
|
|
4176
|
+
source.remove(site.inner.start + shift, site.inner.end + shift);
|
|
4177
|
+
source.appendLeft(site.attrInsertAt + shift, ` ${rule.richtext(expression)}`);
|
|
4178
|
+
} else {
|
|
4179
|
+
source.overwrite(site.inner.start + shift, site.inner.end + shift, rule.text(expression));
|
|
4180
|
+
}
|
|
4181
|
+
const kind = member.kind ? ` data-bcms-kind="${member.kind}"` : "";
|
|
4182
|
+
source.appendLeft(site.attrInsertAt + shift, ` ${rule.attr("data-bcms-field", `\`${literal}\``)}${kind}`);
|
|
4183
|
+
}
|
|
4184
|
+
function emitNested(rule, array, members, prefix, outer, parent, ctx) {
|
|
4185
|
+
if (!rule.loop || !rule.text) return null;
|
|
4186
|
+
const inner = [];
|
|
4187
|
+
for (const member of members) {
|
|
4188
|
+
const split = splitNested(member.leaf);
|
|
4189
|
+
if (!split || splitNested(split.leaf) !== null) return null;
|
|
4190
|
+
inner.push({ ...member, index: split.index, leaf: split.leaf });
|
|
4191
|
+
}
|
|
4192
|
+
const rows = findRows([parent.node], inner);
|
|
4193
|
+
if (!rows || rows.length === 0) return null;
|
|
4194
|
+
if (!consecutive(rows, ctx.content)) return null;
|
|
4195
|
+
if (!sameShape(rows, ctx.content)) return null;
|
|
4196
|
+
const template = rows[0];
|
|
4197
|
+
const used = /* @__PURE__ */ new Set([
|
|
4198
|
+
...ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? [],
|
|
4199
|
+
outer.row,
|
|
4200
|
+
outer.index
|
|
4201
|
+
]);
|
|
4202
|
+
const allocated = allocateNames(used, [
|
|
4203
|
+
{ key: "row", name: "item" },
|
|
4204
|
+
{ key: "index", name: "j" }
|
|
4205
|
+
]);
|
|
4206
|
+
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
4207
|
+
const fallback = [];
|
|
4208
|
+
const writable = inner.every((member) => setLeaf(fallback[member.index] ??= emptyRow(), member.leaf, member.fallback));
|
|
4209
|
+
if (!writable) return null;
|
|
4210
|
+
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
4211
|
+
const shift = -template.range.start;
|
|
4212
|
+
const declarations = [];
|
|
4213
|
+
for (const member of template.members) {
|
|
4214
|
+
const literal = `${prefix}.${array}[\${${names.index}}].${member.leaf}`;
|
|
4215
|
+
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
4216
|
+
declarations.push({ path: shapeOfPath(literal), literal });
|
|
4217
|
+
}
|
|
4218
|
+
const rowsExpr = `${ctx.rows}(${outer.row}, ${JSON.stringify(array)}, ${JSON.stringify(fallback.filter(Boolean))})`;
|
|
4219
|
+
if (rule.loop.kind === "attr") {
|
|
4220
|
+
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
4221
|
+
}
|
|
4222
|
+
if (rule.loopKey) {
|
|
4223
|
+
const key = rule.loopKey(names.index);
|
|
4224
|
+
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
4225
|
+
else if (template.keyAttr.end > template.keyAttr.start)
|
|
4226
|
+
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
4227
|
+
else return null;
|
|
3773
4228
|
}
|
|
3774
|
-
|
|
4229
|
+
const indent = indentOf(ctx.content, template.range.start);
|
|
4230
|
+
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
4231
|
+
${indented(source.toString(), indent)}
|
|
4232
|
+
${indent}${rule.loop.close}` : source.toString();
|
|
4233
|
+
return { range: { start: template.range.start, end: rows[rows.length - 1].range.end }, text, declarations };
|
|
3775
4234
|
}
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
)
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4235
|
+
function emitRepeater(dialect, group, arrayPath, fallbackRows, ctx) {
|
|
4236
|
+
const rule = DIALECT_RULES[dialect];
|
|
4237
|
+
if (!rule.loop || !rule.text || group.length === 0) return null;
|
|
4238
|
+
const rows = findRows(ctx.roots, group);
|
|
4239
|
+
if (!rows || rows.length < 2) return null;
|
|
4240
|
+
if (!consecutive(rows, ctx.content)) return null;
|
|
4241
|
+
if (!sameShape(rows, ctx.content)) return null;
|
|
4242
|
+
if (group.some((m) => m.site.where !== "text" || !isIntrinsic(m.site.tag) || !m.site.inner)) return null;
|
|
4243
|
+
const template = rows[0];
|
|
4244
|
+
const used = new Set(ctx.content.slice(template.range.start, template.range.end).match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
4245
|
+
const allocated = allocateNames(used, [
|
|
4246
|
+
{ key: "row", name: "card" },
|
|
4247
|
+
{ key: "index", name: "i" }
|
|
4248
|
+
]);
|
|
4249
|
+
const names = { row: allocated.get("row"), index: allocated.get("index") };
|
|
4250
|
+
const source = new MagicString(ctx.content.slice(template.range.start, template.range.end));
|
|
4251
|
+
const shift = -template.range.start;
|
|
4252
|
+
const declarations = [];
|
|
4253
|
+
const nested = /* @__PURE__ */ new Map();
|
|
4254
|
+
const direct = [];
|
|
4255
|
+
for (const member of template.members) {
|
|
4256
|
+
const inner = splitNested(member.leaf);
|
|
4257
|
+
if (inner === null) direct.push(member);
|
|
4258
|
+
else nested.set(inner.array, [...nested.get(inner.array) ?? [], member]);
|
|
3793
4259
|
}
|
|
3794
|
-
|
|
3795
|
-
}
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
while (at < content.length) {
|
|
3801
|
-
const char = content[at];
|
|
3802
|
-
if (char === '"') {
|
|
3803
|
-
const start = at++;
|
|
3804
|
-
while (at < content.length && content[at] !== '"') at += content[at] === "\\" ? 2 : 1;
|
|
3805
|
-
out += content.slice(start, Math.min(at + 1, content.length));
|
|
3806
|
-
at++;
|
|
3807
|
-
continue;
|
|
3808
|
-
}
|
|
3809
|
-
if (char === "/" && content[at + 1] === "/") {
|
|
3810
|
-
while (at < content.length && content[at] !== "\n") at++;
|
|
3811
|
-
continue;
|
|
3812
|
-
}
|
|
3813
|
-
if (char === "/" && content[at + 1] === "*") {
|
|
3814
|
-
const close = content.indexOf("*/", at + 2);
|
|
3815
|
-
at = close < 0 ? content.length : close + 2;
|
|
3816
|
-
continue;
|
|
3817
|
-
}
|
|
3818
|
-
out += char;
|
|
3819
|
-
at++;
|
|
4260
|
+
for (const [array, members] of nested) {
|
|
4261
|
+
const plan2 = emitNested(rule, array, members, `${arrayPath}[\${${names.index}}]`, names, template, ctx);
|
|
4262
|
+
if (!plan2) return null;
|
|
4263
|
+
if (direct.some((m) => m.site.range.start >= plan2.range.start && m.site.range.end <= plan2.range.end)) return null;
|
|
4264
|
+
source.overwrite(plan2.range.start + shift, plan2.range.end + shift, plan2.text);
|
|
4265
|
+
declarations.push(...plan2.declarations);
|
|
3820
4266
|
}
|
|
3821
|
-
|
|
3822
|
-
const
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
return null;
|
|
4267
|
+
for (const member of direct) {
|
|
4268
|
+
const literal = `${arrayPath}[\${${names.index}}].${member.leaf}`;
|
|
4269
|
+
writeLeaf(source, shift, member, rule, ctx.read, names.row, literal);
|
|
4270
|
+
declarations.push({ path: shapeOfPath(literal), literal });
|
|
3826
4271
|
}
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
const options = value.compilerOptions;
|
|
3832
|
-
if (options === void 0) return true;
|
|
3833
|
-
if (!isRecord(options)) return false;
|
|
3834
|
-
if (options.baseUrl !== void 0 && typeof options.baseUrl !== "string") return false;
|
|
3835
|
-
if (options.paths === void 0) return true;
|
|
3836
|
-
if (!isRecord(options.paths)) return false;
|
|
3837
|
-
return Object.values(options.paths).every(
|
|
3838
|
-
(targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string")
|
|
3839
|
-
);
|
|
3840
|
-
}
|
|
3841
|
-
function unreadableConfigs(sources) {
|
|
3842
|
-
return sources.filter((source) => isProjectConfig(source.path) && readJsonc(source.content) === null).map((source) => source.path);
|
|
3843
|
-
}
|
|
3844
|
-
function expand(specifier, aliases) {
|
|
3845
|
-
const out = [];
|
|
3846
|
-
for (const [pattern, targets] of aliases) {
|
|
3847
|
-
const star = pattern.indexOf("*");
|
|
3848
|
-
if (star < 0) {
|
|
3849
|
-
if (specifier === pattern) out.push(...targets);
|
|
3850
|
-
continue;
|
|
3851
|
-
}
|
|
3852
|
-
const head = pattern.slice(0, star);
|
|
3853
|
-
const tail = pattern.slice(star + 1);
|
|
3854
|
-
if (!specifier.startsWith(head) || !specifier.endsWith(tail)) continue;
|
|
3855
|
-
const middle = specifier.slice(head.length, specifier.length - tail.length);
|
|
3856
|
-
out.push(...targets.map((target) => target.replace("*", middle)));
|
|
4272
|
+
const args = [ctx.snapshot, JSON.stringify(arrayPath), JSON.stringify(fallbackRows), ...ctx.slug ? [ctx.slug] : []];
|
|
4273
|
+
const rowsExpr = `${ctx.rows}(${args.join(", ")})`;
|
|
4274
|
+
if (rule.loop.kind === "attr") {
|
|
4275
|
+
source.appendRight(template.attrInsertAt + shift, ` ${rule.loop.attr(rowsExpr, names.row, names.index)}`);
|
|
3857
4276
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
if (found) return found;
|
|
4277
|
+
if (rule.loopKey) {
|
|
4278
|
+
const key = rule.loopKey(names.index);
|
|
4279
|
+
if (!template.keyAttr) source.appendRight(template.attrInsertAt + shift, ` ${key}`);
|
|
4280
|
+
else if (template.keyAttr.end > template.keyAttr.start)
|
|
4281
|
+
source.overwrite(template.keyAttr.start + shift, template.keyAttr.end + shift, key);
|
|
4282
|
+
else return null;
|
|
3865
4283
|
}
|
|
3866
|
-
|
|
4284
|
+
const indent = indentOf(ctx.content, template.range.start);
|
|
4285
|
+
const text = rule.loop.kind === "wrap" ? `${rule.loop.open(rowsExpr, names.row, names.index)}
|
|
4286
|
+
${indented(source.toString(), indent)}
|
|
4287
|
+
${indent}${rule.loop.close}` : source.toString();
|
|
4288
|
+
return {
|
|
4289
|
+
range: { start: template.range.start, end: rows[rows.length - 1].range.end },
|
|
4290
|
+
text,
|
|
4291
|
+
declarations,
|
|
4292
|
+
bindings: [...new Set(declarations.map((d) => d.path))],
|
|
4293
|
+
names
|
|
4294
|
+
};
|
|
3867
4295
|
}
|
|
3868
|
-
function
|
|
3869
|
-
const
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
const specifier = node.source?.value;
|
|
3874
|
-
if (typeof specifier !== "string") return;
|
|
3875
|
-
for (const entry of node.specifiers ?? []) {
|
|
3876
|
-
if (entry.importKind === "type") continue;
|
|
3877
|
-
const local = entry.local?.name;
|
|
3878
|
-
if (typeof local !== "string") continue;
|
|
3879
|
-
const kind = entry.type === "ImportDefaultSpecifier" ? "default" : entry.type === "ImportNamespaceSpecifier" ? "namespace" : "named";
|
|
3880
|
-
const imported = kind === "named" ? entry.imported?.name ?? local : null;
|
|
3881
|
-
out.set(local, { specifier, imported, kind });
|
|
3882
|
-
}
|
|
4296
|
+
function setLeaf(row, leaf, value) {
|
|
4297
|
+
const segments = leaf.split(".").flatMap((segment) => {
|
|
4298
|
+
const name = segment.replace(/\[\d+\]/g, "");
|
|
4299
|
+
const indices = [...segment.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]));
|
|
4300
|
+
return [...name ? [name] : [], ...indices];
|
|
3883
4301
|
});
|
|
3884
|
-
return
|
|
4302
|
+
if (segments.some((segment) => typeof segment === "string" && FORBIDDEN.has(segment))) return false;
|
|
4303
|
+
let cursor = row;
|
|
4304
|
+
for (const [at, segment] of segments.entries()) {
|
|
4305
|
+
const key = segment;
|
|
4306
|
+
const last = at === segments.length - 1;
|
|
4307
|
+
if (last) {
|
|
4308
|
+
cursor[key] = value;
|
|
4309
|
+
return true;
|
|
4310
|
+
}
|
|
4311
|
+
const held = Object.prototype.hasOwnProperty.call(cursor, key) ? cursor[key] : void 0;
|
|
4312
|
+
if (held === void 0 || typeof held !== "object" || held === null) {
|
|
4313
|
+
cursor[key] = typeof segments[at + 1] === "number" ? [] : emptyRow();
|
|
4314
|
+
}
|
|
4315
|
+
cursor = cursor[key];
|
|
4316
|
+
}
|
|
4317
|
+
return true;
|
|
4318
|
+
}
|
|
4319
|
+
var FORBIDDEN = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
4320
|
+
var emptyRow = () => /* @__PURE__ */ Object.create(null);
|
|
4321
|
+
function splitIndexed(arrayPath, path) {
|
|
4322
|
+
if (!path.startsWith(`${arrayPath}[`)) return null;
|
|
4323
|
+
const match = path.slice(arrayPath.length).match(/^\[(\d+)\]\.(.+)$/);
|
|
4324
|
+
return match ? { index: Number(match[1]), leaf: match[2] } : null;
|
|
3885
4325
|
}
|
|
3886
4326
|
|
|
3887
4327
|
// src/routes.ts
|
|
@@ -4077,13 +4517,15 @@ function declaringElements(roots, content) {
|
|
|
4077
4517
|
return out;
|
|
4078
4518
|
}
|
|
4079
4519
|
function helperLocals(parsed, dialect, file) {
|
|
4080
|
-
const out = { bcms: [], bcmsRows: [], bcmsLayout: [] };
|
|
4520
|
+
const out = { bcms: [], bcmsRows: [], bcmsRowsAs: [], bcmsImage: [], bcmsLayout: [] };
|
|
4081
4521
|
const helper = HELPER_PATH[dialect];
|
|
4082
4522
|
if (!helper) return out;
|
|
4083
4523
|
for (const [local, from] of importsIn(parsed, dialect)) {
|
|
4084
4524
|
if (!namesFile(file, from.specifier, helper)) continue;
|
|
4085
4525
|
if (from.imported === "bcms") out.bcms.push(local);
|
|
4086
4526
|
if (from.imported === "bcmsRows") out.bcmsRows.push(local);
|
|
4527
|
+
if (from.imported === "bcmsRowsAs") out.bcmsRowsAs.push(local);
|
|
4528
|
+
if (from.imported === "bcmsImage") out.bcmsImage.push(local);
|
|
4087
4529
|
if (from.imported === "bcmsLayout") out.bcmsLayout.push(local);
|
|
4088
4530
|
}
|
|
4089
4531
|
return out;
|
|
@@ -4115,19 +4557,29 @@ function snapshotLocal(parsed, dialect, slug, file) {
|
|
|
4115
4557
|
}
|
|
4116
4558
|
return null;
|
|
4117
4559
|
}
|
|
4560
|
+
function moduleBytes(roots, content) {
|
|
4561
|
+
let out = "";
|
|
4562
|
+
let at = 0;
|
|
4563
|
+
for (const root of roots) {
|
|
4564
|
+
if (root.type !== "element" || root.start < at) continue;
|
|
4565
|
+
out += content.slice(at, root.start);
|
|
4566
|
+
at = Math.max(at, root.end);
|
|
4567
|
+
}
|
|
4568
|
+
return out + content.slice(at);
|
|
4569
|
+
}
|
|
4118
4570
|
var identifier = (name) => `(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`;
|
|
4119
|
-
function elementReads(own, target, locals, snapshot) {
|
|
4571
|
+
function elementReads(own, target, locals, snapshot, viaRow) {
|
|
4120
4572
|
const quoted = escapeRegExp(JSON.stringify(target.path));
|
|
4121
4573
|
if (target.scope === "layout") {
|
|
4122
4574
|
return locals.bcmsLayout.some((name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${quoted}`).test(own));
|
|
4123
4575
|
}
|
|
4124
4576
|
const indexed = splitArrayPath(target.path);
|
|
4125
|
-
if (indexed) {
|
|
4577
|
+
if (viaRow && indexed) {
|
|
4126
4578
|
const leaf = escapeRegExp(JSON.stringify(indexed.leaf));
|
|
4127
4579
|
return locals.bcms.some((name) => new RegExp(`${identifier(name)}\\s*\\([^)]*,\\s*${leaf}`).test(own));
|
|
4128
4580
|
}
|
|
4129
4581
|
if (!snapshot) return false;
|
|
4130
|
-
return locals.bcms.some(
|
|
4582
|
+
return [...locals.bcms, ...locals.bcmsImage].some(
|
|
4131
4583
|
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${quoted}`).test(own)
|
|
4132
4584
|
);
|
|
4133
4585
|
}
|
|
@@ -4137,16 +4589,33 @@ function convertedHere(content, parsed, dialect, target, file) {
|
|
|
4137
4589
|
const snapshot = snapshotLocal(parsed, dialect, target.slug, file);
|
|
4138
4590
|
const shape = shapeOfPath(target.path);
|
|
4139
4591
|
const indexed = splitArrayPath(target.path);
|
|
4592
|
+
let keyed = false;
|
|
4593
|
+
let rows = false;
|
|
4140
4594
|
if (indexed) {
|
|
4141
4595
|
const array = escapeRegExp(JSON.stringify(indexed.array));
|
|
4142
|
-
const
|
|
4596
|
+
const reads = (names) => names.some(
|
|
4143
4597
|
(name) => snapshot !== null && new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${array}`).test(content)
|
|
4144
4598
|
);
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
)
|
|
4599
|
+
keyed = reads(locals.bcmsRowsAs);
|
|
4600
|
+
rows = keyed || reads(locals.bcmsRows);
|
|
4601
|
+
}
|
|
4602
|
+
let cached = null;
|
|
4603
|
+
const moduleRead = () => {
|
|
4604
|
+
if (indexed || snapshot === null) return false;
|
|
4605
|
+
cached ??= moduleBytes(parsed.roots, content);
|
|
4606
|
+
const quoted = escapeRegExp(JSON.stringify(target.path));
|
|
4607
|
+
return locals.bcms.some(
|
|
4608
|
+
(name) => new RegExp(`${identifier(name)}\\s*\\(\\s*${identifier(snapshot)}\\s*,\\s*${quoted}`).test(cached)
|
|
4609
|
+
);
|
|
4610
|
+
};
|
|
4611
|
+
return declaringElements(parsed.roots, content).some((element) => {
|
|
4612
|
+
if (element.scope !== target.scope) return false;
|
|
4613
|
+
if (element.path === target.path) {
|
|
4614
|
+
return elementReads(element.own, target, locals, snapshot, false) || moduleRead();
|
|
4615
|
+
}
|
|
4616
|
+
if (element.path !== shape) return false;
|
|
4617
|
+
return rows && (keyed || elementReads(element.own, target, locals, snapshot, true));
|
|
4618
|
+
});
|
|
4150
4619
|
}
|
|
4151
4620
|
|
|
4152
4621
|
// src/rewrite.ts
|
|
@@ -4212,7 +4681,7 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
4212
4681
|
addDeclaration(site.attrInsertAt, declaration(rewrite));
|
|
4213
4682
|
continue;
|
|
4214
4683
|
}
|
|
4215
|
-
if (kind === "richtext") {
|
|
4684
|
+
if (kind === "richtext" && !(site.mixed && site.textRange)) {
|
|
4216
4685
|
if (!isIntrinsic(site.tag)) continue;
|
|
4217
4686
|
if (rule.richtext && site.inner) {
|
|
4218
4687
|
source.remove(site.inner.start, site.inner.end);
|
|
@@ -4223,11 +4692,13 @@ function rewriteFile(content, dialect, rewrites, imports, extra = []) {
|
|
|
4223
4692
|
continue;
|
|
4224
4693
|
}
|
|
4225
4694
|
if (site.mixed && site.textRange) {
|
|
4695
|
+
const html = kind === "richtext" && rule.richtext ? rule.richtext(expression) : null;
|
|
4226
4696
|
const inner = rule.text ? rule.text(expression) : content.slice(site.textRange.start, site.textRange.end).trim();
|
|
4697
|
+
const attributes2 = attrsFor(path, kind, void 0, rewrite.scope);
|
|
4227
4698
|
source.overwrite(
|
|
4228
4699
|
site.textRange.start,
|
|
4229
4700
|
site.textRange.end,
|
|
4230
|
-
`<span ${
|
|
4701
|
+
html ? `<span ${attributes2} ${html}></span>` : `<span ${attributes2}>${inner}</span>`
|
|
4231
4702
|
);
|
|
4232
4703
|
continue;
|
|
4233
4704
|
}
|
|
@@ -4475,6 +4946,7 @@ function convert3(node, content) {
|
|
|
4475
4946
|
}
|
|
4476
4947
|
];
|
|
4477
4948
|
}
|
|
4949
|
+
if (node.type === "fragment") return (node.children ?? []).flatMap((child) => convert3(child, content));
|
|
4478
4950
|
if (node.type !== "element" && node.type !== "component" && node.type !== "custom-element") return [];
|
|
4479
4951
|
const tag = node.name ?? "";
|
|
4480
4952
|
const children = (node.children ?? []).flatMap((child) => convert3(child, content));
|
|
@@ -4794,7 +5266,7 @@ var parseFile4 = (dialect, content) => Promise.resolve(PARSE_FILE[dialect](conte
|
|
|
4794
5266
|
|
|
4795
5267
|
// src/canvas.ts
|
|
4796
5268
|
import MagicString3 from "magic-string";
|
|
4797
|
-
var SSR_DRAFT_RECIPE = "Astro renders this site at build time, so there is no server to answer the editor's draft request. Either mount <BcmsLiveBlocks client:load> from @bettercms-ai/astro on the pages whose body is a block tree, or switch
|
|
5269
|
+
var SSR_DRAFT_RECIPE = "Astro renders this site at build time, so there is no server to answer the editor's draft request. Either mount <BcmsLiveBlocks client:load> from @bettercms-ai/astro on the pages whose body is a block tree, or switch to `output: 'server'` with @astrojs/node (mode: 'standalone') \u2014 the bettercms() integration injects the draft routes for you \u2014 render pages from loadPage + loadComponents, mount <BcmsLive>, set BCMS_DRAFT_SECRET (BetterCMS hosting sets it for you), redeploy, and switch the project to 'Run as a server app'.";
|
|
4798
5270
|
var isNode = (value) => !!value && typeof value === "object" && typeof value.type === "string";
|
|
4799
5271
|
var FUNCTIONS3 = /* @__PURE__ */ new Set([
|
|
4800
5272
|
"FunctionDeclaration",
|
|
@@ -4845,16 +5317,16 @@ function functionBindings(fn) {
|
|
|
4845
5317
|
if (body?.type === "BlockStatement") statementNames2(body.body, out);
|
|
4846
5318
|
return out;
|
|
4847
5319
|
}
|
|
4848
|
-
function
|
|
5320
|
+
function stringLiterals2(node, out) {
|
|
4849
5321
|
if (!isNode(node)) {
|
|
4850
|
-
if (Array.isArray(node)) for (const child of node)
|
|
5322
|
+
if (Array.isArray(node)) for (const child of node) stringLiterals2(child, out);
|
|
4851
5323
|
return;
|
|
4852
5324
|
}
|
|
4853
5325
|
if (FUNCTIONS3.has(node.type)) return;
|
|
4854
5326
|
if (node.type === "StringLiteral" && typeof node.value === "string") out.add(node.value);
|
|
4855
5327
|
for (const key of Object.keys(node)) {
|
|
4856
5328
|
if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
|
|
4857
|
-
|
|
5329
|
+
stringLiterals2(node[key], out);
|
|
4858
5330
|
}
|
|
4859
5331
|
}
|
|
4860
5332
|
function usages(ast, local) {
|
|
@@ -4871,7 +5343,7 @@ function usages(ast, local) {
|
|
|
4871
5343
|
if (FUNCTIONS3.has(node.type)) {
|
|
4872
5344
|
nextScope = /* @__PURE__ */ new Set([...scope, ...functionBindings(node)]);
|
|
4873
5345
|
nextLiterals = /* @__PURE__ */ new Set();
|
|
4874
|
-
|
|
5346
|
+
stringLiterals2(node.body, nextLiterals);
|
|
4875
5347
|
}
|
|
4876
5348
|
if (node.type === "JSXElement") {
|
|
4877
5349
|
const name = node.openingElement?.name;
|
|
@@ -5201,12 +5673,28 @@ var SOURCE_EXTENSIONS = [
|
|
|
5201
5673
|
".erb",
|
|
5202
5674
|
".twig"
|
|
5203
5675
|
];
|
|
5676
|
+
var PROSE_EXTENSIONS = [".md", ".mdx", ".njk", ".hbs", ".ejs", ".liquid", ".erb", ".twig"];
|
|
5677
|
+
var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
|
|
5678
|
+
"pages",
|
|
5679
|
+
"content",
|
|
5680
|
+
"src",
|
|
5681
|
+
"app",
|
|
5682
|
+
"routes",
|
|
5683
|
+
"layouts",
|
|
5684
|
+
"components",
|
|
5685
|
+
"templates",
|
|
5686
|
+
"views",
|
|
5687
|
+
"_includes",
|
|
5688
|
+
"_layouts"
|
|
5689
|
+
]);
|
|
5204
5690
|
function isSourceCandidate(path) {
|
|
5205
5691
|
const lower = path.toLowerCase();
|
|
5206
5692
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
5207
5693
|
return false;
|
|
5208
5694
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
5209
|
-
|
|
5695
|
+
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
5696
|
+
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
5697
|
+
return lower.split("/").slice(0, -1).some((segment) => TEMPLATE_DIRS.has(segment));
|
|
5210
5698
|
}
|
|
5211
5699
|
|
|
5212
5700
|
// src/carry.ts
|
|
@@ -7024,6 +7512,7 @@ var ConvertError = class extends Error {
|
|
|
7024
7512
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
7025
7513
|
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7026
7514
|
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
7515
|
+
var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
|
|
7027
7516
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
7028
7517
|
const miss = { ok: false, reason: "PROP_TARGET_NOT_FOUND", edits: [] };
|
|
7029
7518
|
if (hop > MAX_HOPS) return { ok: false, reason: "PROP_DRILLED_DEEP", edits: [] };
|
|
@@ -7120,7 +7609,7 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7120
7609
|
const parsed = await parseOf(source.path);
|
|
7121
7610
|
declarations.set(
|
|
7122
7611
|
source.path,
|
|
7123
|
-
parsed === null ? /* @__PURE__ */ new Set() : parsed.error ? declaredByScan(source.content) : declaredInTree(parsed.roots, source.content)
|
|
7612
|
+
parsed === null ? isTolerantDialect(source.path) ? declaredByScan(source.content) : /* @__PURE__ */ new Set() : parsed.error ? declaredByScan(source.content) : declaredInTree(parsed.roots, source.content)
|
|
7124
7613
|
);
|
|
7125
7614
|
}
|
|
7126
7615
|
let declaredOnly = 0;
|
|
@@ -7152,6 +7641,36 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7152
7641
|
const key = keyOf(hit.route, hit.scope ?? "page", hit.path);
|
|
7153
7642
|
if (!locatedByKey.has(key)) locatedByKey.set(key, hit);
|
|
7154
7643
|
}
|
|
7644
|
+
const imageUnresolved = /* @__PURE__ */ new Map();
|
|
7645
|
+
for (const miss of unlocated) {
|
|
7646
|
+
const key = keyOf(miss.route, miss.scope ?? "page", miss.path);
|
|
7647
|
+
const target = targets.get(key);
|
|
7648
|
+
if (!target || target.kind !== "image" || !target.fallback || locatedByKey.has(key)) continue;
|
|
7649
|
+
const hosts = [];
|
|
7650
|
+
for (const file of pageFilesFor(target.route, contents.keys())) {
|
|
7651
|
+
const parsed = await parseOf(file);
|
|
7652
|
+
const dialect = dialectOf(file, contents.get(file) ?? "");
|
|
7653
|
+
const bindings = parsed && dialect ? assetCandidates(parsed, dialect, target.fallback) : [];
|
|
7654
|
+
if (bindings.length > 0) hosts.push({ file, bindings });
|
|
7655
|
+
}
|
|
7656
|
+
const only = hosts.length === 1 && hosts[0].bindings.length === 1 ? hosts[0] : null;
|
|
7657
|
+
if (only) {
|
|
7658
|
+
locatedByKey.set(key, {
|
|
7659
|
+
route: target.route,
|
|
7660
|
+
scope: target.scope,
|
|
7661
|
+
path: target.path,
|
|
7662
|
+
kind: target.kind,
|
|
7663
|
+
original: target.literal,
|
|
7664
|
+
files: [only.file]
|
|
7665
|
+
});
|
|
7666
|
+
continue;
|
|
7667
|
+
}
|
|
7668
|
+
const named = hosts.flatMap((host) => host.bindings.map((binding) => `${host.file}:${binding}`));
|
|
7669
|
+
imageUnresolved.set(
|
|
7670
|
+
key,
|
|
7671
|
+
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.`
|
|
7672
|
+
);
|
|
7673
|
+
}
|
|
7155
7674
|
const sharingPath = /* @__PURE__ */ new Map();
|
|
7156
7675
|
for (const [key, hit] of locatedByKey) {
|
|
7157
7676
|
const held = targets.get(key);
|
|
@@ -7195,6 +7714,11 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7195
7714
|
declaredRow(target, false, byHand);
|
|
7196
7715
|
continue;
|
|
7197
7716
|
}
|
|
7717
|
+
const unresolved = imageUnresolved.get(key);
|
|
7718
|
+
if (unresolved !== void 0) {
|
|
7719
|
+
pend2(target, "IMAGE_ASSET_UNRESOLVED", void 0, unresolved);
|
|
7720
|
+
continue;
|
|
7721
|
+
}
|
|
7198
7722
|
pend2(target, miss.reason === "no-original" ? "NO_ORIGINAL" : "NOT_IN_SOURCE");
|
|
7199
7723
|
}
|
|
7200
7724
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -7252,7 +7776,14 @@ async function convertSources(briefIn, sources, options) {
|
|
|
7252
7776
|
const worst = outcomes.find((o) => o !== "rewritten" && o !== "declared");
|
|
7253
7777
|
if (worst) {
|
|
7254
7778
|
const file = hit.files[outcomes.indexOf(worst)];
|
|
7255
|
-
pend2(
|
|
7779
|
+
pend2(
|
|
7780
|
+
target,
|
|
7781
|
+
worst,
|
|
7782
|
+
file,
|
|
7783
|
+
// The reason says the source does not render it and the file name says where it was
|
|
7784
|
+
// found; without the second half the two read as a contradiction. @see unplacedReason
|
|
7785
|
+
worst === "NOT_IN_SOURCE" && file ? `${file} contains this copy, but no element renders it on its own.` : void 0
|
|
7786
|
+
);
|
|
7256
7787
|
} else if (outcomes.every((o) => o === "declared")) {
|
|
7257
7788
|
const witness = await witnessed(target);
|
|
7258
7789
|
const byHand = witness === null ? declaredByHand(target, hit.files) : null;
|
|
@@ -7363,7 +7894,11 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7363
7894
|
if (pendingTargets.length === 0) return empty;
|
|
7364
7895
|
if (!dialect) {
|
|
7365
7896
|
if (isTolerantDialect(file)) {
|
|
7366
|
-
const
|
|
7897
|
+
const already = declaredByScan(content);
|
|
7898
|
+
const remaining = pendingTargets.filter((target) => !covers(already, target));
|
|
7899
|
+
for (const target of pendingTargets) if (covers(already, target)) outcomes.set(target.key, "declared");
|
|
7900
|
+
if (remaining.length === 0) return { ...empty, dialect: null, outcomes };
|
|
7901
|
+
const tolerantLiterals = [...new Set(remaining.map((t) => t.literal))];
|
|
7367
7902
|
const tolerantSites = findSitesTolerant(content, tolerantLiterals);
|
|
7368
7903
|
if (tolerantSites.length > 0) {
|
|
7369
7904
|
return plan(
|
|
@@ -7373,7 +7908,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7373
7908
|
// argument is behind `!parsed.error` or `tier !== 2`. Passing the writing dialect keeps
|
|
7374
7909
|
// the parameter honest rather than widening its type for one caller.
|
|
7375
7910
|
"html",
|
|
7376
|
-
|
|
7911
|
+
remaining,
|
|
7377
7912
|
tolerantSites,
|
|
7378
7913
|
void 0,
|
|
7379
7914
|
// no parse, no import point: tier 2 declares, it does not read
|
|
@@ -7386,7 +7921,7 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7386
7921
|
);
|
|
7387
7922
|
}
|
|
7388
7923
|
}
|
|
7389
|
-
for (const target of pendingTargets) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
|
|
7924
|
+
for (const target of pendingTargets) if (!outcomes.has(target.key)) outcomes.set(target.key, "DIALECT_UNSUPPORTED");
|
|
7390
7925
|
return { ...empty, dialect: null, outcomes };
|
|
7391
7926
|
}
|
|
7392
7927
|
const literals = [...new Set(pendingTargets.map((t) => t.literal))];
|
|
@@ -7423,8 +7958,8 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7423
7958
|
return { ...empty, tier: 3, outcomes, dialect: null };
|
|
7424
7959
|
}
|
|
7425
7960
|
}
|
|
7426
|
-
if (sites.length === 0) {
|
|
7427
|
-
const reason = tier1.error
|
|
7961
|
+
if (sites.length === 0 && tier1.error) {
|
|
7962
|
+
const reason = tier1.error.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
|
|
7428
7963
|
for (const target of pendingTargets) outcomes.set(target.key, reason);
|
|
7429
7964
|
return { ...empty, tier, outcomes, dialect };
|
|
7430
7965
|
}
|
|
@@ -7442,9 +7977,24 @@ async function convertFile(file, content, targets, parsed, declared, llmFallback
|
|
|
7442
7977
|
tier1.error ? [] : declarationsIn(tier1.roots, content)
|
|
7443
7978
|
);
|
|
7444
7979
|
}
|
|
7980
|
+
function unplacedReason(parsed, literal) {
|
|
7981
|
+
if (!parsed || parsed.error) return "NOT_IN_SOURCE";
|
|
7982
|
+
if (parsed.script && norm(parsed.script.code).includes(literal)) return "IN_EXPRESSION";
|
|
7983
|
+
let found = false;
|
|
7984
|
+
const visit = (node) => {
|
|
7985
|
+
if (found) return;
|
|
7986
|
+
if (node.type === "expr") {
|
|
7987
|
+
if (norm(node.value).includes(literal)) found = true;
|
|
7988
|
+
return;
|
|
7989
|
+
}
|
|
7990
|
+
if (node.type === "element" && !node.raw) for (const child of node.children) visit(child);
|
|
7991
|
+
};
|
|
7992
|
+
for (const root of parsed.roots) visit(root);
|
|
7993
|
+
return found ? "IN_EXPRESSION" : "NOT_IN_SOURCE";
|
|
7994
|
+
}
|
|
7445
7995
|
async function plan(file, content, dialect, targets, sites, importAt, outcomes, tier, parsed, drill, declarations) {
|
|
7446
7996
|
const writing = tier === 2 ? "html" : dialect;
|
|
7447
|
-
const missing = tier !== 2 ?
|
|
7997
|
+
const missing = (literal) => tier !== 2 ? unplacedReason(parsed, literal) : parsed.error?.code === "DIALECT_UNSUPPORTED" ? "DIALECT_UNSUPPORTED" : "PARSE_ERROR";
|
|
7448
7998
|
const taken = parsed.error ? /* @__PURE__ */ new Set() : topLevelBindings(dialect, parsed, content);
|
|
7449
7999
|
const slugs = [...new Set(targets.map((t) => t.slug))].sort();
|
|
7450
8000
|
const preset = /* @__PURE__ */ new Map();
|
|
@@ -7453,7 +8003,9 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7453
8003
|
for (const [key, held] of [
|
|
7454
8004
|
["helper:bcms", locals.bcms],
|
|
7455
8005
|
["helper:bcmsRows", locals.bcmsRows],
|
|
7456
|
-
["helper:bcmsLayout", locals.bcmsLayout]
|
|
8006
|
+
["helper:bcmsLayout", locals.bcmsLayout],
|
|
8007
|
+
["helper:bcmsRowsAs", locals.bcmsRowsAs],
|
|
8008
|
+
["helper:bcmsImage", locals.bcmsImage]
|
|
7457
8009
|
]) {
|
|
7458
8010
|
if (held[0]) preset.set(key, held[0]);
|
|
7459
8011
|
}
|
|
@@ -7466,6 +8018,8 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7466
8018
|
{ key: "helper:bcms", name: "bcms" },
|
|
7467
8019
|
{ key: "helper:bcmsRows", name: "bcmsRows" },
|
|
7468
8020
|
{ key: "helper:bcmsLayout", name: "bcmsLayout" },
|
|
8021
|
+
{ key: "helper:bcmsRowsAs", name: "bcmsRowsAs" },
|
|
8022
|
+
{ key: "helper:bcmsImage", name: "bcmsImage" },
|
|
7469
8023
|
// Keyed by the SLUG: two slugs that spell one identifier are still two pages. @see scope.ts
|
|
7470
8024
|
...slugs.map((slug) => ({ key: `slug:${slug}`, name: identifierFor2(slug) }))
|
|
7471
8025
|
].filter((entry) => !preset.has(entry.key));
|
|
@@ -7483,7 +8037,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7483
8037
|
for (const [literal, sharing] of byLiteral) {
|
|
7484
8038
|
const forLiteral = sites.filter((s) => s.literal === literal);
|
|
7485
8039
|
if (forLiteral.length === 0) {
|
|
7486
|
-
for (const target of sharing) outcomes.set(target.key, missing);
|
|
8040
|
+
for (const target of sharing) outcomes.set(target.key, missing(literal));
|
|
7487
8041
|
continue;
|
|
7488
8042
|
}
|
|
7489
8043
|
const assigned = sharing.length === 1 ? /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]) : resolvePositionally(sharing, forLiteral);
|
|
@@ -7497,7 +8051,7 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7497
8051
|
if (rewritable.length === 0) {
|
|
7498
8052
|
outcomes.set(
|
|
7499
8053
|
target.key,
|
|
7500
|
-
mine.some((s) => s.partial) ? "SUBSTRING_ONLY" : mine.some((s) => s.where === "script" || s.where === "comment") ? "IN_SCRIPT_OR_COMMENT" : missing
|
|
8054
|
+
mine.some((s) => s.partial) ? "SUBSTRING_ONLY" : mine.some((s) => s.where === "script" || s.where === "comment") ? "IN_SCRIPT_OR_COMMENT" : missing(literal)
|
|
7501
8055
|
);
|
|
7502
8056
|
continue;
|
|
7503
8057
|
}
|
|
@@ -7572,7 +8126,11 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7572
8126
|
// `original` has been through `firstBlockText`, so it is the text a reader sees with
|
|
7573
8127
|
// the markup already stripped. Keeping that as the in-code fallback would silently
|
|
7574
8128
|
// flatten every bold and link the moment a build ran against a stub snapshot.
|
|
7575
|
-
|
|
8129
|
+
//
|
|
8130
|
+
// 🔴 AND ONLY WHEN THE ELEMENT'S SUBTREE IS THE VALUE. A value that is one TEXT NODE of
|
|
8131
|
+
// a mixed element gets a wrapper of its own, so taking the element's inner range as its
|
|
8132
|
+
// fallback swallowed the `<em>` beside it — the wrapper then rendered the sibling twice.
|
|
8133
|
+
fallback: target.kind === "richtext" && site.inner && !(site.mixed && site.textRange) ? content.slice(site.inner.start, site.inner.end).trim() : target.fallback,
|
|
7576
8134
|
snapshot: snapshotName(target.slug),
|
|
7577
8135
|
slug: target.slug,
|
|
7578
8136
|
scope: target.scope,
|
|
@@ -7653,10 +8211,48 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7653
8211
|
const collided = new Set(overlapping(kept).map((r) => r.key));
|
|
7654
8212
|
const applied = kept.filter((r) => !collided.has(r.key));
|
|
7655
8213
|
for (const key of collided) outcomes.set(key, "AMBIGUOUS_LITERAL");
|
|
7656
|
-
|
|
8214
|
+
const data = bindDataLiterals(
|
|
8215
|
+
{
|
|
8216
|
+
content,
|
|
8217
|
+
parsed,
|
|
8218
|
+
dialect: writing,
|
|
8219
|
+
rowsAs: helperName("bcmsRowsAs"),
|
|
8220
|
+
read: helperName("bcms"),
|
|
8221
|
+
snapshotName,
|
|
8222
|
+
slug: params?.slug ?? null
|
|
8223
|
+
},
|
|
8224
|
+
targets.filter((t) => {
|
|
8225
|
+
const outcome = outcomes.get(t.key);
|
|
8226
|
+
return t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
|
|
8227
|
+
}).map((t) => ({
|
|
8228
|
+
key: t.key,
|
|
8229
|
+
path: t.path,
|
|
8230
|
+
kind: t.kind,
|
|
8231
|
+
literal: t.literal,
|
|
8232
|
+
slug: t.slug
|
|
8233
|
+
}))
|
|
8234
|
+
);
|
|
8235
|
+
for (const key of data.bound) outcomes.set(key, "rewritten");
|
|
8236
|
+
for (const refusal of data.refused) outcomes.set(refusal.key, refusal.reason);
|
|
8237
|
+
for (const declaration2 of data.declarations) {
|
|
8238
|
+
dynamic.push({ file, prop: null, path: declaration2.path, callSites: [{ file, literal: declaration2.literal }] });
|
|
8239
|
+
}
|
|
8240
|
+
const images = bindImages(
|
|
8241
|
+
{ content, parsed, dialect: writing, image: helperName("bcmsImage"), snapshotName, slug: params?.slug ?? null },
|
|
8242
|
+
targets.filter((t) => {
|
|
8243
|
+
const outcome = outcomes.get(t.key);
|
|
8244
|
+
return t.kind === "image" && t.scope === "page" && (outcome === void 0 || UNPLACED.has(outcome));
|
|
8245
|
+
}).map((t) => ({ key: t.key, path: t.path, kind: t.kind, original: t.fallback, slug: t.slug }))
|
|
8246
|
+
);
|
|
8247
|
+
for (const key of images.bound) outcomes.set(key, "rewritten");
|
|
8248
|
+
for (const refusal of images.refused) outcomes.set(refusal.key, refusal.reason);
|
|
8249
|
+
const lanes = [...data.splices, ...images.splices];
|
|
8250
|
+
if (applied.length === 0 && lanes.length === 0) {
|
|
7657
8251
|
return { tier, content: null, outcomes, bindings: [], occurrences: { located, rewritten: 0 }, dialect: writing, dynamic: [], componentEdits, snapshots: [] };
|
|
7658
8252
|
}
|
|
7659
|
-
const readSlugs = [
|
|
8253
|
+
const readSlugs = [
|
|
8254
|
+
.../* @__PURE__ */ new Set([...applied.filter((r) => r.scope === "page").map((r) => r.slug), ...data.slugs, ...images.slugs])
|
|
8255
|
+
].sort();
|
|
7660
8256
|
const helper = HELPER_PATH[writing];
|
|
7661
8257
|
const rewritten = rewriteFile(content, writing, applied, {
|
|
7662
8258
|
// html reads nothing at build time, so it imports nothing: `injectFields` writes its values
|
|
@@ -7677,10 +8273,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
7677
8273
|
{ name: "bcms", alias: helperName("bcms") },
|
|
7678
8274
|
{ name: "bcmsRows", alias: helperName("bcmsRows") }
|
|
7679
8275
|
] : [],
|
|
7680
|
-
...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : []
|
|
8276
|
+
...applied.some((r) => r.scope === "layout") ? [{ name: "bcmsLayout", alias: helperName("bcmsLayout") }] : [],
|
|
8277
|
+
...data.usesRows ? [{ name: "bcmsRowsAs", alias: helperName("bcmsRowsAs") }] : [],
|
|
8278
|
+
...data.usesRead ? [{ name: "bcms", alias: helperName("bcms") }] : [],
|
|
8279
|
+
...images.splices.length > 0 ? [{ name: "bcmsImage", alias: helperName("bcmsImage") }] : []
|
|
7681
8280
|
].filter((helper2, at, all) => all.findIndex((h) => h.name === helper2.name) === at).filter((helper2) => !preset.has(`helper:${helper2.name}`)),
|
|
7682
8281
|
at: importAt
|
|
7683
|
-
}, params?.splices ?? []);
|
|
8282
|
+
}, [...params?.splices ?? [], ...lanes]);
|
|
7684
8283
|
return {
|
|
7685
8284
|
tier,
|
|
7686
8285
|
content: rewritten,
|