mdorigin 0.1.0

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.
Files changed (42) hide show
  1. package/README.md +29 -0
  2. package/dist/adapters/cloudflare.d.ts +17 -0
  3. package/dist/adapters/cloudflare.js +53 -0
  4. package/dist/adapters/node.d.ts +11 -0
  5. package/dist/adapters/node.js +115 -0
  6. package/dist/cli/build-cloudflare.d.ts +1 -0
  7. package/dist/cli/build-cloudflare.js +48 -0
  8. package/dist/cli/build-index.d.ts +1 -0
  9. package/dist/cli/build-index.js +35 -0
  10. package/dist/cli/dev.d.ts +1 -0
  11. package/dist/cli/dev.js +53 -0
  12. package/dist/cli/init-cloudflare.d.ts +1 -0
  13. package/dist/cli/init-cloudflare.js +59 -0
  14. package/dist/cli/main.d.ts +1 -0
  15. package/dist/cli/main.js +38 -0
  16. package/dist/cloudflare-runtime.d.ts +2 -0
  17. package/dist/cloudflare-runtime.js +1 -0
  18. package/dist/cloudflare.d.ts +31 -0
  19. package/dist/cloudflare.js +130 -0
  20. package/dist/core/content-store.d.ts +27 -0
  21. package/dist/core/content-store.js +95 -0
  22. package/dist/core/content-type.d.ts +9 -0
  23. package/dist/core/content-type.js +19 -0
  24. package/dist/core/directory-index.d.ts +2 -0
  25. package/dist/core/directory-index.js +5 -0
  26. package/dist/core/markdown.d.ts +20 -0
  27. package/dist/core/markdown.js +135 -0
  28. package/dist/core/request-handler.d.ts +12 -0
  29. package/dist/core/request-handler.js +322 -0
  30. package/dist/core/router.d.ts +7 -0
  31. package/dist/core/router.js +82 -0
  32. package/dist/core/site-config.d.ts +38 -0
  33. package/dist/core/site-config.js +123 -0
  34. package/dist/html/template-kind.d.ts +1 -0
  35. package/dist/html/template-kind.js +1 -0
  36. package/dist/html/template.d.ts +19 -0
  37. package/dist/html/template.js +67 -0
  38. package/dist/html/theme.d.ts +2 -0
  39. package/dist/html/theme.js +608 -0
  40. package/dist/index-builder.d.ts +13 -0
  41. package/dist/index-builder.js +299 -0
  42. package/package.json +66 -0
@@ -0,0 +1,27 @@
1
+ export type ContentEntryKind = 'text' | 'binary';
2
+ export interface ContentEntry {
3
+ path: string;
4
+ kind: ContentEntryKind;
5
+ mediaType: string;
6
+ text?: string;
7
+ bytes?: Uint8Array;
8
+ }
9
+ export interface ContentDirectoryEntry {
10
+ name: string;
11
+ path: string;
12
+ kind: 'file' | 'directory';
13
+ }
14
+ export interface ContentStore {
15
+ get(contentPath: string): Promise<ContentEntry | null>;
16
+ listDirectory(contentPath: string): Promise<ContentDirectoryEntry[] | null>;
17
+ }
18
+ export declare class MemoryContentStore implements ContentStore {
19
+ private readonly entries;
20
+ constructor(entries: Iterable<ContentEntry>);
21
+ get(contentPath: string): Promise<ContentEntry | null>;
22
+ listDirectory(contentPath: string): Promise<ContentDirectoryEntry[] | null>;
23
+ }
24
+ export declare function getMediaTypeForPath(contentPath: string): string;
25
+ export declare function isLikelyTextPath(contentPath: string): boolean;
26
+ export declare function normalizeContentPath(inputPath: string): string | null;
27
+ export declare function normalizeDirectoryPath(inputPath: string): string | null;
@@ -0,0 +1,95 @@
1
+ import path from 'node:path';
2
+ export class MemoryContentStore {
3
+ entries;
4
+ constructor(entries) {
5
+ this.entries = new Map(Array.from(entries, (entry) => [entry.path, { ...entry }]));
6
+ }
7
+ async get(contentPath) {
8
+ return this.entries.get(contentPath) ?? null;
9
+ }
10
+ async listDirectory(contentPath) {
11
+ const directoryPath = normalizeDirectoryPath(contentPath);
12
+ if (directoryPath === null) {
13
+ return null;
14
+ }
15
+ const prefix = directoryPath === '' ? '' : `${directoryPath}/`;
16
+ const children = new Map();
17
+ for (const entryPath of this.entries.keys()) {
18
+ if (!entryPath.startsWith(prefix)) {
19
+ continue;
20
+ }
21
+ const remainder = entryPath.slice(prefix.length);
22
+ if (remainder === '') {
23
+ continue;
24
+ }
25
+ const [firstSegment, ...rest] = remainder.split('/');
26
+ if (firstSegment.startsWith('.')) {
27
+ continue;
28
+ }
29
+ if (rest.length === 0) {
30
+ children.set(firstSegment, {
31
+ name: firstSegment,
32
+ path: prefix + firstSegment,
33
+ kind: 'file',
34
+ });
35
+ continue;
36
+ }
37
+ children.set(firstSegment, {
38
+ name: firstSegment,
39
+ path: prefix + firstSegment,
40
+ kind: 'directory',
41
+ });
42
+ }
43
+ const entries = Array.from(children.values()).sort(compareDirectoryEntries);
44
+ return entries.length > 0 || directoryPath === '' ? entries : null;
45
+ }
46
+ }
47
+ const MEDIA_TYPES = new Map([
48
+ ['.css', 'text/css; charset=utf-8'],
49
+ ['.gif', 'image/gif'],
50
+ ['.html', 'text/html; charset=utf-8'],
51
+ ['.jpg', 'image/jpeg'],
52
+ ['.jpeg', 'image/jpeg'],
53
+ ['.js', 'text/javascript; charset=utf-8'],
54
+ ['.json', 'application/json; charset=utf-8'],
55
+ ['.md', 'text/markdown; charset=utf-8'],
56
+ ['.pdf', 'application/pdf'],
57
+ ['.png', 'image/png'],
58
+ ['.svg', 'image/svg+xml'],
59
+ ['.txt', 'text/plain; charset=utf-8'],
60
+ ['.webp', 'image/webp'],
61
+ ]);
62
+ export function getMediaTypeForPath(contentPath) {
63
+ const extension = path.posix.extname(contentPath).toLowerCase();
64
+ return MEDIA_TYPES.get(extension) ?? 'application/octet-stream';
65
+ }
66
+ export function isLikelyTextPath(contentPath) {
67
+ const mediaType = getMediaTypeForPath(contentPath);
68
+ return (mediaType.startsWith('text/') ||
69
+ mediaType === 'application/json; charset=utf-8' ||
70
+ mediaType === 'image/svg+xml');
71
+ }
72
+ export function normalizeContentPath(inputPath) {
73
+ const normalized = inputPath.replace(/\\/g, '/');
74
+ const withoutLeadingSlash = normalized.replace(/^\/+/, '');
75
+ const resolved = path.posix.normalize(withoutLeadingSlash);
76
+ if (resolved === '' ||
77
+ resolved === '.' ||
78
+ resolved.startsWith('../') ||
79
+ resolved.includes('/../')) {
80
+ return null;
81
+ }
82
+ return resolved;
83
+ }
84
+ export function normalizeDirectoryPath(inputPath) {
85
+ if (inputPath === '') {
86
+ return '';
87
+ }
88
+ return normalizeContentPath(inputPath);
89
+ }
90
+ function compareDirectoryEntries(left, right) {
91
+ if (left.kind !== right.kind) {
92
+ return left.kind === 'directory' ? -1 : 1;
93
+ }
94
+ return left.name.localeCompare(right.name);
95
+ }
@@ -0,0 +1,9 @@
1
+ import type { ParsedDocumentMeta } from './markdown.js';
2
+ export type ContentType = 'page' | 'post';
3
+ export interface DirectoryShape {
4
+ hasChildDirectories: boolean;
5
+ hasExtraMarkdownFiles: boolean;
6
+ hasAssetFiles: boolean;
7
+ }
8
+ export declare function resolveContentType(meta: ParsedDocumentMeta): ContentType | undefined;
9
+ export declare function inferDirectoryContentType(meta: ParsedDocumentMeta, shape: DirectoryShape): ContentType;
@@ -0,0 +1,19 @@
1
+ export function resolveContentType(meta) {
2
+ return meta.type === 'page' || meta.type === 'post' ? meta.type : undefined;
3
+ }
4
+ export function inferDirectoryContentType(meta, shape) {
5
+ const explicitType = resolveContentType(meta);
6
+ if (explicitType) {
7
+ return explicitType;
8
+ }
9
+ if (typeof meta.date === 'string' && meta.date !== '') {
10
+ return 'post';
11
+ }
12
+ if (shape.hasChildDirectories || shape.hasExtraMarkdownFiles) {
13
+ return 'page';
14
+ }
15
+ if (shape.hasAssetFiles) {
16
+ return 'post';
17
+ }
18
+ return 'page';
19
+ }
@@ -0,0 +1,2 @@
1
+ export declare const DIRECTORY_INDEX_FILENAMES: readonly ["index.md", "README.md"];
2
+ export declare function getDirectoryIndexCandidates(directoryPath: string): string[];
@@ -0,0 +1,5 @@
1
+ import path from 'node:path';
2
+ export const DIRECTORY_INDEX_FILENAMES = ['index.md', 'README.md'];
3
+ export function getDirectoryIndexCandidates(directoryPath) {
4
+ return DIRECTORY_INDEX_FILENAMES.map((filename) => directoryPath === '' ? filename : path.posix.join(directoryPath, filename));
5
+ }
@@ -0,0 +1,20 @@
1
+ export interface ParsedDocumentMeta {
2
+ title?: string;
3
+ date?: string;
4
+ summary?: string;
5
+ draft?: boolean;
6
+ type?: string;
7
+ order?: number;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface ParsedDocument {
11
+ sourcePath: string;
12
+ body: string;
13
+ html: string;
14
+ meta: ParsedDocumentMeta;
15
+ }
16
+ export declare function parseMarkdownDocument(sourcePath: string, markdown: string): Promise<ParsedDocument>;
17
+ export declare function renderMarkdown(markdown: string): Promise<string>;
18
+ export declare function rewriteMarkdownLinksInHtml(html: string): string;
19
+ export declare function stripManagedIndexBlock(markdown: string): string;
20
+ export declare function stripManagedIndexLinks(markdown: string, hrefs: ReadonlySet<string>): string;
@@ -0,0 +1,135 @@
1
+ import matter from 'gray-matter';
2
+ import { remark } from 'remark';
3
+ import remarkGfm from 'remark-gfm';
4
+ import remarkHtml from 'remark-html';
5
+ export async function parseMarkdownDocument(sourcePath, markdown) {
6
+ const parsed = matter(markdown);
7
+ const html = rewriteMarkdownLinksInHtml(await renderMarkdown(parsed.content));
8
+ return {
9
+ sourcePath,
10
+ body: parsed.content,
11
+ html,
12
+ meta: normalizeMeta(parsed.data),
13
+ };
14
+ }
15
+ export async function renderMarkdown(markdown) {
16
+ const output = await remark()
17
+ .use(remarkGfm)
18
+ .use(remarkHtml)
19
+ .process(markdown);
20
+ return String(output);
21
+ }
22
+ export function rewriteMarkdownLinksInHtml(html) {
23
+ return html.replaceAll(/(<a\b[^>]*?\shref=")([^"]+)(")/g, (_match, prefix, href, suffix) => `${prefix}${rewriteMarkdownHref(href)}${suffix}`);
24
+ }
25
+ export function stripManagedIndexBlock(markdown) {
26
+ return markdown.replace(/\n?<!-- INDEX:START -->[\s\S]*?<!-- INDEX:END -->\n?/g, '\n').trimEnd();
27
+ }
28
+ export function stripManagedIndexLinks(markdown, hrefs) {
29
+ if (hrefs.size === 0) {
30
+ return markdown;
31
+ }
32
+ return markdown.replace(/(\n<!-- INDEX:START -->\n)([\s\S]*?)(\n<!-- INDEX:END -->)/, (_match, start, content, end) => {
33
+ const blocks = content
34
+ .trim()
35
+ .split(/\n\s*\n/g)
36
+ .map((block) => block.trim())
37
+ .filter((block) => block !== '');
38
+ const keptBlocks = blocks.filter((block) => {
39
+ const firstLine = block.split('\n', 1)[0] ?? '';
40
+ const hrefMatch = firstLine.match(/\[[^\]]+\]\(([^)]+)\)/);
41
+ if (!hrefMatch) {
42
+ return true;
43
+ }
44
+ return !hrefs.has(normalizeManagedIndexHref(hrefMatch[1]));
45
+ });
46
+ if (keptBlocks.length === 0) {
47
+ return `${start.trimEnd()}\n\n${end.trimStart()}`;
48
+ }
49
+ return `${start}${keptBlocks.join('\n\n')}\n${end}`;
50
+ });
51
+ }
52
+ function normalizeMeta(data) {
53
+ const meta = { ...data };
54
+ if (typeof data.title === 'string') {
55
+ meta.title = data.title;
56
+ }
57
+ if (typeof data.date === 'string') {
58
+ meta.date = data.date;
59
+ }
60
+ if (data.date instanceof Date && !Number.isNaN(data.date.getTime())) {
61
+ meta.date = data.date.toISOString().slice(0, 10);
62
+ }
63
+ if (typeof data.summary === 'string') {
64
+ meta.summary = data.summary;
65
+ }
66
+ if (typeof data.draft === 'boolean') {
67
+ meta.draft = data.draft;
68
+ }
69
+ if (typeof data.type === 'string') {
70
+ meta.type = data.type;
71
+ }
72
+ if (typeof data.order === 'number' && Number.isFinite(data.order)) {
73
+ meta.order = data.order;
74
+ }
75
+ if (typeof data.order === 'string') {
76
+ const order = Number.parseInt(data.order, 10);
77
+ if (Number.isFinite(order)) {
78
+ meta.order = order;
79
+ }
80
+ }
81
+ return meta;
82
+ }
83
+ function rewriteMarkdownHref(href) {
84
+ if (shouldPreserveHref(href)) {
85
+ return href;
86
+ }
87
+ const [pathPart, hashPart] = splitOnce(href, '#');
88
+ const [pathname, queryPart] = splitOnce(pathPart, '?');
89
+ if (!pathname.toLowerCase().endsWith('.md')) {
90
+ return href;
91
+ }
92
+ const normalizedPath = pathname.replace(/\\/g, '/');
93
+ const rewrittenPath = rewriteMarkdownPath(normalizedPath);
94
+ const querySuffix = queryPart !== undefined ? `?${queryPart}` : '';
95
+ const hashSuffix = hashPart !== undefined ? `#${hashPart}` : '';
96
+ return `${rewrittenPath}${querySuffix}${hashSuffix}`;
97
+ }
98
+ function rewriteMarkdownPath(pathname) {
99
+ if (pathname.toLowerCase().endsWith('/index.md')) {
100
+ return pathname.slice(0, -'index.md'.length);
101
+ }
102
+ if (pathname.toLowerCase() === 'index.md') {
103
+ return './';
104
+ }
105
+ if (pathname.toLowerCase().endsWith('/readme.md')) {
106
+ return pathname.slice(0, -'README.md'.length);
107
+ }
108
+ if (pathname.toLowerCase() === 'readme.md') {
109
+ return './';
110
+ }
111
+ return pathname.slice(0, -'.md'.length);
112
+ }
113
+ function shouldPreserveHref(href) {
114
+ return (href.startsWith('#') ||
115
+ href.startsWith('mailto:') ||
116
+ href.startsWith('tel:') ||
117
+ href.startsWith('//') ||
118
+ /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href));
119
+ }
120
+ function splitOnce(value, separator) {
121
+ const index = value.indexOf(separator);
122
+ if (index === -1) {
123
+ return [value, undefined];
124
+ }
125
+ return [value.slice(0, index), value.slice(index + separator.length)];
126
+ }
127
+ function normalizeManagedIndexHref(href) {
128
+ if (href === './') {
129
+ return '/';
130
+ }
131
+ if (href.startsWith('./')) {
132
+ return `/${href.slice(2)}`;
133
+ }
134
+ return href;
135
+ }
@@ -0,0 +1,12 @@
1
+ import type { ContentStore } from './content-store.js';
2
+ import type { ResolvedSiteConfig } from './site-config.js';
3
+ export interface HandleSiteRequestOptions {
4
+ draftMode: 'include' | 'exclude';
5
+ siteConfig: ResolvedSiteConfig;
6
+ }
7
+ export interface SiteResponse {
8
+ status: number;
9
+ headers: Record<string, string>;
10
+ body?: string | Uint8Array;
11
+ }
12
+ export declare function handleSiteRequest(store: ContentStore, pathname: string, options: HandleSiteRequestOptions): Promise<SiteResponse>;
@@ -0,0 +1,322 @@
1
+ import path from 'node:path';
2
+ import { inferDirectoryContentType } from './content-type.js';
3
+ import { getDirectoryIndexCandidates } from './directory-index.js';
4
+ import { parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
5
+ import { resolveRequest } from './router.js';
6
+ import { escapeHtml, renderDocument } from '../html/template.js';
7
+ export async function handleSiteRequest(store, pathname, options) {
8
+ const resolved = resolveRequest(pathname);
9
+ if (resolved.kind === 'not-found' || !resolved.sourcePath) {
10
+ return notFound();
11
+ }
12
+ const entry = await store.get(resolved.sourcePath);
13
+ if (entry === null) {
14
+ if (resolved.kind === 'html' && resolved.requestPath.endsWith('/')) {
15
+ const directoryIndexResponse = await tryRenderAlternateDirectoryIndex(store, resolved.requestPath, options);
16
+ if (directoryIndexResponse !== null) {
17
+ return directoryIndexResponse;
18
+ }
19
+ return renderDirectoryListing(store, resolved.requestPath, options.siteConfig);
20
+ }
21
+ return notFound();
22
+ }
23
+ if (resolved.kind === 'asset') {
24
+ return serveAsset(entry);
25
+ }
26
+ if (entry.kind !== 'text' || entry.text === undefined) {
27
+ return notFound();
28
+ }
29
+ if (resolved.kind === 'markdown') {
30
+ const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
31
+ if (parsed.meta.draft === true && options.draftMode === 'exclude') {
32
+ return notFound();
33
+ }
34
+ return {
35
+ status: 200,
36
+ headers: {
37
+ 'content-type': entry.mediaType,
38
+ },
39
+ body: entry.text,
40
+ };
41
+ }
42
+ const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
43
+ if (parsed.meta.draft === true && options.draftMode === 'exclude') {
44
+ return notFound();
45
+ }
46
+ const navigation = await resolveTopNav(store, options.siteConfig);
47
+ const renderedBody = isRootHomeRequest(resolved.requestPath) && !options.siteConfig.showHomeIndex
48
+ ? stripManagedIndexBlock(entry.text)
49
+ : isRootHomeRequest(resolved.requestPath) && navigation.items.length > 0
50
+ ? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
51
+ : entry.text;
52
+ const renderedParsed = renderedBody === entry.text
53
+ ? parsed
54
+ : await parseMarkdownDocument(resolved.sourcePath, renderedBody);
55
+ return {
56
+ status: 200,
57
+ headers: {
58
+ 'content-type': 'text/html; charset=utf-8',
59
+ },
60
+ body: renderDocument({
61
+ siteTitle: options.siteConfig.siteTitle,
62
+ siteDescription: options.siteConfig.siteDescription,
63
+ title: getDocumentTitle(parsed),
64
+ body: renderedParsed.html,
65
+ summary: options.siteConfig.showSummary === false ? undefined : parsed.meta.summary,
66
+ date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
67
+ showSummary: options.siteConfig.showSummary,
68
+ showDate: options.siteConfig.showDate,
69
+ theme: options.siteConfig.theme,
70
+ template: options.siteConfig.template,
71
+ topNav: navigation.items,
72
+ stylesheetContent: options.siteConfig.stylesheetContent,
73
+ }),
74
+ };
75
+ }
76
+ function serveAsset(entry) {
77
+ if (entry.kind === 'text' && entry.text !== undefined) {
78
+ return {
79
+ status: 200,
80
+ headers: {
81
+ 'content-type': entry.mediaType,
82
+ },
83
+ body: entry.text,
84
+ };
85
+ }
86
+ if (entry.kind === 'binary' && entry.bytes !== undefined) {
87
+ return {
88
+ status: 200,
89
+ headers: {
90
+ 'content-type': entry.mediaType,
91
+ },
92
+ body: entry.bytes,
93
+ };
94
+ }
95
+ return notFound();
96
+ }
97
+ function notFound() {
98
+ return {
99
+ status: 404,
100
+ headers: {
101
+ 'content-type': 'text/plain; charset=utf-8',
102
+ },
103
+ body: 'Not Found',
104
+ };
105
+ }
106
+ function getDocumentTitle(parsed) {
107
+ if (parsed.meta.title) {
108
+ return parsed.meta.title;
109
+ }
110
+ const basename = path.posix.basename(parsed.sourcePath, '.md');
111
+ return basename === 'index'
112
+ ? path.posix.basename(path.posix.dirname(parsed.sourcePath)) || 'mdorigin'
113
+ : basename;
114
+ }
115
+ async function renderDirectoryListing(store, requestPath, siteConfig) {
116
+ const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
117
+ const entries = await store.listDirectory(directoryPath);
118
+ if (entries === null) {
119
+ return notFound();
120
+ }
121
+ const visibleEntries = entries.filter(isVisibleDirectoryEntry);
122
+ const navigation = await resolveTopNav(store, siteConfig);
123
+ const listItems = visibleEntries
124
+ .map((entry) => `<li><a href="${getDirectoryEntryHref(requestPath, entry)}">${escapeHtml(getDirectoryEntryLabel(entry))}</a></li>`)
125
+ .join('');
126
+ const body = [
127
+ `<h1>${escapeHtml(getDirectoryTitle(requestPath))}</h1>`,
128
+ visibleEntries.length > 0 ? `<ul>${listItems}</ul>` : '<p>This directory is empty.</p>',
129
+ ].join('');
130
+ return {
131
+ status: 200,
132
+ headers: {
133
+ 'content-type': 'text/html; charset=utf-8',
134
+ },
135
+ body: renderDocument({
136
+ siteTitle: siteConfig.siteTitle,
137
+ siteDescription: siteConfig.siteDescription,
138
+ title: getDirectoryTitle(requestPath),
139
+ body,
140
+ showSummary: false,
141
+ showDate: false,
142
+ theme: siteConfig.theme,
143
+ template: siteConfig.template,
144
+ topNav: navigation.items,
145
+ stylesheetContent: siteConfig.stylesheetContent,
146
+ }),
147
+ };
148
+ }
149
+ function isVisibleDirectoryEntry(entry) {
150
+ if (entry.kind === 'directory') {
151
+ return true;
152
+ }
153
+ return path.posix.extname(entry.name).toLowerCase() === '.md';
154
+ }
155
+ function getDirectoryEntryHref(requestPath, entry) {
156
+ const basePath = requestPath.endsWith('/') ? requestPath : `${requestPath}/`;
157
+ if (entry.kind === 'directory') {
158
+ return `${basePath}${entry.name}/`;
159
+ }
160
+ return `${basePath}${entry.name.slice(0, -'.md'.length)}`;
161
+ }
162
+ function getDirectoryEntryLabel(entry) {
163
+ return entry.kind === 'directory'
164
+ ? `${entry.name}/`
165
+ : entry.name.slice(0, -'.md'.length);
166
+ }
167
+ function getDirectoryTitle(requestPath) {
168
+ return requestPath === '/' ? 'Index' : requestPath;
169
+ }
170
+ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
171
+ const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
172
+ for (const candidatePath of getDirectoryIndexCandidates(directoryPath)) {
173
+ if (candidatePath === (directoryPath === '' ? 'index.md' : `${directoryPath}/index.md`)) {
174
+ continue;
175
+ }
176
+ const entry = await store.get(candidatePath);
177
+ if (entry === null || entry.kind !== 'text' || entry.text === undefined) {
178
+ continue;
179
+ }
180
+ const parsed = await parseMarkdownDocument(candidatePath, entry.text);
181
+ if (parsed.meta.draft === true && options.draftMode === 'exclude') {
182
+ return notFound();
183
+ }
184
+ const navigation = await resolveTopNav(store, options.siteConfig);
185
+ const renderedBody = isRootHomeRequest(requestPath) && !options.siteConfig.showHomeIndex
186
+ ? stripManagedIndexBlock(entry.text)
187
+ : isRootHomeRequest(requestPath) && navigation.items.length > 0
188
+ ? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
189
+ : entry.text;
190
+ const renderedParsed = renderedBody === entry.text
191
+ ? parsed
192
+ : await parseMarkdownDocument(candidatePath, renderedBody);
193
+ return {
194
+ status: 200,
195
+ headers: {
196
+ 'content-type': 'text/html; charset=utf-8',
197
+ },
198
+ body: renderDocument({
199
+ siteTitle: options.siteConfig.siteTitle,
200
+ siteDescription: options.siteConfig.siteDescription,
201
+ title: getDocumentTitle(parsed),
202
+ body: renderedParsed.html,
203
+ summary: options.siteConfig.showSummary === false ? undefined : parsed.meta.summary,
204
+ date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
205
+ showSummary: options.siteConfig.showSummary,
206
+ showDate: options.siteConfig.showDate,
207
+ theme: options.siteConfig.theme,
208
+ template: options.siteConfig.template,
209
+ topNav: navigation.items,
210
+ stylesheetContent: options.siteConfig.stylesheetContent,
211
+ }),
212
+ };
213
+ }
214
+ return null;
215
+ }
216
+ function isRootHomeRequest(requestPath) {
217
+ return requestPath === '/';
218
+ }
219
+ async function resolveTopNav(store, siteConfig) {
220
+ if (siteConfig.topNav.length > 0) {
221
+ return {
222
+ items: siteConfig.topNav,
223
+ autoGenerated: false,
224
+ };
225
+ }
226
+ const rootEntries = await store.listDirectory('');
227
+ if (rootEntries === null) {
228
+ return {
229
+ items: [],
230
+ autoGenerated: false,
231
+ };
232
+ }
233
+ const directories = rootEntries.filter((entry) => entry.kind === 'directory');
234
+ const navItems = [];
235
+ const orderedNavItems = [];
236
+ for (const entry of directories) {
237
+ const resolved = await resolveDirectoryNav(store, entry);
238
+ if (resolved.type !== 'page') {
239
+ continue;
240
+ }
241
+ orderedNavItems.push({
242
+ label: resolved.title,
243
+ href: `/${entry.name}/`,
244
+ order: resolved.order,
245
+ });
246
+ }
247
+ orderedNavItems.sort((left, right) => {
248
+ if (left.order !== undefined && right.order !== undefined) {
249
+ if (left.order !== right.order) {
250
+ return left.order - right.order;
251
+ }
252
+ }
253
+ else if (left.order !== undefined) {
254
+ return -1;
255
+ }
256
+ else if (right.order !== undefined) {
257
+ return 1;
258
+ }
259
+ return left.label.localeCompare(right.label);
260
+ });
261
+ navItems.push(...orderedNavItems.map(({ label, href }) => ({ label, href })));
262
+ return {
263
+ items: navItems,
264
+ autoGenerated: navItems.length > 0,
265
+ };
266
+ }
267
+ async function resolveDirectoryNav(store, entry) {
268
+ for (const candidatePath of getDirectoryIndexCandidates(entry.path)) {
269
+ const contentEntry = await store.get(candidatePath);
270
+ if (contentEntry === null || contentEntry.kind !== 'text' || contentEntry.text === undefined) {
271
+ continue;
272
+ }
273
+ const parsed = await parseMarkdownDocument(candidatePath, contentEntry.text);
274
+ const shape = await inspectDirectoryShape(store, entry.path);
275
+ return {
276
+ title: typeof parsed.meta.title === 'string' && parsed.meta.title !== ''
277
+ ? parsed.meta.title
278
+ : entry.name,
279
+ type: inferDirectoryContentType(parsed.meta, shape),
280
+ order: parsed.meta.order,
281
+ };
282
+ }
283
+ return {
284
+ title: entry.name,
285
+ type: 'page',
286
+ };
287
+ }
288
+ async function inspectDirectoryShape(store, directoryPath) {
289
+ const entries = await store.listDirectory(directoryPath);
290
+ if (entries === null) {
291
+ return {
292
+ hasChildDirectories: false,
293
+ hasExtraMarkdownFiles: false,
294
+ hasAssetFiles: false,
295
+ };
296
+ }
297
+ let hasChildDirectories = false;
298
+ let hasExtraMarkdownFiles = false;
299
+ let hasAssetFiles = false;
300
+ for (const entry of entries) {
301
+ if (entry.name.startsWith('.')) {
302
+ continue;
303
+ }
304
+ if (entry.kind === 'directory') {
305
+ hasChildDirectories = true;
306
+ continue;
307
+ }
308
+ const extension = path.posix.extname(entry.name).toLowerCase();
309
+ if (extension === '.md') {
310
+ if (entry.name !== 'index.md' && entry.name !== 'README.md') {
311
+ hasExtraMarkdownFiles = true;
312
+ }
313
+ continue;
314
+ }
315
+ hasAssetFiles = true;
316
+ }
317
+ return {
318
+ hasChildDirectories,
319
+ hasExtraMarkdownFiles,
320
+ hasAssetFiles,
321
+ };
322
+ }
@@ -0,0 +1,7 @@
1
+ export type ResolvedRequestKind = 'markdown' | 'html' | 'asset' | 'not-found';
2
+ export interface ResolvedRequest {
3
+ kind: ResolvedRequestKind;
4
+ requestPath: string;
5
+ sourcePath?: string;
6
+ }
7
+ export declare function resolveRequest(pathname: string): ResolvedRequest;