@contentrain/wp-import 0.10.5 → 0.11.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.d.mts CHANGED
@@ -84,9 +84,25 @@ interface RestImportResult {
84
84
  declare function fetchRestRawIR(options: RestImportOptions): Promise<RestImportResult>;
85
85
  //#endregion
86
86
  //#region src/contentrain.d.ts
87
+ /** A post whose imported slug is not the address it was served at: it gets a 301 from `from`. */
88
+ interface SlugMove {
89
+ wp_id: number;
90
+ type: string;
91
+ /** The path the source site served it at (its `link`), when the source named one. */
92
+ from: string | null;
93
+ /** The slug the entry was written under. */
94
+ slug: string;
95
+ /**
96
+ * `empty`: the source slug left no word, `collision`: another entry of the same type already holds it,
97
+ * `rewritten`: characters outside the slug alphabet (ZWNJ/ZWJ, `・`, an NFD spelling...) were changed.
98
+ */
99
+ reason: 'empty' | 'collision' | 'rewritten';
100
+ }
87
101
  interface ImportReport {
88
102
  slug_rewritten: number;
89
103
  slug_fallback: number;
104
+ /** Posts whose address changed (empty or colliding slug): each needs a 301 from its old address. */
105
+ slug_moves: SlugMove[];
90
106
  title_fallback: number;
91
107
  meta_fields: Record<string, string>;
92
108
  acf_fields: Record<string, string>;
@@ -183,6 +199,13 @@ declare const PLUGIN_META: RegExp;
183
199
  /** Taxonomy → model id. */
184
200
  declare const taxModelId: (t: string) => string;
185
201
  declare const slugify: (s: unknown) => string;
202
+ /**
203
+ * A WordPress `post_name` as the address it is: percent-encoded UTF-8 decoded, NFC, lowercase, anything outside letters,
204
+ * digits and marks (the contract's slug alphabet) a hyphen. Nothing is transliterated: `日本語`, `ışık` and `straße`
205
+ * stay what the source site served, because the slug is the page's address. ASCII comes out exactly as `slugify`
206
+ * gives it. Empty when nothing of a word is left, or when the slug would not fit a file name (200 bytes).
207
+ */
208
+ declare const addressSlug: (raw: string) => string;
186
209
  //#endregion
187
210
  //#region src/delta.d.ts
188
211
  /** A `.contentrain` store as a file map (repo-relative path → file text), with the source map it was built from. */
@@ -227,5 +250,5 @@ declare function planSourceDelta(input: PlanSourceDeltaInput): SourceDeltaPlan;
227
250
  /** The plan as text, for a dry run: one line per record, then conflicts, unmapped records, redirects and warnings. */
228
251
  declare function formatSourceDeltaReport(plan: SourceDeltaPlan): string;
229
252
  //#endregion
230
- export { type ContentrainResult, DEFAULT_IMPORTERS, DEFAULT_TAXONOMIES, type DeltaStore, type ImportReport, PLUGIN_META, type PlanSourceDeltaInput, type RestImportOptions, type RestImportResult, SKIP_TYPES, type UnserializeResult, type WxrParseResult, type WxrStats, buildCommentsExport, canon, fetchRestRawIR, formatSourceDeltaReport, hexId, looksSerialized, parseWxr, phpUnserialize, planSourceDelta, rawToContentrain, selectComments, slugify, strip, summarizeComments, taxModelId, tryUnserialize };
253
+ export { type ContentrainResult, DEFAULT_IMPORTERS, DEFAULT_TAXONOMIES, type DeltaStore, type ImportReport, PLUGIN_META, type PlanSourceDeltaInput, type RestImportOptions, type RestImportResult, SKIP_TYPES, type UnserializeResult, type WxrParseResult, type WxrStats, addressSlug, buildCommentsExport, canon, fetchRestRawIR, formatSourceDeltaReport, hexId, looksSerialized, parseWxr, phpUnserialize, planSourceDelta, rawToContentrain, selectComments, slugify, strip, summarizeComments, taxModelId, tryUnserialize };
231
254
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/wxr.ts","../src/rest.ts","../src/contentrain.ts","../src/comments.ts","../src/php-unserialize.ts","../src/core.ts","../src/delta.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/wxr.ts","../src/rest.ts","../src/contentrain.ts","../src/comments.ts","../src/php-unserialize.ts","../src/core.ts","../src/delta.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -181,6 +181,16 @@ const PLUGIN_META = /^(jetpack|wpdc|discourse|footnotes|inline_featured|spay_|ad
181
181
  /** Taxonomy → model id. */
182
182
  const taxModelId = (t) => t === "post_tag" ? "tags" : t === "category" ? "categories" : t.replace(/_/g, "-");
183
183
  const slugify = (s) => String(s ?? "").normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
184
+ /**
185
+ * A WordPress `post_name` as the address it is: percent-encoded UTF-8 decoded, NFC, lowercase, anything outside letters,
186
+ * digits and marks (the contract's slug alphabet) a hyphen. Nothing is transliterated: `日本語`, `ışık` and `straße`
187
+ * stay what the source site served, because the slug is the page's address. ASCII comes out exactly as `slugify`
188
+ * gives it. Empty when nothing of a word is left, or when the slug would not fit a file name (200 bytes).
189
+ */
190
+ const addressSlug = (raw) => {
191
+ const slug = decodeSlug(raw).normalize("NFC").toLowerCase().normalize("NFC").replace(/[^\p{Ll}\p{Lo}\p{Lm}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "");
192
+ return new TextEncoder().encode(slug).length > 200 ? "" : slug;
193
+ };
184
194
  const decodeSlug = (s) => {
185
195
  try {
186
196
  return decodeURIComponent(s);
@@ -2102,6 +2112,19 @@ async function fetchRestRawIR(options) {
2102
2112
  }
2103
2113
  //#endregion
2104
2114
  //#region src/contentrain.ts
2115
+ const utf8Length = (s) => new TextEncoder().encode(s).length;
2116
+ /** `s` cut on a code-point boundary to at most `max` UTF-8 bytes, without a trailing hyphen. */
2117
+ const fitBytes = (s, max) => {
2118
+ let out = "";
2119
+ let bytes = 0;
2120
+ for (const ch of s) {
2121
+ const n = utf8Length(ch);
2122
+ if (bytes + n > max) break;
2123
+ out += ch;
2124
+ bytes += n;
2125
+ }
2126
+ return out.replace(/-+$/, "");
2127
+ };
2105
2128
  const pick = (o, keys) => Object.fromEntries(Object.entries(o).filter(([k, v]) => keys.has(k) && v !== void 0 && v !== null && v !== ""));
2106
2129
  const typeModelId = (t) => t === "post" ? "posts" : t === "page" ? "pages" : t.replace(/_/g, "-");
2107
2130
  const domainOf = (t) => t === "post" ? "blog" : t === "page" ? "site" : "custom";
@@ -2162,6 +2185,7 @@ function rawToContentrain(raw, opts) {
2162
2185
  const report = {
2163
2186
  slug_rewritten: 0,
2164
2187
  slug_fallback: 0,
2188
+ slug_moves: [],
2165
2189
  title_fallback: 0,
2166
2190
  meta_fields: {},
2167
2191
  acf_fields: {},
@@ -2200,15 +2224,44 @@ function rawToContentrain(raw, opts) {
2200
2224
  const termId = (tax, slug) => hexId(`${tax}:${slug}`);
2201
2225
  const usedSlugs = /* @__PURE__ */ new Map();
2202
2226
  const postSlug = (p) => {
2203
- let s = slugify(decodeSlug(p.slug));
2227
+ let s = addressSlug(p.slug);
2228
+ const moved = (reason, slug) => {
2229
+ let from = null;
2230
+ try {
2231
+ from = p.link ? new URL(p.link).pathname : null;
2232
+ } catch {
2233
+ from = null;
2234
+ }
2235
+ report.slug_moves.push({
2236
+ wp_id: p.id,
2237
+ type: p.type,
2238
+ from,
2239
+ slug,
2240
+ reason
2241
+ });
2242
+ };
2243
+ let empty = false;
2244
+ let rewritten = false;
2204
2245
  if (!s) {
2205
2246
  s = `${p.type}-${p.id}`;
2247
+ empty = true;
2206
2248
  report.slug_fallback++;
2207
- } else if (s !== p.slug) report.slug_rewritten++;
2249
+ } else if (s !== decodeSlug(p.slug)) {
2250
+ report.slug_rewritten++;
2251
+ rewritten = s !== decodeSlug(p.slug).toLowerCase();
2252
+ }
2208
2253
  const key = multilingual ? `${p.type}:${postLocale(p)}:${s}` : `${p.type}:${s}`;
2209
2254
  const n = usedSlugs.get(key) ?? 0;
2210
2255
  usedSlugs.set(key, n + 1);
2211
- return n ? `${s}-${p.id}` : s;
2256
+ if (n) {
2257
+ const suffix = `-${p.id}`;
2258
+ const unique = `${fitBytes(s, 200 - utf8Length(suffix))}${suffix}`;
2259
+ moved("collision", unique);
2260
+ return unique;
2261
+ }
2262
+ if (empty) moved("empty", s);
2263
+ else if (rewritten) moved("rewritten", s);
2264
+ return s;
2212
2265
  };
2213
2266
  const slugOf = /* @__PURE__ */ new Map();
2214
2267
  const postEntry = /* @__PURE__ */ new Map();
@@ -2448,7 +2501,7 @@ function rawToContentrain(raw, opts) {
2448
2501
  const id = termId(tax, t.slug);
2449
2502
  contents[mid][id] = pick({
2450
2503
  name: strip(t.name),
2451
- slug: slugify(decodeSlug(t.slug)) || `term-${t.id}`,
2504
+ slug: addressSlug(t.slug) || `term-${t.id}`,
2452
2505
  description: strip(t.description),
2453
2506
  parent: t.parent && t.parent_resolved ? termId(tax, t.parent) : null,
2454
2507
  wp_id: t.id
@@ -2469,7 +2522,7 @@ function rawToContentrain(raw, opts) {
2469
2522
  const id = termId(t.taxonomy, t.slug);
2470
2523
  contents[mid][id] ??= {
2471
2524
  name: strip(t.name),
2472
- slug: slugify(t.slug)
2525
+ slug: addressSlug(t.slug) || `term-${hexId(`${t.taxonomy}:${t.slug}`).slice(0, 8)}`
2473
2526
  };
2474
2527
  metas[mid][id] ??= importMeta("published");
2475
2528
  }
@@ -2565,7 +2618,7 @@ function rawToContentrain(raw, opts) {
2565
2618
  const mediaSlugs = /* @__PURE__ */ new Set();
2566
2619
  for (const a of raw.attachments) {
2567
2620
  const id = hexId(`media:${a.id}`);
2568
- let slug = slugify(decodeSlug(a.slug)) || `media-${a.id}`;
2621
+ let slug = addressSlug(a.slug) || `media-${a.id}`;
2569
2622
  if (mediaSlugs.has(slug)) slug = `${slug}-${a.id}`;
2570
2623
  mediaSlugs.add(slug);
2571
2624
  const target = a.parent ? postEntry.get(a.parent) : void 0;
@@ -2889,7 +2942,7 @@ function rawToContentrain(raw, opts) {
2889
2942
  const mid = hexId(`menus:${m.slug}`);
2890
2943
  contents.menus[mid] = {
2891
2944
  name: strip(m.name),
2892
- slug: slugify(m.slug) || `menu-${m.id}`,
2945
+ slug: addressSlug(m.slug) || `menu-${m.id}`,
2893
2946
  items: m.items.map((i) => itemRef(i.id)),
2894
2947
  ...m.id !== null && m.id > 0 ? { wp_id: m.id } : {},
2895
2948
  ...m.locations?.length ? { locations: m.locations } : {}
@@ -3492,6 +3545,6 @@ function formatSourceDeltaReport(plan) {
3492
3545
  return `${lines.join("\n")}\n`;
3493
3546
  }
3494
3547
  //#endregion
3495
- export { DEFAULT_IMPORTERS, DEFAULT_TAXONOMIES, PLUGIN_META, SKIP_TYPES, buildCommentsExport, canon, fetchRestRawIR, formatSourceDeltaReport, hexId, looksSerialized, parseWxr, phpUnserialize, planSourceDelta, rawToContentrain, selectComments, slugify, strip, summarizeComments, taxModelId, tryUnserialize };
3548
+ export { DEFAULT_IMPORTERS, DEFAULT_TAXONOMIES, PLUGIN_META, SKIP_TYPES, addressSlug, buildCommentsExport, canon, fetchRestRawIR, formatSourceDeltaReport, hexId, looksSerialized, parseWxr, phpUnserialize, planSourceDelta, rawToContentrain, selectComments, slugify, strip, summarizeComments, taxModelId, tryUnserialize };
3496
3549
 
3497
3550
  //# sourceMappingURL=index.mjs.map