memd-cli 3.2.1 → 3.4.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/CLAUDE.md +4 -2
- package/main.js +41 -12
- package/package.json +1 -1
- package/render-shared.js +9 -1
- package/test/memd.test.js +4 -4
- package/.claude/settings.local.json +0 -28
package/CLAUDE.md
CHANGED
|
@@ -41,7 +41,8 @@ node main.js test/test1.md
|
|
|
41
41
|
node main.js test/test2.md
|
|
42
42
|
node main.js test/complex.md
|
|
43
43
|
node main.js --html test/test1.md # HTML output to stdout
|
|
44
|
-
node main.js serve
|
|
44
|
+
node main.js serve test --port 3000 # HTTP serve mode (directory)
|
|
45
|
+
node main.js serve test/test1.md # HTTP serve mode (single file)
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
## Key CLI flags
|
|
@@ -70,4 +71,5 @@ nord-light, zinc-dark, zinc-light
|
|
|
70
71
|
- `render-shared.js` converts Mermaid fenced blocks to SVG via `@ktrysmt/beautiful-mermaid`, then renders full HTML with `marked`
|
|
71
72
|
- `render-worker.js` runs `renderToHTML` in a worker thread for non-blocking serve mode
|
|
72
73
|
- `render-utils.js` provides theme color resolution and HTML escaping (shared by main.js and render-shared.js)
|
|
73
|
-
- Serve mode supports: directory listing, ETag/304 caching, gzip, static file serving (images/css), sidebar navigation, `--watch` with SSE live reload, CSP nonce
|
|
74
|
+
- Serve mode supports: directory or single-file serving, directory listing, ETag/304 caching, gzip, static file serving (images/css), sidebar navigation, `--watch` with SSE live reload, CSP nonce
|
|
75
|
+
- Single-file mode (`serve foo.md`): sets baseDir to the file's parent directory, root `/` redirects to `/<filename>`
|
package/main.js
CHANGED
|
@@ -622,10 +622,12 @@ async function main() {
|
|
|
622
622
|
|
|
623
623
|
if (options.html) {
|
|
624
624
|
// 3a. HTML path
|
|
625
|
-
const { renderToHTML, MERMAID_MODAL_SCRIPT } = await import('./render-shared.js');
|
|
625
|
+
const { renderToHTML, MERMAID_MODAL_SCRIPT, WIDTH_TOGGLE_SCRIPT } = await import('./render-shared.js');
|
|
626
626
|
const combined = markdownParts.join('\n\n');
|
|
627
627
|
let html = renderToHTML(combined, diagramColors);
|
|
628
|
-
|
|
628
|
+
let inlineScripts = WIDTH_TOGGLE_SCRIPT;
|
|
629
|
+
if (html.includes('mermaid-diagram')) inlineScripts += MERMAID_MODAL_SCRIPT;
|
|
630
|
+
html = html.replace('<!--memd:scripts-->', `<script>${inlineScripts}</script>`);
|
|
629
631
|
process.stdout.write(html);
|
|
630
632
|
} else {
|
|
631
633
|
// 3b. Terminal path
|
|
@@ -707,13 +709,13 @@ async function main() {
|
|
|
707
709
|
program
|
|
708
710
|
.command('serve')
|
|
709
711
|
.description('Start HTTP server to serve .md files as HTML')
|
|
710
|
-
.
|
|
712
|
+
.argument('[path]', 'directory or .md file to serve', '.')
|
|
711
713
|
.option('-p, --port <number>', 'port number (0-65535)', Number, 8888)
|
|
712
714
|
.option('--host <string>', 'host to bind', '127.0.0.1')
|
|
713
715
|
.option('--workers <number>', 'number of render workers (default: min(cpus-1, 4))', Number)
|
|
714
716
|
.option('--watch', 'watch for file changes and live-reload')
|
|
715
717
|
.option('--theme <name>', `color theme (env: MEMD_THEME)\n${THEME_NAMES.join(', ')}`, process.env.MEMD_THEME || 'nord')
|
|
716
|
-
.action(async (options) => {
|
|
718
|
+
.action(async (servePath, options) => {
|
|
717
719
|
if (!(options.theme in THEME_MAP)) {
|
|
718
720
|
const names = Object.keys(THEME_MAP).join(', ');
|
|
719
721
|
console.error(`Unknown theme: ${options.theme}\nAvailable themes: ${names}`);
|
|
@@ -732,14 +734,26 @@ async function main() {
|
|
|
732
734
|
}
|
|
733
735
|
|
|
734
736
|
let baseDir;
|
|
737
|
+
let singleFile = null; // basename of .md file in single-file mode
|
|
738
|
+
let resolvedServePath;
|
|
735
739
|
try {
|
|
736
|
-
|
|
740
|
+
resolvedServePath = fs.realpathSync(path.resolve(servePath));
|
|
737
741
|
} catch {
|
|
738
|
-
console.error(`
|
|
742
|
+
console.error(`Path not found: ${servePath}`);
|
|
739
743
|
process.exit(1);
|
|
740
744
|
}
|
|
741
|
-
|
|
742
|
-
|
|
745
|
+
const serveStat = fs.statSync(resolvedServePath);
|
|
746
|
+
if (serveStat.isFile()) {
|
|
747
|
+
if (!resolvedServePath.endsWith('.md')) {
|
|
748
|
+
console.error(`Not a .md file: ${servePath}`);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
singleFile = path.basename(resolvedServePath);
|
|
752
|
+
baseDir = path.dirname(resolvedServePath);
|
|
753
|
+
} else if (serveStat.isDirectory()) {
|
|
754
|
+
baseDir = resolvedServePath;
|
|
755
|
+
} else {
|
|
756
|
+
console.error(`Not a file or directory: ${servePath}`);
|
|
743
757
|
process.exit(1);
|
|
744
758
|
}
|
|
745
759
|
if (baseDir === '/') {
|
|
@@ -751,7 +765,7 @@ async function main() {
|
|
|
751
765
|
console.error('Invalid --workers: must be a positive integer');
|
|
752
766
|
process.exit(1);
|
|
753
767
|
}
|
|
754
|
-
const { MERMAID_MODAL_SCRIPT: mermaidModalScript } = await import('./render-shared.js');
|
|
768
|
+
const { MERMAID_MODAL_SCRIPT: mermaidModalScript, WIDTH_TOGGLE_SCRIPT: widthToggleScript } = await import('./render-shared.js');
|
|
755
769
|
const poolSize = options.workers ?? Math.min(Math.max(1, os.cpus().length - 1), 4);
|
|
756
770
|
const workerPath = new URL('./render-worker.js', import.meta.url);
|
|
757
771
|
const pool = createRenderPool(workerPath, poolSize, {
|
|
@@ -861,7 +875,10 @@ async function main() {
|
|
|
861
875
|
.memd-sidebar a { color: ${t.accent}; text-decoration: none; display: block; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.9rem; }
|
|
862
876
|
.memd-sidebar a:hover { background: color-mix(in srgb, ${t.fg} 5%, ${t.bg}); }
|
|
863
877
|
.memd-sidebar a[aria-current="page"] { background: color-mix(in srgb, ${t.accent} 12%, ${t.bg}); }
|
|
864
|
-
.memd-content { max-width:
|
|
878
|
+
.memd-content { max-width: 70%; padding: 2rem 1rem; margin: 0 auto; }
|
|
879
|
+
@media (max-width: 1024px) { .memd-content { max-width: 85%; } }
|
|
880
|
+
@media (max-width: 768px) { .memd-content { max-width: 100%; } }
|
|
881
|
+
body.memd-full-width .memd-content { max-width: none; margin: 0; }
|
|
865
882
|
body:has(.memd-layout) { max-width: none; margin: 0; padding: 0; }
|
|
866
883
|
.memd-hamburger { display: none; position: fixed; top: 0.5rem; left: 0.5rem; z-index: 10; background: color-mix(in srgb, ${t.fg} 8%, ${t.bg}); border: 1px solid ${t.line}; color: ${t.fg}; padding: 0.3rem 0.5rem; cursor: pointer; border-radius: 4px; font-size: 1.2rem; }
|
|
867
884
|
@media (max-width: 768px) {
|
|
@@ -884,7 +901,7 @@ body:has(.memd-layout) { max-width: none; margin: 0; padding: 0; }
|
|
|
884
901
|
res.end();
|
|
885
902
|
return;
|
|
886
903
|
}
|
|
887
|
-
let scripts =
|
|
904
|
+
let scripts = widthToggleScript;
|
|
888
905
|
if (options.watch) {
|
|
889
906
|
scripts += 'new EventSource("/_memd/events").onmessage=function(){location.reload()};';
|
|
890
907
|
}
|
|
@@ -1069,6 +1086,14 @@ body:has(.memd-layout) { max-width: none; margin: 0; padding: 0; }
|
|
|
1069
1086
|
return;
|
|
1070
1087
|
}
|
|
1071
1088
|
|
|
1089
|
+
// Single-file mode: redirect root to the served file
|
|
1090
|
+
if (singleFile && urlPath === '/') {
|
|
1091
|
+
const target = '/' + encodeURIComponent(singleFile) + parsedUrl.search;
|
|
1092
|
+
res.writeHead(302, { Location: target });
|
|
1093
|
+
res.end();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1072
1097
|
if (isDotPath(urlPath)) {
|
|
1073
1098
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
1074
1099
|
res.end('Forbidden');
|
|
@@ -1277,7 +1302,11 @@ body:has(.memd-layout) { max-width: none; margin: 0; padding: 0; }
|
|
|
1277
1302
|
let displayHost = options.host === '0.0.0.0' || options.host === '::' ? 'localhost' : options.host;
|
|
1278
1303
|
if (displayHost.includes(':')) displayHost = `[${displayHost}]`;
|
|
1279
1304
|
console.log(`memd serve`);
|
|
1280
|
-
|
|
1305
|
+
if (singleFile) {
|
|
1306
|
+
console.log(` File: ${path.join(baseDir, singleFile)}`);
|
|
1307
|
+
} else {
|
|
1308
|
+
console.log(` Directory: ${baseDir}`);
|
|
1309
|
+
}
|
|
1281
1310
|
console.log(` Theme: ${options.theme}`);
|
|
1282
1311
|
if (options.watch) console.log(' Watch: enabled');
|
|
1283
1312
|
console.log(` URL: http://${displayHost}:${addr.port}/`);
|
package/package.json
CHANGED
package/render-shared.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Marked } from 'marked';
|
|
|
3
3
|
import { renderMermaidSVG } from '@ktrysmt/beautiful-mermaid';
|
|
4
4
|
import { escapeHtml, resolveThemeColors } from './render-utils.js';
|
|
5
5
|
|
|
6
|
+
export const WIDTH_TOGGLE_SCRIPT = "(function(){var b=document.body,btn=document.querySelector('.memd-width-toggle');if(!btn)return;var sb=document.querySelector('.memd-sidebar');if(sb){sb.insertBefore(btn,sb.firstChild);btn.style.position='static';btn.style.margin='0 0 0.5rem 0';btn.style.width='100%'}function u(){btn.textContent=b.classList.contains('memd-full-width')?'Full':'Smart'}if(localStorage.getItem('memd-width')==='full')b.classList.add('memd-full-width');u();btn.onclick=function(){b.classList.toggle('memd-full-width');localStorage.setItem('memd-width',b.classList.contains('memd-full-width')?'full':'smart');u()}})();";
|
|
7
|
+
|
|
6
8
|
export const MERMAID_MODAL_SCRIPT = [
|
|
7
9
|
"document.addEventListener('click',function(e){var d=e.target.closest('.mermaid-diagram');if(d){var o=document.createElement('div');o.className='mermaid-modal';o.innerHTML=d.querySelector('svg').outerHTML;o.onclick=function(){o.remove()};document.body.appendChild(o)}});",
|
|
8
10
|
"document.addEventListener('keydown',function(e){if(e.key==='Escape'){var m=document.querySelector('.mermaid-modal');if(m)m.remove()}});",
|
|
@@ -66,7 +68,12 @@ export function renderToHTML(markdown, diagramColors) {
|
|
|
66
68
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
67
69
|
${title ? `<title>${escapeHtml(title)}</title>` : ''}
|
|
68
70
|
<style>
|
|
69
|
-
body { background: ${t.bg}; color: ${t.fg}; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; max-width:
|
|
71
|
+
body { background: ${t.bg}; color: ${t.fg}; font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; max-width: 70%; margin: 0 auto; padding: 2rem 1rem; }
|
|
72
|
+
@media (max-width: 1024px) { body { max-width: 85%; } }
|
|
73
|
+
@media (max-width: 768px) { body { max-width: 100%; } }
|
|
74
|
+
body.memd-full-width { max-width: none; margin: 0; }
|
|
75
|
+
.memd-width-toggle { position: fixed; top: 0.5rem; left: 0.5rem; z-index: 11; background: color-mix(in srgb, ${t.fg} 8%, ${t.bg}); border: 1px solid ${t.line}; color: ${t.muted}; padding: 0.2rem 0.6rem; cursor: pointer; border-radius: 4px; font-size: 0.75rem; }
|
|
76
|
+
.memd-width-toggle:hover { background: color-mix(in srgb, ${t.fg} 15%, ${t.bg}); }
|
|
70
77
|
a { color: ${t.accent}; }
|
|
71
78
|
hr { border-color: ${t.line}; }
|
|
72
79
|
blockquote { border-left: 3px solid ${t.line}; color: ${t.muted}; padding-left: 1rem; }
|
|
@@ -85,6 +92,7 @@ th { background: color-mix(in srgb, ${t.fg} 5%, ${t.bg}); }
|
|
|
85
92
|
<!--memd:head-->
|
|
86
93
|
</head>
|
|
87
94
|
<body>
|
|
95
|
+
<button class="memd-width-toggle" aria-label="Toggle width"></button>
|
|
88
96
|
<!--memd:content-->
|
|
89
97
|
${body.trimEnd()}
|
|
90
98
|
<!--/memd:content-->
|
package/test/memd.test.js
CHANGED
|
@@ -26,7 +26,7 @@ function runSync(args) {
|
|
|
26
26
|
describe('memd CLI', () => {
|
|
27
27
|
it.concurrent('--version', async () => {
|
|
28
28
|
const output = await run(['-v'])
|
|
29
|
-
expect(output).toContain('3.
|
|
29
|
+
expect(output).toContain('3.4.0')
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
it.concurrent('--help', async () => {
|
|
@@ -416,7 +416,7 @@ describe('memd serve', () => {
|
|
|
416
416
|
let serverProcess
|
|
417
417
|
|
|
418
418
|
beforeAll(async () => {
|
|
419
|
-
serverProcess = spawn('node', [MAIN, 'serve', '--port', String(PORT), '--
|
|
419
|
+
serverProcess = spawn('node', [MAIN, 'serve', __dirname, '--port', String(PORT), '--host', '127.0.0.1'], {
|
|
420
420
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
421
421
|
})
|
|
422
422
|
await new Promise((resolve, reject) => {
|
|
@@ -444,7 +444,7 @@ describe('memd serve', () => {
|
|
|
444
444
|
|
|
445
445
|
it('serve --help shows options', () => {
|
|
446
446
|
const output = runSync(['serve', '--help'])
|
|
447
|
-
expect(output).toContain('
|
|
447
|
+
expect(output).toContain('[path]')
|
|
448
448
|
expect(output).toContain('--port')
|
|
449
449
|
expect(output).toContain('--host')
|
|
450
450
|
expect(output).toContain('--watch')
|
|
@@ -655,7 +655,7 @@ describe('memd serve --watch', () => {
|
|
|
655
655
|
let watchProcess
|
|
656
656
|
|
|
657
657
|
beforeAll(async () => {
|
|
658
|
-
watchProcess = spawn('node', [MAIN, 'serve', '--port', String(WATCH_PORT), '--
|
|
658
|
+
watchProcess = spawn('node', [MAIN, 'serve', __dirname, '--port', String(WATCH_PORT), '--host', '127.0.0.1', '--watch'], {
|
|
659
659
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
660
660
|
})
|
|
661
661
|
await new Promise((resolve, reject) => {
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(echo:*)",
|
|
5
|
-
"Bash(FORCE_COLOR=1 echo:*)",
|
|
6
|
-
"Bash(script:*)",
|
|
7
|
-
"Bash(FORCE_COLOR=1 node:*)",
|
|
8
|
-
"Bash(node -e \"import\\('beautiful-mermaid/src/ascii/ansi.ts'\\).then\\(m=>console.log\\(Object.keys\\(m\\)\\)\\).catch\\(e=>console.error\\('ERROR:',e.message\\)\\)\" 2>&1 | head -5)",
|
|
9
|
-
"Bash(node main.js test/test-highlight.md --no-pager --theme catppuccin-mocha 2>&1 | head -3 | xxd | head -10)",
|
|
10
|
-
"Bash(FORCE_COLOR=1 node main.js test/test-highlight.md --no-pager --theme catppuccin-mocha 2>&1 | head -3 | xxd | head -5)",
|
|
11
|
-
"Bash(FORCE_COLOR=1 node main.js test/test-highlight.md --no-pager --theme zinc-dark 2>&1 | head -3 | xxd | head -5)",
|
|
12
|
-
"mcp__grep-github__searchGitHub",
|
|
13
|
-
"Bash(npx npm-check-updates:*)",
|
|
14
|
-
"Bash(MEMD_THEME= pnpm test 2>&1 | tail -20)",
|
|
15
|
-
"Bash(MEMD_THEME= npx vitest run 2>&1)",
|
|
16
|
-
"Bash(node:*)",
|
|
17
|
-
"Bash(printf:*)",
|
|
18
|
-
"WebFetch(domain:registry.npmjs.org)",
|
|
19
|
-
"WebFetch(domain:api.github.com)",
|
|
20
|
-
"Bash(mkdir:*)",
|
|
21
|
-
"Bash(# Restore original dist\ncp node_modules/@ktrysmt/beautiful-mermaid/dist/index.js.bak node_modules/@ktrysmt/beautiful-mermaid/dist/index.js && rm node_modules/@ktrysmt/beautiful-mermaid/dist/index.js.bak && rm -f debug-mermaid*.mjs bm-debug.mjs)",
|
|
22
|
-
"Bash(cp dist/index.js.bak dist/index.js)",
|
|
23
|
-
"Bash(rm -f dist/index.js.debug)",
|
|
24
|
-
"Bash(rm -f pnpm-lock.yaml)",
|
|
25
|
-
"Bash(rm -rf node_modules)"
|
|
26
|
-
]
|
|
27
|
-
}
|
|
28
|
-
}
|