@zeropress/build-core 0.5.2 → 0.6.1
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 +30 -40
- package/package.json +3 -3
- package/src/api/build-site.js +708 -135
- package/src/index.d.ts +4 -3
- package/src/render/content-renderer.js +20 -2
- package/src/render/control-flow-renderer.js +251 -39
- package/src/render/partial-resolver.js +42 -6
- package/src/render/variable-resolver.js +4 -0
- package/src/render/zeropress-engine.js +2 -2
- package/src/theme/load-theme-dir.js +56 -1
package/src/api/build-site.js
CHANGED
|
@@ -14,10 +14,13 @@ const DEFAULT_OPTIONS = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const DEFAULT_POSTS_PER_PAGE = 10;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const DEFAULT_DATETIME_DISPLAY = 'static';
|
|
18
|
+
const DEFAULT_DATE_STYLE = 'medium';
|
|
19
|
+
const DEFAULT_TIME_STYLE = 'none';
|
|
19
20
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
20
21
|
const DEFAULT_LOCALE = 'en-US';
|
|
22
|
+
const DATETIME_DISPLAY_MODES = new Set(['static', 'client']);
|
|
23
|
+
const DATETIME_STYLES = new Set(['none', 'short', 'medium', 'long', 'full']);
|
|
21
24
|
const DEFAULT_PERMALINKS = Object.freeze({
|
|
22
25
|
output_style: 'directory',
|
|
23
26
|
posts: '/posts/:slug/',
|
|
@@ -35,12 +38,24 @@ const DEFAULT_POST_INDEX = Object.freeze({
|
|
|
35
38
|
});
|
|
36
39
|
const PERMALINK_OUTPUT_STYLES = new Set(['directory', 'html-extension']);
|
|
37
40
|
const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
|
|
41
|
+
const SEARCH_INDEX_OUTPUT_PATH = '_zeropress/search.json';
|
|
42
|
+
const SEARCH_ADAPTER_OUTPUT_PATH = '_zeropress/search.js';
|
|
38
43
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
39
44
|
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
40
45
|
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
41
46
|
const MEDIA_DELIVERY_MODES = new Set(['none', 'media_domain']);
|
|
47
|
+
const DISCOVERABILITY_VALUES = new Set(['default', 'noindex', 'delist']);
|
|
42
48
|
const RESPONSIVE_IMAGE_WIDTHS = [320, 480, 768, 1024, 1280, 1600, 1920];
|
|
43
49
|
const RESPONSIVE_IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif']);
|
|
50
|
+
const SEARCH_FIELD_WEIGHTS = Object.freeze({
|
|
51
|
+
title: 5,
|
|
52
|
+
headings: 3,
|
|
53
|
+
tags: 2.5,
|
|
54
|
+
categories: 2,
|
|
55
|
+
excerpt: 1.5,
|
|
56
|
+
content_text: 1,
|
|
57
|
+
});
|
|
58
|
+
const SEARCH_RECENCY_BOOST_MAX = 0.15;
|
|
44
59
|
export async function buildSite(input) {
|
|
45
60
|
const options = { ...DEFAULT_OPTIONS, ...(input.options || {}) };
|
|
46
61
|
const state = await createBuildState(input, options);
|
|
@@ -93,9 +108,17 @@ export async function buildSite(input) {
|
|
|
93
108
|
);
|
|
94
109
|
|
|
95
110
|
if (options.generateSpecialFiles) {
|
|
111
|
+
await writeOutput(state.writer, state.summaries, SEARCH_INDEX_OUTPUT_PATH, buildSearchIndexJson(state), 'application/json');
|
|
112
|
+
await writeOutput(state.writer, state.summaries, SEARCH_ADAPTER_OUTPUT_PATH, buildSearchAdapterJs(), 'application/javascript');
|
|
96
113
|
await maybeRenderNotFoundPage(state);
|
|
97
114
|
if (hasCanonicalSiteUrl(state.previewData.site.url)) {
|
|
98
|
-
await writeOutput(
|
|
115
|
+
await writeOutput(
|
|
116
|
+
state.writer,
|
|
117
|
+
state.summaries,
|
|
118
|
+
'sitemap.xml',
|
|
119
|
+
buildSitemapXml(state.previewData.site, state.emitted, state.generatedAt, options.sitemapStylesheetHref),
|
|
120
|
+
'application/xml',
|
|
121
|
+
);
|
|
99
122
|
await writeOutput(state.writer, state.summaries, 'feed.xml', buildFeedXml(state.previewData.site, state.emitted, state.generatedAt), 'application/rss+xml');
|
|
100
123
|
}
|
|
101
124
|
if (shouldGenerateRobotsTxt(options)) {
|
|
@@ -144,6 +167,7 @@ async function createBuildState(input, options) {
|
|
|
144
167
|
customCssHref: customCssAsset ? `/${customCssAsset.path}` : '',
|
|
145
168
|
customHtml: previewData.custom_html,
|
|
146
169
|
favicon: previewData.site.favicon,
|
|
170
|
+
exposeGenerator: previewData.site.expose_generator !== false,
|
|
147
171
|
commentPolicyContent: buildCommentPolicyManifest(renderData.posts),
|
|
148
172
|
options,
|
|
149
173
|
generatedAt: new Date(),
|
|
@@ -261,6 +285,7 @@ async function renderFrontPage(state, route) {
|
|
|
261
285
|
description: page.excerpt,
|
|
262
286
|
ogType: 'website',
|
|
263
287
|
image: page.featured_image,
|
|
288
|
+
robotsNoindex: shouldNoindexDocument(page),
|
|
264
289
|
}),
|
|
265
290
|
},
|
|
266
291
|
createRenderContext(state.previewData.site, currentUrl),
|
|
@@ -273,6 +298,7 @@ async function renderFrontPage(state, route) {
|
|
|
273
298
|
title: page.title,
|
|
274
299
|
description: page.excerpt,
|
|
275
300
|
includeInFeed: false,
|
|
301
|
+
includeInSitemap: !isDelistedDocument(page),
|
|
276
302
|
};
|
|
277
303
|
return;
|
|
278
304
|
}
|
|
@@ -325,6 +351,7 @@ async function renderPost(state, post) {
|
|
|
325
351
|
image: post.featured_image,
|
|
326
352
|
publishedTime: post.published_at_iso,
|
|
327
353
|
modifiedTime: post.updated_at_iso,
|
|
354
|
+
robotsNoindex: shouldNoindexDocument(post),
|
|
328
355
|
}),
|
|
329
356
|
},
|
|
330
357
|
createRenderContext(state.previewData.site, currentUrl),
|
|
@@ -332,14 +359,16 @@ async function renderPost(state, post) {
|
|
|
332
359
|
html = state.assetProcessor.updateAssetReferences(html, state.assetMap);
|
|
333
360
|
html = injectSiteCustomizations(html, state);
|
|
334
361
|
await writeOutput(state.writer, state.summaries, routePathToOutputPath(post.url, state.previewData.site.permalinks.output_style), html, 'text/html');
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
362
|
+
if (!isDelistedDocument(post)) {
|
|
363
|
+
state.emitted.posts.push({
|
|
364
|
+
url: currentUrl,
|
|
365
|
+
title: post.title,
|
|
366
|
+
description: post.excerpt,
|
|
367
|
+
publishedAt: post.published_at_iso,
|
|
368
|
+
updatedAt: post.updated_at_iso,
|
|
369
|
+
status: post.status,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
343
372
|
}
|
|
344
373
|
|
|
345
374
|
async function renderPage(state, page) {
|
|
@@ -362,6 +391,7 @@ async function renderPage(state, page) {
|
|
|
362
391
|
description: page.excerpt,
|
|
363
392
|
ogType: 'website',
|
|
364
393
|
image: page.featured_image,
|
|
394
|
+
robotsNoindex: shouldNoindexDocument(page),
|
|
365
395
|
}),
|
|
366
396
|
},
|
|
367
397
|
createRenderContext(state.previewData.site, currentUrl),
|
|
@@ -374,7 +404,7 @@ async function renderPage(state, page) {
|
|
|
374
404
|
title: page.title,
|
|
375
405
|
description: page.excerpt,
|
|
376
406
|
status: page.status,
|
|
377
|
-
includeInSitemap: page.omit_from_sitemap !== true,
|
|
407
|
+
includeInSitemap: page.omit_from_sitemap !== true && !isDelistedDocument(page),
|
|
378
408
|
});
|
|
379
409
|
}
|
|
380
410
|
|
|
@@ -408,19 +438,27 @@ async function maybeRenderNotFoundPage(state) {
|
|
|
408
438
|
function normalizePreviewData(previewData, options = {}) {
|
|
409
439
|
const normalizedSite = {
|
|
410
440
|
...previewData.site,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
? previewData.site.
|
|
441
|
+
media_base_url: normalizeOptionalString(previewData.site.media_base_url),
|
|
442
|
+
media_delivery_mode: MEDIA_DELIVERY_MODES.has(previewData.site.media_delivery_mode)
|
|
443
|
+
? previewData.site.media_delivery_mode
|
|
414
444
|
: 'none',
|
|
415
445
|
favicon: normalizeSiteFavicon(previewData.site.favicon || options.favicon),
|
|
416
|
-
|
|
417
|
-
? previewData.site.
|
|
446
|
+
posts_per_page: Number.isInteger(previewData.site.posts_per_page) && previewData.site.posts_per_page > 0
|
|
447
|
+
? previewData.site.posts_per_page
|
|
418
448
|
: DEFAULT_POSTS_PER_PAGE,
|
|
419
|
-
|
|
420
|
-
|
|
449
|
+
datetime_display: DATETIME_DISPLAY_MODES.has(previewData.site.datetime_display)
|
|
450
|
+
? previewData.site.datetime_display
|
|
451
|
+
: DEFAULT_DATETIME_DISPLAY,
|
|
452
|
+
date_style: DATETIME_STYLES.has(previewData.site.date_style)
|
|
453
|
+
? previewData.site.date_style
|
|
454
|
+
: DEFAULT_DATE_STYLE,
|
|
455
|
+
time_style: DATETIME_STYLES.has(previewData.site.time_style)
|
|
456
|
+
? previewData.site.time_style
|
|
457
|
+
: DEFAULT_TIME_STYLE,
|
|
421
458
|
timezone: normalizeNonEmptyString(previewData.site.timezone, DEFAULT_TIMEZONE),
|
|
422
459
|
locale: normalizeLocale(previewData.site.locale || DEFAULT_LOCALE),
|
|
423
|
-
|
|
460
|
+
disallow_comments: previewData.site.disallow_comments === true,
|
|
461
|
+
expose_generator: previewData.site.expose_generator !== false,
|
|
424
462
|
indexing: previewData.site.indexing !== false,
|
|
425
463
|
permalinks: normalizePermalinks(previewData.site.permalinks),
|
|
426
464
|
front_page: normalizeFrontPage(previewData.site.front_page),
|
|
@@ -435,13 +473,13 @@ function normalizePreviewData(previewData, options = {}) {
|
|
|
435
473
|
site: normalizedSite,
|
|
436
474
|
menus: normalizeRecordMap(previewData.menus),
|
|
437
475
|
collections: normalizeCollections(previewData.collections),
|
|
438
|
-
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.
|
|
476
|
+
widgets: normalizeWidgetAreas(previewData.widgets, normalizedSite.media_base_url),
|
|
439
477
|
custom_css: normalizeCustomCss(previewData.custom_css),
|
|
440
478
|
custom_html: normalizeCustomHtml(previewData.custom_html),
|
|
441
479
|
content: {
|
|
442
480
|
...previewData.content,
|
|
443
481
|
authors: previewData.content.authors.map((author) => {
|
|
444
|
-
const avatar = normalizeMediaField(author.avatar, normalizedSite.
|
|
482
|
+
const avatar = normalizeMediaField(author.avatar, normalizedSite.media_base_url);
|
|
445
483
|
const avatarMedia = deriveManagedMedia(avatar, mediaRegistry, normalizedSite);
|
|
446
484
|
return {
|
|
447
485
|
...author,
|
|
@@ -451,22 +489,24 @@ function normalizePreviewData(previewData, options = {}) {
|
|
|
451
489
|
}),
|
|
452
490
|
posts: previewData.content.posts
|
|
453
491
|
.map((post) => {
|
|
454
|
-
const featuredImage = normalizeMediaField(post.featured_image, normalizedSite.
|
|
492
|
+
const featuredImage = normalizeMediaField(post.featured_image, normalizedSite.media_base_url);
|
|
455
493
|
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
456
494
|
return {
|
|
457
495
|
...post,
|
|
458
496
|
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
459
497
|
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
498
|
+
discoverability: normalizeDiscoverability(post.discoverability),
|
|
460
499
|
featured_image: featuredImage,
|
|
461
500
|
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
462
501
|
};
|
|
463
502
|
})
|
|
464
503
|
.sort((left, right) => toDate(right.published_at_iso).getTime() - toDate(left.published_at_iso).getTime()),
|
|
465
504
|
pages: previewData.content.pages.map((page) => {
|
|
466
|
-
const featuredImage = normalizeMediaField(page.featured_image, normalizedSite.
|
|
505
|
+
const featuredImage = normalizeMediaField(page.featured_image, normalizedSite.media_base_url);
|
|
467
506
|
const featuredMedia = deriveManagedMedia(featuredImage, mediaRegistry, normalizedSite);
|
|
468
507
|
return {
|
|
469
508
|
...page,
|
|
509
|
+
discoverability: normalizeDiscoverability(page.discoverability),
|
|
470
510
|
featured_image: featuredImage,
|
|
471
511
|
...(featuredMedia ? { featured_media: featuredMedia } : {}),
|
|
472
512
|
};
|
|
@@ -486,6 +526,18 @@ function normalizeRecordMap(value) {
|
|
|
486
526
|
return { ...value };
|
|
487
527
|
}
|
|
488
528
|
|
|
529
|
+
function normalizeDiscoverability(value) {
|
|
530
|
+
return DISCOVERABILITY_VALUES.has(value) ? value : 'default';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isDelistedDocument(document) {
|
|
534
|
+
return document?.discoverability === 'delist';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function shouldNoindexDocument(document) {
|
|
538
|
+
return document?.discoverability === 'noindex' || document?.discoverability === 'delist';
|
|
539
|
+
}
|
|
540
|
+
|
|
489
541
|
function normalizeContentMedia(mediaItems, site) {
|
|
490
542
|
if (!Array.isArray(mediaItems)) {
|
|
491
543
|
return [];
|
|
@@ -496,7 +548,7 @@ function normalizeContentMedia(mediaItems, site) {
|
|
|
496
548
|
if (!item || typeof item !== 'object') {
|
|
497
549
|
return null;
|
|
498
550
|
}
|
|
499
|
-
const src = normalizeMediaField(item.src, site.
|
|
551
|
+
const src = normalizeMediaField(item.src, site.media_base_url);
|
|
500
552
|
const width = Number.isInteger(item.width) && item.width > 0 ? item.width : 0;
|
|
501
553
|
const height = Number.isInteger(item.height) && item.height > 0 ? item.height : 0;
|
|
502
554
|
if (!src || !width || !height) {
|
|
@@ -538,12 +590,12 @@ function deriveManagedMedia(src, mediaRegistry, site) {
|
|
|
538
590
|
}
|
|
539
591
|
|
|
540
592
|
function buildResponsiveImageSrcset(media, site) {
|
|
541
|
-
if (site.
|
|
593
|
+
if (site.media_delivery_mode !== 'media_domain') {
|
|
542
594
|
return '';
|
|
543
595
|
}
|
|
544
596
|
|
|
545
|
-
const
|
|
546
|
-
if (!
|
|
597
|
+
const media_base_url = normalizeOptionalString(site.media_base_url);
|
|
598
|
+
if (!media_base_url || !isUrlUnderMediaBase(media.src, media_base_url) || !isResponsiveRasterImage(media.src)) {
|
|
547
599
|
return '';
|
|
548
600
|
}
|
|
549
601
|
|
|
@@ -570,10 +622,10 @@ function buildResponsiveImageVariantUrl(src, width) {
|
|
|
570
622
|
}
|
|
571
623
|
}
|
|
572
624
|
|
|
573
|
-
function isUrlUnderMediaBase(src,
|
|
625
|
+
function isUrlUnderMediaBase(src, media_base_url) {
|
|
574
626
|
try {
|
|
575
627
|
const sourceUrl = new URL(src);
|
|
576
|
-
const baseUrl = new URL(
|
|
628
|
+
const baseUrl = new URL(media_base_url);
|
|
577
629
|
const basePath = baseUrl.pathname.endsWith('/') ? baseUrl.pathname : `${baseUrl.pathname}/`;
|
|
578
630
|
return sourceUrl.origin === baseUrl.origin && sourceUrl.pathname.startsWith(basePath);
|
|
579
631
|
} catch {
|
|
@@ -610,19 +662,19 @@ function normalizeCollections(collections) {
|
|
|
610
662
|
);
|
|
611
663
|
}
|
|
612
664
|
|
|
613
|
-
function normalizeWidgetAreas(
|
|
614
|
-
if (!
|
|
665
|
+
function normalizeWidgetAreas(widget_areas, media_base_url) {
|
|
666
|
+
if (!widget_areas || typeof widget_areas !== 'object') {
|
|
615
667
|
return {};
|
|
616
668
|
}
|
|
617
669
|
|
|
618
670
|
return Object.fromEntries(
|
|
619
|
-
Object.entries(
|
|
671
|
+
Object.entries(widget_areas).map(([widgetAreaId, widgetArea]) => [
|
|
620
672
|
widgetAreaId,
|
|
621
673
|
{
|
|
622
674
|
...widgetArea,
|
|
623
675
|
name: normalizeNonEmptyString(widgetArea?.name, widgetAreaId),
|
|
624
676
|
items: Array.isArray(widgetArea?.items)
|
|
625
|
-
? widgetArea.items.map((item) => normalizeWidgetItem(item,
|
|
677
|
+
? widgetArea.items.map((item) => normalizeWidgetItem(item, media_base_url))
|
|
626
678
|
: [],
|
|
627
679
|
},
|
|
628
680
|
]),
|
|
@@ -631,21 +683,15 @@ function normalizeWidgetAreas(widgetAreas, mediaBaseUrl) {
|
|
|
631
683
|
|
|
632
684
|
function normalizeSiteFooter(footer) {
|
|
633
685
|
const source = footer && typeof footer === 'object' && !Array.isArray(footer) ? footer : {};
|
|
634
|
-
const attribution = source.attribution && typeof source.attribution === 'object' && !Array.isArray(source.attribution)
|
|
635
|
-
? source.attribution
|
|
636
|
-
: {};
|
|
637
686
|
|
|
638
687
|
return {
|
|
639
688
|
...source,
|
|
640
689
|
copyright_text: normalizeOptionalString(source.copyright_text),
|
|
641
|
-
attribution:
|
|
642
|
-
...attribution,
|
|
643
|
-
enabled: attribution.enabled !== false,
|
|
644
|
-
},
|
|
690
|
+
attribution: source.attribution !== false,
|
|
645
691
|
};
|
|
646
692
|
}
|
|
647
693
|
|
|
648
|
-
function normalizeWidgetItem(item,
|
|
694
|
+
function normalizeWidgetItem(item, media_base_url) {
|
|
649
695
|
const normalizedItem = {
|
|
650
696
|
...item,
|
|
651
697
|
title: typeof item?.title === 'string' ? item.title.trim() : '',
|
|
@@ -656,7 +702,7 @@ function normalizeWidgetItem(item, mediaBaseUrl) {
|
|
|
656
702
|
...normalizedItem,
|
|
657
703
|
settings: {
|
|
658
704
|
...item.settings,
|
|
659
|
-
avatar: normalizeMediaField(item.settings.avatar,
|
|
705
|
+
avatar: normalizeMediaField(item.settings.avatar, media_base_url),
|
|
660
706
|
},
|
|
661
707
|
};
|
|
662
708
|
}
|
|
@@ -733,21 +779,21 @@ function normalizeFrontPage(frontPage) {
|
|
|
733
779
|
};
|
|
734
780
|
}
|
|
735
781
|
|
|
736
|
-
function normalizePostIndex(
|
|
737
|
-
if (!
|
|
782
|
+
function normalizePostIndex(post_index) {
|
|
783
|
+
if (!post_index || typeof post_index !== 'object') {
|
|
738
784
|
return { ...DEFAULT_POST_INDEX };
|
|
739
785
|
}
|
|
740
786
|
|
|
741
787
|
return {
|
|
742
|
-
enabled:
|
|
743
|
-
path: normalizeNonEmptyString(
|
|
744
|
-
paginate:
|
|
788
|
+
enabled: post_index.enabled !== false,
|
|
789
|
+
path: normalizeNonEmptyString(post_index.path, DEFAULT_POST_INDEX.path),
|
|
790
|
+
paginate: post_index.paginate !== false,
|
|
745
791
|
};
|
|
746
792
|
}
|
|
747
793
|
|
|
748
794
|
function createRenderData(previewData, themeMetadata = {}) {
|
|
749
795
|
const themeSupportsComments = themeMetadata?.features?.comments === true;
|
|
750
|
-
const themeSupportsPostIndex = themeMetadata?.features?.
|
|
796
|
+
const themeSupportsPostIndex = themeMetadata?.features?.post_index !== false;
|
|
751
797
|
const authorsById = new Map(previewData.content.authors.map((author) => [author.id, author]));
|
|
752
798
|
const categoriesBySlug = new Map(previewData.content.categories.map((category) => [category.slug, category]));
|
|
753
799
|
const tagsBySlug = new Map(previewData.content.tags.map((tag) => [tag.slug, tag]));
|
|
@@ -755,8 +801,9 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
755
801
|
const tagPostsBySlug = new Map();
|
|
756
802
|
const categoryCountBySlug = new Map();
|
|
757
803
|
const tagCountBySlug = new Map();
|
|
804
|
+
const discoverableSourcePosts = previewData.content.posts.filter((post) => !isDelistedDocument(post));
|
|
758
805
|
|
|
759
|
-
for (const post of
|
|
806
|
+
for (const post of discoverableSourcePosts) {
|
|
760
807
|
for (const slug of post.category_slugs) {
|
|
761
808
|
pushToSlugMap(categoryPostsBySlug, slug, post);
|
|
762
809
|
categoryCountBySlug.set(slug, (categoryCountBySlug.get(slug) || 0) + 1);
|
|
@@ -769,25 +816,34 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
769
816
|
}
|
|
770
817
|
|
|
771
818
|
const preparedPosts = previewData.content.posts.map((post) => preparePost(post, previewData.site, authorsById, categoriesBySlug, tagsBySlug, themeSupportsComments));
|
|
772
|
-
const
|
|
819
|
+
const discoverablePreparedPosts = preparedPosts.filter((post) => !isDelistedDocument(post));
|
|
820
|
+
const adjacentPostsBySlug = new Map(
|
|
821
|
+
discoverablePreparedPosts.map((post, index) => [post.slug, {
|
|
822
|
+
prev: index > 0 ? buildAdjacentPostSummary(discoverablePreparedPosts[index - 1]) : null,
|
|
823
|
+
next: index < discoverablePreparedPosts.length - 1 ? buildAdjacentPostSummary(discoverablePreparedPosts[index + 1]) : null,
|
|
824
|
+
}]),
|
|
825
|
+
);
|
|
826
|
+
const posts = preparedPosts.map((post) => ({
|
|
773
827
|
...post,
|
|
774
|
-
prev:
|
|
775
|
-
next:
|
|
828
|
+
prev: adjacentPostsBySlug.get(post.slug)?.prev || null,
|
|
829
|
+
next: adjacentPostsBySlug.get(post.slug)?.next || null,
|
|
776
830
|
}));
|
|
777
831
|
const pages = previewData.content.pages.map((page) => preparePage(page, previewData.site));
|
|
778
832
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
779
833
|
const pageBySlug = new Map(pages.map((page) => [page.slug, page]));
|
|
780
834
|
const frontPage = previewData.site.front_page;
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
835
|
+
const collections = resolveCollections(previewData.collections, postBySlug, pageBySlug, frontPage);
|
|
836
|
+
attachCollectionCursors(posts, pages, collections);
|
|
837
|
+
const post_index = previewData.site.post_index;
|
|
838
|
+
const effectivePostIndexEnabled = post_index.enabled !== false && themeSupportsPostIndex;
|
|
839
|
+
const effectivePostIndexPaginate = effectivePostIndexEnabled && post_index.paginate !== false;
|
|
840
|
+
const post_indexBasePath = normalizeRoutePath(post_index.path || DEFAULT_POST_INDEX.path);
|
|
841
|
+
|
|
842
|
+
if (frontPage.type !== 'theme_index' && effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
787
843
|
throw new Error('Invalid front page configuration: site.front_page occupies "/" so site.post_index.path must not be "/". Set site.post_index.path to a non-root path or disable site.post_index.');
|
|
788
844
|
}
|
|
789
845
|
|
|
790
|
-
const frontPageRoute = buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled,
|
|
846
|
+
const frontPageRoute = buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, post_indexBasePath);
|
|
791
847
|
const pageFrontPageSlug = frontPage.type === 'page' ? frontPage.page_slug : '';
|
|
792
848
|
const preparedPages = pageFrontPageSlug
|
|
793
849
|
? pages.filter((page) => page.slug !== pageFrontPageSlug)
|
|
@@ -797,22 +853,22 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
797
853
|
posts,
|
|
798
854
|
pages: preparedPages,
|
|
799
855
|
postBySlug,
|
|
800
|
-
collections
|
|
856
|
+
collections,
|
|
801
857
|
taxonomies: buildGlobalTaxonomies(previewData, categoryCountBySlug, tagCountBySlug),
|
|
802
858
|
frontPageRoute,
|
|
803
859
|
indexRoutes: buildPostIndexRoutes({
|
|
804
860
|
enabled: effectivePostIndexEnabled,
|
|
805
861
|
paginate: effectivePostIndexPaginate,
|
|
806
|
-
items:
|
|
807
|
-
|
|
808
|
-
basePath:
|
|
862
|
+
items: discoverableSourcePosts,
|
|
863
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
864
|
+
basePath: post_indexBasePath,
|
|
809
865
|
outputStyle: previewData.site.permalinks.output_style,
|
|
810
866
|
postBySlug,
|
|
811
867
|
frontPage,
|
|
812
868
|
}),
|
|
813
869
|
archiveRoutes: buildPaginatedCollection({
|
|
814
|
-
items:
|
|
815
|
-
|
|
870
|
+
items: discoverableSourcePosts,
|
|
871
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
816
872
|
basePath: '/archive/',
|
|
817
873
|
outputStyle: previewData.site.permalinks.output_style,
|
|
818
874
|
}).map((entry) => ({
|
|
@@ -828,7 +884,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
828
884
|
items: previewData.content.categories,
|
|
829
885
|
postsBySlug: categoryPostsBySlug,
|
|
830
886
|
postBySlug,
|
|
831
|
-
|
|
887
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
832
888
|
outputStyle: previewData.site.permalinks.output_style,
|
|
833
889
|
buildBasePath: (category) => resolvePermalink(previewData.site, 'categories', category).path,
|
|
834
890
|
renderExtras: (category) => ({
|
|
@@ -839,7 +895,7 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
839
895
|
items: previewData.content.tags,
|
|
840
896
|
postsBySlug: tagPostsBySlug,
|
|
841
897
|
postBySlug,
|
|
842
|
-
|
|
898
|
+
posts_per_page: previewData.site.posts_per_page,
|
|
843
899
|
outputStyle: previewData.site.permalinks.output_style,
|
|
844
900
|
buildBasePath: (tag) => resolvePermalink(previewData.site, 'tags', tag).path,
|
|
845
901
|
renderExtras: (tag) => ({
|
|
@@ -862,15 +918,19 @@ function resolveCollections(collections, postBySlug, pageBySlug, frontPage) {
|
|
|
862
918
|
}
|
|
863
919
|
|
|
864
920
|
return Object.fromEntries(
|
|
865
|
-
Object.entries(collections).map(([collectionId, collection]) =>
|
|
866
|
-
collectionId,
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
921
|
+
Object.entries(collections).map(([collectionId, collection]) => {
|
|
922
|
+
const items = resolveCollectionItems(collectionId, collection?.items, postBySlug, pageBySlug, frontPage);
|
|
923
|
+
return [
|
|
924
|
+
collectionId,
|
|
925
|
+
{
|
|
926
|
+
id: collectionId,
|
|
927
|
+
title: normalizeOptionalString(collection?.title),
|
|
928
|
+
description: normalizeOptionalString(collection?.description),
|
|
929
|
+
count: items.length,
|
|
930
|
+
items,
|
|
931
|
+
},
|
|
932
|
+
];
|
|
933
|
+
}),
|
|
874
934
|
);
|
|
875
935
|
}
|
|
876
936
|
|
|
@@ -916,12 +976,70 @@ function buildCollectionPageSummary(page, frontPage) {
|
|
|
916
976
|
featured_image: page.featured_image || '',
|
|
917
977
|
...(page.featured_media ? { featured_media: { ...page.featured_media } } : {}),
|
|
918
978
|
meta: page.meta,
|
|
979
|
+
data: page.data,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function attachCollectionCursors(posts, pages, collections) {
|
|
984
|
+
const postTargets = new Map(posts.map((post) => [post.slug, post]));
|
|
985
|
+
const pageTargets = new Map(pages.map((page) => [page.slug, page]));
|
|
986
|
+
|
|
987
|
+
for (const [collectionId, collection] of Object.entries(collections || {})) {
|
|
988
|
+
const items = Array.isArray(collection.items) ? collection.items : [];
|
|
989
|
+
|
|
990
|
+
items.forEach((item, index) => {
|
|
991
|
+
const target = item.type === 'post'
|
|
992
|
+
? postTargets.get(item.slug)
|
|
993
|
+
: item.type === 'page'
|
|
994
|
+
? pageTargets.get(item.slug)
|
|
995
|
+
: null;
|
|
996
|
+
|
|
997
|
+
if (!target) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
target.collection_cursors = target.collection_cursors || {};
|
|
1002
|
+
target.collection_cursors[collectionId] = buildCollectionCursor(collectionId, collection, items, index);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function buildCollectionCursor(collectionId, collection, items, index) {
|
|
1008
|
+
const count = items.length;
|
|
1009
|
+
|
|
1010
|
+
return {
|
|
1011
|
+
collection_id: collectionId,
|
|
1012
|
+
collection_title: collection.title || '',
|
|
1013
|
+
index,
|
|
1014
|
+
position: index + 1,
|
|
1015
|
+
count,
|
|
1016
|
+
first: index === 0,
|
|
1017
|
+
last: index === count - 1,
|
|
1018
|
+
prev: index > 0 ? buildCollectionCursorItemSummary(items[index - 1]) : null,
|
|
1019
|
+
next: index < count - 1 ? buildCollectionCursorItemSummary(items[index + 1]) : null,
|
|
919
1020
|
};
|
|
920
1021
|
}
|
|
921
1022
|
|
|
922
|
-
function
|
|
1023
|
+
function buildCollectionCursorItemSummary(item) {
|
|
1024
|
+
if (!item) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return {
|
|
1029
|
+
type: item.type,
|
|
1030
|
+
title: item.title,
|
|
1031
|
+
slug: item.slug,
|
|
1032
|
+
url: item.url,
|
|
1033
|
+
excerpt: item.excerpt || '',
|
|
1034
|
+
featured_image: item.featured_image || '',
|
|
1035
|
+
meta: item.meta,
|
|
1036
|
+
data: item.data,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function buildFrontPageRoute(frontPage, pages, effectivePostIndexEnabled, post_indexBasePath) {
|
|
923
1041
|
if (frontPage.type === 'theme_index') {
|
|
924
|
-
if (effectivePostIndexEnabled &&
|
|
1042
|
+
if (effectivePostIndexEnabled && post_indexBasePath === '/') {
|
|
925
1043
|
return null;
|
|
926
1044
|
}
|
|
927
1045
|
|
|
@@ -967,7 +1085,7 @@ function buildPostIndexRoutes(options) {
|
|
|
967
1085
|
}
|
|
968
1086
|
|
|
969
1087
|
if (!options.paginate) {
|
|
970
|
-
const items = options.items.slice(0, options.
|
|
1088
|
+
const items = options.items.slice(0, options.posts_per_page);
|
|
971
1089
|
return [{
|
|
972
1090
|
path: options.basePath,
|
|
973
1091
|
route_type: 'post_index',
|
|
@@ -982,7 +1100,7 @@ function buildPostIndexRoutes(options) {
|
|
|
982
1100
|
|
|
983
1101
|
return buildPaginatedCollection({
|
|
984
1102
|
items: options.items,
|
|
985
|
-
|
|
1103
|
+
posts_per_page: options.posts_per_page,
|
|
986
1104
|
basePath: options.basePath,
|
|
987
1105
|
outputStyle: options.outputStyle,
|
|
988
1106
|
}).map((entry) => ({
|
|
@@ -1073,6 +1191,7 @@ function resolveWidgetItem(item, previewData, renderData, widgetAreaId, index) {
|
|
|
1073
1191
|
function resolveRecentPostsWidget(baseWidget, settings, renderData) {
|
|
1074
1192
|
const limit = clampInteger(settings?.limit, 5, 1, 20);
|
|
1075
1193
|
const items = renderData.posts
|
|
1194
|
+
.filter((post) => !isDelistedDocument(post))
|
|
1076
1195
|
.slice(0, limit)
|
|
1077
1196
|
.map((post) => ({
|
|
1078
1197
|
title: post.title,
|
|
@@ -1284,7 +1403,9 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
1284
1403
|
featured_image: post.featured_image,
|
|
1285
1404
|
...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
|
|
1286
1405
|
meta: post.meta,
|
|
1406
|
+
data: post.data,
|
|
1287
1407
|
status: post.status,
|
|
1408
|
+
discoverability: post.discoverability,
|
|
1288
1409
|
allow_comments: post.allow_comments,
|
|
1289
1410
|
category_slugs: post.category_slugs,
|
|
1290
1411
|
tag_slugs: post.tag_slugs,
|
|
@@ -1301,7 +1422,7 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
1301
1422
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
1302
1423
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
1303
1424
|
reading_time: calculateReadingTime(renderedDocument.html),
|
|
1304
|
-
comments_enabled: themeSupportsComments && site.
|
|
1425
|
+
comments_enabled: themeSupportsComments && site.disallow_comments !== true && post.allow_comments === true,
|
|
1305
1426
|
};
|
|
1306
1427
|
}
|
|
1307
1428
|
|
|
@@ -1328,7 +1449,7 @@ function buildTaxonomyRoutes(options) {
|
|
|
1328
1449
|
|
|
1329
1450
|
const paginated = buildPaginatedCollection({
|
|
1330
1451
|
items: matchedPosts,
|
|
1331
|
-
|
|
1452
|
+
posts_per_page: options.posts_per_page,
|
|
1332
1453
|
basePath: options.buildBasePath(item),
|
|
1333
1454
|
outputStyle: options.outputStyle,
|
|
1334
1455
|
});
|
|
@@ -1352,14 +1473,14 @@ function buildTaxonomyRoutes(options) {
|
|
|
1352
1473
|
function buildPaginatedCollection(options) {
|
|
1353
1474
|
const basePath = normalizePaginationBasePath(options.basePath);
|
|
1354
1475
|
const outputStyle = PERMALINK_OUTPUT_STYLES.has(options.outputStyle) ? options.outputStyle : DEFAULT_PERMALINKS.output_style;
|
|
1355
|
-
const
|
|
1476
|
+
const posts_per_page = Number.isInteger(options.posts_per_page) && options.posts_per_page > 0 ? options.posts_per_page : DEFAULT_POSTS_PER_PAGE;
|
|
1356
1477
|
const totalPosts = options.items.length;
|
|
1357
|
-
const totalPages = Math.max(1, Math.ceil(totalPosts /
|
|
1478
|
+
const totalPages = Math.max(1, Math.ceil(totalPosts / posts_per_page));
|
|
1358
1479
|
const pages = [];
|
|
1359
1480
|
|
|
1360
1481
|
for (let page = 1; page <= totalPages; page += 1) {
|
|
1361
|
-
const start = (page - 1) *
|
|
1362
|
-
const end = start +
|
|
1482
|
+
const start = (page - 1) * posts_per_page;
|
|
1483
|
+
const end = start + posts_per_page;
|
|
1363
1484
|
|
|
1364
1485
|
pages.push({
|
|
1365
1486
|
path: buildPaginatedPath(basePath, page),
|
|
@@ -1535,6 +1656,8 @@ function buildStructuredPostSummary(post) {
|
|
|
1535
1656
|
reading_time: post.reading_time,
|
|
1536
1657
|
featured_image: post.featured_image,
|
|
1537
1658
|
...(post.featured_media ? { featured_media: { ...post.featured_media } } : {}),
|
|
1659
|
+
meta: post.meta,
|
|
1660
|
+
data: post.data,
|
|
1538
1661
|
author: {
|
|
1539
1662
|
display_name: post.author?.display_name || '',
|
|
1540
1663
|
avatar: post.author?.avatar || '',
|
|
@@ -1557,6 +1680,7 @@ function buildAdjacentPostSummary(post) {
|
|
|
1557
1680
|
excerpt: post.excerpt,
|
|
1558
1681
|
published_at: post.published_at,
|
|
1559
1682
|
published_at_iso: post.published_at_iso,
|
|
1683
|
+
data: post.data,
|
|
1560
1684
|
};
|
|
1561
1685
|
}
|
|
1562
1686
|
|
|
@@ -1646,50 +1770,25 @@ function formatArchiveLabel(date, site) {
|
|
|
1646
1770
|
function formatTimestamp(value, site) {
|
|
1647
1771
|
const date = toDate(value);
|
|
1648
1772
|
const locale = normalizeLocale(site.locale || DEFAULT_LOCALE);
|
|
1649
|
-
const
|
|
1650
|
-
const
|
|
1773
|
+
const dateStyle = DATETIME_STYLES.has(site.date_style) ? site.date_style : DEFAULT_DATE_STYLE;
|
|
1774
|
+
const timeStyle = DATETIME_STYLES.has(site.time_style) ? site.time_style : DEFAULT_TIME_STYLE;
|
|
1651
1775
|
const siteTimezone = normalizeNonEmptyString(site.timezone, DEFAULT_TIMEZONE);
|
|
1652
1776
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
year: 'numeric',
|
|
1656
|
-
month: dateFormat === 'MMMM D, YYYY' ? 'long' : '2-digit',
|
|
1657
|
-
day: '2-digit',
|
|
1658
|
-
}).formatToParts(date);
|
|
1659
|
-
|
|
1660
|
-
const year = dateParts.find((part) => part.type === 'year')?.value || '';
|
|
1661
|
-
const month = dateParts.find((part) => part.type === 'month')?.value || '';
|
|
1662
|
-
const day = dateParts.find((part) => part.type === 'day')?.value || '';
|
|
1663
|
-
|
|
1664
|
-
let formattedDate = `${year}-${month}-${day}`;
|
|
1665
|
-
if (dateFormat === 'DD/MM/YYYY') {
|
|
1666
|
-
formattedDate = `${day}/${month}/${year}`;
|
|
1667
|
-
} else if (dateFormat === 'MM/DD/YYYY') {
|
|
1668
|
-
formattedDate = `${month}/${day}/${year}`;
|
|
1669
|
-
} else if (dateFormat === 'MMMM D, YYYY') {
|
|
1670
|
-
formattedDate = `${month} ${String(Number(day))}, ${year}`;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
if (!timeFormat) {
|
|
1674
|
-
return formattedDate;
|
|
1777
|
+
if (dateStyle === 'none' && timeStyle === 'none') {
|
|
1778
|
+
return '';
|
|
1675
1779
|
}
|
|
1676
1780
|
|
|
1677
|
-
const
|
|
1781
|
+
const options = {
|
|
1678
1782
|
timeZone: siteTimezone,
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const dayPeriod = (timeParts.find((part) => part.type === 'dayPeriod')?.value || '').toUpperCase();
|
|
1687
|
-
|
|
1688
|
-
const formattedTime = timeFormat === 'hh:mm A'
|
|
1689
|
-
? `${hour}:${minute}${dayPeriod ? ` ${dayPeriod}` : ''}`
|
|
1690
|
-
: `${hour}:${minute}`;
|
|
1783
|
+
};
|
|
1784
|
+
if (dateStyle !== 'none') {
|
|
1785
|
+
options.dateStyle = dateStyle;
|
|
1786
|
+
}
|
|
1787
|
+
if (timeStyle !== 'none') {
|
|
1788
|
+
options.timeStyle = timeStyle;
|
|
1789
|
+
}
|
|
1691
1790
|
|
|
1692
|
-
return
|
|
1791
|
+
return new Intl.DateTimeFormat(locale, options).format(date);
|
|
1693
1792
|
}
|
|
1694
1793
|
|
|
1695
1794
|
function calculateReadingTime(html) {
|
|
@@ -1785,11 +1884,12 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1785
1884
|
...(themePackage.metadata.author ? { author: themePackage.metadata.author } : {}),
|
|
1786
1885
|
...(themePackage.metadata.description ? { description: themePackage.metadata.description } : {}),
|
|
1787
1886
|
...(themePackage.metadata.thumbnail ? { thumbnail: themePackage.metadata.thumbnail } : {}),
|
|
1887
|
+
...(themePackage.metadata.links ? { links: themePackage.metadata.links } : {}),
|
|
1788
1888
|
...(themePackage.metadata.features ? { features: themePackage.metadata.features } : {}),
|
|
1789
|
-
...(themePackage.metadata.
|
|
1790
|
-
...(themePackage.metadata.
|
|
1791
|
-
...(themePackage.metadata.
|
|
1792
|
-
...(themePackage.metadata.
|
|
1889
|
+
...(themePackage.metadata.menu_slots ? { menu_slots: themePackage.metadata.menu_slots } : {}),
|
|
1890
|
+
...(themePackage.metadata.widget_areas ? { widget_areas: themePackage.metadata.widget_areas } : {}),
|
|
1891
|
+
...(themePackage.metadata.site_meta ? { site_meta: themePackage.metadata.site_meta } : {}),
|
|
1892
|
+
...(themePackage.metadata.collection_slots ? { collection_slots: themePackage.metadata.collection_slots } : {}),
|
|
1793
1893
|
}));
|
|
1794
1894
|
|
|
1795
1895
|
for (const [templateName, templateContent] of themePackage.templates.entries()) {
|
|
@@ -1806,7 +1906,7 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1806
1906
|
|
|
1807
1907
|
const validation = await validateThemeFiles(fileMap);
|
|
1808
1908
|
if (!validation.ok) {
|
|
1809
|
-
throw new Error(
|
|
1909
|
+
throw new Error(formatThemeValidationFailure(validation));
|
|
1810
1910
|
}
|
|
1811
1911
|
|
|
1812
1912
|
if (!validation.manifest) {
|
|
@@ -1821,6 +1921,61 @@ async function normalizeAndValidateThemePackage(themePackage) {
|
|
|
1821
1921
|
};
|
|
1822
1922
|
}
|
|
1823
1923
|
|
|
1924
|
+
function formatThemeValidationFailure(validation) {
|
|
1925
|
+
const blocks = [
|
|
1926
|
+
[
|
|
1927
|
+
'Theme validation failed',
|
|
1928
|
+
`Errors: ${validation.errors.length}`,
|
|
1929
|
+
`Checked files: ${validation.checkedFiles}`,
|
|
1930
|
+
].join('\n'),
|
|
1931
|
+
...validation.errors.map((issue) => formatThemeValidationIssue(issue)),
|
|
1932
|
+
];
|
|
1933
|
+
return blocks.join('\n\n');
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function formatThemeValidationIssue(issue) {
|
|
1937
|
+
if (!issue) {
|
|
1938
|
+
return 'Reason: Unknown error';
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const lines = [`ERROR ${issue.code || 'THEME_VALIDATION_ERROR'}`];
|
|
1942
|
+
const location = splitIssuePath(issue.path);
|
|
1943
|
+
if (location.file) {
|
|
1944
|
+
lines.push(`File: ${location.file}`);
|
|
1945
|
+
}
|
|
1946
|
+
if (location.path) {
|
|
1947
|
+
lines.push(`Path: ${location.path}`);
|
|
1948
|
+
}
|
|
1949
|
+
if (Number.isInteger(issue.line) && Number.isInteger(issue.column)) {
|
|
1950
|
+
lines.push(`Line: ${issue.line}, Column: ${issue.column}`);
|
|
1951
|
+
}
|
|
1952
|
+
if (issue.category) {
|
|
1953
|
+
lines.push(`Category: ${issue.category}`);
|
|
1954
|
+
}
|
|
1955
|
+
lines.push(`Reason: ${issue.message || 'Unknown error'}`);
|
|
1956
|
+
if (issue.snippet) {
|
|
1957
|
+
const lineLabel = Number.isInteger(issue.line) ? String(issue.line) : '';
|
|
1958
|
+
lines.push('', `${lineLabel} | ${issue.snippet.line}`, `${' '.repeat(lineLabel.length)} | ${issue.snippet.pointer}`);
|
|
1959
|
+
}
|
|
1960
|
+
if (issue.hint) {
|
|
1961
|
+
lines.push('', 'Hint:', issue.hint);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
return lines.join('\n');
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function splitIssuePath(issuePath) {
|
|
1968
|
+
const normalizedPath = String(issuePath || '');
|
|
1969
|
+
if (normalizedPath.startsWith('theme.json.')) {
|
|
1970
|
+
return {
|
|
1971
|
+
file: 'theme.json',
|
|
1972
|
+
path: normalizedPath.slice('theme.json.'.length),
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
return { file: normalizedPath, path: '' };
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1824
1979
|
function normalizeThemePackageMetadata(sourceMetadata, manifest) {
|
|
1825
1980
|
return {
|
|
1826
1981
|
...manifest,
|
|
@@ -1902,6 +2057,7 @@ function buildPageMeta(site, options = {}) {
|
|
|
1902
2057
|
const ogType = normalizeNonEmptyString(options.ogType, 'website');
|
|
1903
2058
|
const publishedTime = normalizeOptionalString(options.publishedTime);
|
|
1904
2059
|
const modifiedTime = normalizeOptionalString(options.modifiedTime);
|
|
2060
|
+
const robotsNoindex = options.robotsNoindex === true;
|
|
1905
2061
|
|
|
1906
2062
|
const meta = {
|
|
1907
2063
|
title: escapeHtml(resolvedTitle),
|
|
@@ -1915,6 +2071,7 @@ function buildPageMeta(site, options = {}) {
|
|
|
1915
2071
|
og_image: ogImage ? escapeHtml(ogImage) : '',
|
|
1916
2072
|
article_published_time: publishedTime ? escapeHtml(publishedTime) : '',
|
|
1917
2073
|
article_modified_time: modifiedTime ? escapeHtml(modifiedTime) : '',
|
|
2074
|
+
robots_noindex: robotsNoindex,
|
|
1918
2075
|
};
|
|
1919
2076
|
|
|
1920
2077
|
return {
|
|
@@ -1935,6 +2092,9 @@ function buildMetaHeadTags(meta) {
|
|
|
1935
2092
|
if (meta.description) {
|
|
1936
2093
|
tags.push(`<meta name="description" content="${meta.description}">`);
|
|
1937
2094
|
}
|
|
2095
|
+
if (meta.robots_noindex) {
|
|
2096
|
+
tags.push('<meta name="robots" content="noindex">');
|
|
2097
|
+
}
|
|
1938
2098
|
if (meta.canonical_url) {
|
|
1939
2099
|
tags.push(`<link rel="canonical" href="${meta.canonical_url}">`);
|
|
1940
2100
|
}
|
|
@@ -1982,7 +2142,7 @@ function resolveMetaImageUrl(image) {
|
|
|
1982
2142
|
return '';
|
|
1983
2143
|
}
|
|
1984
2144
|
|
|
1985
|
-
function normalizeMediaField(value,
|
|
2145
|
+
function normalizeMediaField(value, media_base_url) {
|
|
1986
2146
|
if (value === undefined) {
|
|
1987
2147
|
return undefined;
|
|
1988
2148
|
}
|
|
@@ -1996,7 +2156,7 @@ function normalizeMediaField(value, mediaBaseUrl) {
|
|
|
1996
2156
|
return normalizeAbsoluteUrl(normalizedValue, SAFE_MEDIA_PROTOCOLS);
|
|
1997
2157
|
}
|
|
1998
2158
|
|
|
1999
|
-
const normalizedBaseUrl = normalizeOptionalString(
|
|
2159
|
+
const normalizedBaseUrl = normalizeOptionalString(media_base_url);
|
|
2000
2160
|
if (!normalizedBaseUrl) {
|
|
2001
2161
|
return normalizedValue;
|
|
2002
2162
|
}
|
|
@@ -2234,6 +2394,7 @@ function assertPlannedOutputPathsSafe(state) {
|
|
|
2234
2394
|
];
|
|
2235
2395
|
|
|
2236
2396
|
if (state.options.generateSpecialFiles) {
|
|
2397
|
+
plannedPaths.push(SEARCH_INDEX_OUTPUT_PATH, SEARCH_ADAPTER_OUTPUT_PATH);
|
|
2237
2398
|
plannedPaths.push('404.html');
|
|
2238
2399
|
if (shouldGenerateRobotsTxt(state.options)) {
|
|
2239
2400
|
plannedPaths.push('robots.txt');
|
|
@@ -2338,6 +2499,7 @@ function sha256(content) {
|
|
|
2338
2499
|
|
|
2339
2500
|
function injectSiteCustomizations(html, state) {
|
|
2340
2501
|
let next = injectFaviconLinks(html, state.favicon);
|
|
2502
|
+
next = injectGeneratorMeta(next, state.exposeGenerator);
|
|
2341
2503
|
next = injectCustomCssAssetLink(next, state.customCssHref);
|
|
2342
2504
|
next = injectCustomHtml(next, state.customHtml);
|
|
2343
2505
|
return next;
|
|
@@ -2374,6 +2536,14 @@ function buildFaviconLinks(favicon) {
|
|
|
2374
2536
|
return lines.join('\n');
|
|
2375
2537
|
}
|
|
2376
2538
|
|
|
2539
|
+
function injectGeneratorMeta(html, exposeGenerator) {
|
|
2540
|
+
if (exposeGenerator === false) {
|
|
2541
|
+
return html;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
return html.replace('</head>', ' <meta name="generator" content="ZeroPress">\n</head>');
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2377
2547
|
function injectCustomCssAssetLink(html, href) {
|
|
2378
2548
|
if (!normalizeOptionalString(href)) {
|
|
2379
2549
|
return html;
|
|
@@ -2397,9 +2567,407 @@ function injectCustomHtml(html, customHtml) {
|
|
|
2397
2567
|
return next;
|
|
2398
2568
|
}
|
|
2399
2569
|
|
|
2400
|
-
function
|
|
2570
|
+
function buildSearchIndexJson(state) {
|
|
2571
|
+
return `${JSON.stringify(buildSearchIndexItems(state), null, 2)}\n`;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function buildSearchIndexItems(state) {
|
|
2575
|
+
const posts = state.renderData.posts
|
|
2576
|
+
.filter((post) => post.status === 'published' && !isDelistedDocument(post))
|
|
2577
|
+
.map((post) => ({
|
|
2578
|
+
id: `post:${post.slug}`,
|
|
2579
|
+
type: 'post',
|
|
2580
|
+
title: post.title,
|
|
2581
|
+
url: post.url,
|
|
2582
|
+
excerpt: normalizeSearchText(post.excerpt),
|
|
2583
|
+
headings: buildSearchHeadings(post.toc),
|
|
2584
|
+
categories: Array.isArray(post.categories) ? post.categories.map((category) => category.name).filter(Boolean) : [],
|
|
2585
|
+
tags: Array.isArray(post.tags) ? post.tags.map((tag) => tag.name).filter(Boolean) : [],
|
|
2586
|
+
published_at_iso: normalizeIsoTimestamp(post.published_at_iso),
|
|
2587
|
+
updated_at_iso: normalizeIsoTimestamp(post.updated_at_iso),
|
|
2588
|
+
content_text: htmlToSearchText(post.html),
|
|
2589
|
+
}));
|
|
2590
|
+
|
|
2591
|
+
const frontPagePage = state.renderData.frontPageRoute?.front_page_type === 'page'
|
|
2592
|
+
? state.renderData.frontPageRoute.page
|
|
2593
|
+
: null;
|
|
2594
|
+
const frontPageItems = frontPagePage && frontPagePage.status === 'published' && !isDelistedDocument(frontPagePage)
|
|
2595
|
+
? [buildSearchPageItem(frontPagePage, '/')]
|
|
2596
|
+
: [];
|
|
2597
|
+
const pageItems = state.renderData.pages
|
|
2598
|
+
.filter((page) => page.status === 'published' && !isDelistedDocument(page))
|
|
2599
|
+
.map((page) => buildSearchPageItem(page, page.url));
|
|
2600
|
+
|
|
2601
|
+
return [...posts, ...frontPageItems, ...pageItems];
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
function buildSearchPageItem(page, url) {
|
|
2605
|
+
return {
|
|
2606
|
+
id: `page:${page.slug}`,
|
|
2607
|
+
type: 'page',
|
|
2608
|
+
title: page.title,
|
|
2609
|
+
url,
|
|
2610
|
+
excerpt: normalizeSearchText(page.excerpt),
|
|
2611
|
+
headings: buildSearchHeadings(page.toc),
|
|
2612
|
+
categories: [],
|
|
2613
|
+
tags: [],
|
|
2614
|
+
published_at_iso: '',
|
|
2615
|
+
updated_at_iso: '',
|
|
2616
|
+
content_text: htmlToSearchText(page.html),
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
function buildSearchHeadings(toc) {
|
|
2621
|
+
return Array.isArray(toc)
|
|
2622
|
+
? toc.map((item) => normalizeSearchText(item?.title)).filter(Boolean)
|
|
2623
|
+
: [];
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function htmlToSearchText(html) {
|
|
2627
|
+
return normalizeSearchText(decodeHtmlEntities(
|
|
2628
|
+
String(html || '')
|
|
2629
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
2630
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
2631
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
2632
|
+
.replace(/<[^>]+>/g, ' '),
|
|
2633
|
+
));
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function decodeHtmlEntities(value) {
|
|
2637
|
+
const namedEntities = {
|
|
2638
|
+
amp: '&',
|
|
2639
|
+
lt: '<',
|
|
2640
|
+
gt: '>',
|
|
2641
|
+
quot: '"',
|
|
2642
|
+
apos: "'",
|
|
2643
|
+
nbsp: ' ',
|
|
2644
|
+
};
|
|
2645
|
+
|
|
2646
|
+
return String(value || '').replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]+);/g, (match, entity) => {
|
|
2647
|
+
if (entity.startsWith('#x')) {
|
|
2648
|
+
const codePoint = Number.parseInt(entity.slice(2), 16);
|
|
2649
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
|
|
2650
|
+
}
|
|
2651
|
+
if (entity.startsWith('#')) {
|
|
2652
|
+
const codePoint = Number.parseInt(entity.slice(1), 10);
|
|
2653
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
|
|
2654
|
+
}
|
|
2655
|
+
return Object.prototype.hasOwnProperty.call(namedEntities, entity) ? namedEntities[entity] : match;
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
function normalizeSearchText(value) {
|
|
2660
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function buildSearchAdapterJs() {
|
|
2664
|
+
const fieldWeightsJson = JSON.stringify(SEARCH_FIELD_WEIGHTS, null, 2);
|
|
2665
|
+
return `const FIELD_WEIGHTS = ${fieldWeightsJson};
|
|
2666
|
+
const FIELD_NAMES = Object.keys(FIELD_WEIGHTS);
|
|
2667
|
+
const RECENCY_BOOST_MAX = ${SEARCH_RECENCY_BOOST_MAX};
|
|
2668
|
+
const DEFAULT_LIMIT = 20;
|
|
2669
|
+
const BM25_K1 = 1.2;
|
|
2670
|
+
const BM25_B = 0.75;
|
|
2671
|
+
|
|
2672
|
+
let preparedIndexPromise;
|
|
2673
|
+
|
|
2674
|
+
export async function preload() {
|
|
2675
|
+
await loadPreparedIndex();
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
export async function search(query, options = {}) {
|
|
2679
|
+
const prepared = await loadPreparedIndex();
|
|
2680
|
+
const terms = tokenize(query);
|
|
2681
|
+
const phrase = normalizeText(query);
|
|
2682
|
+
if (terms.length === 0 && !phrase) {
|
|
2683
|
+
return { results: [] };
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
const hits = [];
|
|
2687
|
+
for (const document of prepared.documents) {
|
|
2688
|
+
const score = scoreDocument(document, terms, phrase, prepared);
|
|
2689
|
+
if (score > 0) {
|
|
2690
|
+
hits.push({ document, score });
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
const limit = normalizeLimit(options.limit);
|
|
2695
|
+
hits.sort((left, right) => right.score - left.score || left.document.raw.title.localeCompare(right.document.raw.title));
|
|
2696
|
+
|
|
2697
|
+
return {
|
|
2698
|
+
results: hits.slice(0, limit).map((hit) => ({
|
|
2699
|
+
id: hit.document.raw.id,
|
|
2700
|
+
score: Number(hit.score.toFixed(6)),
|
|
2701
|
+
data: async () => buildResultData(hit.document.raw, query),
|
|
2702
|
+
})),
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
async function loadPreparedIndex() {
|
|
2707
|
+
if (!preparedIndexPromise) {
|
|
2708
|
+
preparedIndexPromise = fetch(new URL('./search.json', import.meta.url))
|
|
2709
|
+
.then((response) => {
|
|
2710
|
+
if (!response.ok) {
|
|
2711
|
+
throw new Error('ZeroPress search index not found');
|
|
2712
|
+
}
|
|
2713
|
+
return response.json();
|
|
2714
|
+
})
|
|
2715
|
+
.then((items) => prepareIndex(Array.isArray(items) ? items : []));
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
return preparedIndexPromise;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
function prepareIndex(items) {
|
|
2722
|
+
const documents = items.map((item) => prepareDocument(item));
|
|
2723
|
+
const documentFrequencies = new Map();
|
|
2724
|
+
const averageLengths = Object.fromEntries(FIELD_NAMES.map((fieldName) => [fieldName, 1]));
|
|
2725
|
+
const newestPostTime = documents.reduce((newest, document) => {
|
|
2726
|
+
if (document.raw.type !== 'post') {
|
|
2727
|
+
return newest;
|
|
2728
|
+
}
|
|
2729
|
+
return Math.max(newest, document.publishedTime || 0);
|
|
2730
|
+
}, 0);
|
|
2731
|
+
|
|
2732
|
+
for (const document of documents) {
|
|
2733
|
+
const seenTerms = new Set();
|
|
2734
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2735
|
+
for (const term of document.fieldTokens[fieldName]) {
|
|
2736
|
+
seenTerms.add(term);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
for (const term of seenTerms) {
|
|
2740
|
+
documentFrequencies.set(term, (documentFrequencies.get(term) || 0) + 1);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2745
|
+
const total = documents.reduce((sum, document) => sum + document.fieldLengths[fieldName], 0);
|
|
2746
|
+
averageLengths[fieldName] = documents.length > 0 ? Math.max(1, total / documents.length) : 1;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
return {
|
|
2750
|
+
documents,
|
|
2751
|
+
documentFrequencies,
|
|
2752
|
+
averageLengths,
|
|
2753
|
+
documentCount: documents.length,
|
|
2754
|
+
newestPostTime,
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function prepareDocument(item) {
|
|
2759
|
+
const raw = normalizeItem(item);
|
|
2760
|
+
const fields = {
|
|
2761
|
+
title: raw.title,
|
|
2762
|
+
headings: raw.headings.join(' '),
|
|
2763
|
+
tags: raw.tags.join(' '),
|
|
2764
|
+
categories: raw.categories.join(' '),
|
|
2765
|
+
excerpt: raw.excerpt,
|
|
2766
|
+
content_text: raw.content_text,
|
|
2767
|
+
};
|
|
2768
|
+
const fieldTexts = {};
|
|
2769
|
+
const fieldTokens = {};
|
|
2770
|
+
const fieldTermCounts = {};
|
|
2771
|
+
const fieldLengths = {};
|
|
2772
|
+
|
|
2773
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
2774
|
+
const normalizedText = normalizeText(value);
|
|
2775
|
+
const tokens = tokenize(normalizedText);
|
|
2776
|
+
fieldTexts[fieldName] = normalizedText;
|
|
2777
|
+
fieldTokens[fieldName] = tokens;
|
|
2778
|
+
fieldTermCounts[fieldName] = countTerms(tokens);
|
|
2779
|
+
fieldLengths[fieldName] = Math.max(1, tokens.length);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
return {
|
|
2783
|
+
raw,
|
|
2784
|
+
fieldTexts,
|
|
2785
|
+
fieldTokens,
|
|
2786
|
+
fieldTermCounts,
|
|
2787
|
+
fieldLengths,
|
|
2788
|
+
publishedTime: Date.parse(raw.published_at_iso) || 0,
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function normalizeItem(item) {
|
|
2793
|
+
return {
|
|
2794
|
+
id: String(item && item.id || ''),
|
|
2795
|
+
type: item && item.type === 'page' ? 'page' : 'post',
|
|
2796
|
+
title: String(item && item.title || ''),
|
|
2797
|
+
url: String(item && item.url || ''),
|
|
2798
|
+
excerpt: String(item && item.excerpt || ''),
|
|
2799
|
+
headings: Array.isArray(item && item.headings) ? item.headings.map(String) : [],
|
|
2800
|
+
categories: Array.isArray(item && item.categories) ? item.categories.map(String) : [],
|
|
2801
|
+
tags: Array.isArray(item && item.tags) ? item.tags.map(String) : [],
|
|
2802
|
+
published_at_iso: String(item && item.published_at_iso || ''),
|
|
2803
|
+
updated_at_iso: String(item && item.updated_at_iso || ''),
|
|
2804
|
+
content_text: String(item && item.content_text || ''),
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function scoreDocument(document, terms, phrase, prepared) {
|
|
2809
|
+
let score = 0;
|
|
2810
|
+
const uniqueTerms = Array.from(new Set(terms));
|
|
2811
|
+
|
|
2812
|
+
for (const term of uniqueTerms) {
|
|
2813
|
+
const documentFrequency = prepared.documentFrequencies.get(term) || 0;
|
|
2814
|
+
const idf = Math.log(1 + (prepared.documentCount - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
2815
|
+
|
|
2816
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2817
|
+
const termFrequency = document.fieldTermCounts[fieldName].get(term) || 0;
|
|
2818
|
+
if (termFrequency === 0) {
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
const fieldLength = document.fieldLengths[fieldName];
|
|
2823
|
+
const averageLength = prepared.averageLengths[fieldName];
|
|
2824
|
+
const denominator = termFrequency + BM25_K1 * (1 - BM25_B + BM25_B * (fieldLength / averageLength));
|
|
2825
|
+
score += FIELD_WEIGHTS[fieldName] * idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
if (phrase && phrase.length > 1) {
|
|
2830
|
+
for (const fieldName of FIELD_NAMES) {
|
|
2831
|
+
if (document.fieldTexts[fieldName].includes(phrase)) {
|
|
2832
|
+
score += FIELD_WEIGHTS[fieldName] * 0.6;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
if (score <= 0) {
|
|
2838
|
+
return 0;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
return score * (1 + recencyBoost(document, prepared.newestPostTime));
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function recencyBoost(document, newestPostTime) {
|
|
2845
|
+
if (document.raw.type !== 'post' || !document.publishedTime || !newestPostTime) {
|
|
2846
|
+
return 0;
|
|
2847
|
+
}
|
|
2848
|
+
const ageMs = Math.max(0, newestPostTime - document.publishedTime);
|
|
2849
|
+
const halfLifeMs = 180 * 24 * 60 * 60 * 1000;
|
|
2850
|
+
return RECENCY_BOOST_MAX * Math.exp(-ageMs / halfLifeMs);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
function buildResultData(item, query) {
|
|
2854
|
+
const excerpt = buildExcerpt(item, query);
|
|
2855
|
+
return {
|
|
2856
|
+
url: item.url,
|
|
2857
|
+
excerpt,
|
|
2858
|
+
plain_excerpt: excerpt,
|
|
2859
|
+
meta: {
|
|
2860
|
+
title: item.title,
|
|
2861
|
+
type: item.type,
|
|
2862
|
+
published_at_iso: item.published_at_iso,
|
|
2863
|
+
updated_at_iso: item.updated_at_iso,
|
|
2864
|
+
categories: item.categories,
|
|
2865
|
+
tags: item.tags,
|
|
2866
|
+
},
|
|
2867
|
+
sub_results: [],
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
function buildExcerpt(item, query) {
|
|
2872
|
+
const explicitExcerpt = String(item.excerpt || '').trim();
|
|
2873
|
+
if (explicitExcerpt) {
|
|
2874
|
+
return explicitExcerpt;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
const text = String(item.content_text || '').replace(/\\s+/g, ' ').trim();
|
|
2878
|
+
if (!text) {
|
|
2879
|
+
return '';
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
const normalizedText = normalizeText(text);
|
|
2883
|
+
const terms = tokenize(query);
|
|
2884
|
+
const firstMatch = terms.map((term) => normalizedText.indexOf(term)).filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
2885
|
+
const start = Math.max(0, (firstMatch || 0) - 80);
|
|
2886
|
+
const end = Math.min(text.length, start + 180);
|
|
2887
|
+
const prefix = start > 0 ? '...' : '';
|
|
2888
|
+
const suffix = end < text.length ? '...' : '';
|
|
2889
|
+
return prefix + text.slice(start, end).trim() + suffix;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function countTerms(tokens) {
|
|
2893
|
+
const counts = new Map();
|
|
2894
|
+
for (const token of tokens) {
|
|
2895
|
+
counts.set(token, (counts.get(token) || 0) + 1);
|
|
2896
|
+
}
|
|
2897
|
+
return counts;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
function tokenize(value) {
|
|
2901
|
+
const text = normalizeText(value);
|
|
2902
|
+
if (!text) {
|
|
2903
|
+
return [];
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
const tokens = [];
|
|
2907
|
+
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
2908
|
+
try {
|
|
2909
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
|
|
2910
|
+
for (const part of segmenter.segment(text)) {
|
|
2911
|
+
if (part.isWordLike && isUsefulToken(part.segment)) {
|
|
2912
|
+
tokens.push(part.segment);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
} catch {
|
|
2916
|
+
// Fall through to regex tokenization.
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
for (const match of text.matchAll(/[\\p{Letter}\\p{Number}]+/gu)) {
|
|
2921
|
+
if (isUsefulToken(match[0])) {
|
|
2922
|
+
tokens.push(match[0]);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
for (const match of text.matchAll(/[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]+/gu)) {
|
|
2927
|
+
tokens.push(...buildNgrams(match[0], 2));
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
return tokens;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function buildNgrams(value, size) {
|
|
2934
|
+
const normalized = Array.from(value);
|
|
2935
|
+
if (normalized.length <= size) {
|
|
2936
|
+
return isUsefulToken(value) ? [value] : [];
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
const tokens = [];
|
|
2940
|
+
for (let index = 0; index <= normalized.length - size; index += 1) {
|
|
2941
|
+
tokens.push(normalized.slice(index, index + size).join(''));
|
|
2942
|
+
}
|
|
2943
|
+
tokens.push(value);
|
|
2944
|
+
return tokens;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
function isUsefulToken(value) {
|
|
2948
|
+
const token = String(value || '').trim();
|
|
2949
|
+
return token.length > 1 || /^\\d$/.test(token);
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
function normalizeText(value) {
|
|
2953
|
+
return String(value || '')
|
|
2954
|
+
.normalize('NFKC')
|
|
2955
|
+
.toLowerCase()
|
|
2956
|
+
.replace(/[\\u2018\\u2019]/g, "'")
|
|
2957
|
+
.replace(/[_-]+/g, ' ')
|
|
2958
|
+
.replace(/\\s+/g, ' ')
|
|
2959
|
+
.trim();
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
function normalizeLimit(value) {
|
|
2963
|
+
return Number.isInteger(value) && value > 0 ? Math.min(value, 100) : DEFAULT_LIMIT;
|
|
2964
|
+
}
|
|
2965
|
+
`;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
function buildSitemapXml(site, emitted, generatedAt, stylesheetHref = '') {
|
|
2401
2969
|
const entries = [
|
|
2402
|
-
...(emitted.frontPage
|
|
2970
|
+
...(emitted.frontPage && emitted.frontPage.includeInSitemap !== false
|
|
2403
2971
|
? [{
|
|
2404
2972
|
url: emitted.frontPage.url,
|
|
2405
2973
|
changefreq: 'daily',
|
|
@@ -2436,7 +3004,12 @@ function buildSitemapXml(site, emitted, generatedAt) {
|
|
|
2436
3004
|
return ` <url>\n <loc>${loc}</loc>${lastmod}\n <changefreq>${entry.changefreq}</changefreq>\n <priority>${entry.priority.toFixed(1)}</priority>\n </url>`;
|
|
2437
3005
|
}).join('\n');
|
|
2438
3006
|
|
|
2439
|
-
|
|
3007
|
+
const normalizedStylesheetHref = normalizeOptionalString(stylesheetHref);
|
|
3008
|
+
const stylesheet = normalizedStylesheetHref
|
|
3009
|
+
? `\n<?xml-stylesheet type="text/xsl" href="${escapeXml(normalizedStylesheetHref)}"?>`
|
|
3010
|
+
: '';
|
|
3011
|
+
|
|
3012
|
+
return `<?xml version="1.0" encoding="UTF-8"?>${stylesheet}\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>`;
|
|
2440
3013
|
}
|
|
2441
3014
|
|
|
2442
3015
|
function buildFeedXml(site, emitted, generatedAt) {
|