scribe-cms 0.0.10 → 0.0.11

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/cli/index.js CHANGED
@@ -5,6 +5,9 @@ import fs2 from 'fs';
5
5
  import Database from 'better-sqlite3';
6
6
  import { z } from 'zod';
7
7
  import matter from 'gray-matter';
8
+ import remarkMdx from 'remark-mdx';
9
+ import remarkParse from 'remark-parse';
10
+ import { unified } from 'unified';
8
11
  import { createJiti } from 'jiti';
9
12
  import { createHash } from 'crypto';
10
13
  import { GoogleGenAI } from '@google/genai';
@@ -712,6 +715,140 @@ function bulkLoadTranslations(db) {
712
715
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
713
716
  }
714
717
 
718
+ // src/translate/validate-mdx-body.ts
719
+ init_esm_shims();
720
+
721
+ // src/translate/normalize-mdx-body.ts
722
+ init_esm_shims();
723
+ function needsMdxEscapeNormalization(body) {
724
+ return /\\n[ \t/>]|<[\w.-][^>]*\\n/.test(body);
725
+ }
726
+ function normalizeTranslatedMdxBody(body) {
727
+ if (!needsMdxEscapeNormalization(body)) {
728
+ return { body, adjusted: false };
729
+ }
730
+ const normalized = body.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
731
+ return { body: normalized, adjusted: normalized !== body };
732
+ }
733
+
734
+ // src/translate/sanitize-mdx-jsx.ts
735
+ init_esm_shims();
736
+ function sanitizeMdxJsxAttributeQuotes(body) {
737
+ let adjusted = false;
738
+ let out = "";
739
+ let i = 0;
740
+ while (i < body.length) {
741
+ if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
742
+ out += body[i];
743
+ i += 1;
744
+ continue;
745
+ }
746
+ const tagStart = i;
747
+ i += 1;
748
+ while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
749
+ const tagName = body.slice(tagStart + 1, i);
750
+ let tagOut = `<${tagName}`;
751
+ let tagAdjusted = false;
752
+ while (i < body.length && body[i] !== ">" && body[i] !== "/") {
753
+ while (i < body.length && /\s/.test(body[i] ?? "")) {
754
+ tagOut += body[i];
755
+ i += 1;
756
+ }
757
+ if (i >= body.length || body[i] === ">" || body[i] === "/") break;
758
+ const attrStart = i;
759
+ while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
760
+ body.slice(attrStart, i);
761
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
762
+ if (body[i] !== "=") {
763
+ tagOut += body.slice(attrStart, i);
764
+ continue;
765
+ }
766
+ tagOut += body.slice(attrStart, i);
767
+ tagOut += "=";
768
+ i += 1;
769
+ while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
770
+ const quote = body[i];
771
+ if (quote !== '"' && quote !== "'") {
772
+ continue;
773
+ }
774
+ if (quote === "'") {
775
+ const valStart2 = i + 1;
776
+ i += 1;
777
+ while (i < body.length) {
778
+ if (body[i] === "\\") {
779
+ i += 2;
780
+ continue;
781
+ }
782
+ if (body[i] === "'") break;
783
+ i += 1;
784
+ }
785
+ tagOut += body.slice(valStart2 - 1, i + 1);
786
+ i += 1;
787
+ continue;
788
+ }
789
+ const valStart = i + 1;
790
+ i += 1;
791
+ let closeIdx = -1;
792
+ for (let scan = valStart; scan < body.length; scan += 1) {
793
+ if (body[scan] !== '"') continue;
794
+ let j = scan + 1;
795
+ while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
796
+ if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
797
+ closeIdx = scan;
798
+ break;
799
+ }
800
+ }
801
+ if (closeIdx === -1) {
802
+ tagOut += body.slice(valStart - 1, i);
803
+ break;
804
+ }
805
+ const value = body.slice(valStart, closeIdx);
806
+ const hasInternalQuote = value.includes('"');
807
+ if (hasInternalQuote) {
808
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
809
+ tagOut += `'${escaped}'`;
810
+ tagAdjusted = true;
811
+ } else {
812
+ tagOut += `"${value}"`;
813
+ }
814
+ i = closeIdx + 1;
815
+ }
816
+ if (tagAdjusted) adjusted = true;
817
+ tagOut += body[i] ?? "";
818
+ out += tagOut;
819
+ i += 1;
820
+ }
821
+ return { body: out, adjusted };
822
+ }
823
+
824
+ // src/translate/validate-mdx-body.ts
825
+ var parser = unified().use(remarkParse).use(remarkMdx);
826
+ function prepareTranslatedMdxBody(body) {
827
+ const normalized = normalizeTranslatedMdxBody(body);
828
+ const sanitized = sanitizeMdxJsxAttributeQuotes(normalized.body);
829
+ return {
830
+ body: sanitized.body,
831
+ adjusted: normalized.adjusted || sanitized.adjusted
832
+ };
833
+ }
834
+ function validateMdxBody(body) {
835
+ try {
836
+ parser.parse(body);
837
+ return { ok: true };
838
+ } catch (error) {
839
+ const message = error instanceof Error ? error.message : String(error);
840
+ return { ok: false, error: message };
841
+ }
842
+ }
843
+ function assertValidTranslatedMdxBody(body) {
844
+ const prepared = prepareTranslatedMdxBody(body);
845
+ const validated = validateMdxBody(prepared.body);
846
+ if (!validated.ok) {
847
+ throw new Error(`MDX validation failed: ${validated.error}`);
848
+ }
849
+ return prepared;
850
+ }
851
+
715
852
  // src/loader/normalize-en.ts
716
853
  init_esm_shims();
717
854
  function normalizeEnFrontmatter(data) {
@@ -833,7 +970,7 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
833
970
  noindex: seo.noindex,
834
971
  canonicalPathOverride: seo.canonicalPathOverride,
835
972
  frontmatter,
836
- content: row.body
973
+ content: prepareTranslatedMdxBody(row.body).body
837
974
  };
838
975
  }
839
976
  var DEV_REVALIDATE_MS = 1500;
@@ -1825,15 +1962,27 @@ function validateProject(config) {
1825
1962
  message: issue.message
1826
1963
  });
1827
1964
  }
1965
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1828
1966
  for (const issue of validateDocumentAssets(config, {
1829
1967
  contentType: type.id,
1830
1968
  enSlug,
1831
1969
  locale,
1832
1970
  frontmatter: localeFm,
1833
- body: row.body
1971
+ body: preparedBody
1834
1972
  })) {
1835
1973
  issues.push(issue);
1836
1974
  }
1975
+ const mdxValidation = validateMdxBody(preparedBody);
1976
+ if (!mdxValidation.ok) {
1977
+ issues.push({
1978
+ level: "error",
1979
+ contentType: type.id,
1980
+ enSlug,
1981
+ locale,
1982
+ field: "body",
1983
+ message: `Invalid MDX: ${mdxValidation.error}`
1984
+ });
1985
+ }
1837
1986
  }
1838
1987
  }
1839
1988
  try {
@@ -2201,24 +2350,21 @@ function buildGeminiResponseSchema(schema, slugStrategy) {
2201
2350
 
2202
2351
  // src/translate/resolve-translate-config.ts
2203
2352
  init_esm_shims();
2204
- function slugStrategyRules(slugStrategy, preserveTerms) {
2353
+ var BUILTIN_TRANSLATE_RULES = [
2354
+ "Do not translate brand or product names unless the brand has a well-known localized name in the target market (rare). Keep the original spelling and capitalization.",
2355
+ "Return the MDX body with real line breaks; do not use JSON escape sequences like \\n or \\t in the body string.",
2356
+ 'In JSX attributes (e.g. FaqItem question="..."), use single quotes when the value contains double-quote characters (e.g. Hebrew \u05D3\u05D5\u05D0"\u05DC), or escape them as \\".'
2357
+ ];
2358
+ function slugStrategyRules(slugStrategy) {
2205
2359
  if (slugStrategy === "localized") {
2206
- const rules = [
2360
+ return [
2207
2361
  "Provide a URL slug in JSON field `slug` that is Localized into the target language.",
2208
2362
  "Base the slug on the meaning of the translated title \u2014 do NOT reuse the English slug words.",
2209
2363
  "Slug MUST be ASCII only: a-z, 0-9, hyphens. No uppercase, accents, underscores, or spaces.",
2210
2364
  "For non-Latin languages, write the words in the target language and transliterate them into Latin script (e.g. Russian -> romanized Russian, not English).",
2211
2365
  "Do NOT append locale codes to the slug (e.g. -fr, -he, -zh-cn). Locale routing is handled by the URL prefix, not the slug.",
2212
- 'In JSX attributes (e.g. FaqItem question="..."), use single quotes when the value contains double-quote characters (e.g. Hebrew \u05D3\u05D5\u05D0"\u05DC), or escape them as \\".'
2366
+ "Preserve 4-digit years and proper nouns in slugs."
2213
2367
  ];
2214
- if (preserveTerms?.length) {
2215
- rules.push(
2216
- `Preserve these terms verbatim in slugs (lowercase): ${preserveTerms.join(", ")}.`
2217
- );
2218
- } else {
2219
- rules.push("Preserve 4-digit years and proper nouns in slugs.");
2220
- }
2221
- return rules;
2222
2368
  }
2223
2369
  return ["Do NOT output a slug \u2014 slugStrategy is fixed."];
2224
2370
  }
@@ -2231,9 +2377,10 @@ function mergeTranslateConfig(project, type, slugStrategy) {
2231
2377
  promptOverride: type?.prompt ?? project?.prompt,
2232
2378
  context: mergeContext(project, type),
2233
2379
  rules: [
2380
+ ...BUILTIN_TRANSLATE_RULES,
2234
2381
  ...project?.rules ?? [],
2235
2382
  ...type?.rules ?? [],
2236
- ...slugStrategyRules(slugStrategy, project?.slugPreserveTerms)
2383
+ ...slugStrategyRules(slugStrategy)
2237
2384
  ],
2238
2385
  model: type?.model ?? project?.defaultModel
2239
2386
  };
@@ -2242,96 +2389,6 @@ function resolveTranslateConfig(config, type) {
2242
2389
  return mergeTranslateConfig(config.translate, type.translate, type.slugStrategy);
2243
2390
  }
2244
2391
 
2245
- // src/translate/sanitize-mdx-jsx.ts
2246
- init_esm_shims();
2247
- function sanitizeMdxJsxAttributeQuotes(body) {
2248
- let adjusted = false;
2249
- let out = "";
2250
- let i = 0;
2251
- while (i < body.length) {
2252
- if (body[i] !== "<" || !/[\w.]/.test(body[i + 1] ?? "")) {
2253
- out += body[i];
2254
- i += 1;
2255
- continue;
2256
- }
2257
- const tagStart = i;
2258
- i += 1;
2259
- while (i < body.length && /[\w.-]/.test(body[i] ?? "")) i += 1;
2260
- const tagName = body.slice(tagStart + 1, i);
2261
- let tagOut = `<${tagName}`;
2262
- let tagAdjusted = false;
2263
- while (i < body.length && body[i] !== ">" && body[i] !== "/") {
2264
- while (i < body.length && /\s/.test(body[i] ?? "")) {
2265
- tagOut += body[i];
2266
- i += 1;
2267
- }
2268
- if (i >= body.length || body[i] === ">" || body[i] === "/") break;
2269
- const attrStart = i;
2270
- while (i < body.length && /[\w:.-]/.test(body[i] ?? "")) i += 1;
2271
- body.slice(attrStart, i);
2272
- while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2273
- if (body[i] !== "=") {
2274
- tagOut += body.slice(attrStart, i);
2275
- continue;
2276
- }
2277
- tagOut += body.slice(attrStart, i);
2278
- tagOut += "=";
2279
- i += 1;
2280
- while (i < body.length && /\s/.test(body[i] ?? "")) i += 1;
2281
- const quote = body[i];
2282
- if (quote !== '"' && quote !== "'") {
2283
- continue;
2284
- }
2285
- if (quote === "'") {
2286
- const valStart2 = i + 1;
2287
- i += 1;
2288
- while (i < body.length) {
2289
- if (body[i] === "\\") {
2290
- i += 2;
2291
- continue;
2292
- }
2293
- if (body[i] === "'") break;
2294
- i += 1;
2295
- }
2296
- tagOut += body.slice(valStart2 - 1, i + 1);
2297
- i += 1;
2298
- continue;
2299
- }
2300
- const valStart = i + 1;
2301
- i += 1;
2302
- let closeIdx = -1;
2303
- for (let scan = valStart; scan < body.length; scan += 1) {
2304
- if (body[scan] !== '"') continue;
2305
- let j = scan + 1;
2306
- while (j < body.length && /\s/.test(body[j] ?? "")) j += 1;
2307
- if (j < body.length && (body[j] === ">" || body[j] === "/" || /[a-zA-Z]/.test(body[j] ?? ""))) {
2308
- closeIdx = scan;
2309
- break;
2310
- }
2311
- }
2312
- if (closeIdx === -1) {
2313
- tagOut += body.slice(valStart - 1, i);
2314
- break;
2315
- }
2316
- const value = body.slice(valStart, closeIdx);
2317
- const hasInternalQuote = value.includes('"');
2318
- if (hasInternalQuote) {
2319
- const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2320
- tagOut += `'${escaped}'`;
2321
- tagAdjusted = true;
2322
- } else {
2323
- tagOut += `"${value}"`;
2324
- }
2325
- i = closeIdx + 1;
2326
- }
2327
- if (tagAdjusted) adjusted = true;
2328
- tagOut += body[i] ?? "";
2329
- out += tagOut;
2330
- i += 1;
2331
- }
2332
- return { body: out, adjusted };
2333
- }
2334
-
2335
2392
  // src/translate/validate-translation.ts
2336
2393
  init_esm_shims();
2337
2394
  function formatZodIssues(error) {
@@ -2466,7 +2523,7 @@ async function translatePage(config, item, options = {}) {
2466
2523
  if (!validated.ok) {
2467
2524
  throw new Error(`Translation validation failed: ${validated.error}`);
2468
2525
  }
2469
- const { body: translatedBody, adjusted: mdxAdjusted } = sanitizeMdxJsxAttributeQuotes(
2526
+ const { body: translatedBody, adjusted: mdxAdjusted } = assertValidTranslatedMdxBody(
2470
2527
  result.parsed.body
2471
2528
  );
2472
2529
  const writeDb = openStore(config, "readwrite");