mkdnsite 0.0.1

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/src/cli.ts ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bun
2
+ import { resolve } from 'node:path'
3
+ import { access } from 'node:fs/promises'
4
+ import { resolveConfig } from './config/defaults.ts'
5
+ import type { MkdnSiteConfig } from './config/schema.ts'
6
+ import { LocalAdapter } from './adapters/local.ts'
7
+ import { createHandler } from './handler.ts'
8
+
9
+ function parseArgs (args: string[]): Partial<MkdnSiteConfig> {
10
+ const result: Record<string, unknown> = {}
11
+
12
+ for (let i = 0; i < args.length; i++) {
13
+ const arg = args[i]
14
+
15
+ if (arg === '--port' || arg === '-p') {
16
+ result.server = { ...(result.server as object ?? {}), port: parseInt(args[++i], 10) }
17
+ } else if (arg === '--title') {
18
+ result.site = { ...(result.site as object ?? {}), title: args[++i] }
19
+ } else if (arg === '--url') {
20
+ result.site = { ...(result.site as object ?? {}), url: args[++i] }
21
+ } else if (arg === '--no-nav') {
22
+ result.theme = { ...(result.theme as object ?? {}), showNav: false }
23
+ } else if (arg === '--no-llms-txt') {
24
+ result.llmsTxt = { enabled: false }
25
+ } else if (arg === '--no-negotiate') {
26
+ result.negotiation = { enabled: false }
27
+ } else if (arg === '--no-client-js') {
28
+ result.client = { enabled: false, mermaid: false, copyButton: false, themeToggle: false, math: false, search: false }
29
+ } else if (arg === '--no-theme-toggle') {
30
+ result.client = { ...(result.client as object ?? {}), themeToggle: false }
31
+ } else if (arg === '--no-math') {
32
+ result.client = { ...(result.client as object ?? {}), math: false }
33
+ } else if (arg === '--color-scheme') {
34
+ result.theme = { ...(result.theme as object ?? {}), colorScheme: args[++i] }
35
+ } else if (arg === '--theme-mode') {
36
+ result.theme = { ...(result.theme as object ?? {}), mode: args[++i] }
37
+ } else if (arg === '--renderer') {
38
+ result.renderer = args[++i]
39
+ } else if (arg === '--static') {
40
+ result.staticDir = resolve(args[++i])
41
+ } else if (arg === '--help' || arg === '-h') {
42
+ printHelp()
43
+ process.exit(0)
44
+ } else if (arg === '--version' || arg === '-v') {
45
+ console.log('mkdnsite 0.0.1')
46
+ process.exit(0)
47
+ } else if (!arg.startsWith('-')) {
48
+ result.contentDir = resolve(arg)
49
+ }
50
+ }
51
+
52
+ return result as Partial<MkdnSiteConfig>
53
+ }
54
+
55
+ function printHelp (): void {
56
+ console.log(`
57
+ mkdnsite — Markdown for the web
58
+
59
+ Usage:
60
+ mkdnsite [directory] [options]
61
+
62
+ Arguments:
63
+ directory Path to markdown content (default: ./content)
64
+
65
+ Options:
66
+ -p, --port <n> Port to listen on (default: 3000)
67
+ --title <text> Site title
68
+ --url <url> Base URL for absolute links
69
+ --static <dir> Directory for static assets
70
+ --color-scheme <val> Color scheme: system (default), light, or dark
71
+ --theme-mode <mode> Theme mode: prose (default) or components
72
+ --no-nav Disable navigation sidebar
73
+ --no-llms-txt Disable /llms.txt generation
74
+ --no-negotiate Disable content negotiation
75
+ --renderer <engine> Renderer: portable (default) or bun-native (Bun only)
76
+ --no-client-js Disable client-side JavaScript (mermaid, copy, search, theme toggle)
77
+ --no-theme-toggle Disable light/dark theme toggle button
78
+ --no-math Disable KaTeX math rendering
79
+ -h, --help Show this help
80
+ -v, --version Show version
81
+
82
+ Content Negotiation:
83
+ Browsers get HTML: curl http://localhost:3000
84
+ AI agents get MD: curl -H "Accept: text/markdown" http://localhost:3000
85
+ Append .md to URL: curl http://localhost:3000/page.md
86
+ AI content index: curl http://localhost:3000/llms.txt
87
+
88
+ https://mkdn.site
89
+ `)
90
+ }
91
+
92
+ async function main (): Promise<void> {
93
+ const args = process.argv.slice(2)
94
+ const cliConfig = parseArgs(args)
95
+
96
+ // Try to load mkdnsite.config.ts from cwd
97
+ let fileConfig: Partial<MkdnSiteConfig> = {}
98
+ try {
99
+ const configPath = resolve('mkdnsite.config.ts')
100
+ await access(configPath)
101
+ const mod = await import(configPath)
102
+ fileConfig = mod.default ?? mod
103
+ } catch {
104
+ // No config file, that's fine
105
+ }
106
+
107
+ const merged: Partial<MkdnSiteConfig> = {
108
+ ...fileConfig,
109
+ ...cliConfig
110
+ }
111
+ if (fileConfig.site != null || cliConfig.site != null) {
112
+ const site: Partial<MkdnSiteConfig['site']> = { ...fileConfig.site, ...cliConfig.site }
113
+ merged.site = site as MkdnSiteConfig['site']
114
+ }
115
+ if (fileConfig.server != null || cliConfig.server != null) {
116
+ const server: Partial<MkdnSiteConfig['server']> = { ...fileConfig.server, ...cliConfig.server }
117
+ merged.server = server as MkdnSiteConfig['server']
118
+ }
119
+ if (fileConfig.theme != null || cliConfig.theme != null) {
120
+ const theme: Partial<MkdnSiteConfig['theme']> = { ...fileConfig.theme, ...cliConfig.theme }
121
+ merged.theme = theme as MkdnSiteConfig['theme']
122
+ }
123
+ if (fileConfig.client != null || cliConfig.client != null) {
124
+ const client: Partial<MkdnSiteConfig['client']> = { ...fileConfig.client, ...cliConfig.client }
125
+ merged.client = client as MkdnSiteConfig['client']
126
+ }
127
+ const config = resolveConfig(merged)
128
+
129
+ const adapter = new LocalAdapter()
130
+ const source = adapter.createContentSource(config)
131
+ const renderer = await adapter.createRenderer(config)
132
+ const handler = createHandler({ source, renderer, config })
133
+
134
+ await adapter.start(handler, config)
135
+ }
136
+
137
+ main().catch(err => {
138
+ console.error('Error starting mkdnsite:', err)
139
+ process.exit(1)
140
+ })
@@ -0,0 +1,106 @@
1
+ import type { ClientConfig } from '../config/schema.ts'
2
+
3
+ /**
4
+ * Generate client-side JavaScript for progressive enhancements.
5
+ * Returns a <script> tag string or empty string if all features are disabled.
6
+ */
7
+ export function CLIENT_SCRIPTS (client: ClientConfig): string {
8
+ if (!client.enabled) return ''
9
+
10
+ const scripts: string[] = []
11
+
12
+ if (client.themeToggle) {
13
+ scripts.push(THEME_TOGGLE_SCRIPT)
14
+ }
15
+
16
+ if (client.copyButton) {
17
+ scripts.push(COPY_BUTTON_SCRIPT)
18
+ }
19
+
20
+ if (client.mermaid) {
21
+ scripts.push(MERMAID_SCRIPT)
22
+ }
23
+
24
+ if (client.search) {
25
+ scripts.push(SEARCH_SCRIPT)
26
+ }
27
+
28
+ if (scripts.length === 0) return ''
29
+
30
+ return `<script>${scripts.join('\n')}</script>`
31
+ }
32
+
33
+ const THEME_TOGGLE_SCRIPT = `
34
+ (function(){
35
+ var btn = document.querySelector('.mkdn-theme-toggle');
36
+ if (!btn) return;
37
+ btn.addEventListener('click', function(e){
38
+ var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
39
+ var next = isDark ? 'light' : 'dark';
40
+ var rect = btn.getBoundingClientRect();
41
+ var x = rect.left + rect.width / 2;
42
+ var y = rect.top + rect.height / 2;
43
+ document.documentElement.style.setProperty('--mkdn-toggle-x', x + 'px');
44
+ document.documentElement.style.setProperty('--mkdn-toggle-y', y + 'px');
45
+ function apply(){
46
+ document.documentElement.setAttribute('data-theme', next);
47
+ localStorage.setItem('mkdn-theme', next);
48
+ }
49
+ if (document.startViewTransition) {
50
+ document.startViewTransition(apply);
51
+ } else {
52
+ apply();
53
+ }
54
+ });
55
+ })();
56
+ `.trim()
57
+
58
+ const COPY_BUTTON_SCRIPT = `
59
+ (function(){
60
+ document.querySelectorAll('.mkdn-code-block, .mkdn-prose pre').forEach(function(block){
61
+ var code = block.querySelector('code');
62
+ if(!code) return;
63
+ if(code.classList.contains('language-mermaid')) return;
64
+ var btn = document.createElement('button');
65
+ btn.className = 'mkdn-copy-btn';
66
+ btn.textContent = 'Copy';
67
+ btn.addEventListener('click', function(){
68
+ navigator.clipboard.writeText(code.textContent||'').then(function(){
69
+ btn.textContent = 'Copied!';
70
+ setTimeout(function(){ btn.textContent = 'Copy'; }, 2000);
71
+ });
72
+ });
73
+ block.style.position = 'relative';
74
+ block.appendChild(btn);
75
+ });
76
+ })();
77
+ `.trim()
78
+
79
+ const MERMAID_SCRIPT = `
80
+ (function(){
81
+ var mermaidBlocks = document.querySelectorAll('code.language-mermaid');
82
+ if(mermaidBlocks.length === 0) return;
83
+ var s = document.createElement('script');
84
+ s.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
85
+ s.onload = function(){
86
+ mermaid.initialize({ startOnLoad: false, theme: 'default' });
87
+ mermaidBlocks.forEach(function(block){
88
+ var pre = block.parentElement;
89
+ var container = document.createElement('div');
90
+ container.className = 'mkdn-mermaid';
91
+ var id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
92
+ mermaid.render(id, block.textContent||'').then(function(result){
93
+ container.innerHTML = result.svg;
94
+ pre.parentElement.replaceChild(container, pre);
95
+ });
96
+ });
97
+ };
98
+ document.head.appendChild(s);
99
+ })();
100
+ `.trim()
101
+
102
+ const SEARCH_SCRIPT = `
103
+ /* Search: placeholder for client-side search functionality.
104
+ Will be implemented with a pre-built content index served at /search-index.json.
105
+ For MVP, this is a no-op that can be activated once the index endpoint exists. */
106
+ `.trim()
@@ -0,0 +1,68 @@
1
+ import type { MkdnSiteConfig } from './schema.ts'
2
+
3
+ export const DEFAULT_CONFIG: MkdnSiteConfig = {
4
+ contentDir: './content',
5
+ site: {
6
+ title: 'mkdnsite',
7
+ lang: 'en'
8
+ },
9
+ server: {
10
+ port: 3000,
11
+ hostname: '0.0.0.0'
12
+ },
13
+ theme: {
14
+ mode: 'prose',
15
+ showNav: true,
16
+ showToc: true,
17
+ colorScheme: 'system',
18
+ syntaxTheme: 'github-light',
19
+ syntaxThemeDark: 'github-dark'
20
+ },
21
+ negotiation: {
22
+ enabled: true,
23
+ includeTokenCount: true,
24
+ contentSignals: {
25
+ aiTrain: 'yes',
26
+ search: 'yes',
27
+ aiInput: 'yes'
28
+ }
29
+ },
30
+ llmsTxt: {
31
+ enabled: true
32
+ },
33
+ client: {
34
+ enabled: true,
35
+ mermaid: true,
36
+ copyButton: true,
37
+ themeToggle: true,
38
+ math: true,
39
+ search: true
40
+ },
41
+ renderer: 'portable'
42
+ }
43
+
44
+ /**
45
+ * Deep merge user config with defaults.
46
+ * User values take precedence at every nesting level.
47
+ */
48
+ export function resolveConfig (
49
+ userConfig: Partial<MkdnSiteConfig>
50
+ ): MkdnSiteConfig {
51
+ return {
52
+ ...DEFAULT_CONFIG,
53
+ ...userConfig,
54
+ site: { ...DEFAULT_CONFIG.site, ...userConfig.site },
55
+ server: { ...DEFAULT_CONFIG.server, ...userConfig.server },
56
+ theme: { ...DEFAULT_CONFIG.theme, ...userConfig.theme },
57
+ negotiation: {
58
+ ...DEFAULT_CONFIG.negotiation,
59
+ ...userConfig.negotiation,
60
+ contentSignals: {
61
+ ...DEFAULT_CONFIG.negotiation.contentSignals,
62
+ ...userConfig.negotiation?.contentSignals
63
+ }
64
+ },
65
+ llmsTxt: { ...DEFAULT_CONFIG.llmsTxt, ...userConfig.llmsTxt },
66
+ client: { ...DEFAULT_CONFIG.client, ...userConfig.client }
67
+ }
68
+ }
@@ -0,0 +1,192 @@
1
+ import type { ComponentType, ReactNode } from 'react'
2
+
3
+ /**
4
+ * Top-level mkdnsite configuration.
5
+ *
6
+ * Can be defined in mkdnsite.config.ts at the project root,
7
+ * or passed programmatically when creating a handler.
8
+ */
9
+ export interface MkdnSiteConfig {
10
+ /** Directory containing .md files (default: ./content) */
11
+ contentDir: string
12
+
13
+ /** Site metadata */
14
+ site: SiteConfig
15
+
16
+ /** Server options (local dev / self-hosted only) */
17
+ server: ServerConfig
18
+
19
+ /** Theme and rendering configuration */
20
+ theme: ThemeConfig
21
+
22
+ /** Content negotiation options */
23
+ negotiation: NegotiationConfig
24
+
25
+ /** Auto-generate /llms.txt */
26
+ llmsTxt: LlmsTxtConfig
27
+
28
+ /** Client-side enhancement modules */
29
+ client: ClientConfig
30
+
31
+ /** Markdown renderer engine (default: 'portable') */
32
+ renderer: RendererEngine
33
+
34
+ /** Static files directory for images, videos, etc. */
35
+ staticDir?: string
36
+ }
37
+
38
+ export interface SiteConfig {
39
+ title: string
40
+ description?: string
41
+ url?: string
42
+ lang?: string
43
+ }
44
+
45
+ export interface ServerConfig {
46
+ port: number
47
+ hostname: string
48
+ }
49
+
50
+ export interface ThemeConfig {
51
+ /**
52
+ * Rendering mode for markdown content.
53
+ * - 'prose': Uses @tailwindcss/typography prose classes (default)
54
+ * - 'components': Full custom React component overrides per element
55
+ */
56
+ mode: 'prose' | 'components'
57
+
58
+ /** Custom React components to override default element rendering */
59
+ components?: ComponentOverrides
60
+
61
+ /** Custom CSS file path or URL to use instead of default theme */
62
+ customCss?: string
63
+
64
+ /** Show navigation sidebar */
65
+ showNav: boolean
66
+
67
+ /** Show table of contents per page */
68
+ showToc: boolean
69
+
70
+ /** Edit URL template (e.g. https://github.com/org/repo/edit/main/{path}) */
71
+ editUrl?: string
72
+
73
+ /** Color scheme: 'system' (default), 'light', or 'dark' */
74
+ colorScheme: 'system' | 'light' | 'dark'
75
+
76
+ /** Syntax highlighting theme for Shiki */
77
+ syntaxTheme: string
78
+
79
+ /** Dark mode syntax highlighting theme */
80
+ syntaxThemeDark?: string
81
+ }
82
+
83
+ export interface NegotiationConfig {
84
+ /** Enable serving raw markdown via Accept: text/markdown (default: true) */
85
+ enabled: boolean
86
+
87
+ /** Include x-markdown-tokens header (default: true) */
88
+ includeTokenCount: boolean
89
+
90
+ /** Content-Signal header values */
91
+ contentSignals: ContentSignals
92
+ }
93
+
94
+ export interface ContentSignals {
95
+ aiTrain: 'yes' | 'no'
96
+ search: 'yes' | 'no'
97
+ aiInput: 'yes' | 'no'
98
+ }
99
+
100
+ export interface LlmsTxtConfig {
101
+ enabled: boolean
102
+ description?: string
103
+ sections?: Record<string, string>
104
+ }
105
+
106
+ export interface ClientConfig {
107
+ /**
108
+ * Enable client-side JavaScript enhancements.
109
+ * When false, only static HTML/CSS is served (performance mode).
110
+ * Default: true
111
+ */
112
+ enabled: boolean
113
+
114
+ /** Enable Mermaid diagram rendering (default: true when client enabled) */
115
+ mermaid: boolean
116
+
117
+ /** Enable copy-to-clipboard on code blocks (default: true when client enabled) */
118
+ copyButton: boolean
119
+
120
+ /** Enable light/dark theme toggle button (default: true when client enabled) */
121
+ themeToggle: boolean
122
+
123
+ /** Enable KaTeX math rendering (default: true when client enabled) */
124
+ math: boolean
125
+
126
+ /** Enable client-side search (default: true when client enabled) */
127
+ search: boolean
128
+ }
129
+
130
+ /**
131
+ * Markdown renderer engine selection.
132
+ * - 'portable': react-markdown + remark/rehype (works everywhere)
133
+ * - 'bun-native': Bun.markdown.react() (Bun only, faster)
134
+ */
135
+ export type RendererEngine = 'portable' | 'bun-native'
136
+
137
+ /**
138
+ * React component overrides for markdown elements.
139
+ * Each key maps to an HTML element produced by the markdown renderer.
140
+ */
141
+ export interface ComponentOverrides {
142
+ h1?: ComponentType<HeadingProps>
143
+ h2?: ComponentType<HeadingProps>
144
+ h3?: ComponentType<HeadingProps>
145
+ h4?: ComponentType<HeadingProps>
146
+ h5?: ComponentType<HeadingProps>
147
+ h6?: ComponentType<HeadingProps>
148
+ p?: ComponentType<{ children?: ReactNode }>
149
+ a?: ComponentType<LinkProps>
150
+ img?: ComponentType<ImageProps>
151
+ pre?: ComponentType<CodeBlockProps>
152
+ code?: ComponentType<InlineCodeProps>
153
+ blockquote?: ComponentType<{ children?: ReactNode }>
154
+ table?: ComponentType<{ children?: ReactNode }>
155
+ thead?: ComponentType<{ children?: ReactNode }>
156
+ tbody?: ComponentType<{ children?: ReactNode }>
157
+ tr?: ComponentType<{ children?: ReactNode }>
158
+ th?: ComponentType<{ children?: ReactNode, align?: string }>
159
+ td?: ComponentType<{ children?: ReactNode, align?: string }>
160
+ ul?: ComponentType<{ children?: ReactNode }>
161
+ ol?: ComponentType<{ children?: ReactNode, start?: number }>
162
+ li?: ComponentType<{ children?: ReactNode, checked?: boolean }>
163
+ hr?: ComponentType<Record<string, never>>
164
+ }
165
+
166
+ export interface HeadingProps {
167
+ children?: ReactNode
168
+ id?: string
169
+ level: number
170
+ }
171
+
172
+ export interface LinkProps {
173
+ children?: ReactNode
174
+ href?: string
175
+ title?: string
176
+ }
177
+
178
+ export interface ImageProps {
179
+ src?: string
180
+ alt?: string
181
+ title?: string
182
+ }
183
+
184
+ export interface CodeBlockProps {
185
+ children?: ReactNode
186
+ language?: string
187
+ raw?: string
188
+ }
189
+
190
+ export interface InlineCodeProps {
191
+ children?: ReactNode
192
+ }