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,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('&', '&amp;')
63
+ .replaceAll('<', '&lt;')
64
+ .replaceAll('>', '&gt;')
65
+ .replaceAll('"', '&quot;')
66
+ .replaceAll("'", '&#39;');
67
+ }
@@ -0,0 +1,2 @@
1
+ export type BuiltInThemeName = 'paper' | 'atlas' | 'gazette';
2
+ export declare function getBuiltInThemeStyles(theme: BuiltInThemeName): string;