@bettercms-ai/convert 0.11.0 → 0.12.1
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 +52 -0
- package/README.md +54 -0
- package/dist/cli.js +201 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +89 -2
- package/dist/index.js +202 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -197,6 +197,83 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
|
|
|
197
197
|
unlocated: UnlocatedPath[];
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* What to DO about a path this run could not bind — an instruction, never a category name.
|
|
202
|
+
*
|
|
203
|
+
* 🔴 A REASON IS A DIAGNOSIS AND AN AGENT CANNOT ACT ON ONE. `IN_EXPRESSION` tells a coding agent
|
|
204
|
+
* that something, somewhere, is an expression; it does not say which file, which line, or what to
|
|
205
|
+
* type. So every pending row leaves with a `fix`: one file, one line, the bytes that are there
|
|
206
|
+
* now, and the one edit that makes the path bindable on the next run. The closed `action` set is
|
|
207
|
+
* the contract the server validates (`src/lib/validation/schemas/conversion-receipt.ts`) and the
|
|
208
|
+
* loop `get_next_steps` reads — an unknown action is a 400, so it is minted in exactly one place.
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The edits an agent can be asked for, and nothing else.
|
|
213
|
+
*
|
|
214
|
+
* `wrap-span` the value has no element of its own — wrap it in `<span data-bcms-field>`.
|
|
215
|
+
* `declare-attr` the element is there — add `data-bcms-field` to it at file:line.
|
|
216
|
+
* `declare-richtext` the container renders markup — bind it with the richtext helper.
|
|
217
|
+
* `bind-expression` the value is computed — replace the expression with the helper's read.
|
|
218
|
+
* `bind-data` the copy lives in a data file or a content entry — bind THAT field.
|
|
219
|
+
* `manual` none of the above is true, and `why` says what a human has to decide.
|
|
220
|
+
*/
|
|
221
|
+
type FixAction = "wrap-span" | "declare-attr" | "bind-expression" | "declare-richtext" | "bind-data" | "manual";
|
|
222
|
+
/** One mechanical instruction, at one place in one file. @see FixAction */
|
|
223
|
+
interface PendingFix {
|
|
224
|
+
file: string;
|
|
225
|
+
/** 1-based. */
|
|
226
|
+
line: number;
|
|
227
|
+
/** 1-based, when the literal was found on the line. */
|
|
228
|
+
col?: number;
|
|
229
|
+
/** The bytes at `line`, trimmed — so the agent can see it is looking at the right place. */
|
|
230
|
+
snippet: string;
|
|
231
|
+
action: FixAction;
|
|
232
|
+
/** One sentence, present whenever the action alone does not say enough. */
|
|
233
|
+
why?: string;
|
|
234
|
+
}
|
|
235
|
+
/** Everything about one pending path a fix is derived from. Pure input; no filesystem. */
|
|
236
|
+
interface FixInput {
|
|
237
|
+
/** The copy the repository renders, flattened. Empty when the brief captured none. */
|
|
238
|
+
literal: string;
|
|
239
|
+
kind: string;
|
|
240
|
+
/** The richtext value IS the element's whole inline subtree. @see brief.ts */
|
|
241
|
+
inline?: true;
|
|
242
|
+
/** Files this path's copy was found in, best first. May be empty. */
|
|
243
|
+
candidates: string[];
|
|
244
|
+
/** Source, by path — only the files named in `candidates` need be present. */
|
|
245
|
+
contents: ReadonlyMap<string, string>;
|
|
246
|
+
/** How many ROUTES share this `(scope, path)` in the pending list. @see sharedChrome */
|
|
247
|
+
routes: number;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Where `literal` sits in `content` — tolerant of the two things every template does to copy.
|
|
251
|
+
*
|
|
252
|
+
* A template wraps its copy across lines and indents it, and it puts inline markup in the middle
|
|
253
|
+
* of it (`Read <strong>this</strong> now`). Both are invisible to a reader and fatal to
|
|
254
|
+
* `indexOf`, so the words are matched with whitespace OR a tag between them. Bounded at
|
|
255
|
+
* `MAX_WORDS`, because a paragraph's first dozen words identify it as well as all ninety do.
|
|
256
|
+
*/
|
|
257
|
+
declare function findLiteral(content: string, literal: string): {
|
|
258
|
+
index: number;
|
|
259
|
+
length: number;
|
|
260
|
+
} | null;
|
|
261
|
+
/**
|
|
262
|
+
* The fix for one pending row. Always answers: a row without one is a row an agent cannot act on.
|
|
263
|
+
*
|
|
264
|
+
* The FILE is the row's own when it has one (the lane that refused named it), and otherwise the
|
|
265
|
+
* first candidate `locate` found — and when there is neither, the fix still names the route's page
|
|
266
|
+
* file with line 1, because "go and look here" beats "go and look".
|
|
267
|
+
*/
|
|
268
|
+
declare function fixFor(row: PendingPath, input: FixInput): PendingFix;
|
|
269
|
+
/**
|
|
270
|
+
* How many ROUTES each `(scope, path)` is pending on — the shared-chrome tell. @see actionFor
|
|
271
|
+
*
|
|
272
|
+
* Counted over the whole pending list rather than per row: one nav label pending on twelve routes
|
|
273
|
+
* is one element, and the fix for every one of those rows is the same sentence about promotion.
|
|
274
|
+
*/
|
|
275
|
+
declare function routeCounts(pending: readonly PendingPath[]): Map<string, number>;
|
|
276
|
+
|
|
200
277
|
/**
|
|
201
278
|
* What the conversion did, and — the half that matters — what it did not.
|
|
202
279
|
*
|
|
@@ -209,6 +286,7 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
|
|
|
209
286
|
* sentence appears in three files" and "this field is bound" are different questions — and a path
|
|
210
287
|
* is only `rewritten` when EVERY located occurrence of it was.
|
|
211
288
|
*/
|
|
289
|
+
|
|
212
290
|
type PendingReason = "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY"
|
|
213
291
|
/** An INLINE richtext value whose element holds a component, a `<slot>` or an expression. */
|
|
214
292
|
| "SUBTREE_NOT_HTML" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
|
|
@@ -231,6 +309,15 @@ interface PendingPath {
|
|
|
231
309
|
line?: number;
|
|
232
310
|
/** What to do about it, in prose — a recipe, not a diagnosis. @see message */
|
|
233
311
|
hint?: string;
|
|
312
|
+
/**
|
|
313
|
+
* The EDIT that makes this path bindable on the next run — one file, one line, one action.
|
|
314
|
+
*
|
|
315
|
+
* 🔴 THE REASON IS A DIAGNOSIS AND NOBODY CAN ACT ON ONE. Every row carries this, because a
|
|
316
|
+
* conversion ends when nothing is pending and the only way an agent gets there is by being told
|
|
317
|
+
* what to type and where. OPTIONAL in the type so a receipt written by an older version of this
|
|
318
|
+
* package still parses; this package emits one for every row. @see fix.ts
|
|
319
|
+
*/
|
|
320
|
+
fix?: PendingFix;
|
|
234
321
|
}
|
|
235
322
|
/**
|
|
236
323
|
* One path that was already DECLARED before this run — and how honestly.
|
|
@@ -869,7 +956,7 @@ declare function isKnownHelper(content: string, dialect: Dialect): number | null
|
|
|
869
956
|
* renderer's constant occurs in it byte for byte, which is the only thing that keeps the copy a
|
|
870
957
|
* copy. Change it there first, then paste it here.
|
|
871
958
|
*/
|
|
872
|
-
declare const FORM_SCRIPT = "<script>\ndocument.querySelectorAll('form[data-bcms-form]').forEach(function(f){\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var msg=f.querySelector('.bcms-form-msg');\n var data={};\n // A checkboxes group posts one entry per checked box under the SAME name; the old\n // one-at-a-time assignment kept only the last, so \"pick many\" delivered one answer.\n // One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a\n // lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts\n // both an array and a comma string.\n new FormData(f).forEach(function(v,k){\n if(k==='cf-turnstile-response')return;\n if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);\n else data[k]=v;\n });\n var body={data:data};\n var tok=f.querySelector('[name=\"cf-turnstile-response\"]');\n if(tok&&tok.value)body['cf-turnstile-response']=tok.value;\n fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})\n .then(function(r){return r.ok;})\n .then(function(ok){if(msg){msg.hidden=false;msg.textContent=ok?(f.getAttribute('data-bcms-form-success')||'Thanks! Your submission was received.'):'Sorry, something went wrong.';}if(ok)f.reset();})\n .catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});\n });\n});\n</script>";
|
|
959
|
+
declare const FORM_SCRIPT = "<script>\nfunction bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}\ndocument.querySelectorAll('form[data-bcms-form]').forEach(function(f){\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var msg=f.querySelector('.bcms-form-msg');\n var data={};\n // A checkboxes group posts one entry per checked box under the SAME name; the old\n // one-at-a-time assignment kept only the last, so \"pick many\" delivered one answer.\n // One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a\n // lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts\n // both an array and a comma string.\n new FormData(f).forEach(function(v,k){\n if(k==='cf-turnstile-response')return;\n if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);\n else data[k]=v;\n });\n // Country-picker phone fields post ONE key. The __country select is scaffolding: it is\n // composed into the field's own value here, AFTER collection, so a showIf that points at a\n // phone key still compares the raw digits. Then it is removed from the body.\n f.querySelectorAll('select[name$=\"__country\"]').forEach(function(sel){\n var k=sel.name.slice(0,-9);\n var o=sel.options[sel.selectedIndex];\n data[k]=bcmsPhone(o?(o.getAttribute('data-cc')||''):'',data[k],o?o.getAttribute('data-trunk')==='1':false);\n delete data[k+'__country'];\n });\n var body={data:data};\n var tok=f.querySelector('[name=\"cf-turnstile-response\"]');\n if(tok&&tok.value)body['cf-turnstile-response']=tok.value;\n fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})\n .then(function(r){return r.ok;})\n .then(function(ok){if(msg){msg.hidden=false;msg.textContent=ok?(f.getAttribute('data-bcms-form-success')||'Thanks! Your submission was received.'):'Sorry, something went wrong.';}if(ok)f.reset();})\n .catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});\n });\n});\n</script>";
|
|
873
960
|
/**
|
|
874
961
|
* Wire every form the brief lists, and say what could not be.
|
|
875
962
|
*
|
|
@@ -1720,4 +1807,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
|
|
|
1720
1807
|
receipt: ConversionReceipt;
|
|
1721
1808
|
}>;
|
|
1722
1809
|
|
|
1723
|
-
export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findSites, findSitesTolerant, flat, helperSource, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };
|
|
1810
|
+
export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, type FixAction, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingFix, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findLiteral, findSites, findSitesTolerant, fixFor, flat, helperSource, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeCounts, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };
|
package/dist/index.js
CHANGED
|
@@ -5318,6 +5318,138 @@ function coverageOf(receipt) {
|
|
|
5318
5318
|
};
|
|
5319
5319
|
}
|
|
5320
5320
|
|
|
5321
|
+
// src/fix.ts
|
|
5322
|
+
var MAX_SNIPPET = 160;
|
|
5323
|
+
var MAX_WORDS = 12;
|
|
5324
|
+
var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5325
|
+
function findLiteral(content, literal) {
|
|
5326
|
+
const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
|
|
5327
|
+
if (words.length === 0) return null;
|
|
5328
|
+
const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
|
|
5329
|
+
const match = pattern.exec(content);
|
|
5330
|
+
return match ? { index: match.index, length: match[0].length } : null;
|
|
5331
|
+
}
|
|
5332
|
+
function placeOf(content, index) {
|
|
5333
|
+
const before = content.slice(0, index);
|
|
5334
|
+
const line = before.split("\n").length;
|
|
5335
|
+
const start = before.lastIndexOf("\n") + 1;
|
|
5336
|
+
const end = content.indexOf("\n", index);
|
|
5337
|
+
return {
|
|
5338
|
+
line,
|
|
5339
|
+
col: index - start + 1,
|
|
5340
|
+
snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
function fileHead(content) {
|
|
5344
|
+
return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
|
|
5345
|
+
}
|
|
5346
|
+
var isMarkdown = (file) => /\.mdx?$/i.test(file);
|
|
5347
|
+
var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
|
|
5348
|
+
function inDataRegion(content, index) {
|
|
5349
|
+
if (content.startsWith("---")) {
|
|
5350
|
+
const close = content.indexOf("\n---", 3);
|
|
5351
|
+
if (close >= 0 && index < close) return true;
|
|
5352
|
+
}
|
|
5353
|
+
for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
5354
|
+
const start = match.index + match[0].indexOf(">") + 1;
|
|
5355
|
+
if (index >= start && index < start + match[1].length) return true;
|
|
5356
|
+
}
|
|
5357
|
+
return false;
|
|
5358
|
+
}
|
|
5359
|
+
var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
|
|
5360
|
+
"wrap-span",
|
|
5361
|
+
"declare-attr",
|
|
5362
|
+
"declare-richtext"
|
|
5363
|
+
]);
|
|
5364
|
+
function actionFor(reason, input, file, inData) {
|
|
5365
|
+
const chosen = reasonAction(reason, input, file, inData);
|
|
5366
|
+
if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
|
|
5367
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5368
|
+
}
|
|
5369
|
+
return chosen;
|
|
5370
|
+
}
|
|
5371
|
+
function reasonAction(reason, input, file, inData) {
|
|
5372
|
+
const richtext = input.kind === "richtext" || input.inline === true;
|
|
5373
|
+
switch (reason) {
|
|
5374
|
+
case "AMBIGUOUS_LITERAL":
|
|
5375
|
+
if (input.routes > 1) {
|
|
5376
|
+
return {
|
|
5377
|
+
action: "manual",
|
|
5378
|
+
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.`
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
return {
|
|
5382
|
+
action: "declare-attr",
|
|
5383
|
+
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."
|
|
5384
|
+
};
|
|
5385
|
+
case "REPEATER_AMBIGUOUS":
|
|
5386
|
+
case "REPEATER_FIXED_LENGTH":
|
|
5387
|
+
return {
|
|
5388
|
+
action: "declare-attr",
|
|
5389
|
+
why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
|
|
5390
|
+
};
|
|
5391
|
+
case "IN_EXPRESSION":
|
|
5392
|
+
if (inData) {
|
|
5393
|
+
return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
|
|
5394
|
+
}
|
|
5395
|
+
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." };
|
|
5396
|
+
case "DIALECT_UNSUPPORTED":
|
|
5397
|
+
if (isMarkdown(file)) {
|
|
5398
|
+
return {
|
|
5399
|
+
action: "bind-data",
|
|
5400
|
+
why: "This is a content-collection body: bind the element that renders it and make the entry the source."
|
|
5401
|
+
};
|
|
5402
|
+
}
|
|
5403
|
+
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.` };
|
|
5404
|
+
case "NOT_IN_SOURCE":
|
|
5405
|
+
case "NO_ORIGINAL":
|
|
5406
|
+
return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
|
|
5407
|
+
action: "manual",
|
|
5408
|
+
why: "The built page renders this and no source file holds it verbatim; find what formats it."
|
|
5409
|
+
};
|
|
5410
|
+
case "IN_DATA_FILE":
|
|
5411
|
+
case "IMAGE_ASSET_UNRESOLVED":
|
|
5412
|
+
return { action: "bind-data", why: "Bind the data field this value comes from." };
|
|
5413
|
+
case "PROP_TARGET_NOT_FOUND":
|
|
5414
|
+
case "PROP_DRILLED_DEEP":
|
|
5415
|
+
return {
|
|
5416
|
+
action: "wrap-span",
|
|
5417
|
+
why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
|
|
5418
|
+
};
|
|
5419
|
+
case "SUBSTRING_ONLY":
|
|
5420
|
+
return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
|
|
5421
|
+
case "SUBTREE_NOT_HTML":
|
|
5422
|
+
return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
|
|
5423
|
+
default:
|
|
5424
|
+
return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
function fixFor(row, input) {
|
|
5428
|
+
const file = row.file ?? input.candidates[0] ?? "";
|
|
5429
|
+
const content = input.contents.get(file);
|
|
5430
|
+
const at = content ? findLiteral(content, input.literal) : null;
|
|
5431
|
+
const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
|
|
5432
|
+
const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
|
|
5433
|
+
return {
|
|
5434
|
+
file,
|
|
5435
|
+
line: row.line ?? place.line,
|
|
5436
|
+
...place.col !== void 0 ? { col: place.col } : {},
|
|
5437
|
+
snippet: place.snippet,
|
|
5438
|
+
action,
|
|
5439
|
+
...why ? { why } : {}
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
function routeCounts(pending) {
|
|
5443
|
+
const routes = /* @__PURE__ */ new Map();
|
|
5444
|
+
for (const row of pending) {
|
|
5445
|
+
const id = `${row.scope}|${row.path}`;
|
|
5446
|
+
const held = routes.get(id) ?? /* @__PURE__ */ new Set();
|
|
5447
|
+
held.add(row.route);
|
|
5448
|
+
routes.set(id, held);
|
|
5449
|
+
}
|
|
5450
|
+
return new Map([...routes].map(([id, set]) => [id, set.size]));
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5321
5453
|
// src/sites/html.ts
|
|
5322
5454
|
import { parse as parseHtml } from "parse5";
|
|
5323
5455
|
var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
|
|
@@ -6199,6 +6331,7 @@ import MagicString4 from "magic-string";
|
|
|
6199
6331
|
var WIRABLE = ["html", "astro", "jsx"];
|
|
6200
6332
|
var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
6201
6333
|
var FORM_SCRIPT = `<script>
|
|
6334
|
+
function bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}
|
|
6202
6335
|
document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
6203
6336
|
f.addEventListener('submit',function(e){
|
|
6204
6337
|
e.preventDefault();
|
|
@@ -6214,6 +6347,15 @@ document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
|
|
|
6214
6347
|
if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);
|
|
6215
6348
|
else data[k]=v;
|
|
6216
6349
|
});
|
|
6350
|
+
// Country-picker phone fields post ONE key. The __country select is scaffolding: it is
|
|
6351
|
+
// composed into the field's own value here, AFTER collection, so a showIf that points at a
|
|
6352
|
+
// phone key still compares the raw digits. Then it is removed from the body.
|
|
6353
|
+
f.querySelectorAll('select[name$="__country"]').forEach(function(sel){
|
|
6354
|
+
var k=sel.name.slice(0,-9);
|
|
6355
|
+
var o=sel.options[sel.selectedIndex];
|
|
6356
|
+
data[k]=bcmsPhone(o?(o.getAttribute('data-cc')||''):'',data[k],o?o.getAttribute('data-trunk')==='1':false);
|
|
6357
|
+
delete data[k+'__country'];
|
|
6358
|
+
});
|
|
6217
6359
|
var body={data:data};
|
|
6218
6360
|
var tok=f.querySelector('[name="cf-turnstile-response"]');
|
|
6219
6361
|
if(tok&&tok.value)body['cf-turnstile-response']=tok.value;
|
|
@@ -6438,7 +6580,9 @@ var SKIP_DIRS = [
|
|
|
6438
6580
|
".vercel/",
|
|
6439
6581
|
".cache/",
|
|
6440
6582
|
"coverage/",
|
|
6441
|
-
"vendor/"
|
|
6583
|
+
"vendor/",
|
|
6584
|
+
"__tests__/",
|
|
6585
|
+
"__mocks__/"
|
|
6442
6586
|
];
|
|
6443
6587
|
var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
|
|
6444
6588
|
var SOURCE_EXTENSIONS = [
|
|
@@ -6476,11 +6620,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
|
|
|
6476
6620
|
"_includes",
|
|
6477
6621
|
"_layouts"
|
|
6478
6622
|
]);
|
|
6623
|
+
var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
|
|
6479
6624
|
function isSourceCandidate(path) {
|
|
6480
6625
|
const lower = path.toLowerCase();
|
|
6481
6626
|
if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
|
|
6482
6627
|
return false;
|
|
6483
6628
|
if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
|
|
6629
|
+
if (isAboutTheSite(lower)) return false;
|
|
6484
6630
|
if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
|
|
6485
6631
|
if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
|
|
6486
6632
|
if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
@@ -8429,7 +8575,7 @@ var ConvertError = class extends Error {
|
|
|
8429
8575
|
code;
|
|
8430
8576
|
};
|
|
8431
8577
|
var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
|
|
8432
|
-
var
|
|
8578
|
+
var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8433
8579
|
var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
|
|
8434
8580
|
var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
|
|
8435
8581
|
async function drillProp(ctx, fromFile, component, prop, kind, hop) {
|
|
@@ -8776,6 +8922,33 @@ async function convertSources(briefIn, sources, options) {
|
|
|
8776
8922
|
else outFiles.push({ path, operation: "modify", content });
|
|
8777
8923
|
}
|
|
8778
8924
|
pending.push(...canvas.pending);
|
|
8925
|
+
for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
|
|
8926
|
+
const pageFilesOrTemplate = (route, files) => {
|
|
8927
|
+
const all = [...files];
|
|
8928
|
+
const exact = pageFilesFor(route, all);
|
|
8929
|
+
if (exact.length > 0) return exact;
|
|
8930
|
+
const wanted = route.split("/").filter(Boolean);
|
|
8931
|
+
return all.filter((file) => {
|
|
8932
|
+
if (!isDynamicRoute(file)) return false;
|
|
8933
|
+
const pattern = routeOfFile(file)?.split("/").filter(Boolean);
|
|
8934
|
+
if (!pattern) return false;
|
|
8935
|
+
const rest = pattern.some((segment) => segment.startsWith("[..."));
|
|
8936
|
+
if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
|
|
8937
|
+
return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
|
|
8938
|
+
}).sort();
|
|
8939
|
+
};
|
|
8940
|
+
const routes = routeCounts(pending);
|
|
8941
|
+
for (const row of pending) {
|
|
8942
|
+
const target = targets.get(keyOf(row.route, row.scope, row.path));
|
|
8943
|
+
row.fix = fixFor(row, {
|
|
8944
|
+
literal: target?.literal ?? "",
|
|
8945
|
+
kind: row.kind,
|
|
8946
|
+
...target?.inline ? { inline: true } : {},
|
|
8947
|
+
candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
|
|
8948
|
+
contents: latest,
|
|
8949
|
+
routes: routes.get(`${row.scope}|${row.path}`) ?? 1
|
|
8950
|
+
});
|
|
8951
|
+
}
|
|
8779
8952
|
const receipt = assertReceipt({
|
|
8780
8953
|
briefDigest: digest,
|
|
8781
8954
|
paths: {
|
|
@@ -8988,13 +9161,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
|
|
|
8988
9161
|
for (const target of sharing) outcomes.set(target.key, missing(literal));
|
|
8989
9162
|
continue;
|
|
8990
9163
|
}
|
|
8991
|
-
const
|
|
8992
|
-
if (!assigned) {
|
|
8993
|
-
for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
8994
|
-
continue;
|
|
8995
|
-
}
|
|
9164
|
+
const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
|
|
8996
9165
|
for (const target of sharing) {
|
|
8997
|
-
|
|
9166
|
+
if (resolved.ambiguous.has(target.key)) {
|
|
9167
|
+
outcomes.set(target.key, "AMBIGUOUS_LITERAL");
|
|
9168
|
+
continue;
|
|
9169
|
+
}
|
|
9170
|
+
const mine = resolved.assigned.get(target.key) ?? [];
|
|
8998
9171
|
const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
|
|
8999
9172
|
if (rewritable.length === 0) {
|
|
9000
9173
|
outcomes.set(
|
|
@@ -9339,19 +9512,25 @@ function repeatGroups(targets) {
|
|
|
9339
9512
|
}
|
|
9340
9513
|
function resolvePositionally(targets, sites) {
|
|
9341
9514
|
const assigned = /* @__PURE__ */ new Map();
|
|
9342
|
-
const
|
|
9515
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
9516
|
+
const picked = /* @__PURE__ */ new Map();
|
|
9517
|
+
const wanters = /* @__PURE__ */ new Map();
|
|
9343
9518
|
for (const target of targets) {
|
|
9344
9519
|
const locator = target.locator;
|
|
9345
|
-
|
|
9346
|
-
const
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9520
|
+
const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
|
|
9521
|
+
const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
|
|
9522
|
+
if (!site) {
|
|
9523
|
+
ambiguous.add(target.key);
|
|
9524
|
+
continue;
|
|
9525
|
+
}
|
|
9526
|
+
picked.set(target.key, site);
|
|
9527
|
+
wanters.set(site, (wanters.get(site) ?? 0) + 1);
|
|
9528
|
+
}
|
|
9529
|
+
for (const [key, site] of picked) {
|
|
9530
|
+
if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
|
|
9531
|
+
else assigned.set(key, [site]);
|
|
9353
9532
|
}
|
|
9354
|
-
return assigned;
|
|
9533
|
+
return { assigned, ambiguous };
|
|
9355
9534
|
}
|
|
9356
9535
|
function isSubsequence(wanted, chain) {
|
|
9357
9536
|
let at = 0;
|
|
@@ -9374,7 +9553,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9374
9553
|
if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
|
|
9375
9554
|
const locals = helperLocals(reparsed, dialect, file);
|
|
9376
9555
|
const reads = DIALECT_RULES[dialect].text !== null;
|
|
9377
|
-
const boundary = (name) => `(?<![\\w$])${
|
|
9556
|
+
const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
|
|
9378
9557
|
const claimed = /* @__PURE__ */ new Set();
|
|
9379
9558
|
for (const element of elements) {
|
|
9380
9559
|
const candidates = targets.filter(
|
|
@@ -9382,7 +9561,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
|
|
|
9382
9561
|
);
|
|
9383
9562
|
const match = candidates.find((target) => {
|
|
9384
9563
|
if (!reads) return true;
|
|
9385
|
-
const quoted =
|
|
9564
|
+
const quoted = escapeRegExp3(JSON.stringify(target.path));
|
|
9386
9565
|
if (target.scope === "layout") {
|
|
9387
9566
|
return locals.bcmsLayout.some(
|
|
9388
9567
|
(name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
|
|
@@ -9482,8 +9661,10 @@ export {
|
|
|
9482
9661
|
dialectOf,
|
|
9483
9662
|
exportedFunction,
|
|
9484
9663
|
extendedConfigs,
|
|
9664
|
+
findLiteral,
|
|
9485
9665
|
findSites,
|
|
9486
9666
|
findSitesTolerant,
|
|
9667
|
+
fixFor,
|
|
9487
9668
|
flat,
|
|
9488
9669
|
helperSource,
|
|
9489
9670
|
importsIn,
|
|
@@ -9505,6 +9686,7 @@ export {
|
|
|
9505
9686
|
relativeImport,
|
|
9506
9687
|
resolveSpecifier,
|
|
9507
9688
|
rewriteFile,
|
|
9689
|
+
routeCounts,
|
|
9508
9690
|
routeOfFile,
|
|
9509
9691
|
stripTags,
|
|
9510
9692
|
unreadableConfigs,
|