@zeropress/build-core 0.1.15 → 0.1.16
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/api/build-site.js +150 -387
- package/src/index.d.ts +2 -21
- package/src/render/partial-resolver.js +23 -0
- package/src/render/variable-resolver.js +0 -5
- package/src/render/zeropress-engine.js +12 -2
- package/src/theme/load-theme-dir.js +5 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeropress/build-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Shared ZeroPress build core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@zeropress/slug-policy": "^0.1.0",
|
|
37
37
|
"@zeropress/preview-data-validator": "^0.4.4",
|
|
38
|
-
"@zeropress/theme-validator": "^0.
|
|
38
|
+
"@zeropress/theme-validator": "^0.4.0",
|
|
39
39
|
"highlight.js": "^11.11.1",
|
|
40
40
|
"markdown-it": "^14.1.0",
|
|
41
41
|
"markdown-it-anchor": "^9.2.0"
|
package/src/api/build-site.js
CHANGED
|
@@ -17,8 +17,6 @@ const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
|
|
17
17
|
const DEFAULT_TIME_FORMAT = 'HH:mm';
|
|
18
18
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
19
19
|
const DEFAULT_LOCALE = 'en-US';
|
|
20
|
-
const DEFAULT_THEME_RUNTIME = '0.3';
|
|
21
|
-
const THEME_RUNTIME_V0_4 = '0.4';
|
|
22
20
|
const COMMENT_POLICY_OUTPUT_PATH = '_zeropress/comment-policy.json';
|
|
23
21
|
const OUTPUT_PATH_CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/;
|
|
24
22
|
const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:']);
|
|
@@ -89,15 +87,13 @@ async function createBuildState(input, options) {
|
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
assertPreviewData(input.previewData);
|
|
92
|
-
await
|
|
90
|
+
const themePackage = await normalizeAndValidateThemePackage(input.themePackage);
|
|
93
91
|
|
|
94
92
|
const engine = new ZeroPressEngine();
|
|
95
93
|
const assetProcessor = new AssetProcessor();
|
|
96
94
|
const summaries = [];
|
|
97
|
-
const themePackage = input.themePackage;
|
|
98
95
|
const previewData = normalizePreviewData(input.previewData);
|
|
99
96
|
const renderData = createRenderData(previewData, themePackage.metadata);
|
|
100
|
-
const themeRuntime = normalizeThemeRuntime(themePackage.metadata?.runtime);
|
|
101
97
|
|
|
102
98
|
engine.initialize(themePackage);
|
|
103
99
|
|
|
@@ -114,8 +110,7 @@ async function createBuildState(input, options) {
|
|
|
114
110
|
writer: input.writer,
|
|
115
111
|
previewData,
|
|
116
112
|
renderData,
|
|
117
|
-
|
|
118
|
-
widgets: buildWidgetsForTheme(previewData, renderData, themeRuntime),
|
|
113
|
+
widgets: resolveWidgetAreas(previewData, renderData),
|
|
119
114
|
engine,
|
|
120
115
|
assetProcessor,
|
|
121
116
|
summaries,
|
|
@@ -375,11 +370,14 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
375
370
|
}
|
|
376
371
|
}
|
|
377
372
|
|
|
378
|
-
const
|
|
373
|
+
const preparedPosts = previewData.content.posts.map((post) => preparePost(post, previewData.site, authorsById, categoriesBySlug, tagsBySlug, themeSupportsComments));
|
|
374
|
+
const posts = preparedPosts.map((post, index) => ({
|
|
375
|
+
...post,
|
|
376
|
+
prev: index > 0 ? buildAdjacentPostSummary(preparedPosts[index - 1]) : null,
|
|
377
|
+
next: index < preparedPosts.length - 1 ? buildAdjacentPostSummary(preparedPosts[index + 1]) : null,
|
|
378
|
+
}));
|
|
379
379
|
const pages = previewData.content.pages.map((page) => preparePage(page));
|
|
380
380
|
const postBySlug = new Map(posts.map((post) => [post.slug, post]));
|
|
381
|
-
const categoryLinks = renderCategoryLinks(previewData.content.categories, categoryCountBySlug);
|
|
382
|
-
const tagLinks = renderTagLinks(previewData.content.tags, tagCountBySlug);
|
|
383
381
|
|
|
384
382
|
return {
|
|
385
383
|
posts,
|
|
@@ -393,10 +391,8 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
393
391
|
path: entry.path,
|
|
394
392
|
page: entry.page,
|
|
395
393
|
totalPages: entry.totalPages,
|
|
396
|
-
posts:
|
|
397
|
-
|
|
398
|
-
tags: tagLinks,
|
|
399
|
-
pagination: renderPagination(entry.paginationData),
|
|
394
|
+
posts: buildStructuredPostCollection(entry.items, postBySlug),
|
|
395
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
400
396
|
})),
|
|
401
397
|
archiveRoutes: buildPaginatedCollection({
|
|
402
398
|
items: previewData.content.posts,
|
|
@@ -406,40 +402,34 @@ function createRenderData(previewData, themeMetadata = {}) {
|
|
|
406
402
|
path: entry.path,
|
|
407
403
|
page: entry.page,
|
|
408
404
|
totalPages: entry.totalPages,
|
|
409
|
-
|
|
410
|
-
|
|
405
|
+
archive: {
|
|
406
|
+
groups: buildArchiveGroups(entry.items, postBySlug),
|
|
407
|
+
},
|
|
408
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
411
409
|
})),
|
|
412
410
|
categoryRoutes: buildTaxonomyRoutes({
|
|
413
411
|
items: previewData.content.categories,
|
|
414
412
|
postsBySlug: categoryPostsBySlug,
|
|
413
|
+
postBySlug,
|
|
415
414
|
postsPerPage: previewData.site.postsPerPage,
|
|
416
415
|
buildBasePath: (category) => `/categories/${encodeSlugSegment(category.slug)}/`,
|
|
417
|
-
|
|
418
|
-
|
|
416
|
+
renderExtras: (category) => ({
|
|
417
|
+
taxonomy: buildTaxonomyRouteData('category', category, categoryCountBySlug),
|
|
418
|
+
}),
|
|
419
419
|
}),
|
|
420
420
|
tagRoutes: buildTaxonomyRoutes({
|
|
421
421
|
items: previewData.content.tags,
|
|
422
422
|
postsBySlug: tagPostsBySlug,
|
|
423
|
+
postBySlug,
|
|
423
424
|
postsPerPage: previewData.site.postsPerPage,
|
|
424
425
|
buildBasePath: (tag) => `/tags/${encodeSlugSegment(tag.slug)}/`,
|
|
425
|
-
|
|
426
|
-
|
|
426
|
+
renderExtras: (tag) => ({
|
|
427
|
+
taxonomy: buildTaxonomyRouteData('tag', tag, tagCountBySlug),
|
|
428
|
+
}),
|
|
427
429
|
}),
|
|
428
430
|
};
|
|
429
431
|
}
|
|
430
432
|
|
|
431
|
-
function normalizeThemeRuntime(value) {
|
|
432
|
-
return value === THEME_RUNTIME_V0_4 ? THEME_RUNTIME_V0_4 : DEFAULT_THEME_RUNTIME;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function buildWidgetsForTheme(previewData, renderData, themeRuntime) {
|
|
436
|
-
if (themeRuntime === THEME_RUNTIME_V0_4) {
|
|
437
|
-
return resolveWidgetAreas(previewData, renderData);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return renderWidgetAreas(previewData, renderData);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
433
|
function resolveWidgetAreas(previewData, renderData) {
|
|
444
434
|
if (!previewData.widgets || typeof previewData.widgets !== 'object') {
|
|
445
435
|
return {};
|
|
@@ -667,253 +657,6 @@ function resolveProfileWidget(baseWidget, settings) {
|
|
|
667
657
|
};
|
|
668
658
|
}
|
|
669
659
|
|
|
670
|
-
function renderWidgetAreas(previewData, renderData) {
|
|
671
|
-
if (!previewData.widgets || typeof previewData.widgets !== 'object') {
|
|
672
|
-
return {};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return Object.fromEntries(
|
|
676
|
-
Object.entries(previewData.widgets).map(([widgetAreaId, widgetArea]) => [
|
|
677
|
-
widgetAreaId,
|
|
678
|
-
renderWidgetArea(widgetArea, previewData, renderData, widgetAreaId),
|
|
679
|
-
]),
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function renderWidgetArea(widgetArea, previewData, renderData, widgetAreaId) {
|
|
684
|
-
if (!widgetArea || !Array.isArray(widgetArea.items) || widgetArea.items.length === 0) {
|
|
685
|
-
return '';
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
return widgetArea.items
|
|
689
|
-
.map((item, index) => renderWidgetItem(item, previewData, renderData, widgetAreaId, index))
|
|
690
|
-
.filter(Boolean)
|
|
691
|
-
.join('\n');
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function renderWidgetItem(item, previewData, renderData, widgetAreaId, index) {
|
|
695
|
-
if (!item || typeof item !== 'object') {
|
|
696
|
-
return '';
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const title = normalizeNonEmptyString(item.title, 'Widget');
|
|
700
|
-
|
|
701
|
-
switch (item.type) {
|
|
702
|
-
case 'recent-posts':
|
|
703
|
-
return renderRecentPostsWidget(title, item.settings, renderData);
|
|
704
|
-
case 'categories':
|
|
705
|
-
return renderCategoriesWidget(title, item.settings, previewData);
|
|
706
|
-
case 'tags':
|
|
707
|
-
return renderTagsWidget(title, item.settings, previewData);
|
|
708
|
-
case 'archives':
|
|
709
|
-
return renderArchivesWidget(title, item.settings, previewData);
|
|
710
|
-
case 'image':
|
|
711
|
-
return renderImageWidget(title, item.settings);
|
|
712
|
-
case 'text':
|
|
713
|
-
return renderTextWidget(title, item.settings);
|
|
714
|
-
case 'link-list':
|
|
715
|
-
return renderLinkListWidget(title, item.settings);
|
|
716
|
-
case 'search':
|
|
717
|
-
return renderSearchWidget(title, item.settings, widgetAreaId, index);
|
|
718
|
-
case 'profile':
|
|
719
|
-
return renderProfileWidget(title, item.settings);
|
|
720
|
-
default:
|
|
721
|
-
return '';
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function renderRecentPostsWidget(title, settings, renderData) {
|
|
726
|
-
const limit = clampInteger(settings?.limit, 5, 1, 20);
|
|
727
|
-
const showDate = settings?.show_date !== false;
|
|
728
|
-
const items = renderData.posts
|
|
729
|
-
.slice(0, limit)
|
|
730
|
-
.map((post) => {
|
|
731
|
-
const dateMeta = showDate
|
|
732
|
-
? `<span class="widget-list__meta">${escapeHtml(post.published_at)}</span>`
|
|
733
|
-
: '';
|
|
734
|
-
|
|
735
|
-
return `<li class="widget-list__item"><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a>${dateMeta}</li>`;
|
|
736
|
-
})
|
|
737
|
-
.join('');
|
|
738
|
-
|
|
739
|
-
if (!items) {
|
|
740
|
-
return '';
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return renderWidgetCard(title, 'recent-posts', `<ul class="widget-list">${items}</ul>`);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function renderCategoriesWidget(title, settings, previewData) {
|
|
747
|
-
const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'category_slugs');
|
|
748
|
-
const items = previewData.content.categories
|
|
749
|
-
.map((category) => {
|
|
750
|
-
const count = countBySlug.get(category.slug) || 0;
|
|
751
|
-
if (count === 0) {
|
|
752
|
-
return '';
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const suffix = settings?.show_count === true ? ` <span class="widget-taxonomy__count">(${count})</span>` : '';
|
|
756
|
-
return `<a href="/categories/${escapeHtml(encodeSlugSegment(category.slug))}/" class="category-link">${escapeHtml(category.name)}${suffix}</a>`;
|
|
757
|
-
})
|
|
758
|
-
.filter(Boolean)
|
|
759
|
-
.join('');
|
|
760
|
-
|
|
761
|
-
if (!items) {
|
|
762
|
-
return '';
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
return renderWidgetCard(title, 'categories', `<div class="taxonomy-list taxonomy-list--stack widget-taxonomy">${items}</div>`);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function renderTagsWidget(title, settings, previewData) {
|
|
769
|
-
const countBySlug = buildTaxonomyCountMap(previewData.content.posts, 'tag_slugs');
|
|
770
|
-
const limit = clampInteger(settings?.limit, 20, 1, 100);
|
|
771
|
-
const items = previewData.content.tags
|
|
772
|
-
.map((tag) => ({
|
|
773
|
-
...tag,
|
|
774
|
-
count: countBySlug.get(tag.slug) || 0,
|
|
775
|
-
}))
|
|
776
|
-
.filter((tag) => tag.count > 0)
|
|
777
|
-
.slice(0, limit)
|
|
778
|
-
.map((tag) => {
|
|
779
|
-
const suffix = settings?.show_count === true ? ` <span class="widget-taxonomy__count">(${tag.count})</span>` : '';
|
|
780
|
-
return `<a href="/tags/${escapeHtml(encodeSlugSegment(tag.slug))}/" class="tag-link">${escapeHtml(tag.name)}${suffix}</a>`;
|
|
781
|
-
})
|
|
782
|
-
.join('');
|
|
783
|
-
|
|
784
|
-
if (!items) {
|
|
785
|
-
return '';
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
return renderWidgetCard(title, 'tags', `<div class="taxonomy-list widget-taxonomy">${items}</div>`);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function renderArchivesWidget(title, settings, previewData) {
|
|
792
|
-
const limit = clampInteger(settings?.limit, 12, 1, 120);
|
|
793
|
-
const archiveEntries = buildArchiveEntries(previewData.content.posts, previewData.site).slice(0, limit);
|
|
794
|
-
const items = archiveEntries
|
|
795
|
-
.map((entry) => `<li class="widget-list__item"><a href="/archive/">${escapeHtml(entry.label)}</a><span class="widget-list__meta">${entry.count} posts</span></li>`)
|
|
796
|
-
.join('');
|
|
797
|
-
|
|
798
|
-
if (!items) {
|
|
799
|
-
return '';
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return renderWidgetCard(title, 'archives', `<ul class="widget-list">${items}</ul>`);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function renderImageWidget(title, settings) {
|
|
806
|
-
const src = normalizeOptionalString(settings?.src);
|
|
807
|
-
if (!src) {
|
|
808
|
-
return '';
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const alt = escapeHtml(normalizeOptionalString(settings?.alt));
|
|
812
|
-
const caption = normalizeOptionalString(settings?.caption);
|
|
813
|
-
const href = normalizeOptionalString(settings?.href);
|
|
814
|
-
const target = normalizeLinkTarget(settings?.target);
|
|
815
|
-
const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
|
|
816
|
-
const image = `<img class="widget-image__media" src="${escapeHtml(src)}" alt="${alt}">`;
|
|
817
|
-
const linkedImage = href ? `<a href="${escapeHtml(href)}" target="${target}"${rel}>${image}</a>` : image;
|
|
818
|
-
const captionHtml = caption ? `<figcaption class="widget-image__caption">${escapeHtml(caption)}</figcaption>` : '';
|
|
819
|
-
|
|
820
|
-
return renderWidgetCard(title, 'image', `<figure class="widget-image">${linkedImage}${captionHtml}</figure>`);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function renderTextWidget(title, settings) {
|
|
824
|
-
const content = typeof settings?.content === 'string' ? settings.content : '';
|
|
825
|
-
const html = renderDocumentContent(content, normalizeDocumentType(settings?.document_type));
|
|
826
|
-
if (!normalizeOptionalString(html)) {
|
|
827
|
-
return '';
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
return renderWidgetCard(title, 'text', `<div class="widget-copy">${html}</div>`);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function renderLinkListWidget(title, settings) {
|
|
834
|
-
const links = Array.isArray(settings?.links) ? settings.links : [];
|
|
835
|
-
const items = links
|
|
836
|
-
.map((link) => {
|
|
837
|
-
const label = normalizeOptionalString(link?.label);
|
|
838
|
-
const url = normalizeThemeLinkUrl(link?.url);
|
|
839
|
-
if (!label || !url) {
|
|
840
|
-
return '';
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
const target = normalizeLinkTarget(link?.target);
|
|
844
|
-
const rel = target === '_blank' ? ' rel="noreferrer noopener"' : '';
|
|
845
|
-
return `<a href="${escapeHtml(url)}" target="${target}"${rel}>${escapeHtml(label)}</a>`;
|
|
846
|
-
})
|
|
847
|
-
.filter(Boolean)
|
|
848
|
-
.join('');
|
|
849
|
-
|
|
850
|
-
if (!items) {
|
|
851
|
-
return '';
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
return renderWidgetCard(title, 'link-list', `<div class="taxonomy-list taxonomy-list--stack widget-link-list">${items}</div>`);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function renderSearchWidget(title, settings, widgetAreaId, index) {
|
|
858
|
-
const placeholder = escapeHtml(normalizeNonEmptyString(settings?.placeholder, 'Search...'));
|
|
859
|
-
const buttonLabel = escapeHtml(normalizeNonEmptyString(settings?.button_label, 'Search'));
|
|
860
|
-
const searchId = `widget-search-${widgetAreaId}-${index + 1}`;
|
|
861
|
-
|
|
862
|
-
return renderWidgetCard(title, 'search', [
|
|
863
|
-
'<div class="widget-search" data-search-root>',
|
|
864
|
-
` <label class="sr-only" for="${escapeHtml(searchId)}">${escapeHtml(title)}</label>`,
|
|
865
|
-
' <form class="search-theme-wrapper" data-search-form>',
|
|
866
|
-
` <input id="${escapeHtml(searchId)}" class="search-input" data-theme-search type="search" placeholder="${placeholder}" autocomplete="off" enterkeyhint="search">`,
|
|
867
|
-
` <button class="widget-search-button" type="submit">${buttonLabel}</button>`,
|
|
868
|
-
' </form>',
|
|
869
|
-
' <div class="search-results" data-search-results hidden></div>',
|
|
870
|
-
' <p class="search-feedback search-feedback--sidebar" data-search-feedback aria-live="polite"></p>',
|
|
871
|
-
'</div>',
|
|
872
|
-
].join('\n'));
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function renderProfileWidget(title, settings) {
|
|
876
|
-
const displayName = normalizeOptionalString(settings?.display_name);
|
|
877
|
-
const affiliation = normalizeOptionalString(settings?.affiliation);
|
|
878
|
-
const bioShort = normalizeOptionalString(settings?.bio_short);
|
|
879
|
-
const avatar = normalizeOptionalString(settings?.avatar);
|
|
880
|
-
|
|
881
|
-
if (!displayName && !bioShort && !avatar) {
|
|
882
|
-
return '';
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const avatarHtml = avatar
|
|
886
|
-
? `<img class="widget-profile__avatar" src="${escapeHtml(avatar)}" alt="${escapeHtml(displayName || title)}">`
|
|
887
|
-
: '';
|
|
888
|
-
const affiliationHtml = affiliation ? `<p class="widget-profile__affiliation">${escapeHtml(affiliation)}</p>` : '';
|
|
889
|
-
const bioHtml = bioShort ? `<p class="sidebar-copy">${escapeHtml(bioShort)}</p>` : '';
|
|
890
|
-
|
|
891
|
-
return renderWidgetCard(title, 'profile', [
|
|
892
|
-
'<div class="widget-profile">',
|
|
893
|
-
avatarHtml,
|
|
894
|
-
' <div class="widget-profile__body">',
|
|
895
|
-
displayName ? ` <p class="widget-profile__name">${escapeHtml(displayName)}</p>` : '',
|
|
896
|
-
affiliationHtml ? ` ${affiliationHtml}` : '',
|
|
897
|
-
bioHtml ? ` ${bioHtml}` : '',
|
|
898
|
-
' </div>',
|
|
899
|
-
'</div>',
|
|
900
|
-
].filter(Boolean).join('\n'));
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function renderWidgetCard(title, modifier, body) {
|
|
904
|
-
if (!normalizeOptionalString(body)) {
|
|
905
|
-
return '';
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
return [
|
|
909
|
-
`<section class="content-card sidebar-card widget-card widget-card--${escapeHtml(modifier)}">`,
|
|
910
|
-
' <p class="section-kicker">Widget</p>',
|
|
911
|
-
` <h2>${escapeHtml(title)}</h2>`,
|
|
912
|
-
` ${body}`,
|
|
913
|
-
'</section>',
|
|
914
|
-
].join('\n');
|
|
915
|
-
}
|
|
916
|
-
|
|
917
660
|
function preparePage(page) {
|
|
918
661
|
const documentType = normalizeDocumentType(page.document_type);
|
|
919
662
|
|
|
@@ -928,11 +671,28 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
928
671
|
const documentType = normalizeDocumentType(post.document_type);
|
|
929
672
|
const html = renderDocumentContent(post.content, documentType);
|
|
930
673
|
const author = authorsById.get(post.author_id);
|
|
674
|
+
const categories = post.category_slugs
|
|
675
|
+
.map((slug) => categoriesBySlug.get(slug))
|
|
676
|
+
.filter(Boolean)
|
|
677
|
+
.map((category) => ({
|
|
678
|
+
name: category.name,
|
|
679
|
+
slug: category.slug,
|
|
680
|
+
url: `/categories/${encodeSlugSegment(category.slug)}/`,
|
|
681
|
+
}));
|
|
682
|
+
const tags = post.tag_slugs
|
|
683
|
+
.map((slug) => tagsBySlug.get(slug))
|
|
684
|
+
.filter(Boolean)
|
|
685
|
+
.map((tag) => ({
|
|
686
|
+
name: tag.name,
|
|
687
|
+
slug: tag.slug,
|
|
688
|
+
url: `/tags/${encodeSlugSegment(tag.slug)}/`,
|
|
689
|
+
}));
|
|
931
690
|
|
|
932
691
|
return {
|
|
933
692
|
public_id: post.public_id,
|
|
934
693
|
title: post.title,
|
|
935
694
|
slug: post.slug,
|
|
695
|
+
url: `/posts/${encodeSlugSegment(post.slug)}/`,
|
|
936
696
|
content: post.content,
|
|
937
697
|
document_type: documentType,
|
|
938
698
|
excerpt: post.excerpt,
|
|
@@ -944,14 +704,17 @@ function preparePost(post, site, authorsById, categoriesBySlug, tagsBySlug, them
|
|
|
944
704
|
allow_comments: post.allow_comments,
|
|
945
705
|
category_slugs: post.category_slugs,
|
|
946
706
|
tag_slugs: post.tag_slugs,
|
|
707
|
+
author: {
|
|
708
|
+
id: post.author_id,
|
|
709
|
+
display_name: normalizeNonEmptyString(author?.display_name, post.author_id),
|
|
710
|
+
avatar: author?.avatar || '',
|
|
711
|
+
},
|
|
712
|
+
categories,
|
|
713
|
+
tags,
|
|
947
714
|
html,
|
|
948
|
-
author_name: normalizeNonEmptyString(author?.display_name, post.author_id),
|
|
949
|
-
author_avatar: author?.avatar,
|
|
950
715
|
published_at: formatTimestamp(post.published_at_iso, site),
|
|
951
716
|
updated_at: formatTimestamp(post.updated_at_iso, site),
|
|
952
717
|
reading_time: calculateReadingTime(html),
|
|
953
|
-
categories_html: renderInlineTaxonomyLinks(post.category_slugs, categoriesBySlug, 'category'),
|
|
954
|
-
tags_html: renderInlineTaxonomyLinks(post.tag_slugs, tagsBySlug, 'tag'),
|
|
955
718
|
comments_enabled: themeSupportsComments && site.disallowComments !== true && post.allow_comments === true,
|
|
956
719
|
};
|
|
957
720
|
}
|
|
@@ -989,8 +752,8 @@ function buildTaxonomyRoutes(options) {
|
|
|
989
752
|
slug: item.slug,
|
|
990
753
|
page: entry.page,
|
|
991
754
|
totalPages: entry.totalPages,
|
|
992
|
-
posts:
|
|
993
|
-
pagination:
|
|
755
|
+
posts: buildStructuredPostCollection(entry.items, options.postBySlug),
|
|
756
|
+
pagination: buildStructuredPagination(entry.paginationData),
|
|
994
757
|
...options.renderExtras(item),
|
|
995
758
|
});
|
|
996
759
|
}
|
|
@@ -1022,6 +785,12 @@ function buildPaginatedCollection(options) {
|
|
|
1022
785
|
return pages;
|
|
1023
786
|
}
|
|
1024
787
|
|
|
788
|
+
function buildStructuredPostCollection(posts, postBySlug) {
|
|
789
|
+
return {
|
|
790
|
+
items: buildStructuredPostItems(posts, postBySlug),
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
1025
794
|
function buildPaginatedPath(basePath, page) {
|
|
1026
795
|
const normalizedBasePath = normalizePaginationBasePath(basePath);
|
|
1027
796
|
if (page <= 1) {
|
|
@@ -1062,124 +831,99 @@ function buildPaginationData(currentPage, totalPages, totalPosts, basePath) {
|
|
|
1062
831
|
};
|
|
1063
832
|
}
|
|
1064
833
|
|
|
1065
|
-
function
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
834
|
+
function buildStructuredPagination(paginationData) {
|
|
835
|
+
return {
|
|
836
|
+
current_page: paginationData.currentPage,
|
|
837
|
+
total_pages: paginationData.totalPages,
|
|
838
|
+
total_items: paginationData.totalPosts,
|
|
839
|
+
has_prev: paginationData.hasPrev,
|
|
840
|
+
has_next: paginationData.hasNext,
|
|
841
|
+
has_multiple_pages: paginationData.totalPages > 1,
|
|
842
|
+
prev_url: paginationData.prevUrl || '',
|
|
843
|
+
next_url: paginationData.nextUrl || '',
|
|
844
|
+
pages: paginationData.pages.map((page) => ({
|
|
845
|
+
number: page.number,
|
|
846
|
+
url: page.url,
|
|
847
|
+
current: page.current,
|
|
848
|
+
})),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
1069
851
|
|
|
852
|
+
function buildStructuredPostItems(posts, postBySlug) {
|
|
1070
853
|
return posts
|
|
1071
|
-
.map((post) =>
|
|
1072
|
-
const prepared = postBySlug.get(post.slug);
|
|
1073
|
-
if (!prepared) {
|
|
1074
|
-
return '';
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
return [
|
|
1078
|
-
'<article class="post-list-item">',
|
|
1079
|
-
` <h2><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a></h2>`,
|
|
1080
|
-
` <div class="post-meta"><time datetime="${escapeHtml(prepared.published_at_iso)}">${escapeHtml(prepared.published_at)}</time> • ${escapeHtml(prepared.reading_time)}</div>`,
|
|
1081
|
-
` <div class="post-excerpt">${escapeHtml(prepared.excerpt)}</div>`,
|
|
1082
|
-
'</article>',
|
|
1083
|
-
].join('\n');
|
|
1084
|
-
})
|
|
854
|
+
.map((post) => postBySlug.get(post.slug))
|
|
1085
855
|
.filter(Boolean)
|
|
1086
|
-
.
|
|
856
|
+
.map((post) => buildStructuredPostSummary(post));
|
|
1087
857
|
}
|
|
1088
858
|
|
|
1089
|
-
function
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
.map((
|
|
1105
|
-
|
|
1106
|
-
.map((post) => {
|
|
1107
|
-
const prepared = postBySlug.get(post.slug);
|
|
1108
|
-
if (!prepared) {
|
|
1109
|
-
return '';
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
return `<li><a href="/posts/${escapeHtml(encodeSlugSegment(post.slug))}/">${escapeHtml(post.title)}</a> <time datetime="${escapeHtml(prepared.published_at_iso)}">${escapeHtml(prepared.published_at)}</time></li>`;
|
|
1113
|
-
})
|
|
1114
|
-
.filter(Boolean)
|
|
1115
|
-
.join('\n');
|
|
1116
|
-
|
|
1117
|
-
return `<section class="archive-group"><h2>${escapeHtml(key)}</h2><ul>${items}</ul></section>`;
|
|
1118
|
-
})
|
|
1119
|
-
.join('\n');
|
|
859
|
+
function buildStructuredPostSummary(post) {
|
|
860
|
+
return {
|
|
861
|
+
title: post.title,
|
|
862
|
+
slug: post.slug,
|
|
863
|
+
url: post.url,
|
|
864
|
+
excerpt: post.excerpt,
|
|
865
|
+
published_at: post.published_at,
|
|
866
|
+
published_at_iso: post.published_at_iso,
|
|
867
|
+
reading_time: post.reading_time,
|
|
868
|
+
featured_image: post.featured_image,
|
|
869
|
+
author: {
|
|
870
|
+
display_name: post.author?.display_name || '',
|
|
871
|
+
avatar: post.author?.avatar || '',
|
|
872
|
+
},
|
|
873
|
+
categories: Array.isArray(post.categories) ? post.categories.map((category) => ({ ...category })) : [],
|
|
874
|
+
tags: Array.isArray(post.tags) ? post.tags.map((tag) => ({ ...tag })) : [],
|
|
875
|
+
};
|
|
1120
876
|
}
|
|
1121
877
|
|
|
1122
|
-
function
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if (count === 0) {
|
|
1127
|
-
return null;
|
|
1128
|
-
}
|
|
878
|
+
function buildAdjacentPostSummary(post) {
|
|
879
|
+
if (!post) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
1129
882
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
.
|
|
1133
|
-
.
|
|
883
|
+
return {
|
|
884
|
+
title: post.title,
|
|
885
|
+
slug: post.slug,
|
|
886
|
+
url: post.url,
|
|
887
|
+
excerpt: post.excerpt,
|
|
888
|
+
published_at: post.published_at,
|
|
889
|
+
published_at_iso: post.published_at_iso,
|
|
890
|
+
};
|
|
1134
891
|
}
|
|
1135
892
|
|
|
1136
|
-
function
|
|
1137
|
-
|
|
1138
|
-
.map((tag) => {
|
|
1139
|
-
const count = countBySlug.get(tag.slug) || 0;
|
|
1140
|
-
if (count === 0) {
|
|
1141
|
-
return null;
|
|
1142
|
-
}
|
|
893
|
+
function buildArchiveGroups(posts, postBySlug) {
|
|
894
|
+
const groups = new Map();
|
|
1143
895
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
}
|
|
896
|
+
for (const post of posts) {
|
|
897
|
+
const prepared = postBySlug.get(post.slug);
|
|
898
|
+
if (!prepared?.published_at_iso) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
1149
901
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
.
|
|
1153
|
-
.
|
|
1154
|
-
.
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
902
|
+
const date = toDate(prepared.published_at_iso);
|
|
903
|
+
const year = date.getUTCFullYear();
|
|
904
|
+
const month = date.getUTCMonth() + 1;
|
|
905
|
+
const label = `${year}-${String(month).padStart(2, '0')}`;
|
|
906
|
+
const current = groups.get(label) || {
|
|
907
|
+
label,
|
|
908
|
+
year,
|
|
909
|
+
month,
|
|
910
|
+
items: [],
|
|
911
|
+
};
|
|
1159
912
|
|
|
1160
|
-
|
|
1161
|
-
|
|
913
|
+
current.items.push(buildStructuredPostSummary(prepared));
|
|
914
|
+
groups.set(label, current);
|
|
1162
915
|
}
|
|
1163
916
|
|
|
1164
|
-
return
|
|
917
|
+
return Array.from(groups.values());
|
|
1165
918
|
}
|
|
1166
919
|
|
|
1167
|
-
function
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
: '';
|
|
1175
|
-
const nextLink = paginationData.hasNext && paginationData.nextUrl
|
|
1176
|
-
? `<a href="${paginationData.nextUrl}" class="next">Next</a>`
|
|
1177
|
-
: '';
|
|
1178
|
-
const pageLinks = paginationData.pages
|
|
1179
|
-
.map((page) => `<a href="${page.url}" class="page-link ${page.current ? 'current' : ''}">${page.number}</a>`)
|
|
1180
|
-
.join(' ');
|
|
1181
|
-
|
|
1182
|
-
return `<nav class="pagination">\n ${prevLink}\n <span class="pages">${pageLinks}</span>\n ${nextLink}\n</nav>`;
|
|
920
|
+
function buildTaxonomyRouteData(kind, item, countBySlug) {
|
|
921
|
+
return {
|
|
922
|
+
kind,
|
|
923
|
+
slug: item.slug,
|
|
924
|
+
name: item.name,
|
|
925
|
+
count: countBySlug.get(item.slug) || 0,
|
|
926
|
+
};
|
|
1183
927
|
}
|
|
1184
928
|
|
|
1185
929
|
function buildTaxonomyCountMap(posts, fieldName) {
|
|
@@ -1351,7 +1095,7 @@ function recordRouteEmission(state, templateName, route, currentUrl) {
|
|
|
1351
1095
|
});
|
|
1352
1096
|
}
|
|
1353
1097
|
|
|
1354
|
-
async function
|
|
1098
|
+
async function normalizeAndValidateThemePackage(themePackage) {
|
|
1355
1099
|
if (!themePackage?.templates || !themePackage?.partials || !themePackage?.assets || !themePackage?.metadata) {
|
|
1356
1100
|
throw new Error('Invalid themePackage: expected metadata, templates, partials, and assets');
|
|
1357
1101
|
}
|
|
@@ -1389,6 +1133,25 @@ async function assertValidThemePackage(themePackage) {
|
|
|
1389
1133
|
if (!validation.ok) {
|
|
1390
1134
|
throw new Error(`Theme validation failed: ${validation.errors[0]?.message || 'Unknown error'}`);
|
|
1391
1135
|
}
|
|
1136
|
+
|
|
1137
|
+
if (!validation.manifest) {
|
|
1138
|
+
throw new Error('Theme validation failed: normalized manifest not available');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return {
|
|
1142
|
+
metadata: normalizeThemePackageMetadata(themePackage.metadata, validation.manifest),
|
|
1143
|
+
templates: themePackage.templates,
|
|
1144
|
+
partials: themePackage.partials,
|
|
1145
|
+
assets: themePackage.assets,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function normalizeThemePackageMetadata(sourceMetadata, manifest) {
|
|
1150
|
+
return {
|
|
1151
|
+
...manifest,
|
|
1152
|
+
...(sourceMetadata?.thumbnail ? { thumbnail: sourceMetadata.thumbnail } : {}),
|
|
1153
|
+
settings: sourceMetadata?.settings || {},
|
|
1154
|
+
};
|
|
1392
1155
|
}
|
|
1393
1156
|
|
|
1394
1157
|
async function buildAssetOutputs(assets, assetProcessor, options) {
|
package/src/index.d.ts
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
import type { PreviewDataV05 } from '@zeropress/preview-data-validator';
|
|
2
|
+
import type { ThemeManifest } from '@zeropress/theme-validator';
|
|
2
3
|
|
|
3
4
|
export interface ThemePackage {
|
|
4
|
-
metadata: {
|
|
5
|
-
name: string;
|
|
6
|
-
version: string;
|
|
7
|
-
author?: string | null;
|
|
8
|
-
description?: string;
|
|
5
|
+
metadata: ThemeManifest & {
|
|
9
6
|
thumbnail?: string;
|
|
10
7
|
settings?: Record<string, unknown>;
|
|
11
|
-
features?: {
|
|
12
|
-
comments?: boolean;
|
|
13
|
-
newsletter?: boolean;
|
|
14
|
-
};
|
|
15
|
-
menuSlots?: Record<string, {
|
|
16
|
-
title: string;
|
|
17
|
-
description?: string;
|
|
18
|
-
}>;
|
|
19
|
-
widgetAreas?: Record<string, {
|
|
20
|
-
title: string;
|
|
21
|
-
description?: string;
|
|
22
|
-
}>;
|
|
23
|
-
namespace?: string;
|
|
24
|
-
slug?: string;
|
|
25
|
-
license?: 'MIT' | 'Apache-2.0' | 'BSD-3-Clause' | 'GPL-3.0-only' | 'GPL-3.0-or-later';
|
|
26
|
-
runtime?: '0.3' | '0.4';
|
|
27
8
|
};
|
|
28
9
|
templates: Map<string, string>;
|
|
29
10
|
partials: Map<string, string>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class PartialResolver {
|
|
2
|
+
static PARTIAL_PATTERN = /\{\{partial:([a-zA-Z_][a-zA-Z0-9_-]*(?:\/[a-zA-Z_][a-zA-Z0-9_-]*)*)\}\}/g;
|
|
3
|
+
|
|
4
|
+
resolve(template, partials) {
|
|
5
|
+
return this.resolveTemplate(String(template || ''), partials, []);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
resolveTemplate(template, partials, activeStack) {
|
|
9
|
+
return template.replace(PartialResolver.PARTIAL_PATTERN, (_, partialName) => {
|
|
10
|
+
if (!partials.has(partialName)) {
|
|
11
|
+
throw new Error(`Partial "${partialName}" not found`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (activeStack.includes(partialName)) {
|
|
15
|
+
const cycle = [...activeStack, partialName].join(' -> ');
|
|
16
|
+
throw new Error(`Circular partial reference detected: ${cycle}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const partialContent = partials.get(partialName);
|
|
20
|
+
return this.resolveTemplate(String(partialContent || ''), partials, [...activeStack, partialName]);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -2,7 +2,6 @@ export class VariableResolver {
|
|
|
2
2
|
static MENU_PATTERN = /\{\{menu:([a-z][a-z0-9_-]{0,63})\}\}/g;
|
|
3
3
|
static VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\}\}/g;
|
|
4
4
|
static STANDALONE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
|
5
|
-
static RAW_STANDALONE = new Set(['posts', 'categories', 'tags', 'pagination']);
|
|
6
5
|
|
|
7
6
|
resolve(template, data, options = {}) {
|
|
8
7
|
let result = template.replace(VariableResolver.MENU_PATTERN, (_, menuId) => {
|
|
@@ -95,10 +94,6 @@ export class VariableResolver {
|
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
if (VariableResolver.RAW_STANDALONE.has(variablePath)) {
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
97
|
const lastSegment = variablePath.split('.').pop();
|
|
103
98
|
if (lastSegment === 'html' || lastSegment?.endsWith('_html')) {
|
|
104
99
|
return true;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { SlotResolver } from './slot-resolver.js';
|
|
2
2
|
import { VariableResolver } from './variable-resolver.js';
|
|
3
3
|
import { ControlFlowRenderer } from './control-flow-renderer.js';
|
|
4
|
+
import { PartialResolver } from './partial-resolver.js';
|
|
4
5
|
|
|
5
6
|
export class ZeroPressEngine {
|
|
6
7
|
constructor() {
|
|
7
8
|
this.slotResolver = new SlotResolver();
|
|
9
|
+
this.partialResolver = new PartialResolver();
|
|
8
10
|
this.variableResolver = new VariableResolver();
|
|
9
11
|
this.controlFlowRenderer = new ControlFlowRenderer({
|
|
10
12
|
resolvePath: (data, path) => this.variableResolver.resolvePath(data, path),
|
|
@@ -33,9 +35,9 @@ export class ZeroPressEngine {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const renderData = this.combineRenderData(data, context);
|
|
36
|
-
const renderedContent = this.renderTemplate(template, renderData);
|
|
38
|
+
const renderedContent = this.renderTemplate(this.expandPartials(template), renderData);
|
|
37
39
|
const layoutWithSlots = this.slotResolver.resolve(layout, this.themePackage.partials, renderedContent);
|
|
38
|
-
return this.renderTemplate(layoutWithSlots, renderData);
|
|
40
|
+
return this.renderTemplate(this.expandPartials(layoutWithSlots), renderData);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
combineRenderData(data, context) {
|
|
@@ -58,4 +60,12 @@ export class ZeroPressEngine {
|
|
|
58
60
|
|
|
59
61
|
return this.variableResolver.resolve(template, data);
|
|
60
62
|
}
|
|
63
|
+
|
|
64
|
+
expandPartials(template) {
|
|
65
|
+
if (this.themePackage?.metadata?.runtime !== '0.4') {
|
|
66
|
+
return template;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this.partialResolver.resolve(template, this.themePackage.partials);
|
|
70
|
+
}
|
|
61
71
|
}
|
|
@@ -16,6 +16,10 @@ export async function loadThemePackageFromDir(themeDir) {
|
|
|
16
16
|
|
|
17
17
|
const rawThemeJson = String(fileMap.get('theme.json'));
|
|
18
18
|
const themeJson = JSON.parse(rawThemeJson);
|
|
19
|
+
const manifest = validation.manifest;
|
|
20
|
+
if (!manifest) {
|
|
21
|
+
throw new Error('Theme validation failed: normalized manifest not available');
|
|
22
|
+
}
|
|
19
23
|
const templates = new Map();
|
|
20
24
|
const partials = new Map();
|
|
21
25
|
const assets = new Map();
|
|
@@ -45,19 +49,9 @@ export async function loadThemePackageFromDir(themeDir) {
|
|
|
45
49
|
|
|
46
50
|
return {
|
|
47
51
|
metadata: {
|
|
48
|
-
|
|
49
|
-
version: themeJson.version,
|
|
50
|
-
author: themeJson.author,
|
|
51
|
-
description: themeJson.description,
|
|
52
|
+
...manifest,
|
|
52
53
|
thumbnail: themeJson.thumbnail,
|
|
53
54
|
settings: themeJson.settings || {},
|
|
54
|
-
features: themeJson.features,
|
|
55
|
-
menuSlots: themeJson.menuSlots,
|
|
56
|
-
widgetAreas: themeJson.widgetAreas,
|
|
57
|
-
namespace: themeJson.namespace,
|
|
58
|
-
slug: themeJson.slug,
|
|
59
|
-
license: themeJson.license,
|
|
60
|
-
runtime: themeJson.runtime,
|
|
61
55
|
},
|
|
62
56
|
templates,
|
|
63
57
|
partials,
|