memd-cli 1.2.0 → 1.3.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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(echo:*)",
5
+ "Bash(FORCE_COLOR=1 echo:*)",
6
+ "Bash(script:*)",
7
+ "Bash(FORCE_COLOR=1 node:*)"
8
+ ]
9
+ }
10
+ }
package/README.md CHANGED
@@ -22,10 +22,13 @@ Arguments:
22
22
  Options:
23
23
  -v, --version output the version number
24
24
  --no-pager disable pager (less)
25
+ --no-mouse disable mouse scroll in pager
25
26
  --no-color disable colored output
26
27
  --no-highlight disable syntax highlighting
27
28
  --width <number> terminal width override
28
29
  --ascii use pure ASCII mode for diagrams (default: unicode)
30
+ --theme <name> syntax highlight theme (default, monokai, dracula,
31
+ github-dark, solarized, nord) (default: "default")
29
32
  -h, --help display help for command
30
33
  ```
31
34
 
@@ -306,7 +309,13 @@ $ echo '# Hello\n\n```mermaid\nflowchart LR\n A --> B\n```' | memd
306
309
  npm remove -g memd-cli
307
310
  ```
308
311
 
309
- ## Debug
312
+ ## Development
313
+
314
+ ```bash
315
+ node main.js test/test1.md
316
+ ```
317
+
318
+ ## Use specific version
310
319
 
311
320
  ```bash
312
321
  # tag
package/main.js CHANGED
@@ -3,7 +3,8 @@
3
3
  import { marked } from 'marked';
4
4
  import { markedTerminal } from 'marked-terminal';
5
5
  import { renderMermaidAscii } from 'beautiful-mermaid';
6
- import { highlight, supportsLanguage } from 'cli-highlight';
6
+ import { highlight, supportsLanguage, plain } from 'cli-highlight';
7
+ import chalk from 'chalk';
7
8
  import { program } from 'commander';
8
9
  import { spawn } from 'child_process';
9
10
  import * as fs from 'fs';
@@ -14,7 +15,147 @@ const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = path.dirname(__filename);
15
16
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
16
17
 
17
- // Initialize marked configuration (will be configured in main function with color options)
18
+ // HSL color utilities for deriving muted variants from base hex colors
19
+ function hexToHsl(hex) {
20
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
21
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
22
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
23
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
24
+ let h, s, l = (max + min) / 2;
25
+ if (max === min) {
26
+ h = s = 0;
27
+ } else {
28
+ const d = max - min;
29
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
30
+ switch (max) {
31
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
32
+ case g: h = ((b - r) / d + 2) / 6; break;
33
+ case b: h = ((r - g) / d + 4) / 6; break;
34
+ }
35
+ }
36
+ return [h * 360, s * 100, l * 100];
37
+ }
38
+
39
+ function hslToHex(h, s, l) {
40
+ h /= 360; s /= 100; l /= 100;
41
+ let r, g, b;
42
+ if (s === 0) {
43
+ r = g = b = l;
44
+ } else {
45
+ const hue2rgb = (p, q, t) => {
46
+ if (t < 0) t += 1;
47
+ if (t > 1) t -= 1;
48
+ if (t < 1/6) return p + (q - p) * 6 * t;
49
+ if (t < 1/2) return q;
50
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
51
+ return p;
52
+ };
53
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
54
+ const p = 2 * l - q;
55
+ r = hue2rgb(p, q, h + 1/3);
56
+ g = hue2rgb(p, q, h);
57
+ b = hue2rgb(p, q, h - 1/3);
58
+ }
59
+ const toHex = x => Math.round(x * 255).toString(16).padStart(2, '0');
60
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
61
+ }
62
+
63
+ // Mute a hex color: reduce saturation and shift lightness toward middle
64
+ function mute(hex, satMul = 0.55, lightShift = 0.85) {
65
+ const [h, s, l] = hexToHsl(hex);
66
+ const newS = s * satMul;
67
+ const newL = l * lightShift;
68
+ return hslToHex(h, Math.min(newS, 100), Math.max(Math.min(newL, 100), 0));
69
+ }
70
+
71
+ // Build theme from base palette: markdown gets vivid colors, code blocks get muted variants
72
+ function buildTheme(colors) {
73
+ const m = (hex) => mute(hex); // muted variant for code block elements
74
+ return {
75
+ highlight: {
76
+ keyword: chalk.hex(m(colors.keyword)),
77
+ built_in: chalk.hex(m(colors.type)).italic,
78
+ type: chalk.hex(m(colors.type)),
79
+ literal: chalk.hex(m(colors.literal)),
80
+ number: chalk.hex(m(colors.number)),
81
+ regexp: chalk.hex(m(colors.string)),
82
+ string: chalk.hex(m(colors.string)),
83
+ subst: chalk.hex(m(colors.fg)),
84
+ symbol: chalk.hex(m(colors.literal)),
85
+ class: chalk.hex(m(colors.type)),
86
+ function: chalk.hex(m(colors.func)),
87
+ title: chalk.hex(m(colors.func)),
88
+ params: chalk.hex(m(colors.param)).italic,
89
+ comment: chalk.hex(m(colors.comment)).italic,
90
+ doctag: chalk.hex(m(colors.comment)).bold,
91
+ meta: chalk.hex(m(colors.comment)),
92
+ 'meta-keyword': chalk.hex(m(colors.keyword)),
93
+ 'meta-string': chalk.hex(m(colors.string)),
94
+ tag: chalk.hex(m(colors.keyword)),
95
+ name: chalk.hex(m(colors.keyword)),
96
+ attr: chalk.hex(m(colors.func)),
97
+ attribute: chalk.hex(m(colors.func)),
98
+ variable: chalk.hex(m(colors.fg)),
99
+ addition: chalk.hex(m(colors.added)),
100
+ deletion: chalk.hex(m(colors.deleted)),
101
+ default: plain,
102
+ },
103
+ markdown: {
104
+ firstHeading: chalk.hex(colors.heading1).underline.bold,
105
+ heading: chalk.hex(colors.heading2).bold,
106
+ code: chalk.hex(colors.string),
107
+ codespan: chalk.hex(colors.string),
108
+ blockquote: chalk.hex(colors.comment).italic,
109
+ strong: chalk.hex(colors.fg).bold,
110
+ em: chalk.hex(colors.param).italic,
111
+ del: chalk.hex(colors.comment).strikethrough,
112
+ link: chalk.hex(colors.type),
113
+ href: chalk.hex(colors.type).underline,
114
+ },
115
+ };
116
+ }
117
+
118
+ // Theme color palettes
119
+ const THEMES = {
120
+ default: { highlight: null, markdown: {} },
121
+ monokai: buildTheme({
122
+ keyword: '#F92672', func: '#A6E22E', string: '#E6DB74',
123
+ type: '#66D9EF', number: '#AE81FF', literal: '#AE81FF',
124
+ param: '#FD971F', comment: '#75715E', fg: '#F8F8F2',
125
+ heading1: '#F92672', heading2:'#A6E22E',
126
+ added: '#A6E22E', deleted: '#F92672',
127
+ }),
128
+ dracula: buildTheme({
129
+ keyword: '#FF79C6', func: '#50FA7B', string: '#F1FA8C',
130
+ type: '#8BE9FD', number: '#BD93F9', literal: '#BD93F9',
131
+ param: '#FFB86C', comment: '#6272A4', fg: '#F8F8F2',
132
+ heading1: '#BD93F9', heading2:'#FF79C6',
133
+ added: '#50FA7B', deleted: '#FF5555',
134
+ }),
135
+ 'github-dark': buildTheme({
136
+ keyword: '#FF7B72', func: '#D2A8FF', string: '#A5D6FF',
137
+ type: '#FFA657', number: '#79C0FF', literal: '#79C0FF',
138
+ param: '#C9D1D9', comment: '#8B949E', fg: '#C9D1D9',
139
+ heading1: '#FFFFFF', heading2:'#79C0FF',
140
+ added: '#7EE787', deleted: '#FF7B72',
141
+ }),
142
+ solarized: buildTheme({
143
+ keyword: '#859900', func: '#268BD2', string: '#2AA198',
144
+ type: '#B58900', number: '#D33682', literal: '#2AA198',
145
+ param: '#93A1A1', comment: '#586E75', fg: '#93A1A1',
146
+ heading1: '#CB4B16', heading2:'#268BD2',
147
+ added: '#859900', deleted: '#DC322F',
148
+ }),
149
+ nord: buildTheme({
150
+ keyword: '#81A1C1', func: '#88C0D0', string: '#A3BE8C',
151
+ type: '#8FBCBB', number: '#B48EAD', literal: '#81A1C1',
152
+ param: '#D8DEE9', comment: '#616E88', fg: '#D8DEE9',
153
+ heading1: '#88C0D0', heading2:'#81A1C1',
154
+ added: '#A3BE8C', deleted: '#BF616A',
155
+ }),
156
+ };
157
+
158
+ const THEME_NAMES = Object.keys(THEMES);
18
159
 
19
160
  function readMarkdownFile(filePath) {
20
161
  const absolutePath = path.resolve(filePath);
@@ -138,7 +279,15 @@ async function main() {
138
279
  .option('--no-highlight', 'disable syntax highlighting')
139
280
  .option('--width <number>', 'terminal width override', Number)
140
281
  .option('--ascii', 'use pure ASCII mode for diagrams (default: unicode)')
282
+ .option('--theme <name>', `syntax highlight theme (${THEME_NAMES.join(', ')})`, 'default')
141
283
  .action(async (files, options) => {
284
+ // Validate theme option
285
+ const themeName = options.theme || 'default';
286
+ if (!(themeName in THEMES)) {
287
+ console.error(`Unknown theme: ${themeName}\nAvailable themes: ${THEME_NAMES.join(', ')}`);
288
+ process.exit(1);
289
+ }
290
+
142
291
  // Check if color should be disabled (--no-color or NO_COLOR env var)
143
292
  const useColor = options.color !== false && !process.env.NO_COLOR;
144
293
 
@@ -146,11 +295,18 @@ async function main() {
146
295
  const shouldHighlight = options.highlight !== false && useColor;
147
296
 
148
297
  // Configure marked with terminal renderer
149
- const highlightOptions = shouldHighlight ? { ignoreIllegals: true } : undefined;
298
+ const selectedTheme = THEMES[themeName];
299
+ const highlightOptions = shouldHighlight
300
+ ? {
301
+ ignoreIllegals: true,
302
+ ...(selectedTheme.highlight ? { theme: selectedTheme.highlight } : {}),
303
+ }
304
+ : undefined;
150
305
 
151
306
  marked.use(markedTerminal({
152
307
  reflowText: true,
153
308
  width: options.width ?? process.stdout.columns ?? 80,
309
+ ...selectedTheme.markdown,
154
310
  }, highlightOptions));
155
311
 
156
312
  // Override link renderer to avoid OSC 8 escape sequences (fixes tmux display issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memd-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "beautiful-mermaid": "^0.1.3",
14
+ "chalk": "^5.6.2",
14
15
  "cli-highlight": "^2.1.11",
15
16
  "commander": "^14.0.3",
16
17
  "marked": "^17.0.3",
@@ -27,6 +28,9 @@
27
28
  "vitest": "^4.0.18"
28
29
  },
29
30
  "pnpm": {
30
- "onlyBuiltDependencies": ["esbuild", "node-pty"]
31
+ "onlyBuiltDependencies": [
32
+ "esbuild",
33
+ "node-pty"
34
+ ]
31
35
  }
32
36
  }
package/test/memd.test.js CHANGED
@@ -22,7 +22,7 @@ async function run(args, { waitFor = null } = {}) {
22
22
  describe('memd CLI', () => {
23
23
  it('--version', async () => {
24
24
  const output = await run(['-v'])
25
- expect(output).toContain('1.1.1')
25
+ expect(output).toContain('1.3.0')
26
26
  })
27
27
 
28
28
  it('--help', async () => {