mdorigin 0.1.1 → 0.1.3
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 +97 -9
- package/dist/adapters/cloudflare.d.ts +7 -1
- package/dist/adapters/cloudflare.js +11 -1
- package/dist/adapters/node.d.ts +4 -0
- package/dist/adapters/node.js +51 -11
- package/dist/cli/build-cloudflare.js +11 -4
- package/dist/cli/build-index.js +17 -3
- package/dist/cli/build-search.d.ts +1 -0
- package/dist/cli/build-search.js +45 -0
- package/dist/cli/dev.js +14 -4
- package/dist/cli/main.js +15 -3
- package/dist/cli/search.d.ts +1 -0
- package/dist/cli/search.js +36 -0
- package/dist/cloudflare.d.ts +3 -0
- package/dist/cloudflare.js +67 -6
- 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/extensions.d.ts +66 -0
- package/dist/core/extensions.js +86 -0
- package/dist/core/markdown.d.ts +4 -0
- package/dist/core/markdown.js +53 -0
- package/dist/core/request-handler.d.ts +6 -0
- package/dist/core/request-handler.js +211 -68
- package/dist/core/site-config.d.ts +18 -0
- package/dist/core/site-config.js +88 -16
- package/dist/html/template.d.ts +8 -0
- package/dist/html/template.js +228 -12
- package/dist/html/theme.js +254 -9
- package/dist/index-builder.d.ts +3 -1
- package/dist/index-builder.js +82 -45
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/search.d.ts +59 -0
- package/dist/search.js +370 -0
- package/package.json +20 -5
package/dist/cloudflare.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createCloudflareWorker } from './adapters/cloudflare.js';
|
|
4
4
|
import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, } from './core/content-store.js';
|
|
@@ -30,10 +30,14 @@ export async function buildCloudflareManifest(options) {
|
|
|
30
30
|
base64: (await readFile(filePath)).toString('base64'),
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
const searchEntries = options.searchDir
|
|
34
|
+
? await readBundleEntries(path.resolve(options.searchDir))
|
|
35
|
+
: undefined;
|
|
33
36
|
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
34
37
|
return {
|
|
35
38
|
entries,
|
|
36
39
|
siteConfig: options.siteConfig,
|
|
40
|
+
searchEntries,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
export async function writeCloudflareBundle(options) {
|
|
@@ -41,15 +45,41 @@ export async function writeCloudflareBundle(options) {
|
|
|
41
45
|
const manifest = await buildCloudflareManifest({
|
|
42
46
|
rootDir: options.rootDir,
|
|
43
47
|
siteConfig: options.siteConfig,
|
|
48
|
+
searchDir: options.searchDir,
|
|
44
49
|
});
|
|
45
50
|
const packageImport = options.packageImport ?? 'mdorigin/cloudflare-runtime';
|
|
46
51
|
const workerFile = path.join(outDir, 'worker.mjs');
|
|
52
|
+
const configImportPath = options.configModulePath
|
|
53
|
+
? toPosixPath(path.relative(outDir, options.configModulePath))
|
|
54
|
+
: null;
|
|
47
55
|
const workerSource = [
|
|
48
56
|
`import { createCloudflareWorker } from '${packageImport}';`,
|
|
57
|
+
configImportPath
|
|
58
|
+
? `import * as userConfigModule from '${configImportPath.startsWith('.') ? configImportPath : `./${configImportPath}`}';`
|
|
59
|
+
: '',
|
|
49
60
|
'',
|
|
50
61
|
`const manifest = ${JSON.stringify(manifest, null, 2)};`,
|
|
51
62
|
'',
|
|
52
|
-
|
|
63
|
+
configImportPath
|
|
64
|
+
? [
|
|
65
|
+
'function unwrapUserConfigModule(moduleValue) {',
|
|
66
|
+
' let current = moduleValue;',
|
|
67
|
+
" while (current && typeof current === 'object' && 'default' in current && current.default !== undefined) {",
|
|
68
|
+
' current = current.default;',
|
|
69
|
+
' }',
|
|
70
|
+
" if (current && typeof current === 'object' && 'config' in current && current.config !== undefined) {",
|
|
71
|
+
' return current.config;',
|
|
72
|
+
' }',
|
|
73
|
+
' return current;',
|
|
74
|
+
'}',
|
|
75
|
+
'',
|
|
76
|
+
'const userConfig = unwrapUserConfigModule(userConfigModule);',
|
|
77
|
+
'',
|
|
78
|
+
].join('\n')
|
|
79
|
+
: '',
|
|
80
|
+
configImportPath
|
|
81
|
+
? 'export default createCloudflareWorker(manifest, { plugins: Array.isArray(userConfig?.plugins) ? userConfig.plugins : [] });'
|
|
82
|
+
: 'export default createCloudflareWorker(manifest);',
|
|
53
83
|
'',
|
|
54
84
|
].join('\n');
|
|
55
85
|
await mkdir(outDir, { recursive: true });
|
|
@@ -84,21 +114,52 @@ export async function initCloudflareProject(options) {
|
|
|
84
114
|
await writeFile(configFile, wranglerConfig, 'utf8');
|
|
85
115
|
return { configFile };
|
|
86
116
|
}
|
|
87
|
-
async function listFiles(directory) {
|
|
117
|
+
async function listFiles(directory, visitedRealDirectories = new Set()) {
|
|
118
|
+
const directoryRealPath = await realpath(directory);
|
|
119
|
+
if (visitedRealDirectories.has(directoryRealPath)) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
visitedRealDirectories.add(directoryRealPath);
|
|
88
123
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
89
124
|
const files = [];
|
|
90
125
|
for (const entry of entries) {
|
|
91
126
|
const fullPath = path.join(directory, entry.name);
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
const entryStats = await stat(fullPath);
|
|
128
|
+
if (entryStats.isDirectory()) {
|
|
129
|
+
files.push(...(await listFiles(fullPath, visitedRealDirectories)));
|
|
94
130
|
continue;
|
|
95
131
|
}
|
|
96
|
-
if (
|
|
132
|
+
if (entryStats.isFile()) {
|
|
97
133
|
files.push(fullPath);
|
|
98
134
|
}
|
|
99
135
|
}
|
|
100
136
|
return files;
|
|
101
137
|
}
|
|
138
|
+
async function readBundleEntries(directory) {
|
|
139
|
+
const files = await listFiles(directory);
|
|
140
|
+
const entries = [];
|
|
141
|
+
for (const filePath of files) {
|
|
142
|
+
const relativePath = path.relative(directory, filePath).replaceAll(path.sep, '/');
|
|
143
|
+
const mediaType = getMediaTypeForPath(relativePath);
|
|
144
|
+
if (isLikelyTextPath(relativePath) || relativePath.endsWith('.json')) {
|
|
145
|
+
entries.push({
|
|
146
|
+
path: relativePath,
|
|
147
|
+
kind: 'text',
|
|
148
|
+
mediaType,
|
|
149
|
+
text: await readFile(filePath, 'utf8'),
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
entries.push({
|
|
154
|
+
path: relativePath,
|
|
155
|
+
kind: 'binary',
|
|
156
|
+
mediaType,
|
|
157
|
+
base64: (await readFile(filePath)).toString('base64'),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
161
|
+
return entries;
|
|
162
|
+
}
|
|
102
163
|
async function pathExists(filePath) {
|
|
103
164
|
try {
|
|
104
165
|
await stat(filePath);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResolvedSiteConfig } from './site-config.js';
|
|
2
|
+
import type { SearchApi } from '../search.js';
|
|
3
|
+
export interface ApiRouteOptions {
|
|
4
|
+
searchApi?: SearchApi;
|
|
5
|
+
siteConfig: ResolvedSiteConfig;
|
|
6
|
+
requestUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ApiRouteResponse {
|
|
9
|
+
status: number;
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
body: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function handleApiRoute(pathname: string, searchParams: URLSearchParams | undefined, options: ApiRouteOptions): Promise<ApiRouteResponse | null>;
|
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
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ManagedIndexEntry } from './markdown.js';
|
|
2
|
+
import type { EditLinkConfig, ResolvedSiteConfig, SiteLogo, SiteNavItem, SiteSocialLink } from './site-config.js';
|
|
3
|
+
import type { TemplateName } from '../html/template-kind.js';
|
|
4
|
+
import type { BuiltInThemeName } from '../html/theme.js';
|
|
5
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
6
|
+
export interface IndexTransformContext {
|
|
7
|
+
mode: 'build' | 'render';
|
|
8
|
+
directoryPath?: string;
|
|
9
|
+
requestPath?: string;
|
|
10
|
+
sourcePath?: string;
|
|
11
|
+
siteConfig?: ResolvedSiteConfig;
|
|
12
|
+
}
|
|
13
|
+
export interface PageRenderModel {
|
|
14
|
+
kind: 'document' | 'catalog';
|
|
15
|
+
requestPath: string;
|
|
16
|
+
sourcePath: string;
|
|
17
|
+
siteTitle: string;
|
|
18
|
+
siteDescription?: string;
|
|
19
|
+
siteUrl?: string;
|
|
20
|
+
favicon?: string;
|
|
21
|
+
socialImage?: string;
|
|
22
|
+
logo?: SiteLogo;
|
|
23
|
+
title: string;
|
|
24
|
+
bodyHtml: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
date?: string;
|
|
27
|
+
showSummary: boolean;
|
|
28
|
+
showDate: boolean;
|
|
29
|
+
theme: BuiltInThemeName;
|
|
30
|
+
template: TemplateName;
|
|
31
|
+
topNav: SiteNavItem[];
|
|
32
|
+
footerNav: SiteNavItem[];
|
|
33
|
+
footerText?: string;
|
|
34
|
+
socialLinks: SiteSocialLink[];
|
|
35
|
+
editLink?: EditLinkConfig;
|
|
36
|
+
editLinkHref?: string;
|
|
37
|
+
stylesheetContent?: string;
|
|
38
|
+
canonicalPath?: string;
|
|
39
|
+
alternateMarkdownPath?: string;
|
|
40
|
+
catalogEntries: ManagedIndexEntry[];
|
|
41
|
+
catalogRequestPath: string;
|
|
42
|
+
catalogInitialPostCount: number;
|
|
43
|
+
catalogLoadMoreStep: number;
|
|
44
|
+
searchEnabled: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface RenderHookContext {
|
|
47
|
+
page: PageRenderModel;
|
|
48
|
+
siteConfig: ResolvedSiteConfig;
|
|
49
|
+
}
|
|
50
|
+
export interface MdoPlugin {
|
|
51
|
+
name?: string;
|
|
52
|
+
transformIndex?(entries: ManagedIndexEntry[], context: IndexTransformContext): MaybePromise<ManagedIndexEntry[]>;
|
|
53
|
+
renderHeader?(context: RenderHookContext): MaybePromise<string | undefined | null>;
|
|
54
|
+
renderFooter?(context: RenderHookContext): MaybePromise<string | undefined | null>;
|
|
55
|
+
renderPage?(page: PageRenderModel, context: RenderHookContext, next: (page: PageRenderModel) => MaybePromise<string>): MaybePromise<string | undefined | null>;
|
|
56
|
+
transformHtml?(html: string, context: RenderHookContext): MaybePromise<string>;
|
|
57
|
+
}
|
|
58
|
+
export declare function applyIndexTransforms(entries: ManagedIndexEntry[], plugins: MdoPlugin[], context: IndexTransformContext): Promise<ManagedIndexEntry[]>;
|
|
59
|
+
export declare function renderHeaderOverride(plugins: MdoPlugin[], context: RenderHookContext): Promise<string | undefined>;
|
|
60
|
+
export declare function renderFooterOverride(plugins: MdoPlugin[], context: RenderHookContext): Promise<string | undefined>;
|
|
61
|
+
export declare function renderPageWithPlugins(page: PageRenderModel, plugins: MdoPlugin[], context: RenderHookContext, renderDefault: (page: PageRenderModel) => MaybePromise<string>): Promise<{
|
|
62
|
+
html: string;
|
|
63
|
+
page: PageRenderModel;
|
|
64
|
+
}>;
|
|
65
|
+
export declare function transformHtmlWithPlugins(html: string, plugins: MdoPlugin[], context: RenderHookContext): Promise<string>;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export async function applyIndexTransforms(entries, plugins, context) {
|
|
2
|
+
let current = [...entries];
|
|
3
|
+
for (const plugin of plugins) {
|
|
4
|
+
if (!plugin.transformIndex) {
|
|
5
|
+
continue;
|
|
6
|
+
}
|
|
7
|
+
current = [...(await plugin.transformIndex(current, context))];
|
|
8
|
+
}
|
|
9
|
+
return current;
|
|
10
|
+
}
|
|
11
|
+
export async function renderHeaderOverride(plugins, context) {
|
|
12
|
+
let rendered;
|
|
13
|
+
for (const plugin of plugins) {
|
|
14
|
+
if (!plugin.renderHeader) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const value = await plugin.renderHeader(context);
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
rendered = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return rendered;
|
|
23
|
+
}
|
|
24
|
+
export async function renderFooterOverride(plugins, context) {
|
|
25
|
+
let rendered;
|
|
26
|
+
for (const plugin of plugins) {
|
|
27
|
+
if (!plugin.renderFooter) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const value = await plugin.renderFooter(context);
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
rendered = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return rendered;
|
|
36
|
+
}
|
|
37
|
+
export async function renderPageWithPlugins(page, plugins, context, renderDefault) {
|
|
38
|
+
const renderers = plugins
|
|
39
|
+
.map((plugin) => plugin.renderPage)
|
|
40
|
+
.filter((renderPage) => typeof renderPage === 'function');
|
|
41
|
+
let finalPage = page;
|
|
42
|
+
const dispatch = async (index, currentPage) => {
|
|
43
|
+
const renderPage = renderers[index];
|
|
44
|
+
if (!renderPage) {
|
|
45
|
+
finalPage = currentPage;
|
|
46
|
+
return renderDefault(currentPage);
|
|
47
|
+
}
|
|
48
|
+
const currentContext = {
|
|
49
|
+
...context,
|
|
50
|
+
page: currentPage,
|
|
51
|
+
};
|
|
52
|
+
let nextInvoked = false;
|
|
53
|
+
const next = async (nextPage) => {
|
|
54
|
+
nextInvoked = true;
|
|
55
|
+
finalPage = nextPage;
|
|
56
|
+
return dispatch(index + 1, nextPage);
|
|
57
|
+
};
|
|
58
|
+
const rendered = await renderPage(currentPage, currentContext, next);
|
|
59
|
+
if (typeof rendered === 'string') {
|
|
60
|
+
if (!nextInvoked) {
|
|
61
|
+
finalPage = currentPage;
|
|
62
|
+
}
|
|
63
|
+
return rendered;
|
|
64
|
+
}
|
|
65
|
+
return next(currentPage);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
html: await dispatch(0, page),
|
|
69
|
+
page: finalPage,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export async function transformHtmlWithPlugins(html, plugins, context) {
|
|
73
|
+
let current = html;
|
|
74
|
+
for (const plugin of plugins) {
|
|
75
|
+
if (!plugin.transformHtml) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const result = await plugin.transformHtml(current, context);
|
|
79
|
+
if (typeof result !== 'string') {
|
|
80
|
+
const pluginName = plugin.name ?? 'unknown plugin';
|
|
81
|
+
throw new Error(`transformHtmlWithPlugins expected plugin "${pluginName}" to return a string, but got ${typeof result}`);
|
|
82
|
+
}
|
|
83
|
+
current = result;
|
|
84
|
+
}
|
|
85
|
+
return current;
|
|
86
|
+
}
|
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,15 @@
|
|
|
1
1
|
import type { ContentStore } from './content-store.js';
|
|
2
|
+
import type { MdoPlugin } from './extensions.js';
|
|
2
3
|
import type { ResolvedSiteConfig } from './site-config.js';
|
|
4
|
+
import type { SearchApi } from '../search.js';
|
|
3
5
|
export interface HandleSiteRequestOptions {
|
|
4
6
|
draftMode: 'include' | 'exclude';
|
|
5
7
|
siteConfig: ResolvedSiteConfig;
|
|
6
8
|
acceptHeader?: string;
|
|
9
|
+
searchParams?: URLSearchParams;
|
|
10
|
+
requestUrl?: string;
|
|
11
|
+
searchApi?: SearchApi;
|
|
12
|
+
plugins?: MdoPlugin[];
|
|
7
13
|
}
|
|
8
14
|
export interface SiteResponse {
|
|
9
15
|
status: number;
|