@dbp-wp/core 0.2.13 → 0.2.14

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.ts CHANGED
@@ -200,8 +200,8 @@ interface ListTermsParams {
200
200
  /**
201
201
  * Outcome of a term merge ({@link WpClient.mergeTerm}). The source term is deleted only when the
202
202
  * merge completed fully: every assignment across every reachable post type was re-assigned to the
203
- * target, with no per-post failure and no page-cap truncation. Otherwise the source is kept so the
204
- * caller can surface the partial result and re-run.
203
+ * target, with no per-post failure, no page-cap truncation, and no cancellation. Otherwise the
204
+ * source is kept so the caller can surface the partial result and re-run.
205
205
  */
206
206
  interface MergeTermResult {
207
207
  /** Posts successfully re-assigned from the source term to the target. */
@@ -218,6 +218,27 @@ interface MergeTermResult {
218
218
  * taxonomy is attached to a post type not addressable over REST. The source is kept.
219
219
  */
220
220
  truncated: boolean;
221
+ /** True when the caller aborted the merge (via the `signal`) before it finished; source kept. */
222
+ canceled: boolean;
223
+ }
224
+ /**
225
+ * Progress of an in-flight term merge ({@link WpClient.mergeTerm}), reported via `onProgress` so a
226
+ * long re-assignment (one REST write per post) can show a live count rather than freezing the UI.
227
+ */
228
+ interface MergeProgress {
229
+ /** Posts re-assigned to the target so far. */
230
+ reassigned: number;
231
+ /** Posts that failed to re-assign so far. */
232
+ failed: number;
233
+ /** Total posts discovered carrying the source term (known once discovery completes). */
234
+ total: number;
235
+ }
236
+ /** Options for {@link WpClient.mergeTerm}: live progress and cooperative cancellation. */
237
+ interface MergeTermOptions {
238
+ /** Called after discovery and after each re-assignment with cumulative counts. */
239
+ onProgress?: (progress: MergeProgress) => void;
240
+ /** Aborts the merge between re-assignments; the source term is kept (`canceled: true`). */
241
+ signal?: AbortSignal;
221
242
  }
222
243
  /** Result of a per-post meta delete via the companion plugin. */
223
244
  interface DeleteMetaResult {
@@ -664,13 +685,18 @@ declare class WpClient {
664
685
  * lose the term without gaining the target. So this scans all of the taxonomy's post types.
665
686
  *
666
687
  * The source is deleted only on a fully clean merge: every reachable type fully paged (no
667
- * {@link MAX_MERGE_PAGES} cap hit, no type unaddressable over REST) and every re-assignment
668
- * succeeded. Otherwise it is kept (`deleted: false`) so the caller can surface the partial
669
- * result and re-run — re-running re-queries the source's remaining posts (idempotent-ish). The
670
- * caller must reject merging a term that has children (WordPress would reparent them on delete);
671
- * this method does not enforce that. A core REST call — no companion plugin needed.
688
+ * {@link MAX_MERGE_PAGES} cap hit, no type unaddressable over REST), every re-assignment
689
+ * succeeded, and the caller did not cancel. Otherwise it is kept (`deleted: false`) so the caller
690
+ * can surface the partial result and re-run — re-running re-queries the source's remaining posts
691
+ * (idempotent-ish). The caller must reject merging a term that has children (WordPress would
692
+ * reparent them on delete); this method does not enforce that.
693
+ *
694
+ * Because the re-assignment is one REST write per post (minutes for a heavily-used term),
695
+ * `options.onProgress` reports live counts and `options.signal` cancels cooperatively between
696
+ * writes (the in-flight write completes, then the merge stops and keeps the source). A core REST
697
+ * call — no companion plugin needed.
672
698
  */
673
- mergeTerm(taxRestBase: string, fromId: number, toId: number): Promise<MergeTermResult>;
699
+ mergeTerm(taxRestBase: string, fromId: number, toId: number, options?: MergeTermOptions): Promise<MergeTermResult>;
674
700
  /**
675
701
  * Resolve specific term ids to their names in one request per 100 ids (`?include=`), used to
676
702
  * label the taxonomy columns for the posts currently shown. A core REST call — no plugin needed.
@@ -817,4 +843,4 @@ declare function normalizeStatus(value: string): string;
817
843
  */
818
844
  declare function buildImportPlan(table: ParsedTable, mapping: ImportTarget[]): ImportCreate[];
819
845
 
820
- export { type ChildRecord, type DeleteMetaResult, type FormulaEngine, type ImportCreate, type ImportTarget, type ListMediaParams, type ListPostsParams, type ListTermsParams, MARKDOWN_META_KEY, type MergeTermResult, PARENT_META_KEY, PARENT_TYPE_META_KEY, type ParentAggregateRecord, type ParsedTable, type PrintRecord, RelationError, type RelationTarget, SafeFormulaEngine, TemplateParseError, type UpdatePostFields, WpClient, type WpCredentials, type WpMedia, type WpMediaSize, type WpPost, type WpPostEdit, type WpPostResponse, type WpPostType, WpRequestError, type WpTaxonomy, type WpTerm, assertValidRelation, buildAuthHeader, buildChildRecord, buildClearRelationMeta, buildContentDisposition, buildImportPlan, buildParentAggregate, buildPrintRecord, buildSetRelationMeta, computeMergedTermIds, deriveChildren, getRelation, normalizeMedia, normalizePostForEdit, normalizeSiteUrl, normalizeStatus, normalizeTaxonomies, normalizeTerm, parseCsv, parseJsonRecords, renderChildData, renderMarkdown, renderRecordTemplate, renderTemplate };
846
+ export { type ChildRecord, type DeleteMetaResult, type FormulaEngine, type ImportCreate, type ImportTarget, type ListMediaParams, type ListPostsParams, type ListTermsParams, MARKDOWN_META_KEY, type MergeProgress, type MergeTermOptions, type MergeTermResult, PARENT_META_KEY, PARENT_TYPE_META_KEY, type ParentAggregateRecord, type ParsedTable, type PrintRecord, RelationError, type RelationTarget, SafeFormulaEngine, TemplateParseError, type UpdatePostFields, WpClient, type WpCredentials, type WpMedia, type WpMediaSize, type WpPost, type WpPostEdit, type WpPostResponse, type WpPostType, WpRequestError, type WpTaxonomy, type WpTerm, assertValidRelation, buildAuthHeader, buildChildRecord, buildClearRelationMeta, buildContentDisposition, buildImportPlan, buildParentAggregate, buildPrintRecord, buildSetRelationMeta, computeMergedTermIds, deriveChildren, getRelation, normalizeMedia, normalizePostForEdit, normalizeSiteUrl, normalizeStatus, normalizeTaxonomies, normalizeTerm, parseCsv, parseJsonRecords, renderChildData, renderMarkdown, renderRecordTemplate, renderTemplate };
package/dist/index.js CHANGED
@@ -839,19 +839,25 @@ var WpClient = class {
839
839
  * lose the term without gaining the target. So this scans all of the taxonomy's post types.
840
840
  *
841
841
  * The source is deleted only on a fully clean merge: every reachable type fully paged (no
842
- * {@link MAX_MERGE_PAGES} cap hit, no type unaddressable over REST) and every re-assignment
843
- * succeeded. Otherwise it is kept (`deleted: false`) so the caller can surface the partial
844
- * result and re-run — re-running re-queries the source's remaining posts (idempotent-ish). The
845
- * caller must reject merging a term that has children (WordPress would reparent them on delete);
846
- * this method does not enforce that. A core REST call — no companion plugin needed.
842
+ * {@link MAX_MERGE_PAGES} cap hit, no type unaddressable over REST), every re-assignment
843
+ * succeeded, and the caller did not cancel. Otherwise it is kept (`deleted: false`) so the caller
844
+ * can surface the partial result and re-run — re-running re-queries the source's remaining posts
845
+ * (idempotent-ish). The caller must reject merging a term that has children (WordPress would
846
+ * reparent them on delete); this method does not enforce that.
847
+ *
848
+ * Because the re-assignment is one REST write per post (minutes for a heavily-used term),
849
+ * `options.onProgress` reports live counts and `options.signal` cancels cooperatively between
850
+ * writes (the in-flight write completes, then the merge stops and keeps the source). A core REST
851
+ * call — no companion plugin needed.
847
852
  */
848
- async mergeTerm(taxRestBase, fromId, toId) {
853
+ async mergeTerm(taxRestBase, fromId, toId, options = {}) {
849
854
  assertRouteSegment(taxRestBase);
850
855
  assertTermId(fromId);
851
856
  assertTermId(toId);
852
857
  if (fromId === toId) {
853
858
  throw new Error("Cannot merge a term into itself.");
854
859
  }
860
+ const { onProgress, signal } = options;
855
861
  const taxonomies = await this.listTaxonomies();
856
862
  const tax = taxonomies.find((t) => t.restBase === taxRestBase);
857
863
  if (!tax) {
@@ -872,9 +878,15 @@ var WpClient = class {
872
878
  truncated = true;
873
879
  }
874
880
  const targets = [];
881
+ let canceled = false;
875
882
  for (const type of restBases) {
883
+ if (canceled) break;
876
884
  let page = 1;
877
885
  for (; ; ) {
886
+ if (signal?.aborted) {
887
+ canceled = true;
888
+ break;
889
+ }
878
890
  if (page > MAX_MERGE_PAGES) {
879
891
  truncated = true;
880
892
  break;
@@ -897,22 +909,33 @@ var WpClient = class {
897
909
  }
898
910
  let reassigned = 0;
899
911
  const failed = [];
900
- for (const { type, post } of targets) {
901
- const current = post.terms[taxRestBase] ?? [];
902
- const next = computeMergedTermIds(current, fromId, toId);
903
- try {
904
- await this.updatePost(post.id, { terms: { [taxRestBase]: next } }, type);
905
- reassigned += 1;
906
- } catch (e) {
907
- failed.push({ id: post.id, error: e instanceof Error ? e.message : String(e) });
912
+ if (!canceled) {
913
+ onProgress?.({ reassigned, failed: failed.length, total: targets.length });
914
+ for (const { type, post } of targets) {
915
+ if (signal?.aborted) {
916
+ canceled = true;
917
+ break;
918
+ }
919
+ const current = post.terms[taxRestBase] ?? [];
920
+ const next = computeMergedTermIds(current, fromId, toId);
921
+ try {
922
+ await this.updatePost(post.id, { terms: { [taxRestBase]: next } }, type);
923
+ reassigned += 1;
924
+ } catch (e) {
925
+ failed.push({ id: post.id, error: e instanceof Error ? e.message : String(e) });
926
+ }
927
+ onProgress?.({ reassigned, failed: failed.length, total: targets.length });
908
928
  }
909
929
  }
930
+ if (signal?.aborted) {
931
+ canceled = true;
932
+ }
910
933
  let deleted = false;
911
- if (failed.length === 0 && !truncated) {
934
+ if (failed.length === 0 && !truncated && !canceled) {
912
935
  await this.deleteTerm(taxRestBase, fromId);
913
936
  deleted = true;
914
937
  }
915
- return { reassigned, failed, deleted, truncated };
938
+ return { reassigned, failed, deleted, truncated, canceled };
916
939
  }
917
940
  /**
918
941
  * Resolve specific term ids to their names in one request per 100 ids (`?include=`), used to
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/print.ts","../src/relation.ts","../src/wp-client.ts","../src/markdown.ts","../src/calc/index.ts","../src/importer.ts"],"sourcesContent":["/**\n * Print Design template engine.\n *\n * Renders a user-authored HTML template against a {@link PrintRecord} using a small,\n * mustache-like `{{ }}` syntax. Pure and framework-agnostic: the browser UI renders the\n * result inside a sandboxed iframe and prints it (see planning doc 04-print-design).\n *\n * Syntax:\n * - `{{ path }}` resolve a dotted path, HTML-escape the value, and output it.\n * - `{{{ path }}}` same, but output the value raw (no escaping) for HTML fields such as\n * `content` / `excerpt`, which already hold WordPress-rendered HTML.\n * - `{{#each path}} ... {{/each}}` iterate an array; inside the block, `this` is the\n * current item (and `this.<key>` reaches into an object item).\n *\n * Unknown paths render as the empty string. Paths resolve against the record, except\n * `this` / `this.<key>`, which resolve against the current `{{#each}}` item.\n */\n\nimport type { WpPostResponse } from './types';\n\n/** The data a template is rendered against (one WordPress post, flattened for templating). */\nexport interface PrintRecord {\n id: number;\n title: string;\n /** WordPress-rendered HTML. Use `{{{ content }}}` so it is not escaped. */\n content: string;\n /** WordPress-rendered HTML. Use `{{{ excerpt }}}` so it is not escaped. */\n excerpt: string;\n status: string;\n menuOrder: number;\n /** Absolute URL of the featured image, or `''` when the post has none. */\n featuredImageUrl: string;\n /**\n * Flattened post meta, keyed by meta key (values stringified; arrays joined with `, `).\n * Reachable in templates as `{{ meta.<key> }}`. A meta key containing a literal dot\n * cannot be addressed, since template paths split on `.`.\n */\n meta: Record<string, string>;\n /** Taxonomy terms keyed by REST base (e.g. `tax.category`), each an array of term names. */\n tax: Record<string, string[]>;\n}\n\n/** Thrown when a template has an unbalanced `{{#each}}` / `{{/each}}`. */\nexport class TemplateParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TemplateParseError';\n }\n}\n\ntype TemplateNode = TextNode | VarNode | EachNode;\ninterface TextNode {\n kind: 'text';\n value: string;\n}\ninterface VarNode {\n kind: 'var';\n path: string;\n /** `true` for triple-brace `{{{ }}}` (unescaped) output. */\n raw: boolean;\n}\ninterface EachNode {\n kind: 'each';\n path: string;\n body: TemplateNode[];\n}\n\ninterface Scope {\n /** The record paths resolve against by default. */\n record: object;\n /** The current `{{#each}}` item, reachable as `this` / `this.<key>`. */\n current?: unknown;\n}\n\n// Order matters: triple-brace and the each tags must be tried before the generic `{{ }}`.\n// `[\\s\\S]` (not `.`) so a tag may span newlines.\nconst TAG =\n /(\\{\\{\\{\\s*([\\s\\S]+?)\\s*\\}\\}\\})|(\\{\\{\\s*#each\\s+([\\s\\S]+?)\\s*\\}\\})|(\\{\\{\\s*\\/each\\s*\\}\\})|(\\{\\{\\s*([\\s\\S]+?)\\s*\\}\\})/g;\n\n/** Parse a template string into a node tree, validating `{{#each}}` nesting. */\nfunction parseTemplate(template: string): TemplateNode[] {\n const root: TemplateNode[] = [];\n // `current` is the node list we append to; `parents` lets us pop back out of an each\n // block; `open` tracks open each blocks so we can detect unbalanced tags.\n let current: TemplateNode[] = root;\n const parents: TemplateNode[][] = [];\n const open: EachNode[] = [];\n const pushText = (text: string): void => {\n if (text) current.push({ kind: 'text', value: text });\n };\n\n let last = 0;\n TAG.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = TAG.exec(template)) !== null) {\n pushText(template.slice(last, m.index));\n last = TAG.lastIndex;\n if (m[2] !== undefined) {\n // {{{ raw }}}\n current.push({ kind: 'var', path: m[2].trim(), raw: true });\n } else if (m[4] !== undefined) {\n // {{#each path}}\n const node: EachNode = { kind: 'each', path: m[4].trim(), body: [] };\n current.push(node);\n parents.push(current);\n current = node.body;\n open.push(node);\n } else if (m[5] !== undefined) {\n // {{/each}}\n if (open.length === 0) {\n throw new TemplateParseError('Unexpected {{/each}} without a matching {{#each}}.');\n }\n open.pop();\n current = parents.pop() ?? root;\n } else if (m[7] !== undefined) {\n // {{ var }}\n current.push({ kind: 'var', path: m[7].trim(), raw: false });\n }\n }\n pushText(template.slice(last));\n\n if (open.length > 0) {\n const unclosed = open[open.length - 1];\n throw new TemplateParseError(`Unclosed {{#each ${unclosed ? unclosed.path : ''}}}.`);\n }\n return root;\n}\n\n/** Walk a dotted path (e.g. `meta.price`) into a value, returning `undefined` if any hop misses. */\nfunction getPath(obj: unknown, path: string): unknown {\n let cur: unknown = obj;\n for (const key of path.split('.')) {\n if (cur === null || cur === undefined || typeof cur !== 'object') {\n return undefined;\n }\n cur = (cur as Record<string, unknown>)[key];\n }\n return cur;\n}\n\n/** Resolve a template path against the scope. `this` / `this.<key>` target the each item. */\nfunction resolve(path: string, scope: Scope): unknown {\n if (path === 'this') {\n return scope.current;\n }\n if (path.startsWith('this.')) {\n return getPath(scope.current, path.slice('this.'.length));\n }\n return getPath(scope.record, path);\n}\n\n/** Stringify a scalar for output; non-scalars (objects/arrays) and nullish render as ''. */\nfunction stringifyScalar(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n if (typeof value === 'string') {\n return value;\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n // Objects/arrays have no sensible inline string form; render nothing.\n return '';\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\nfunction renderNodes(nodes: TemplateNode[], scope: Scope, escape: boolean): string {\n let out = '';\n for (const node of nodes) {\n if (node.kind === 'text') {\n out += node.value;\n } else if (node.kind === 'var') {\n const value = stringifyScalar(resolve(node.path, scope));\n // `{{{ }}}` is always raw; `{{ }}` is HTML-escaped only in escaping (HTML) mode.\n out += node.raw || !escape ? value : escapeHtml(value);\n } else {\n // each: iterate only when the path resolves to an array; otherwise render nothing.\n const list = resolve(node.path, scope);\n if (Array.isArray(list)) {\n for (const item of list) {\n out += renderNodes(node.body, { record: scope.record, current: item }, escape);\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Render a Print Design template against one record. Values are HTML-escaped for `{{ }}`\n * and emitted raw for `{{{ }}}`. Throws {@link TemplateParseError} on unbalanced\n * `{{#each}}` / `{{/each}}`.\n */\nexport function renderTemplate(template: string, record: PrintRecord): string {\n return renderNodes(parseTemplate(template), { record }, true);\n}\n\n/**\n * Render a template against an arbitrary record object, reusing the same `{{ }}` /\n * `{{#each}}` engine as {@link renderTemplate}. By default `{{ }}` values are HTML-escaped\n * (HTML output). Pass `escape: false` for a plain-text context (e.g. a spreadsheet cell\n * rendered as text, where the host already escapes for the DOM and re-escaping here would\n * surface literal entities). Throws {@link TemplateParseError} on unbalanced `{{#each}}`.\n */\nexport function renderRecordTemplate(\n template: string,\n record: object,\n options: { escape?: boolean } = {},\n): string {\n return renderNodes(parseTemplate(template), { record }, options.escape ?? true);\n}\n\n/** Flatten one meta value to a string. Arrays join their non-empty scalar parts. */\nfunction flattenMetaValue(value: unknown): string {\n if (Array.isArray(value)) {\n return value\n .map(stringifyScalar)\n .filter((s) => s !== '')\n .join(', ');\n }\n return stringifyScalar(value);\n}\n\n/** Merge core meta and connector meta (connector wins) into flat string values. */\nfunction flattenMeta(raw: WpPostResponse): Record<string, string> {\n // Null-proto so untrusted meta keys (e.g. `__proto__`, `constructor`) become plain own\n // entries with no prototype pollution and no inherited-key collisions.\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n const add = (src: Record<string, unknown> | undefined): void => {\n if (!src) return;\n for (const [key, value] of Object.entries(src)) {\n out[key] = flattenMetaValue(value);\n }\n };\n add(raw.meta);\n add(raw.dbp_wp_meta); // connector meta overlays core meta for the same key\n return out;\n}\n\n/** Extract the featured image URL from an `_embed`ded response, or '' when absent. */\nfunction extractFeaturedImageUrl(embedded: WpPostResponse['_embedded']): string {\n const media = embedded?.['wp:featuredmedia'];\n if (Array.isArray(media) && media.length > 0) {\n const first = media[0];\n if (first !== null && typeof first === 'object') {\n const sourceUrl = (first as Record<string, unknown>).source_url;\n if (typeof sourceUrl === 'string') {\n return sourceUrl;\n }\n }\n }\n return '';\n}\n\n/** Group `_embed`ded terms into `{ <taxonomy slug>: [term name, ...] }`. */\nfunction extractTerms(embedded: WpPostResponse['_embedded']): Record<string, string[]> {\n // Null-proto so a taxonomy slug like `toString`/`__proto__` cannot resolve an inherited\n // value (which would make `(tax[slug] ??= [])` skip the array and throw on `.push`).\n const tax: Record<string, string[]> = Object.create(null) as Record<string, string[]>;\n const groups = embedded?.['wp:term'];\n if (!Array.isArray(groups)) {\n return tax;\n }\n for (const group of groups) {\n if (!Array.isArray(group)) continue;\n for (const term of group) {\n if (term === null || typeof term !== 'object') continue;\n const entry = term as Record<string, unknown>;\n if (typeof entry.taxonomy === 'string' && typeof entry.name === 'string') {\n (tax[entry.taxonomy] ??= []).push(entry.name);\n }\n }\n }\n return tax;\n}\n\n/**\n * Build a {@link PrintRecord} from a raw WordPress REST post. Expects the request to have\n * used `context=edit` and `_embed` so `content`/`excerpt` and embedded media/terms are\n * present; missing pieces degrade to empty values rather than throwing.\n *\n * `title` uses the raw (unescaped) value so `{{ title }}` escaping is not doubled.\n * `content`/`excerpt` use the rendered HTML (intended for `{{{ }}}`).\n */\nexport function buildPrintRecord(raw: WpPostResponse): PrintRecord {\n return {\n id: raw.id,\n title: raw.title?.raw ?? raw.title?.rendered ?? '',\n content: raw.content?.rendered ?? '',\n excerpt: raw.excerpt?.rendered ?? '',\n status: raw.status,\n menuOrder: raw.menu_order,\n featuredImageUrl: extractFeaturedImageUrl(raw._embedded),\n meta: flattenMeta(raw),\n tax: extractTerms(raw._embedded),\n };\n}\n","import { renderRecordTemplate } from './print';\nimport type { WpPost } from './types';\n\n/**\n * Parent/child relations (MVP: links only).\n *\n * The relation is stored single-source on the **child**: `_dbp_wp_parent` holds the\n * parent post ID and `_dbp_wp_parent_type` holds the parent post type's REST route base.\n * The parent keeps no child list — a parent's children are derived from already-loaded\n * posts ({@link deriveChildren}), so there is no denormalized list to keep in sync.\n *\n * These keys are `_`-prefixed (protected) and are exposed over REST only because the\n * companion plugin registers them with `register_post_meta()` + an `edit_post`\n * `auth_callback`. They therefore travel through the standard core `meta` field — not the\n * connector's `dbp_wp_meta` field — so relation writes use a dedicated path.\n */\n\n/** Meta key holding a child's parent post ID. */\nexport const PARENT_META_KEY = '_dbp_wp_parent';\n\n/** Meta key holding the parent post type's REST route base. */\nexport const PARENT_TYPE_META_KEY = '_dbp_wp_parent_type';\n\n/** Allowed characters for a REST route segment (post type slug); mirrors the WpClient check. */\nconst ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;\n\n/** A parent assignment for a child post. */\nexport interface RelationTarget {\n /** Parent post ID (positive integer). */\n parentId: number;\n /** Parent post type's REST route base (e.g. `pages`). */\n parentType: string;\n}\n\n/** Error thrown when a relation assignment is invalid (bad id/type, or self-parent). */\nexport class RelationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RelationError';\n }\n}\n\n/**\n * Validate a parent assignment for a child. Throws {@link RelationError} when the parent\n * id is not a positive safe integer, the parent type is not a valid route segment, or the\n * parent is the child itself (post IDs are unique across types, so this catches a\n * self-parent regardless of type).\n */\nexport function assertValidRelation(childId: number, relation: RelationTarget): void {\n if (!Number.isSafeInteger(relation.parentId) || relation.parentId <= 0) {\n throw new RelationError(`Invalid parent id: ${String(relation.parentId)}`);\n }\n if (typeof relation.parentType !== 'string' || !ROUTE_SEGMENT.test(relation.parentType)) {\n throw new RelationError(`Invalid parent type: ${String(relation.parentType)}`);\n }\n if (relation.parentId === childId) {\n throw new RelationError('A post cannot be its own parent.');\n }\n}\n\n/**\n * Build the standard-`meta` body that sets a child's parent. Validates the assignment\n * first ({@link assertValidRelation}). The keys ride the core `meta` field, so the caller\n * sends `{ meta: <this> }`.\n */\nexport function buildSetRelationMeta(\n childId: number,\n relation: RelationTarget,\n): Record<string, unknown> {\n assertValidRelation(childId, relation);\n return {\n [PARENT_META_KEY]: relation.parentId,\n [PARENT_TYPE_META_KEY]: relation.parentType,\n };\n}\n\n/**\n * Build the standard-`meta` body that clears a child's parent. Sending `null` for a\n * registered single meta key makes WordPress delete it, so no stale value is left behind.\n */\nexport function buildClearRelationMeta(): Record<string, unknown> {\n return {\n [PARENT_META_KEY]: null,\n [PARENT_TYPE_META_KEY]: null,\n };\n}\n\n/**\n * Read a post's parent relation from its normalized fields, or null when it has no parent.\n * Both a positive `parent` id and a non-empty `parentType` are required for a relation to\n * count (a half-written relation reads as none).\n */\nexport function getRelation(post: WpPost): RelationTarget | null {\n if (\n typeof post.parent === 'number' &&\n post.parent > 0 &&\n typeof post.parentType === 'string' &&\n post.parentType !== ''\n ) {\n return { parentId: post.parent, parentType: post.parentType };\n }\n return null;\n}\n\n/**\n * Derive a parent's children from already-loaded posts: every post whose `_dbp_wp_parent`\n * equals `parentId`. This covers the common case of same-type children visible in the\n * current grid; cross-type or off-grid children would need a server-side query (deferred).\n */\nexport function deriveChildren(posts: WpPost[], parentId: number): WpPost[] {\n if (!Number.isSafeInteger(parentId) || parentId <= 0) {\n return [];\n }\n return posts.filter((post) => post.parent === parentId);\n}\n\n// --- Child data (template aggregation of a parent's children) ---\n//\n// The legacy app stored a per-parent template (`_dbpcloudwp_child_formula`) and cached its\n// rendered output (`_dbpcloudwp_child_value`) as parent meta — a denormalized value that\n// went stale whenever a child changed. The new model persists nothing: a column-level\n// template is rendered live against each parent's derived children, reusing the Print\n// template engine ({@link renderRecordTemplate}). So `{{ }}`/`{{#each}}` work as in Print.\n\n/** A child post flattened for templating inside a parent's child-data column. */\nexport interface ChildRecord {\n id: number;\n title: string;\n status: string;\n menuOrder: number;\n /** Flattened meta (core overlaid by connector), values stringified; `this.meta.<key>`. */\n meta: Record<string, string>;\n}\n\n/**\n * A parent exposed to a child-data template: its own fields plus the derived `children`\n * (and `childCount`), so a template can aggregate them, e.g.\n * `{{#each children}}{{ this.title }}{{/each}}`.\n */\nexport interface ParentAggregateRecord {\n id: number;\n title: string;\n status: string;\n menuOrder: number;\n meta: Record<string, string>;\n childCount: number;\n children: ChildRecord[];\n}\n\n/** Stringify one meta value; an array joins its non-empty scalar parts with `, `. */\nfunction flattenMetaValue(value: unknown): string {\n if (Array.isArray(value)) {\n return value\n .map((v) => (v === null || v === undefined ? '' : String(v)))\n .filter((s) => s !== '')\n .join(', ');\n }\n return value === null || value === undefined ? '' : String(value);\n}\n\n/**\n * Flatten a post's meta (core `meta` overlaid by connector `dbpWpMeta`) to flat string\n * values. Uses a null-proto accumulator so untrusted meta keys (`__proto__`, `constructor`)\n * become plain own entries with no prototype pollution and no inherited-key collisions.\n */\nfunction flattenPostMeta(post: WpPost): Record<string, string> {\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n const add = (src: Record<string, unknown> | undefined): void => {\n if (!src) return;\n for (const [key, value] of Object.entries(src)) {\n out[key] = flattenMetaValue(value);\n }\n };\n add(post.meta);\n add(post.dbpWpMeta); // connector meta overlays core meta for the same key\n return out;\n}\n\n/** Build a {@link ChildRecord} from a loaded post (meta flattened for templating). */\nexport function buildChildRecord(post: WpPost): ChildRecord {\n return {\n id: post.id,\n title: post.title,\n status: post.status,\n menuOrder: post.menuOrder,\n meta: flattenPostMeta(post),\n };\n}\n\n/** Build the {@link ParentAggregateRecord} a child-data template renders against. */\nexport function buildParentAggregate(parent: WpPost, children: WpPost[]): ParentAggregateRecord {\n return {\n id: parent.id,\n title: parent.title,\n status: parent.status,\n menuOrder: parent.menuOrder,\n meta: flattenPostMeta(parent),\n childCount: children.length,\n children: children.map(buildChildRecord),\n };\n}\n\n/**\n * Render a \"child data\" template for one parent: derive its children from the already-loaded\n * posts ({@link deriveChildren}), build the aggregate record, and render the template with\n * the shared Print engine. Same-type, in-grid children only (cross-type/off-grid children\n * need a server query — deferred). Rendered in plain-text (non-escaping) mode, since the\n * result is shown as a spreadsheet cell's text. Throws {@link TemplateParseError} on an\n * unbalanced `{{#each}}`.\n */\nexport function renderChildData(template: string, parent: WpPost, posts: WpPost[]): string {\n const children = deriveChildren(posts, parent.id);\n return renderRecordTemplate(template, buildParentAggregate(parent, children), { escape: false });\n}\n","import { buildPrintRecord, type PrintRecord } from './print';\nimport {\n PARENT_META_KEY,\n PARENT_TYPE_META_KEY,\n buildClearRelationMeta,\n buildSetRelationMeta,\n type RelationTarget,\n} from './relation';\nimport type {\n DeleteMetaResult,\n ListMediaParams,\n ListPostsParams,\n ListTermsParams,\n MergeTermResult,\n UpdatePostFields,\n WpCredentials,\n WpMedia,\n WpMediaSize,\n WpPost,\n WpPostEdit,\n WpPostResponse,\n WpPostType,\n WpTaxonomy,\n WpTerm,\n} from './types';\n\n/** Hosts for which plain http is tolerated (local development). */\nconst LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);\n\n/** Allowed characters for a REST route segment (post type slug). No dots: a `.`/`..`\n * segment would be resolved by the URL parser and traverse the REST path. */\nconst ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;\n\n/** JS magic property names that pass {@link ROUTE_SEGMENT} but are unsafe as plain-object keys\n * (prototype pollution / silent drop). A taxonomy REST base matching one is rejected. */\nconst RESERVED_KEYS = new Set(['__proto__', 'prototype', 'constructor']);\n\n/** Page cap for {@link WpClient.listAllTerms} (100 terms/page) — bounds a runaway on a huge\n * taxonomy while covering typical category trees. */\nconst MAX_TERM_PAGES = 10;\n\n/** Per-post-type page cap for a term merge ({@link WpClient.mergeTerm}, 100 posts/page) — bounds a\n * runaway on a term assigned to a very large number of posts. When a type exceeds it, the merge\n * is reported truncated and the source term is kept rather than deleted with stragglers behind. */\nconst MAX_MERGE_PAGES = 50;\n\n/** WordPress posts-collection query parameters. A taxonomy whose REST base collides with one of\n * these cannot be used as a term filter ({@link WpClient.listPostsByTerm}) without overwriting a\n * control parameter (e.g. `?page=<termId>` would page rather than filter, silently returning the\n * wrong posts). Such a base is rejected so a merge can never re-assign and delete based on a\n * mis-filtered result. Lower-cased; the taxonomy REST base is compared case-insensitively. */\nconst RESERVED_POST_QUERY_PARAMS = new Set([\n 'context',\n 'page',\n 'per_page',\n 'search',\n 'after',\n 'modified_after',\n 'before',\n 'modified_before',\n 'author',\n 'author_exclude',\n 'exclude',\n 'include',\n 'offset',\n 'order',\n 'orderby',\n 'slug',\n 'status',\n 'tax_relation',\n 'sticky',\n '_embed',\n '_fields',\n '_method',\n '_envelope',\n '_jsonp',\n '_locale',\n]);\n\n/** REST field added by the companion plugin to carry arbitrary post meta. */\nconst META_FIELD = 'dbp_wp_meta';\n\n/**\n * Meta key holding the lossless Markdown source for a post edited in Markdown mode. An\n * underscore-prefixed (protected) key, so the companion plugin must register it with\n * `register_post_meta` + an `edit_post` auth callback to expose it over REST — the generic\n * `dbp_wp_meta` field excludes protected keys. Like the relation keys, it therefore rides\n * the standard core `meta` field, not `dbp_wp_meta`.\n */\nexport const MARKDOWN_META_KEY = '_dbp_wp_markdown';\n\n/** REST namespace registered by the companion plugin. */\nconst CONNECTOR_NAMESPACE = 'dbp-wp/v1';\n\n/**\n * Validate and normalize a WordPress site URL into a REST base (origin + base path).\n *\n * Requires https, except plain http is allowed for local development hosts. Rejects\n * embedded credentials, query strings, and fragments, and strips trailing slashes — so\n * an Application Password is never sent over cleartext to an unexpected target.\n */\nexport function normalizeSiteUrl(siteUrl: string): string {\n let url: URL;\n try {\n url = new URL(siteUrl);\n } catch {\n throw new Error(`Invalid site URL: ${siteUrl}`);\n }\n\n const isLocal = LOCAL_HOSTS.has(url.hostname);\n if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLocal)) {\n throw new Error(`Site URL must use https (http is allowed only for local hosts): ${siteUrl}`);\n }\n // SSRF defense: the client sends the Application Password to whatever the site URL points to,\n // so block private, loopback, and link-local IP-literal targets (e.g. 10.x, 192.168.x,\n // 169.254.169.254 cloud metadata, IPv6 ULA/link-local). The explicit local-dev hosts stay\n // allowed. This covers IP literals only — a hostname that resolves to a private address (DNS\n // rebinding) is not caught here, as fetch does not expose the resolved address.\n if (!isLocal && isPrivateAddress(url.hostname)) {\n throw new Error(`Site URL must not point to a private or local network address: ${siteUrl}`);\n }\n if (url.username !== '' || url.password !== '') {\n throw new Error('Site URL must not contain embedded credentials.');\n }\n if (url.search !== '' || url.hash !== '') {\n throw new Error('Site URL must not contain a query string or fragment.');\n }\n\n return `${url.origin}${url.pathname}`.replace(/\\/+$/, '');\n}\n\n/**\n * True when a URL hostname is an IP literal in a private, loopback, link-local, or unspecified\n * range — the SSRF block list used by {@link normalizeSiteUrl}. Returns false for DNS hostnames\n * (not resolved here) and for public IPs. Exported for unit testing.\n */\nexport function isPrivateAddress(hostname: string): boolean {\n // IPv4 literal.\n const v4 = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(hostname);\n if (v4) {\n const octets = [Number(v4[1]), Number(v4[2]), Number(v4[3]), Number(v4[4])];\n if (octets.some((o) => o > 255)) {\n return false; // not a valid IPv4 literal\n }\n return isPrivateV4(octets);\n }\n // IPv6 literal. A URL's hostname keeps the brackets (e.g. `[fe80::1]`), so strip them.\n if (hostname.includes(':')) {\n return isPrivateV6(hostname.toLowerCase().replace(/^\\[/, '').replace(/\\]$/, ''));\n }\n return false; // DNS hostname or non-literal: not blocked here\n}\n\n/** True for private, loopback, link-local, or unspecified IPv4 octets. */\nfunction isPrivateV4(octets: readonly number[]): boolean {\n const [a, b] = octets;\n return (\n a === 0 || // \"this\" network\n a === 10 || // private\n a === 127 || // loopback\n (a === 169 && b === 254) || // link-local (incl. 169.254.169.254 metadata)\n (a === 172 && b !== undefined && b >= 16 && b <= 31) || // private\n (a === 192 && b === 168) // private\n );\n}\n\n/**\n * Expand an IPv6 literal to its 8 16-bit groups, or null if unparseable. Handles `::`\n * compression and an embedded dotted-quad tail (e.g. `::ffff:1.2.3.4`). The WHATWG URL parser\n * canonicalizes an IPv4-mapped address to hex (`[::ffff:7f00:1]`), so the range check must work\n * on the expanded groups rather than string-matching the dotted form.\n */\nfunction expandV6(input: string): number[] | null {\n let s = input;\n // Convert a trailing dotted-quad to two hex groups.\n const v4m = /^(.*:)(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(s);\n if (v4m) {\n const o = [Number(v4m[2]), Number(v4m[3]), Number(v4m[4]), Number(v4m[5])];\n if (o.some((x) => x > 255)) {\n return null;\n }\n s = `${v4m[1]}${(((o[0] as number) << 8) | (o[1] as number)).toString(16)}:${(((o[2] as number) << 8) | (o[3] as number)).toString(16)}`;\n }\n const halves = s.split('::');\n if (halves.length > 2) {\n return null;\n }\n const toGroups = (side: string): number[] | null => {\n if (side === '') {\n return [];\n }\n const out: number[] = [];\n for (const part of side.split(':')) {\n if (!/^[0-9a-f]{1,4}$/.test(part)) {\n return null;\n }\n out.push(parseInt(part, 16));\n }\n return out;\n };\n const left = toGroups(halves[0] ?? '');\n if (left === null) {\n return null;\n }\n if (halves.length === 2) {\n const right = toGroups(halves[1] ?? '');\n if (right === null) {\n return null;\n }\n const gap = 8 - left.length - right.length;\n if (gap < 1) {\n return null; // `::` must stand for at least one zero group\n }\n return [...left, ...new Array<number>(gap).fill(0), ...right];\n }\n return left.length === 8 ? left : null;\n}\n\n/** True for IPv6 loopback/unspecified, ULA (fc00::/7), link-local (fe80::/10), or a mapped private v4. */\nfunction isPrivateV6(addr: string): boolean {\n const groups = expandV6((addr.split('%')[0] ?? '').trim()); // drop any zone id\n if (groups === null) {\n return false;\n }\n if (groups.every((g) => g === 0)) {\n return true; // :: unspecified\n }\n if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) {\n return true; // ::1 loopback\n }\n const first = groups[0] ?? 0;\n if ((first & 0xfe00) === 0xfc00) {\n return true; // ULA fc00::/7\n }\n if ((first & 0xffc0) === 0xfe80) {\n return true; // link-local fe80::/10\n }\n // IPv4-mapped ::ffff:a.b.c.d\n if (groups.slice(0, 5).every((g) => g === 0) && groups[5] === 0xffff) {\n const g6 = groups[6] ?? 0;\n const g7 = groups[7] ?? 0;\n return isPrivateV4([(g6 >> 8) & 0xff, g6 & 0xff, (g7 >> 8) & 0xff, g7 & 0xff]);\n }\n return false;\n}\n\n/**\n * Build the HTTP Basic `Authorization` header value from Application Password\n * credentials. WordPress treats the Application Password as the Basic-auth password.\n */\nexport function buildAuthHeader(credentials: WpCredentials): string {\n if (credentials.username.includes(':')) {\n throw new Error('Username must not contain a colon (\":\") for HTTP Basic authentication.');\n }\n const token = `${credentials.username}:${credentials.applicationPassword}`;\n const base64 = Buffer.from(token, 'utf-8').toString('base64');\n return `Basic ${base64}`;\n}\n\n/** Error thrown when the WordPress REST API returns a non-2xx response. */\nexport class WpRequestError extends Error {\n constructor(\n readonly status: number,\n readonly path: string,\n message: string,\n ) {\n super(message);\n this.name = 'WpRequestError';\n }\n}\n\n/**\n * Minimal WordPress REST client.\n *\n * Runs in the Node process (CLI shell), never in the browser, so Application Password\n * credentials stay server-side. MVP scope: list posts, read/write post meta.\n */\nexport class WpClient {\n private readonly restBase: string;\n\n constructor(private readonly credentials: WpCredentials) {\n this.restBase = normalizeSiteUrl(credentials.siteUrl);\n }\n\n /**\n * Send an authenticated request and return the raw {@link Response} (throwing on non-2xx),\n * so callers that need response headers — e.g. media pagination via `X-WP-TotalPages` —\n * can read them. A JSON `Content-Type` is declared only when a body is sent; a caller may\n * override it via `init.headers` (the media upload sends the file's own type).\n */\n private async send(path: string, init: RequestInit = {}): Promise<Response> {\n const response = await fetch(`${this.restBase}/wp-json${path}`, {\n ...init,\n headers: {\n Authorization: buildAuthHeader(this.credentials),\n // Only declare a JSON body when one is actually sent (GETs carry none).\n ...(init.body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...init.headers,\n },\n });\n\n if (!response.ok) {\n throw new WpRequestError(\n response.status,\n path,\n `WordPress REST request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return response;\n }\n\n private async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n return (await this.send(path, init)).json() as Promise<T>;\n }\n\n /**\n * List the REST-enabled post types on the site (edit context), so the app can offer\n * a type selector. Returns each type's REST route base and display name.\n */\n async listPostTypes(): Promise<WpPostType[]> {\n const raw = await this.request<unknown>('/wp/v2/types?context=edit');\n return normalizePostTypes(raw);\n }\n\n /** List posts of a given type in edit context (raw fields, for editing). */\n async listPosts(params: ListPostsParams = {}): Promise<WpPost[]> {\n const type = params.type ?? 'posts';\n assertRouteSegment(type);\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n });\n const raw = await this.request<WpPostResponse[]>(`/wp/v2/${type}?${query.toString()}`);\n return raw.map(normalizePost);\n }\n\n /**\n * List posts as {@link PrintRecord}s for Print Design. Requests `_embed` (so featured\n * media and taxonomy terms come back inline) plus `content`/`excerpt`; the standard\n * table/spreadsheet listing ({@link WpClient.listPosts}) is unaffected.\n */\n async listPostsForPrint(params: ListPostsParams = {}): Promise<PrintRecord[]> {\n const type = params.type ?? 'posts';\n assertRouteSegment(type);\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n _embed: '1',\n });\n const raw = await this.request<WpPostResponse[]>(`/wp/v2/${type}?${query.toString()}`);\n return raw.map(buildPrintRecord);\n }\n\n /**\n * List posts of a type that are assigned a given taxonomy term (`GET /wp/v2/<type>?<taxRestBase>=\n * <termId>`), in edit context and across all statuses (`status=any`, so drafts/pending are not\n * silently skipped). Reads `X-WP-TotalPages` for paging. Used by {@link WpClient.mergeTerm} to\n * find every post that must be re-assigned. A core REST call — no companion plugin needed.\n */\n async listPostsByTerm(\n type: string,\n taxRestBase: string,\n termId: number,\n params: { page?: number; perPage?: number } = {},\n ): Promise<{ items: WpPost[]; totalPages: number; pagesReliable: boolean }> {\n assertRouteSegment(type);\n assertRouteSegment(taxRestBase);\n assertTermId(termId);\n // The taxonomy's REST base becomes the posts-collection filter param (e.g. `categories=5`).\n // Reject one that collides with a control parameter: it would overwrite the control rather than\n // filter, returning the wrong posts — dangerous when the caller then re-assigns and deletes.\n if (RESERVED_POST_QUERY_PARAMS.has(taxRestBase.toLowerCase())) {\n throw new Error(`Taxonomy REST base collides with a reserved query parameter: ${taxRestBase}`);\n }\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n status: 'any',\n });\n query.set(taxRestBase, String(termId));\n const response = await this.send(`/wp/v2/${type}?${query.toString()}`);\n const raw = (await response.json()) as unknown;\n // `pagesReliable` is false when WordPress did not send a usable `X-WP-TotalPages`. A merge must\n // not trust a defaulted page count of 1 to mean \"fully enumerated\" before deleting the source.\n const header = response.headers.get('X-WP-TotalPages');\n const pagesReliable = header !== null && /^\\d+$/.test(header.trim());\n return {\n items: Array.isArray(raw) ? raw.map(normalizePost) : [],\n totalPages: parseTotalPages(header),\n pagesReliable,\n };\n }\n\n /**\n * Update post fields in a single request. Standard fields (title, menu_order,\n * status) are core REST fields and need no plugin. When `meta` is supplied it rides\n * the same request through the companion plugin's `dbp_wp_meta` field (ignored by\n * WordPress without the connector). Pass the REST route slug as `type` (e.g.\n * `posts`, `pages`) — not the object type returned on a post.\n */\n async updatePost(\n id: number,\n fields: UpdatePostFields,\n type = 'posts',\n meta?: Record<string, unknown>,\n ): Promise<WpPost> {\n assertPostId(id);\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}/${String(id)}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(buildPostBody(fields, meta)),\n });\n return normalizePost(raw);\n }\n\n /**\n * Create a new post in a single request, symmetric to {@link WpClient.updatePost}.\n * Standard fields (title, menu_order, status) are core REST fields; when `meta` is\n * supplied it rides the same request through the companion plugin's `dbp_wp_meta`\n * field. Pass the REST route slug as `type` (e.g. `posts`, `pages`).\n */\n async createPost(\n fields: UpdatePostFields,\n type = 'posts',\n meta?: Record<string, unknown>,\n ): Promise<WpPost> {\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(buildPostBody(fields, meta)),\n });\n return normalizePost(raw);\n }\n\n /**\n * Update only arbitrary post meta through the companion plugin's `dbp_wp_meta`\n * field. A thin wrapper over {@link WpClient.updatePost} with no standard fields.\n * Requires the connector; the connector writes scalar values only.\n */\n async updatePostMeta(\n id: number,\n meta: Record<string, unknown>,\n type = 'posts',\n ): Promise<WpPost> {\n return this.updatePost(id, {}, type, meta);\n }\n\n /**\n * Fetch a single post for body editing (edit context), returning the raw body\n * (`content.raw`) and, when present, the lossless Markdown source from\n * `_dbp_wp_markdown`. The standard listing ({@link WpClient.listPosts}) omits the body, so\n * the editor uses this dedicated read. The Markdown source comes back via the standard\n * `meta` field only when the connector registered the key; in restricted mode the post is\n * HTML-only. Pass the REST route slug as `type`.\n */\n async getPostForEdit(id: number, type = 'posts'): Promise<WpPostEdit> {\n assertPostId(id);\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}/${String(id)}?context=edit`);\n return normalizePostForEdit(raw);\n }\n\n /**\n * Save a single post's body. Writes `content` (core REST) and, when `markdown` is given,\n * the `_dbp_wp_markdown` source via the standard `meta` field (registered by the connector)\n * — both in one request. Pass `markdown` as the source string for Markdown mode, `null` to\n * clear it (HTML mode on a post previously saved as Markdown), or omit it for an HTML-only\n * post. Writing/clearing `markdown` requires the connector; a content-only save does not.\n * Returns the re-fetched edit model (edit context), so the caller sees the persisted mode.\n */\n async updatePostBody(\n id: number,\n type: string,\n body: { content: string; markdown?: string | null },\n ): Promise<WpPostEdit> {\n assertPostId(id);\n assertRouteSegment(type);\n const reqBody = buildUpdateBody({ content: body.content });\n if (body.markdown !== undefined) {\n // Standard `meta` field (like the relation keys), not the connector's `dbp_wp_meta`:\n // `_dbp_wp_markdown` is protected and is only writable through register_post_meta.\n // `null` makes WordPress delete the key (no stale source left to mis-detect the mode).\n reqBody.meta = { [MARKDOWN_META_KEY]: body.markdown };\n }\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}/${String(id)}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(reqBody),\n });\n return normalizePostForEdit(raw);\n }\n\n /**\n * Delete named meta keys from a single post via the companion plugin's\n * `DELETE /dbp-wp/v1/posts/<id>/meta` route. This route is keyed by id only (the\n * post type is irrelevant). Requires the connector.\n */\n async deletePostMeta(id: number, keys: string[]): Promise<DeleteMetaResult> {\n assertPostId(id);\n const cleanKeys = sanitizeMetaKeys(keys);\n // Use send() (not request()) so an empty/204 response from the connector does not throw in\n // JSON.parse; parseDeleteMetaResponse tolerates it.\n const response = await this.send(`/${CONNECTOR_NAMESPACE}/posts/${String(id)}/meta`, {\n method: 'DELETE',\n body: JSON.stringify({ keys: cleanKeys }),\n });\n return parseDeleteMetaResponse(await response.text(), id, cleanKeys);\n }\n\n /**\n * Set a child post's parent relation. The relation keys ride the standard core `meta`\n * field (the connector registers them with `register_post_meta`), so this is a distinct\n * path from {@link WpClient.updatePostMeta} (which uses the connector's `dbp_wp_meta`\n * field). Validates the assignment (positive id, valid type, no self-parent) before\n * writing. Requires the connector; without it WordPress silently ignores the keys.\n */\n async setRelation(\n childId: number,\n childType: string,\n relation: RelationTarget,\n ): Promise<WpPost> {\n assertPostId(childId);\n assertRouteSegment(childType);\n const raw = await this.request<WpPostResponse>(\n `/wp/v2/${childType}/${String(childId)}?context=edit`,\n { method: 'POST', body: JSON.stringify({ meta: buildSetRelationMeta(childId, relation) }) },\n );\n return normalizePost(raw);\n }\n\n /**\n * Clear a child post's parent relation. Sends `null` for both relation keys, which makes\n * WordPress delete them (no stale `0`/empty value left behind). Requires the connector.\n */\n async clearRelation(childId: number, childType: string): Promise<WpPost> {\n assertPostId(childId);\n assertRouteSegment(childType);\n const raw = await this.request<WpPostResponse>(\n `/wp/v2/${childType}/${String(childId)}?context=edit`,\n { method: 'POST', body: JSON.stringify({ meta: buildClearRelationMeta() }) },\n );\n return normalizePost(raw);\n }\n\n /**\n * Detect whether the companion plugin is active by checking the REST index\n * (`/wp-json/`) for the connector's namespace. Throws on a failed request; a caller\n * that wants a non-fatal probe should treat a thrown error as \"not available\".\n */\n async detectConnector(): Promise<boolean> {\n const index = await this.request<{ namespaces?: unknown }>('/');\n return hasConnectorNamespace(index.namespaces);\n }\n\n /**\n * Upload an image to the media library via core REST (`POST /wp/v2/media`), the same\n * contract WordPress uses: raw bytes with a `Content-Disposition` filename and the file's\n * MIME type (octet-stream when unknown). No companion plugin needed; the authenticated\n * user must have `upload_files`. Returns the normalized media item.\n */\n async uploadMedia(bytes: Uint8Array, filename: string, mimeType?: string): Promise<WpMedia> {\n const response = await this.send('/wp/v2/media', {\n method: 'POST',\n body: bytes,\n headers: {\n 'Content-Type': mimeType && mimeType.length > 0 ? mimeType : 'application/octet-stream',\n 'Content-Disposition': buildContentDisposition(filename),\n },\n });\n return normalizeMedia(await response.json());\n }\n\n /**\n * List image attachments (`GET /wp/v2/media?media_type=image`), paginated. Reads the\n * `X-WP-TotalPages` response header so the picker can page through the library. A core\n * REST call — no companion plugin needed.\n */\n async listMedia(params: ListMediaParams = {}): Promise<{ items: WpMedia[]; totalPages: number }> {\n const perPage = clampInt(params.perPage ?? 30, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n media_type: 'image',\n per_page: String(perPage),\n page: String(page),\n });\n const search = params.search?.trim();\n if (search) {\n query.set('search', search);\n }\n const response = await this.send(`/wp/v2/media?${query.toString()}`);\n const raw = (await response.json()) as unknown;\n return {\n items: Array.isArray(raw) ? raw.map(normalizeMedia) : [],\n totalPages: parseTotalPages(response.headers.get('X-WP-TotalPages')),\n };\n }\n\n /**\n * Resolve specific media ids to their URLs in one request (`?include=`), used to fill in\n * the featured-image thumbnails for the posts currently shown — without embedding media\n * into the lean post listing. A core REST call — no companion plugin needed.\n */\n async resolveMedia(ids: number[]): Promise<WpMedia[]> {\n const clean = [...new Set(ids.filter((id) => Number.isSafeInteger(id) && id > 0))];\n if (clean.length === 0) {\n return [];\n }\n // WordPress caps per_page at 100, so resolve in chunks of 100 — otherwise a request for\n // more than 100 ids would be silently truncated, leaving the rest unresolved.\n const out: WpMedia[] = [];\n for (let i = 0; i < clean.length; i += 100) {\n const chunk = clean.slice(i, i + 100);\n const query = new URLSearchParams({\n include: chunk.join(','),\n per_page: String(chunk.length),\n });\n const raw = await this.request<unknown>(`/wp/v2/media?${query.toString()}`);\n if (Array.isArray(raw)) {\n out.push(...raw.map(normalizeMedia));\n }\n }\n return out;\n }\n\n /**\n * List the REST-enabled taxonomies, optionally filtered to those that apply to a post type\n * (e.g. categories and tags for `posts`). A core REST call — no companion plugin needed.\n */\n async listTaxonomies(type?: string): Promise<WpTaxonomy[]> {\n const query = new URLSearchParams({ context: 'edit' });\n if (type !== undefined) {\n assertRouteSegment(type);\n query.set('type', type);\n }\n const raw = await this.request<unknown>(`/wp/v2/taxonomies?${query.toString()}`);\n return normalizeTaxonomies(raw);\n }\n\n /**\n * List terms of a taxonomy (`GET /wp/v2/<taxRestBase>`), paginated/searchable, reading the\n * `X-WP-TotalPages` header so the picker can page through. A core REST call — no plugin needed.\n */\n async listTerms(\n taxRestBase: string,\n params: ListTermsParams = {},\n ): Promise<{ items: WpTerm[]; totalPages: number }> {\n assertRouteSegment(taxRestBase);\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n });\n const search = params.search?.trim();\n if (search) {\n query.set('search', search);\n }\n const response = await this.send(`/wp/v2/${taxRestBase}?${query.toString()}`);\n const raw = (await response.json()) as unknown;\n return {\n items: Array.isArray(raw) ? raw.map(normalizeTerm) : [],\n totalPages: parseTotalPages(response.headers.get('X-WP-TotalPages')),\n };\n }\n\n /**\n * Fetch every term of a taxonomy by paging through the list, so the picker can build a complete\n * hierarchy tree (a child's parent may be on another page). Capped at {@link MAX_TERM_PAGES}\n * pages to avoid a runaway on a huge taxonomy. A core REST call — no companion plugin needed.\n */\n async listAllTerms(\n taxRestBase: string,\n params: { search?: string } = {},\n ): Promise<{ items: WpTerm[]; truncated: boolean }> {\n assertRouteSegment(taxRestBase);\n const out: WpTerm[] = [];\n let page = 1;\n let totalPages = 1;\n do {\n const result = await this.listTerms(taxRestBase, {\n page,\n perPage: 100,\n ...(params.search ? { search: params.search } : {}),\n });\n out.push(...result.items);\n totalPages = result.totalPages;\n page += 1;\n } while (page <= totalPages && page <= MAX_TERM_PAGES);\n // Signal when the cap stopped us short of every page, so the UI can warn rather than show a\n // silently incomplete tree.\n return { items: out, truncated: totalPages > MAX_TERM_PAGES };\n }\n\n /**\n * Create a new term in a taxonomy (`POST /wp/v2/<taxRestBase>`), optionally under a parent\n * (hierarchical taxonomies only; `parent` is ignored when `0`/absent). WordPress enforces the\n * caller's term-management capability and returns the created term. A core REST call.\n */\n async createTerm(taxRestBase: string, input: { name: string; parent?: number }): Promise<WpTerm> {\n assertRouteSegment(taxRestBase);\n const body: Record<string, unknown> = { name: input.name };\n if (typeof input.parent === 'number' && input.parent > 0) {\n body.parent = input.parent;\n }\n const raw = await this.request<unknown>(`/wp/v2/${taxRestBase}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(body),\n });\n return normalizeTerm(raw);\n }\n\n /**\n * Update a term (`POST /wp/v2/<taxRestBase>/<id>`): rename (`name`), reparent (`parent`, `0`\n * moves it to top level), or set `slug`/`description`. Only provided fields are sent. WordPress\n * enforces the caller's capability (a 403 surfaces) and rejects an invalid parent (e.g. a cycle).\n * A core REST call — no companion plugin.\n */\n async updateTerm(\n taxRestBase: string,\n id: number,\n input: { name?: string; parent?: number; slug?: string; description?: string },\n ): Promise<WpTerm> {\n assertRouteSegment(taxRestBase);\n assertTermId(id);\n const body: Record<string, unknown> = {};\n if (input.name !== undefined) body.name = input.name;\n if (input.parent !== undefined) body.parent = input.parent;\n if (input.slug !== undefined) body.slug = input.slug;\n if (input.description !== undefined) body.description = input.description;\n const raw = await this.request<unknown>(`/wp/v2/${taxRestBase}/${id}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(body),\n });\n return normalizeTerm(raw);\n }\n\n /**\n * Delete a term (`DELETE /wp/v2/<taxRestBase>/<id>?force=true`). Terms have no trash, so WordPress\n * requires `force=true`; its children are reparented to the deleted term's parent (WordPress\n * behavior). WordPress enforces the caller's capability (a 403 surfaces). A core REST call.\n */\n async deleteTerm(taxRestBase: string, id: number): Promise<void> {\n assertRouteSegment(taxRestBase);\n assertTermId(id);\n await this.send(`/wp/v2/${taxRestBase}/${id}?force=true&context=edit`, { method: 'DELETE' });\n }\n\n /**\n * Merge the source term (`fromId`) into the target term (`toId`) within one taxonomy: re-assign\n * every post that carries the source term to the target — across *every* post type the taxonomy\n * is attached to — then delete the source. WordPress has no native term-merge endpoint, so this\n * is a non-atomic compose of core REST calls.\n *\n * Why cross-type: `deleteTerm` force-deletes (terms have no trash), severing *all* of the source\n * term's relationships. If only one post type were re-assigned, posts of any other type would\n * lose the term without gaining the target. So this scans all of the taxonomy's post types.\n *\n * The source is deleted only on a fully clean merge: every reachable type fully paged (no\n * {@link MAX_MERGE_PAGES} cap hit, no type unaddressable over REST) and every re-assignment\n * succeeded. Otherwise it is kept (`deleted: false`) so the caller can surface the partial\n * result and re-run — re-running re-queries the source's remaining posts (idempotent-ish). The\n * caller must reject merging a term that has children (WordPress would reparent them on delete);\n * this method does not enforce that. A core REST call — no companion plugin needed.\n */\n async mergeTerm(taxRestBase: string, fromId: number, toId: number): Promise<MergeTermResult> {\n assertRouteSegment(taxRestBase);\n assertTermId(fromId);\n assertTermId(toId);\n if (fromId === toId) {\n throw new Error('Cannot merge a term into itself.');\n }\n\n // Resolve the REST bases of every post type this taxonomy applies to. A taxonomy type with no\n // REST-addressable post type cannot be re-assigned, so the merge cannot be guaranteed complete.\n const taxonomies = await this.listTaxonomies();\n const tax = taxonomies.find((t) => t.restBase === taxRestBase);\n if (!tax) {\n throw new Error(`Unknown taxonomy: ${taxRestBase}`);\n }\n const postTypes = await this.listPostTypes();\n const restBases: string[] = [];\n let truncated = false;\n for (const slug of tax.types) {\n const pt = postTypes.find((p) => p.slug === slug);\n if (pt) {\n restBases.push(pt.restBase);\n } else {\n // Attached to a type we cannot reach over REST: cannot fully reassign, so keep the source.\n truncated = true;\n }\n }\n // No reachable post type at all (e.g. the taxonomies response reported no `types`): we cannot\n // know what carries the source term, so refuse to delete it — deleting now would sever any\n // hidden assignments with nothing re-assigned, the exact data loss this method exists to avoid.\n if (restBases.length === 0) {\n truncated = true;\n }\n\n // Phase A — snapshot every post carrying the source term, reading (not mutating) so forward\n // paging is stable. (Re-assigning shrinks the filtered set, which would shift pages mid-scan.)\n const targets: { type: string; post: WpPost }[] = [];\n for (const type of restBases) {\n let page = 1;\n for (;;) {\n if (page > MAX_MERGE_PAGES) {\n // Too many pages to enumerate safely: keep the source rather than delete with stragglers.\n truncated = true;\n break;\n }\n const { items, totalPages, pagesReliable } = await this.listPostsByTerm(\n type,\n taxRestBase,\n fromId,\n { page, perPage: 100 },\n );\n for (const post of items) {\n targets.push({ type, post });\n }\n // With a trustworthy page count, stop at the last page; without one, trust only a short\n // (non-full) page as the end — never a defaulted \"1 page\" that could hide later pages.\n const finished = pagesReliable ? page >= totalPages : items.length < 100;\n if (finished) {\n break;\n }\n page += 1;\n }\n }\n\n // Phase B — re-assign each snapshotted post from the source term to the target.\n let reassigned = 0;\n const failed: { id: number; error: string }[] = [];\n for (const { type, post } of targets) {\n const current = post.terms[taxRestBase] ?? [];\n const next = computeMergedTermIds(current, fromId, toId);\n try {\n await this.updatePost(post.id, { terms: { [taxRestBase]: next } }, type);\n reassigned += 1;\n } catch (e) {\n failed.push({ id: post.id, error: e instanceof Error ? e.message : String(e) });\n }\n }\n\n // Delete the source only when the merge is provably complete and clean.\n let deleted = false;\n if (failed.length === 0 && !truncated) {\n await this.deleteTerm(taxRestBase, fromId);\n deleted = true;\n }\n return { reassigned, failed, deleted, truncated };\n }\n\n /**\n * Resolve specific term ids to their names in one request per 100 ids (`?include=`), used to\n * label the taxonomy columns for the posts currently shown. A core REST call — no plugin needed.\n */\n async resolveTerms(taxRestBase: string, ids: number[]): Promise<WpTerm[]> {\n assertRouteSegment(taxRestBase);\n const clean = [...new Set(ids.filter((id) => Number.isSafeInteger(id) && id > 0))];\n if (clean.length === 0) {\n return [];\n }\n const out: WpTerm[] = [];\n for (let i = 0; i < clean.length; i += 100) {\n const chunk = clean.slice(i, i + 100);\n const query = new URLSearchParams({\n context: 'edit',\n include: chunk.join(','),\n per_page: String(chunk.length),\n });\n const raw = await this.request<unknown>(`/wp/v2/${taxRestBase}?${query.toString()}`);\n if (Array.isArray(raw)) {\n out.push(...raw.map(normalizeTerm));\n }\n }\n return out;\n }\n}\n\n/**\n * Compute a post's taxonomy term IDs after merging `fromId` into `toId`: drop the source, add the\n * target, and de-duplicate (the post may already carry the target). Order-preserving for the\n * surviving ids. Pure — the merge decision for one post, unit-testable without the network.\n */\nexport function computeMergedTermIds(currentIds: number[], fromId: number, toId: number): number[] {\n return [...new Set(currentIds.filter((id) => id !== fromId).concat(toId))];\n}\n\n/** Map editable fields to the WordPress REST request body (camelCase → snake_case). */\nexport function buildUpdateBody(fields: UpdatePostFields): Record<string, unknown> {\n const body: Record<string, unknown> = {};\n if (fields.title !== undefined) {\n body.title = fields.title;\n }\n if (fields.menuOrder !== undefined) {\n body.menu_order = fields.menuOrder;\n }\n if (fields.status !== undefined) {\n body.status = fields.status;\n }\n if (fields.featuredMedia !== undefined) {\n // `0` is meaningful here (removes the featured image), so check for undefined, not falsy.\n body.featured_media = fields.featuredMedia;\n }\n if (fields.content !== undefined) {\n // `''` is meaningful here (clears the body), so check for undefined, not falsy.\n body.content = fields.content;\n }\n if (fields.terms !== undefined) {\n // Each taxonomy is sent as its own REST field keyed by REST base (e.g. `categories`); an\n // empty array clears that taxonomy's terms. Guard the key so an odd REST base can't inject\n // an unrelated body field, and skip JS magic names (`__proto__` etc.) that would pollute or\n // silently drop on this plain object.\n for (const [restBase, ids] of Object.entries(fields.terms)) {\n if (ROUTE_SEGMENT.test(restBase) && !RESERVED_KEYS.has(restBase)) {\n body[restBase] = ids;\n }\n }\n }\n return body;\n}\n\nfunction assertRouteSegment(segment: string): void {\n if (!ROUTE_SEGMENT.test(segment)) {\n throw new Error(`Invalid REST route segment: ${segment}`);\n }\n}\n\nfunction assertPostId(id: number): void {\n if (!Number.isSafeInteger(id) || id <= 0) {\n throw new Error(`Invalid post id: ${id}`);\n }\n}\n\nfunction assertTermId(id: number): void {\n if (!Number.isSafeInteger(id) || id <= 0) {\n throw new Error(`Invalid term id: ${id}`);\n }\n}\n\nfunction clampInt(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) {\n return min;\n }\n return Math.min(max, Math.max(min, Math.trunc(value)));\n}\n\n/** Wrap arbitrary meta in the companion plugin's REST field for a write request. */\nexport function buildMetaBody(meta: Record<string, unknown>): Record<string, unknown> {\n return { [META_FIELD]: meta };\n}\n\n/**\n * Build a post-update body, folding in connector meta under `dbp_wp_meta` when given.\n * A provided `meta` is included as-is, even when empty — callers that should skip empty\n * meta (e.g. the CLI batch parser) omit it before calling.\n */\nexport function buildPostBody(\n fields: UpdatePostFields,\n meta?: Record<string, unknown>,\n): Record<string, unknown> {\n const body = buildUpdateBody(fields);\n if (meta !== undefined) {\n Object.assign(body, buildMetaBody(meta));\n }\n return body;\n}\n\n/**\n * Parse a per-post meta-delete response. `send()` already threw on a non-2xx status, so an empty\n * body (e.g. a 204 from a backend that returns no content) or a non-JSON body means the delete\n * succeeded — the requested keys are reported as deleted. A JSON body's `deleted` list (the keys\n * actually present) takes precedence, and the request `id` is trusted over a malformed `post_id`.\n */\nexport function parseDeleteMetaResponse(\n body: string,\n id: number,\n requestedKeys: string[],\n): DeleteMetaResult {\n const text = body.trim();\n if (text === '') {\n return { postId: id, deleted: [...requestedKeys] };\n }\n let raw: { post_id?: unknown; deleted?: unknown };\n try {\n raw = JSON.parse(text) as { post_id?: unknown; deleted?: unknown };\n } catch {\n return { postId: id, deleted: [...requestedKeys] };\n }\n return {\n // Trust the request `id` over a malformed `post_id` (0, negative, or fractional included).\n postId:\n typeof raw.post_id === 'number' && Number.isSafeInteger(raw.post_id) && raw.post_id > 0\n ? raw.post_id\n : id,\n deleted: Array.isArray(raw.deleted)\n ? raw.deleted.filter((k): k is string => typeof k === 'string')\n : [],\n };\n}\n\n/** Validate and clean a list of meta keys for deletion (non-empty strings only). */\nexport function sanitizeMetaKeys(keys: unknown): string[] {\n if (!Array.isArray(keys)) {\n throw new Error('Meta keys must be an array of strings.');\n }\n const clean = keys.filter((key): key is string => typeof key === 'string' && key.length > 0);\n if (clean.length === 0) {\n throw new Error('At least one non-empty meta key is required.');\n }\n return clean;\n}\n\n/** True when a REST index `namespaces` list includes the connector namespace. */\nexport function hasConnectorNamespace(namespaces: unknown): boolean {\n return Array.isArray(namespaces) && namespaces.includes(CONNECTOR_NAMESPACE);\n}\n\n/**\n * Build a `Content-Disposition` value for a media upload. Emits an ASCII-only\n * `filename=\"…\"` fallback (non-ASCII and header-unsafe characters replaced with `_`) plus an\n * RFC 5987 `filename*=UTF-8''…` parameter, so a non-ASCII filename survives and never\n * produces an invalid (non-ByteString) header value that `fetch` would reject. The path is\n * reduced to its basename and quotes/CR/LF are dropped so the value cannot break the header.\n */\nexport function buildContentDisposition(filename: string): string {\n const base = filename.split(/[/\\\\]/).pop() ?? filename;\n const trimmed = base.replace(/[\\r\\n\"]/g, '').trim() || 'upload';\n const ascii = trimmed.replace(/[^\\x20-\\x7e]/g, '_');\n // Percent-encode per RFC 5987, also encoding the chars encodeURIComponent leaves bare.\n const encoded = encodeURIComponent(trimmed).replace(\n /['()*!]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n return `attachment; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\n}\n\n/** Parse the `X-WP-TotalPages` header into a positive page count, defaulting to 1. */\nfunction parseTotalPages(header: string | null): number {\n if (header === null) {\n return 1;\n }\n const n = Number.parseInt(header, 10);\n return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\n/** Read `<obj>.rendered` as a string, or `''` when absent/malformed. */\nfunction extractRendered(value: unknown): string {\n if (value !== null && typeof value === 'object') {\n const rendered = (value as Record<string, unknown>).rendered;\n if (typeof rendered === 'string') {\n return rendered;\n }\n }\n return '';\n}\n\n/** Prefer a thumbnail-sized URL from `media_details.sizes`, else a medium one, else `''`. */\nfunction extractThumbnailUrl(mediaDetails: unknown): string {\n if (mediaDetails === null || typeof mediaDetails !== 'object') {\n return '';\n }\n const sizes = (mediaDetails as Record<string, unknown>).sizes;\n if (sizes === null || typeof sizes !== 'object') {\n return '';\n }\n for (const sizeName of ['thumbnail', 'medium']) {\n const size = (sizes as Record<string, unknown>)[sizeName];\n if (size !== null && typeof size === 'object') {\n const url = (size as Record<string, unknown>).source_url;\n if (typeof url === 'string') {\n return url;\n }\n }\n }\n return '';\n}\n\n/**\n * Build the size list for the body editor's image picker from `media_details`: every generated\n * size (smallest first by width) plus a synthesized `full` entry pointing at the source URL when\n * the response did not already include one. Returns `[]` when nothing is resolvable (e.g. a\n * non-image attachment).\n */\nfunction extractSizes(mediaDetails: unknown, sourceUrl: string, mimeType: string): WpMediaSize[] {\n // Sizes are only meaningful for images; a non-image attachment has nothing to insert as <img>.\n if (!mimeType.startsWith('image/')) {\n return [];\n }\n const details =\n typeof mediaDetails === 'object' && mediaDetails !== null\n ? (mediaDetails as Record<string, unknown>)\n : {};\n const out: WpMediaSize[] = [];\n const sizes = details.sizes;\n if (sizes !== null && typeof sizes === 'object') {\n for (const [name, raw] of Object.entries(sizes as Record<string, unknown>)) {\n if (raw === null || typeof raw !== 'object') {\n continue;\n }\n const s = raw as Record<string, unknown>;\n const url = typeof s.source_url === 'string' ? s.source_url : '';\n if (url === '') {\n continue;\n }\n out.push({\n name,\n url,\n width: typeof s.width === 'number' ? s.width : 0,\n height: typeof s.height === 'number' ? s.height : 0,\n });\n }\n }\n out.sort((a, b) => a.width - b.width);\n // Some responses omit `full` from `sizes`; ensure it is always offered (and not duplicated).\n if (sourceUrl !== '' && !out.some((s) => s.name === 'full' || s.url === sourceUrl)) {\n out.push({\n name: 'full',\n url: sourceUrl,\n width: typeof details.width === 'number' ? details.width : 0,\n height: typeof details.height === 'number' ? details.height : 0,\n });\n }\n return out;\n}\n\n/**\n * Normalize a raw `/wp/v2/media` item into {@link WpMedia}. Accepts `unknown` and guards each\n * field, so a malformed entry degrades to empty strings rather than throwing.\n */\nexport function normalizeMedia(raw: unknown): WpMedia {\n const obj = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>;\n const sourceUrl = typeof obj.source_url === 'string' ? obj.source_url : '';\n const mimeType = typeof obj.mime_type === 'string' ? obj.mime_type : '';\n return {\n id: typeof obj.id === 'number' ? obj.id : 0,\n sourceUrl,\n thumbnailUrl: extractThumbnailUrl(obj.media_details) || sourceUrl,\n title: extractRendered(obj.title),\n mimeType,\n sizes: extractSizes(obj.media_details, sourceUrl, mimeType),\n };\n}\n\n/**\n * Normalize the `/wp/v2/types` response (an object keyed by type name) into a list.\n * Skips entries without a string `rest_base` (not addressable over REST).\n */\nexport function normalizePostTypes(raw: unknown): WpPostType[] {\n if (typeof raw !== 'object' || raw === null) {\n return [];\n }\n const result: WpPostType[] = [];\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null) {\n continue;\n }\n const entry = value as Record<string, unknown>;\n // Validate rest_base at ingestion so a malformed slug can't become a broken option.\n if (typeof entry.rest_base !== 'string' || !ROUTE_SEGMENT.test(entry.rest_base)) {\n continue;\n }\n result.push({\n slug: typeof entry.slug === 'string' ? entry.slug : key,\n restBase: entry.rest_base,\n name: typeof entry.name === 'string' ? entry.name : key,\n });\n }\n return result;\n}\n\n/**\n * Normalize the `/wp/v2/taxonomies` response (an object keyed by taxonomy slug) into a list.\n * Skips entries without a valid `rest_base` (not addressable over REST), mirroring\n * {@link normalizePostTypes}.\n */\nexport function normalizeTaxonomies(raw: unknown): WpTaxonomy[] {\n if (typeof raw !== 'object' || raw === null) {\n return [];\n }\n const result: WpTaxonomy[] = [];\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null) {\n continue;\n }\n const entry = value as Record<string, unknown>;\n // Reject a magic-name REST base (e.g. `__proto__`): it passes the route allowlist but is\n // unsafe as a key in the plain-object term maps the UI and write path build.\n if (\n typeof entry.rest_base !== 'string' ||\n !ROUTE_SEGMENT.test(entry.rest_base) ||\n RESERVED_KEYS.has(entry.rest_base)\n ) {\n continue;\n }\n result.push({\n slug: typeof entry.slug === 'string' ? entry.slug : key,\n restBase: entry.rest_base,\n name: typeof entry.name === 'string' ? entry.name : key,\n hierarchical: entry.hierarchical === true,\n // The post types this taxonomy applies to; a term merge re-assigns across all of them.\n types: Array.isArray(entry.types) ? entry.types.filter((t): t is string => typeof t === 'string') : [],\n });\n }\n return result;\n}\n\n/**\n * Normalize a raw `/wp/v2/<taxonomy>` term. Accepts `unknown` and guards each field, so a\n * malformed entry degrades to a zero id / empty name rather than throwing.\n */\nexport function normalizeTerm(raw: unknown): WpTerm {\n const obj = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>;\n return {\n id: typeof obj.id === 'number' ? obj.id : 0,\n name: typeof obj.name === 'string' ? obj.name : '',\n parent: typeof obj.parent === 'number' ? obj.parent : 0,\n count: typeof obj.count === 'number' ? obj.count : 0,\n };\n}\n\n/**\n * Extract taxonomy term-ID assignments from a raw post, keyed by the taxonomy's REST base.\n *\n * On a WordPress post the only top-level fields that are arrays of positive integers are\n * taxonomy assignments (e.g. `categories`, `tags`, custom taxonomies), so capture exactly those.\n * The UI reads only the taxonomies it knows about (from `/wp/v2/taxonomies`), so a stray numeric\n * array would simply be ignored downstream. Empty arrays are omitted (no terms assigned). Uses a\n * null-prototype map so a pathological key (`__proto__`) is an ordinary own property.\n */\nfunction extractPostTerms(raw: WpPostResponse): Record<string, number[]> {\n const result: Record<string, number[]> = Object.create(null) as Record<string, number[]>;\n for (const [key, value] of Object.entries(raw as unknown as Record<string, unknown>)) {\n if (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((v) => typeof v === 'number' && Number.isInteger(v) && v > 0)\n ) {\n result[key] = value as number[];\n }\n }\n return result;\n}\n\nexport function normalizePost(raw: WpPostResponse): WpPost {\n const post: WpPost = {\n id: raw.id,\n type: raw.type,\n status: raw.status,\n title: raw.title.raw ?? raw.title.rendered,\n menuOrder: raw.menu_order,\n meta: raw.meta,\n terms: extractPostTerms(raw),\n };\n if (raw.dbp_wp_meta !== undefined) {\n post.dbpWpMeta = raw.dbp_wp_meta;\n }\n // featured_media is always present on a core post; 0 means \"no featured image\".\n if (typeof raw.featured_media === 'number' && raw.featured_media > 0) {\n post.featuredMedia = raw.featured_media;\n }\n // Parent relation rides the standard `meta` field (registered by the connector). It needs\n // both a positive id and a non-empty type; a half-written pair — e.g. from a raw REST\n // write that set only one key, or a parent type sanitized to '' — reads as no relation.\n // parent and parentType are therefore set together or not at all, so normalizePost,\n // getRelation, and deriveChildren stay consistent on malformed data.\n const rawParent = raw.meta?.[PARENT_META_KEY];\n const rawParentType = raw.meta?.[PARENT_TYPE_META_KEY];\n if (\n typeof rawParent === 'number' &&\n Number.isInteger(rawParent) &&\n rawParent > 0 &&\n typeof rawParentType === 'string' &&\n rawParentType !== ''\n ) {\n post.parent = rawParent;\n post.parentType = rawParentType;\n }\n return post;\n}\n\n/**\n * Normalize a raw post (edit context) into the body-editing model {@link WpPostEdit}.\n *\n * Reads `content.raw` for the body. Mode is determined by the Markdown source: only a\n * *non-empty* `_dbp_wp_markdown` value marks Markdown mode. This mirrors the relation keys'\n * value-based sentinel (a positive id means \"set\") and sidesteps the REST default for a\n * registered string meta (an unset key can come back as `''`), so an HTML-mode post is never\n * mistaken for an empty-bodied Markdown post.\n */\nexport function normalizePostForEdit(raw: WpPostResponse): WpPostEdit {\n const edit: WpPostEdit = {\n id: raw.id,\n type: raw.type,\n status: raw.status,\n title: raw.title.raw ?? raw.title.rendered,\n content: raw.content?.raw ?? '',\n };\n const markdown = raw.meta?.[MARKDOWN_META_KEY];\n if (typeof markdown === 'string' && markdown !== '') {\n edit.markdown = markdown;\n }\n return edit;\n}\n","import { marked } from 'marked';\n\n/**\n * Convert Markdown source to HTML for the single-post body editor.\n *\n * One shared implementation, used by the UI (live preview and the HTML mirrored into\n * `post_content` on save) and available to the CLI/future MCP layer, so a post's stored\n * HTML always matches what the editor previewed. GitHub-flavored Markdown is enabled.\n *\n * This does NOT sanitize the output. Sanitization is delegated to WordPress `kses` at save\n * time (capability-dependent), and the live preview is isolated in a sandboxed iframe with\n * no script execution — matching the body-editing design (no extra sanitizer dependency in\n * DBP WP).\n */\nexport function renderMarkdown(markdown: string): string {\n // `async: false` selects marked's synchronous overload (string return). Guard the result\n // so an unexpected non-string (e.g. a future async extension) degrades to empty rather\n // than leaking a Promise into the rendered body.\n const html = marked.parse(markdown, { async: false, gfm: true });\n return typeof html === 'string' ? html : '';\n}\n","import { Parser, type Expression } from 'expr-eval-fork';\n\n/**\n * Formula engine: evaluates a spreadsheet expression against named numeric cells.\n *\n * Implementations MUST NOT use `eval`, `Function`, or any other dynamic code execution.\n */\nexport interface FormulaEngine {\n /**\n * Evaluate a single expression against a map of cell references to numbers.\n * Throws on invalid syntax, unknown variables, or a non-numeric result.\n */\n evaluate(expression: string, context: Record<string, number>): number;\n}\n\n/**\n * Formula engine backed by expr-eval-fork, which parses to an AST and evaluates without\n * `eval`/`Function`. Member access, assignment, and function definitions are disabled,\n * the nondeterministic `random()` function is removed, and results are constrained to\n * finite numbers, so expressions stay pure, deterministic, and side-effect free.\n */\nexport class SafeFormulaEngine implements FormulaEngine {\n private readonly parser: Parser;\n\n constructor() {\n this.parser = new Parser({\n allowMemberAccess: false,\n operators: { assignment: false, fndef: false },\n });\n // The `operators.random` flag does not remove the random() function; delete it so\n // evaluation stays deterministic.\n delete this.parser.functions.random;\n }\n\n evaluate(expression: string, context: Record<string, number>): number {\n let parsed: Expression;\n try {\n parsed = this.parser.parse(expression);\n } catch (e) {\n throw new Error(`Invalid formula: ${e instanceof Error ? e.message : 'parse error'}`);\n }\n\n let result: unknown;\n try {\n result = parsed.evaluate(context);\n } catch (e) {\n throw new Error(`Formula evaluation failed: ${e instanceof Error ? e.message : 'error'}`);\n }\n\n if (typeof result !== 'number' || !Number.isFinite(result)) {\n throw new Error('Formula must evaluate to a finite number.');\n }\n return result;\n }\n}\n","import type { UpdatePostFields } from './types';\n\n// WordPress stores menu_order in a signed 32-bit column; ignore cells outside that range\n// so one bad value does not get the whole server-side chunk rejected.\nconst MENU_ORDER_MIN = -2_147_483_648;\nconst MENU_ORDER_MAX = 2_147_483_647;\n\n/**\n * A tabular view of an import file: a header row plus data rows. Both CSV and JSON\n * sources are normalized to this shape so the column-mapping logic is source-agnostic.\n * Each data row is aligned to `headers` by index; a short row has missing trailing cells.\n */\nexport interface ParsedTable {\n /** Column headers (CSV first row, or the union of JSON object keys). */\n headers: string[];\n /** Data rows; `rows[r][c]` is the cell under `headers[c]`. */\n rows: string[][];\n}\n\n/**\n * Where a file column is imported to. `skip` drops the column; `title`/`status`/\n * `menuOrder` map to standard post fields; `meta` writes an arbitrary post-meta key\n * (companion plugin required).\n */\nexport type ImportTarget =\n | { kind: 'skip' }\n | { kind: 'title' }\n | { kind: 'status' }\n | { kind: 'menuOrder' }\n | { kind: 'meta'; key: string };\n\n/** A single new post to create, derived from one import row. */\nexport interface ImportCreate {\n /** Standard fields (title / menuOrder / status). */\n fields: UpdatePostFields;\n /** Arbitrary meta to write via the companion plugin (omitted when none). */\n meta?: Record<string, unknown>;\n}\n\n/**\n * Parse CSV text into a table, taking the first record as headers. Implements the\n * RFC 4180 essentials: double-quoted fields, embedded commas/newlines, `\"\"` escapes,\n * and CRLF or LF line endings. A trailing newline does not produce an empty record.\n */\nexport function parseCsv(text: string): ParsedTable {\n const records = parseCsvRecords(text);\n const headers = records[0] ?? [];\n return { headers, rows: records.slice(1) };\n}\n\nfunction parseCsvRecords(text: string): string[][] {\n const records: string[][] = [];\n let record: string[] = [];\n let field = '';\n let inQuotes = false;\n let i = 0;\n\n const endField = (): void => {\n record.push(field);\n field = '';\n };\n const endRecord = (): void => {\n endField();\n records.push(record);\n record = [];\n };\n\n while (i < text.length) {\n const ch = text[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (text[i + 1] === '\"') {\n field += '\"';\n i += 2;\n continue;\n }\n inQuotes = false;\n i += 1;\n continue;\n }\n field += ch;\n i += 1;\n continue;\n }\n if (ch === '\"') {\n inQuotes = true;\n i += 1;\n continue;\n }\n if (ch === ',') {\n endField();\n i += 1;\n continue;\n }\n if (ch === '\\r') {\n endRecord();\n i += text[i + 1] === '\\n' ? 2 : 1;\n continue;\n }\n if (ch === '\\n') {\n endRecord();\n i += 1;\n continue;\n }\n field += ch;\n i += 1;\n }\n // An unclosed quote means the file is malformed; surface it rather than silently\n // merging the rest of the file into one field and writing corrupted data.\n if (inQuotes) {\n throw new Error('Malformed CSV: unterminated quoted field.');\n }\n // Flush a final record only if there is pending content (no trailing-newline ghost row).\n if (field !== '' || record.length > 0) {\n endRecord();\n }\n return records;\n}\n\n/**\n * Parse JSON text (an array of objects) into a table. Headers are the union of all\n * object keys in first-seen order. Object/array cell values are JSON-stringified;\n * null and undefined become an empty string. Throws if the JSON is not an array.\n */\nexport function parseJsonRecords(text: string): ParsedTable {\n const data: unknown = JSON.parse(text);\n if (!Array.isArray(data)) {\n throw new Error('JSON import must be an array of objects.');\n }\n const headers: string[] = [];\n const seen = new Set<string>();\n for (const entry of data) {\n if (isPlainRecord(entry)) {\n for (const key of Object.keys(entry)) {\n if (!seen.has(key)) {\n seen.add(key);\n headers.push(key);\n }\n }\n }\n }\n const rows = data.map((entry) => {\n const record = isPlainRecord(entry) ? entry : {};\n return headers.map((header) => stringifyCell(record[header]));\n });\n return { headers, rows };\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction stringifyCell(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n if (typeof value === 'object') {\n return JSON.stringify(value);\n }\n return String(value);\n}\n\n/**\n * Known post-status values and English/value labels, mapped to the WordPress status.\n * A null-prototype map so inherited keys (`constructor`, `toString`, `__proto__`, …) do\n * not accidentally resolve to a function/object instead of falling back to the raw value.\n */\nconst STATUS_LABELS: Record<string, string> = Object.assign(\n Object.create(null) as Record<string, string>,\n {\n publish: 'publish',\n published: 'publish',\n draft: 'draft',\n pending: 'pending',\n private: 'private',\n future: 'future',\n },\n);\n\n/**\n * Normalize a status cell to a WordPress status. Known labels/values (case-insensitive,\n * e.g. `Published` → `publish`) are mapped; anything else passes through trimmed so the\n * WordPress REST API can validate it and surface a per-row error if invalid.\n */\nexport function normalizeStatus(value: string): string {\n const trimmed = value.trim();\n return STATUS_LABELS[trimmed.toLowerCase()] ?? trimmed;\n}\n\n/**\n * Apply a column mapping to a parsed table, producing one {@link ImportCreate} per row.\n * Empty cells contribute nothing; a row that maps to no fields and no meta is skipped.\n * Non-integer `menuOrder` cells are ignored. Meta is stored on a null-prototype object\n * so a header named `__proto__` is kept as data, never touching any prototype.\n */\nexport function buildImportPlan(table: ParsedTable, mapping: ImportTarget[]): ImportCreate[] {\n const creates: ImportCreate[] = [];\n for (const row of table.rows) {\n const fields: UpdatePostFields = {};\n let meta: Record<string, unknown> | undefined;\n\n for (let col = 0; col < mapping.length; col += 1) {\n const target = mapping[col];\n if (!target || target.kind === 'skip') {\n continue;\n }\n const value = row[col] ?? '';\n if (value === '') {\n continue;\n }\n switch (target.kind) {\n case 'title':\n fields.title = value;\n break;\n case 'status':\n fields.status = normalizeStatus(value);\n break;\n case 'menuOrder': {\n const order = Number(value);\n if (Number.isInteger(order) && order >= MENU_ORDER_MIN && order <= MENU_ORDER_MAX) {\n fields.menuOrder = order;\n }\n break;\n }\n case 'meta':\n // An empty/whitespace meta key would be rejected by the server (empty key),\n // failing the whole chunk; skip it rather than emit `meta[\"\"]`.\n if (target.key.trim() === '') {\n break;\n }\n if (!meta) {\n meta = Object.create(null) as Record<string, unknown>;\n }\n meta[target.key] = value;\n break;\n }\n }\n\n if (meta !== undefined && Object.keys(meta).length > 0) {\n creates.push({ fields, meta });\n } else if (Object.keys(fields).length > 0) {\n creates.push({ fields });\n }\n }\n return creates;\n}\n"],"mappings":"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","names":["flattenMetaValue","ROUTE_SEGMENT"]}
1
+ {"version":3,"sources":["../src/print.ts","../src/relation.ts","../src/wp-client.ts","../src/markdown.ts","../src/calc/index.ts","../src/importer.ts"],"sourcesContent":["/**\n * Print Design template engine.\n *\n * Renders a user-authored HTML template against a {@link PrintRecord} using a small,\n * mustache-like `{{ }}` syntax. Pure and framework-agnostic: the browser UI renders the\n * result inside a sandboxed iframe and prints it (see planning doc 04-print-design).\n *\n * Syntax:\n * - `{{ path }}` resolve a dotted path, HTML-escape the value, and output it.\n * - `{{{ path }}}` same, but output the value raw (no escaping) for HTML fields such as\n * `content` / `excerpt`, which already hold WordPress-rendered HTML.\n * - `{{#each path}} ... {{/each}}` iterate an array; inside the block, `this` is the\n * current item (and `this.<key>` reaches into an object item).\n *\n * Unknown paths render as the empty string. Paths resolve against the record, except\n * `this` / `this.<key>`, which resolve against the current `{{#each}}` item.\n */\n\nimport type { WpPostResponse } from './types';\n\n/** The data a template is rendered against (one WordPress post, flattened for templating). */\nexport interface PrintRecord {\n id: number;\n title: string;\n /** WordPress-rendered HTML. Use `{{{ content }}}` so it is not escaped. */\n content: string;\n /** WordPress-rendered HTML. Use `{{{ excerpt }}}` so it is not escaped. */\n excerpt: string;\n status: string;\n menuOrder: number;\n /** Absolute URL of the featured image, or `''` when the post has none. */\n featuredImageUrl: string;\n /**\n * Flattened post meta, keyed by meta key (values stringified; arrays joined with `, `).\n * Reachable in templates as `{{ meta.<key> }}`. A meta key containing a literal dot\n * cannot be addressed, since template paths split on `.`.\n */\n meta: Record<string, string>;\n /** Taxonomy terms keyed by REST base (e.g. `tax.category`), each an array of term names. */\n tax: Record<string, string[]>;\n}\n\n/** Thrown when a template has an unbalanced `{{#each}}` / `{{/each}}`. */\nexport class TemplateParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TemplateParseError';\n }\n}\n\ntype TemplateNode = TextNode | VarNode | EachNode;\ninterface TextNode {\n kind: 'text';\n value: string;\n}\ninterface VarNode {\n kind: 'var';\n path: string;\n /** `true` for triple-brace `{{{ }}}` (unescaped) output. */\n raw: boolean;\n}\ninterface EachNode {\n kind: 'each';\n path: string;\n body: TemplateNode[];\n}\n\ninterface Scope {\n /** The record paths resolve against by default. */\n record: object;\n /** The current `{{#each}}` item, reachable as `this` / `this.<key>`. */\n current?: unknown;\n}\n\n// Order matters: triple-brace and the each tags must be tried before the generic `{{ }}`.\n// `[\\s\\S]` (not `.`) so a tag may span newlines.\nconst TAG =\n /(\\{\\{\\{\\s*([\\s\\S]+?)\\s*\\}\\}\\})|(\\{\\{\\s*#each\\s+([\\s\\S]+?)\\s*\\}\\})|(\\{\\{\\s*\\/each\\s*\\}\\})|(\\{\\{\\s*([\\s\\S]+?)\\s*\\}\\})/g;\n\n/** Parse a template string into a node tree, validating `{{#each}}` nesting. */\nfunction parseTemplate(template: string): TemplateNode[] {\n const root: TemplateNode[] = [];\n // `current` is the node list we append to; `parents` lets us pop back out of an each\n // block; `open` tracks open each blocks so we can detect unbalanced tags.\n let current: TemplateNode[] = root;\n const parents: TemplateNode[][] = [];\n const open: EachNode[] = [];\n const pushText = (text: string): void => {\n if (text) current.push({ kind: 'text', value: text });\n };\n\n let last = 0;\n TAG.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = TAG.exec(template)) !== null) {\n pushText(template.slice(last, m.index));\n last = TAG.lastIndex;\n if (m[2] !== undefined) {\n // {{{ raw }}}\n current.push({ kind: 'var', path: m[2].trim(), raw: true });\n } else if (m[4] !== undefined) {\n // {{#each path}}\n const node: EachNode = { kind: 'each', path: m[4].trim(), body: [] };\n current.push(node);\n parents.push(current);\n current = node.body;\n open.push(node);\n } else if (m[5] !== undefined) {\n // {{/each}}\n if (open.length === 0) {\n throw new TemplateParseError('Unexpected {{/each}} without a matching {{#each}}.');\n }\n open.pop();\n current = parents.pop() ?? root;\n } else if (m[7] !== undefined) {\n // {{ var }}\n current.push({ kind: 'var', path: m[7].trim(), raw: false });\n }\n }\n pushText(template.slice(last));\n\n if (open.length > 0) {\n const unclosed = open[open.length - 1];\n throw new TemplateParseError(`Unclosed {{#each ${unclosed ? unclosed.path : ''}}}.`);\n }\n return root;\n}\n\n/** Walk a dotted path (e.g. `meta.price`) into a value, returning `undefined` if any hop misses. */\nfunction getPath(obj: unknown, path: string): unknown {\n let cur: unknown = obj;\n for (const key of path.split('.')) {\n if (cur === null || cur === undefined || typeof cur !== 'object') {\n return undefined;\n }\n cur = (cur as Record<string, unknown>)[key];\n }\n return cur;\n}\n\n/** Resolve a template path against the scope. `this` / `this.<key>` target the each item. */\nfunction resolve(path: string, scope: Scope): unknown {\n if (path === 'this') {\n return scope.current;\n }\n if (path.startsWith('this.')) {\n return getPath(scope.current, path.slice('this.'.length));\n }\n return getPath(scope.record, path);\n}\n\n/** Stringify a scalar for output; non-scalars (objects/arrays) and nullish render as ''. */\nfunction stringifyScalar(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n if (typeof value === 'string') {\n return value;\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n // Objects/arrays have no sensible inline string form; render nothing.\n return '';\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\nfunction renderNodes(nodes: TemplateNode[], scope: Scope, escape: boolean): string {\n let out = '';\n for (const node of nodes) {\n if (node.kind === 'text') {\n out += node.value;\n } else if (node.kind === 'var') {\n const value = stringifyScalar(resolve(node.path, scope));\n // `{{{ }}}` is always raw; `{{ }}` is HTML-escaped only in escaping (HTML) mode.\n out += node.raw || !escape ? value : escapeHtml(value);\n } else {\n // each: iterate only when the path resolves to an array; otherwise render nothing.\n const list = resolve(node.path, scope);\n if (Array.isArray(list)) {\n for (const item of list) {\n out += renderNodes(node.body, { record: scope.record, current: item }, escape);\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Render a Print Design template against one record. Values are HTML-escaped for `{{ }}`\n * and emitted raw for `{{{ }}}`. Throws {@link TemplateParseError} on unbalanced\n * `{{#each}}` / `{{/each}}`.\n */\nexport function renderTemplate(template: string, record: PrintRecord): string {\n return renderNodes(parseTemplate(template), { record }, true);\n}\n\n/**\n * Render a template against an arbitrary record object, reusing the same `{{ }}` /\n * `{{#each}}` engine as {@link renderTemplate}. By default `{{ }}` values are HTML-escaped\n * (HTML output). Pass `escape: false` for a plain-text context (e.g. a spreadsheet cell\n * rendered as text, where the host already escapes for the DOM and re-escaping here would\n * surface literal entities). Throws {@link TemplateParseError} on unbalanced `{{#each}}`.\n */\nexport function renderRecordTemplate(\n template: string,\n record: object,\n options: { escape?: boolean } = {},\n): string {\n return renderNodes(parseTemplate(template), { record }, options.escape ?? true);\n}\n\n/** Flatten one meta value to a string. Arrays join their non-empty scalar parts. */\nfunction flattenMetaValue(value: unknown): string {\n if (Array.isArray(value)) {\n return value\n .map(stringifyScalar)\n .filter((s) => s !== '')\n .join(', ');\n }\n return stringifyScalar(value);\n}\n\n/** Merge core meta and connector meta (connector wins) into flat string values. */\nfunction flattenMeta(raw: WpPostResponse): Record<string, string> {\n // Null-proto so untrusted meta keys (e.g. `__proto__`, `constructor`) become plain own\n // entries with no prototype pollution and no inherited-key collisions.\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n const add = (src: Record<string, unknown> | undefined): void => {\n if (!src) return;\n for (const [key, value] of Object.entries(src)) {\n out[key] = flattenMetaValue(value);\n }\n };\n add(raw.meta);\n add(raw.dbp_wp_meta); // connector meta overlays core meta for the same key\n return out;\n}\n\n/** Extract the featured image URL from an `_embed`ded response, or '' when absent. */\nfunction extractFeaturedImageUrl(embedded: WpPostResponse['_embedded']): string {\n const media = embedded?.['wp:featuredmedia'];\n if (Array.isArray(media) && media.length > 0) {\n const first = media[0];\n if (first !== null && typeof first === 'object') {\n const sourceUrl = (first as Record<string, unknown>).source_url;\n if (typeof sourceUrl === 'string') {\n return sourceUrl;\n }\n }\n }\n return '';\n}\n\n/** Group `_embed`ded terms into `{ <taxonomy slug>: [term name, ...] }`. */\nfunction extractTerms(embedded: WpPostResponse['_embedded']): Record<string, string[]> {\n // Null-proto so a taxonomy slug like `toString`/`__proto__` cannot resolve an inherited\n // value (which would make `(tax[slug] ??= [])` skip the array and throw on `.push`).\n const tax: Record<string, string[]> = Object.create(null) as Record<string, string[]>;\n const groups = embedded?.['wp:term'];\n if (!Array.isArray(groups)) {\n return tax;\n }\n for (const group of groups) {\n if (!Array.isArray(group)) continue;\n for (const term of group) {\n if (term === null || typeof term !== 'object') continue;\n const entry = term as Record<string, unknown>;\n if (typeof entry.taxonomy === 'string' && typeof entry.name === 'string') {\n (tax[entry.taxonomy] ??= []).push(entry.name);\n }\n }\n }\n return tax;\n}\n\n/**\n * Build a {@link PrintRecord} from a raw WordPress REST post. Expects the request to have\n * used `context=edit` and `_embed` so `content`/`excerpt` and embedded media/terms are\n * present; missing pieces degrade to empty values rather than throwing.\n *\n * `title` uses the raw (unescaped) value so `{{ title }}` escaping is not doubled.\n * `content`/`excerpt` use the rendered HTML (intended for `{{{ }}}`).\n */\nexport function buildPrintRecord(raw: WpPostResponse): PrintRecord {\n return {\n id: raw.id,\n title: raw.title?.raw ?? raw.title?.rendered ?? '',\n content: raw.content?.rendered ?? '',\n excerpt: raw.excerpt?.rendered ?? '',\n status: raw.status,\n menuOrder: raw.menu_order,\n featuredImageUrl: extractFeaturedImageUrl(raw._embedded),\n meta: flattenMeta(raw),\n tax: extractTerms(raw._embedded),\n };\n}\n","import { renderRecordTemplate } from './print';\nimport type { WpPost } from './types';\n\n/**\n * Parent/child relations (MVP: links only).\n *\n * The relation is stored single-source on the **child**: `_dbp_wp_parent` holds the\n * parent post ID and `_dbp_wp_parent_type` holds the parent post type's REST route base.\n * The parent keeps no child list — a parent's children are derived from already-loaded\n * posts ({@link deriveChildren}), so there is no denormalized list to keep in sync.\n *\n * These keys are `_`-prefixed (protected) and are exposed over REST only because the\n * companion plugin registers them with `register_post_meta()` + an `edit_post`\n * `auth_callback`. They therefore travel through the standard core `meta` field — not the\n * connector's `dbp_wp_meta` field — so relation writes use a dedicated path.\n */\n\n/** Meta key holding a child's parent post ID. */\nexport const PARENT_META_KEY = '_dbp_wp_parent';\n\n/** Meta key holding the parent post type's REST route base. */\nexport const PARENT_TYPE_META_KEY = '_dbp_wp_parent_type';\n\n/** Allowed characters for a REST route segment (post type slug); mirrors the WpClient check. */\nconst ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;\n\n/** A parent assignment for a child post. */\nexport interface RelationTarget {\n /** Parent post ID (positive integer). */\n parentId: number;\n /** Parent post type's REST route base (e.g. `pages`). */\n parentType: string;\n}\n\n/** Error thrown when a relation assignment is invalid (bad id/type, or self-parent). */\nexport class RelationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RelationError';\n }\n}\n\n/**\n * Validate a parent assignment for a child. Throws {@link RelationError} when the parent\n * id is not a positive safe integer, the parent type is not a valid route segment, or the\n * parent is the child itself (post IDs are unique across types, so this catches a\n * self-parent regardless of type).\n */\nexport function assertValidRelation(childId: number, relation: RelationTarget): void {\n if (!Number.isSafeInteger(relation.parentId) || relation.parentId <= 0) {\n throw new RelationError(`Invalid parent id: ${String(relation.parentId)}`);\n }\n if (typeof relation.parentType !== 'string' || !ROUTE_SEGMENT.test(relation.parentType)) {\n throw new RelationError(`Invalid parent type: ${String(relation.parentType)}`);\n }\n if (relation.parentId === childId) {\n throw new RelationError('A post cannot be its own parent.');\n }\n}\n\n/**\n * Build the standard-`meta` body that sets a child's parent. Validates the assignment\n * first ({@link assertValidRelation}). The keys ride the core `meta` field, so the caller\n * sends `{ meta: <this> }`.\n */\nexport function buildSetRelationMeta(\n childId: number,\n relation: RelationTarget,\n): Record<string, unknown> {\n assertValidRelation(childId, relation);\n return {\n [PARENT_META_KEY]: relation.parentId,\n [PARENT_TYPE_META_KEY]: relation.parentType,\n };\n}\n\n/**\n * Build the standard-`meta` body that clears a child's parent. Sending `null` for a\n * registered single meta key makes WordPress delete it, so no stale value is left behind.\n */\nexport function buildClearRelationMeta(): Record<string, unknown> {\n return {\n [PARENT_META_KEY]: null,\n [PARENT_TYPE_META_KEY]: null,\n };\n}\n\n/**\n * Read a post's parent relation from its normalized fields, or null when it has no parent.\n * Both a positive `parent` id and a non-empty `parentType` are required for a relation to\n * count (a half-written relation reads as none).\n */\nexport function getRelation(post: WpPost): RelationTarget | null {\n if (\n typeof post.parent === 'number' &&\n post.parent > 0 &&\n typeof post.parentType === 'string' &&\n post.parentType !== ''\n ) {\n return { parentId: post.parent, parentType: post.parentType };\n }\n return null;\n}\n\n/**\n * Derive a parent's children from already-loaded posts: every post whose `_dbp_wp_parent`\n * equals `parentId`. This covers the common case of same-type children visible in the\n * current grid; cross-type or off-grid children would need a server-side query (deferred).\n */\nexport function deriveChildren(posts: WpPost[], parentId: number): WpPost[] {\n if (!Number.isSafeInteger(parentId) || parentId <= 0) {\n return [];\n }\n return posts.filter((post) => post.parent === parentId);\n}\n\n// --- Child data (template aggregation of a parent's children) ---\n//\n// The legacy app stored a per-parent template (`_dbpcloudwp_child_formula`) and cached its\n// rendered output (`_dbpcloudwp_child_value`) as parent meta — a denormalized value that\n// went stale whenever a child changed. The new model persists nothing: a column-level\n// template is rendered live against each parent's derived children, reusing the Print\n// template engine ({@link renderRecordTemplate}). So `{{ }}`/`{{#each}}` work as in Print.\n\n/** A child post flattened for templating inside a parent's child-data column. */\nexport interface ChildRecord {\n id: number;\n title: string;\n status: string;\n menuOrder: number;\n /** Flattened meta (core overlaid by connector), values stringified; `this.meta.<key>`. */\n meta: Record<string, string>;\n}\n\n/**\n * A parent exposed to a child-data template: its own fields plus the derived `children`\n * (and `childCount`), so a template can aggregate them, e.g.\n * `{{#each children}}{{ this.title }}{{/each}}`.\n */\nexport interface ParentAggregateRecord {\n id: number;\n title: string;\n status: string;\n menuOrder: number;\n meta: Record<string, string>;\n childCount: number;\n children: ChildRecord[];\n}\n\n/** Stringify one meta value; an array joins its non-empty scalar parts with `, `. */\nfunction flattenMetaValue(value: unknown): string {\n if (Array.isArray(value)) {\n return value\n .map((v) => (v === null || v === undefined ? '' : String(v)))\n .filter((s) => s !== '')\n .join(', ');\n }\n return value === null || value === undefined ? '' : String(value);\n}\n\n/**\n * Flatten a post's meta (core `meta` overlaid by connector `dbpWpMeta`) to flat string\n * values. Uses a null-proto accumulator so untrusted meta keys (`__proto__`, `constructor`)\n * become plain own entries with no prototype pollution and no inherited-key collisions.\n */\nfunction flattenPostMeta(post: WpPost): Record<string, string> {\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n const add = (src: Record<string, unknown> | undefined): void => {\n if (!src) return;\n for (const [key, value] of Object.entries(src)) {\n out[key] = flattenMetaValue(value);\n }\n };\n add(post.meta);\n add(post.dbpWpMeta); // connector meta overlays core meta for the same key\n return out;\n}\n\n/** Build a {@link ChildRecord} from a loaded post (meta flattened for templating). */\nexport function buildChildRecord(post: WpPost): ChildRecord {\n return {\n id: post.id,\n title: post.title,\n status: post.status,\n menuOrder: post.menuOrder,\n meta: flattenPostMeta(post),\n };\n}\n\n/** Build the {@link ParentAggregateRecord} a child-data template renders against. */\nexport function buildParentAggregate(parent: WpPost, children: WpPost[]): ParentAggregateRecord {\n return {\n id: parent.id,\n title: parent.title,\n status: parent.status,\n menuOrder: parent.menuOrder,\n meta: flattenPostMeta(parent),\n childCount: children.length,\n children: children.map(buildChildRecord),\n };\n}\n\n/**\n * Render a \"child data\" template for one parent: derive its children from the already-loaded\n * posts ({@link deriveChildren}), build the aggregate record, and render the template with\n * the shared Print engine. Same-type, in-grid children only (cross-type/off-grid children\n * need a server query — deferred). Rendered in plain-text (non-escaping) mode, since the\n * result is shown as a spreadsheet cell's text. Throws {@link TemplateParseError} on an\n * unbalanced `{{#each}}`.\n */\nexport function renderChildData(template: string, parent: WpPost, posts: WpPost[]): string {\n const children = deriveChildren(posts, parent.id);\n return renderRecordTemplate(template, buildParentAggregate(parent, children), { escape: false });\n}\n","import { buildPrintRecord, type PrintRecord } from './print';\nimport {\n PARENT_META_KEY,\n PARENT_TYPE_META_KEY,\n buildClearRelationMeta,\n buildSetRelationMeta,\n type RelationTarget,\n} from './relation';\nimport type {\n DeleteMetaResult,\n ListMediaParams,\n ListPostsParams,\n ListTermsParams,\n MergeTermOptions,\n MergeTermResult,\n UpdatePostFields,\n WpCredentials,\n WpMedia,\n WpMediaSize,\n WpPost,\n WpPostEdit,\n WpPostResponse,\n WpPostType,\n WpTaxonomy,\n WpTerm,\n} from './types';\n\n/** Hosts for which plain http is tolerated (local development). */\nconst LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);\n\n/** Allowed characters for a REST route segment (post type slug). No dots: a `.`/`..`\n * segment would be resolved by the URL parser and traverse the REST path. */\nconst ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;\n\n/** JS magic property names that pass {@link ROUTE_SEGMENT} but are unsafe as plain-object keys\n * (prototype pollution / silent drop). A taxonomy REST base matching one is rejected. */\nconst RESERVED_KEYS = new Set(['__proto__', 'prototype', 'constructor']);\n\n/** Page cap for {@link WpClient.listAllTerms} (100 terms/page) — bounds a runaway on a huge\n * taxonomy while covering typical category trees. */\nconst MAX_TERM_PAGES = 10;\n\n/** Per-post-type page cap for a term merge ({@link WpClient.mergeTerm}, 100 posts/page) — bounds a\n * runaway on a term assigned to a very large number of posts. When a type exceeds it, the merge\n * is reported truncated and the source term is kept rather than deleted with stragglers behind. */\nconst MAX_MERGE_PAGES = 50;\n\n/** WordPress posts-collection query parameters. A taxonomy whose REST base collides with one of\n * these cannot be used as a term filter ({@link WpClient.listPostsByTerm}) without overwriting a\n * control parameter (e.g. `?page=<termId>` would page rather than filter, silently returning the\n * wrong posts). Such a base is rejected so a merge can never re-assign and delete based on a\n * mis-filtered result. Lower-cased; the taxonomy REST base is compared case-insensitively. */\nconst RESERVED_POST_QUERY_PARAMS = new Set([\n 'context',\n 'page',\n 'per_page',\n 'search',\n 'after',\n 'modified_after',\n 'before',\n 'modified_before',\n 'author',\n 'author_exclude',\n 'exclude',\n 'include',\n 'offset',\n 'order',\n 'orderby',\n 'slug',\n 'status',\n 'tax_relation',\n 'sticky',\n '_embed',\n '_fields',\n '_method',\n '_envelope',\n '_jsonp',\n '_locale',\n]);\n\n/** REST field added by the companion plugin to carry arbitrary post meta. */\nconst META_FIELD = 'dbp_wp_meta';\n\n/**\n * Meta key holding the lossless Markdown source for a post edited in Markdown mode. An\n * underscore-prefixed (protected) key, so the companion plugin must register it with\n * `register_post_meta` + an `edit_post` auth callback to expose it over REST — the generic\n * `dbp_wp_meta` field excludes protected keys. Like the relation keys, it therefore rides\n * the standard core `meta` field, not `dbp_wp_meta`.\n */\nexport const MARKDOWN_META_KEY = '_dbp_wp_markdown';\n\n/** REST namespace registered by the companion plugin. */\nconst CONNECTOR_NAMESPACE = 'dbp-wp/v1';\n\n/**\n * Validate and normalize a WordPress site URL into a REST base (origin + base path).\n *\n * Requires https, except plain http is allowed for local development hosts. Rejects\n * embedded credentials, query strings, and fragments, and strips trailing slashes — so\n * an Application Password is never sent over cleartext to an unexpected target.\n */\nexport function normalizeSiteUrl(siteUrl: string): string {\n let url: URL;\n try {\n url = new URL(siteUrl);\n } catch {\n throw new Error(`Invalid site URL: ${siteUrl}`);\n }\n\n const isLocal = LOCAL_HOSTS.has(url.hostname);\n if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLocal)) {\n throw new Error(`Site URL must use https (http is allowed only for local hosts): ${siteUrl}`);\n }\n // SSRF defense: the client sends the Application Password to whatever the site URL points to,\n // so block private, loopback, and link-local IP-literal targets (e.g. 10.x, 192.168.x,\n // 169.254.169.254 cloud metadata, IPv6 ULA/link-local). The explicit local-dev hosts stay\n // allowed. This covers IP literals only — a hostname that resolves to a private address (DNS\n // rebinding) is not caught here, as fetch does not expose the resolved address.\n if (!isLocal && isPrivateAddress(url.hostname)) {\n throw new Error(`Site URL must not point to a private or local network address: ${siteUrl}`);\n }\n if (url.username !== '' || url.password !== '') {\n throw new Error('Site URL must not contain embedded credentials.');\n }\n if (url.search !== '' || url.hash !== '') {\n throw new Error('Site URL must not contain a query string or fragment.');\n }\n\n return `${url.origin}${url.pathname}`.replace(/\\/+$/, '');\n}\n\n/**\n * True when a URL hostname is an IP literal in a private, loopback, link-local, or unspecified\n * range — the SSRF block list used by {@link normalizeSiteUrl}. Returns false for DNS hostnames\n * (not resolved here) and for public IPs. Exported for unit testing.\n */\nexport function isPrivateAddress(hostname: string): boolean {\n // IPv4 literal.\n const v4 = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(hostname);\n if (v4) {\n const octets = [Number(v4[1]), Number(v4[2]), Number(v4[3]), Number(v4[4])];\n if (octets.some((o) => o > 255)) {\n return false; // not a valid IPv4 literal\n }\n return isPrivateV4(octets);\n }\n // IPv6 literal. A URL's hostname keeps the brackets (e.g. `[fe80::1]`), so strip them.\n if (hostname.includes(':')) {\n return isPrivateV6(hostname.toLowerCase().replace(/^\\[/, '').replace(/\\]$/, ''));\n }\n return false; // DNS hostname or non-literal: not blocked here\n}\n\n/** True for private, loopback, link-local, or unspecified IPv4 octets. */\nfunction isPrivateV4(octets: readonly number[]): boolean {\n const [a, b] = octets;\n return (\n a === 0 || // \"this\" network\n a === 10 || // private\n a === 127 || // loopback\n (a === 169 && b === 254) || // link-local (incl. 169.254.169.254 metadata)\n (a === 172 && b !== undefined && b >= 16 && b <= 31) || // private\n (a === 192 && b === 168) // private\n );\n}\n\n/**\n * Expand an IPv6 literal to its 8 16-bit groups, or null if unparseable. Handles `::`\n * compression and an embedded dotted-quad tail (e.g. `::ffff:1.2.3.4`). The WHATWG URL parser\n * canonicalizes an IPv4-mapped address to hex (`[::ffff:7f00:1]`), so the range check must work\n * on the expanded groups rather than string-matching the dotted form.\n */\nfunction expandV6(input: string): number[] | null {\n let s = input;\n // Convert a trailing dotted-quad to two hex groups.\n const v4m = /^(.*:)(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(s);\n if (v4m) {\n const o = [Number(v4m[2]), Number(v4m[3]), Number(v4m[4]), Number(v4m[5])];\n if (o.some((x) => x > 255)) {\n return null;\n }\n s = `${v4m[1]}${(((o[0] as number) << 8) | (o[1] as number)).toString(16)}:${(((o[2] as number) << 8) | (o[3] as number)).toString(16)}`;\n }\n const halves = s.split('::');\n if (halves.length > 2) {\n return null;\n }\n const toGroups = (side: string): number[] | null => {\n if (side === '') {\n return [];\n }\n const out: number[] = [];\n for (const part of side.split(':')) {\n if (!/^[0-9a-f]{1,4}$/.test(part)) {\n return null;\n }\n out.push(parseInt(part, 16));\n }\n return out;\n };\n const left = toGroups(halves[0] ?? '');\n if (left === null) {\n return null;\n }\n if (halves.length === 2) {\n const right = toGroups(halves[1] ?? '');\n if (right === null) {\n return null;\n }\n const gap = 8 - left.length - right.length;\n if (gap < 1) {\n return null; // `::` must stand for at least one zero group\n }\n return [...left, ...new Array<number>(gap).fill(0), ...right];\n }\n return left.length === 8 ? left : null;\n}\n\n/** True for IPv6 loopback/unspecified, ULA (fc00::/7), link-local (fe80::/10), or a mapped private v4. */\nfunction isPrivateV6(addr: string): boolean {\n const groups = expandV6((addr.split('%')[0] ?? '').trim()); // drop any zone id\n if (groups === null) {\n return false;\n }\n if (groups.every((g) => g === 0)) {\n return true; // :: unspecified\n }\n if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) {\n return true; // ::1 loopback\n }\n const first = groups[0] ?? 0;\n if ((first & 0xfe00) === 0xfc00) {\n return true; // ULA fc00::/7\n }\n if ((first & 0xffc0) === 0xfe80) {\n return true; // link-local fe80::/10\n }\n // IPv4-mapped ::ffff:a.b.c.d\n if (groups.slice(0, 5).every((g) => g === 0) && groups[5] === 0xffff) {\n const g6 = groups[6] ?? 0;\n const g7 = groups[7] ?? 0;\n return isPrivateV4([(g6 >> 8) & 0xff, g6 & 0xff, (g7 >> 8) & 0xff, g7 & 0xff]);\n }\n return false;\n}\n\n/**\n * Build the HTTP Basic `Authorization` header value from Application Password\n * credentials. WordPress treats the Application Password as the Basic-auth password.\n */\nexport function buildAuthHeader(credentials: WpCredentials): string {\n if (credentials.username.includes(':')) {\n throw new Error('Username must not contain a colon (\":\") for HTTP Basic authentication.');\n }\n const token = `${credentials.username}:${credentials.applicationPassword}`;\n const base64 = Buffer.from(token, 'utf-8').toString('base64');\n return `Basic ${base64}`;\n}\n\n/** Error thrown when the WordPress REST API returns a non-2xx response. */\nexport class WpRequestError extends Error {\n constructor(\n readonly status: number,\n readonly path: string,\n message: string,\n ) {\n super(message);\n this.name = 'WpRequestError';\n }\n}\n\n/**\n * Minimal WordPress REST client.\n *\n * Runs in the Node process (CLI shell), never in the browser, so Application Password\n * credentials stay server-side. MVP scope: list posts, read/write post meta.\n */\nexport class WpClient {\n private readonly restBase: string;\n\n constructor(private readonly credentials: WpCredentials) {\n this.restBase = normalizeSiteUrl(credentials.siteUrl);\n }\n\n /**\n * Send an authenticated request and return the raw {@link Response} (throwing on non-2xx),\n * so callers that need response headers — e.g. media pagination via `X-WP-TotalPages` —\n * can read them. A JSON `Content-Type` is declared only when a body is sent; a caller may\n * override it via `init.headers` (the media upload sends the file's own type).\n */\n private async send(path: string, init: RequestInit = {}): Promise<Response> {\n const response = await fetch(`${this.restBase}/wp-json${path}`, {\n ...init,\n headers: {\n Authorization: buildAuthHeader(this.credentials),\n // Only declare a JSON body when one is actually sent (GETs carry none).\n ...(init.body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...init.headers,\n },\n });\n\n if (!response.ok) {\n throw new WpRequestError(\n response.status,\n path,\n `WordPress REST request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return response;\n }\n\n private async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n return (await this.send(path, init)).json() as Promise<T>;\n }\n\n /**\n * List the REST-enabled post types on the site (edit context), so the app can offer\n * a type selector. Returns each type's REST route base and display name.\n */\n async listPostTypes(): Promise<WpPostType[]> {\n const raw = await this.request<unknown>('/wp/v2/types?context=edit');\n return normalizePostTypes(raw);\n }\n\n /** List posts of a given type in edit context (raw fields, for editing). */\n async listPosts(params: ListPostsParams = {}): Promise<WpPost[]> {\n const type = params.type ?? 'posts';\n assertRouteSegment(type);\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n });\n const raw = await this.request<WpPostResponse[]>(`/wp/v2/${type}?${query.toString()}`);\n return raw.map(normalizePost);\n }\n\n /**\n * List posts as {@link PrintRecord}s for Print Design. Requests `_embed` (so featured\n * media and taxonomy terms come back inline) plus `content`/`excerpt`; the standard\n * table/spreadsheet listing ({@link WpClient.listPosts}) is unaffected.\n */\n async listPostsForPrint(params: ListPostsParams = {}): Promise<PrintRecord[]> {\n const type = params.type ?? 'posts';\n assertRouteSegment(type);\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n _embed: '1',\n });\n const raw = await this.request<WpPostResponse[]>(`/wp/v2/${type}?${query.toString()}`);\n return raw.map(buildPrintRecord);\n }\n\n /**\n * List posts of a type that are assigned a given taxonomy term (`GET /wp/v2/<type>?<taxRestBase>=\n * <termId>`), in edit context and across all statuses (`status=any`, so drafts/pending are not\n * silently skipped). Reads `X-WP-TotalPages` for paging. Used by {@link WpClient.mergeTerm} to\n * find every post that must be re-assigned. A core REST call — no companion plugin needed.\n */\n async listPostsByTerm(\n type: string,\n taxRestBase: string,\n termId: number,\n params: { page?: number; perPage?: number } = {},\n ): Promise<{ items: WpPost[]; totalPages: number; pagesReliable: boolean }> {\n assertRouteSegment(type);\n assertRouteSegment(taxRestBase);\n assertTermId(termId);\n // The taxonomy's REST base becomes the posts-collection filter param (e.g. `categories=5`).\n // Reject one that collides with a control parameter: it would overwrite the control rather than\n // filter, returning the wrong posts — dangerous when the caller then re-assigns and deletes.\n if (RESERVED_POST_QUERY_PARAMS.has(taxRestBase.toLowerCase())) {\n throw new Error(`Taxonomy REST base collides with a reserved query parameter: ${taxRestBase}`);\n }\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n status: 'any',\n });\n query.set(taxRestBase, String(termId));\n const response = await this.send(`/wp/v2/${type}?${query.toString()}`);\n const raw = (await response.json()) as unknown;\n // `pagesReliable` is false when WordPress did not send a usable `X-WP-TotalPages`. A merge must\n // not trust a defaulted page count of 1 to mean \"fully enumerated\" before deleting the source.\n const header = response.headers.get('X-WP-TotalPages');\n const pagesReliable = header !== null && /^\\d+$/.test(header.trim());\n return {\n items: Array.isArray(raw) ? raw.map(normalizePost) : [],\n totalPages: parseTotalPages(header),\n pagesReliable,\n };\n }\n\n /**\n * Update post fields in a single request. Standard fields (title, menu_order,\n * status) are core REST fields and need no plugin. When `meta` is supplied it rides\n * the same request through the companion plugin's `dbp_wp_meta` field (ignored by\n * WordPress without the connector). Pass the REST route slug as `type` (e.g.\n * `posts`, `pages`) — not the object type returned on a post.\n */\n async updatePost(\n id: number,\n fields: UpdatePostFields,\n type = 'posts',\n meta?: Record<string, unknown>,\n ): Promise<WpPost> {\n assertPostId(id);\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}/${String(id)}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(buildPostBody(fields, meta)),\n });\n return normalizePost(raw);\n }\n\n /**\n * Create a new post in a single request, symmetric to {@link WpClient.updatePost}.\n * Standard fields (title, menu_order, status) are core REST fields; when `meta` is\n * supplied it rides the same request through the companion plugin's `dbp_wp_meta`\n * field. Pass the REST route slug as `type` (e.g. `posts`, `pages`).\n */\n async createPost(\n fields: UpdatePostFields,\n type = 'posts',\n meta?: Record<string, unknown>,\n ): Promise<WpPost> {\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(buildPostBody(fields, meta)),\n });\n return normalizePost(raw);\n }\n\n /**\n * Update only arbitrary post meta through the companion plugin's `dbp_wp_meta`\n * field. A thin wrapper over {@link WpClient.updatePost} with no standard fields.\n * Requires the connector; the connector writes scalar values only.\n */\n async updatePostMeta(\n id: number,\n meta: Record<string, unknown>,\n type = 'posts',\n ): Promise<WpPost> {\n return this.updatePost(id, {}, type, meta);\n }\n\n /**\n * Fetch a single post for body editing (edit context), returning the raw body\n * (`content.raw`) and, when present, the lossless Markdown source from\n * `_dbp_wp_markdown`. The standard listing ({@link WpClient.listPosts}) omits the body, so\n * the editor uses this dedicated read. The Markdown source comes back via the standard\n * `meta` field only when the connector registered the key; in restricted mode the post is\n * HTML-only. Pass the REST route slug as `type`.\n */\n async getPostForEdit(id: number, type = 'posts'): Promise<WpPostEdit> {\n assertPostId(id);\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}/${String(id)}?context=edit`);\n return normalizePostForEdit(raw);\n }\n\n /**\n * Save a single post's body. Writes `content` (core REST) and, when `markdown` is given,\n * the `_dbp_wp_markdown` source via the standard `meta` field (registered by the connector)\n * — both in one request. Pass `markdown` as the source string for Markdown mode, `null` to\n * clear it (HTML mode on a post previously saved as Markdown), or omit it for an HTML-only\n * post. Writing/clearing `markdown` requires the connector; a content-only save does not.\n * Returns the re-fetched edit model (edit context), so the caller sees the persisted mode.\n */\n async updatePostBody(\n id: number,\n type: string,\n body: { content: string; markdown?: string | null },\n ): Promise<WpPostEdit> {\n assertPostId(id);\n assertRouteSegment(type);\n const reqBody = buildUpdateBody({ content: body.content });\n if (body.markdown !== undefined) {\n // Standard `meta` field (like the relation keys), not the connector's `dbp_wp_meta`:\n // `_dbp_wp_markdown` is protected and is only writable through register_post_meta.\n // `null` makes WordPress delete the key (no stale source left to mis-detect the mode).\n reqBody.meta = { [MARKDOWN_META_KEY]: body.markdown };\n }\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}/${String(id)}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(reqBody),\n });\n return normalizePostForEdit(raw);\n }\n\n /**\n * Delete named meta keys from a single post via the companion plugin's\n * `DELETE /dbp-wp/v1/posts/<id>/meta` route. This route is keyed by id only (the\n * post type is irrelevant). Requires the connector.\n */\n async deletePostMeta(id: number, keys: string[]): Promise<DeleteMetaResult> {\n assertPostId(id);\n const cleanKeys = sanitizeMetaKeys(keys);\n // Use send() (not request()) so an empty/204 response from the connector does not throw in\n // JSON.parse; parseDeleteMetaResponse tolerates it.\n const response = await this.send(`/${CONNECTOR_NAMESPACE}/posts/${String(id)}/meta`, {\n method: 'DELETE',\n body: JSON.stringify({ keys: cleanKeys }),\n });\n return parseDeleteMetaResponse(await response.text(), id, cleanKeys);\n }\n\n /**\n * Set a child post's parent relation. The relation keys ride the standard core `meta`\n * field (the connector registers them with `register_post_meta`), so this is a distinct\n * path from {@link WpClient.updatePostMeta} (which uses the connector's `dbp_wp_meta`\n * field). Validates the assignment (positive id, valid type, no self-parent) before\n * writing. Requires the connector; without it WordPress silently ignores the keys.\n */\n async setRelation(\n childId: number,\n childType: string,\n relation: RelationTarget,\n ): Promise<WpPost> {\n assertPostId(childId);\n assertRouteSegment(childType);\n const raw = await this.request<WpPostResponse>(\n `/wp/v2/${childType}/${String(childId)}?context=edit`,\n { method: 'POST', body: JSON.stringify({ meta: buildSetRelationMeta(childId, relation) }) },\n );\n return normalizePost(raw);\n }\n\n /**\n * Clear a child post's parent relation. Sends `null` for both relation keys, which makes\n * WordPress delete them (no stale `0`/empty value left behind). Requires the connector.\n */\n async clearRelation(childId: number, childType: string): Promise<WpPost> {\n assertPostId(childId);\n assertRouteSegment(childType);\n const raw = await this.request<WpPostResponse>(\n `/wp/v2/${childType}/${String(childId)}?context=edit`,\n { method: 'POST', body: JSON.stringify({ meta: buildClearRelationMeta() }) },\n );\n return normalizePost(raw);\n }\n\n /**\n * Detect whether the companion plugin is active by checking the REST index\n * (`/wp-json/`) for the connector's namespace. Throws on a failed request; a caller\n * that wants a non-fatal probe should treat a thrown error as \"not available\".\n */\n async detectConnector(): Promise<boolean> {\n const index = await this.request<{ namespaces?: unknown }>('/');\n return hasConnectorNamespace(index.namespaces);\n }\n\n /**\n * Upload an image to the media library via core REST (`POST /wp/v2/media`), the same\n * contract WordPress uses: raw bytes with a `Content-Disposition` filename and the file's\n * MIME type (octet-stream when unknown). No companion plugin needed; the authenticated\n * user must have `upload_files`. Returns the normalized media item.\n */\n async uploadMedia(bytes: Uint8Array, filename: string, mimeType?: string): Promise<WpMedia> {\n const response = await this.send('/wp/v2/media', {\n method: 'POST',\n body: bytes,\n headers: {\n 'Content-Type': mimeType && mimeType.length > 0 ? mimeType : 'application/octet-stream',\n 'Content-Disposition': buildContentDisposition(filename),\n },\n });\n return normalizeMedia(await response.json());\n }\n\n /**\n * List image attachments (`GET /wp/v2/media?media_type=image`), paginated. Reads the\n * `X-WP-TotalPages` response header so the picker can page through the library. A core\n * REST call — no companion plugin needed.\n */\n async listMedia(params: ListMediaParams = {}): Promise<{ items: WpMedia[]; totalPages: number }> {\n const perPage = clampInt(params.perPage ?? 30, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n media_type: 'image',\n per_page: String(perPage),\n page: String(page),\n });\n const search = params.search?.trim();\n if (search) {\n query.set('search', search);\n }\n const response = await this.send(`/wp/v2/media?${query.toString()}`);\n const raw = (await response.json()) as unknown;\n return {\n items: Array.isArray(raw) ? raw.map(normalizeMedia) : [],\n totalPages: parseTotalPages(response.headers.get('X-WP-TotalPages')),\n };\n }\n\n /**\n * Resolve specific media ids to their URLs in one request (`?include=`), used to fill in\n * the featured-image thumbnails for the posts currently shown — without embedding media\n * into the lean post listing. A core REST call — no companion plugin needed.\n */\n async resolveMedia(ids: number[]): Promise<WpMedia[]> {\n const clean = [...new Set(ids.filter((id) => Number.isSafeInteger(id) && id > 0))];\n if (clean.length === 0) {\n return [];\n }\n // WordPress caps per_page at 100, so resolve in chunks of 100 — otherwise a request for\n // more than 100 ids would be silently truncated, leaving the rest unresolved.\n const out: WpMedia[] = [];\n for (let i = 0; i < clean.length; i += 100) {\n const chunk = clean.slice(i, i + 100);\n const query = new URLSearchParams({\n include: chunk.join(','),\n per_page: String(chunk.length),\n });\n const raw = await this.request<unknown>(`/wp/v2/media?${query.toString()}`);\n if (Array.isArray(raw)) {\n out.push(...raw.map(normalizeMedia));\n }\n }\n return out;\n }\n\n /**\n * List the REST-enabled taxonomies, optionally filtered to those that apply to a post type\n * (e.g. categories and tags for `posts`). A core REST call — no companion plugin needed.\n */\n async listTaxonomies(type?: string): Promise<WpTaxonomy[]> {\n const query = new URLSearchParams({ context: 'edit' });\n if (type !== undefined) {\n assertRouteSegment(type);\n query.set('type', type);\n }\n const raw = await this.request<unknown>(`/wp/v2/taxonomies?${query.toString()}`);\n return normalizeTaxonomies(raw);\n }\n\n /**\n * List terms of a taxonomy (`GET /wp/v2/<taxRestBase>`), paginated/searchable, reading the\n * `X-WP-TotalPages` header so the picker can page through. A core REST call — no plugin needed.\n */\n async listTerms(\n taxRestBase: string,\n params: ListTermsParams = {},\n ): Promise<{ items: WpTerm[]; totalPages: number }> {\n assertRouteSegment(taxRestBase);\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n });\n const search = params.search?.trim();\n if (search) {\n query.set('search', search);\n }\n const response = await this.send(`/wp/v2/${taxRestBase}?${query.toString()}`);\n const raw = (await response.json()) as unknown;\n return {\n items: Array.isArray(raw) ? raw.map(normalizeTerm) : [],\n totalPages: parseTotalPages(response.headers.get('X-WP-TotalPages')),\n };\n }\n\n /**\n * Fetch every term of a taxonomy by paging through the list, so the picker can build a complete\n * hierarchy tree (a child's parent may be on another page). Capped at {@link MAX_TERM_PAGES}\n * pages to avoid a runaway on a huge taxonomy. A core REST call — no companion plugin needed.\n */\n async listAllTerms(\n taxRestBase: string,\n params: { search?: string } = {},\n ): Promise<{ items: WpTerm[]; truncated: boolean }> {\n assertRouteSegment(taxRestBase);\n const out: WpTerm[] = [];\n let page = 1;\n let totalPages = 1;\n do {\n const result = await this.listTerms(taxRestBase, {\n page,\n perPage: 100,\n ...(params.search ? { search: params.search } : {}),\n });\n out.push(...result.items);\n totalPages = result.totalPages;\n page += 1;\n } while (page <= totalPages && page <= MAX_TERM_PAGES);\n // Signal when the cap stopped us short of every page, so the UI can warn rather than show a\n // silently incomplete tree.\n return { items: out, truncated: totalPages > MAX_TERM_PAGES };\n }\n\n /**\n * Create a new term in a taxonomy (`POST /wp/v2/<taxRestBase>`), optionally under a parent\n * (hierarchical taxonomies only; `parent` is ignored when `0`/absent). WordPress enforces the\n * caller's term-management capability and returns the created term. A core REST call.\n */\n async createTerm(taxRestBase: string, input: { name: string; parent?: number }): Promise<WpTerm> {\n assertRouteSegment(taxRestBase);\n const body: Record<string, unknown> = { name: input.name };\n if (typeof input.parent === 'number' && input.parent > 0) {\n body.parent = input.parent;\n }\n const raw = await this.request<unknown>(`/wp/v2/${taxRestBase}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(body),\n });\n return normalizeTerm(raw);\n }\n\n /**\n * Update a term (`POST /wp/v2/<taxRestBase>/<id>`): rename (`name`), reparent (`parent`, `0`\n * moves it to top level), or set `slug`/`description`. Only provided fields are sent. WordPress\n * enforces the caller's capability (a 403 surfaces) and rejects an invalid parent (e.g. a cycle).\n * A core REST call — no companion plugin.\n */\n async updateTerm(\n taxRestBase: string,\n id: number,\n input: { name?: string; parent?: number; slug?: string; description?: string },\n ): Promise<WpTerm> {\n assertRouteSegment(taxRestBase);\n assertTermId(id);\n const body: Record<string, unknown> = {};\n if (input.name !== undefined) body.name = input.name;\n if (input.parent !== undefined) body.parent = input.parent;\n if (input.slug !== undefined) body.slug = input.slug;\n if (input.description !== undefined) body.description = input.description;\n const raw = await this.request<unknown>(`/wp/v2/${taxRestBase}/${id}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(body),\n });\n return normalizeTerm(raw);\n }\n\n /**\n * Delete a term (`DELETE /wp/v2/<taxRestBase>/<id>?force=true`). Terms have no trash, so WordPress\n * requires `force=true`; its children are reparented to the deleted term's parent (WordPress\n * behavior). WordPress enforces the caller's capability (a 403 surfaces). A core REST call.\n */\n async deleteTerm(taxRestBase: string, id: number): Promise<void> {\n assertRouteSegment(taxRestBase);\n assertTermId(id);\n await this.send(`/wp/v2/${taxRestBase}/${id}?force=true&context=edit`, { method: 'DELETE' });\n }\n\n /**\n * Merge the source term (`fromId`) into the target term (`toId`) within one taxonomy: re-assign\n * every post that carries the source term to the target — across *every* post type the taxonomy\n * is attached to — then delete the source. WordPress has no native term-merge endpoint, so this\n * is a non-atomic compose of core REST calls.\n *\n * Why cross-type: `deleteTerm` force-deletes (terms have no trash), severing *all* of the source\n * term's relationships. If only one post type were re-assigned, posts of any other type would\n * lose the term without gaining the target. So this scans all of the taxonomy's post types.\n *\n * The source is deleted only on a fully clean merge: every reachable type fully paged (no\n * {@link MAX_MERGE_PAGES} cap hit, no type unaddressable over REST), every re-assignment\n * succeeded, and the caller did not cancel. Otherwise it is kept (`deleted: false`) so the caller\n * can surface the partial result and re-run — re-running re-queries the source's remaining posts\n * (idempotent-ish). The caller must reject merging a term that has children (WordPress would\n * reparent them on delete); this method does not enforce that.\n *\n * Because the re-assignment is one REST write per post (minutes for a heavily-used term),\n * `options.onProgress` reports live counts and `options.signal` cancels cooperatively between\n * writes (the in-flight write completes, then the merge stops and keeps the source). A core REST\n * call — no companion plugin needed.\n */\n async mergeTerm(\n taxRestBase: string,\n fromId: number,\n toId: number,\n options: MergeTermOptions = {},\n ): Promise<MergeTermResult> {\n assertRouteSegment(taxRestBase);\n assertTermId(fromId);\n assertTermId(toId);\n if (fromId === toId) {\n throw new Error('Cannot merge a term into itself.');\n }\n const { onProgress, signal } = options;\n\n // Resolve the REST bases of every post type this taxonomy applies to. A taxonomy type with no\n // REST-addressable post type cannot be re-assigned, so the merge cannot be guaranteed complete.\n const taxonomies = await this.listTaxonomies();\n const tax = taxonomies.find((t) => t.restBase === taxRestBase);\n if (!tax) {\n throw new Error(`Unknown taxonomy: ${taxRestBase}`);\n }\n const postTypes = await this.listPostTypes();\n const restBases: string[] = [];\n let truncated = false;\n for (const slug of tax.types) {\n const pt = postTypes.find((p) => p.slug === slug);\n if (pt) {\n restBases.push(pt.restBase);\n } else {\n // Attached to a type we cannot reach over REST: cannot fully reassign, so keep the source.\n truncated = true;\n }\n }\n // No reachable post type at all (e.g. the taxonomies response reported no `types`): we cannot\n // know what carries the source term, so refuse to delete it — deleting now would sever any\n // hidden assignments with nothing re-assigned, the exact data loss this method exists to avoid.\n if (restBases.length === 0) {\n truncated = true;\n }\n\n // Phase A — snapshot every post carrying the source term, reading (not mutating) so forward\n // paging is stable. (Re-assigning shrinks the filtered set, which would shift pages mid-scan.)\n const targets: { type: string; post: WpPost }[] = [];\n let canceled = false;\n for (const type of restBases) {\n if (canceled) break;\n let page = 1;\n for (;;) {\n if (signal?.aborted) {\n // Cancel during discovery: stop scanning, run no re-assignment, keep the source.\n canceled = true;\n break;\n }\n if (page > MAX_MERGE_PAGES) {\n // Too many pages to enumerate safely: keep the source rather than delete with stragglers.\n truncated = true;\n break;\n }\n const { items, totalPages, pagesReliable } = await this.listPostsByTerm(\n type,\n taxRestBase,\n fromId,\n { page, perPage: 100 },\n );\n for (const post of items) {\n targets.push({ type, post });\n }\n // With a trustworthy page count, stop at the last page; without one, trust only a short\n // (non-full) page as the end — never a defaulted \"1 page\" that could hide later pages.\n const finished = pagesReliable ? page >= totalPages : items.length < 100;\n if (finished) {\n break;\n }\n page += 1;\n }\n }\n\n // Phase B — re-assign each snapshotted post from the source term to the target. Report the\n // total up front (count known) so a caller can render a determinate progress bar.\n let reassigned = 0;\n const failed: { id: number; error: string }[] = [];\n // Skip re-assignment entirely if discovery was canceled (no misleading progress on a partial\n // discovery count — `total` is only meaningful once discovery completed).\n if (!canceled) {\n onProgress?.({ reassigned, failed: failed.length, total: targets.length });\n for (const { type, post } of targets) {\n // Cancel cooperatively between writes: stop and keep the source (it can be re-run later).\n if (signal?.aborted) {\n canceled = true;\n break;\n }\n const current = post.terms[taxRestBase] ?? [];\n const next = computeMergedTermIds(current, fromId, toId);\n try {\n await this.updatePost(post.id, { terms: { [taxRestBase]: next } }, type);\n reassigned += 1;\n } catch (e) {\n failed.push({ id: post.id, error: e instanceof Error ? e.message : String(e) });\n }\n onProgress?.({ reassigned, failed: failed.length, total: targets.length });\n }\n }\n\n // Re-check after the loop: a cancel that arrived during or after the final re-assignment must\n // still keep the source (the per-iteration check above can't catch a late abort).\n if (signal?.aborted) {\n canceled = true;\n }\n\n // Delete the source only when the merge is provably complete and clean (and not canceled).\n let deleted = false;\n if (failed.length === 0 && !truncated && !canceled) {\n await this.deleteTerm(taxRestBase, fromId);\n deleted = true;\n }\n return { reassigned, failed, deleted, truncated, canceled };\n }\n\n /**\n * Resolve specific term ids to their names in one request per 100 ids (`?include=`), used to\n * label the taxonomy columns for the posts currently shown. A core REST call — no plugin needed.\n */\n async resolveTerms(taxRestBase: string, ids: number[]): Promise<WpTerm[]> {\n assertRouteSegment(taxRestBase);\n const clean = [...new Set(ids.filter((id) => Number.isSafeInteger(id) && id > 0))];\n if (clean.length === 0) {\n return [];\n }\n const out: WpTerm[] = [];\n for (let i = 0; i < clean.length; i += 100) {\n const chunk = clean.slice(i, i + 100);\n const query = new URLSearchParams({\n context: 'edit',\n include: chunk.join(','),\n per_page: String(chunk.length),\n });\n const raw = await this.request<unknown>(`/wp/v2/${taxRestBase}?${query.toString()}`);\n if (Array.isArray(raw)) {\n out.push(...raw.map(normalizeTerm));\n }\n }\n return out;\n }\n}\n\n/**\n * Compute a post's taxonomy term IDs after merging `fromId` into `toId`: drop the source, add the\n * target, and de-duplicate (the post may already carry the target). Order-preserving for the\n * surviving ids. Pure — the merge decision for one post, unit-testable without the network.\n */\nexport function computeMergedTermIds(currentIds: number[], fromId: number, toId: number): number[] {\n return [...new Set(currentIds.filter((id) => id !== fromId).concat(toId))];\n}\n\n/** Map editable fields to the WordPress REST request body (camelCase → snake_case). */\nexport function buildUpdateBody(fields: UpdatePostFields): Record<string, unknown> {\n const body: Record<string, unknown> = {};\n if (fields.title !== undefined) {\n body.title = fields.title;\n }\n if (fields.menuOrder !== undefined) {\n body.menu_order = fields.menuOrder;\n }\n if (fields.status !== undefined) {\n body.status = fields.status;\n }\n if (fields.featuredMedia !== undefined) {\n // `0` is meaningful here (removes the featured image), so check for undefined, not falsy.\n body.featured_media = fields.featuredMedia;\n }\n if (fields.content !== undefined) {\n // `''` is meaningful here (clears the body), so check for undefined, not falsy.\n body.content = fields.content;\n }\n if (fields.terms !== undefined) {\n // Each taxonomy is sent as its own REST field keyed by REST base (e.g. `categories`); an\n // empty array clears that taxonomy's terms. Guard the key so an odd REST base can't inject\n // an unrelated body field, and skip JS magic names (`__proto__` etc.) that would pollute or\n // silently drop on this plain object.\n for (const [restBase, ids] of Object.entries(fields.terms)) {\n if (ROUTE_SEGMENT.test(restBase) && !RESERVED_KEYS.has(restBase)) {\n body[restBase] = ids;\n }\n }\n }\n return body;\n}\n\nfunction assertRouteSegment(segment: string): void {\n if (!ROUTE_SEGMENT.test(segment)) {\n throw new Error(`Invalid REST route segment: ${segment}`);\n }\n}\n\nfunction assertPostId(id: number): void {\n if (!Number.isSafeInteger(id) || id <= 0) {\n throw new Error(`Invalid post id: ${id}`);\n }\n}\n\nfunction assertTermId(id: number): void {\n if (!Number.isSafeInteger(id) || id <= 0) {\n throw new Error(`Invalid term id: ${id}`);\n }\n}\n\nfunction clampInt(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) {\n return min;\n }\n return Math.min(max, Math.max(min, Math.trunc(value)));\n}\n\n/** Wrap arbitrary meta in the companion plugin's REST field for a write request. */\nexport function buildMetaBody(meta: Record<string, unknown>): Record<string, unknown> {\n return { [META_FIELD]: meta };\n}\n\n/**\n * Build a post-update body, folding in connector meta under `dbp_wp_meta` when given.\n * A provided `meta` is included as-is, even when empty — callers that should skip empty\n * meta (e.g. the CLI batch parser) omit it before calling.\n */\nexport function buildPostBody(\n fields: UpdatePostFields,\n meta?: Record<string, unknown>,\n): Record<string, unknown> {\n const body = buildUpdateBody(fields);\n if (meta !== undefined) {\n Object.assign(body, buildMetaBody(meta));\n }\n return body;\n}\n\n/**\n * Parse a per-post meta-delete response. `send()` already threw on a non-2xx status, so an empty\n * body (e.g. a 204 from a backend that returns no content) or a non-JSON body means the delete\n * succeeded — the requested keys are reported as deleted. A JSON body's `deleted` list (the keys\n * actually present) takes precedence, and the request `id` is trusted over a malformed `post_id`.\n */\nexport function parseDeleteMetaResponse(\n body: string,\n id: number,\n requestedKeys: string[],\n): DeleteMetaResult {\n const text = body.trim();\n if (text === '') {\n return { postId: id, deleted: [...requestedKeys] };\n }\n let raw: { post_id?: unknown; deleted?: unknown };\n try {\n raw = JSON.parse(text) as { post_id?: unknown; deleted?: unknown };\n } catch {\n return { postId: id, deleted: [...requestedKeys] };\n }\n return {\n // Trust the request `id` over a malformed `post_id` (0, negative, or fractional included).\n postId:\n typeof raw.post_id === 'number' && Number.isSafeInteger(raw.post_id) && raw.post_id > 0\n ? raw.post_id\n : id,\n deleted: Array.isArray(raw.deleted)\n ? raw.deleted.filter((k): k is string => typeof k === 'string')\n : [],\n };\n}\n\n/** Validate and clean a list of meta keys for deletion (non-empty strings only). */\nexport function sanitizeMetaKeys(keys: unknown): string[] {\n if (!Array.isArray(keys)) {\n throw new Error('Meta keys must be an array of strings.');\n }\n const clean = keys.filter((key): key is string => typeof key === 'string' && key.length > 0);\n if (clean.length === 0) {\n throw new Error('At least one non-empty meta key is required.');\n }\n return clean;\n}\n\n/** True when a REST index `namespaces` list includes the connector namespace. */\nexport function hasConnectorNamespace(namespaces: unknown): boolean {\n return Array.isArray(namespaces) && namespaces.includes(CONNECTOR_NAMESPACE);\n}\n\n/**\n * Build a `Content-Disposition` value for a media upload. Emits an ASCII-only\n * `filename=\"…\"` fallback (non-ASCII and header-unsafe characters replaced with `_`) plus an\n * RFC 5987 `filename*=UTF-8''…` parameter, so a non-ASCII filename survives and never\n * produces an invalid (non-ByteString) header value that `fetch` would reject. The path is\n * reduced to its basename and quotes/CR/LF are dropped so the value cannot break the header.\n */\nexport function buildContentDisposition(filename: string): string {\n const base = filename.split(/[/\\\\]/).pop() ?? filename;\n const trimmed = base.replace(/[\\r\\n\"]/g, '').trim() || 'upload';\n const ascii = trimmed.replace(/[^\\x20-\\x7e]/g, '_');\n // Percent-encode per RFC 5987, also encoding the chars encodeURIComponent leaves bare.\n const encoded = encodeURIComponent(trimmed).replace(\n /['()*!]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n return `attachment; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\n}\n\n/** Parse the `X-WP-TotalPages` header into a positive page count, defaulting to 1. */\nfunction parseTotalPages(header: string | null): number {\n if (header === null) {\n return 1;\n }\n const n = Number.parseInt(header, 10);\n return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\n/** Read `<obj>.rendered` as a string, or `''` when absent/malformed. */\nfunction extractRendered(value: unknown): string {\n if (value !== null && typeof value === 'object') {\n const rendered = (value as Record<string, unknown>).rendered;\n if (typeof rendered === 'string') {\n return rendered;\n }\n }\n return '';\n}\n\n/** Prefer a thumbnail-sized URL from `media_details.sizes`, else a medium one, else `''`. */\nfunction extractThumbnailUrl(mediaDetails: unknown): string {\n if (mediaDetails === null || typeof mediaDetails !== 'object') {\n return '';\n }\n const sizes = (mediaDetails as Record<string, unknown>).sizes;\n if (sizes === null || typeof sizes !== 'object') {\n return '';\n }\n for (const sizeName of ['thumbnail', 'medium']) {\n const size = (sizes as Record<string, unknown>)[sizeName];\n if (size !== null && typeof size === 'object') {\n const url = (size as Record<string, unknown>).source_url;\n if (typeof url === 'string') {\n return url;\n }\n }\n }\n return '';\n}\n\n/**\n * Build the size list for the body editor's image picker from `media_details`: every generated\n * size (smallest first by width) plus a synthesized `full` entry pointing at the source URL when\n * the response did not already include one. Returns `[]` when nothing is resolvable (e.g. a\n * non-image attachment).\n */\nfunction extractSizes(mediaDetails: unknown, sourceUrl: string, mimeType: string): WpMediaSize[] {\n // Sizes are only meaningful for images; a non-image attachment has nothing to insert as <img>.\n if (!mimeType.startsWith('image/')) {\n return [];\n }\n const details =\n typeof mediaDetails === 'object' && mediaDetails !== null\n ? (mediaDetails as Record<string, unknown>)\n : {};\n const out: WpMediaSize[] = [];\n const sizes = details.sizes;\n if (sizes !== null && typeof sizes === 'object') {\n for (const [name, raw] of Object.entries(sizes as Record<string, unknown>)) {\n if (raw === null || typeof raw !== 'object') {\n continue;\n }\n const s = raw as Record<string, unknown>;\n const url = typeof s.source_url === 'string' ? s.source_url : '';\n if (url === '') {\n continue;\n }\n out.push({\n name,\n url,\n width: typeof s.width === 'number' ? s.width : 0,\n height: typeof s.height === 'number' ? s.height : 0,\n });\n }\n }\n out.sort((a, b) => a.width - b.width);\n // Some responses omit `full` from `sizes`; ensure it is always offered (and not duplicated).\n if (sourceUrl !== '' && !out.some((s) => s.name === 'full' || s.url === sourceUrl)) {\n out.push({\n name: 'full',\n url: sourceUrl,\n width: typeof details.width === 'number' ? details.width : 0,\n height: typeof details.height === 'number' ? details.height : 0,\n });\n }\n return out;\n}\n\n/**\n * Normalize a raw `/wp/v2/media` item into {@link WpMedia}. Accepts `unknown` and guards each\n * field, so a malformed entry degrades to empty strings rather than throwing.\n */\nexport function normalizeMedia(raw: unknown): WpMedia {\n const obj = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>;\n const sourceUrl = typeof obj.source_url === 'string' ? obj.source_url : '';\n const mimeType = typeof obj.mime_type === 'string' ? obj.mime_type : '';\n return {\n id: typeof obj.id === 'number' ? obj.id : 0,\n sourceUrl,\n thumbnailUrl: extractThumbnailUrl(obj.media_details) || sourceUrl,\n title: extractRendered(obj.title),\n mimeType,\n sizes: extractSizes(obj.media_details, sourceUrl, mimeType),\n };\n}\n\n/**\n * Normalize the `/wp/v2/types` response (an object keyed by type name) into a list.\n * Skips entries without a string `rest_base` (not addressable over REST).\n */\nexport function normalizePostTypes(raw: unknown): WpPostType[] {\n if (typeof raw !== 'object' || raw === null) {\n return [];\n }\n const result: WpPostType[] = [];\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null) {\n continue;\n }\n const entry = value as Record<string, unknown>;\n // Validate rest_base at ingestion so a malformed slug can't become a broken option.\n if (typeof entry.rest_base !== 'string' || !ROUTE_SEGMENT.test(entry.rest_base)) {\n continue;\n }\n result.push({\n slug: typeof entry.slug === 'string' ? entry.slug : key,\n restBase: entry.rest_base,\n name: typeof entry.name === 'string' ? entry.name : key,\n });\n }\n return result;\n}\n\n/**\n * Normalize the `/wp/v2/taxonomies` response (an object keyed by taxonomy slug) into a list.\n * Skips entries without a valid `rest_base` (not addressable over REST), mirroring\n * {@link normalizePostTypes}.\n */\nexport function normalizeTaxonomies(raw: unknown): WpTaxonomy[] {\n if (typeof raw !== 'object' || raw === null) {\n return [];\n }\n const result: WpTaxonomy[] = [];\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null) {\n continue;\n }\n const entry = value as Record<string, unknown>;\n // Reject a magic-name REST base (e.g. `__proto__`): it passes the route allowlist but is\n // unsafe as a key in the plain-object term maps the UI and write path build.\n if (\n typeof entry.rest_base !== 'string' ||\n !ROUTE_SEGMENT.test(entry.rest_base) ||\n RESERVED_KEYS.has(entry.rest_base)\n ) {\n continue;\n }\n result.push({\n slug: typeof entry.slug === 'string' ? entry.slug : key,\n restBase: entry.rest_base,\n name: typeof entry.name === 'string' ? entry.name : key,\n hierarchical: entry.hierarchical === true,\n // The post types this taxonomy applies to; a term merge re-assigns across all of them.\n types: Array.isArray(entry.types) ? entry.types.filter((t): t is string => typeof t === 'string') : [],\n });\n }\n return result;\n}\n\n/**\n * Normalize a raw `/wp/v2/<taxonomy>` term. Accepts `unknown` and guards each field, so a\n * malformed entry degrades to a zero id / empty name rather than throwing.\n */\nexport function normalizeTerm(raw: unknown): WpTerm {\n const obj = (typeof raw === 'object' && raw !== null ? raw : {}) as Record<string, unknown>;\n return {\n id: typeof obj.id === 'number' ? obj.id : 0,\n name: typeof obj.name === 'string' ? obj.name : '',\n parent: typeof obj.parent === 'number' ? obj.parent : 0,\n count: typeof obj.count === 'number' ? obj.count : 0,\n };\n}\n\n/**\n * Extract taxonomy term-ID assignments from a raw post, keyed by the taxonomy's REST base.\n *\n * On a WordPress post the only top-level fields that are arrays of positive integers are\n * taxonomy assignments (e.g. `categories`, `tags`, custom taxonomies), so capture exactly those.\n * The UI reads only the taxonomies it knows about (from `/wp/v2/taxonomies`), so a stray numeric\n * array would simply be ignored downstream. Empty arrays are omitted (no terms assigned). Uses a\n * null-prototype map so a pathological key (`__proto__`) is an ordinary own property.\n */\nfunction extractPostTerms(raw: WpPostResponse): Record<string, number[]> {\n const result: Record<string, number[]> = Object.create(null) as Record<string, number[]>;\n for (const [key, value] of Object.entries(raw as unknown as Record<string, unknown>)) {\n if (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((v) => typeof v === 'number' && Number.isInteger(v) && v > 0)\n ) {\n result[key] = value as number[];\n }\n }\n return result;\n}\n\nexport function normalizePost(raw: WpPostResponse): WpPost {\n const post: WpPost = {\n id: raw.id,\n type: raw.type,\n status: raw.status,\n title: raw.title.raw ?? raw.title.rendered,\n menuOrder: raw.menu_order,\n meta: raw.meta,\n terms: extractPostTerms(raw),\n };\n if (raw.dbp_wp_meta !== undefined) {\n post.dbpWpMeta = raw.dbp_wp_meta;\n }\n // featured_media is always present on a core post; 0 means \"no featured image\".\n if (typeof raw.featured_media === 'number' && raw.featured_media > 0) {\n post.featuredMedia = raw.featured_media;\n }\n // Parent relation rides the standard `meta` field (registered by the connector). It needs\n // both a positive id and a non-empty type; a half-written pair — e.g. from a raw REST\n // write that set only one key, or a parent type sanitized to '' — reads as no relation.\n // parent and parentType are therefore set together or not at all, so normalizePost,\n // getRelation, and deriveChildren stay consistent on malformed data.\n const rawParent = raw.meta?.[PARENT_META_KEY];\n const rawParentType = raw.meta?.[PARENT_TYPE_META_KEY];\n if (\n typeof rawParent === 'number' &&\n Number.isInteger(rawParent) &&\n rawParent > 0 &&\n typeof rawParentType === 'string' &&\n rawParentType !== ''\n ) {\n post.parent = rawParent;\n post.parentType = rawParentType;\n }\n return post;\n}\n\n/**\n * Normalize a raw post (edit context) into the body-editing model {@link WpPostEdit}.\n *\n * Reads `content.raw` for the body. Mode is determined by the Markdown source: only a\n * *non-empty* `_dbp_wp_markdown` value marks Markdown mode. This mirrors the relation keys'\n * value-based sentinel (a positive id means \"set\") and sidesteps the REST default for a\n * registered string meta (an unset key can come back as `''`), so an HTML-mode post is never\n * mistaken for an empty-bodied Markdown post.\n */\nexport function normalizePostForEdit(raw: WpPostResponse): WpPostEdit {\n const edit: WpPostEdit = {\n id: raw.id,\n type: raw.type,\n status: raw.status,\n title: raw.title.raw ?? raw.title.rendered,\n content: raw.content?.raw ?? '',\n };\n const markdown = raw.meta?.[MARKDOWN_META_KEY];\n if (typeof markdown === 'string' && markdown !== '') {\n edit.markdown = markdown;\n }\n return edit;\n}\n","import { marked } from 'marked';\n\n/**\n * Convert Markdown source to HTML for the single-post body editor.\n *\n * One shared implementation, used by the UI (live preview and the HTML mirrored into\n * `post_content` on save) and available to the CLI/future MCP layer, so a post's stored\n * HTML always matches what the editor previewed. GitHub-flavored Markdown is enabled.\n *\n * This does NOT sanitize the output. Sanitization is delegated to WordPress `kses` at save\n * time (capability-dependent), and the live preview is isolated in a sandboxed iframe with\n * no script execution — matching the body-editing design (no extra sanitizer dependency in\n * DBP WP).\n */\nexport function renderMarkdown(markdown: string): string {\n // `async: false` selects marked's synchronous overload (string return). Guard the result\n // so an unexpected non-string (e.g. a future async extension) degrades to empty rather\n // than leaking a Promise into the rendered body.\n const html = marked.parse(markdown, { async: false, gfm: true });\n return typeof html === 'string' ? html : '';\n}\n","import { Parser, type Expression } from 'expr-eval-fork';\n\n/**\n * Formula engine: evaluates a spreadsheet expression against named numeric cells.\n *\n * Implementations MUST NOT use `eval`, `Function`, or any other dynamic code execution.\n */\nexport interface FormulaEngine {\n /**\n * Evaluate a single expression against a map of cell references to numbers.\n * Throws on invalid syntax, unknown variables, or a non-numeric result.\n */\n evaluate(expression: string, context: Record<string, number>): number;\n}\n\n/**\n * Formula engine backed by expr-eval-fork, which parses to an AST and evaluates without\n * `eval`/`Function`. Member access, assignment, and function definitions are disabled,\n * the nondeterministic `random()` function is removed, and results are constrained to\n * finite numbers, so expressions stay pure, deterministic, and side-effect free.\n */\nexport class SafeFormulaEngine implements FormulaEngine {\n private readonly parser: Parser;\n\n constructor() {\n this.parser = new Parser({\n allowMemberAccess: false,\n operators: { assignment: false, fndef: false },\n });\n // The `operators.random` flag does not remove the random() function; delete it so\n // evaluation stays deterministic.\n delete this.parser.functions.random;\n }\n\n evaluate(expression: string, context: Record<string, number>): number {\n let parsed: Expression;\n try {\n parsed = this.parser.parse(expression);\n } catch (e) {\n throw new Error(`Invalid formula: ${e instanceof Error ? e.message : 'parse error'}`);\n }\n\n let result: unknown;\n try {\n result = parsed.evaluate(context);\n } catch (e) {\n throw new Error(`Formula evaluation failed: ${e instanceof Error ? e.message : 'error'}`);\n }\n\n if (typeof result !== 'number' || !Number.isFinite(result)) {\n throw new Error('Formula must evaluate to a finite number.');\n }\n return result;\n }\n}\n","import type { UpdatePostFields } from './types';\n\n// WordPress stores menu_order in a signed 32-bit column; ignore cells outside that range\n// so one bad value does not get the whole server-side chunk rejected.\nconst MENU_ORDER_MIN = -2_147_483_648;\nconst MENU_ORDER_MAX = 2_147_483_647;\n\n/**\n * A tabular view of an import file: a header row plus data rows. Both CSV and JSON\n * sources are normalized to this shape so the column-mapping logic is source-agnostic.\n * Each data row is aligned to `headers` by index; a short row has missing trailing cells.\n */\nexport interface ParsedTable {\n /** Column headers (CSV first row, or the union of JSON object keys). */\n headers: string[];\n /** Data rows; `rows[r][c]` is the cell under `headers[c]`. */\n rows: string[][];\n}\n\n/**\n * Where a file column is imported to. `skip` drops the column; `title`/`status`/\n * `menuOrder` map to standard post fields; `meta` writes an arbitrary post-meta key\n * (companion plugin required).\n */\nexport type ImportTarget =\n | { kind: 'skip' }\n | { kind: 'title' }\n | { kind: 'status' }\n | { kind: 'menuOrder' }\n | { kind: 'meta'; key: string };\n\n/** A single new post to create, derived from one import row. */\nexport interface ImportCreate {\n /** Standard fields (title / menuOrder / status). */\n fields: UpdatePostFields;\n /** Arbitrary meta to write via the companion plugin (omitted when none). */\n meta?: Record<string, unknown>;\n}\n\n/**\n * Parse CSV text into a table, taking the first record as headers. Implements the\n * RFC 4180 essentials: double-quoted fields, embedded commas/newlines, `\"\"` escapes,\n * and CRLF or LF line endings. A trailing newline does not produce an empty record.\n */\nexport function parseCsv(text: string): ParsedTable {\n const records = parseCsvRecords(text);\n const headers = records[0] ?? [];\n return { headers, rows: records.slice(1) };\n}\n\nfunction parseCsvRecords(text: string): string[][] {\n const records: string[][] = [];\n let record: string[] = [];\n let field = '';\n let inQuotes = false;\n let i = 0;\n\n const endField = (): void => {\n record.push(field);\n field = '';\n };\n const endRecord = (): void => {\n endField();\n records.push(record);\n record = [];\n };\n\n while (i < text.length) {\n const ch = text[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (text[i + 1] === '\"') {\n field += '\"';\n i += 2;\n continue;\n }\n inQuotes = false;\n i += 1;\n continue;\n }\n field += ch;\n i += 1;\n continue;\n }\n if (ch === '\"') {\n inQuotes = true;\n i += 1;\n continue;\n }\n if (ch === ',') {\n endField();\n i += 1;\n continue;\n }\n if (ch === '\\r') {\n endRecord();\n i += text[i + 1] === '\\n' ? 2 : 1;\n continue;\n }\n if (ch === '\\n') {\n endRecord();\n i += 1;\n continue;\n }\n field += ch;\n i += 1;\n }\n // An unclosed quote means the file is malformed; surface it rather than silently\n // merging the rest of the file into one field and writing corrupted data.\n if (inQuotes) {\n throw new Error('Malformed CSV: unterminated quoted field.');\n }\n // Flush a final record only if there is pending content (no trailing-newline ghost row).\n if (field !== '' || record.length > 0) {\n endRecord();\n }\n return records;\n}\n\n/**\n * Parse JSON text (an array of objects) into a table. Headers are the union of all\n * object keys in first-seen order. Object/array cell values are JSON-stringified;\n * null and undefined become an empty string. Throws if the JSON is not an array.\n */\nexport function parseJsonRecords(text: string): ParsedTable {\n const data: unknown = JSON.parse(text);\n if (!Array.isArray(data)) {\n throw new Error('JSON import must be an array of objects.');\n }\n const headers: string[] = [];\n const seen = new Set<string>();\n for (const entry of data) {\n if (isPlainRecord(entry)) {\n for (const key of Object.keys(entry)) {\n if (!seen.has(key)) {\n seen.add(key);\n headers.push(key);\n }\n }\n }\n }\n const rows = data.map((entry) => {\n const record = isPlainRecord(entry) ? entry : {};\n return headers.map((header) => stringifyCell(record[header]));\n });\n return { headers, rows };\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction stringifyCell(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n if (typeof value === 'object') {\n return JSON.stringify(value);\n }\n return String(value);\n}\n\n/**\n * Known post-status values and English/value labels, mapped to the WordPress status.\n * A null-prototype map so inherited keys (`constructor`, `toString`, `__proto__`, …) do\n * not accidentally resolve to a function/object instead of falling back to the raw value.\n */\nconst STATUS_LABELS: Record<string, string> = Object.assign(\n Object.create(null) as Record<string, string>,\n {\n publish: 'publish',\n published: 'publish',\n draft: 'draft',\n pending: 'pending',\n private: 'private',\n future: 'future',\n },\n);\n\n/**\n * Normalize a status cell to a WordPress status. Known labels/values (case-insensitive,\n * e.g. `Published` → `publish`) are mapped; anything else passes through trimmed so the\n * WordPress REST API can validate it and surface a per-row error if invalid.\n */\nexport function normalizeStatus(value: string): string {\n const trimmed = value.trim();\n return STATUS_LABELS[trimmed.toLowerCase()] ?? trimmed;\n}\n\n/**\n * Apply a column mapping to a parsed table, producing one {@link ImportCreate} per row.\n * Empty cells contribute nothing; a row that maps to no fields and no meta is skipped.\n * Non-integer `menuOrder` cells are ignored. Meta is stored on a null-prototype object\n * so a header named `__proto__` is kept as data, never touching any prototype.\n */\nexport function buildImportPlan(table: ParsedTable, mapping: ImportTarget[]): ImportCreate[] {\n const creates: ImportCreate[] = [];\n for (const row of table.rows) {\n const fields: UpdatePostFields = {};\n let meta: Record<string, unknown> | undefined;\n\n for (let col = 0; col < mapping.length; col += 1) {\n const target = mapping[col];\n if (!target || target.kind === 'skip') {\n continue;\n }\n const value = row[col] ?? '';\n if (value === '') {\n continue;\n }\n switch (target.kind) {\n case 'title':\n fields.title = value;\n break;\n case 'status':\n fields.status = normalizeStatus(value);\n break;\n case 'menuOrder': {\n const order = Number(value);\n if (Number.isInteger(order) && order >= MENU_ORDER_MIN && order <= MENU_ORDER_MAX) {\n fields.menuOrder = order;\n }\n break;\n }\n case 'meta':\n // An empty/whitespace meta key would be rejected by the server (empty key),\n // failing the whole chunk; skip it rather than emit `meta[\"\"]`.\n if (target.key.trim() === '') {\n break;\n }\n if (!meta) {\n meta = Object.create(null) as Record<string, unknown>;\n }\n meta[target.key] = value;\n break;\n }\n }\n\n if (meta !== undefined && Object.keys(meta).length > 0) {\n creates.push({ fields, meta });\n } else if (Object.keys(fields).length > 0) {\n creates.push({ fields });\n }\n }\n return creates;\n}\n"],"mappings":"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","names":["flattenMetaValue","ROUTE_SEGMENT"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbp-wp/core",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Core library for DBP WP: WordPress REST client, formula engine, importer, and typesetting data generation.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Takashi Matsuyama",