@zeropress/build-core 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  ![license](https://img.shields.io/npm/l/%40zeropress%2Fbuild-core)
5
5
  ![node](https://img.shields.io/node/v/%40zeropress%2Fbuild-core)
6
6
 
7
- Shared build and partial-render core for ZeroPress.
7
+ Shared build core for ZeroPress.
8
8
 
9
9
  This package is the deterministic rendering engine used by:
10
10
 
@@ -33,7 +33,6 @@ npm install @zeropress/build-core
33
33
  ```js
34
34
  import {
35
35
  buildSite,
36
- buildSelectedRoutes,
37
36
  buildSiteFromThemeDir,
38
37
  MemoryWriter,
39
38
  FilesystemWriter,
@@ -123,47 +122,6 @@ Notes:
123
122
  - `sitemap.xml` and `feed.xml` are emitted only when `site.url` is a non-empty canonical URL
124
123
  - `robots.txt` and `meta.json` are still emitted when `generateSpecialFiles` is enabled
125
124
 
126
- ### `buildSelectedRoutes(input)`
127
-
128
- Renders only selected post and list routes while preserving full-build parity for those outputs.
129
-
130
- This is intended for targeted rebuilds, output parity checks, and other partial-render workflows.
131
-
132
- Current selection scope is limited to:
133
-
134
- - post detail routes
135
- - index pagination routes
136
- - archive routes
137
- - category routes
138
- - tag routes
139
-
140
- It does not currently support selecting individual page routes.
141
-
142
- Optional route templates are respected during both full and partial renders:
143
-
144
- - missing `archive.html` skips archive outputs
145
- - missing `category.html` skips category outputs
146
- - missing `tag.html` skips tag outputs
147
- - missing `404.html` skips `404.html` emission
148
-
149
- These cases do not fail the build.
150
-
151
- ```js
152
- await buildSelectedRoutes({
153
- previewData,
154
- themePackage,
155
- writer,
156
- selection: {
157
- posts: ['hello-zeropress'],
158
- indexRoutes: ['/'],
159
- archiveRoutes: ['/archive/'],
160
- categoryRoutes: ['/categories/general/'],
161
- tagRoutes: ['/tags/intro/'],
162
- includeAssets: false,
163
- },
164
- });
165
- ```
166
-
167
125
  ### `buildSiteFromThemeDir(input)`
168
126
 
169
127
  Loads a theme directory from disk and renders it using the same core pipeline.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.1.10",
4
- "description": "Shared ZeroPress build and partial-render core",
3
+ "version": "0.1.11",
4
+ "description": "Shared ZeroPress build core",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -33,8 +33,8 @@
33
33
  "node": ">=18.18.0"
34
34
  },
35
35
  "dependencies": {
36
- "@zeropress/preview-data-validator": "^0.4.0",
37
- "@zeropress/theme-validator": "^0.2.4",
36
+ "@zeropress/preview-data-validator": "^0.4.1",
37
+ "@zeropress/theme-validator": "^0.3.0",
38
38
  "highlight.js": "^11.11.1",
39
39
  "markdown-it": "^14.1.0",
40
40
  "markdown-it-anchor": "^9.2.0"
@@ -68,62 +68,6 @@ export async function buildSite(input) {
68
68
  return finalizeBuildResult(state.writer, state.summaries, options);
69
69
  }
70
70
 
71
- export async function buildSelectedRoutes(input) {
72
- const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
73
- validateSelection(input.selection);
74
-
75
- const state = await createBuildState(input, options);
76
- const selectedPostSlugs = new Set((input.selection.posts || []).map((slug) => String(slug)));
77
- const selectedIndexRoutes = new Set((input.selection.indexRoutes || []).map((route) => normalizeRoutePath(route)));
78
- const selectedArchiveRoutes = new Set((input.selection.archiveRoutes || []).map((route) => normalizeRoutePath(route)));
79
- const selectedCategoryRoutes = new Set((input.selection.categoryRoutes || []).map((route) => normalizeRoutePath(route)));
80
- const selectedTagRoutes = new Set((input.selection.tagRoutes || []).map((route) => normalizeRoutePath(route)));
81
-
82
- for (const post of state.renderData.posts) {
83
- if (selectedPostSlugs.has(post.slug)) {
84
- await renderPost(state, post);
85
- }
86
- }
87
-
88
- for (const route of state.renderData.indexRoutes) {
89
- if (selectedIndexRoutes.has(normalizeRoutePath(route.path))) {
90
- await renderRoute(state, 'index', route);
91
- }
92
- }
93
-
94
- if (hasTemplate(state, 'archive')) {
95
- for (const route of state.renderData.archiveRoutes) {
96
- if (selectedArchiveRoutes.has(normalizeRoutePath(route.path))) {
97
- await renderRoute(state, 'archive', route);
98
- }
99
- }
100
- }
101
-
102
- if (hasTemplate(state, 'category')) {
103
- for (const route of state.renderData.categoryRoutes) {
104
- if (selectedCategoryRoutes.has(normalizeRoutePath(route.path))) {
105
- await renderRoute(state, 'category', route);
106
- }
107
- }
108
- }
109
-
110
- if (hasTemplate(state, 'tag')) {
111
- for (const route of state.renderData.tagRoutes) {
112
- if (selectedTagRoutes.has(normalizeRoutePath(route.path))) {
113
- await renderRoute(state, 'tag', route);
114
- }
115
- }
116
- }
117
-
118
- if (input.selection.includeAssets) {
119
- for (const assetOutput of state.assetOutputs) {
120
- await writeOutput(state.writer, state.summaries, assetOutput.path, assetOutput.content, assetOutput.contentType);
121
- }
122
- }
123
-
124
- return finalizeBuildResult(state.writer, state.summaries, options);
125
- }
126
-
127
71
  async function createBuildState(input, options) {
128
72
  if (!input?.writer || typeof input.writer.write !== 'function') {
129
73
  throw new Error('buildSite requires a writer with an async write(file) method');
@@ -194,6 +138,7 @@ async function renderRoute(state, templateName, route) {
194
138
  let html = await state.engine.render(
195
139
  templateName,
196
140
  {
141
+ menus: state.previewData.menus,
197
142
  ...route,
198
143
  meta: buildPageMeta(state.previewData.site, {
199
144
  currentUrl,
@@ -214,6 +159,7 @@ async function renderPost(state, post) {
214
159
  let html = await state.engine.render(
215
160
  'post',
216
161
  {
162
+ menus: state.previewData.menus,
217
163
  post,
218
164
  meta: buildPageMeta(state.previewData.site, {
219
165
  currentUrl,
@@ -247,6 +193,7 @@ async function renderPage(state, page) {
247
193
  let html = await state.engine.render(
248
194
  'page',
249
195
  {
196
+ menus: state.previewData.menus,
250
197
  page,
251
198
  meta: buildPageMeta(state.previewData.site, {
252
199
  currentUrl,
@@ -276,6 +223,7 @@ async function maybeRenderNotFoundPage(state) {
276
223
  let html = await state.engine.render(
277
224
  '404',
278
225
  {
226
+ menus: state.previewData.menus,
279
227
  meta: buildPageMeta(state.previewData.site, {
280
228
  currentUrl: '/404.html',
281
229
  title: state.previewData.site.title,
@@ -289,23 +237,6 @@ async function maybeRenderNotFoundPage(state) {
289
237
  await writeOutput(state.writer, state.summaries, '404.html', html, 'text/html');
290
238
  }
291
239
 
292
- function validateSelection(selection) {
293
- if (!selection || typeof selection !== 'object') {
294
- throw new Error('buildSelectedRoutes requires a selection object');
295
- }
296
-
297
- const keys = ['posts', 'indexRoutes', 'archiveRoutes', 'categoryRoutes', 'tagRoutes'];
298
- for (const key of keys) {
299
- if (!Array.isArray(selection[key])) {
300
- throw new Error(`buildSelectedRoutes selection.${key} must be an array`);
301
- }
302
- }
303
-
304
- if (typeof selection.includeAssets !== 'boolean') {
305
- throw new Error('buildSelectedRoutes selection.includeAssets must be a boolean');
306
- }
307
- }
308
-
309
240
  function normalizePreviewData(previewData) {
310
241
  const normalizedSite = {
311
242
  ...previewData.site,
package/src/index.d.ts CHANGED
@@ -8,10 +8,14 @@ export interface ThemePackage {
8
8
  description?: string;
9
9
  thumbnail?: string;
10
10
  settings?: Record<string, unknown>;
11
+ menuSlots?: Record<string, {
12
+ title: string;
13
+ description?: string;
14
+ }>;
11
15
  namespace?: string;
12
16
  slug?: string;
13
17
  license?: 'MIT' | 'Apache-2.0' | 'BSD-3-Clause' | 'GPL-3.0-only' | 'GPL-3.0-or-later';
14
- runtime?: '0.2';
18
+ runtime?: '0.3';
15
19
  };
16
20
  templates: Map<string, string>;
17
21
  partials: Map<string, string>;
@@ -35,15 +39,6 @@ export interface BuildOptions {
35
39
  writeManifest?: boolean;
36
40
  }
37
41
 
38
- export interface BuildSelection {
39
- posts: string[];
40
- indexRoutes: string[];
41
- archiveRoutes: string[];
42
- categoryRoutes: string[];
43
- tagRoutes: string[];
44
- includeAssets: boolean;
45
- }
46
-
47
42
  export interface BuildSummaryFile {
48
43
  path: string;
49
44
  contentType: string;
@@ -68,14 +63,6 @@ export function buildSite(input: {
68
63
  options?: BuildOptions;
69
64
  }): Promise<BuildSiteResult>;
70
65
 
71
- export function buildSelectedRoutes(input: {
72
- previewData: PreviewDataV05;
73
- themePackage: ThemePackage;
74
- writer: BuildWriter;
75
- selection: BuildSelection;
76
- options?: BuildOptions;
77
- }): Promise<BuildSiteResult>;
78
-
79
66
  export function buildSiteFromThemeDir(input: {
80
67
  previewData: PreviewDataV05;
81
68
  themeDir: string;
package/src/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export { buildSite } from './api/build-site.js';
2
- export { buildSelectedRoutes } from './api/build-site.js';
3
2
  export { buildSiteFromThemeDir } from './api/build-site-from-theme-dir.js';
4
3
  export { MemoryWriter } from './output/memory-writer.js';
5
4
  export { FilesystemWriter } from './output/filesystem-writer.js';
@@ -1,17 +1,15 @@
1
1
  export class VariableResolver {
2
- static VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
2
+ static MENU_PATTERN = /\{\{menu:([a-z][a-z0-9_-]{0,63})\}\}/g;
3
+ static VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\}\}/g;
3
4
  static STANDALONE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
4
5
 
5
6
  resolve(template, data) {
6
- let result = template.replace(VariableResolver.VARIABLE_PATTERN, (_, variablePath) => {
7
- const [prefix, field] = variablePath.split('.');
8
- const prefixData = data[prefix];
9
-
10
- if (!prefixData || typeof prefixData !== 'object') {
11
- return '';
12
- }
7
+ let result = template.replace(VariableResolver.MENU_PATTERN, (_, menuId) => {
8
+ return this.renderMenu(data?.menus?.[menuId]);
9
+ });
13
10
 
14
- const value = prefixData[field];
11
+ result = result.replace(VariableResolver.VARIABLE_PATTERN, (_, variablePath) => {
12
+ const value = this.resolvePath(data, variablePath);
15
13
  return value == null ? '' : String(value);
16
14
  });
17
15
 
@@ -26,4 +24,51 @@ export class VariableResolver {
26
24
 
27
25
  return result;
28
26
  }
27
+
28
+ resolvePath(data, variablePath) {
29
+ const segments = variablePath.split('.');
30
+ let current = data;
31
+
32
+ for (const segment of segments) {
33
+ if (current == null || typeof current !== 'object') {
34
+ return undefined;
35
+ }
36
+
37
+ current = current[segment];
38
+ }
39
+
40
+ return current;
41
+ }
42
+
43
+ renderMenu(menu) {
44
+ if (!menu || !Array.isArray(menu.items) || menu.items.length === 0) {
45
+ return '';
46
+ }
47
+
48
+ return `<ul>${menu.items.map((item) => this.renderMenuItem(item)).join('')}</ul>`;
49
+ }
50
+
51
+ renderMenuItem(item) {
52
+ if (!item || typeof item !== 'object') {
53
+ return '';
54
+ }
55
+
56
+ const title = this.escapeHtml(String(item.title || ''));
57
+ const url = this.escapeHtml(String(item.url || ''));
58
+ const target = item.target === '_blank' ? '_blank' : '_self';
59
+ const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
60
+ const children = Array.isArray(item.children) && item.children.length > 0
61
+ ? `<ul>${item.children.map((child) => this.renderMenuItem(child)).join('')}</ul>`
62
+ : '';
63
+
64
+ return `<li><a href="${url}" target="${target}"${rel}>${title}</a>${children}</li>`;
65
+ }
66
+
67
+ escapeHtml(value) {
68
+ return value
69
+ .replaceAll('&', '&amp;')
70
+ .replaceAll('<', '&lt;')
71
+ .replaceAll('>', '&gt;')
72
+ .replaceAll('"', '&quot;');
73
+ }
29
74
  }