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.
- package/README.md +29 -0
- package/dist/adapters/cloudflare.d.ts +17 -0
- package/dist/adapters/cloudflare.js +53 -0
- package/dist/adapters/node.d.ts +11 -0
- package/dist/adapters/node.js +115 -0
- package/dist/cli/build-cloudflare.d.ts +1 -0
- package/dist/cli/build-cloudflare.js +48 -0
- package/dist/cli/build-index.d.ts +1 -0
- package/dist/cli/build-index.js +35 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +53 -0
- package/dist/cli/init-cloudflare.d.ts +1 -0
- package/dist/cli/init-cloudflare.js +59 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +38 -0
- package/dist/cloudflare-runtime.d.ts +2 -0
- package/dist/cloudflare-runtime.js +1 -0
- package/dist/cloudflare.d.ts +31 -0
- package/dist/cloudflare.js +130 -0
- package/dist/core/content-store.d.ts +27 -0
- package/dist/core/content-store.js +95 -0
- package/dist/core/content-type.d.ts +9 -0
- package/dist/core/content-type.js +19 -0
- package/dist/core/directory-index.d.ts +2 -0
- package/dist/core/directory-index.js +5 -0
- package/dist/core/markdown.d.ts +20 -0
- package/dist/core/markdown.js +135 -0
- package/dist/core/request-handler.d.ts +12 -0
- package/dist/core/request-handler.js +322 -0
- package/dist/core/router.d.ts +7 -0
- package/dist/core/router.js +82 -0
- package/dist/core/site-config.d.ts +38 -0
- package/dist/core/site-config.js +123 -0
- package/dist/html/template-kind.d.ts +1 -0
- package/dist/html/template-kind.js +1 -0
- package/dist/html/template.d.ts +19 -0
- package/dist/html/template.js +67 -0
- package/dist/html/theme.d.ts +2 -0
- package/dist/html/theme.js +608 -0
- package/dist/index-builder.d.ts +13 -0
- package/dist/index-builder.js +299 -0
- package/package.json +66 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { normalizeContentPath } from './content-store.js';
|
|
3
|
+
export function resolveRequest(pathname) {
|
|
4
|
+
const normalizedRequestPath = normalizeRequestPath(pathname);
|
|
5
|
+
if (normalizedRequestPath === null) {
|
|
6
|
+
return { kind: 'not-found', requestPath: pathname };
|
|
7
|
+
}
|
|
8
|
+
if (normalizedRequestPath === '/') {
|
|
9
|
+
return {
|
|
10
|
+
kind: 'html',
|
|
11
|
+
requestPath: normalizedRequestPath,
|
|
12
|
+
sourcePath: 'index.md',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (normalizedRequestPath.endsWith('/')) {
|
|
16
|
+
const sourcePath = normalizeContentPath(`${normalizedRequestPath.slice(1)}index.md`);
|
|
17
|
+
return sourcePath === null
|
|
18
|
+
? { kind: 'not-found', requestPath: normalizedRequestPath }
|
|
19
|
+
: {
|
|
20
|
+
kind: 'html',
|
|
21
|
+
requestPath: normalizedRequestPath,
|
|
22
|
+
sourcePath,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const relativePath = normalizedRequestPath.slice(1);
|
|
26
|
+
const extension = path.posix.extname(relativePath).toLowerCase();
|
|
27
|
+
if (extension === '.html') {
|
|
28
|
+
const sourcePath = normalizeContentPath(`${relativePath.slice(0, -'.html'.length)}.md`);
|
|
29
|
+
return sourcePath === null
|
|
30
|
+
? { kind: 'not-found', requestPath: normalizedRequestPath }
|
|
31
|
+
: {
|
|
32
|
+
kind: 'html',
|
|
33
|
+
requestPath: normalizedRequestPath,
|
|
34
|
+
sourcePath,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (extension === '.md') {
|
|
38
|
+
const sourcePath = normalizeContentPath(relativePath);
|
|
39
|
+
return sourcePath === null
|
|
40
|
+
? { kind: 'not-found', requestPath: normalizedRequestPath }
|
|
41
|
+
: {
|
|
42
|
+
kind: 'markdown',
|
|
43
|
+
requestPath: normalizedRequestPath,
|
|
44
|
+
sourcePath,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (extension === '') {
|
|
48
|
+
const sourcePath = normalizeContentPath(`${relativePath}.md`);
|
|
49
|
+
return sourcePath === null
|
|
50
|
+
? { kind: 'not-found', requestPath: normalizedRequestPath }
|
|
51
|
+
: {
|
|
52
|
+
kind: 'html',
|
|
53
|
+
requestPath: normalizedRequestPath,
|
|
54
|
+
sourcePath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const sourcePath = normalizeContentPath(relativePath);
|
|
58
|
+
return sourcePath === null
|
|
59
|
+
? { kind: 'not-found', requestPath: normalizedRequestPath }
|
|
60
|
+
: {
|
|
61
|
+
kind: 'asset',
|
|
62
|
+
requestPath: normalizedRequestPath,
|
|
63
|
+
sourcePath,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function normalizeRequestPath(pathname) {
|
|
67
|
+
try {
|
|
68
|
+
const decoded = decodeURIComponent(pathname || '/');
|
|
69
|
+
const collapsed = decoded.replace(/\/{2,}/g, '/');
|
|
70
|
+
const absolute = collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
|
|
71
|
+
const segments = absolute.split('/');
|
|
72
|
+
if (segments.some((segment) => segment === '..')) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const normalized = path.posix.normalize(absolute);
|
|
76
|
+
const hasTrailingSlash = absolute.endsWith('/') && normalized !== '/' && !normalized.endsWith('/');
|
|
77
|
+
return hasTrailingSlash ? `${normalized}/` : normalized;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ContentStore } from './content-store.js';
|
|
2
|
+
import type { TemplateName } from '../html/template-kind.js';
|
|
3
|
+
import type { BuiltInThemeName } from '../html/theme.js';
|
|
4
|
+
export interface SiteNavItem {
|
|
5
|
+
label: string;
|
|
6
|
+
href: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SiteConfig {
|
|
9
|
+
siteTitle?: string;
|
|
10
|
+
siteDescription?: string;
|
|
11
|
+
showDate?: boolean;
|
|
12
|
+
showSummary?: boolean;
|
|
13
|
+
stylesheet?: string;
|
|
14
|
+
theme?: BuiltInThemeName;
|
|
15
|
+
template?: TemplateName;
|
|
16
|
+
topNav?: SiteNavItem[];
|
|
17
|
+
showHomeIndex?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ResolvedSiteConfig {
|
|
20
|
+
siteTitle: string;
|
|
21
|
+
siteDescription?: string;
|
|
22
|
+
showDate: boolean;
|
|
23
|
+
showSummary: boolean;
|
|
24
|
+
theme: BuiltInThemeName;
|
|
25
|
+
template: TemplateName;
|
|
26
|
+
topNav: SiteNavItem[];
|
|
27
|
+
showHomeIndex: boolean;
|
|
28
|
+
stylesheetContent?: string;
|
|
29
|
+
siteTitleConfigured: boolean;
|
|
30
|
+
siteDescriptionConfigured: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface LoadSiteConfigOptions {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
rootDir?: string;
|
|
35
|
+
configPath?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function loadSiteConfig(options?: LoadSiteConfigOptions): Promise<ResolvedSiteConfig>;
|
|
38
|
+
export declare function applySiteConfigFrontmatterDefaults(store: ContentStore, siteConfig: ResolvedSiteConfig): Promise<ResolvedSiteConfig>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getDirectoryIndexCandidates } from './directory-index.js';
|
|
4
|
+
import { parseMarkdownDocument } from './markdown.js';
|
|
5
|
+
export async function loadSiteConfig(options = {}) {
|
|
6
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
7
|
+
const rootDir = options.rootDir ? path.resolve(options.rootDir) : null;
|
|
8
|
+
const configFilePath = options.configPath
|
|
9
|
+
? path.resolve(cwd, options.configPath)
|
|
10
|
+
: await resolveDefaultConfigPath(cwd, rootDir);
|
|
11
|
+
let parsedConfig = {};
|
|
12
|
+
try {
|
|
13
|
+
const configSource = await readFile(configFilePath, 'utf8');
|
|
14
|
+
parsedConfig = JSON.parse(configSource);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (!isNodeNotFound(error)) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const stylesheetPath = parsedConfig.stylesheet
|
|
22
|
+
? path.resolve(path.dirname(configFilePath), parsedConfig.stylesheet)
|
|
23
|
+
: null;
|
|
24
|
+
const stylesheetContent = stylesheetPath
|
|
25
|
+
? await readFile(stylesheetPath, 'utf8')
|
|
26
|
+
: undefined;
|
|
27
|
+
return {
|
|
28
|
+
siteTitle: typeof parsedConfig.siteTitle === 'string' && parsedConfig.siteTitle !== ''
|
|
29
|
+
? parsedConfig.siteTitle
|
|
30
|
+
: 'mdorigin',
|
|
31
|
+
siteDescription: typeof parsedConfig.siteDescription === 'string' &&
|
|
32
|
+
parsedConfig.siteDescription !== ''
|
|
33
|
+
? parsedConfig.siteDescription
|
|
34
|
+
: undefined,
|
|
35
|
+
showDate: parsedConfig.showDate ?? true,
|
|
36
|
+
showSummary: parsedConfig.showSummary ?? true,
|
|
37
|
+
theme: isBuiltInThemeName(parsedConfig.theme) ? parsedConfig.theme : 'paper',
|
|
38
|
+
template: isTemplateName(parsedConfig.template) ? parsedConfig.template : 'document',
|
|
39
|
+
topNav: normalizeTopNav(parsedConfig.topNav),
|
|
40
|
+
showHomeIndex: typeof parsedConfig.showHomeIndex === 'boolean'
|
|
41
|
+
? parsedConfig.showHomeIndex
|
|
42
|
+
: normalizeTopNav(parsedConfig.topNav).length === 0,
|
|
43
|
+
stylesheetContent,
|
|
44
|
+
siteTitleConfigured: typeof parsedConfig.siteTitle === 'string' && parsedConfig.siteTitle !== '',
|
|
45
|
+
siteDescriptionConfigured: typeof parsedConfig.siteDescription === 'string' &&
|
|
46
|
+
parsedConfig.siteDescription !== '',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function applySiteConfigFrontmatterDefaults(store, siteConfig) {
|
|
50
|
+
if (siteConfig.siteTitleConfigured && siteConfig.siteDescriptionConfigured) {
|
|
51
|
+
return siteConfig;
|
|
52
|
+
}
|
|
53
|
+
for (const candidatePath of getDirectoryIndexCandidates('')) {
|
|
54
|
+
const entry = await store.get(candidatePath);
|
|
55
|
+
if (entry === null || entry.kind !== 'text' || entry.text === undefined) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const parsed = await parseMarkdownDocument(candidatePath, entry.text);
|
|
59
|
+
return {
|
|
60
|
+
...siteConfig,
|
|
61
|
+
siteTitle: siteConfig.siteTitleConfigured
|
|
62
|
+
? siteConfig.siteTitle
|
|
63
|
+
: typeof parsed.meta.title === 'string' && parsed.meta.title !== ''
|
|
64
|
+
? parsed.meta.title
|
|
65
|
+
: siteConfig.siteTitle,
|
|
66
|
+
siteDescription: siteConfig.siteDescriptionConfigured
|
|
67
|
+
? siteConfig.siteDescription
|
|
68
|
+
: typeof parsed.meta.summary === 'string' && parsed.meta.summary !== ''
|
|
69
|
+
? parsed.meta.summary
|
|
70
|
+
: siteConfig.siteDescription,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return siteConfig;
|
|
74
|
+
}
|
|
75
|
+
async function resolveDefaultConfigPath(cwd, rootDir) {
|
|
76
|
+
const rootConfigPath = rootDir ? path.join(rootDir, 'mdorigin.config.json') : null;
|
|
77
|
+
if (rootConfigPath && (await pathExists(rootConfigPath))) {
|
|
78
|
+
return rootConfigPath;
|
|
79
|
+
}
|
|
80
|
+
return path.join(cwd, 'mdorigin.config.json');
|
|
81
|
+
}
|
|
82
|
+
function isBuiltInThemeName(value) {
|
|
83
|
+
return value === 'paper' || value === 'atlas' || value === 'gazette';
|
|
84
|
+
}
|
|
85
|
+
function isTemplateName(value) {
|
|
86
|
+
return value === 'document' || value === 'editorial';
|
|
87
|
+
}
|
|
88
|
+
function isNodeNotFound(error) {
|
|
89
|
+
return (typeof error === 'object' &&
|
|
90
|
+
error !== null &&
|
|
91
|
+
'code' in error &&
|
|
92
|
+
error.code === 'ENOENT');
|
|
93
|
+
}
|
|
94
|
+
async function pathExists(filePath) {
|
|
95
|
+
try {
|
|
96
|
+
await readFile(filePath, 'utf8');
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (isNodeNotFound(error)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function normalizeTopNav(value) {
|
|
107
|
+
if (!Array.isArray(value)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return value.flatMap((item) => {
|
|
111
|
+
if (typeof item === 'object' &&
|
|
112
|
+
item !== null &&
|
|
113
|
+
'label' in item &&
|
|
114
|
+
'href' in item &&
|
|
115
|
+
typeof item.label === 'string' &&
|
|
116
|
+
item.label !== '' &&
|
|
117
|
+
typeof item.href === 'string' &&
|
|
118
|
+
item.href !== '') {
|
|
119
|
+
return [{ label: item.label, href: item.href }];
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type TemplateName = 'document' | 'editorial';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SiteNavItem } from '../core/site-config.js';
|
|
2
|
+
import type { TemplateName } from './template-kind.js';
|
|
3
|
+
import { type BuiltInThemeName } from './theme.js';
|
|
4
|
+
export interface RenderDocumentOptions {
|
|
5
|
+
siteTitle: string;
|
|
6
|
+
siteDescription?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
body: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
date?: string;
|
|
11
|
+
showSummary?: boolean;
|
|
12
|
+
showDate?: boolean;
|
|
13
|
+
theme: BuiltInThemeName;
|
|
14
|
+
template: TemplateName;
|
|
15
|
+
topNav?: SiteNavItem[];
|
|
16
|
+
stylesheetContent?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function renderDocument(options: RenderDocumentOptions): string;
|
|
19
|
+
export declare function escapeHtml(value: string): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getBuiltInThemeStyles } from './theme.js';
|
|
2
|
+
export function renderDocument(options) {
|
|
3
|
+
const title = escapeHtml(options.title);
|
|
4
|
+
const siteTitle = escapeHtml(options.siteTitle);
|
|
5
|
+
const siteDescription = options.siteDescription
|
|
6
|
+
? escapeHtml(options.siteDescription)
|
|
7
|
+
: undefined;
|
|
8
|
+
const summaryMeta = options.summary
|
|
9
|
+
? `<meta name="description" content="${escapeHtml(options.summary)}">`
|
|
10
|
+
: '';
|
|
11
|
+
const stylesheetBlock = `<style>${getBuiltInThemeStyles(options.theme)}${options.stylesheetContent ? `\n${options.stylesheetContent}` : ''}</style>`;
|
|
12
|
+
const navBlock = options.topNav && options.topNav.length > 0
|
|
13
|
+
? `<nav class="site-nav"><ul>${options.topNav
|
|
14
|
+
.map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
|
|
15
|
+
.join('')}</ul></nav>`
|
|
16
|
+
: '';
|
|
17
|
+
const siteDescriptionBlock = siteDescription
|
|
18
|
+
? `<span>${siteDescription}</span>`
|
|
19
|
+
: '';
|
|
20
|
+
const articleBody = options.template === 'editorial'
|
|
21
|
+
? renderEditorialBody(options)
|
|
22
|
+
: options.body;
|
|
23
|
+
return [
|
|
24
|
+
'<!doctype html>',
|
|
25
|
+
'<html lang="en">',
|
|
26
|
+
'<head>',
|
|
27
|
+
'<meta charset="utf-8">',
|
|
28
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
29
|
+
`<title>${title} | ${siteTitle}</title>`,
|
|
30
|
+
summaryMeta,
|
|
31
|
+
stylesheetBlock,
|
|
32
|
+
'</head>',
|
|
33
|
+
`<body data-theme="${options.theme}" data-template="${options.template}">`,
|
|
34
|
+
`<header class="site-header"><div class="site-header__inner"><div class="site-header__brand"><p class="site-header__title"><a href="/">${siteTitle}</a></p>${siteDescriptionBlock}</div>${navBlock}</div></header>`,
|
|
35
|
+
'<main>',
|
|
36
|
+
`<article>${articleBody}</article>`,
|
|
37
|
+
'</main>',
|
|
38
|
+
'</body>',
|
|
39
|
+
'</html>',
|
|
40
|
+
].join('');
|
|
41
|
+
}
|
|
42
|
+
function renderEditorialBody(options) {
|
|
43
|
+
const heroLines = [
|
|
44
|
+
'<div class="page-intro">',
|
|
45
|
+
`<p class="page-intro__eyebrow">${escapeHtml(options.siteTitle)}</p>`,
|
|
46
|
+
`<h1 class="page-intro__title">${escapeHtml(options.title)}</h1>`,
|
|
47
|
+
];
|
|
48
|
+
if (options.showSummary !== false && options.summary) {
|
|
49
|
+
heroLines.push(`<p class="page-intro__summary">${escapeHtml(options.summary)}</p>`);
|
|
50
|
+
}
|
|
51
|
+
if (options.showDate !== false && options.date) {
|
|
52
|
+
heroLines.push(`<p class="page-intro__meta">${escapeHtml(options.date)}</p>`);
|
|
53
|
+
}
|
|
54
|
+
heroLines.push('</div>');
|
|
55
|
+
return `${heroLines.join('')}<div class="page-body">${stripLeadingH1(options.body)}</div>`;
|
|
56
|
+
}
|
|
57
|
+
function stripLeadingH1(html) {
|
|
58
|
+
return html.replace(/^\s*<h1>.*?<\/h1>\s*/s, '');
|
|
59
|
+
}
|
|
60
|
+
export function escapeHtml(value) {
|
|
61
|
+
return value
|
|
62
|
+
.replaceAll('&', '&')
|
|
63
|
+
.replaceAll('<', '<')
|
|
64
|
+
.replaceAll('>', '>')
|
|
65
|
+
.replaceAll('"', '"')
|
|
66
|
+
.replaceAll("'", ''');
|
|
67
|
+
}
|