@nuasite/cli 0.55.0 → 0.56.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/dist/index.js CHANGED
@@ -7295,7 +7295,7 @@ var init_check_shape = __esm(() => {
7295
7295
  function isBlankFieldValue(value) {
7296
7296
  return value === undefined || value === null || value === "";
7297
7297
  }
7298
- function defaultValueForNewEntry(field, today = () => new Date) {
7298
+ function seedValueForRequiredField(field, today = () => new Date) {
7299
7299
  if (field.defaultValue !== undefined)
7300
7300
  return field.defaultValue;
7301
7301
  switch (field.type) {
@@ -7311,12 +7311,35 @@ function defaultValueForNewEntry(field, today = () => new Date) {
7311
7311
  return "";
7312
7312
  }
7313
7313
  }
7314
+ function blankFieldValue(field) {
7315
+ if (field.defaultValue !== undefined)
7316
+ return field.defaultValue;
7317
+ switch (field.type) {
7318
+ case "boolean":
7319
+ return false;
7320
+ case "array":
7321
+ return [];
7322
+ case "object":
7323
+ return {};
7324
+ case "date":
7325
+ case "datetime":
7326
+ case "time":
7327
+ case "month":
7328
+ case "number":
7329
+ case "year":
7330
+ case "select":
7331
+ case "reference":
7332
+ return;
7333
+ default:
7334
+ return "";
7335
+ }
7336
+ }
7314
7337
  function newRepeaterItem(fields, today) {
7315
7338
  const item = {};
7316
7339
  for (const field of fields) {
7317
7340
  if (!field.required || field.hidden)
7318
7341
  continue;
7319
- item[field.name] = defaultValueForNewEntry(field, today);
7342
+ item[field.name] = seedValueForRequiredField(field, today);
7320
7343
  }
7321
7344
  return item;
7322
7345
  }
@@ -7326,6 +7349,25 @@ function isDeclaredDerived(field) {
7326
7349
  function blankRequiredFields(fields, frontmatter) {
7327
7350
  return fields.filter((field) => field.required && !(field.hidden ?? field.layout?.hidden) && !isDeclaredDerived(field) && isBlankFieldValue(frontmatter[field.name])).map((field) => field.name);
7328
7351
  }
7352
+ function isPlainRecord(value) {
7353
+ if (typeof value !== "object" || value === null || Array.isArray(value))
7354
+ return false;
7355
+ const prototype = Object.getPrototypeOf(value);
7356
+ return prototype === Object.prototype || prototype === null;
7357
+ }
7358
+ function prunedValue(value) {
7359
+ if (Array.isArray(value))
7360
+ return value.map(prunedValue).filter((item) => !isBlankFieldValue(item));
7361
+ if (isPlainRecord(value))
7362
+ return withoutBlankArrayItems(value);
7363
+ return value;
7364
+ }
7365
+ function withoutBlankArrayItems(frontmatter) {
7366
+ const out = {};
7367
+ for (const [key, value] of Object.entries(frontmatter))
7368
+ out[key] = prunedValue(value);
7369
+ return out;
7370
+ }
7329
7371
  // ../cms-types/src/index.ts
7330
7372
  function isFieldType(value) {
7331
7373
  return FIELD_TYPES.includes(value);
@@ -7361,12 +7403,12 @@ var init_src = __esm(() => {
7361
7403
  });
7362
7404
 
7363
7405
  // ../cms-core/src/editor-write-model.ts
7364
- function newEntryFrontmatter(fields, today) {
7406
+ function newEntryFrontmatter(fields) {
7365
7407
  const frontmatter = {};
7366
7408
  for (const field of fields) {
7367
7409
  if (field.name === "title" || field.hidden)
7368
7410
  continue;
7369
- frontmatter[field.name] = defaultValueForNewEntry(field, today);
7411
+ frontmatter[field.name] = blankFieldValue(field);
7370
7412
  }
7371
7413
  return frontmatter;
7372
7414
  }
@@ -7439,16 +7481,27 @@ function rejectionFinding(action, issue, kind, written) {
7439
7481
  finding.hint = hintFor(kind, valueAt(written, issue.path));
7440
7482
  return finding;
7441
7483
  }
7484
+ function scalarItemType(field) {
7485
+ if (field.reference?.isArray)
7486
+ return "reference";
7487
+ if (field.type !== "array")
7488
+ return;
7489
+ return field.itemType === undefined || field.itemType === "object" ? undefined : field.itemType;
7490
+ }
7442
7491
  function blankItemsFor(field, current, today) {
7492
+ const scalar = scalarItemType(field);
7493
+ if (scalar !== undefined)
7494
+ return [blankFieldValue({ name: field.name, type: scalar })];
7443
7495
  const itemFields = field.fields ?? [];
7444
7496
  if (itemFields.length > 0) {
7445
7497
  return [newRepeaterItem(itemFields.map((item) => ({ ...toWriteModelField(item), required: item.required })), today)];
7446
7498
  }
7447
- if (current.length === 0)
7448
- return [{}, { name: "" }];
7449
7499
  const first = current[0];
7450
- const template = isPlainObject(first) ? Object.fromEntries(Object.keys(first).map((key) => [key, ""])) : {};
7451
- return [{}, template];
7500
+ if (isPlainObject(first))
7501
+ return [{}, Object.fromEntries(Object.keys(first).map((key) => [key, ""]))];
7502
+ if (field.itemType !== "object")
7503
+ return [];
7504
+ return current.length === 0 ? [{}, { name: "" }] : [{}];
7452
7505
  }
7453
7506
  async function checkEditorWrites(input) {
7454
7507
  const findings = [];
@@ -7479,7 +7532,7 @@ async function checkEditorWrites(input) {
7479
7532
  const loaded = input.collections.get(name);
7480
7533
  const entries = loaded?.entries ?? [];
7481
7534
  const kind = loaded ? collectionKind(loaded, collection.loaderPattern) : "markdown";
7482
- const created = applyCreateRouteFields(omitEmptyOnCreate(newEntryFrontmatter(collection.fields.map(toWriteModelField), input.today)), kind, input.today);
7535
+ const created = withoutBlankArrayItems(applyCreateRouteFields(omitEmptyOnCreate(newEntryFrontmatter(collection.fields.map(toWriteModelField))), kind, input.today));
7483
7536
  const guarded = new Set(blankRequiredFields(collection.fields, created));
7484
7537
  const createAction = `Creating a new entry in "${name}"`;
7485
7538
  const createVerdict = await parseWrite(schema, created);
@@ -7493,11 +7546,11 @@ async function checkEditorWrites(input) {
7493
7546
  report(createKey, rejectionFinding(createAction, issue, "create", created));
7494
7547
  }
7495
7548
  }
7496
- const repeaters = collection.fields.filter((field) => field.type === "array" && field.itemType === "object");
7497
- if (repeaters.length === 0)
7549
+ const lists = collection.fields.filter(isListField);
7550
+ if (lists.length === 0)
7498
7551
  continue;
7499
7552
  const seed = await firstAcceptedEntry(schema, entries);
7500
- for (const field of repeaters) {
7553
+ for (const field of lists) {
7501
7554
  const key = `${name} add:${field.name}`;
7502
7555
  if (!seed) {
7503
7556
  const hasEntries = entries.some((entry) => entry.frontmatter !== undefined);
@@ -7516,7 +7569,7 @@ async function checkEditorWrites(input) {
7516
7569
  const existing = Array.isArray(current) ? current : [];
7517
7570
  const action = `Clicking "+ Add" on "${name}.${field.name}"`;
7518
7571
  for (const item of blankItemsFor(field, existing, input.today)) {
7519
- const written = { ...seed, [field.name]: [...existing, item] };
7572
+ const written = withoutBlankArrayItems({ ...seed, [field.name]: [...existing, item] });
7520
7573
  const verdict = await parseWrite(schema, written);
7521
7574
  if ("threw" in verdict) {
7522
7575
  report(key, throwFinding(action, verdict.threw));
@@ -7535,7 +7588,7 @@ var CONFIG_FILE2 = "src/content.config.ts", toWriteModelField = (field) => ({ na
7535
7588
  file: CONFIG_FILE2,
7536
7589
  message: `${action} produces a write the schema throws on: ${threw}`,
7537
7590
  hint: "A schema that throws instead of returning an issue is usually a refinement reading a field this write does not carry. Guard it against a missing value \u2014 `astro sync` runs the same schema over the same record."
7538
- });
7591
+ }), isListField = (field) => field.type === "array" || field.reference?.isArray === true;
7539
7592
  var init_check_write = __esm(() => {
7540
7593
  init_check_entries();
7541
7594
  init_editor_write_model();
@@ -22429,8 +22482,23 @@ function parseSource(source) {
22429
22482
  return null;
22430
22483
  }
22431
22484
  }
22485
+ async function classifyEmptyContentConfig(fs) {
22486
+ let found = false;
22487
+ for (const configPath of CONTENT_CONFIG_PATHS) {
22488
+ try {
22489
+ await fs.stat(configPath);
22490
+ } catch {
22491
+ continue;
22492
+ }
22493
+ found = true;
22494
+ if (parseSource(await fs.readFile(configPath)) === null) {
22495
+ return "unparseable";
22496
+ }
22497
+ }
22498
+ return found ? "no-collections" : "missing";
22499
+ }
22432
22500
  async function parseContentConfig(fs, cache) {
22433
- for (const configPath of ["src/content/config.ts", "src/content.config.ts"]) {
22501
+ for (const configPath of CONTENT_CONFIG_PATHS) {
22434
22502
  let stat;
22435
22503
  try {
22436
22504
  stat = await fs.stat(configPath);
@@ -23113,7 +23181,7 @@ function dropNestedDerivedFrom(collectionName, path2, fields) {
23113
23181
  dropNestedDerivedFrom(collectionName, fieldPath, field.fields);
23114
23182
  }
23115
23183
  }
23116
- var import_parser, FIELD_HELPER_TYPES, VALID_HINT_KEYS, WRAPPER_METHODS;
23184
+ var import_parser, FIELD_HELPER_TYPES, VALID_HINT_KEYS, WRAPPER_METHODS, CONTENT_CONFIG_PATHS;
23117
23185
  var init_content_config_ast = __esm(() => {
23118
23186
  init_src();
23119
23187
  import_parser = __toESM(require_lib(), 1);
@@ -23145,6 +23213,7 @@ var init_content_config_ast = __esm(() => {
23145
23213
  "accept"
23146
23214
  ]);
23147
23215
  WRAPPER_METHODS = new Set(["optional", "nullable", "nullish", "default"]);
23216
+ CONTENT_CONFIG_PATHS = ["src/content/config.ts", "src/content.config.ts"];
23148
23217
  });
23149
23218
 
23150
23219
  // ../cms-core/src/shared.ts
@@ -23309,12 +23378,15 @@ async function checkContent(fs, options = {}) {
23309
23378
  const findings = [];
23310
23379
  const config = await parseContentConfig(fs, new Map);
23311
23380
  if (config.size === 0) {
23312
- findings.push({
23313
- severity: "error",
23314
- code: "config/no-collections",
23315
- file: "src/content.config.ts",
23316
- message: "No collections found \u2014 the content config is missing, unreadable, or declares nothing."
23317
- });
23381
+ const reason = await classifyEmptyContentConfig(fs);
23382
+ if (reason !== "no-collections") {
23383
+ findings.push({
23384
+ severity: "error",
23385
+ code: reason === "missing" ? "config/missing" : "config/unreadable",
23386
+ file: "src/content.config.ts",
23387
+ message: reason === "missing" ? `No content config found \u2014 expected one of ${CONTENT_CONFIG_PATHS.join(" or ")}.` : "The content config could not be parsed."
23388
+ });
23389
+ }
23318
23390
  return { findings, collections: 0, entries: 0 };
23319
23391
  }
23320
23392
  const collections = await loadCollections(fs, config);
@@ -33499,7 +33571,7 @@ async function applyImageChange(content, change, absFilePath, originUrl) {
33499
33571
  break;
33500
33572
  }
33501
33573
  }
33502
- if (replacedIndex < 0 && change.sourceSnippet) {
33574
+ if (replacedIndex < 0 && change.sourceSnippet && isYamlValueSource(newContent, change.sourceSnippet, change.sourcePath)) {
33503
33575
  for (const srcToFind of srcCandidates) {
33504
33576
  const yamlResult = tryYamlValueReplacement(change.sourceSnippet, srcToFind, newSrc);
33505
33577
  if (yamlResult !== null) {
@@ -33806,7 +33878,7 @@ function replaceLiteralInSnippet(content, snippet, oldValue, newValue) {
33806
33878
  continue;
33807
33879
  const updated = snippet.replace(pattern, `${quote}${safeNewValue}${quote}`);
33808
33880
  if (updated !== snippet)
33809
- return content.replace(snippet, updated);
33881
+ return content.replace(snippet, escapeReplacement(updated));
33810
33882
  }
33811
33883
  return;
33812
33884
  }
@@ -33900,35 +33972,66 @@ function applyTextChange(content, change, manifest) {
33900
33972
  if (!content.includes(sourceSnippet)) {
33901
33973
  return { success: false, error: "Source snippet not found in file" };
33902
33974
  }
33903
- const stylingAllowed = manifest.entries[change.cmsId]?.allowStyling !== false;
33975
+ const entry = manifest.entries[change.cmsId];
33976
+ const insideYaml = isYamlValueSource(content, sourceSnippet, change.sourcePath);
33977
+ const stylingAllowed = entry?.allowStyling !== false && !insideYaml;
33904
33978
  const newText = stylingAllowed ? htmlValue ?? newValue : newValue;
33905
33979
  const placeholderPattern = /\{\{cms:[^}]+\}\}/g;
33906
33980
  if (placeholderPattern.test(originalValue)) {
33907
- return applyTextChangeWithPlaceholders(content, sourceSnippet, originalValue, newText);
33981
+ return applyTextChangeWithPlaceholders(insideYaml, content, sourceSnippet, originalValue, newText);
33908
33982
  }
33909
33983
  const resolvedNewText = resolveCmsPlaceholders(newText, manifest);
33910
33984
  const resolvedOriginal = resolveCmsPlaceholders(originalValue, manifest);
33911
- const updatedSnippet = sourceSnippet.replace(resolvedOriginal, resolvedNewText);
33912
- if (updatedSnippet === sourceSnippet) {
33913
- const yamlResult = tryYamlValueReplacement(sourceSnippet, resolvedOriginal, resolvedNewText);
33914
- if (yamlResult !== null) {
33915
- return { success: true, content: content.replace(sourceSnippet, yamlResult) };
33985
+ const write = (updatedSnippet2) => writeSnippet(content, sourceSnippet, updatedSnippet2, insideYaml, resolvedNewText);
33986
+ const snippetIsJavaScript = isJavaScriptSource(change.sourcePath) && (!!entry?.variableName || !looksLikeMarkup(sourceSnippet));
33987
+ if (snippetIsJavaScript) {
33988
+ const literalResult = tryJsStringLiteralChange(sourceSnippet, resolvedOriginal, resolvedNewText);
33989
+ if (literalResult !== null) {
33990
+ return write(literalResult);
33991
+ }
33992
+ }
33993
+ if (insideYaml) {
33994
+ const frontmatterResult = tryYamlValueReplacement(sourceSnippet, resolvedOriginal, resolvedNewText, true);
33995
+ if (frontmatterResult !== null) {
33996
+ return write(frontmatterResult);
33916
33997
  }
33998
+ }
33999
+ const updatedSnippet = sourceSnippet.replace(resolvedOriginal, escapeReplacement(resolvedNewText));
34000
+ if (updatedSnippet === sourceSnippet) {
33917
34001
  const brResult = tryBrNormalizedChange(sourceSnippet, resolvedOriginal, resolvedNewText);
33918
34002
  if (brResult !== null) {
33919
- return { success: true, content: content.replace(sourceSnippet, brResult) };
34003
+ return write(brResult);
34004
+ }
34005
+ if (snippetIsJavaScript) {
34006
+ const literalResult = tryJsStringLiteralChange(sourceSnippet, resolvedOriginal, resolvedNewText);
34007
+ if (literalResult !== null) {
34008
+ return write(literalResult);
34009
+ }
33920
34010
  }
33921
34011
  const matchedText = findTextInSnippet(sourceSnippet, resolvedOriginal);
33922
34012
  if (matchedText) {
33923
- const updatedWithEntity = sourceSnippet.replace(matchedText, resolvedNewText);
33924
- return { success: true, content: content.replace(sourceSnippet, updatedWithEntity) };
34013
+ const spliced = /<[^>]+>/.test(resolvedNewText) ? null : spliceIntoEntitySource(matchedText, resolvedOriginal, resolvedNewText);
34014
+ const replacement = spliced ?? (/<[^>]+>/.test(resolvedNewText) ? encodeNbspLike(resolvedNewText, matchedText) : encodeEntitiesLike(resolvedNewText, matchedText));
34015
+ const updatedWithEntity = sourceSnippet.replace(matchedText, escapeReplacement(replacement));
34016
+ return write(updatedWithEntity);
33925
34017
  }
33926
34018
  const innerMatch = sourceSnippet.match(/^(\s*<(\w+)\b[^>]*>)([\s\S]*)(<\/\2>\s*)$/);
33927
34019
  if (innerMatch) {
33928
34020
  const [, openTag, , innerContent, closeTag] = innerMatch;
33929
34021
  const textOnly = innerContent.replace(/<[^>]+>/g, "");
33930
34022
  if (textOnly === resolvedOriginal) {
33931
- return { success: true, content: content.replace(sourceSnippet, openTag + resolvedNewText + closeTag) };
34023
+ const isHtmlReplacement = /<[^>]+>/.test(resolvedNewText);
34024
+ if (isHtmlReplacement || !/<[^>]+>/.test(innerContent)) {
34025
+ return write(openTag + resolvedNewText + closeTag);
34026
+ }
34027
+ const splicedInner = spliceTextAcrossInlineMarkup(innerContent, resolvedOriginal, resolvedNewText);
34028
+ if (splicedInner !== null) {
34029
+ return write(openTag + splicedInner + closeTag);
34030
+ }
34031
+ return {
34032
+ success: false,
34033
+ error: "Cannot apply this edit without dropping the inline markup inside the element " + "\u2014 edit the styled part separately"
34034
+ };
33932
34035
  }
33933
34036
  }
33934
34037
  return {
@@ -33936,9 +34039,9 @@ function applyTextChange(content, change, manifest) {
33936
34039
  error: `Original text "${resolvedOriginal.substring(0, 50)}..." not found in source snippet`
33937
34040
  };
33938
34041
  }
33939
- return { success: true, content: content.replace(sourceSnippet, updatedSnippet) };
34042
+ return write(updatedSnippet);
33940
34043
  }
33941
- function applyTextChangeWithPlaceholders(content, sourceSnippet, originalValue, newText) {
34044
+ function applyTextChangeWithPlaceholders(insideYaml, content, sourceSnippet, originalValue, newText) {
33942
34045
  const placeholderPattern = /\{\{cms:[^}]+\}\}/g;
33943
34046
  const originalParts = originalValue.split(placeholderPattern);
33944
34047
  const newParts = newText.split(placeholderPattern);
@@ -33958,7 +34061,7 @@ function applyTextChangeWithPlaceholders(content, sourceSnippet, originalValue,
33958
34061
  if (matchedText !== oldPart) {
33959
34062
  newPart = encodeEntitiesLike(newPart, matchedText);
33960
34063
  }
33961
- updatedSnippet = updatedSnippet.replace(matchedText, newPart);
34064
+ updatedSnippet = updatedSnippet.replace(matchedText, escapeReplacement(newPart));
33962
34065
  anyChange = true;
33963
34066
  } else {
33964
34067
  return {
@@ -33970,27 +34073,192 @@ function applyTextChangeWithPlaceholders(content, sourceSnippet, originalValue,
33970
34073
  if (!anyChange) {
33971
34074
  return { success: false, error: "No text changes detected between original and new values" };
33972
34075
  }
33973
- return { success: true, content: content.replace(sourceSnippet, updatedSnippet) };
34076
+ return writeSnippet(content, sourceSnippet, updatedSnippet, insideYaml, newText);
34077
+ }
34078
+ function isJavaScriptSource(sourcePath) {
34079
+ return !!sourcePath && JAVASCRIPT_SOURCE.test(sourcePath);
34080
+ }
34081
+ function isYamlValueSource(content, sourceSnippet, sourcePath) {
34082
+ if (!sourcePath)
34083
+ return false;
34084
+ if (YAML_DOCUMENT_SOURCE.test(sourcePath))
34085
+ return true;
34086
+ if (!YAML_FRONTMATTER_SOURCE.test(sourcePath))
34087
+ return false;
34088
+ const block = frontmatterBlock(content);
34089
+ if (!block)
34090
+ return false;
34091
+ const snippetStart = content.indexOf(sourceSnippet);
34092
+ return snippetStart >= block.start && snippetStart + sourceSnippet.length <= block.end;
34093
+ }
34094
+ function frontmatterBlock(content) {
34095
+ const opening = /^(?:\uFEFF)?(?:[ \t]*\r?\n)*(---|\+\+\+)[ \t]*\r?\n/.exec(content);
34096
+ if (!opening || opening[1] !== "---")
34097
+ return null;
34098
+ const start = opening[0].length;
34099
+ const closing = /\n(?:---|\+\+\+)[ \t]*(\r?\n|$)/.exec(content.slice(start));
34100
+ return closing ? { start, end: start + closing.index } : null;
34101
+ }
34102
+ function looksLikeMarkup(text) {
34103
+ return /<[a-zA-Z/!]/.test(text);
34104
+ }
34105
+ function decodeJsString(raw) {
34106
+ let out = "";
34107
+ for (let i2 = 0;i2 < raw.length; i2++) {
34108
+ const char = raw[i2];
34109
+ if (char !== "\\") {
34110
+ out += char;
34111
+ continue;
34112
+ }
34113
+ const next = raw[++i2];
34114
+ if (next === undefined)
34115
+ break;
34116
+ if (next === "u") {
34117
+ if (raw[i2 + 1] === "{") {
34118
+ const close = raw.indexOf("}", i2 + 2);
34119
+ if (close !== -1) {
34120
+ const code2 = parseInt(raw.slice(i2 + 2, close), 16);
34121
+ if (!Number.isNaN(code2)) {
34122
+ out += String.fromCodePoint(code2);
34123
+ i2 = close;
34124
+ continue;
34125
+ }
34126
+ }
34127
+ }
34128
+ const code = parseInt(raw.slice(i2 + 1, i2 + 5), 16);
34129
+ if (!Number.isNaN(code)) {
34130
+ out += String.fromCharCode(code);
34131
+ i2 += 4;
34132
+ continue;
34133
+ }
34134
+ }
34135
+ if (next === "x") {
34136
+ const code = parseInt(raw.slice(i2 + 1, i2 + 3), 16);
34137
+ if (!Number.isNaN(code)) {
34138
+ out += String.fromCharCode(code);
34139
+ i2 += 2;
34140
+ continue;
34141
+ }
34142
+ }
34143
+ if (next === `
34144
+ `)
34145
+ continue;
34146
+ out += JS_ESCAPES[next] ?? next;
34147
+ }
34148
+ return out;
34149
+ }
34150
+ function encodeJsString(value, quote, escapeNbsp) {
34151
+ let out = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replaceAll(quote, `\\${quote}`);
34152
+ if (escapeNbsp)
34153
+ out = out.replace(/\u00A0/g, "\\u00A0");
34154
+ if (quote === "`")
34155
+ out = out.replace(/\$\{/g, "\\${");
34156
+ return quote + out + quote;
34157
+ }
34158
+ function findStringLiterals(snippet) {
34159
+ const literals = [];
34160
+ for (let i2 = 0;i2 < snippet.length; i2++) {
34161
+ const quote = snippet[i2];
34162
+ if (quote !== "'" && quote !== '"' && quote !== "`")
34163
+ continue;
34164
+ let j = i2 + 1;
34165
+ while (j < snippet.length) {
34166
+ if (snippet[j] === "\\")
34167
+ j += 2;
34168
+ else if (snippet[j] === quote)
34169
+ break;
34170
+ else
34171
+ j++;
34172
+ }
34173
+ if (j >= snippet.length)
34174
+ break;
34175
+ const raw = snippet.slice(i2 + 1, j);
34176
+ literals.push({ start: i2, end: j + 1, quote, value: decodeJsString(raw) });
34177
+ i2 = j;
34178
+ }
34179
+ return literals;
34180
+ }
34181
+ function tryJsStringLiteralChange(sourceSnippet, resolvedOriginal, resolvedNewText) {
34182
+ const literals = findStringLiterals(sourceSnippet);
34183
+ if (literals.length === 0)
34184
+ return null;
34185
+ const splice = (start, end, replacement) => sourceSnippet.slice(0, start) + replacement + sourceSnippet.slice(end);
34186
+ for (const literal of literals) {
34187
+ if (literal.value !== resolvedOriginal)
34188
+ continue;
34189
+ const escapeNbsp = /\\u00[aA]0/.test(sourceSnippet.slice(literal.start, literal.end));
34190
+ return splice(literal.start, literal.end, encodeJsString(resolvedNewText, literal.quote, escapeNbsp));
34191
+ }
34192
+ for (let first = 0;first < literals.length; first++) {
34193
+ let combined = "";
34194
+ for (let last = first;last < literals.length; last++) {
34195
+ if (last > first) {
34196
+ const between = sourceSnippet.slice(literals[last - 1].end, literals[last].start);
34197
+ if (!/^\s*\+\s*$/.test(between))
34198
+ break;
34199
+ }
34200
+ combined += literals[last].value;
34201
+ if (last === first || combined !== resolvedOriginal)
34202
+ continue;
34203
+ const start = literals[first].start;
34204
+ const end = literals[last].end;
34205
+ const escapeNbsp = /\\u00[aA]0/.test(sourceSnippet.slice(start, end));
34206
+ return splice(start, end, encodeJsString(resolvedNewText, literals[first].quote, escapeNbsp));
34207
+ }
34208
+ }
34209
+ return null;
34210
+ }
34211
+ function pickRunAtSeam(tokens, earlier, later) {
34212
+ const seam = tokens.slice(earlier.index + 1, later.index).find((token) => token.startsWith("<"));
34213
+ return seam?.startsWith("</") ? later : earlier;
34214
+ }
34215
+ function spliceTextAcrossInlineMarkup(innerContent, originalText, newText) {
34216
+ const tokens = innerContent.split(/(<[^>]+>)/);
34217
+ const isTag = (token2) => token2.startsWith("<");
34218
+ const runs = [];
34219
+ let plain = "";
34220
+ for (let i2 = 0;i2 < tokens.length; i2++) {
34221
+ const token2 = tokens[i2];
34222
+ if (isTag(token2) || !token2)
34223
+ continue;
34224
+ runs.push({ index: i2, start: plain.length, end: plain.length + token2.length });
34225
+ plain += token2;
34226
+ }
34227
+ if (plain !== originalText)
34228
+ return null;
34229
+ let start = 0;
34230
+ while (start < originalText.length && start < newText.length && originalText[start] === newText[start])
34231
+ start++;
34232
+ let tail = 0;
34233
+ while (tail < originalText.length - start && tail < newText.length - start && originalText[originalText.length - 1 - tail] === newText[newText.length - 1 - tail])
34234
+ tail++;
34235
+ const originalEnd = originalText.length - tail;
34236
+ const newEnd = newText.length - tail;
34237
+ if (start === originalEnd && start === newEnd)
34238
+ return innerContent;
34239
+ if (start === originalEnd) {
34240
+ const inserted = newText.slice(start, newEnd);
34241
+ if (start === 0)
34242
+ return inserted + innerContent;
34243
+ if (start === originalText.length)
34244
+ return innerContent + inserted;
34245
+ }
34246
+ const fitting = runs.filter((r) => start >= r.start && originalEnd <= r.end);
34247
+ const run = start === originalEnd && fitting.length > 1 ? pickRunAtSeam(tokens, fitting[0], fitting[fitting.length - 1]) : fitting[0];
34248
+ if (!run)
34249
+ return null;
34250
+ const token = tokens[run.index];
34251
+ tokens[run.index] = token.slice(0, start - run.start) + newText.slice(start, newEnd) + token.slice(originalEnd - run.start);
34252
+ return tokens.join("");
33974
34253
  }
33975
34254
  function findTextInSnippet(snippet, decodedText) {
33976
34255
  if (snippet.includes(decodedText)) {
33977
34256
  return decodedText;
33978
34257
  }
33979
- const entityMap = [
33980
- ["&", "&amp;"],
33981
- [" ", "&nbsp;"],
33982
- [" ", "&#160;"],
33983
- ["<", "&lt;"],
33984
- [">", "&gt;"],
33985
- ['"', "&quot;"],
33986
- ["'", "&#39;"],
33987
- ["'", "&apos;"]
33988
- ];
33989
- let pattern = escapeRegex(decodedText);
33990
- for (const [char, entity] of entityMap) {
33991
- const escapedChar = escapeRegex(char);
33992
- const escapedEntity = escapeRegex(entity);
33993
- pattern = pattern.replace(new RegExp(escapedChar, "g"), `(?:${escapedChar}|${escapedEntity})`);
34258
+ let pattern = "";
34259
+ for (const char of decodedText) {
34260
+ const alternatives = ENTITY_ALTERNATIVES.get(char);
34261
+ pattern += alternatives ? `(?:${[char, ...alternatives].map(escapeRegex).join("|")})` : escapeRegex(char);
33994
34262
  }
33995
34263
  const regex = new RegExp(pattern);
33996
34264
  const match = snippet.match(regex);
@@ -34002,11 +34270,70 @@ function findTextInSnippet(snippet, decodedText) {
34002
34270
  const brMatch = snippet.match(brRegex);
34003
34271
  return brMatch && brMatch[0] !== decodedText ? brMatch[0] : null;
34004
34272
  }
34273
+ function sameCharacter(a, b2) {
34274
+ if (a === b2)
34275
+ return true;
34276
+ const spaceLike = (c) => c === " " || c === "\xA0";
34277
+ return spaceLike(a) && spaceLike(b2);
34278
+ }
34279
+ function decodeEntitiesWithOffsets(source) {
34280
+ const entityPattern = /&(?:#x([0-9a-f]+)|#(\d+)|([a-z]+));/iy;
34281
+ let text = "";
34282
+ const offsets = [];
34283
+ let i2 = 0;
34284
+ while (i2 < source.length) {
34285
+ entityPattern.lastIndex = i2;
34286
+ const match = entityPattern.exec(source);
34287
+ const decoded = match ? decodeEntityMatch(match) : undefined;
34288
+ offsets.push(i2);
34289
+ if (decoded !== undefined) {
34290
+ text += decoded;
34291
+ i2 += match[0].length;
34292
+ } else {
34293
+ text += source[i2];
34294
+ i2++;
34295
+ }
34296
+ }
34297
+ offsets.push(source.length);
34298
+ return { text, offsets };
34299
+ }
34300
+ function decodeEntityMatch(match) {
34301
+ const [, hex, dec, name] = match;
34302
+ if (hex)
34303
+ return String.fromCodePoint(parseInt(hex, 16));
34304
+ if (dec)
34305
+ return String.fromCodePoint(parseInt(dec, 10));
34306
+ return name ? NAMED_ENTITIES[name.toLowerCase()] : undefined;
34307
+ }
34308
+ function spliceIntoEntitySource(source, original, replacement) {
34309
+ const { text: decoded, offsets } = decodeEntitiesWithOffsets(source);
34310
+ if (decoded.length !== original.length)
34311
+ return null;
34312
+ for (let i2 = 0;i2 < decoded.length; i2++) {
34313
+ if (!sameCharacter(decoded[i2], original[i2]))
34314
+ return null;
34315
+ }
34316
+ let start = 0;
34317
+ while (start < original.length && start < replacement.length && sameCharacter(original[start], replacement[start])) {
34318
+ start++;
34319
+ }
34320
+ let tail = 0;
34321
+ while (tail < original.length - start && tail < replacement.length - start && sameCharacter(original[original.length - 1 - tail], replacement[replacement.length - 1 - tail]))
34322
+ tail++;
34323
+ const originalEnd = original.length - tail;
34324
+ const newEnd = replacement.length - tail;
34325
+ if (start === originalEnd && start === newEnd)
34326
+ return source;
34327
+ const head = source.slice(0, offsets[start]);
34328
+ const rest = source.slice(offsets[originalEnd]);
34329
+ return head + encodeEntitiesLike(replacement.slice(start, newEnd), source) + rest;
34330
+ }
34005
34331
  function encodeEntitiesLike(text, reference) {
34006
34332
  let result = text;
34007
34333
  if (reference.includes("&amp;")) {
34008
34334
  result = result.replace(/&/g, "&amp;");
34009
34335
  }
34336
+ result = encodeNbspLike(result, reference);
34010
34337
  if (reference.includes("&lt;")) {
34011
34338
  result = result.replace(/</g, "&lt;");
34012
34339
  }
@@ -34021,6 +34348,13 @@ function encodeEntitiesLike(text, reference) {
34021
34348
  }
34022
34349
  return result;
34023
34350
  }
34351
+ function encodeNbspLike(text, reference) {
34352
+ if (reference.includes("&nbsp;"))
34353
+ return text.replace(/\u00A0/g, "&nbsp;");
34354
+ if (reference.includes("&#160;"))
34355
+ return text.replace(/\u00A0/g, "&#160;");
34356
+ return text;
34357
+ }
34024
34358
  function resolveCmsPlaceholders(text, manifest) {
34025
34359
  const placeholderPattern = /\{\{cms:([^}]+)\}\}/g;
34026
34360
  return text.replace(placeholderPattern, (match, cmsId) => {
@@ -34082,24 +34416,164 @@ function getVisibleText(html) {
34082
34416
  `);
34083
34417
  return text.trim();
34084
34418
  }
34085
- function tryYamlValueReplacement(sourceSnippet, resolvedOriginal, resolvedNewText) {
34086
- const keyMatch = sourceSnippet.match(/^(\s*([\w][\w-]*):\s*)/);
34087
- if (!keyMatch)
34419
+ function findYamlTargets(sourceSnippet, allowQuotedKey) {
34420
+ let doc;
34421
+ try {
34422
+ doc = import_yaml6.parseDocument(sourceSnippet);
34423
+ if (doc.errors.length > 0)
34424
+ return [];
34425
+ } catch {
34426
+ return [];
34427
+ }
34428
+ const targets = [];
34429
+ import_yaml6.visit(doc, {
34430
+ Scalar(key, node, path13) {
34431
+ if (key === "key" || !node.range)
34432
+ return;
34433
+ if (typeof node.value !== "string" && typeof node.value !== "number")
34434
+ return;
34435
+ const parent = path13[path13.length - 1];
34436
+ const pair = key === "value" && parent && "key" in parent ? parent : null;
34437
+ if (!allowQuotedKey && (pair ? pair.key?.type !== "PLAIN" : node.type !== "PLAIN"))
34438
+ return;
34439
+ const ownerStart = pair ? pair.key.range[0] : dashBefore(sourceSnippet, node.range[0]);
34440
+ const [start, end] = node.range;
34441
+ const value = node.value;
34442
+ const trailingCr = typeof value === "string" && value.endsWith("\r") && sourceSnippet[end - 1] === "\r";
34443
+ targets.push({
34444
+ start,
34445
+ end: trailingCr ? end - 1 : end,
34446
+ value: trailingCr ? value.slice(0, -1) : value,
34447
+ key: pair ? String(pair.key.value) : null,
34448
+ ownerColumn: columnOf(sourceSnippet, ownerStart)
34449
+ });
34450
+ }
34451
+ });
34452
+ return targets;
34453
+ }
34454
+ function dashBefore(sourceSnippet, valueStart) {
34455
+ const dash = sourceSnippet.lastIndexOf("-", valueStart);
34456
+ return dash < 0 ? valueStart : dash;
34457
+ }
34458
+ function columnOf(sourceSnippet, offset) {
34459
+ return offset - (sourceSnippet.lastIndexOf(`
34460
+ `, offset - 1) + 1);
34461
+ }
34462
+ function tryYamlValueReplacement(sourceSnippet, resolvedOriginal, resolvedNewText, allowQuotedKey = false) {
34463
+ const matches = findYamlTargets(sourceSnippet, allowQuotedKey).filter((candidate) => String(candidate.value) === resolvedOriginal);
34464
+ if (matches.length !== 1)
34088
34465
  return null;
34466
+ const target = matches[0];
34467
+ const fieldIsTyped = resolvesAsNonString(sourceSnippet.slice(target.start, target.end));
34468
+ const replacement = typeof target.value === "number" && YAML_PLAIN_NUMBER.test(resolvedNewText) ? resolvedNewText : serializeYamlValue(resolvedNewText, target.key, target.ownerColumn, fieldIsTyped);
34469
+ if (replacement === null)
34470
+ return null;
34471
+ const updated = sourceSnippet.slice(0, target.start) + replacement + sourceSnippet.slice(target.end);
34472
+ return yamlValueRoundTrips(updated, resolvedNewText) ? updated : null;
34473
+ }
34474
+ function yamlValueRoundTrips(updatedSnippet, expected) {
34475
+ return findYamlTargets(normalizeCr(updatedSnippet), true).some((target) => scalarSays(target.value, expected));
34476
+ }
34477
+ function scalarSays(actual, expected) {
34478
+ if (String(actual) === expected)
34479
+ return true;
34480
+ if (typeof actual !== "number")
34481
+ return false;
34482
+ const parsed = tryParseYaml(expected);
34483
+ return typeof parsed === "number" && parsed === actual;
34484
+ }
34485
+ function writeSnippet(content, sourceSnippet, updatedSnippet, insideYaml, expected) {
34486
+ if (insideYaml && !yamlSnippetSurvives(sourceSnippet, updatedSnippet, expected)) {
34487
+ return {
34488
+ success: false,
34489
+ error: `"${expected.substring(0, 50)}" cannot be written into this frontmatter field without breaking it`
34490
+ };
34491
+ }
34492
+ return { success: true, content: content.replace(sourceSnippet, escapeReplacement(updatedSnippet)) };
34493
+ }
34494
+ function yamlSnippetSurvives(sourceSnippet, updatedSnippet, expected) {
34495
+ const before = yamlScalars(sourceSnippet);
34496
+ const after = yamlScalars(updatedSnippet);
34497
+ if (!before || !after || before.length !== after.length)
34498
+ return false;
34499
+ let changed = 0;
34500
+ for (let i2 = 0;i2 < before.length; i2++) {
34501
+ if (before[i2].owner !== after[i2].owner)
34502
+ return false;
34503
+ const wrote = after[i2].value;
34504
+ if (String(before[i2].value) === String(wrote))
34505
+ continue;
34506
+ changed++;
34507
+ const saysIt = expected === "" ? wrote === "" : scalarSays(wrote, expected) || String(wrote).includes(expected);
34508
+ if (!saysIt)
34509
+ return false;
34510
+ }
34511
+ return changed > 0;
34512
+ }
34513
+ function yamlScalars(snippet) {
34089
34514
  try {
34090
- const parsed = import_yaml6.parse(sourceSnippet);
34091
- if (parsed == null || typeof parsed !== "object")
34092
- return null;
34093
- const value = parsed[keyMatch[2]];
34094
- if (typeof value !== "string" && typeof value !== "number")
34515
+ const doc = import_yaml6.parseDocument(normalizeCr(snippet));
34516
+ if (doc.errors.length > 0)
34095
34517
  return null;
34096
- if (String(value) !== resolvedOriginal)
34518
+ const scalars = [];
34519
+ import_yaml6.visit(doc, {
34520
+ Scalar(key, node) {
34521
+ scalars.push({ owner: String(key), value: node.value });
34522
+ }
34523
+ });
34524
+ return scalars;
34525
+ } catch {
34526
+ return null;
34527
+ }
34528
+ }
34529
+ function normalizeCr(snippet) {
34530
+ return snippet.replace(/\r(?=\n|$)/g, "");
34531
+ }
34532
+ function serializeYamlValue(value, key, ownerColumn, fieldIsTyped = false) {
34533
+ let body = renderYamlValue(value, key, undefined);
34534
+ if (!fieldIsTyped && body !== null && !body.includes(`
34535
+ `) && resolvesAsNonString(body)) {
34536
+ body = renderYamlValue(value, key, "QUOTE_DOUBLE");
34537
+ }
34538
+ if (body === null || !body.includes(`
34539
+ `))
34540
+ return body;
34541
+ const pad = " ".repeat(ownerColumn);
34542
+ const [header, ...rest] = body.split(`
34543
+ `);
34544
+ return [header, ...rest.map((line) => line === "" ? line : pad + line)].join(`
34545
+ `);
34546
+ }
34547
+ function renderYamlValue(value, key, defaultStringType) {
34548
+ const options = { lineWidth: 0, ...defaultStringType ? { defaultStringType, defaultKeyType: "PLAIN" } : {} };
34549
+ const rendered = key === null ? import_yaml6.stringify([value], options) : import_yaml6.stringify({ [key]: value }, options);
34550
+ let valueStart;
34551
+ try {
34552
+ const doc = import_yaml6.parseDocument(rendered);
34553
+ const first = doc.contents?.items?.[0];
34554
+ const node = key === null ? first : first?.value;
34555
+ if (!node?.range)
34097
34556
  return null;
34557
+ valueStart = node.range[0];
34098
34558
  } catch {
34099
34559
  return null;
34100
34560
  }
34101
- const serialized = import_yaml6.stringify(resolvedNewText, { lineWidth: 0 }).trimEnd();
34102
- return `${keyMatch[1]}${serialized}`;
34561
+ const text = rendered.slice(valueStart);
34562
+ return /^[|>]\d*\+/.test(text) ? text : text.replace(/\n$/, "");
34563
+ }
34564
+ function resolvesAsNonString(scalar) {
34565
+ try {
34566
+ return typeof import_yaml6.parse(scalar, { version: "1.1" }) !== "string";
34567
+ } catch {
34568
+ return false;
34569
+ }
34570
+ }
34571
+ function tryParseYaml(text) {
34572
+ try {
34573
+ return import_yaml6.parse(text);
34574
+ } catch {
34575
+ return;
34576
+ }
34103
34577
  }
34104
34578
  function tryDataFileValueReplacement(content, sourceSnippet, originalValue, newValue, sourceLine) {
34105
34579
  const doubleQuoted = `"${originalValue}"`;
@@ -34115,7 +34589,7 @@ function tryDataFileValueReplacement(content, sourceSnippet, originalValue, newV
34115
34589
  } else {
34116
34590
  return null;
34117
34591
  }
34118
- const updatedSnippet = sourceSnippet.replace(quotedOriginal, quotedNew);
34592
+ const updatedSnippet = sourceSnippet.replace(quotedOriginal, escapeReplacement(quotedNew));
34119
34593
  if (updatedSnippet === sourceSnippet)
34120
34594
  return null;
34121
34595
  let searchStart = 0;
@@ -34179,14 +34653,14 @@ function tryBrNormalizedChange(sourceSnippet, resolvedOriginal, resolvedNewText)
34179
34653
  const leadingWs = raw.slice(0, raw.indexOf(trimmed));
34180
34654
  const trailingWs = raw.slice(raw.indexOf(trimmed) + trimmed.length);
34181
34655
  const newRaw = leadingWs + newSegment + trailingWs;
34182
- result = result.replace(raw, newRaw);
34656
+ result = result.replace(raw, escapeReplacement(newRaw));
34183
34657
  break;
34184
34658
  }
34185
34659
  }
34186
34660
  }
34187
34661
  return result !== sourceSnippet ? result : null;
34188
34662
  }
34189
- var import_node_html_parser, import_yaml6, ASSET_IMPORT_EXT_RE, REMOTE_FETCH_TIMEOUT_MS = 15000, REMOTE_FETCH_MAX_BYTES, QUOTED_LITERAL_DELIMITERS;
34663
+ var import_node_html_parser, import_yaml6, ASSET_IMPORT_EXT_RE, REMOTE_FETCH_TIMEOUT_MS = 15000, REMOTE_FETCH_MAX_BYTES, QUOTED_LITERAL_DELIMITERS, JAVASCRIPT_SOURCE, YAML_DOCUMENT_SOURCE, YAML_FRONTMATTER_SOURCE, JS_ESCAPES, ENTITY_ALTERNATIVES, NAMED_ENTITIES, YAML_PLAIN_NUMBER;
34190
34664
  var init_source_writer = __esm(() => {
34191
34665
  init_astro_image_paths();
34192
34666
  init_config();
@@ -34197,6 +34671,37 @@ var init_source_writer = __esm(() => {
34197
34671
  ASSET_IMPORT_EXT_RE = /\.(jpe?g|png|gif|webp|avif|svg|ico|bmp|tiff?)$/i;
34198
34672
  REMOTE_FETCH_MAX_BYTES = 50 * 1024 * 1024;
34199
34673
  QUOTED_LITERAL_DELIMITERS = [`'`, `"`, "`"];
34674
+ JAVASCRIPT_SOURCE = /\.(astro|[cm]?[jt]sx?)$/i;
34675
+ YAML_DOCUMENT_SOURCE = /\.ya?ml$/i;
34676
+ YAML_FRONTMATTER_SOURCE = /\.(md|mdx|markdown)$/i;
34677
+ JS_ESCAPES = {
34678
+ n: `
34679
+ `,
34680
+ r: "\r",
34681
+ t: "\t",
34682
+ b: "\b",
34683
+ f: "\f",
34684
+ v: "\v",
34685
+ 0: "\x00"
34686
+ };
34687
+ ENTITY_ALTERNATIVES = new Map([
34688
+ ["&", ["&amp;"]],
34689
+ ["\xA0", ["&nbsp;", "&#160;", " "]],
34690
+ [" ", ["&nbsp;", "&#160;"]],
34691
+ ["<", ["&lt;"]],
34692
+ [">", ["&gt;"]],
34693
+ ['"', ["&quot;"]],
34694
+ ["'", ["&#39;", "&apos;"]]
34695
+ ]);
34696
+ NAMED_ENTITIES = {
34697
+ amp: "&",
34698
+ lt: "<",
34699
+ gt: ">",
34700
+ quot: '"',
34701
+ apos: "'",
34702
+ nbsp: "\xA0"
34703
+ };
34704
+ YAML_PLAIN_NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
34200
34705
  });
34201
34706
 
34202
34707
  // ../cms/src/handlers/api-routes.ts