memd-cli 1.5.1 → 2.0.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/settings.local.json +6 -1
- package/README.md +19 -5
- package/highlight-plan.md +710 -0
- package/main.js +332 -214
- package/package.json +6 -3
- package/poc-html.mjs +134 -0
- package/poc-output.html +460 -0
- package/svgplan.md +805 -0
- package/test/memd.test.js +199 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memd-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,15 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"beautiful-mermaid": "^1.1.2",
|
|
14
14
|
"chalk": "^5.6.2",
|
|
15
|
-
"cli-highlight": "^2.1.11",
|
|
16
15
|
"commander": "^14.0.3",
|
|
17
16
|
"marked": "^17.0.3",
|
|
18
|
-
"marked-terminal": "^7.3.0"
|
|
17
|
+
"marked-terminal": "^7.3.0",
|
|
18
|
+
"shiki": "^4.0.2"
|
|
19
19
|
},
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
22
|
"url": "https://github.com/ktrysmt/memd.git"
|
|
23
23
|
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
24
27
|
"license": "MIT",
|
|
25
28
|
"packageManager": "pnpm@10.15.1",
|
|
26
29
|
"devDependencies": {
|
package/poc-html.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PoC: Markdown + Mermaid -> HTML w/ inline SVG
|
|
3
|
+
// Tests: marker ID uniquification, theme CSS, mermaid-error styling
|
|
4
|
+
import { Marked } from 'marked';
|
|
5
|
+
import { renderMermaidSVG, THEMES } from 'beautiful-mermaid';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
|
|
8
|
+
const THEME_NAMES = Object.keys(THEMES);
|
|
9
|
+
const themeName = process.argv[2] || 'github-dark';
|
|
10
|
+
const inputFile = process.argv[3] || 'test/test3.md';
|
|
11
|
+
|
|
12
|
+
if (!(themeName in THEMES)) {
|
|
13
|
+
console.error(`Unknown theme: ${themeName}`);
|
|
14
|
+
console.error(`Available: ${THEME_NAMES.join(', ')}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const theme = THEMES[themeName];
|
|
19
|
+
console.log(`Theme: ${themeName}, Input: ${inputFile}`);
|
|
20
|
+
console.log(`Theme fields: ${JSON.stringify(theme)}`);
|
|
21
|
+
|
|
22
|
+
const testMarkdown = fs.readFileSync(inputFile, 'utf-8');
|
|
23
|
+
|
|
24
|
+
function escapeHtml(str) {
|
|
25
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Resolve optional DiagramColors fields with color mixing fallback
|
|
29
|
+
// Matches beautiful-mermaid's internal MIX ratios (theme.ts:64-87)
|
|
30
|
+
function mixHex(hex1, hex2, pct) {
|
|
31
|
+
const p = pct / 100;
|
|
32
|
+
const r1 = parseInt(hex1.slice(1, 3), 16), g1 = parseInt(hex1.slice(3, 5), 16), b1 = parseInt(hex1.slice(5, 7), 16);
|
|
33
|
+
const r2 = parseInt(hex2.slice(1, 3), 16), g2 = parseInt(hex2.slice(3, 5), 16), b2 = parseInt(hex2.slice(5, 7), 16);
|
|
34
|
+
const toHex = x => Math.round(x).toString(16).padStart(2, '0');
|
|
35
|
+
return `#${toHex(r1 * p + r2 * (1 - p))}${toHex(g1 * p + g2 * (1 - p))}${toHex(b1 * p + b2 * (1 - p))}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Resolve theme colors with fallback derivation for optional fields
|
|
39
|
+
function resolveThemeColors(colors) {
|
|
40
|
+
const line = colors.line ?? mixHex(colors.fg, colors.bg, 50);
|
|
41
|
+
const accent = colors.accent ?? mixHex(colors.fg, colors.bg, 85);
|
|
42
|
+
const muted = colors.muted ?? mixHex(colors.fg, colors.bg, 60);
|
|
43
|
+
const border = colors.border ?? mixHex(colors.fg, colors.bg, 20);
|
|
44
|
+
return { bg: colors.bg, fg: colors.fg, line, accent, muted, border };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function convertMermaidToSVG(markdown, diagramTheme) {
|
|
48
|
+
const mermaidRegex = /```mermaid\s+([\s\S]+?)```/g;
|
|
49
|
+
let svgIndex = 0;
|
|
50
|
+
return markdown.replace(mermaidRegex, (_, code) => {
|
|
51
|
+
try {
|
|
52
|
+
const prefix = `m${svgIndex++}`;
|
|
53
|
+
let svg = renderMermaidSVG(code.trim(), diagramTheme);
|
|
54
|
+
svg = svg.replace(/@import url\([^)]+\);\s*/g, '');
|
|
55
|
+
// Prefix all id="..." and url(#...) to avoid cross-SVG collisions
|
|
56
|
+
svg = svg.replace(/ id="([^"]+)"/g, ` id="${prefix}-$1"`);
|
|
57
|
+
svg = svg.replace(/url\(#([^)]+)\)/g, `url(#${prefix}-$1)`);
|
|
58
|
+
return svg;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return `<pre class="mermaid-error">${escapeHtml(e.message)}\n\n${escapeHtml(code.trim())}</pre>`;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderToHTML(markdown, diagramTheme) {
|
|
66
|
+
const t = resolveThemeColors(diagramTheme);
|
|
67
|
+
const processed = convertMermaidToSVG(markdown, diagramTheme);
|
|
68
|
+
const htmlMarked = new Marked();
|
|
69
|
+
const htmlBody = htmlMarked.parse(processed);
|
|
70
|
+
|
|
71
|
+
return `<!DOCTYPE html>
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="UTF-8">
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
76
|
+
<title>memd preview</title>
|
|
77
|
+
<style>
|
|
78
|
+
body {
|
|
79
|
+
max-width: 800px;
|
|
80
|
+
margin: 2rem auto;
|
|
81
|
+
padding: 0 1rem;
|
|
82
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
83
|
+
line-height: 1.6;
|
|
84
|
+
background: ${t.bg};
|
|
85
|
+
color: ${t.fg};
|
|
86
|
+
}
|
|
87
|
+
a { color: ${t.accent}; }
|
|
88
|
+
hr { border-color: ${t.line}; }
|
|
89
|
+
blockquote { border-left: 3px solid ${t.line}; padding-left: 1rem; color: ${t.muted}; }
|
|
90
|
+
svg { max-width: 100%; height: auto; }
|
|
91
|
+
pre { background: color-mix(in srgb, ${t.fg} 8%, ${t.bg}); padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
|
92
|
+
code { font-size: 0.9em; color: ${t.accent}; }
|
|
93
|
+
pre code { color: inherit; }
|
|
94
|
+
table { border-collapse: collapse; }
|
|
95
|
+
th, td { border: 1px solid ${t.line}; padding: 0.4rem 0.8rem; }
|
|
96
|
+
th { background: color-mix(in srgb, ${t.fg} 5%, ${t.bg}); }
|
|
97
|
+
.mermaid-error {
|
|
98
|
+
background: color-mix(in srgb, ${t.accent} 10%, ${t.bg});
|
|
99
|
+
border: 1px solid color-mix(in srgb, ${t.accent} 40%, ${t.bg});
|
|
100
|
+
color: ${t.fg};
|
|
101
|
+
padding: 1rem;
|
|
102
|
+
border-radius: 6px;
|
|
103
|
+
overflow-x: auto;
|
|
104
|
+
white-space: pre-wrap;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
${htmlBody}
|
|
110
|
+
</body>
|
|
111
|
+
</html>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const html = renderToHTML(testMarkdown, theme);
|
|
115
|
+
const outputFile = 'poc-output.html';
|
|
116
|
+
fs.writeFileSync(outputFile, html);
|
|
117
|
+
|
|
118
|
+
// Validation
|
|
119
|
+
const svgCount = (html.match(/<svg/g) || []).length;
|
|
120
|
+
const markerIds = [...html.matchAll(/ id="([^"]+)"/g)].map(m => m[1]);
|
|
121
|
+
const uniqueMarkerIds = new Set(markerIds);
|
|
122
|
+
const errorBlocks = (html.match(/mermaid-error/g) || []).length;
|
|
123
|
+
|
|
124
|
+
console.log(`Written to ${outputFile} (${html.length} bytes)`);
|
|
125
|
+
console.log(`SVGs: ${svgCount}`);
|
|
126
|
+
console.log(`Marker IDs (${markerIds.length}): ${markerIds.join(', ')}`);
|
|
127
|
+
console.log(`Unique: ${uniqueMarkerIds.size}, Duplicates: ${markerIds.length - uniqueMarkerIds.size}`);
|
|
128
|
+
console.log(`Mermaid errors: ${errorBlocks / 2}`); // each error has class + selector = 2 matches
|
|
129
|
+
if (markerIds.length !== uniqueMarkerIds.size) {
|
|
130
|
+
console.error('ERROR: Duplicate marker IDs detected!');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
} else {
|
|
133
|
+
console.log('OK: All marker IDs are unique');
|
|
134
|
+
}
|