@deneb-ui/cli 2.0.34 → 2.0.36
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/package.json +2 -2
- package/src/common/fivora-site-data.ts +339 -0
- package/src/common/media.constants.ts +92 -0
- package/src/common/template-editor-schema.ts +2280 -0
- package/src/common/template-package-archive.ts +176 -0
- package/src/common/template-package-manifest.ts +272 -0
- package/src/common/template-visual-edit-contract.ts +2277 -0
- package/src/common/visual-customization.ts +679 -0
- package/src/sites/universal-page-selection.ts +520 -0
- package/src/tools/deneb-template-validator.cjs +23 -17
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const UNIVERSAL_PAGE_SELECTION_STYLE_ID =
|
|
5
|
+
'fivora-page-selection-style';
|
|
6
|
+
export const UNIVERSAL_PAGE_SELECTION_SCRIPT_ID =
|
|
7
|
+
'fivora-page-selection-script';
|
|
8
|
+
|
|
9
|
+
export type FivoraTemplatePageDefinition = {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
route: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function getTemplateValidationSelectedPages(
|
|
16
|
+
pages: Array<{ id: string; required?: boolean }>,
|
|
17
|
+
) {
|
|
18
|
+
const selected = pages
|
|
19
|
+
.filter((page, index) => page.required === true || index === 0)
|
|
20
|
+
.map((page) => page.id);
|
|
21
|
+
return selected.length > 0
|
|
22
|
+
? selected
|
|
23
|
+
: pages.slice(0, 1).map((page) => page.id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeRoute(value: string, pageId: string, isFirstPage: boolean) {
|
|
31
|
+
const route = value.trim().split(/[?#]/, 1)[0];
|
|
32
|
+
if (route === '/' || route === '') return isFirstPage ? '/' : `/${pageId}`;
|
|
33
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(route) || route.startsWith('//')) {
|
|
34
|
+
return isFirstPage ? '/' : `/${pageId}`;
|
|
35
|
+
}
|
|
36
|
+
return `/${route.replace(/^\/+|\/+$/g, '')}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeTemplatePageDefinitions(
|
|
40
|
+
manifest: unknown,
|
|
41
|
+
fallbackPageIds: string[] = [],
|
|
42
|
+
): FivoraTemplatePageDefinition[] {
|
|
43
|
+
const manifestPages =
|
|
44
|
+
isRecord(manifest) && Array.isArray(manifest.pages) ? manifest.pages : [];
|
|
45
|
+
const definitions = manifestPages.flatMap((page, index) => {
|
|
46
|
+
if (!isRecord(page) || typeof page.id !== 'string' || !page.id.trim()) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const id = page.id.trim();
|
|
50
|
+
const label =
|
|
51
|
+
typeof page.label === 'string' && page.label.trim()
|
|
52
|
+
? page.label.trim()
|
|
53
|
+
: id.replace(/_/g, ' ');
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
id,
|
|
57
|
+
label,
|
|
58
|
+
route: normalizeRoute(
|
|
59
|
+
typeof page.route === 'string' ? page.route : '',
|
|
60
|
+
id,
|
|
61
|
+
index === 0,
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
});
|
|
66
|
+
if (definitions.length > 0) return definitions;
|
|
67
|
+
|
|
68
|
+
const fallback = fallbackPageIds
|
|
69
|
+
.map((id) => id.trim())
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((id, index) => ({
|
|
72
|
+
id,
|
|
73
|
+
label: id.replace(/_/g, ' '),
|
|
74
|
+
route: index === 0 ? '/' : `/${id}`,
|
|
75
|
+
}));
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Hides navigation and teaser UI that targets unselected pages. Keep this
|
|
81
|
+
* function self-contained because it is serialized into preview/final HTML.
|
|
82
|
+
*/
|
|
83
|
+
export function enforceSelectedTemplatePages(
|
|
84
|
+
siteDataValue: unknown,
|
|
85
|
+
documentValue?: Document,
|
|
86
|
+
) {
|
|
87
|
+
const doc = documentValue ?? document;
|
|
88
|
+
const record =
|
|
89
|
+
siteDataValue &&
|
|
90
|
+
typeof siteDataValue === 'object' &&
|
|
91
|
+
!Array.isArray(siteDataValue)
|
|
92
|
+
? (siteDataValue as Record<string, unknown>)
|
|
93
|
+
: null;
|
|
94
|
+
const requirements =
|
|
95
|
+
record?.requirements &&
|
|
96
|
+
typeof record.requirements === 'object' &&
|
|
97
|
+
!Array.isArray(record.requirements)
|
|
98
|
+
? (record.requirements as Record<string, unknown>)
|
|
99
|
+
: null;
|
|
100
|
+
const template =
|
|
101
|
+
record?.template &&
|
|
102
|
+
typeof record.template === 'object' &&
|
|
103
|
+
!Array.isArray(record.template)
|
|
104
|
+
? (record.template as Record<string, unknown>)
|
|
105
|
+
: null;
|
|
106
|
+
const structure =
|
|
107
|
+
template?.structure &&
|
|
108
|
+
typeof template.structure === 'object' &&
|
|
109
|
+
!Array.isArray(template.structure)
|
|
110
|
+
? (template.structure as Record<string, unknown>)
|
|
111
|
+
: null;
|
|
112
|
+
const hasRequiredPages = Array.isArray(requirements?.requiredPages);
|
|
113
|
+
const hasStructurePages = Array.isArray(structure?.pages);
|
|
114
|
+
if (!hasRequiredPages && !hasStructurePages) return;
|
|
115
|
+
|
|
116
|
+
const selectedSource = hasRequiredPages
|
|
117
|
+
? (requirements?.requiredPages as unknown[])
|
|
118
|
+
: (structure?.pages as unknown[]);
|
|
119
|
+
const selected = new Set(
|
|
120
|
+
selectedSource
|
|
121
|
+
.filter((value): value is string => typeof value === 'string')
|
|
122
|
+
.map((value) => value.trim())
|
|
123
|
+
.filter(Boolean),
|
|
124
|
+
);
|
|
125
|
+
const templateDefinitions = Array.isArray(template?.pageDefinitions)
|
|
126
|
+
? template.pageDefinitions
|
|
127
|
+
: [];
|
|
128
|
+
const structureDefinitions = Array.isArray(structure?.pageDefinitions)
|
|
129
|
+
? structure.pageDefinitions
|
|
130
|
+
: [];
|
|
131
|
+
const providedDefinitions =
|
|
132
|
+
templateDefinitions.length > 0 ? templateDefinitions : structureDefinitions;
|
|
133
|
+
const definitions = providedDefinitions.flatMap((page) => {
|
|
134
|
+
if (!page || typeof page !== 'object' || Array.isArray(page)) return [];
|
|
135
|
+
const value = page as Record<string, unknown>;
|
|
136
|
+
if (typeof value.id !== 'string' || !value.id.trim()) return [];
|
|
137
|
+
const id = value.id.trim();
|
|
138
|
+
const label =
|
|
139
|
+
typeof value.label === 'string' && value.label.trim()
|
|
140
|
+
? value.label.trim()
|
|
141
|
+
: id.replace(/_/g, ' ');
|
|
142
|
+
const rawRoute = typeof value.route === 'string' ? value.route.trim() : '';
|
|
143
|
+
const route =
|
|
144
|
+
rawRoute === '/' ? '/' : `/${(rawRoute || id).replace(/^\/+|\/+$/g, '')}`;
|
|
145
|
+
return [{ id, label, route }];
|
|
146
|
+
});
|
|
147
|
+
const fallbackPageSource = hasStructurePages
|
|
148
|
+
? (structure?.pages as unknown[])
|
|
149
|
+
: selectedSource;
|
|
150
|
+
const fallbackDefinitions = fallbackPageSource
|
|
151
|
+
.filter((value): value is string => typeof value === 'string')
|
|
152
|
+
.map((value) => value.trim())
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.map((id, index) => ({
|
|
155
|
+
id,
|
|
156
|
+
label: id.replace(/_/g, ' '),
|
|
157
|
+
route: index === 0 ? '/' : `/${id}`,
|
|
158
|
+
}));
|
|
159
|
+
const pages = definitions.length > 0 ? definitions : fallbackDefinitions;
|
|
160
|
+
if (pages.length === 0) return;
|
|
161
|
+
const disabledPages = pages.filter((page) => !selected.has(page.id));
|
|
162
|
+
if (disabledPages.length === 0) {
|
|
163
|
+
doc
|
|
164
|
+
.querySelectorAll('[data-fivora-page-disabled]')
|
|
165
|
+
.forEach((element) =>
|
|
166
|
+
element.removeAttribute('data-fivora-page-disabled'),
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let style = doc.getElementById('fivora-page-selection-style');
|
|
172
|
+
if (!style || style.tagName !== 'STYLE') {
|
|
173
|
+
style = doc.createElement('style');
|
|
174
|
+
style.id = 'fivora-page-selection-style';
|
|
175
|
+
style.textContent =
|
|
176
|
+
'[data-fivora-page-disabled="true"]{display:none!important}';
|
|
177
|
+
doc.head.appendChild(style);
|
|
178
|
+
}
|
|
179
|
+
doc
|
|
180
|
+
.querySelectorAll('[data-fivora-page-disabled]')
|
|
181
|
+
.forEach((element) =>
|
|
182
|
+
element.removeAttribute('data-fivora-page-disabled'),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const normalizePath = (value: string) => {
|
|
186
|
+
let pathname = value;
|
|
187
|
+
try {
|
|
188
|
+
const url = new URL(value, doc.baseURI);
|
|
189
|
+
if (url.origin !== doc.location?.origin) return '';
|
|
190
|
+
pathname = url.pathname;
|
|
191
|
+
} catch {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
const previewRoot = pathname.match(
|
|
195
|
+
/^(\/uploads\/generated-sites\/(?:template-preview|preview|live)\/[^/]+)/,
|
|
196
|
+
)?.[1];
|
|
197
|
+
if (previewRoot && pathname.startsWith(previewRoot)) {
|
|
198
|
+
pathname = pathname.slice(previewRoot.length) || '/';
|
|
199
|
+
}
|
|
200
|
+
pathname = pathname.replace(/\/index\.html$/i, '/').replace(/\.html$/i, '');
|
|
201
|
+
const clean = pathname.replace(/\/+$/g, '') || '/';
|
|
202
|
+
return clean.startsWith('/') ? clean : `/${clean}`;
|
|
203
|
+
};
|
|
204
|
+
const pageForPath = (path: string) =>
|
|
205
|
+
pages.find((page) => {
|
|
206
|
+
const route = normalizePath(page.route);
|
|
207
|
+
if (!route) return false;
|
|
208
|
+
if (route === '/') return path === '/';
|
|
209
|
+
return path === route || path.startsWith(`${route}/`);
|
|
210
|
+
});
|
|
211
|
+
const pageForControl = (element: Element) => {
|
|
212
|
+
for (const attribute of [
|
|
213
|
+
'href',
|
|
214
|
+
'formaction',
|
|
215
|
+
'data-href',
|
|
216
|
+
'data-route',
|
|
217
|
+
'data-url',
|
|
218
|
+
]) {
|
|
219
|
+
const destination = element.getAttribute(attribute);
|
|
220
|
+
if (!destination) continue;
|
|
221
|
+
const page = pageForPath(normalizePath(destination));
|
|
222
|
+
if (page) return page;
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
};
|
|
226
|
+
const disable = (element: Element | null) => {
|
|
227
|
+
if (element) element.setAttribute('data-fivora-page-disabled', 'true');
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
for (const page of disabledPages) {
|
|
231
|
+
for (const attribute of [
|
|
232
|
+
'data-page-key',
|
|
233
|
+
'data-required-page',
|
|
234
|
+
'data-target-page',
|
|
235
|
+
]) {
|
|
236
|
+
doc.querySelectorAll(`[${attribute}]`).forEach((element) => {
|
|
237
|
+
if (element.getAttribute(attribute) === page.id) disable(element);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const anchors = Array.from(
|
|
243
|
+
doc.querySelectorAll<HTMLAnchorElement>('a[href]'),
|
|
244
|
+
);
|
|
245
|
+
for (const anchor of anchors) {
|
|
246
|
+
const targetPage = pageForControl(anchor);
|
|
247
|
+
if (!targetPage || selected.has(targetPage.id)) continue;
|
|
248
|
+
const fullCardLink =
|
|
249
|
+
anchor.classList.contains('absolute') &&
|
|
250
|
+
(anchor.classList.contains('inset-0') ||
|
|
251
|
+
(anchor.classList.contains('inset-x-0') &&
|
|
252
|
+
anchor.classList.contains('inset-y-0')));
|
|
253
|
+
if (fullCardLink) {
|
|
254
|
+
disable(
|
|
255
|
+
anchor.closest(
|
|
256
|
+
'[data-preview-item-path],[data-design-card],article,li',
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
const listItem = anchor.closest('li');
|
|
261
|
+
disable(
|
|
262
|
+
listItem && listItem.querySelectorAll('a[href]').length === 1
|
|
263
|
+
? listItem
|
|
264
|
+
: anchor,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const buttons = Array.from(
|
|
270
|
+
doc.querySelectorAll<HTMLElement>(
|
|
271
|
+
'button,[role="button"],[data-href],[data-route],[data-url]',
|
|
272
|
+
),
|
|
273
|
+
).filter((element) => element.tagName !== 'A');
|
|
274
|
+
for (const button of buttons) {
|
|
275
|
+
const targetPage = pageForControl(button);
|
|
276
|
+
if (!targetPage || selected.has(targetPage.id)) continue;
|
|
277
|
+
const listItem = button.closest('li');
|
|
278
|
+
disable(
|
|
279
|
+
listItem &&
|
|
280
|
+
listItem.querySelectorAll(
|
|
281
|
+
'a[href],button,[role="button"],[data-href],[data-route],[data-url]',
|
|
282
|
+
).length === 1
|
|
283
|
+
? listItem
|
|
284
|
+
: button,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const firstMainSection = doc.querySelector('main section');
|
|
289
|
+
for (const section of doc.querySelectorAll<HTMLElement>('main section')) {
|
|
290
|
+
const allActions = Array.from(
|
|
291
|
+
section.querySelectorAll<HTMLElement>(
|
|
292
|
+
'a[href],button,[role="button"],[data-href],[data-route],[data-url]',
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
const routeLinks = allActions
|
|
296
|
+
.map(pageForControl)
|
|
297
|
+
.filter((page): page is { id: string; label: string; route: string } =>
|
|
298
|
+
Boolean(page),
|
|
299
|
+
);
|
|
300
|
+
const isPageRoot = section.hasAttribute('data-preview-page-key');
|
|
301
|
+
const isLikelyHero =
|
|
302
|
+
section === firstMainSection ||
|
|
303
|
+
section.hasAttribute('data-design-hero') ||
|
|
304
|
+
/(?:^|\s)(?:hero|banner|masthead)(?:\s|$)/i.test(section.className) ||
|
|
305
|
+
Boolean(section.querySelector('h1'));
|
|
306
|
+
if (
|
|
307
|
+
!isPageRoot &&
|
|
308
|
+
!isLikelyHero &&
|
|
309
|
+
routeLinks.length > 0 &&
|
|
310
|
+
routeLinks.length === allActions.length &&
|
|
311
|
+
routeLinks.every((page) => !selected.has(page.id))
|
|
312
|
+
) {
|
|
313
|
+
disable(section);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function pageSelectionPayload(siteData: unknown, basePath = '') {
|
|
319
|
+
if (!isRecord(siteData)) return null;
|
|
320
|
+
const requirements = isRecord(siteData.requirements)
|
|
321
|
+
? siteData.requirements
|
|
322
|
+
: null;
|
|
323
|
+
const template = isRecord(siteData.template) ? siteData.template : null;
|
|
324
|
+
const structure = isRecord(template?.structure) ? template.structure : null;
|
|
325
|
+
const requiredPages = Array.isArray(requirements?.requiredPages)
|
|
326
|
+
? requirements.requiredPages.filter(
|
|
327
|
+
(page): page is string => typeof page === 'string',
|
|
328
|
+
)
|
|
329
|
+
: undefined;
|
|
330
|
+
const structurePages = Array.isArray(structure?.pages)
|
|
331
|
+
? structure.pages.filter((page): page is string => typeof page === 'string')
|
|
332
|
+
: undefined;
|
|
333
|
+
const pageDefinitions = normalizeTemplatePageDefinitions(
|
|
334
|
+
{
|
|
335
|
+
pages:
|
|
336
|
+
Array.isArray(template?.pageDefinitions)
|
|
337
|
+
? template.pageDefinitions
|
|
338
|
+
: Array.isArray(structure?.pageDefinitions)
|
|
339
|
+
? structure.pageDefinitions
|
|
340
|
+
: undefined,
|
|
341
|
+
},
|
|
342
|
+
requiredPages ?? structurePages ?? [],
|
|
343
|
+
);
|
|
344
|
+
const routes = Object.fromEntries(
|
|
345
|
+
pageDefinitions.map((page) => [page.id, page.route]),
|
|
346
|
+
);
|
|
347
|
+
return {
|
|
348
|
+
basePath: basePath.replace(/\/+$/g, ''),
|
|
349
|
+
routes,
|
|
350
|
+
requirements: {
|
|
351
|
+
requiredPages,
|
|
352
|
+
},
|
|
353
|
+
template: {
|
|
354
|
+
pageDefinitions: Array.isArray(template?.pageDefinitions)
|
|
355
|
+
? template.pageDefinitions
|
|
356
|
+
: undefined,
|
|
357
|
+
structure: {
|
|
358
|
+
pages: structurePages,
|
|
359
|
+
pageDefinitions: Array.isArray(structure?.pageDefinitions)
|
|
360
|
+
? structure.pageDefinitions
|
|
361
|
+
: undefined,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function installProgressivePageNavigation(
|
|
368
|
+
payload: {
|
|
369
|
+
basePath?: string;
|
|
370
|
+
routes?: Record<string, string>;
|
|
371
|
+
},
|
|
372
|
+
doc: Document = document,
|
|
373
|
+
) {
|
|
374
|
+
const win = doc.defaultView;
|
|
375
|
+
if (!win || (win as Window & { __fivoraProgressiveNav?: boolean }).__fivoraProgressiveNav) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
(win as Window & { __fivoraProgressiveNav?: boolean }).__fivoraProgressiveNav =
|
|
379
|
+
true;
|
|
380
|
+
const routes = payload.routes ?? {};
|
|
381
|
+
const base = (payload.basePath ?? '').replace(/\/+$/g, '');
|
|
382
|
+
doc.addEventListener(
|
|
383
|
+
'click',
|
|
384
|
+
(event) => {
|
|
385
|
+
const target = event.target;
|
|
386
|
+
if (!(target instanceof Element)) return;
|
|
387
|
+
const control = target.closest('[data-target-page]');
|
|
388
|
+
if (
|
|
389
|
+
!control ||
|
|
390
|
+
control.getAttribute('data-fivora-page-disabled') === 'true'
|
|
391
|
+
) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (control.tagName === 'A' && control.getAttribute('href')) return;
|
|
395
|
+
const pageKey = control.getAttribute('data-target-page');
|
|
396
|
+
if (!pageKey) return;
|
|
397
|
+
const route = routes[pageKey];
|
|
398
|
+
if (!route) return;
|
|
399
|
+
event.preventDefault();
|
|
400
|
+
const path = route === '/' ? '/' : route.startsWith('/') ? route : `/${route}`;
|
|
401
|
+
win.location.assign(`${base}${path}`);
|
|
402
|
+
},
|
|
403
|
+
true,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function upsertUniversalPageSelection(
|
|
408
|
+
html: string,
|
|
409
|
+
siteData: unknown,
|
|
410
|
+
basePath = '',
|
|
411
|
+
) {
|
|
412
|
+
const payload = pageSelectionPayload(siteData, basePath);
|
|
413
|
+
const stylePattern = new RegExp(
|
|
414
|
+
`<style\\b[^>]*\\bid=["']${UNIVERSAL_PAGE_SELECTION_STYLE_ID}["'][^>]*>[\\s\\S]*?<\\/style>`,
|
|
415
|
+
'gi',
|
|
416
|
+
);
|
|
417
|
+
const scriptPattern = new RegExp(
|
|
418
|
+
`<script\\b[^>]*\\bid=["']${UNIVERSAL_PAGE_SELECTION_SCRIPT_ID}["'][^>]*>[\\s\\S]*?<\\/script>`,
|
|
419
|
+
'gi',
|
|
420
|
+
);
|
|
421
|
+
let updated = html.replace(stylePattern, '').replace(scriptPattern, '');
|
|
422
|
+
if (!payload) return updated;
|
|
423
|
+
const style = `<style id="${UNIVERSAL_PAGE_SELECTION_STYLE_ID}">[data-fivora-page-disabled="true"]{display:none!important}</style>`;
|
|
424
|
+
const payloadJson = JSON.stringify(payload).replace(/</g, '\\u003c');
|
|
425
|
+
const script =
|
|
426
|
+
`<script id="${UNIVERSAL_PAGE_SELECTION_SCRIPT_ID}">` +
|
|
427
|
+
`;(()=>{const d=${payloadJson};` +
|
|
428
|
+
`const n=${installProgressivePageNavigation.toString()};n(d,document);` +
|
|
429
|
+
`const f=${enforceSelectedTemplatePages.toString()};` +
|
|
430
|
+
`const a=()=>f(d,document);if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',a,{once:true})}else{a()}` +
|
|
431
|
+
`;[0,80,300,1000].forEach(t=>setTimeout(a,t));})();</script>`;
|
|
432
|
+
updated = /<\/head>/i.test(updated)
|
|
433
|
+
? updated.replace(/<\/head>/i, `${style}</head>`)
|
|
434
|
+
: `${style}${updated}`;
|
|
435
|
+
return /<\/body>/i.test(updated)
|
|
436
|
+
? updated.replace(/<\/body>/i, `${script}</body>`)
|
|
437
|
+
: `${updated}${script}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export async function applyUniversalPageSelectionToDirectory(input: {
|
|
441
|
+
root: string;
|
|
442
|
+
siteData: unknown;
|
|
443
|
+
basePath?: string;
|
|
444
|
+
}) {
|
|
445
|
+
const htmlFiles: string[] = [];
|
|
446
|
+
const visit = async (directory: string) => {
|
|
447
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
448
|
+
const path = join(directory, entry.name);
|
|
449
|
+
if (entry.isDirectory()) await visit(path);
|
|
450
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
|
|
451
|
+
htmlFiles.push(path);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
await visit(input.root);
|
|
456
|
+
await Promise.all(
|
|
457
|
+
htmlFiles.map(async (filePath) => {
|
|
458
|
+
const html = await readFile(filePath, 'utf8');
|
|
459
|
+
await writeFile(
|
|
460
|
+
filePath,
|
|
461
|
+
upsertUniversalPageSelection(
|
|
462
|
+
html,
|
|
463
|
+
input.siteData,
|
|
464
|
+
input.basePath ?? '',
|
|
465
|
+
),
|
|
466
|
+
'utf8',
|
|
467
|
+
);
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
return { pageCount: htmlFiles.length };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function findLinksToUnselectedPages(input: {
|
|
474
|
+
html: string;
|
|
475
|
+
pageDefinitions: FivoraTemplatePageDefinition[];
|
|
476
|
+
selectedPages: string[];
|
|
477
|
+
basePath?: string;
|
|
478
|
+
}) {
|
|
479
|
+
const selected = new Set(input.selectedPages);
|
|
480
|
+
const disabled = input.pageDefinitions.filter(
|
|
481
|
+
(page) => !selected.has(page.id),
|
|
482
|
+
);
|
|
483
|
+
const findings: Array<{ pageId: string; route: string; href: string }> = [];
|
|
484
|
+
for (const match of input.html.matchAll(
|
|
485
|
+
/<[a-z][^>]*\b(?:href|formaction|data-href|data-route|data-url)\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>/gi,
|
|
486
|
+
)) {
|
|
487
|
+
const rawHref = match[1] ?? match[2] ?? '';
|
|
488
|
+
if (
|
|
489
|
+
!rawHref.trim() ||
|
|
490
|
+
rawHref.trim().startsWith('#') ||
|
|
491
|
+
/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(rawHref.trim())
|
|
492
|
+
) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const href = rawHref.split(/[?#]/, 1)[0];
|
|
496
|
+
const normalizedBasePath = input.basePath?.replace(/\/+$/g, '') || '';
|
|
497
|
+
const withoutBase =
|
|
498
|
+
normalizedBasePath &&
|
|
499
|
+
(href === normalizedBasePath || href.startsWith(`${normalizedBasePath}/`))
|
|
500
|
+
? href.slice(normalizedBasePath.length) || '/'
|
|
501
|
+
: href;
|
|
502
|
+
const normalized = `/${withoutBase
|
|
503
|
+
.replace(/\/index\.html$/i, '/')
|
|
504
|
+
.replace(/\.html$/i, '')
|
|
505
|
+
.replace(/^\.\//, '')
|
|
506
|
+
.replace(/^\/+|\/+$/g, '')}`;
|
|
507
|
+
const path = normalized === '/' ? '/' : normalized;
|
|
508
|
+
const page = disabled.find((candidate) => {
|
|
509
|
+
const route =
|
|
510
|
+
candidate.route === '/'
|
|
511
|
+
? '/'
|
|
512
|
+
: `/${candidate.route.replace(/^\/+|\/+$/g, '')}`;
|
|
513
|
+
return route === '/'
|
|
514
|
+
? path === '/'
|
|
515
|
+
: path === route || path.startsWith(`${route}/`);
|
|
516
|
+
});
|
|
517
|
+
if (page) findings.push({ pageId: page.id, route: page.route, href });
|
|
518
|
+
}
|
|
519
|
+
return findings;
|
|
520
|
+
}
|