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/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,10 +1,13 @@
|
|
|
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;
|
|
10
|
+
aliases?: string[];
|
|
8
11
|
[key: string]: unknown;
|
|
9
12
|
}
|
|
10
13
|
export interface ParsedDocument {
|
|
@@ -13,8 +16,17 @@ export interface ParsedDocument {
|
|
|
13
16
|
html: string;
|
|
14
17
|
meta: ParsedDocumentMeta;
|
|
15
18
|
}
|
|
19
|
+
export interface ManagedIndexEntry {
|
|
20
|
+
kind: 'directory' | 'article';
|
|
21
|
+
title: string;
|
|
22
|
+
href: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function getDocumentTitle(meta: ParsedDocumentMeta, body: string, fallback: string): string;
|
|
26
|
+
export declare function getDocumentSummary(meta: ParsedDocumentMeta, body: string): string | undefined;
|
|
16
27
|
export declare function parseMarkdownDocument(sourcePath: string, markdown: string): Promise<ParsedDocument>;
|
|
17
28
|
export declare function renderMarkdown(markdown: string): Promise<string>;
|
|
18
29
|
export declare function rewriteMarkdownLinksInHtml(html: string): string;
|
|
19
30
|
export declare function stripManagedIndexBlock(markdown: string): string;
|
|
20
31
|
export declare function stripManagedIndexLinks(markdown: string, hrefs: ReadonlySet<string>): string;
|
|
32
|
+
export declare function extractManagedIndexEntries(markdown: string): ManagedIndexEntry[];
|
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));
|
|
@@ -49,11 +58,43 @@ export function stripManagedIndexLinks(markdown, hrefs) {
|
|
|
49
58
|
return `${start}${keptBlocks.join('\n\n')}\n${end}`;
|
|
50
59
|
});
|
|
51
60
|
}
|
|
61
|
+
export function extractManagedIndexEntries(markdown) {
|
|
62
|
+
const match = markdown.match(/<!-- INDEX:START -->\n?([\s\S]*?)\n?<!-- INDEX:END -->/);
|
|
63
|
+
if (!match) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const blocks = match[1]
|
|
67
|
+
.trim()
|
|
68
|
+
.split(/\n\s*\n/g)
|
|
69
|
+
.map((block) => block.trim())
|
|
70
|
+
.filter((block) => block !== '');
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const block of blocks) {
|
|
73
|
+
const lines = block.split('\n');
|
|
74
|
+
const firstLine = lines[0]?.trim() ?? '';
|
|
75
|
+
const entryMatch = firstLine.match(/^- \[([^\]]+)\]\(([^)]+)\)$/);
|
|
76
|
+
if (!entryMatch) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const rawHref = entryMatch[2];
|
|
80
|
+
const href = rewriteMarkdownHref(rawHref);
|
|
81
|
+
entries.push({
|
|
82
|
+
kind: href.endsWith('/') ? 'directory' : 'article',
|
|
83
|
+
title: entryMatch[1],
|
|
84
|
+
href,
|
|
85
|
+
detail: lines[1]?.trim() || undefined,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
52
90
|
function normalizeMeta(data) {
|
|
53
91
|
const meta = { ...data };
|
|
54
92
|
if (typeof data.title === 'string') {
|
|
55
93
|
meta.title = data.title;
|
|
56
94
|
}
|
|
95
|
+
if (typeof data.name === 'string') {
|
|
96
|
+
meta.name = data.name;
|
|
97
|
+
}
|
|
57
98
|
if (typeof data.date === 'string') {
|
|
58
99
|
meta.date = data.date;
|
|
59
100
|
}
|
|
@@ -63,6 +104,9 @@ function normalizeMeta(data) {
|
|
|
63
104
|
if (typeof data.summary === 'string') {
|
|
64
105
|
meta.summary = data.summary;
|
|
65
106
|
}
|
|
107
|
+
if (typeof data.description === 'string') {
|
|
108
|
+
meta.description = data.description;
|
|
109
|
+
}
|
|
66
110
|
if (typeof data.draft === 'boolean') {
|
|
67
111
|
meta.draft = data.draft;
|
|
68
112
|
}
|
|
@@ -78,6 +122,12 @@ function normalizeMeta(data) {
|
|
|
78
122
|
meta.order = order;
|
|
79
123
|
}
|
|
80
124
|
}
|
|
125
|
+
if (typeof data.aliases === 'string' && data.aliases !== '') {
|
|
126
|
+
meta.aliases = [data.aliases];
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(data.aliases)) {
|
|
129
|
+
meta.aliases = data.aliases.filter((value) => typeof value === 'string' && value !== '');
|
|
130
|
+
}
|
|
81
131
|
return meta;
|
|
82
132
|
}
|
|
83
133
|
function rewriteMarkdownHref(href) {
|
|
@@ -108,6 +158,12 @@ function rewriteMarkdownPath(pathname) {
|
|
|
108
158
|
if (pathname.toLowerCase() === 'readme.md') {
|
|
109
159
|
return './';
|
|
110
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
|
+
}
|
|
111
167
|
return pathname.slice(0, -'.md'.length);
|
|
112
168
|
}
|
|
113
169
|
function shouldPreserveHref(href) {
|
|
@@ -133,3 +189,35 @@ function normalizeManagedIndexHref(href) {
|
|
|
133
189
|
}
|
|
134
190
|
return href;
|
|
135
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,8 +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;
|
|
7
|
+
acceptHeader?: string;
|
|
8
|
+
searchParams?: URLSearchParams;
|
|
9
|
+
requestUrl?: string;
|
|
10
|
+
searchApi?: SearchApi;
|
|
6
11
|
}
|
|
7
12
|
export interface SiteResponse {
|
|
8
13
|
status: number;
|