@dbp-wp/core 0.1.0 → 0.2.0

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
@@ -33,6 +33,25 @@ interface WpPostResponse {
33
33
  * Present only when the connector is active; absent in restricted mode.
34
34
  */
35
35
  dbp_wp_meta?: Record<string, unknown>;
36
+ /** Rendered/raw content. Present when the request asks for it (e.g. Print Design). */
37
+ content?: {
38
+ rendered?: string;
39
+ raw?: string;
40
+ };
41
+ /** Rendered/raw excerpt. Present when the request asks for it. */
42
+ excerpt?: {
43
+ rendered?: string;
44
+ raw?: string;
45
+ };
46
+ /**
47
+ * Embedded resources, present only when the request was made with `_embed`. Shapes are
48
+ * intentionally loose (entries may be error objects); consumers validate at runtime.
49
+ * `wp:featuredmedia` carries media objects; `wp:term` is an array of term-arrays.
50
+ */
51
+ _embedded?: {
52
+ 'wp:featuredmedia'?: unknown;
53
+ 'wp:term'?: unknown;
54
+ };
36
55
  }
37
56
  /** Normalized, internal post model used by DBP WP. */
38
57
  interface WpPost {
@@ -53,6 +72,17 @@ interface WpPost {
53
72
  * mode (no connector installed).
54
73
  */
55
74
  dbpWpMeta?: Record<string, unknown>;
75
+ /**
76
+ * Parent post ID from the `_dbp_wp_parent` relation meta. Present only when the post
77
+ * has a parent and the connector registered the key; `undefined` otherwise. The parent
78
+ * post type's REST base is {@link WpPost.parentType}.
79
+ */
80
+ parent?: number;
81
+ /**
82
+ * Parent post type's REST route base from `_dbp_wp_parent_type`. Pairs with
83
+ * {@link WpPost.parent}; `undefined` when the post has no parent.
84
+ */
85
+ parentType?: string;
56
86
  }
57
87
  /**
58
88
  * Editable standard post fields. These map to core WordPress REST fields and need no
@@ -92,6 +122,124 @@ interface ListPostsParams {
92
122
  page?: number;
93
123
  }
94
124
 
125
+ /**
126
+ * Print Design template engine.
127
+ *
128
+ * Renders a user-authored HTML template against a {@link PrintRecord} using a small,
129
+ * mustache-like `{{ }}` syntax. Pure and framework-agnostic: the browser UI renders the
130
+ * result inside a sandboxed iframe and prints it (see planning doc 04-print-design).
131
+ *
132
+ * Syntax:
133
+ * - `{{ path }}` resolve a dotted path, HTML-escape the value, and output it.
134
+ * - `{{{ path }}}` same, but output the value raw (no escaping) for HTML fields such as
135
+ * `content` / `excerpt`, which already hold WordPress-rendered HTML.
136
+ * - `{{#each path}} ... {{/each}}` iterate an array; inside the block, `this` is the
137
+ * current item (and `this.<key>` reaches into an object item).
138
+ *
139
+ * Unknown paths render as the empty string. Paths resolve against the record, except
140
+ * `this` / `this.<key>`, which resolve against the current `{{#each}}` item.
141
+ */
142
+
143
+ /** The data a template is rendered against (one WordPress post, flattened for templating). */
144
+ interface PrintRecord {
145
+ id: number;
146
+ title: string;
147
+ /** WordPress-rendered HTML. Use `{{{ content }}}` so it is not escaped. */
148
+ content: string;
149
+ /** WordPress-rendered HTML. Use `{{{ excerpt }}}` so it is not escaped. */
150
+ excerpt: string;
151
+ status: string;
152
+ menuOrder: number;
153
+ /** Absolute URL of the featured image, or `''` when the post has none. */
154
+ featuredImageUrl: string;
155
+ /**
156
+ * Flattened post meta, keyed by meta key (values stringified; arrays joined with `, `).
157
+ * Reachable in templates as `{{ meta.<key> }}`. A meta key containing a literal dot
158
+ * cannot be addressed, since template paths split on `.`.
159
+ */
160
+ meta: Record<string, string>;
161
+ /** Taxonomy terms keyed by REST base (e.g. `tax.category`), each an array of term names. */
162
+ tax: Record<string, string[]>;
163
+ }
164
+ /** Thrown when a template has an unbalanced `{{#each}}` / `{{/each}}`. */
165
+ declare class TemplateParseError extends Error {
166
+ constructor(message: string);
167
+ }
168
+ /**
169
+ * Render a Print Design template against one record. Values are HTML-escaped for `{{ }}`
170
+ * and emitted raw for `{{{ }}}`. Throws {@link TemplateParseError} on unbalanced
171
+ * `{{#each}}` / `{{/each}}`.
172
+ */
173
+ declare function renderTemplate(template: string, record: PrintRecord): string;
174
+ /**
175
+ * Build a {@link PrintRecord} from a raw WordPress REST post. Expects the request to have
176
+ * used `context=edit` and `_embed` so `content`/`excerpt` and embedded media/terms are
177
+ * present; missing pieces degrade to empty values rather than throwing.
178
+ *
179
+ * `title` uses the raw (unescaped) value so `{{ title }}` escaping is not doubled.
180
+ * `content`/`excerpt` use the rendered HTML (intended for `{{{ }}}`).
181
+ */
182
+ declare function buildPrintRecord(raw: WpPostResponse): PrintRecord;
183
+
184
+ /**
185
+ * Parent/child relations (MVP: links only).
186
+ *
187
+ * The relation is stored single-source on the **child**: `_dbp_wp_parent` holds the
188
+ * parent post ID and `_dbp_wp_parent_type` holds the parent post type's REST route base.
189
+ * The parent keeps no child list — a parent's children are derived from already-loaded
190
+ * posts ({@link deriveChildren}), so there is no denormalized list to keep in sync.
191
+ *
192
+ * These keys are `_`-prefixed (protected) and are exposed over REST only because the
193
+ * companion plugin registers them with `register_post_meta()` + an `edit_post`
194
+ * `auth_callback`. They therefore travel through the standard core `meta` field — not the
195
+ * connector's `dbp_wp_meta` field — so relation writes use a dedicated path.
196
+ */
197
+ /** Meta key holding a child's parent post ID. */
198
+ declare const PARENT_META_KEY = "_dbp_wp_parent";
199
+ /** Meta key holding the parent post type's REST route base. */
200
+ declare const PARENT_TYPE_META_KEY = "_dbp_wp_parent_type";
201
+ /** A parent assignment for a child post. */
202
+ interface RelationTarget {
203
+ /** Parent post ID (positive integer). */
204
+ parentId: number;
205
+ /** Parent post type's REST route base (e.g. `pages`). */
206
+ parentType: string;
207
+ }
208
+ /** Error thrown when a relation assignment is invalid (bad id/type, or self-parent). */
209
+ declare class RelationError extends Error {
210
+ constructor(message: string);
211
+ }
212
+ /**
213
+ * Validate a parent assignment for a child. Throws {@link RelationError} when the parent
214
+ * id is not a positive safe integer, the parent type is not a valid route segment, or the
215
+ * parent is the child itself (post IDs are unique across types, so this catches a
216
+ * self-parent regardless of type).
217
+ */
218
+ declare function assertValidRelation(childId: number, relation: RelationTarget): void;
219
+ /**
220
+ * Build the standard-`meta` body that sets a child's parent. Validates the assignment
221
+ * first ({@link assertValidRelation}). The keys ride the core `meta` field, so the caller
222
+ * sends `{ meta: <this> }`.
223
+ */
224
+ declare function buildSetRelationMeta(childId: number, relation: RelationTarget): Record<string, unknown>;
225
+ /**
226
+ * Build the standard-`meta` body that clears a child's parent. Sending `null` for a
227
+ * registered single meta key makes WordPress delete it, so no stale value is left behind.
228
+ */
229
+ declare function buildClearRelationMeta(): Record<string, unknown>;
230
+ /**
231
+ * Read a post's parent relation from its normalized fields, or null when it has no parent.
232
+ * Both a positive `parent` id and a non-empty `parentType` are required for a relation to
233
+ * count (a half-written relation reads as none).
234
+ */
235
+ declare function getRelation(post: WpPost): RelationTarget | null;
236
+ /**
237
+ * Derive a parent's children from already-loaded posts: every post whose `_dbp_wp_parent`
238
+ * equals `parentId`. This covers the common case of same-type children visible in the
239
+ * current grid; cross-type or off-grid children would need a server-side query (deferred).
240
+ */
241
+ declare function deriveChildren(posts: WpPost[], parentId: number): WpPost[];
242
+
95
243
  /**
96
244
  * Validate and normalize a WordPress site URL into a REST base (origin + base path).
97
245
  *
@@ -129,6 +277,12 @@ declare class WpClient {
129
277
  listPostTypes(): Promise<WpPostType[]>;
130
278
  /** List posts of a given type in edit context (raw fields, for editing). */
131
279
  listPosts(params?: ListPostsParams): Promise<WpPost[]>;
280
+ /**
281
+ * List posts as {@link PrintRecord}s for Print Design. Requests `_embed` (so featured
282
+ * media and taxonomy terms come back inline) plus `content`/`excerpt`; the standard
283
+ * table/spreadsheet listing ({@link WpClient.listPosts}) is unaffected.
284
+ */
285
+ listPostsForPrint(params?: ListPostsParams): Promise<PrintRecord[]>;
132
286
  /**
133
287
  * Update post fields in a single request. Standard fields (title, menu_order,
134
288
  * status) are core REST fields and need no plugin. When `meta` is supplied it rides
@@ -156,6 +310,19 @@ declare class WpClient {
156
310
  * post type is irrelevant). Requires the connector.
157
311
  */
158
312
  deletePostMeta(id: number, keys: string[]): Promise<DeleteMetaResult>;
313
+ /**
314
+ * Set a child post's parent relation. The relation keys ride the standard core `meta`
315
+ * field (the connector registers them with `register_post_meta`), so this is a distinct
316
+ * path from {@link WpClient.updatePostMeta} (which uses the connector's `dbp_wp_meta`
317
+ * field). Validates the assignment (positive id, valid type, no self-parent) before
318
+ * writing. Requires the connector; without it WordPress silently ignores the keys.
319
+ */
320
+ setRelation(childId: number, childType: string, relation: RelationTarget): Promise<WpPost>;
321
+ /**
322
+ * Clear a child post's parent relation. Sends `null` for both relation keys, which makes
323
+ * WordPress delete them (no stale `0`/empty value left behind). Requires the connector.
324
+ */
325
+ clearRelation(childId: number, childType: string): Promise<WpPost>;
159
326
  /**
160
327
  * Detect whether the companion plugin is active by checking the REST index
161
328
  * (`/wp-json/`) for the connector's namespace. Throws on a failed request; a caller
@@ -249,4 +416,4 @@ declare function normalizeStatus(value: string): string;
249
416
  */
250
417
  declare function buildImportPlan(table: ParsedTable, mapping: ImportTarget[]): ImportCreate[];
251
418
 
252
- export { type DeleteMetaResult, type FormulaEngine, type ImportCreate, type ImportTarget, type ListPostsParams, type ParsedTable, SafeFormulaEngine, type UpdatePostFields, WpClient, type WpCredentials, type WpPost, type WpPostResponse, type WpPostType, WpRequestError, buildAuthHeader, buildImportPlan, normalizeSiteUrl, normalizeStatus, parseCsv, parseJsonRecords };
419
+ export { type DeleteMetaResult, type FormulaEngine, type ImportCreate, type ImportTarget, type ListPostsParams, PARENT_META_KEY, PARENT_TYPE_META_KEY, type ParsedTable, type PrintRecord, RelationError, type RelationTarget, SafeFormulaEngine, TemplateParseError, type UpdatePostFields, WpClient, type WpCredentials, type WpPost, type WpPostResponse, type WpPostType, WpRequestError, assertValidRelation, buildAuthHeader, buildClearRelationMeta, buildImportPlan, buildPrintRecord, buildSetRelationMeta, deriveChildren, getRelation, normalizeSiteUrl, normalizeStatus, parseCsv, parseJsonRecords, renderTemplate };
package/dist/index.js CHANGED
@@ -1,6 +1,219 @@
1
+ // src/print.ts
2
+ var TemplateParseError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "TemplateParseError";
6
+ }
7
+ };
8
+ var TAG = /(\{\{\{\s*([\s\S]+?)\s*\}\}\})|(\{\{\s*#each\s+([\s\S]+?)\s*\}\})|(\{\{\s*\/each\s*\}\})|(\{\{\s*([\s\S]+?)\s*\}\})/g;
9
+ function parseTemplate(template) {
10
+ const root = [];
11
+ let current = root;
12
+ const parents = [];
13
+ const open = [];
14
+ const pushText = (text) => {
15
+ if (text) current.push({ kind: "text", value: text });
16
+ };
17
+ let last = 0;
18
+ TAG.lastIndex = 0;
19
+ let m;
20
+ while ((m = TAG.exec(template)) !== null) {
21
+ pushText(template.slice(last, m.index));
22
+ last = TAG.lastIndex;
23
+ if (m[2] !== void 0) {
24
+ current.push({ kind: "var", path: m[2].trim(), raw: true });
25
+ } else if (m[4] !== void 0) {
26
+ const node = { kind: "each", path: m[4].trim(), body: [] };
27
+ current.push(node);
28
+ parents.push(current);
29
+ current = node.body;
30
+ open.push(node);
31
+ } else if (m[5] !== void 0) {
32
+ if (open.length === 0) {
33
+ throw new TemplateParseError("Unexpected {{/each}} without a matching {{#each}}.");
34
+ }
35
+ open.pop();
36
+ current = parents.pop() ?? root;
37
+ } else if (m[7] !== void 0) {
38
+ current.push({ kind: "var", path: m[7].trim(), raw: false });
39
+ }
40
+ }
41
+ pushText(template.slice(last));
42
+ if (open.length > 0) {
43
+ const unclosed = open[open.length - 1];
44
+ throw new TemplateParseError(`Unclosed {{#each ${unclosed ? unclosed.path : ""}}}.`);
45
+ }
46
+ return root;
47
+ }
48
+ function getPath(obj, path) {
49
+ let cur = obj;
50
+ for (const key of path.split(".")) {
51
+ if (cur === null || cur === void 0 || typeof cur !== "object") {
52
+ return void 0;
53
+ }
54
+ cur = cur[key];
55
+ }
56
+ return cur;
57
+ }
58
+ function resolve(path, scope) {
59
+ if (path === "this") {
60
+ return scope.current;
61
+ }
62
+ if (path.startsWith("this.")) {
63
+ return getPath(scope.current, path.slice("this.".length));
64
+ }
65
+ return getPath(scope.record, path);
66
+ }
67
+ function stringifyScalar(value) {
68
+ if (value === null || value === void 0) {
69
+ return "";
70
+ }
71
+ if (typeof value === "string") {
72
+ return value;
73
+ }
74
+ if (typeof value === "number" || typeof value === "boolean") {
75
+ return String(value);
76
+ }
77
+ return "";
78
+ }
79
+ function escapeHtml(value) {
80
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
81
+ }
82
+ function renderNodes(nodes, scope) {
83
+ let out = "";
84
+ for (const node of nodes) {
85
+ if (node.kind === "text") {
86
+ out += node.value;
87
+ } else if (node.kind === "var") {
88
+ const value = resolve(node.path, scope);
89
+ out += node.raw ? stringifyScalar(value) : escapeHtml(stringifyScalar(value));
90
+ } else {
91
+ const list = resolve(node.path, scope);
92
+ if (Array.isArray(list)) {
93
+ for (const item of list) {
94
+ out += renderNodes(node.body, { record: scope.record, current: item });
95
+ }
96
+ }
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ function renderTemplate(template, record) {
102
+ return renderNodes(parseTemplate(template), { record });
103
+ }
104
+ function flattenMetaValue(value) {
105
+ if (Array.isArray(value)) {
106
+ return value.map(stringifyScalar).filter((s) => s !== "").join(", ");
107
+ }
108
+ return stringifyScalar(value);
109
+ }
110
+ function flattenMeta(raw) {
111
+ const out = /* @__PURE__ */ Object.create(null);
112
+ const add = (src) => {
113
+ if (!src) return;
114
+ for (const [key, value] of Object.entries(src)) {
115
+ out[key] = flattenMetaValue(value);
116
+ }
117
+ };
118
+ add(raw.meta);
119
+ add(raw.dbp_wp_meta);
120
+ return out;
121
+ }
122
+ function extractFeaturedImageUrl(embedded) {
123
+ const media = embedded?.["wp:featuredmedia"];
124
+ if (Array.isArray(media) && media.length > 0) {
125
+ const first = media[0];
126
+ if (first !== null && typeof first === "object") {
127
+ const sourceUrl = first.source_url;
128
+ if (typeof sourceUrl === "string") {
129
+ return sourceUrl;
130
+ }
131
+ }
132
+ }
133
+ return "";
134
+ }
135
+ function extractTerms(embedded) {
136
+ const tax = /* @__PURE__ */ Object.create(null);
137
+ const groups = embedded?.["wp:term"];
138
+ if (!Array.isArray(groups)) {
139
+ return tax;
140
+ }
141
+ for (const group of groups) {
142
+ if (!Array.isArray(group)) continue;
143
+ for (const term of group) {
144
+ if (term === null || typeof term !== "object") continue;
145
+ const entry = term;
146
+ if (typeof entry.taxonomy === "string" && typeof entry.name === "string") {
147
+ (tax[entry.taxonomy] ??= []).push(entry.name);
148
+ }
149
+ }
150
+ }
151
+ return tax;
152
+ }
153
+ function buildPrintRecord(raw) {
154
+ return {
155
+ id: raw.id,
156
+ title: raw.title?.raw ?? raw.title?.rendered ?? "",
157
+ content: raw.content?.rendered ?? "",
158
+ excerpt: raw.excerpt?.rendered ?? "",
159
+ status: raw.status,
160
+ menuOrder: raw.menu_order,
161
+ featuredImageUrl: extractFeaturedImageUrl(raw._embedded),
162
+ meta: flattenMeta(raw),
163
+ tax: extractTerms(raw._embedded)
164
+ };
165
+ }
166
+
167
+ // src/relation.ts
168
+ var PARENT_META_KEY = "_dbp_wp_parent";
169
+ var PARENT_TYPE_META_KEY = "_dbp_wp_parent_type";
170
+ var ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;
171
+ var RelationError = class extends Error {
172
+ constructor(message) {
173
+ super(message);
174
+ this.name = "RelationError";
175
+ }
176
+ };
177
+ function assertValidRelation(childId, relation) {
178
+ if (!Number.isSafeInteger(relation.parentId) || relation.parentId <= 0) {
179
+ throw new RelationError(`Invalid parent id: ${String(relation.parentId)}`);
180
+ }
181
+ if (typeof relation.parentType !== "string" || !ROUTE_SEGMENT.test(relation.parentType)) {
182
+ throw new RelationError(`Invalid parent type: ${String(relation.parentType)}`);
183
+ }
184
+ if (relation.parentId === childId) {
185
+ throw new RelationError("A post cannot be its own parent.");
186
+ }
187
+ }
188
+ function buildSetRelationMeta(childId, relation) {
189
+ assertValidRelation(childId, relation);
190
+ return {
191
+ [PARENT_META_KEY]: relation.parentId,
192
+ [PARENT_TYPE_META_KEY]: relation.parentType
193
+ };
194
+ }
195
+ function buildClearRelationMeta() {
196
+ return {
197
+ [PARENT_META_KEY]: null,
198
+ [PARENT_TYPE_META_KEY]: null
199
+ };
200
+ }
201
+ function getRelation(post) {
202
+ if (typeof post.parent === "number" && post.parent > 0 && typeof post.parentType === "string" && post.parentType !== "") {
203
+ return { parentId: post.parent, parentType: post.parentType };
204
+ }
205
+ return null;
206
+ }
207
+ function deriveChildren(posts, parentId) {
208
+ if (!Number.isSafeInteger(parentId) || parentId <= 0) {
209
+ return [];
210
+ }
211
+ return posts.filter((post) => post.parent === parentId);
212
+ }
213
+
1
214
  // src/wp-client.ts
2
215
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
3
- var ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;
216
+ var ROUTE_SEGMENT2 = /^[a-z0-9_-]+$/i;
4
217
  var META_FIELD = "dbp_wp_meta";
5
218
  var CONNECTOR_NAMESPACE = "dbp-wp/v1";
6
219
  function normalizeSiteUrl(siteUrl) {
@@ -88,6 +301,25 @@ var WpClient = class {
88
301
  const raw = await this.request(`/wp/v2/${type}?${query.toString()}`);
89
302
  return raw.map(normalizePost);
90
303
  }
304
+ /**
305
+ * List posts as {@link PrintRecord}s for Print Design. Requests `_embed` (so featured
306
+ * media and taxonomy terms come back inline) plus `content`/`excerpt`; the standard
307
+ * table/spreadsheet listing ({@link WpClient.listPosts}) is unaffected.
308
+ */
309
+ async listPostsForPrint(params = {}) {
310
+ const type = params.type ?? "posts";
311
+ assertRouteSegment(type);
312
+ const perPage = clampInt(params.perPage ?? 100, 1, 100);
313
+ const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);
314
+ const query = new URLSearchParams({
315
+ context: "edit",
316
+ per_page: String(perPage),
317
+ page: String(page),
318
+ _embed: "1"
319
+ });
320
+ const raw = await this.request(`/wp/v2/${type}?${query.toString()}`);
321
+ return raw.map(buildPrintRecord);
322
+ }
91
323
  /**
92
324
  * Update post fields in a single request. Standard fields (title, menu_order,
93
325
  * status) are core REST fields and need no plugin. When `meta` is supplied it rides
@@ -143,6 +375,35 @@ var WpClient = class {
143
375
  deleted: Array.isArray(raw.deleted) ? raw.deleted : []
144
376
  };
145
377
  }
378
+ /**
379
+ * Set a child post's parent relation. The relation keys ride the standard core `meta`
380
+ * field (the connector registers them with `register_post_meta`), so this is a distinct
381
+ * path from {@link WpClient.updatePostMeta} (which uses the connector's `dbp_wp_meta`
382
+ * field). Validates the assignment (positive id, valid type, no self-parent) before
383
+ * writing. Requires the connector; without it WordPress silently ignores the keys.
384
+ */
385
+ async setRelation(childId, childType, relation) {
386
+ assertPostId(childId);
387
+ assertRouteSegment(childType);
388
+ const raw = await this.request(
389
+ `/wp/v2/${childType}/${String(childId)}?context=edit`,
390
+ { method: "POST", body: JSON.stringify({ meta: buildSetRelationMeta(childId, relation) }) }
391
+ );
392
+ return normalizePost(raw);
393
+ }
394
+ /**
395
+ * Clear a child post's parent relation. Sends `null` for both relation keys, which makes
396
+ * WordPress delete them (no stale `0`/empty value left behind). Requires the connector.
397
+ */
398
+ async clearRelation(childId, childType) {
399
+ assertPostId(childId);
400
+ assertRouteSegment(childType);
401
+ const raw = await this.request(
402
+ `/wp/v2/${childType}/${String(childId)}?context=edit`,
403
+ { method: "POST", body: JSON.stringify({ meta: buildClearRelationMeta() }) }
404
+ );
405
+ return normalizePost(raw);
406
+ }
146
407
  /**
147
408
  * Detect whether the companion plugin is active by checking the REST index
148
409
  * (`/wp-json/`) for the connector's namespace. Throws on a failed request; a caller
@@ -167,7 +428,7 @@ function buildUpdateBody(fields) {
167
428
  return body;
168
429
  }
169
430
  function assertRouteSegment(segment) {
170
- if (!ROUTE_SEGMENT.test(segment)) {
431
+ if (!ROUTE_SEGMENT2.test(segment)) {
171
432
  throw new Error(`Invalid REST route segment: ${segment}`);
172
433
  }
173
434
  }
@@ -215,7 +476,7 @@ function normalizePostTypes(raw) {
215
476
  continue;
216
477
  }
217
478
  const entry = value;
218
- if (typeof entry.rest_base !== "string" || !ROUTE_SEGMENT.test(entry.rest_base)) {
479
+ if (typeof entry.rest_base !== "string" || !ROUTE_SEGMENT2.test(entry.rest_base)) {
219
480
  continue;
220
481
  }
221
482
  result.push({
@@ -238,6 +499,14 @@ function normalizePost(raw) {
238
499
  if (raw.dbp_wp_meta !== void 0) {
239
500
  post.dbpWpMeta = raw.dbp_wp_meta;
240
501
  }
502
+ const rawParent = raw.meta?.[PARENT_META_KEY];
503
+ if (typeof rawParent === "number" && Number.isInteger(rawParent) && rawParent > 0) {
504
+ post.parent = rawParent;
505
+ }
506
+ const rawParentType = raw.meta?.[PARENT_TYPE_META_KEY];
507
+ if (typeof rawParentType === "string" && rawParentType !== "") {
508
+ post.parentType = rawParentType;
509
+ }
241
510
  return post;
242
511
  }
243
512
 
@@ -441,14 +710,25 @@ function buildImportPlan(table, mapping) {
441
710
  return creates;
442
711
  }
443
712
  export {
713
+ PARENT_META_KEY,
714
+ PARENT_TYPE_META_KEY,
715
+ RelationError,
444
716
  SafeFormulaEngine,
717
+ TemplateParseError,
445
718
  WpClient,
446
719
  WpRequestError,
720
+ assertValidRelation,
447
721
  buildAuthHeader,
722
+ buildClearRelationMeta,
448
723
  buildImportPlan,
724
+ buildPrintRecord,
725
+ buildSetRelationMeta,
726
+ deriveChildren,
727
+ getRelation,
449
728
  normalizeSiteUrl,
450
729
  normalizeStatus,
451
730
  parseCsv,
452
- parseJsonRecords
731
+ parseJsonRecords,
732
+ renderTemplate
453
733
  };
454
734
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/wp-client.ts","../src/calc/index.ts","../src/importer.ts"],"sourcesContent":["import type {\n DeleteMetaResult,\n ListPostsParams,\n UpdatePostFields,\n WpCredentials,\n WpPost,\n WpPostResponse,\n WpPostType,\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/** REST field added by the companion plugin to carry arbitrary post meta. */\nconst META_FIELD = 'dbp_wp_meta';\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 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 * 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 private async request<T>(path: string, init: RequestInit = {}): Promise<T> {\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 (await response.json()) as 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 * 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 * 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 const raw = await this.request<{ post_id?: unknown; deleted?: string[] }>(\n `/${CONNECTOR_NAMESPACE}/posts/${String(id)}/meta`,\n { method: 'DELETE', body: JSON.stringify({ keys: cleanKeys }) },\n );\n // Trust our own request `id` over a malformed connector `post_id`.\n return {\n postId: typeof raw.post_id === 'number' ? raw.post_id : id,\n deleted: Array.isArray(raw.deleted) ? raw.deleted : [],\n };\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/** 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 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 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/** 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 * 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\nfunction 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 };\n if (raw.dbp_wp_meta !== undefined) {\n post.dbpWpMeta = raw.dbp_wp_meta;\n }\n return post;\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":[]}
1
+ {"version":3,"sources":["../src/print.ts","../src/relation.ts","../src/wp-client.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: PrintRecord;\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): 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 = resolve(node.path, scope);\n out += node.raw ? stringifyScalar(value) : escapeHtml(stringifyScalar(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 });\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 });\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 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","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 ListPostsParams,\n UpdatePostFields,\n WpCredentials,\n WpPost,\n WpPostResponse,\n WpPostType,\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/** REST field added by the companion plugin to carry arbitrary post meta. */\nconst META_FIELD = 'dbp_wp_meta';\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 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 * 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 private async request<T>(path: string, init: RequestInit = {}): Promise<T> {\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 (await response.json()) as 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 * 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 * 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 const raw = await this.request<{ post_id?: unknown; deleted?: string[] }>(\n `/${CONNECTOR_NAMESPACE}/posts/${String(id)}/meta`,\n { method: 'DELETE', body: JSON.stringify({ keys: cleanKeys }) },\n );\n // Trust our own request `id` over a malformed connector `post_id`.\n return {\n postId: typeof raw.post_id === 'number' ? raw.post_id : id,\n deleted: Array.isArray(raw.deleted) ? raw.deleted : [],\n };\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/** 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 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 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/** 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 * 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\nfunction 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 };\n if (raw.dbp_wp_meta !== undefined) {\n post.dbpWpMeta = raw.dbp_wp_meta;\n }\n // Parent relation rides the standard `meta` field (registered by the connector). A\n // missing/zero id or empty type reads as \"no parent\", so only a complete pair is set.\n const rawParent = raw.meta?.[PARENT_META_KEY];\n if (typeof rawParent === 'number' && Number.isInteger(rawParent) && rawParent > 0) {\n post.parent = rawParent;\n }\n const rawParentType = raw.meta?.[PARENT_TYPE_META_KEY];\n if (typeof rawParentType === 'string' && rawParentType !== '') {\n post.parentType = rawParentType;\n }\n return post;\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":["ROUTE_SEGMENT"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbp-wp/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",