mdorigin 0.1.0 → 0.1.2
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 +52 -3
- package/dist/adapters/cloudflare.d.ts +2 -0
- package/dist/adapters/cloudflare.js +17 -0
- package/dist/adapters/node.d.ts +2 -0
- package/dist/adapters/node.js +51 -11
- package/dist/cli/build-cloudflare.js +7 -1
- package/dist/cli/build-search.d.ts +1 -0
- package/dist/cli/build-search.js +44 -0
- package/dist/cli/dev.js +10 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +15 -2
- package/dist/cli/search.d.ts +1 -0
- package/dist/cli/search.js +36 -0
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.js +41 -5
- package/dist/core/api.d.ts +13 -0
- package/dist/core/api.js +160 -0
- package/dist/core/content-store.js +5 -0
- package/dist/core/content-type.d.ts +1 -0
- package/dist/core/content-type.js +3 -0
- package/dist/core/directory-index.d.ts +1 -1
- package/dist/core/directory-index.js +5 -1
- package/dist/core/markdown.d.ts +12 -0
- package/dist/core/markdown.js +88 -0
- package/dist/core/request-handler.d.ts +5 -0
- package/dist/core/request-handler.js +412 -24
- package/dist/core/router.d.ts +1 -0
- package/dist/core/router.js +1 -1
- package/dist/core/site-config.d.ts +31 -0
- package/dist/core/site-config.js +98 -2
- package/dist/html/template-kind.d.ts +1 -1
- package/dist/html/template.d.ts +17 -1
- package/dist/html/template.js +282 -21
- package/dist/html/theme.js +542 -100
- package/dist/index-builder.js +62 -29
- package/dist/search.d.ts +59 -0
- package/dist/search.js +370 -0
- package/package.json +12 -3
package/dist/core/site-config.js
CHANGED
|
@@ -32,14 +32,25 @@ export async function loadSiteConfig(options = {}) {
|
|
|
32
32
|
parsedConfig.siteDescription !== ''
|
|
33
33
|
? parsedConfig.siteDescription
|
|
34
34
|
: undefined,
|
|
35
|
+
siteUrl: normalizeSiteUrl(parsedConfig.siteUrl),
|
|
36
|
+
favicon: normalizeSiteHref(parsedConfig.favicon),
|
|
37
|
+
logo: normalizeLogo(parsedConfig.logo),
|
|
35
38
|
showDate: parsedConfig.showDate ?? true,
|
|
36
39
|
showSummary: parsedConfig.showSummary ?? true,
|
|
37
40
|
theme: isBuiltInThemeName(parsedConfig.theme) ? parsedConfig.theme : 'paper',
|
|
38
41
|
template: isTemplateName(parsedConfig.template) ? parsedConfig.template : 'document',
|
|
39
42
|
topNav: normalizeTopNav(parsedConfig.topNav),
|
|
43
|
+
footerNav: normalizeTopNav(parsedConfig.footerNav),
|
|
44
|
+
footerText: typeof parsedConfig.footerText === 'string' && parsedConfig.footerText !== ''
|
|
45
|
+
? parsedConfig.footerText
|
|
46
|
+
: undefined,
|
|
47
|
+
socialLinks: normalizeSocialLinks(parsedConfig.socialLinks),
|
|
48
|
+
editLink: normalizeEditLink(parsedConfig.editLink),
|
|
40
49
|
showHomeIndex: typeof parsedConfig.showHomeIndex === 'boolean'
|
|
41
50
|
? parsedConfig.showHomeIndex
|
|
42
51
|
: normalizeTopNav(parsedConfig.topNav).length === 0,
|
|
52
|
+
catalogInitialPostCount: normalizePositiveInteger(parsedConfig.catalogInitialPostCount, 10),
|
|
53
|
+
catalogLoadMoreStep: normalizePositiveInteger(parsedConfig.catalogLoadMoreStep, 10),
|
|
43
54
|
stylesheetContent,
|
|
44
55
|
siteTitleConfigured: typeof parsedConfig.siteTitle === 'string' && parsedConfig.siteTitle !== '',
|
|
45
56
|
siteDescriptionConfigured: typeof parsedConfig.siteDescription === 'string' &&
|
|
@@ -83,7 +94,7 @@ function isBuiltInThemeName(value) {
|
|
|
83
94
|
return value === 'paper' || value === 'atlas' || value === 'gazette';
|
|
84
95
|
}
|
|
85
96
|
function isTemplateName(value) {
|
|
86
|
-
return value === 'document' || value === '
|
|
97
|
+
return value === 'document' || value === 'catalog';
|
|
87
98
|
}
|
|
88
99
|
function isNodeNotFound(error) {
|
|
89
100
|
return (typeof error === 'object' &&
|
|
@@ -116,8 +127,93 @@ function normalizeTopNav(value) {
|
|
|
116
127
|
item.label !== '' &&
|
|
117
128
|
typeof item.href === 'string' &&
|
|
118
129
|
item.href !== '') {
|
|
119
|
-
|
|
130
|
+
const href = normalizeSiteHref(item.href);
|
|
131
|
+
return href ? [{ label: item.label, href }] : [];
|
|
120
132
|
}
|
|
121
133
|
return [];
|
|
122
134
|
});
|
|
123
135
|
}
|
|
136
|
+
function normalizePositiveInteger(value, fallback) {
|
|
137
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === 'string') {
|
|
141
|
+
const parsed = Number.parseInt(value, 10);
|
|
142
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return fallback;
|
|
147
|
+
}
|
|
148
|
+
function normalizeLogo(value) {
|
|
149
|
+
if (typeof value !== 'object' ||
|
|
150
|
+
value === null ||
|
|
151
|
+
!('src' in value) ||
|
|
152
|
+
typeof value.src !== 'string' ||
|
|
153
|
+
value.src === '') {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const src = normalizeSiteHref(value.src);
|
|
157
|
+
if (!src) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const href = 'href' in value && typeof value.href === 'string'
|
|
161
|
+
? normalizeSiteHref(value.href)
|
|
162
|
+
: undefined;
|
|
163
|
+
return {
|
|
164
|
+
src,
|
|
165
|
+
alt: 'alt' in value && typeof value.alt === 'string' && value.alt !== ''
|
|
166
|
+
? value.alt
|
|
167
|
+
: undefined,
|
|
168
|
+
href,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function normalizeSocialLinks(value) {
|
|
172
|
+
if (!Array.isArray(value)) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
return value.flatMap((item) => {
|
|
176
|
+
if (typeof item !== 'object' ||
|
|
177
|
+
item === null ||
|
|
178
|
+
!('icon' in item) ||
|
|
179
|
+
!('label' in item) ||
|
|
180
|
+
!('href' in item) ||
|
|
181
|
+
typeof item.icon !== 'string' ||
|
|
182
|
+
item.icon === '' ||
|
|
183
|
+
typeof item.label !== 'string' ||
|
|
184
|
+
item.label === '' ||
|
|
185
|
+
typeof item.href !== 'string' ||
|
|
186
|
+
item.href === '') {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const href = normalizeSiteHref(item.href);
|
|
190
|
+
return href
|
|
191
|
+
? [{ icon: item.icon, label: item.label, href }]
|
|
192
|
+
: [];
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function normalizeEditLink(value) {
|
|
196
|
+
if (typeof value !== 'object' ||
|
|
197
|
+
value === null ||
|
|
198
|
+
!('baseUrl' in value) ||
|
|
199
|
+
typeof value.baseUrl !== 'string' ||
|
|
200
|
+
value.baseUrl === '') {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
return { baseUrl: value.baseUrl };
|
|
204
|
+
}
|
|
205
|
+
function normalizeSiteUrl(value) {
|
|
206
|
+
return typeof value === 'string' && value !== '' ? value.replace(/\/+$/, '') : undefined;
|
|
207
|
+
}
|
|
208
|
+
function normalizeSiteHref(value) {
|
|
209
|
+
if (typeof value !== 'string' || value === '') {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (value.startsWith('/') ||
|
|
213
|
+
value.startsWith('#') ||
|
|
214
|
+
value.startsWith('//') ||
|
|
215
|
+
/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value)) {
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
return `/${value.replace(/^\.?\//, '')}`;
|
|
219
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type TemplateName = 'document' | '
|
|
1
|
+
export type TemplateName = 'document' | 'catalog';
|
package/dist/html/template.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { SiteNavItem } from '../core/site-config.js';
|
|
1
|
+
import type { SiteLogo, SiteNavItem, SiteSocialLink } from '../core/site-config.js';
|
|
2
|
+
import type { ManagedIndexEntry } from '../core/markdown.js';
|
|
2
3
|
import type { TemplateName } from './template-kind.js';
|
|
3
4
|
import { type BuiltInThemeName } from './theme.js';
|
|
4
5
|
export interface RenderDocumentOptions {
|
|
5
6
|
siteTitle: string;
|
|
6
7
|
siteDescription?: string;
|
|
8
|
+
siteUrl?: string;
|
|
9
|
+
favicon?: string;
|
|
10
|
+
logo?: SiteLogo;
|
|
7
11
|
title: string;
|
|
8
12
|
body: string;
|
|
9
13
|
summary?: string;
|
|
@@ -13,7 +17,19 @@ export interface RenderDocumentOptions {
|
|
|
13
17
|
theme: BuiltInThemeName;
|
|
14
18
|
template: TemplateName;
|
|
15
19
|
topNav?: SiteNavItem[];
|
|
20
|
+
footerNav?: SiteNavItem[];
|
|
21
|
+
footerText?: string;
|
|
22
|
+
socialLinks?: SiteSocialLink[];
|
|
23
|
+
editLinkHref?: string;
|
|
16
24
|
stylesheetContent?: string;
|
|
25
|
+
canonicalPath?: string;
|
|
26
|
+
alternateMarkdownPath?: string;
|
|
27
|
+
catalogEntries?: ManagedIndexEntry[];
|
|
28
|
+
catalogRequestPath?: string;
|
|
29
|
+
catalogInitialPostCount?: number;
|
|
30
|
+
catalogLoadMoreStep?: number;
|
|
31
|
+
searchEnabled?: boolean;
|
|
17
32
|
}
|
|
18
33
|
export declare function renderDocument(options: RenderDocumentOptions): string;
|
|
19
34
|
export declare function escapeHtml(value: string): string;
|
|
35
|
+
export declare function renderCatalogArticleItems(entries: readonly ManagedIndexEntry[]): string;
|
package/dist/html/template.js
CHANGED
|
@@ -8,18 +8,76 @@ export function renderDocument(options) {
|
|
|
8
8
|
const summaryMeta = options.summary
|
|
9
9
|
? `<meta name="description" content="${escapeHtml(options.summary)}">`
|
|
10
10
|
: '';
|
|
11
|
+
const canonicalMeta = options.siteUrl && options.canonicalPath
|
|
12
|
+
? `<link rel="canonical" href="${escapeHtml(`${options.siteUrl}${options.canonicalPath}`)}">`
|
|
13
|
+
: '';
|
|
14
|
+
const faviconMeta = options.favicon
|
|
15
|
+
? `<link rel="icon" href="${escapeHtml(options.favicon)}">`
|
|
16
|
+
: '';
|
|
17
|
+
const alternateMarkdownMeta = options.alternateMarkdownPath
|
|
18
|
+
? `<link rel="alternate" type="text/markdown" href="${escapeHtml(options.alternateMarkdownPath)}">`
|
|
19
|
+
: '';
|
|
11
20
|
const stylesheetBlock = `<style>${getBuiltInThemeStyles(options.theme)}${options.stylesheetContent ? `\n${options.stylesheetContent}` : ''}</style>`;
|
|
12
21
|
const navBlock = options.topNav && options.topNav.length > 0
|
|
13
22
|
? `<nav class="site-nav"><ul>${options.topNav
|
|
14
23
|
.map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
|
|
15
24
|
.join('')}</ul></nav>`
|
|
16
25
|
: '';
|
|
26
|
+
const searchToggleBlock = options.searchEnabled
|
|
27
|
+
? [
|
|
28
|
+
'<div class="site-search" data-site-search>',
|
|
29
|
+
'<button type="button" class="site-search__toggle" aria-expanded="false" aria-controls="site-search-panel">Search</button>',
|
|
30
|
+
'<div id="site-search-panel" class="site-search__panel" hidden>',
|
|
31
|
+
'<form class="site-search__form" role="search" action="/api/search" method="get">',
|
|
32
|
+
'<label class="site-search__label" for="site-search-input">Search site</label>',
|
|
33
|
+
'<div class="site-search__controls">',
|
|
34
|
+
'<input id="site-search-input" class="site-search__input" type="search" name="q" placeholder="Search docs and skills" autocomplete="off">',
|
|
35
|
+
'<button type="submit" class="site-search__submit">Go</button>',
|
|
36
|
+
'</div>',
|
|
37
|
+
'<p class="site-search__hint">Search is powered by <code>/api/search</code>.</p>',
|
|
38
|
+
'</form>',
|
|
39
|
+
'<div class="site-search__results" data-site-search-results></div>',
|
|
40
|
+
'</div>',
|
|
41
|
+
'</div>',
|
|
42
|
+
].join('')
|
|
43
|
+
: '';
|
|
44
|
+
const footerNavBlock = options.footerNav && options.footerNav.length > 0
|
|
45
|
+
? `<nav class="site-footer__nav"><ul>${options.footerNav
|
|
46
|
+
.map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
|
|
47
|
+
.join('')}</ul></nav>`
|
|
48
|
+
: '';
|
|
49
|
+
const socialLinksBlock = options.socialLinks && options.socialLinks.length > 0
|
|
50
|
+
? `<ul class="site-footer__social">${options.socialLinks
|
|
51
|
+
.map((item) => `<li><a href="${escapeHtml(item.href)}" aria-label="${escapeHtml(item.label)}" title="${escapeHtml(item.label)}">${renderSocialIcon(item.icon)}</a></li>`)
|
|
52
|
+
.join('')}</ul>`
|
|
53
|
+
: '';
|
|
17
54
|
const siteDescriptionBlock = siteDescription
|
|
18
55
|
? `<span>${siteDescription}</span>`
|
|
19
56
|
: '';
|
|
20
|
-
const
|
|
21
|
-
?
|
|
57
|
+
const logoBlock = options.logo
|
|
58
|
+
? `<span class="site-header__logo"><img src="${escapeHtml(options.logo.src)}" alt="${escapeHtml(options.logo.alt ?? '')}"></span>`
|
|
59
|
+
: '';
|
|
60
|
+
const brandHref = escapeHtml(options.logo?.href ?? '/');
|
|
61
|
+
const editLinkBlock = options.editLinkHref
|
|
62
|
+
? `<a class="site-footer__edit-link" href="${escapeHtml(options.editLinkHref)}">Edit this page</a>`
|
|
63
|
+
: '';
|
|
64
|
+
const footerTextBlock = options.footerText
|
|
65
|
+
? `<p class="site-footer__text">${escapeHtml(options.footerText)}</p>`
|
|
66
|
+
: '';
|
|
67
|
+
const footerMetaBlock = socialLinksBlock || editLinkBlock
|
|
68
|
+
? `<div class="site-footer__meta">${socialLinksBlock}${editLinkBlock}</div>`
|
|
69
|
+
: '';
|
|
70
|
+
const footerBlock = footerNavBlock || footerTextBlock || footerMetaBlock
|
|
71
|
+
? `<footer class="site-footer"><div class="site-footer__inner">${footerNavBlock}${footerTextBlock}${footerMetaBlock}</div></footer>`
|
|
72
|
+
: '';
|
|
73
|
+
const articleBody = options.template === 'catalog'
|
|
74
|
+
? renderCatalogArticle(options.body, options.catalogEntries ?? [], {
|
|
75
|
+
requestPath: options.catalogRequestPath ?? '/',
|
|
76
|
+
initialPostCount: options.catalogInitialPostCount ?? 10,
|
|
77
|
+
loadMoreStep: options.catalogLoadMoreStep ?? 10,
|
|
78
|
+
})
|
|
22
79
|
: options.body;
|
|
80
|
+
const searchScript = options.searchEnabled ? renderSearchScript() : '';
|
|
23
81
|
return [
|
|
24
82
|
'<!doctype html>',
|
|
25
83
|
'<html lang="en">',
|
|
@@ -28,35 +86,22 @@ export function renderDocument(options) {
|
|
|
28
86
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
29
87
|
`<title>${title} | ${siteTitle}</title>`,
|
|
30
88
|
summaryMeta,
|
|
89
|
+
canonicalMeta,
|
|
90
|
+
faviconMeta,
|
|
91
|
+
alternateMarkdownMeta,
|
|
31
92
|
stylesheetBlock,
|
|
32
93
|
'</head>',
|
|
33
94
|
`<body data-theme="${options.theme}" data-template="${options.template}">`,
|
|
34
|
-
`<header class="site-header"><div class="site-header__inner"><div class="site-header__brand"><p class="site-header__title"><a href="
|
|
95
|
+
`<header class="site-header"><div class="site-header__inner"><div class="site-header__brand"><p class="site-header__title"><a href="${brandHref}">${logoBlock}<span>${siteTitle}</span></a></p>${siteDescriptionBlock}</div><div class="site-header__actions">${navBlock}${searchToggleBlock}</div></div></header>`,
|
|
35
96
|
'<main>',
|
|
36
97
|
`<article>${articleBody}</article>`,
|
|
37
98
|
'</main>',
|
|
99
|
+
footerBlock,
|
|
100
|
+
searchScript,
|
|
38
101
|
'</body>',
|
|
39
102
|
'</html>',
|
|
40
103
|
].join('');
|
|
41
104
|
}
|
|
42
|
-
function renderEditorialBody(options) {
|
|
43
|
-
const heroLines = [
|
|
44
|
-
'<div class="page-intro">',
|
|
45
|
-
`<p class="page-intro__eyebrow">${escapeHtml(options.siteTitle)}</p>`,
|
|
46
|
-
`<h1 class="page-intro__title">${escapeHtml(options.title)}</h1>`,
|
|
47
|
-
];
|
|
48
|
-
if (options.showSummary !== false && options.summary) {
|
|
49
|
-
heroLines.push(`<p class="page-intro__summary">${escapeHtml(options.summary)}</p>`);
|
|
50
|
-
}
|
|
51
|
-
if (options.showDate !== false && options.date) {
|
|
52
|
-
heroLines.push(`<p class="page-intro__meta">${escapeHtml(options.date)}</p>`);
|
|
53
|
-
}
|
|
54
|
-
heroLines.push('</div>');
|
|
55
|
-
return `${heroLines.join('')}<div class="page-body">${stripLeadingH1(options.body)}</div>`;
|
|
56
|
-
}
|
|
57
|
-
function stripLeadingH1(html) {
|
|
58
|
-
return html.replace(/^\s*<h1>.*?<\/h1>\s*/s, '');
|
|
59
|
-
}
|
|
60
105
|
export function escapeHtml(value) {
|
|
61
106
|
return value
|
|
62
107
|
.replaceAll('&', '&')
|
|
@@ -65,3 +110,219 @@ export function escapeHtml(value) {
|
|
|
65
110
|
.replaceAll('"', '"')
|
|
66
111
|
.replaceAll("'", ''');
|
|
67
112
|
}
|
|
113
|
+
function renderSocialIcon(icon) {
|
|
114
|
+
switch (icon) {
|
|
115
|
+
case 'github':
|
|
116
|
+
return iconSvg('M12 2C6.48 2 2 6.58 2 12.11c0 4.43 2.87 8.18 6.84 9.5.5.1.68-.22.68-.48 0-.24-.01-1.04-.01-1.89-2.78.62-3.37-1.19-3.37-1.19-.46-1.17-1.11-1.48-1.11-1.48-.91-.63.07-.62.07-.62 1 .08 1.53 1.04 1.53 1.04.9 1.54 2.35 1.09 2.92.84.09-.66.35-1.09.63-1.34-2.22-.25-4.55-1.12-4.55-4.97 0-1.1.39-2 1.03-2.71-.1-.26-.45-1.29.1-2.68 0 0 .84-.27 2.75 1.03A9.4 9.4 0 0 1 12 6.84c.85 0 1.71.12 2.51.35 1.91-1.3 2.75-1.03 2.75-1.03.55 1.39.2 2.42.1 2.68.64.71 1.03 1.61 1.03 2.71 0 3.86-2.33 4.72-4.56 4.97.36.31.68.91.68 1.84 0 1.33-.01 2.4-.01 2.73 0 .27.18.58.69.48A10.11 10.11 0 0 0 22 12.11C22 6.58 17.52 2 12 2Z');
|
|
117
|
+
case 'rss':
|
|
118
|
+
return iconSvg('M5 3a16 16 0 0 1 16 16h-3A13 13 0 0 0 5 6V3Zm0 6a10 10 0 0 1 10 10h-3a7 7 0 0 0-7-7V9Zm0 6a4 4 0 0 1 4 4H5v-4Zm0 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z');
|
|
119
|
+
case 'npm':
|
|
120
|
+
return iconSvg('M3 7h18v10h-9v-8h-3v8H3V7Zm10 2h2v6h2V9h2v6h1V9h1V8h-8v1Z');
|
|
121
|
+
case 'x':
|
|
122
|
+
return iconSvg('M18.9 3H21l-6.87 7.85L22 21h-6.17l-4.83-6.32L5.47 21H3.36l7.35-8.4L2 3h6.32l4.37 5.77L18.9 3Zm-2.17 16h1.17L7.68 4H6.43l10.3 15Z');
|
|
123
|
+
case 'home':
|
|
124
|
+
return iconSvg('M12 3 3 10.2V21h6v-6h6v6h6V10.2L12 3Z');
|
|
125
|
+
default:
|
|
126
|
+
return `<span class="site-footer__social-label">${escapeHtml(icon.slice(0, 1).toUpperCase())}</span>`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function iconSvg(pathData) {
|
|
130
|
+
return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${pathData}"></path></svg>`;
|
|
131
|
+
}
|
|
132
|
+
function renderCatalogArticle(body, entries, options) {
|
|
133
|
+
if (entries.length === 0) {
|
|
134
|
+
return body;
|
|
135
|
+
}
|
|
136
|
+
const directories = entries.filter((entry) => entry.kind === 'directory');
|
|
137
|
+
const articles = entries.filter((entry) => entry.kind === 'article');
|
|
138
|
+
const initialPostCount = Math.max(1, options.initialPostCount);
|
|
139
|
+
const visibleArticles = articles.slice(0, initialPostCount);
|
|
140
|
+
const shouldLoadMore = articles.length > visibleArticles.length;
|
|
141
|
+
return [
|
|
142
|
+
`<div class="catalog-page__body">${body}</div>`,
|
|
143
|
+
'<section class="catalog-page" aria-label="Catalog">',
|
|
144
|
+
directories.length > 0 ? renderCatalogDirectories(directories) : '',
|
|
145
|
+
articles.length > 0
|
|
146
|
+
? renderCatalogArticles(visibleArticles, {
|
|
147
|
+
requestPath: options.requestPath,
|
|
148
|
+
nextOffset: visibleArticles.length,
|
|
149
|
+
loadMoreStep: options.loadMoreStep,
|
|
150
|
+
hasMore: shouldLoadMore,
|
|
151
|
+
})
|
|
152
|
+
: '',
|
|
153
|
+
'</section>',
|
|
154
|
+
shouldLoadMore ? renderCatalogLoadMoreScript() : '',
|
|
155
|
+
].join('');
|
|
156
|
+
}
|
|
157
|
+
function renderCatalogDirectories(entries) {
|
|
158
|
+
return [
|
|
159
|
+
'<div class="catalog-list catalog-list--directories">',
|
|
160
|
+
...entries.map((entry) => `<a class="catalog-item catalog-item--directory" href="${escapeHtml(entry.href)}"><strong class="catalog-item__title">${escapeHtml(entry.title)}</strong>${entry.detail
|
|
161
|
+
? `<span class="catalog-item__detail">${escapeHtml(entry.detail)}</span>`
|
|
162
|
+
: '<span class="catalog-item__detail">Browse this section.</span>'}</a>`),
|
|
163
|
+
'</div>',
|
|
164
|
+
].join('');
|
|
165
|
+
}
|
|
166
|
+
export function renderCatalogArticleItems(entries) {
|
|
167
|
+
return entries
|
|
168
|
+
.map((entry) => `<a class="catalog-item" href="${escapeHtml(entry.href)}"><strong class="catalog-item__title">${escapeHtml(entry.title)}</strong>${entry.detail
|
|
169
|
+
? `<span class="catalog-item__detail">${escapeHtml(entry.detail)}</span>`
|
|
170
|
+
: ''}</a>`)
|
|
171
|
+
.join('');
|
|
172
|
+
}
|
|
173
|
+
function renderCatalogArticles(entries, options) {
|
|
174
|
+
return [
|
|
175
|
+
'<div class="catalog-list" data-catalog-articles>',
|
|
176
|
+
renderCatalogArticleItems(entries),
|
|
177
|
+
'</div>',
|
|
178
|
+
options.hasMore
|
|
179
|
+
? `<div class="catalog-load-more"><button type="button" class="catalog-load-more__button" data-catalog-load-more data-request-path="${escapeHtml(options.requestPath)}" data-next-offset="${escapeHtml(String(options.nextOffset))}" data-load-more-step="${escapeHtml(String(options.loadMoreStep))}">Load more</button></div>`
|
|
180
|
+
: '',
|
|
181
|
+
].join('');
|
|
182
|
+
}
|
|
183
|
+
function renderCatalogLoadMoreScript() {
|
|
184
|
+
return `<script>
|
|
185
|
+
(() => {
|
|
186
|
+
const button = document.querySelector('[data-catalog-load-more]');
|
|
187
|
+
const list = document.querySelector('[data-catalog-articles]');
|
|
188
|
+
if (!(button instanceof HTMLButtonElement) || !(list instanceof HTMLElement)) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const loadMore = async () => {
|
|
193
|
+
const requestPath = button.dataset.requestPath;
|
|
194
|
+
const nextOffset = button.dataset.nextOffset;
|
|
195
|
+
const loadMoreStep = button.dataset.loadMoreStep;
|
|
196
|
+
if (!requestPath || !nextOffset || !loadMoreStep) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
button.disabled = true;
|
|
201
|
+
const previousLabel = button.textContent;
|
|
202
|
+
button.textContent = 'Loading...';
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const url = new URL(requestPath, window.location.origin);
|
|
206
|
+
url.searchParams.set('catalog-format', 'posts');
|
|
207
|
+
url.searchParams.set('catalog-offset', nextOffset);
|
|
208
|
+
url.searchParams.set('catalog-limit', loadMoreStep);
|
|
209
|
+
|
|
210
|
+
const response = await fetch(url.toString(), {
|
|
211
|
+
headers: { Accept: 'application/json' },
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error('Failed to load more posts');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const payload = await response.json();
|
|
218
|
+
if (typeof payload.itemsHtml === 'string' && payload.itemsHtml !== '') {
|
|
219
|
+
list.insertAdjacentHTML('beforeend', payload.itemsHtml);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (payload.hasMore === true && typeof payload.nextOffset === 'number') {
|
|
223
|
+
button.dataset.nextOffset = String(payload.nextOffset);
|
|
224
|
+
button.disabled = false;
|
|
225
|
+
button.textContent = previousLabel ?? 'Load more';
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
button.remove();
|
|
230
|
+
} catch {
|
|
231
|
+
button.disabled = false;
|
|
232
|
+
button.textContent = previousLabel ?? 'Load more';
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
button.addEventListener('click', () => {
|
|
237
|
+
void loadMore();
|
|
238
|
+
});
|
|
239
|
+
})();
|
|
240
|
+
</script>`;
|
|
241
|
+
}
|
|
242
|
+
function renderSearchScript() {
|
|
243
|
+
return [
|
|
244
|
+
'<script>',
|
|
245
|
+
'(function () {',
|
|
246
|
+
' const root = document.querySelector("[data-site-search]");',
|
|
247
|
+
' if (!root) return;',
|
|
248
|
+
' const toggle = root.querySelector(".site-search__toggle");',
|
|
249
|
+
' const panel = root.querySelector(".site-search__panel");',
|
|
250
|
+
' const form = root.querySelector(".site-search__form");',
|
|
251
|
+
' const input = root.querySelector(".site-search__input");',
|
|
252
|
+
' const results = root.querySelector("[data-site-search-results]");',
|
|
253
|
+
' if (!toggle || !panel || !form || !input || !results) return;',
|
|
254
|
+
' let controller = null;',
|
|
255
|
+
' function renderMessage(message) {',
|
|
256
|
+
' results.innerHTML = `<p class="site-search__message">${escapeHtmlForScript(message)}</p>`;',
|
|
257
|
+
' }',
|
|
258
|
+
' function renderHits(hits) {',
|
|
259
|
+
' if (!Array.isArray(hits) || hits.length === 0) {',
|
|
260
|
+
' renderMessage("No results.");',
|
|
261
|
+
' return;',
|
|
262
|
+
' }',
|
|
263
|
+
' results.innerHTML = hits.map((hit) => {',
|
|
264
|
+
' const href = escapeHtmlForScript(hit.canonicalUrl || hit.docId || "#");',
|
|
265
|
+
' const title = escapeHtmlForScript(hit.title || hit.relativePath || "Untitled");',
|
|
266
|
+
' const summary = typeof hit.summary === "string" ? `<span class="site-search__item-summary">${escapeHtmlForScript(hit.summary)}</span>` : "";',
|
|
267
|
+
' const excerpt = hit.bestMatch && typeof hit.bestMatch.excerpt === "string" ? `<span class="site-search__item-excerpt">${escapeHtmlForScript(hit.bestMatch.excerpt)}</span>` : "";',
|
|
268
|
+
' return `<a class="site-search__item" href="${href}"><strong class="site-search__item-title">${title}</strong>${summary}${excerpt}</a>`;',
|
|
269
|
+
' }).join("");',
|
|
270
|
+
' }',
|
|
271
|
+
' async function runSearch(query) {',
|
|
272
|
+
' if (controller) controller.abort();',
|
|
273
|
+
' controller = new AbortController();',
|
|
274
|
+
' renderMessage("Searching...");',
|
|
275
|
+
' try {',
|
|
276
|
+
' const url = new URL("/api/search", window.location.origin);',
|
|
277
|
+
' url.searchParams.set("q", query);',
|
|
278
|
+
' url.searchParams.set("topK", "8");',
|
|
279
|
+
' const response = await fetch(url, { signal: controller.signal });',
|
|
280
|
+
' if (!response.ok) {',
|
|
281
|
+
' renderMessage(`Search failed (${response.status}).`);',
|
|
282
|
+
' return;',
|
|
283
|
+
' }',
|
|
284
|
+
' const payload = await response.json();',
|
|
285
|
+
' renderHits(payload.hits);',
|
|
286
|
+
' } catch (error) {',
|
|
287
|
+
' if (error && typeof error === "object" && "name" in error && error.name === "AbortError") return;',
|
|
288
|
+
' renderMessage("Search failed.");',
|
|
289
|
+
' }',
|
|
290
|
+
' }',
|
|
291
|
+
' function setOpen(open) {',
|
|
292
|
+
' toggle.setAttribute("aria-expanded", open ? "true" : "false");',
|
|
293
|
+
' panel.hidden = !open;',
|
|
294
|
+
' if (open) {',
|
|
295
|
+
' input.focus();',
|
|
296
|
+
' if (!results.innerHTML) renderMessage("Search docs, guides, and skills.");',
|
|
297
|
+
' }',
|
|
298
|
+
' }',
|
|
299
|
+
' toggle.addEventListener("click", () => setOpen(panel.hidden));',
|
|
300
|
+
' form.addEventListener("submit", (event) => {',
|
|
301
|
+
' event.preventDefault();',
|
|
302
|
+
' const query = input.value.trim();',
|
|
303
|
+
' if (!query) {',
|
|
304
|
+
' renderMessage("Enter a search query.");',
|
|
305
|
+
' return;',
|
|
306
|
+
' }',
|
|
307
|
+
' void runSearch(query);',
|
|
308
|
+
' });',
|
|
309
|
+
' document.addEventListener("keydown", (event) => {',
|
|
310
|
+
' if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {',
|
|
311
|
+
' event.preventDefault();',
|
|
312
|
+
' setOpen(true);',
|
|
313
|
+
' }',
|
|
314
|
+
' if (event.key === "Escape" && !panel.hidden) setOpen(false);',
|
|
315
|
+
' });',
|
|
316
|
+
'})();',
|
|
317
|
+
'',
|
|
318
|
+
'function escapeHtmlForScript(value) {',
|
|
319
|
+
' return String(value)',
|
|
320
|
+
' .replaceAll("&", "&")',
|
|
321
|
+
' .replaceAll("<", "<")',
|
|
322
|
+
' .replaceAll(">", ">")',
|
|
323
|
+
' .replaceAll(\'"\', """)',
|
|
324
|
+
' .replaceAll("\\\'", "'");',
|
|
325
|
+
'}',
|
|
326
|
+
'</script>',
|
|
327
|
+
].join('');
|
|
328
|
+
}
|