mdorigin 0.1.1 → 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 +47 -4
- package/dist/adapters/cloudflare.d.ts +2 -0
- package/dist/adapters/cloudflare.js +9 -0
- package/dist/adapters/node.d.ts +2 -0
- package/dist/adapters/node.js +50 -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.js +14 -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 +4 -0
- package/dist/core/markdown.js +53 -0
- package/dist/core/request-handler.d.ts +4 -0
- package/dist/core/request-handler.js +90 -14
- package/dist/core/site-config.d.ts +4 -0
- package/dist/core/site-config.js +14 -0
- package/dist/html/template.d.ts +5 -0
- package/dist/html/template.js +203 -11
- package/dist/html/theme.js +254 -9
- package/dist/index-builder.js +54 -29
- package/dist/search.d.ts +59 -0
- package/dist/search.js +370 -0
- package/package.json +10 -1
package/dist/core/api.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export async function handleApiRoute(pathname, searchParams, options) {
|
|
2
|
+
if (pathname === '/api/openapi.json') {
|
|
3
|
+
return json(200, buildOpenApiDocument(options));
|
|
4
|
+
}
|
|
5
|
+
if (pathname === '/api/search') {
|
|
6
|
+
if (!options.searchApi) {
|
|
7
|
+
return json(404, { error: 'search is not enabled for this site' });
|
|
8
|
+
}
|
|
9
|
+
const query = searchParams?.get('q')?.trim() ?? '';
|
|
10
|
+
if (query === '') {
|
|
11
|
+
return json(400, {
|
|
12
|
+
error: 'missing required query parameter: q',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const topK = normalizePositiveInteger(searchParams?.get('topK')) ?? 10;
|
|
16
|
+
const hits = await options.searchApi.search(query, { topK });
|
|
17
|
+
return json(200, {
|
|
18
|
+
query,
|
|
19
|
+
topK,
|
|
20
|
+
count: hits.length,
|
|
21
|
+
hits: hits.map(serializeSearchHit),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function buildOpenApiDocument(options) {
|
|
27
|
+
return {
|
|
28
|
+
openapi: '3.1.0',
|
|
29
|
+
info: {
|
|
30
|
+
title: `${options.siteConfig.siteTitle} API`,
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
description: 'Search API for a mdorigin site.',
|
|
33
|
+
},
|
|
34
|
+
servers: options.siteConfig.siteUrl
|
|
35
|
+
? [{ url: options.siteConfig.siteUrl }]
|
|
36
|
+
: options.requestUrl
|
|
37
|
+
? [{ url: new URL(options.requestUrl).origin }]
|
|
38
|
+
: undefined,
|
|
39
|
+
paths: {
|
|
40
|
+
'/api/search': {
|
|
41
|
+
get: {
|
|
42
|
+
operationId: 'searchSite',
|
|
43
|
+
summary: 'Search published site content',
|
|
44
|
+
parameters: [
|
|
45
|
+
{
|
|
46
|
+
name: 'q',
|
|
47
|
+
in: 'query',
|
|
48
|
+
required: true,
|
|
49
|
+
schema: { type: 'string' },
|
|
50
|
+
description: 'Search query string.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'topK',
|
|
54
|
+
in: 'query',
|
|
55
|
+
required: false,
|
|
56
|
+
schema: { type: 'integer', minimum: 1, default: 10 },
|
|
57
|
+
description: 'Maximum number of hits to return.',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
responses: {
|
|
61
|
+
'200': {
|
|
62
|
+
description: 'Search results.',
|
|
63
|
+
content: {
|
|
64
|
+
'application/json': {
|
|
65
|
+
schema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
required: ['query', 'topK', 'count', 'hits'],
|
|
68
|
+
properties: {
|
|
69
|
+
query: { type: 'string' },
|
|
70
|
+
topK: { type: 'integer' },
|
|
71
|
+
count: { type: 'integer' },
|
|
72
|
+
hits: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
items: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
required: [
|
|
77
|
+
'docId',
|
|
78
|
+
'relativePath',
|
|
79
|
+
'score',
|
|
80
|
+
'metadata',
|
|
81
|
+
'bestMatch',
|
|
82
|
+
],
|
|
83
|
+
properties: {
|
|
84
|
+
docId: { type: 'string' },
|
|
85
|
+
relativePath: { type: 'string' },
|
|
86
|
+
canonicalUrl: { type: 'string' },
|
|
87
|
+
title: { type: 'string' },
|
|
88
|
+
summary: { type: 'string' },
|
|
89
|
+
score: { type: 'number' },
|
|
90
|
+
metadata: { type: 'object', additionalProperties: true },
|
|
91
|
+
bestMatch: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
required: [
|
|
94
|
+
'chunkId',
|
|
95
|
+
'excerpt',
|
|
96
|
+
'headingPath',
|
|
97
|
+
'charStart',
|
|
98
|
+
'charEnd',
|
|
99
|
+
'score',
|
|
100
|
+
],
|
|
101
|
+
properties: {
|
|
102
|
+
chunkId: { type: 'number' },
|
|
103
|
+
excerpt: { type: 'string' },
|
|
104
|
+
headingPath: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
items: { type: 'string' },
|
|
107
|
+
},
|
|
108
|
+
charStart: { type: 'integer' },
|
|
109
|
+
charEnd: { type: 'integer' },
|
|
110
|
+
score: { type: 'number' },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
'400': {
|
|
122
|
+
description: 'Invalid request.',
|
|
123
|
+
},
|
|
124
|
+
'404': {
|
|
125
|
+
description: 'Search API not enabled.',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function serializeSearchHit(hit) {
|
|
134
|
+
return {
|
|
135
|
+
docId: hit.docId,
|
|
136
|
+
relativePath: hit.relativePath,
|
|
137
|
+
canonicalUrl: hit.canonicalUrl,
|
|
138
|
+
title: hit.title,
|
|
139
|
+
summary: hit.summary,
|
|
140
|
+
metadata: hit.metadata,
|
|
141
|
+
score: hit.score,
|
|
142
|
+
bestMatch: hit.bestMatch,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function normalizePositiveInteger(value) {
|
|
146
|
+
if (!value) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const parsed = Number.parseInt(value, 10);
|
|
150
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
151
|
+
}
|
|
152
|
+
function json(status, body) {
|
|
153
|
+
return {
|
|
154
|
+
status,
|
|
155
|
+
headers: {
|
|
156
|
+
'content-type': 'application/json; charset=utf-8',
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify(body, null, 2),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -54,10 +54,15 @@ const MEDIA_TYPES = new Map([
|
|
|
54
54
|
['.json', 'application/json; charset=utf-8'],
|
|
55
55
|
['.md', 'text/markdown; charset=utf-8'],
|
|
56
56
|
['.pdf', 'application/pdf'],
|
|
57
|
+
['.py', 'text/plain; charset=utf-8'],
|
|
57
58
|
['.png', 'image/png'],
|
|
59
|
+
['.sh', 'text/plain; charset=utf-8'],
|
|
58
60
|
['.svg', 'image/svg+xml'],
|
|
59
61
|
['.txt', 'text/plain; charset=utf-8'],
|
|
62
|
+
['.toml', 'text/plain; charset=utf-8'],
|
|
60
63
|
['.webp', 'image/webp'],
|
|
64
|
+
['.yaml', 'text/plain; charset=utf-8'],
|
|
65
|
+
['.yml', 'text/plain; charset=utf-8'],
|
|
61
66
|
]);
|
|
62
67
|
export function getMediaTypeForPath(contentPath) {
|
|
63
68
|
const extension = path.posix.extname(contentPath).toLowerCase();
|
|
@@ -9,6 +9,9 @@ export function inferDirectoryContentType(meta, shape) {
|
|
|
9
9
|
if (typeof meta.date === 'string' && meta.date !== '') {
|
|
10
10
|
return 'post';
|
|
11
11
|
}
|
|
12
|
+
if (shape.hasSkillIndex) {
|
|
13
|
+
return 'post';
|
|
14
|
+
}
|
|
12
15
|
if (shape.hasChildDirectories || shape.hasExtraMarkdownFiles) {
|
|
13
16
|
return 'page';
|
|
14
17
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const DIRECTORY_INDEX_FILENAMES: readonly ["index.md", "README.md"];
|
|
1
|
+
export declare const DIRECTORY_INDEX_FILENAMES: readonly ["index.md", "README.md", "SKILL.md"];
|
|
2
2
|
export declare function getDirectoryIndexCandidates(directoryPath: string): string[];
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
export const DIRECTORY_INDEX_FILENAMES = [
|
|
2
|
+
export const DIRECTORY_INDEX_FILENAMES = [
|
|
3
|
+
'index.md',
|
|
4
|
+
'README.md',
|
|
5
|
+
'SKILL.md',
|
|
6
|
+
];
|
|
3
7
|
export function getDirectoryIndexCandidates(directoryPath) {
|
|
4
8
|
return DIRECTORY_INDEX_FILENAMES.map((filename) => directoryPath === '' ? filename : path.posix.join(directoryPath, filename));
|
|
5
9
|
}
|
package/dist/core/markdown.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export interface ParsedDocumentMeta {
|
|
2
2
|
title?: string;
|
|
3
|
+
name?: string;
|
|
3
4
|
date?: string;
|
|
4
5
|
summary?: string;
|
|
6
|
+
description?: string;
|
|
5
7
|
draft?: boolean;
|
|
6
8
|
type?: string;
|
|
7
9
|
order?: number;
|
|
@@ -20,6 +22,8 @@ export interface ManagedIndexEntry {
|
|
|
20
22
|
href: string;
|
|
21
23
|
detail?: string;
|
|
22
24
|
}
|
|
25
|
+
export declare function getDocumentTitle(meta: ParsedDocumentMeta, body: string, fallback: string): string;
|
|
26
|
+
export declare function getDocumentSummary(meta: ParsedDocumentMeta, body: string): string | undefined;
|
|
23
27
|
export declare function parseMarkdownDocument(sourcePath: string, markdown: string): Promise<ParsedDocument>;
|
|
24
28
|
export declare function renderMarkdown(markdown: string): Promise<string>;
|
|
25
29
|
export declare function rewriteMarkdownLinksInHtml(html: string): string;
|
package/dist/core/markdown.js
CHANGED
|
@@ -2,6 +2,15 @@ import matter from 'gray-matter';
|
|
|
2
2
|
import { remark } from 'remark';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import remarkHtml from 'remark-html';
|
|
5
|
+
export function getDocumentTitle(meta, body, fallback) {
|
|
6
|
+
return (firstNonEmptyString(meta.title, meta.name) ??
|
|
7
|
+
extractFirstHeading(body) ??
|
|
8
|
+
fallback);
|
|
9
|
+
}
|
|
10
|
+
export function getDocumentSummary(meta, body) {
|
|
11
|
+
return (firstNonEmptyString(meta.summary, meta.description) ??
|
|
12
|
+
extractFirstParagraph(body));
|
|
13
|
+
}
|
|
5
14
|
export async function parseMarkdownDocument(sourcePath, markdown) {
|
|
6
15
|
const parsed = matter(markdown);
|
|
7
16
|
const html = rewriteMarkdownLinksInHtml(await renderMarkdown(parsed.content));
|
|
@@ -83,6 +92,9 @@ function normalizeMeta(data) {
|
|
|
83
92
|
if (typeof data.title === 'string') {
|
|
84
93
|
meta.title = data.title;
|
|
85
94
|
}
|
|
95
|
+
if (typeof data.name === 'string') {
|
|
96
|
+
meta.name = data.name;
|
|
97
|
+
}
|
|
86
98
|
if (typeof data.date === 'string') {
|
|
87
99
|
meta.date = data.date;
|
|
88
100
|
}
|
|
@@ -92,6 +104,9 @@ function normalizeMeta(data) {
|
|
|
92
104
|
if (typeof data.summary === 'string') {
|
|
93
105
|
meta.summary = data.summary;
|
|
94
106
|
}
|
|
107
|
+
if (typeof data.description === 'string') {
|
|
108
|
+
meta.description = data.description;
|
|
109
|
+
}
|
|
95
110
|
if (typeof data.draft === 'boolean') {
|
|
96
111
|
meta.draft = data.draft;
|
|
97
112
|
}
|
|
@@ -143,6 +158,12 @@ function rewriteMarkdownPath(pathname) {
|
|
|
143
158
|
if (pathname.toLowerCase() === 'readme.md') {
|
|
144
159
|
return './';
|
|
145
160
|
}
|
|
161
|
+
if (pathname.toLowerCase().endsWith('/skill.md')) {
|
|
162
|
+
return pathname.slice(0, -'SKILL.md'.length);
|
|
163
|
+
}
|
|
164
|
+
if (pathname.toLowerCase() === 'skill.md') {
|
|
165
|
+
return './';
|
|
166
|
+
}
|
|
146
167
|
return pathname.slice(0, -'.md'.length);
|
|
147
168
|
}
|
|
148
169
|
function shouldPreserveHref(href) {
|
|
@@ -168,3 +189,35 @@ function normalizeManagedIndexHref(href) {
|
|
|
168
189
|
}
|
|
169
190
|
return href;
|
|
170
191
|
}
|
|
192
|
+
function extractFirstHeading(markdown) {
|
|
193
|
+
for (const line of markdown.split('\n')) {
|
|
194
|
+
const trimmed = line.trim();
|
|
195
|
+
if (!trimmed.startsWith('#')) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const heading = trimmed.replace(/^#+\s*/, '').trim();
|
|
199
|
+
if (heading !== '') {
|
|
200
|
+
return heading;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
function extractFirstParagraph(markdown) {
|
|
206
|
+
const paragraphs = markdown
|
|
207
|
+
.split(/\n\s*\n/g)
|
|
208
|
+
.map((paragraph) => paragraph.trim())
|
|
209
|
+
.filter((paragraph) => paragraph !== '');
|
|
210
|
+
for (const paragraph of paragraphs) {
|
|
211
|
+
if (paragraph.startsWith('#') ||
|
|
212
|
+
paragraph.startsWith('<!--') ||
|
|
213
|
+
paragraph.startsWith('- ') ||
|
|
214
|
+
paragraph.startsWith('* ')) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
return paragraph.replace(/\s+/g, ' ');
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
function firstNonEmptyString(...values) {
|
|
222
|
+
return values.find((value) => typeof value === 'string' && value !== '');
|
|
223
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { ContentStore } from './content-store.js';
|
|
2
2
|
import type { ResolvedSiteConfig } from './site-config.js';
|
|
3
|
+
import type { SearchApi } from '../search.js';
|
|
3
4
|
export interface HandleSiteRequestOptions {
|
|
4
5
|
draftMode: 'include' | 'exclude';
|
|
5
6
|
siteConfig: ResolvedSiteConfig;
|
|
6
7
|
acceptHeader?: string;
|
|
8
|
+
searchParams?: URLSearchParams;
|
|
9
|
+
requestUrl?: string;
|
|
10
|
+
searchApi?: SearchApi;
|
|
7
11
|
}
|
|
8
12
|
export interface SiteResponse {
|
|
9
13
|
status: number;
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { inferDirectoryContentType } from './content-type.js';
|
|
3
3
|
import { getDirectoryIndexCandidates } from './directory-index.js';
|
|
4
|
-
import { extractManagedIndexEntries, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
|
|
4
|
+
import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
|
|
5
|
+
import { handleApiRoute } from './api.js';
|
|
5
6
|
import { normalizeRequestPath, resolveRequest } from './router.js';
|
|
6
|
-
import { escapeHtml, renderDocument } from '../html/template.js';
|
|
7
|
+
import { escapeHtml, renderCatalogArticleItems, renderDocument, } from '../html/template.js';
|
|
7
8
|
export async function handleSiteRequest(store, pathname, options) {
|
|
9
|
+
const searchEnabled = options.searchApi !== undefined;
|
|
10
|
+
const apiRoute = await handleApiRoute(pathname, options.searchParams, {
|
|
11
|
+
searchApi: options.searchApi,
|
|
12
|
+
siteConfig: options.siteConfig,
|
|
13
|
+
requestUrl: options.requestUrl,
|
|
14
|
+
});
|
|
15
|
+
if (apiRoute !== null) {
|
|
16
|
+
return apiRoute;
|
|
17
|
+
}
|
|
8
18
|
if (pathname === '/sitemap.xml') {
|
|
9
19
|
return renderSitemap(store, options);
|
|
10
20
|
}
|
|
11
21
|
const resolved = resolveRequest(pathname);
|
|
22
|
+
const catalogFragmentRequest = getCatalogFragmentRequest(options.searchParams);
|
|
12
23
|
const negotiatedMarkdown = shouldServeMarkdownForRequest(resolved, options.acceptHeader);
|
|
13
24
|
if (resolved.kind === 'not-found' || !resolved.sourcePath) {
|
|
14
25
|
const aliasRedirect = await tryRedirectAlias(store, pathname, options);
|
|
@@ -36,7 +47,7 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
36
47
|
if (directoryIndexResponse !== null) {
|
|
37
48
|
return directoryIndexResponse;
|
|
38
49
|
}
|
|
39
|
-
return renderDirectoryListing(store, resolved.requestPath, options.siteConfig);
|
|
50
|
+
return renderDirectoryListing(store, resolved.requestPath, options.siteConfig, searchEnabled);
|
|
40
51
|
}
|
|
41
52
|
return notFound();
|
|
42
53
|
}
|
|
@@ -72,6 +83,10 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
72
83
|
const catalogEntries = options.siteConfig.template === 'catalog'
|
|
73
84
|
? extractManagedIndexEntries(renderedBody)
|
|
74
85
|
: [];
|
|
86
|
+
if (catalogFragmentRequest !== null &&
|
|
87
|
+
options.siteConfig.template === 'catalog') {
|
|
88
|
+
return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
|
|
89
|
+
}
|
|
75
90
|
const documentBody = options.siteConfig.template === 'catalog'
|
|
76
91
|
? stripManagedIndexBlock(renderedBody)
|
|
77
92
|
: renderedBody;
|
|
@@ -91,7 +106,9 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
91
106
|
logo: options.siteConfig.logo,
|
|
92
107
|
title: getDocumentTitle(parsed),
|
|
93
108
|
body: renderedParsed.html,
|
|
94
|
-
summary: options.siteConfig.showSummary === false
|
|
109
|
+
summary: options.siteConfig.showSummary === false
|
|
110
|
+
? undefined
|
|
111
|
+
: getDocumentSummary(parsed.meta, parsed.body),
|
|
95
112
|
date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
|
|
96
113
|
showSummary: options.siteConfig.showSummary,
|
|
97
114
|
showDate: options.siteConfig.showDate,
|
|
@@ -106,6 +123,51 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
106
123
|
canonicalPath: getCanonicalHtmlPathForContentPath(resolved.sourcePath),
|
|
107
124
|
alternateMarkdownPath: getMarkdownRequestPathForContentPath(resolved.sourcePath),
|
|
108
125
|
catalogEntries,
|
|
126
|
+
catalogRequestPath: resolved.requestPath,
|
|
127
|
+
catalogInitialPostCount: options.siteConfig.catalogInitialPostCount,
|
|
128
|
+
catalogLoadMoreStep: options.siteConfig.catalogLoadMoreStep,
|
|
129
|
+
searchEnabled,
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function getCatalogFragmentRequest(searchParams) {
|
|
134
|
+
if (searchParams?.get('catalog-format') !== 'posts') {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const offset = normalizeNonNegativeInteger(searchParams.get('catalog-offset'));
|
|
138
|
+
const limit = normalizePositiveInteger(searchParams.get('catalog-limit'));
|
|
139
|
+
if (offset === null || limit === null) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return { offset, limit };
|
|
143
|
+
}
|
|
144
|
+
function normalizeNonNegativeInteger(value) {
|
|
145
|
+
if (value === null) {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
const parsed = Number.parseInt(value, 10);
|
|
149
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
150
|
+
}
|
|
151
|
+
function normalizePositiveInteger(value) {
|
|
152
|
+
if (value === null) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const parsed = Number.parseInt(value, 10);
|
|
156
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
157
|
+
}
|
|
158
|
+
function renderCatalogPostsFragment(entries, request) {
|
|
159
|
+
const articles = entries.filter((entry) => entry.kind === 'article');
|
|
160
|
+
const visibleArticles = articles.slice(request.offset, request.offset + request.limit);
|
|
161
|
+
const nextOffset = request.offset + visibleArticles.length;
|
|
162
|
+
return {
|
|
163
|
+
status: 200,
|
|
164
|
+
headers: {
|
|
165
|
+
'content-type': 'application/json; charset=utf-8',
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
itemsHtml: renderCatalogArticleItems(visibleArticles),
|
|
169
|
+
hasMore: nextOffset < articles.length,
|
|
170
|
+
nextOffset,
|
|
109
171
|
}),
|
|
110
172
|
};
|
|
111
173
|
}
|
|
@@ -195,13 +257,11 @@ function appendVary(existing, value) {
|
|
|
195
257
|
return `${existing}, ${value}`;
|
|
196
258
|
}
|
|
197
259
|
function getDocumentTitle(parsed) {
|
|
198
|
-
if (parsed.meta.title) {
|
|
199
|
-
return parsed.meta.title;
|
|
200
|
-
}
|
|
201
260
|
const basename = path.posix.basename(parsed.sourcePath, '.md');
|
|
202
|
-
|
|
261
|
+
const fallback = basename === 'index' || basename === 'README' || basename === 'SKILL'
|
|
203
262
|
? path.posix.basename(path.posix.dirname(parsed.sourcePath)) || 'mdorigin'
|
|
204
263
|
: basename;
|
|
264
|
+
return getParsedDocumentTitle(parsed.meta, parsed.body, fallback);
|
|
205
265
|
}
|
|
206
266
|
async function collectSitemapEntries(store, directoryPath, options) {
|
|
207
267
|
const entries = await store.listDirectory(directoryPath);
|
|
@@ -247,7 +307,7 @@ function dedupeSitemapEntries(entries) {
|
|
|
247
307
|
}
|
|
248
308
|
return Array.from(deduped.values());
|
|
249
309
|
}
|
|
250
|
-
async function renderDirectoryListing(store, requestPath, siteConfig) {
|
|
310
|
+
async function renderDirectoryListing(store, requestPath, siteConfig, searchEnabled) {
|
|
251
311
|
const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
|
|
252
312
|
const entries = await store.listDirectory(directoryPath);
|
|
253
313
|
if (entries === null) {
|
|
@@ -286,6 +346,7 @@ async function renderDirectoryListing(store, requestPath, siteConfig) {
|
|
|
286
346
|
stylesheetContent: siteConfig.stylesheetContent,
|
|
287
347
|
canonicalPath: requestPath,
|
|
288
348
|
alternateMarkdownPath: getMarkdownRequestPathForContentPath(getDirectoryIndexContentPathForRequestPath(requestPath)),
|
|
349
|
+
searchEnabled,
|
|
289
350
|
}),
|
|
290
351
|
};
|
|
291
352
|
}
|
|
@@ -338,6 +399,11 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
338
399
|
const catalogEntries = options.siteConfig.template === 'catalog'
|
|
339
400
|
? extractManagedIndexEntries(renderedBody)
|
|
340
401
|
: [];
|
|
402
|
+
const catalogFragmentRequest = getCatalogFragmentRequest(options.searchParams);
|
|
403
|
+
if (catalogFragmentRequest !== null &&
|
|
404
|
+
options.siteConfig.template === 'catalog') {
|
|
405
|
+
return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
|
|
406
|
+
}
|
|
341
407
|
const documentBody = options.siteConfig.template === 'catalog'
|
|
342
408
|
? stripManagedIndexBlock(renderedBody)
|
|
343
409
|
: renderedBody;
|
|
@@ -357,7 +423,9 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
357
423
|
logo: options.siteConfig.logo,
|
|
358
424
|
title: getDocumentTitle(parsed),
|
|
359
425
|
body: renderedParsed.html,
|
|
360
|
-
summary: options.siteConfig.showSummary === false
|
|
426
|
+
summary: options.siteConfig.showSummary === false
|
|
427
|
+
? undefined
|
|
428
|
+
: getDocumentSummary(parsed.meta, parsed.body),
|
|
361
429
|
date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
|
|
362
430
|
showSummary: options.siteConfig.showSummary,
|
|
363
431
|
showDate: options.siteConfig.showDate,
|
|
@@ -372,6 +440,10 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
372
440
|
canonicalPath: requestPath,
|
|
373
441
|
alternateMarkdownPath: getMarkdownRequestPathForContentPath(candidatePath),
|
|
374
442
|
catalogEntries,
|
|
443
|
+
catalogRequestPath: requestPath,
|
|
444
|
+
catalogInitialPostCount: options.siteConfig.catalogInitialPostCount,
|
|
445
|
+
catalogLoadMoreStep: options.siteConfig.catalogLoadMoreStep,
|
|
446
|
+
searchEnabled: options.searchApi !== undefined,
|
|
375
447
|
}),
|
|
376
448
|
};
|
|
377
449
|
}
|
|
@@ -585,9 +657,7 @@ async function resolveDirectoryNav(store, entry) {
|
|
|
585
657
|
const parsed = await parseMarkdownDocument(candidatePath, contentEntry.text);
|
|
586
658
|
const shape = await inspectDirectoryShape(store, entry.path);
|
|
587
659
|
return {
|
|
588
|
-
title:
|
|
589
|
-
? parsed.meta.title
|
|
590
|
-
: entry.name,
|
|
660
|
+
title: getParsedDocumentTitle(parsed.meta, parsed.body, entry.name),
|
|
591
661
|
type: inferDirectoryContentType(parsed.meta, shape),
|
|
592
662
|
order: parsed.meta.order,
|
|
593
663
|
};
|
|
@@ -601,11 +671,13 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
601
671
|
const entries = await store.listDirectory(directoryPath);
|
|
602
672
|
if (entries === null) {
|
|
603
673
|
return {
|
|
674
|
+
hasSkillIndex: false,
|
|
604
675
|
hasChildDirectories: false,
|
|
605
676
|
hasExtraMarkdownFiles: false,
|
|
606
677
|
hasAssetFiles: false,
|
|
607
678
|
};
|
|
608
679
|
}
|
|
680
|
+
let hasSkillIndex = false;
|
|
609
681
|
let hasChildDirectories = false;
|
|
610
682
|
let hasExtraMarkdownFiles = false;
|
|
611
683
|
let hasAssetFiles = false;
|
|
@@ -619,7 +691,10 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
619
691
|
}
|
|
620
692
|
const extension = path.posix.extname(entry.name).toLowerCase();
|
|
621
693
|
if (extension === '.md') {
|
|
622
|
-
if (entry.name
|
|
694
|
+
if (entry.name === 'SKILL.md') {
|
|
695
|
+
hasSkillIndex = true;
|
|
696
|
+
}
|
|
697
|
+
else if (entry.name !== 'index.md' && entry.name !== 'README.md') {
|
|
623
698
|
hasExtraMarkdownFiles = true;
|
|
624
699
|
}
|
|
625
700
|
continue;
|
|
@@ -627,6 +702,7 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
627
702
|
hasAssetFiles = true;
|
|
628
703
|
}
|
|
629
704
|
return {
|
|
705
|
+
hasSkillIndex,
|
|
630
706
|
hasChildDirectories,
|
|
631
707
|
hasExtraMarkdownFiles,
|
|
632
708
|
hasAssetFiles,
|
|
@@ -35,6 +35,8 @@ export interface SiteConfig {
|
|
|
35
35
|
socialLinks?: SiteSocialLink[];
|
|
36
36
|
editLink?: EditLinkConfig;
|
|
37
37
|
showHomeIndex?: boolean;
|
|
38
|
+
catalogInitialPostCount?: number;
|
|
39
|
+
catalogLoadMoreStep?: number;
|
|
38
40
|
}
|
|
39
41
|
export interface ResolvedSiteConfig {
|
|
40
42
|
siteTitle: string;
|
|
@@ -52,6 +54,8 @@ export interface ResolvedSiteConfig {
|
|
|
52
54
|
socialLinks: SiteSocialLink[];
|
|
53
55
|
editLink?: EditLinkConfig;
|
|
54
56
|
showHomeIndex: boolean;
|
|
57
|
+
catalogInitialPostCount: number;
|
|
58
|
+
catalogLoadMoreStep: number;
|
|
55
59
|
stylesheetContent?: string;
|
|
56
60
|
siteTitleConfigured: boolean;
|
|
57
61
|
siteDescriptionConfigured: boolean;
|
package/dist/core/site-config.js
CHANGED
|
@@ -49,6 +49,8 @@ export async function loadSiteConfig(options = {}) {
|
|
|
49
49
|
showHomeIndex: typeof parsedConfig.showHomeIndex === 'boolean'
|
|
50
50
|
? parsedConfig.showHomeIndex
|
|
51
51
|
: normalizeTopNav(parsedConfig.topNav).length === 0,
|
|
52
|
+
catalogInitialPostCount: normalizePositiveInteger(parsedConfig.catalogInitialPostCount, 10),
|
|
53
|
+
catalogLoadMoreStep: normalizePositiveInteger(parsedConfig.catalogLoadMoreStep, 10),
|
|
52
54
|
stylesheetContent,
|
|
53
55
|
siteTitleConfigured: typeof parsedConfig.siteTitle === 'string' && parsedConfig.siteTitle !== '',
|
|
54
56
|
siteDescriptionConfigured: typeof parsedConfig.siteDescription === 'string' &&
|
|
@@ -131,6 +133,18 @@ function normalizeTopNav(value) {
|
|
|
131
133
|
return [];
|
|
132
134
|
});
|
|
133
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
|
+
}
|
|
134
148
|
function normalizeLogo(value) {
|
|
135
149
|
if (typeof value !== 'object' ||
|
|
136
150
|
value === null ||
|
package/dist/html/template.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface RenderDocumentOptions {
|
|
|
25
25
|
canonicalPath?: string;
|
|
26
26
|
alternateMarkdownPath?: string;
|
|
27
27
|
catalogEntries?: ManagedIndexEntry[];
|
|
28
|
+
catalogRequestPath?: string;
|
|
29
|
+
catalogInitialPostCount?: number;
|
|
30
|
+
catalogLoadMoreStep?: number;
|
|
31
|
+
searchEnabled?: boolean;
|
|
28
32
|
}
|
|
29
33
|
export declare function renderDocument(options: RenderDocumentOptions): string;
|
|
30
34
|
export declare function escapeHtml(value: string): string;
|
|
35
|
+
export declare function renderCatalogArticleItems(entries: readonly ManagedIndexEntry[]): string;
|