nextpress-core 1.0.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.
Files changed (92) hide show
  1. package/lib/acf-functions/core/acf-builder.ts +105 -0
  2. package/lib/acf-functions/core/acf-component-autoloader.ts +33 -0
  3. package/lib/acf-functions/core/acf-field-group-autoloader.ts +28 -0
  4. package/lib/acf-functions/services/define-field-group.ts +10 -0
  5. package/lib/acf-functions/services/define-layout.ts +10 -0
  6. package/lib/acf-functions/services/map-fields/helpers/map-choice-object.ts +20 -0
  7. package/lib/acf-functions/services/map-fields/map-fields.ts +359 -0
  8. package/lib/acf-functions/types/acf-field-group.d.ts +118 -0
  9. package/lib/acf-functions/types/acf-field.d.ts +2851 -0
  10. package/lib/acf-functions/types/acf-layout.d.ts +20 -0
  11. package/lib/acf-functions/types/acf-values.d.ts +4 -0
  12. package/lib/acf-functions/types/components/field-props.d.ts +189 -0
  13. package/lib/acf-functions/types/components/nextpress-component.d.ts +11 -0
  14. package/lib/ambient.d.ts +15 -0
  15. package/lib/entities/common.d.ts +30 -0
  16. package/lib/entities/option/option.interface.d.ts +8 -0
  17. package/lib/entities/option/option.ts +43 -0
  18. package/lib/entities/post/post.interface.d.ts +72 -0
  19. package/lib/entities/post/post.ts +209 -0
  20. package/lib/entities/term/term.interface.d.ts +8 -0
  21. package/lib/entities/term/term.ts +82 -0
  22. package/lib/entities/user/user.interface.d.ts +10 -0
  23. package/lib/entities/user/user.ts +86 -0
  24. package/lib/globals/entity-loader/entity-loader-base.ts +141 -0
  25. package/lib/globals/entity-loader/entity-loader.d.ts +50 -0
  26. package/lib/globals/entity-loader/option-loader.ts +56 -0
  27. package/lib/globals/entity-loader/post-loader.ts +59 -0
  28. package/lib/globals/entity-loader/term-loader.ts +59 -0
  29. package/lib/globals/entity-loader/user-loader.ts +60 -0
  30. package/lib/globals/get-field/get-field.ts +75 -0
  31. package/lib/globals/globals.ts +25 -0
  32. package/lib/globals/nextpress-config/nextpress-config.interface.d.ts +18 -0
  33. package/lib/globals/nextpress-config/nextpress-config.ts +7 -0
  34. package/lib/globals/queried-object/queried-object.ts +124 -0
  35. package/lib/repository/optionquery/option-query-args.d.ts +21 -0
  36. package/lib/repository/optionquery/option-query.ts +29 -0
  37. package/lib/repository/postquery/post-query-args.d.ts +108 -0
  38. package/lib/repository/postquery/post-query.ts +281 -0
  39. package/lib/repository/termquery/term-query-args.d.ts +61 -0
  40. package/lib/repository/termquery/term-query.ts +243 -0
  41. package/lib/repository/userquery/user-query-args.d.ts +75 -0
  42. package/lib/repository/userquery/user-query.ts +195 -0
  43. package/lib/router/helpers.ts +20 -0
  44. package/lib/router/nextpress-layout.tsx +48 -0
  45. package/lib/router/nextpress-proxy.ts +45 -0
  46. package/lib/router/nextpress-static-params.ts +62 -0
  47. package/lib/router/router.tsx +61 -0
  48. package/lib/router/routes/api/api-get-admin-bar.ts +29 -0
  49. package/lib/router/routes/api/api-get-draft-mode.ts +47 -0
  50. package/lib/router/routes/api/api-get-field-groups.ts +29 -0
  51. package/lib/router/routes/api/api-post-revalidate.ts +30 -0
  52. package/lib/router/routes/api/helpers.ts +17 -0
  53. package/lib/router/routes/author-archive.tsx +75 -0
  54. package/lib/router/routes/not-found-page.tsx +22 -0
  55. package/lib/router/routes/post-index-page.tsx +58 -0
  56. package/lib/router/routes/singular-page.tsx +78 -0
  57. package/lib/router/routes/site-front-page.tsx +51 -0
  58. package/lib/router/routes/term-archive.tsx +93 -0
  59. package/lib/router/types.d.ts +9 -0
  60. package/lib/services/get-menu.ts +104 -0
  61. package/lib/services/get-theme-mods.ts +16 -0
  62. package/lib/services/metadata/get-blogname.ts +9 -0
  63. package/lib/services/metadata/get-favicon-url.ts +13 -0
  64. package/lib/services/metadata/get-language-attribute.ts +9 -0
  65. package/lib/services/services.ts +7 -0
  66. package/lib/services/utilities/capitalise-first-letter.ts +10 -0
  67. package/lib/services/utilities/esc-html.ts +12 -0
  68. package/lib/services/utilities/get-date-time-formatter.ts +49 -0
  69. package/lib/services/utilities/index.ts +0 -0
  70. package/lib/services/utilities/kses-post.ts +13 -0
  71. package/lib/services/utilities/process-url.ts +15 -0
  72. package/lib/template-heirarchy/_autoloader/template-autoloader.ts +54 -0
  73. package/lib/template-heirarchy/archive/archive.tsx +28 -0
  74. package/lib/template-heirarchy/archive/author.tsx +28 -0
  75. package/lib/template-heirarchy/archive/category.tsx +29 -0
  76. package/lib/template-heirarchy/archive/posttypearchive.tsx +33 -0
  77. package/lib/template-heirarchy/archive/tag.tsx +30 -0
  78. package/lib/template-heirarchy/archive/taxonomy.tsx +28 -0
  79. package/lib/template-heirarchy/home/home.tsx +28 -0
  80. package/lib/template-heirarchy/index.tsx +30 -0
  81. package/lib/template-heirarchy/not-found.tsx/not-found.tsx +28 -0
  82. package/lib/template-heirarchy/page/page.tsx +28 -0
  83. package/lib/template-heirarchy/page/posttype.tsx +34 -0
  84. package/lib/template-heirarchy/page/single.tsx +28 -0
  85. package/lib/template-heirarchy/page/singular.tsx +28 -0
  86. package/lib/ui/render-attachment-image.tsx +29 -0
  87. package/lib/ui/render-components.tsx +12 -0
  88. package/lib/ui/render-the-admin-bar.tsx +79 -0
  89. package/lib/ui/render-the-logo.tsx +24 -0
  90. package/lib/wpdb/wpdb.interface.d.ts +171 -0
  91. package/lib/wpdb/wpdb.ts +36 -0
  92. package/package.json +38 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Class representing ACF builder.
3
+ */
4
+ export class ACFBuilder
5
+ {
6
+ /** Array of ACF field groups. */
7
+ private fieldGroups: ACFFieldGroup[] = [];
8
+
9
+ public constructor() {};
10
+
11
+ /**
12
+ * Gets built field groups.
13
+ *
14
+ * @returns {ACFFieldGroup[]} Array of field groups.
15
+ */
16
+ public getFieldGroups(): ACFFieldGroup[] {
17
+ return this.fieldGroups;
18
+ }
19
+
20
+ /**
21
+ * Converts field groups to JSON string.
22
+ *
23
+ * @returns {string} JSON string representation of field groups.
24
+ */
25
+ public toJSON(): string {
26
+ return JSON.stringify(this.fieldGroups);
27
+ }
28
+
29
+ /**
30
+ * Registers and processes Nextpress field groups.
31
+ *
32
+ * @param {NextpressFieldGroup[]} fieldGroups - Array of field groups to register.
33
+ * @returns {this} Current instance.
34
+ */
35
+ public registerFieldGroups(fieldGroups: NextpressFieldGroup[]): this {
36
+ const keyedFieldGroups: ACFFieldGroup[] = fieldGroups.map(fieldGroup => ({
37
+ ...fieldGroup,
38
+ key: `group_${this.formatKeySuffix(fieldGroup.title)}`,
39
+ fields: this.setFieldKeys(fieldGroup.fields, fieldGroup.title),
40
+ }));
41
+
42
+ this.fieldGroups = [...this.fieldGroups, ...keyedFieldGroups];
43
+
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Sets keys for fields based on parent name.
49
+ *
50
+ * @param {NextpressField[]} fields - Array of fields to process.
51
+ * @param {string} parentName - Parent string name.
52
+ * @returns {ACFField[]} Array of fields with generated keys.
53
+ */
54
+ private setFieldKeys(fields: NextpressField[], parentName: string): ACFField[] {
55
+ return fields.map(field => {
56
+ const keySuffix = this.formatKeySuffix(`${parentName}_${field.name}`);
57
+
58
+ const childFields = field.sub_fields
59
+ ? this.setFieldKeys(field.sub_fields, keySuffix)
60
+ : undefined;
61
+
62
+ const childLayouts = field.layouts
63
+ ? this.setLayoutKeys(field.layouts, keySuffix)
64
+ : undefined;
65
+
66
+ return {
67
+ ...field,
68
+ key: `field_${keySuffix}`,
69
+ sub_fields: childFields,
70
+ layouts: childLayouts
71
+ }});
72
+ }
73
+
74
+ /**
75
+ * Sets keys for layouts based on parent name.
76
+ *
77
+ * @param {NextpressLayout[]} layouts - Array of layouts to process.
78
+ * @param {string} parentName - Parent string name.
79
+ * @returns {ACFLayout[]} Array of layouts with generated keys.
80
+ */
81
+ private setLayoutKeys(layouts: NextpressLayout[], parentName: string): ACFLayout[] {
82
+ return layouts.map(layout => {
83
+ const keySuffix = this.formatKeySuffix(`${parentName}_${layout.name}`);
84
+
85
+ const childFields = layout.sub_fields
86
+ ? this.setFieldKeys(layout.sub_fields, `${parentName}_${layout.name}`)
87
+ : undefined;
88
+
89
+ return {
90
+ ...layout,
91
+ key: `layout_${keySuffix}`,
92
+ sub_fields: childFields || [],
93
+ }})
94
+ }
95
+
96
+ /**
97
+ * Formats suffix string.
98
+ *
99
+ * @param {string} suffix - Suffix to format.
100
+ * @returns {string} Formatted string.
101
+ */
102
+ private formatKeySuffix(suffix: string): string {
103
+ return suffix.replace(/[\s-]+/g, '_').toLowerCase();
104
+ }
105
+ }
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { NextpressComponent } from '../types/components/nextpress-component';
4
+
5
+ /**
6
+ * Autoloads ACF components dynamically from the templates directory.
7
+ * * **Requirements for it to work:**
8
+ * Each `.tsx` file within the `src/app/_templates/components/` directory MUST export the following:
9
+ * 1. `layout` (Named Export): The configuration for the ACF layout, typically defined using the `defineLayout` function.
10
+ * 2. `default` (Default Export): The React component (JSX/TSX function) that renders the layout.
11
+ *
12
+ * @returns {Promise<NextpressComponent[]>} A promise resolving to an array of mapped layout configurations and their respective components.
13
+ */
14
+ export async function acfComponentAutoloader(): Promise<NextpressComponent[]> {
15
+ const absolutePath = path.join(process.cwd(), 'src', 'app', '_templates', 'components');
16
+ const files = fs.readdirSync(absolutePath);
17
+
18
+ const layouts: NextpressComponent[] = [];
19
+
20
+ for (const file of files) {
21
+ if (!file.endsWith('.tsx')) continue;
22
+
23
+ const imported = await import(`@/app/_templates/components/${file}`);
24
+
25
+ const layout = imported.layout;
26
+ const component = imported.default;
27
+ if (!layout || !component) continue;
28
+
29
+ layouts.push({layout, Component: component});
30
+ }
31
+
32
+ return layouts;
33
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Autoloads ACF field groups dynamically from the field-groups directory.
6
+ * * **Requirements for it to work:**
7
+ * Each `.ts` file within the `src/app/_templates/components/field-groups/` directory MUST export the following:
8
+ * 1. `default` (Default Export): The configuration object for the ACF Field Group, typically defined using the `defineFieldGroup` function.
9
+ *
10
+ * @returns {Promise<NextpressFieldGroup[]>} A promise resolving to an array of loaded ACF field group configurations.
11
+ */
12
+ export async function acfFieldGroupAutoloader(): Promise<NextpressFieldGroup[]> {
13
+ const absolutePath = path.join(process.cwd(), 'src', 'app', '_templates', 'components', 'field-groups');
14
+ const files = fs.readdirSync(absolutePath);
15
+
16
+ const fieldGroups: NextpressFieldGroup[] = [];
17
+
18
+ for (const file of files) {
19
+ if (!file.endsWith('.ts')) continue;
20
+
21
+ const imported = await import(`@/app/_templates/components/field-groups/${file}`);
22
+ const fieldGroup = imported.default;
23
+
24
+ fieldGroups.push(fieldGroup);
25
+ }
26
+
27
+ return fieldGroups;
28
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Defines field group configuration and applies typing.
3
+ *
4
+ * @template T - Field group type extending NextpressFieldGroup.
5
+ * @param {T} layout - Field group configuration object.
6
+ * @returns {T} Passed field group configuration object.
7
+ */
8
+ export function defineFieldGroup<const T extends NextpressFieldGroup>(layout: T): T {
9
+ return layout;
10
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Defines layout configuration and applies typing.
3
+ *
4
+ * @template T - Layout type extending NextpressLayout.
5
+ * @param {T} layout - Layout configuration object.
6
+ * @returns {T} Passed layout configuration object.
7
+ */
8
+ export function defineLayout<const T extends NextpressLayout>(layout: T): T {
9
+ return layout;
10
+ }
@@ -0,0 +1,20 @@
1
+ import { ACFChoiceObject } from "@/acf-functions/types/components/field-props";
2
+
3
+ export function mapChoiceObject(return_format: 'array' | 'label' | 'value', value?: string, choices?: Choices): ACFChoiceObject | string | undefined {
4
+ if (return_format === 'value') {
5
+ return value;
6
+ }
7
+ if (!choices) return;
8
+
9
+ const label = choices[value ?? ''];
10
+
11
+ if (return_format === 'array') {
12
+ const result: ACFChoiceObject = {
13
+ label: label,
14
+ value: value
15
+ }
16
+ return result;
17
+ } else if (return_format === 'label') {
18
+ return label;
19
+ }
20
+ }
@@ -0,0 +1,359 @@
1
+ import { unserialize } from "php-serialize";
2
+ import { acfComponentAutoloader } from "../../core/acf-component-autoloader";
3
+ import { ACFGoogleMapsObject, ACFIconObject, ACFLinkObject } from "@/acf-functions/types/components/field-props";
4
+ import { mapChoiceObject } from "./helpers/map-choice-object";
5
+ import { processURL } from "@/services/utilities/process-url";
6
+
7
+ const components = await acfComponentAutoloader();
8
+
9
+ /**
10
+ * Parses PHP serialized string into JavaScript object or array.
11
+ *
12
+ * @param {string} [string] - PHP serialized string.
13
+ * @returns {unknown[] | { [key: string]: unknown }} Parsed object or array.
14
+ */
15
+ function parsePhp(string?: string): unknown[] | { [key: string]: unknown } {
16
+ return unserialize(string ?? 'a:0:{}') ?? [];
17
+ }
18
+
19
+ /**
20
+ * Gets array of object IDs from string value.
21
+ *
22
+ * @param {string} value - String value containing IDs.
23
+ * @param {boolean} multiple - Indicates if multiple IDs are expected.
24
+ * @returns {number[]} Array of parsed object IDs.
25
+ */
26
+ function getObjectIDs(value: string, multiple: boolean): number[] {
27
+ if (multiple) {
28
+ const postValueArray = parsePhp(value);
29
+ if (!Array.isArray(postValueArray)) return [];
30
+
31
+ return postValueArray.map(Number).filter(Boolean);
32
+ } else {
33
+ return [Number(value) ?? 0]
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Maps individual ACF field to corresponding value.
39
+ *
40
+ * @param {NextpressField} field - Field configuration.
41
+ * @param {ACFRawValues} rawValues - Raw values map.
42
+ * @returns {Promise<any>} Promise resolving to mapped value.
43
+ */
44
+ export async function mapField(field: NextpressField, rawValues: ACFRawValues): Promise<any> {
45
+ /**
46
+ * Basic scalar values and strings.
47
+ * These are stored as simple strings in the database and require no complex parsing.
48
+ */
49
+ switch (field.type){
50
+ case 'color_picker':
51
+ case 'date_picker':
52
+ case 'date_time_picker':
53
+ case 'time_picker':
54
+ case 'email':
55
+ case 'password':
56
+ case 'text':
57
+ case 'textarea':
58
+ case 'oembed':
59
+ case 'wysiwyg':
60
+ case 'file':
61
+ case 'image':
62
+ return rawValues.get(field.name);
63
+
64
+ /**
65
+ * Numeric fields.
66
+ * Casts the raw string from the database into a strict JavaScript Number.
67
+ */
68
+ case 'number':
69
+ case 'range':
70
+ return Number(rawValues.get(field.name));
71
+
72
+ /**
73
+ * True/False boolean field.
74
+ * Converts the truthy/falsy raw string/number into a strict JavaScript boolean.
75
+ */
76
+ case 'true_false':
77
+ return !!rawValues.get(field.name);
78
+
79
+ /**
80
+ * Google Map field.
81
+ * Deserializes the PHP array containing map data (address, lat, lng) into a structured object.
82
+ */
83
+ case 'google_map':
84
+ const mapValue = parsePhp(rawValues.get(field.name));
85
+ if (Array.isArray(mapValue)) return;
86
+
87
+ const mapObject: ACFGoogleMapsObject = mapValue as ACFGoogleMapsObject;
88
+ return mapObject;
89
+
90
+ /**
91
+ * Icon Picker field.
92
+ * Deserializes the PHP array and respects the return format (either a raw string class or an object).
93
+ */
94
+ case 'icon_picker':
95
+ const iconValue = parsePhp(rawValues.get(field.name));
96
+ if (Array.isArray(iconValue)) return;
97
+
98
+ const iconObject: ACFIconObject = iconValue as ACFIconObject;
99
+ if (field.return_format === 'string') {
100
+ return iconObject.value;
101
+ } else {
102
+ return iconObject;
103
+ }
104
+
105
+ /**
106
+ * Button Group field.
107
+ * Maps the selected choice value to its corresponding label/array using the choice map.
108
+ */
109
+ case 'button_group':
110
+ return mapChoiceObject(field.return_format ?? 'value', rawValues.get(field.name));
111
+
112
+ /**
113
+ * Checkbox field.
114
+ * Deserializes the array of selected options and maps each one to the requested return format.
115
+ */
116
+ case 'checkbox':
117
+ const checkBoxValues = parsePhp(rawValues.get(field.name));
118
+ if (!Array.isArray(checkBoxValues)) return;
119
+
120
+ return checkBoxValues.map(value => mapChoiceObject(field.return_format ?? 'value', typeof value === 'string' ? value : undefined, field.choices));
121
+
122
+ /**
123
+ * Radio field.
124
+ * Maps a single selected choice using the field's available choices.
125
+ */
126
+ case 'radio':
127
+ return mapChoiceObject(field.return_format ?? 'value', rawValues.get(field.name), field.choices);
128
+
129
+ /**
130
+ * Select field.
131
+ * Handles both multi-select (deserializes array) and single-select (processes raw string),
132
+ * applying the requested return format.
133
+ */
134
+ case 'select':
135
+ if (field.multiple === 1) {
136
+ const selectValues = parsePhp(rawValues.get(field.name));
137
+ if (!Array.isArray(selectValues)) return;
138
+
139
+ return selectValues.map(value => mapChoiceObject(field.return_format ?? 'value', typeof value === 'string' ? value : undefined, field.choices));
140
+ } else {
141
+ return mapChoiceObject(field.return_format ?? 'value', rawValues.get(field.name), field.choices);
142
+ }
143
+
144
+ /**
145
+ * Gallery field.
146
+ * Deserializes the PHP array of image IDs or objects.
147
+ */
148
+ case 'gallery':
149
+ const galleryValues = parsePhp(rawValues.get(field.name));
150
+ if (!Array.isArray(galleryValues)) return;
151
+
152
+ return galleryValues;
153
+
154
+ /**
155
+ * Flexible Content field.
156
+ * Iterates over saved layout names, extracts the relevant prefixed sub-fields for each block,
157
+ * recursively maps those sub-fields, and binds the resolved data to the correct React component.
158
+ */
159
+ case 'flexible_content':
160
+ const layoutValues = parsePhp(rawValues.get(field.name));
161
+ if (!Array.isArray(layoutValues)) return;
162
+
163
+ const layouts = field.layouts;
164
+ const fcRawEntries = Array.from(rawValues.entries());
165
+
166
+ const promises = layoutValues.map(async (layoutValue, index) => {
167
+ const layout = layouts?.find(layout => layout.name === layoutValue);
168
+
169
+ const prefix = `${field.name}_${index}_`;
170
+ const prefixLength = prefix.length;
171
+
172
+ const component = components.find(comp => comp.layout.name === layout?.name);
173
+
174
+ const subFieldEntries = fcRawEntries
175
+ .filter(([key]) => key.startsWith(prefix))
176
+ .map(([key, value]) => [key.slice(prefixLength), value] as [string, any]);
177
+
178
+ const flexibleValues = new Map<string, any>(subFieldEntries);
179
+
180
+ const resolvedValues = await mapLayout(layout as any, flexibleValues);
181
+
182
+ return {
183
+ Component: component?.Component,
184
+ content: resolvedValues
185
+ };
186
+ });
187
+
188
+ return Promise.all(promises);
189
+
190
+ /**
191
+ * Group field.
192
+ * Strips the parent group prefix from the database keys and recursively maps the internal sub-fields.
193
+ */
194
+ case 'group':
195
+ const groupValues = new Map(
196
+ [...rawValues.entries()]
197
+ .filter(([key]) => key.startsWith(field.name) && key !== field.name)
198
+ .map(([key, value]) => {
199
+ const newKey = key.replace(`${field.name}_`, '');
200
+ return [newKey, value];
201
+ })
202
+ );
203
+ return await mapLayout(field as any, groupValues);
204
+
205
+ /**
206
+ * Repeater field.
207
+ * Gets the total repeat count, then iterates to extract prefixed sub-fields for each row,
208
+ * mapping them recursively into an array of objects.
209
+ */
210
+ case 'repeater':
211
+ const repeaterRepeats = Number(rawValues.get(field.name)) || 0;
212
+ const repeaterResults = [];
213
+
214
+ const rRawEntries = Array.from(rawValues.entries());
215
+
216
+ for (let index = 0; index < repeaterRepeats; index++) {
217
+ const prefix = `${field.name}_${index}_`;
218
+ const prefixLength = prefix.length;
219
+
220
+ const subFieldEntries = rRawEntries
221
+ .filter(([key]) => key.startsWith(prefix))
222
+ .map(([key, value]) => [key.slice(prefixLength), value] as [string, any]);
223
+
224
+ const repeatValues = new Map<string, any>(subFieldEntries);
225
+
226
+ repeaterResults.push(mapLayout(field as any, repeatValues));
227
+ }
228
+
229
+ return Promise.all(repeaterResults);
230
+
231
+ /**
232
+ * Link field.
233
+ * Deserializes link properties (title, url, target) and returns either the raw URL string or a structured object.
234
+ */
235
+ case 'link':
236
+ const linkValue = parsePhp(rawValues.get(field.name));
237
+ if (Array.isArray(linkValue)) return;
238
+
239
+ const linkObject: ACFLinkObject = {
240
+ title: typeof linkValue.title === 'string' ? linkValue.title : '',
241
+ url: typeof linkValue.url === 'string' ? processURL(linkValue.url) : '',
242
+ target: typeof linkValue.target === 'string' ? linkValue.target : '',
243
+ }
244
+
245
+ if (field.return_format === 'url') {
246
+ return linkObject.url;
247
+ } else {
248
+ return linkObject
249
+ }
250
+
251
+ /**
252
+ * Page Link field.
253
+ * Retrieves internal post paths dynamically based on post ID, or falls back to standard external URLs.
254
+ */
255
+ case 'page_link':
256
+ if (field.multiple == 1) {
257
+ const pageLinkValue = parsePhp(rawValues.get(field.name));
258
+ if (!Array.isArray(pageLinkValue)) return;
259
+ const pageLinkIds = pageLinkValue.map(Number).filter(Boolean);
260
+
261
+ const posts = await getPosts(pageLinkIds);
262
+ const postPaths = new Map(posts.map(post => [post.ID, post.path]));
263
+
264
+ return pageLinkValue.map(page => postPaths.has(Number(page)) ? postPaths.get(Number(page)) : processURL(page as string));
265
+ } else {
266
+ const pageLinkValue = rawValues.get(field.name);
267
+ const pageLinkId = Number(rawValues.get(field.name));
268
+ if (!pageLinkValue) return;
269
+
270
+ return pageLinkId ? (await getPost(pageLinkId))?.path : processURL(pageLinkValue);
271
+ }
272
+
273
+ /**
274
+ * Post Object field.
275
+ * Returns either the raw Post IDs or uses the Entity Loader to fetch and return the complete post models.
276
+ */
277
+ case 'post_object':
278
+ const postObjectIds: number[] = getObjectIDs(rawValues.get(field.name) ?? '', !!field.multiple)
279
+
280
+ if (field.return_format === 'id') {
281
+ return field.multiple ? postObjectIds : postObjectIds[0];
282
+ } else {
283
+ postLoader.prime(postObjectIds);
284
+ const posts = await Promise.all(postObjectIds.map(id => getPost(id)));
285
+ return field.multiple ? posts : posts[0];
286
+ }
287
+
288
+ /**
289
+ * Relationship field.
290
+ * Extracts multiple selected IDs from a PHP array, returning them directly or fetching the associated post models.
291
+ */
292
+ case 'relationship':
293
+ const relationshipArray = parsePhp(rawValues.get(field.name));
294
+ if (!Array.isArray(relationshipArray)) return [];
295
+
296
+ const relationshipIds = relationshipArray.map(Number).filter(Boolean);
297
+
298
+ if (field.return_format === 'id') {
299
+ return relationshipIds;
300
+ } {
301
+ postLoader.prime(relationshipIds);
302
+ return await Promise.all(relationshipIds.map(id => getPost(id)));
303
+ }
304
+
305
+ /**
306
+ * Taxonomy field.
307
+ * Parses selected Term IDs and optionally fetches the complete taxonomy Term objects from the database.
308
+ */
309
+ case 'taxonomy':
310
+ const termObjectIds = parsePhp(rawValues.get(field.name))
311
+ if (!Array.isArray(termObjectIds)) return [];
312
+
313
+ const termIds = termObjectIds.map(Number).filter(Boolean);
314
+
315
+ if (field.return_format === 'id') {
316
+ return field.multiple ? termIds : termIds[0];
317
+ } else {
318
+ const terms = await Promise.all(termIds.map(id => getTerm(id)));
319
+ return field.multiple ? terms : terms[0];
320
+ }
321
+
322
+ /**
323
+ * User field.
324
+ * Extracts User IDs and returns either the raw IDs or fetches and returns the full User entity objects.
325
+ */
326
+ case 'user':
327
+ const userObjectIds: number[] = getObjectIDs(rawValues.get(field.name) ?? '', !!field.multiple)
328
+
329
+ if (field.return_format === 'id') {
330
+ return field.multiple ? userObjectIds : userObjectIds[0];
331
+ } else {
332
+ const users = await Promise.all(userObjectIds.map(id => getUser(id)));
333
+ return field.multiple ? users : users[0];
334
+ }
335
+ }
336
+
337
+ return undefined;
338
+ }
339
+
340
+ /**
341
+ * Maps layout configuration to values object.
342
+ *
343
+ * @param {NextpressLayout} layout - Layout configuration.
344
+ * @param {ACFRawValues} rawValues - Raw values map.
345
+ * @returns {Promise<{ [key: string]: any }>} Promise resolving to layout values object.
346
+ */
347
+ export async function mapLayout(layout: NextpressLayout, rawValues: ACFRawValues) {
348
+ const values: { [key: string]: any } = {};
349
+
350
+ for (const subField of layout.sub_fields) {
351
+ try {
352
+ values[subField.name] = await mapField(subField, rawValues)
353
+ } catch (error: any) {
354
+ console.warn('Failed to map sub field: ', subField.name, error.message);
355
+ }
356
+ }
357
+
358
+ return values;
359
+ }
@@ -0,0 +1,118 @@
1
+ type TransformACFFieldGroup<T> = T extends any
2
+ ? Omit<T, 'key' | 'fields'> &
3
+ ('fields' extends keyof T ? { fields: NextpressField[] } : [])
4
+ : never;
5
+
6
+ /** ACF Field Group without key constraints to allow key generation */
7
+ type NextpressFieldGroup = TransformACFFieldGroup<ACFFieldGroup>
8
+
9
+ /**
10
+ * Represents an Advanced Custom Fields (ACF) Field Group configuration.
11
+ */
12
+ interface ACFFieldGroup {
13
+ /**
14
+ * Unique identifier for the field group with group_ prefix (e.g. 'group_abc123')
15
+ */
16
+ key: string
17
+ /**
18
+ * The title/name of this entity
19
+ */
20
+ title: string
21
+ /**
22
+ * Array of field definitions. If empty array, field group appears but contains no fields.
23
+ */
24
+ fields: ACFField[]
25
+ /**
26
+ * The order of this entity in the admin menu
27
+ */
28
+ menu_order?: number
29
+ /**
30
+ * [ACF] Whether this entity is active
31
+ */
32
+ active?: boolean
33
+ /**
34
+ * [ACF Export Only] Unix timestamp of last modification
35
+ */
36
+ modified?: number
37
+ /**
38
+ * Location rules determining where this field group appears. Array of rule groups (OR logic between groups, AND logic within groups). If omitted or empty, field group won't appear on any edit screens.
39
+ */
40
+ location?: ACFLocationGroup[]
41
+ /**
42
+ * Position of the field group on the edit screen
43
+ */
44
+ position?: "normal" | "side" | "acf_after_title"
45
+ /**
46
+ * Visual style of the field group metabox
47
+ */
48
+ style?: "default" | "seamless"
49
+ /**
50
+ * Placement of field labels relative to fields
51
+ */
52
+ label_placement?: "top" | "left"
53
+ /**
54
+ * Placement of field instructions
55
+ */
56
+ instruction_placement?: "label" | "field"
57
+ /**
58
+ * Screen elements to hide when this field group is displayed
59
+ */
60
+ hide_on_screen?: (
61
+ | "permalink"
62
+ | "the_content"
63
+ | "excerpt"
64
+ | "custom_fields"
65
+ | "discussion"
66
+ | "comments"
67
+ | "revisions"
68
+ | "slug"
69
+ | "author"
70
+ | "format"
71
+ | "page_attributes"
72
+ | "featured_image"
73
+ | "categories"
74
+ | "tags"
75
+ | "send-trackbacks"
76
+ )[]
77
+ /**
78
+ * Description of the field group
79
+ */
80
+ description?: string
81
+ /**
82
+ * Whether to expose this field group's fields in the REST API
83
+ */
84
+ show_in_rest?: boolean | number
85
+ /**
86
+ * Whether to expose this field group to AI integrations
87
+ */
88
+ allow_ai_access?: boolean | number
89
+ /**
90
+ * Description that helps AI integrations understand how to use this field group
91
+ */
92
+ ai_description?: string
93
+ /**
94
+ * Alternative display title for the field group
95
+ */
96
+ display_title?: string
97
+ }
98
+
99
+ /**
100
+ * Group of location rules (AND logic within group)
101
+ */
102
+ type ACFLocationGroup = [ACFLocationRule, ...ACFLocationRule[]]
103
+
104
+ interface ACFLocationRule {
105
+ /**
106
+ * The parameter to compare (e.g. 'post_type', 'page_template', 'user_role')
107
+ */
108
+ param: string
109
+ /**
110
+ * Comparison operator
111
+ */
112
+ operator: "==" | "!="
113
+ /**
114
+ * Value to compare against
115
+ */
116
+ value: string
117
+ [k: string]: unknown
118
+ }