@bettercms-ai/convert 0.11.0 → 0.12.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 +31 -0
- package/README.md +54 -0
- package/dist/cli.js +190 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.js +192 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @bettercms-ai/convert
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a243594: Every pending path leaves with an exact fix, and a locator resolves each path sharing a literal.
|
|
8
|
+
|
|
9
|
+
`paths.pending[]` rows now carry `fix: { file, line, col?, snippet, action, why? }` — one action
|
|
10
|
+
out of a closed set (`wrap-span`, `declare-attr`, `bind-expression`, `declare-richtext`,
|
|
11
|
+
`bind-data`, `manual`), so "rerun until nothing is pending" is a loop an agent can finish instead
|
|
12
|
+
of a list of category names. A literal one element renders on several routes is named as shared
|
|
13
|
+
chrome rather than sent for an attribute that could only ever be one route's.
|
|
14
|
+
|
|
15
|
+
Positional resolution is now per PATH rather than per literal: one path with no locator used to
|
|
16
|
+
refuse the whole literal, so a page whose rows carried locators converted none of them.
|
|
17
|
+
|
|
18
|
+
Two paths whose locators cannot be told apart now place NEITHER. Resolution counts the targets
|
|
19
|
+
wanting each element before it assigns any, so it no longer depends on brief order: `plan` runs
|
|
20
|
+
per file, and claiming greedily let the first of two indistinguishable paths win in every file —
|
|
21
|
+
writing one path's `data-bcms-field` onto the other's element and reporting it bound.
|
|
22
|
+
|
|
23
|
+
An action that edits an element (`wrap-span`, `declare-attr`, `declare-richtext`) is never issued
|
|
24
|
+
at a position inside a frontmatter array or a `<script>`; there is no element there, so the fix is
|
|
25
|
+
`bind-data` on the row the copy actually lives in. A fix for a dynamic route names the template
|
|
26
|
+
that generates it instead of an empty file. Files that talk ABOUT the site — `*.test.*`,
|
|
27
|
+
`*.spec.*`, `__tests__/`, `__mocks__/`, root `*.config.*` — are no longer read, so a test
|
|
28
|
+
asserting on the hero line stops holding an already-declared path pending.
|
|
29
|
+
|
|
30
|
+
Fix positions are measured against the bytes the run WRITES, not the ones it read — the same run
|
|
31
|
+
inserts a helper import above most pending lines, so a receipt minted from the originals named a
|
|
32
|
+
line one or more short of the element on 616 of 948 Shatter rows.
|
|
33
|
+
|
|
3
34
|
## 0.11.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -184,6 +184,60 @@ the receipt's arithmetic is checked, not hoped for:
|
|
|
184
184
|
paths.declared === paths.rewritten + paths.alreadyDeclared + paths.pending.length
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
### What pending means — every row carries the edit, not the diagnosis
|
|
188
|
+
|
|
189
|
+
A reason is a diagnosis and nobody can act on one. So every `paths.pending[]` row also carries a
|
|
190
|
+
`fix`: the file, the 1-based line and column, the bytes that are there now, and ONE action out of
|
|
191
|
+
a closed set. That is what makes "run it again until nothing is pending" a loop an agent can
|
|
192
|
+
actually finish — and what `get_next_steps` prints, one line per path.
|
|
193
|
+
|
|
194
|
+
```jsonc
|
|
195
|
+
{ "route": "/about", "scope": "page", "path": "p-founded-in-2019-in", "kind": "text",
|
|
196
|
+
"reason": "DIALECT_UNSUPPORTED",
|
|
197
|
+
"fix": { "file": "src/data/facts.js", "line": 3, "col": 25,
|
|
198
|
+
"snippet": "export const founded = \"Founded in 2019 in Bristol.\";",
|
|
199
|
+
"action": "bind-data",
|
|
200
|
+
"why": "src/data/facts.js holds this copy as data — bind the element that renders it." } }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| action | what to type |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `wrap-span` | the value shares its element with something else, or is rendered in a position no attribute reaches — give it a `<span data-bcms-field="…">` of its own. |
|
|
206
|
+
| `declare-attr` | the element is there and nothing said which path it is — add `data-bcms-field` to it at `file:line`. |
|
|
207
|
+
| `declare-richtext` | the container is fed markup (`set:html`, `dangerouslySetInnerHTML`) — bind it with the richtext helper. |
|
|
208
|
+
| `bind-expression` | the value is computed in the markup (a ternary, a template literal) — read the path there instead. |
|
|
209
|
+
| `bind-data` | the copy lives in a data module, a frontmatter array or a content-collection entry — bind THAT field, and the element that renders it. |
|
|
210
|
+
| `manual` | none of the above is true; `why` says what a human has to decide. |
|
|
211
|
+
|
|
212
|
+
Reason → action, and the two places the answer is not the obvious one:
|
|
213
|
+
|
|
214
|
+
| reason | action |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `AMBIGUOUS_LITERAL`, one route | `declare-attr` — `why` names every candidate file. |
|
|
217
|
+
| `AMBIGUOUS_LITERAL`, **several routes** | `manual` — one element cannot carry N routes' bindings. This is SHARED CHROME the derive lane did not promote: a Layout field is one value for the whole site, and an attribute here would bind one route and silently freeze the rest. |
|
|
218
|
+
| `IN_EXPRESSION` in the markup | `bind-expression`, or `declare-richtext` for a richtext value. |
|
|
219
|
+
| `IN_EXPRESSION` **in the frontmatter or a `<script>`** | `bind-data` — the copy is a row in the file's own data, and the array is what gets bound, not the `{l.label}` that reads it. |
|
|
220
|
+
| `DIALECT_UNSUPPORTED` on `.md`/`.mdx` | `bind-data` — a content-collection body is the entry's `document` field. |
|
|
221
|
+
| `NOT_IN_SOURCE` / `NO_ORIGINAL` | `bind-data` when the copy is in a data module, else `manual`: the built page formats it and the source holds the input. |
|
|
222
|
+
| `PROP_TARGET_NOT_FOUND`, `PROP_DRILLED_DEEP`, `SUBSTRING_ONLY` | `wrap-span`. |
|
|
223
|
+
| `REPEATER_AMBIGUOUS`, `REPEATER_FIXED_LENGTH` | `declare-attr` on the row's own element. |
|
|
224
|
+
| `IMAGE_ASSET_UNRESOLVED`, `IN_DATA_FILE` | `bind-data`. |
|
|
225
|
+
| `SUBTREE_NOT_HTML` | `declare-richtext`. |
|
|
226
|
+
| `KIND_MISMATCH`, `IN_SCRIPT_OR_COMMENT`, `DYNAMIC_PARAMS_UNAVAILABLE`, `BRIEF_META_UNPLACED`, `PARSE_ERROR`, `TIER3_UNVERIFIABLE`, `CANVAS_BRIDGE_MANUAL`, `SSR_DRAFT_ROUTE` | `manual`, with the reason quoted in `why` — read the row's `message` for what blocked it. |
|
|
227
|
+
|
|
228
|
+
**A file that talks ABOUT the site is not a source file.** A path is rewritten only when every
|
|
229
|
+
located occurrence was, so a test asserting on the hero line, or a build config quoting the font
|
|
230
|
+
stack, used to hold an already-declared path pending. `*.test.*`, `*.spec.*`, `__tests__/`,
|
|
231
|
+
`__mocks__/` and root-level `*.config.*` are never opened, alongside `node_modules/` and `dist/`.
|
|
232
|
+
A data module under `src/` — `src/site.config.ts` — is not a build config and stays bindable.
|
|
233
|
+
|
|
234
|
+
**`--xray` and the receipt exclude the same things.** The xray measures a BUILT site's visible
|
|
235
|
+
copy and the receipt measures the brief's paths in SOURCE, so they never have the same
|
|
236
|
+
denominator — but neither counts a decorative glyph, a `<script>` or a `<style>` as copy
|
|
237
|
+
(`isDecorativeGlyph` is the one reader), and the two lane rows `CANVAS_BRIDGE_MANUAL` and
|
|
238
|
+
`SSR_DRAFT_ROUTE` ride `paths.pending` without entering the arithmetic above. A path the receipt
|
|
239
|
+
calls pending is copy the xray will still count as unaddressed; a path it calls rewritten is not.
|
|
240
|
+
|
|
187
241
|
A path counts as rewritten only when EVERY located occurrence of it was. A value rendered by both
|
|
188
242
|
the home page and a shared header is two places a reader sees it, and converting one of them reads
|
|
189
243
|
as a finished job.
|
package/dist/cli.js
CHANGED
|
@@ -5312,6 +5312,138 @@ function assertReceipt(receipt) {
|
|
|
5312
5312
|
return receipt;
|
|
5313
5313
|
}
|
|
5314
5314
|
|
|
5315
|
+
// src/fix.ts
|
|
5316
|
+
var MAX_SNIPPET = 160;
|
|
5317
|
+
var MAX_WORDS = 12;
|
|
5318
|
+
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5319
|
+
function findLiteral(content, literal) {
|
|
5320
|
+
const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
|
|
5321
|
+
if (words.length === 0) return null;
|
|
5322
|
+
const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
|
|
5323
|
+
const match = pattern.exec(content);
|
|
5324
|
+
return match ? { index: match.index, length: match[0].length } : null;
|
|
5325
|
+
}
|
|
5326
|
+
function placeOf(content, index) {
|
|
5327
|
+
const before = content.slice(0, index);
|
|
5328
|
+
const line = before.split("\n").length;
|
|
5329
|
+
const start = before.lastIndexOf("\n") + 1;
|
|
5330
|
+
const end = content.indexOf("\n", index);
|
|
5331
|
+
return {
|
|
5332
|
+
line,
|
|
5333
|
+
col: index - start + 1,
|
|
5334
|
+
snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
function fileHead(content) {
|
|
5338
|
+
return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
|
|
5339
|
+
}
|
|
5340
|
+
var isMarkdown = (file) => /\.mdx?$/i.test(file);
|
|
5341
|
+
var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
|
|
5342
|
+
function inDataRegion(content, index) {
|
|
5343
|
+
if (content.startsWith("---")) {
|
|
5344
|
+
const close = content.indexOf("\n---", 3);
|
|
5345
|
+
if (close >= 0 && index < close) return true;
|
|
5346
|
+
}
|
|
5347
|
+
for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
5348
|
+
const start = match.index + match[0].indexOf(">") + 1;
|
|
5349
|
+
if (index >= start && index < start + match[1].length) return true;
|
|
5350
|
+
}
|
|
5351
|
+
return false;
|
|
5352
|
+
}
|
|
5353
|
+
var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
|
|
5354
|
+
"wrap-span",
|
|
5355
|
+
"declare-attr",
|
|
5356
|
+
"declare-richtext"
|
|
5357
|
+
]);
|
|
5358
|
+
function actionFor(reason, input, file, inData) {
|
|
5359
|
+
const chosen = reasonAction(reason, input, file, inData);
|
|
5360
|
+
if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
|
|
5361
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5362
|
+
}
|
|
5363
|
+
return chosen;
|
|
5364
|
+
}
|
|
5365
|
+
function reasonAction(reason, input, file, inData) {
|
|
5366
|
+
const richtext = input.kind === "richtext" || input.inline === true;
|
|
5367
|
+
switch (reason) {
|
|
5368
|
+
case "AMBIGUOUS_LITERAL":
|
|
5369
|
+
if (input.routes > 1) {
|
|
5370
|
+
return {
|
|
5371
|
+
action: "manual",
|
|
5372
|
+
why: `${file} renders this value on ${input.routes} routes, so it is shared chrome rather than page copy: promote it to the project Layout (a Layout field is one value for the whole site) and rerun, or give each route its own element.`
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
return {
|
|
5376
|
+
action: "declare-attr",
|
|
5377
|
+
why: input.candidates.length > 1 ? `This copy appears in ${input.candidates.join(", ")}; declare the one this route renders.` : "Two paths want this element and nothing says which \u2014 declare the right one by hand."
|
|
5378
|
+
};
|
|
5379
|
+
case "REPEATER_AMBIGUOUS":
|
|
5380
|
+
case "REPEATER_FIXED_LENGTH":
|
|
5381
|
+
return {
|
|
5382
|
+
action: "declare-attr",
|
|
5383
|
+
why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
|
|
5384
|
+
};
|
|
5385
|
+
case "IN_EXPRESSION":
|
|
5386
|
+
if (inData) {
|
|
5387
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5388
|
+
}
|
|
5389
|
+
return richtext ? { action: "declare-richtext", why: "The container is fed markup \u2014 bind it with the richtext helper." } : { action: "bind-expression", why: "Replace the computed value with the helper's read for this path." };
|
|
5390
|
+
case "DIALECT_UNSUPPORTED":
|
|
5391
|
+
if (isMarkdown(file)) {
|
|
5392
|
+
return {
|
|
5393
|
+
action: "bind-data",
|
|
5394
|
+
why: "This is a content-collection body: bind the element that renders it and make the entry the source."
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
return isDataModule(file) ? { action: "bind-data", why: `${file} holds this copy as data \u2014 bind the element that renders it.` } : { action: "manual", why: `No parser here reads ${file}, so the declaration has to be written by hand.` };
|
|
5398
|
+
case "NOT_IN_SOURCE":
|
|
5399
|
+
case "NO_ORIGINAL":
|
|
5400
|
+
return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
|
|
5401
|
+
action: "manual",
|
|
5402
|
+
why: "The built page renders this and no source file holds it verbatim; find what formats it."
|
|
5403
|
+
};
|
|
5404
|
+
case "IN_DATA_FILE":
|
|
5405
|
+
case "IMAGE_ASSET_UNRESOLVED":
|
|
5406
|
+
return { action: "bind-data", why: "Bind the data field this value comes from." };
|
|
5407
|
+
case "PROP_TARGET_NOT_FOUND":
|
|
5408
|
+
case "PROP_DRILLED_DEEP":
|
|
5409
|
+
return {
|
|
5410
|
+
action: "wrap-span",
|
|
5411
|
+
why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
|
|
5412
|
+
};
|
|
5413
|
+
case "SUBSTRING_ONLY":
|
|
5414
|
+
return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
|
|
5415
|
+
case "SUBTREE_NOT_HTML":
|
|
5416
|
+
return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
|
|
5417
|
+
default:
|
|
5418
|
+
return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
function fixFor(row, input) {
|
|
5422
|
+
const file = row.file ?? input.candidates[0] ?? "";
|
|
5423
|
+
const content = input.contents.get(file);
|
|
5424
|
+
const at = content ? findLiteral(content, input.literal) : null;
|
|
5425
|
+
const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
|
|
5426
|
+
const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
|
|
5427
|
+
return {
|
|
5428
|
+
file,
|
|
5429
|
+
line: row.line ?? place.line,
|
|
5430
|
+
...place.col !== void 0 ? { col: place.col } : {},
|
|
5431
|
+
snippet: place.snippet,
|
|
5432
|
+
action,
|
|
5433
|
+
...why ? { why } : {}
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
function routeCounts(pending) {
|
|
5437
|
+
const routes = /* @__PURE__ */ new Map();
|
|
5438
|
+
for (const row of pending) {
|
|
5439
|
+
const id = `${row.scope}|${row.path}`;
|
|
5440
|
+
const held = routes.get(id) ?? /* @__PURE__ */ new Set();
|
|
5441
|
+
held.add(row.route);
|
|
5442
|
+
routes.set(id, held);
|
|
5443
|
+
}
|
|
5444
|
+
return new Map([...routes].map(([id, set]) => [id, set.size]));
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5315
5447
|
// src/sites/html.ts
|
|
5316
5448
|
import { parse as parseHtml } from "parse5";
|
|
5317
5449
|
var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
|
|
@@ -6428,7 +6560,9 @@ var SKIP_DIRS = [
|
|
|
6428
6560
|
".vercel/",
|
|
6429
6561
|
".cache/",
|
|
6430
6562
|
"coverage/",
|
|
6431
|
-
"vendor/"
|
|
6563
|
+
"vendor/",
|
|
6564
|
+
"__tests__/",
|
|
6565
|
+
"__mocks__/"
|
|
6432
6566
|
];
|
|
6433
6567
|
var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
|
|
6434
6568
|
var SOURCE_EXTENSIONS = [
|
|
@@ -6466,11 +6600,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
|
|
|
6466
6600
|
"_includes",
|
|
6467
6601
|
"_layouts"
|
|
6468
6602
|
]);
|
|
6603
|
+
var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
|
|
6469
6604
|
function isSourceCandidate(path) {
|
|
6470
6605
|
const lower = path.toLowerCase();
|
|
6471
6606
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
6472
6607
|
return false;
|
|
6473
6608
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6609
|
+
if (isAboutTheSite(lower)) return false;
|
|
6474
6610
|
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
6475
6611
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
6476
6612
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
@@ -8411,7 +8547,7 @@ var ConvertError = class extends Error {
|
|
|
8411
8547
|
code;
|
|
8412
8548
|
};
|
|
8413
8549
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
8414
|
-
var
|
|
8550
|
+
var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8415
8551
|
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
8416
8552
|
var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
|
|
8417
8553
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
@@ -8758,6 +8894,33 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8758
8894
|
else outFiles.push({ path, operation: "modify", content });
|
|
8759
8895
|
}
|
|
8760
8896
|
pending.push(...canvas.pending);
|
|
8897
|
+
for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
|
|
8898
|
+
const pageFilesOrTemplate = (route, files) => {
|
|
8899
|
+
const all = [...files];
|
|
8900
|
+
const exact = pageFilesFor(route, all);
|
|
8901
|
+
if (exact.length > 0) return exact;
|
|
8902
|
+
const wanted = route.split("/").filter(Boolean);
|
|
8903
|
+
return all.filter((file) => {
|
|
8904
|
+
if (!isDynamicRoute(file)) return false;
|
|
8905
|
+
const pattern = routeOfFile(file)?.split("/").filter(Boolean);
|
|
8906
|
+
if (!pattern) return false;
|
|
8907
|
+
const rest = pattern.some((segment) => segment.startsWith("[..."));
|
|
8908
|
+
if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
|
|
8909
|
+
return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
|
|
8910
|
+
}).sort();
|
|
8911
|
+
};
|
|
8912
|
+
const routes = routeCounts(pending);
|
|
8913
|
+
for (const row of pending) {
|
|
8914
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8915
|
+
row.fix = fixFor(row, {
|
|
8916
|
+
literal: target?.literal ?? "",
|
|
8917
|
+
kind: row.kind,
|
|
8918
|
+
...target?.inline ? { inline: true } : {},
|
|
8919
|
+
candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
|
|
8920
|
+
contents: latest,
|
|
8921
|
+
routes: routes.get(`${row.scope}|${row.path}`) ?? 1
|
|
8922
|
+
});
|
|
8923
|
+
}
|
|
8761
8924
|
const receipt = assertReceipt({
|
|
8762
8925
|
briefDigest: digest,
|
|
8763
8926
|
paths: {
|
|
@@ -8970,13 +9133,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8970
9133
|
for (const target of sharing) outcomes.set(target.key, missing(literal));
|
|
8971
9134
|
continue;
|
|
8972
9135
|
}
|
|
8973
|
-
const
|
|
8974
|
-
if (!assigned) {
|
|
8975
|
-
for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
8976
|
-
continue;
|
|
8977
|
-
}
|
|
9136
|
+
const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
|
|
8978
9137
|
for (const target of sharing) {
|
|
8979
|
-
|
|
9138
|
+
if (resolved.ambiguous.has(target.key)) {
|
|
9139
|
+
outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
9140
|
+
continue;
|
|
9141
|
+
}
|
|
9142
|
+
const mine = resolved.assigned.get(target.key) ?? [];
|
|
8980
9143
|
const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
|
|
8981
9144
|
if (rewritable.length === 0) {
|
|
8982
9145
|
outcomes.set(
|
|
@@ -9321,19 +9484,25 @@ function repeatGroups(targets) {
|
|
|
9321
9484
|
}
|
|
9322
9485
|
function resolvePositionally(targets, sites) {
|
|
9323
9486
|
const assigned = /* @__PURE__ */ new Map();
|
|
9324
|
-
const
|
|
9487
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
9488
|
+
const picked = /* @__PURE__ */ new Map();
|
|
9489
|
+
const wanters = /* @__PURE__ */ new Map();
|
|
9325
9490
|
for (const target of targets) {
|
|
9326
9491
|
const locator = target.locator;
|
|
9327
|
-
|
|
9328
|
-
const
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9492
|
+
const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
|
|
9493
|
+
const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
|
|
9494
|
+
if (!site) {
|
|
9495
|
+
ambiguous.add(target.key);
|
|
9496
|
+
continue;
|
|
9497
|
+
}
|
|
9498
|
+
picked.set(target.key, site);
|
|
9499
|
+
wanters.set(site, (wanters.get(site) ?? 0) + 1);
|
|
9500
|
+
}
|
|
9501
|
+
for (const [key, site] of picked) {
|
|
9502
|
+
if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
|
|
9503
|
+
else assigned.set(key, [site]);
|
|
9335
9504
|
}
|
|
9336
|
-
return assigned;
|
|
9505
|
+
return { assigned, ambiguous };
|
|
9337
9506
|
}
|
|
9338
9507
|
function isSubsequence(wanted, chain) {
|
|
9339
9508
|
let at = 0;
|
|
@@ -9356,7 +9525,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9356
9525
|
if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
|
|
9357
9526
|
const locals = helperLocals(reparsed, dialect, file);
|
|
9358
9527
|
const reads = DIALECT_RULES[dialect].text !== null;
|
|
9359
|
-
const boundary = (name) => `(?<![\\w$])${
|
|
9528
|
+
const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
|
|
9360
9529
|
const claimed = /* @__PURE__ */ new Set();
|
|
9361
9530
|
for (const element of elements) {
|
|
9362
9531
|
const candidates = targets.filter(
|
|
@@ -9364,7 +9533,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9364
9533
|
);
|
|
9365
9534
|
const match = candidates.find((target) => {
|
|
9366
9535
|
if (!reads) return true;
|
|
9367
|
-
const quoted =
|
|
9536
|
+
const quoted = escapeRegExp3(JSON.stringify(target.path));
|
|
9368
9537
|
if (target.scope === "layout") {
|
|
9369
9538
|
return locals.bcmsLayout.some(
|
|
9370
9539
|
(name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
|
|
@@ -9426,7 +9595,7 @@ function emitStubs2(dialects, slugs, contents, out) {
|
|
|
9426
9595
|
// package.json
|
|
9427
9596
|
var package_default = {
|
|
9428
9597
|
name: "@bettercms-ai/convert",
|
|
9429
|
-
version: "0.
|
|
9598
|
+
version: "0.12.0",
|
|
9430
9599
|
type: "module",
|
|
9431
9600
|
description: "Deterministic codemod that turns an imported site's text-matched bindings into declared ones \u2014 parser offsets in, one magic-string pass out, plus a receipt of everything it could not do.",
|
|
9432
9601
|
bin: {
|