@chromamark/cli 0.1.3 → 0.2.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/README.md +24 -3
- package/package.json +3 -3
- package/src/cli.js +116 -10
- package/src/index.js +7 -1
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Compile [ChromaMark](https://github.com/cjfravel-dev/ChromaMark) (`.cm`) files
|
|
4
4
|
into **self-contained HTML** — the theme is inlined, so there's no CDN or build
|
|
5
|
-
step to deploy
|
|
5
|
+
step to deploy — render them straight to a **color terminal**, or **lint** them
|
|
6
|
+
in CI.
|
|
6
7
|
|
|
7
8
|
## Install
|
|
8
9
|
|
|
@@ -21,19 +22,39 @@ chromamark build docs/ -o site/ # every .cm in a tree → site/*.html
|
|
|
21
22
|
chromamark report.cm --stdout > out.html # write to stdout
|
|
22
23
|
cat report.cm | chromamark - # read from stdin
|
|
23
24
|
chromamark build report.cm --watch # rebuild on change
|
|
25
|
+
|
|
26
|
+
chromamark render report.cm # render to the terminal (ANSI)
|
|
27
|
+
cat report.cm | chromamark render # render piped stdin
|
|
28
|
+
chromamark render report.cm --no-color # plain, icon-annotated text
|
|
29
|
+
|
|
30
|
+
chromamark lint report.cm # check for common mistakes (CI)
|
|
31
|
+
chromamark lint report.cm --disable CM001 # suppress a rule
|
|
24
32
|
```
|
|
25
33
|
|
|
26
|
-
Options: `-o/--output`, `--stdout`, `--title <text>`, `--watch`,
|
|
34
|
+
Options: `-o/--output`, `--stdout`, `--title <text>`, `--watch`,
|
|
35
|
+
`--color <auto|always|never>`, `--no-color`, `--disable <rules>`, `-h/--help`,
|
|
27
36
|
`-v/--version`.
|
|
28
37
|
|
|
38
|
+
The `render` command turns tones into terminal colors, pills into bracketed
|
|
39
|
+
icon chips (`[✓ PASS]`), blocks into a colored left bar, and meters into a
|
|
40
|
+
unicode bar. Color is automatic on a TTY and can be forced with `--color`; it
|
|
41
|
+
also honors the [`NO_COLOR`](https://no-color.org) convention.
|
|
42
|
+
|
|
43
|
+
The `lint` command flags mistakes that otherwise degrade silently — a construct
|
|
44
|
+
wrapped in backticks (`CM001`), an unknown tone (`CM002`), an unknown block kind
|
|
45
|
+
(`CM003`), a bad meter value (`CM004`), or an unclosed container (`CM005`) — and
|
|
46
|
+
exits non-zero when any are found. Suppress rules with `--disable CM001,CM003`.
|
|
47
|
+
|
|
29
48
|
## Programmatic
|
|
30
49
|
|
|
31
50
|
```js
|
|
32
|
-
import { compile, render } from '@chromamark/cli';
|
|
51
|
+
import { compile, render, renderAnsi, lint } from '@chromamark/cli';
|
|
33
52
|
|
|
34
53
|
const page = compile('::: success\nAll good [!pass]\n:::', { title: 'Report' });
|
|
35
54
|
// → complete <!DOCTYPE html> document with the theme inlined
|
|
36
55
|
const fragment = render('[!pass]'); // just the HTML fragment
|
|
56
|
+
const ansi = renderAnsi('[!pass]', { color: 'always' }); // terminal-styled text
|
|
57
|
+
const problems = lint('Use `[!pass]` here'); // [{ line, column, rule, ... }]
|
|
37
58
|
```
|
|
38
59
|
|
|
39
60
|
## License
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromamark/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ChromaMark command-line
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "ChromaMark command-line tool — compile .cm to self-contained HTML, render to a color terminal, or lint.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "cjfravel-dev",
|
|
7
7
|
"homepage": "https://github.com/cjfravel-dev/ChromaMark#readme",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"html"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@chromamark/renderer": "^0.
|
|
45
|
+
"@chromamark/renderer": "^0.3.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/cli.js
CHANGED
|
@@ -4,37 +4,75 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, watch,
|
|
7
|
+
readFileSync, readSync, writeFileSync, statSync, readdirSync, mkdirSync, watch,
|
|
8
8
|
} from 'node:fs';
|
|
9
9
|
import { join, basename, extname, dirname, relative } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { compile } from './index.js';
|
|
11
|
+
import { compile, renderAnsi, lint } from './index.js';
|
|
12
12
|
|
|
13
|
-
const HELP = `chromamark — compile ChromaMark (.cm) to
|
|
13
|
+
const HELP = `chromamark — compile ChromaMark (.cm) to HTML, render it, or lint it
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
|
-
chromamark [build] <input.cm> [-o <output.html>] compile one file
|
|
16
|
+
chromamark [build] <input.cm> [-o <output.html>] compile one file to HTML
|
|
17
17
|
chromamark [build] <dir> -o <outdir> compile every .cm in a tree
|
|
18
|
+
chromamark render <input.cm> render to ANSI on stdout
|
|
19
|
+
chromamark lint <input.cm> check for common mistakes
|
|
20
|
+
cat file.cm | chromamark render render stdin to ANSI
|
|
18
21
|
chromamark <input.cm> --stdout write HTML to stdout
|
|
19
|
-
cat file.cm | chromamark - read from stdin
|
|
22
|
+
cat file.cm | chromamark - read HTML from stdin
|
|
20
23
|
|
|
21
24
|
Options:
|
|
22
|
-
-o, --output <path> output file or directory
|
|
25
|
+
-o, --output <path> output file or directory (build)
|
|
23
26
|
--stdout write HTML to stdout instead of a file
|
|
24
27
|
--title <text> page title (default: derived from the file name)
|
|
28
|
+
--color <when> colorize render output: auto (default), always, never
|
|
29
|
+
--no-color disable color (same as --color never)
|
|
30
|
+
--disable <rules> lint: comma-separated rule ids to suppress (e.g. CM001)
|
|
25
31
|
--watch rebuild when inputs change
|
|
26
32
|
-h, --help show this help
|
|
27
33
|
-v, --version print the version
|
|
28
34
|
`;
|
|
29
35
|
|
|
36
|
+
const COMMANDS = new Set(['build', 'render', 'lint']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read all of stdin synchronously. Unlike readFileSync(0), this tolerates a
|
|
40
|
+
* non-blocking pipe from another process (which throws EAGAIN when no data is
|
|
41
|
+
* ready yet) by retrying after a short sleep until EOF.
|
|
42
|
+
*/
|
|
43
|
+
function readStdin() {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
const buf = Buffer.alloc(65536);
|
|
46
|
+
const idle = new Int32Array(new SharedArrayBuffer(4));
|
|
47
|
+
for (;;) {
|
|
48
|
+
let bytes;
|
|
49
|
+
try {
|
|
50
|
+
bytes = readSync(0, buf, 0, buf.length, null);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.code === 'EAGAIN') { Atomics.wait(idle, 0, 0, 5); continue; }
|
|
53
|
+
if (err.code === 'EOF') break; // some platforms signal EOF this way
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
if (bytes === 0) break;
|
|
57
|
+
chunks.push(Buffer.from(buf.subarray(0, bytes)));
|
|
58
|
+
}
|
|
59
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
30
62
|
function version() {
|
|
31
63
|
const url = new URL('../package.json', import.meta.url);
|
|
32
64
|
return JSON.parse(readFileSync(fileURLToPath(url), 'utf8')).version;
|
|
33
65
|
}
|
|
34
66
|
|
|
35
67
|
function parseArgs(argv) {
|
|
36
|
-
const opts = {
|
|
37
|
-
|
|
68
|
+
const opts = {
|
|
69
|
+
command: 'build', input: null, output: null, stdout: false, title: null, watch: false, color: 'auto', disable: [],
|
|
70
|
+
};
|
|
71
|
+
let rest = argv;
|
|
72
|
+
if (argv.length && COMMANDS.has(argv[0])) {
|
|
73
|
+
opts.command = argv[0];
|
|
74
|
+
rest = argv.slice(1);
|
|
75
|
+
}
|
|
38
76
|
for (let i = 0; i < rest.length; i++) {
|
|
39
77
|
const a = rest[i];
|
|
40
78
|
const takeValue = (flag) => {
|
|
@@ -49,7 +87,14 @@ function parseArgs(argv) {
|
|
|
49
87
|
else if (a === '-v' || a === '--version') opts.version = true;
|
|
50
88
|
else if (a === '--stdout') opts.stdout = true;
|
|
51
89
|
else if (a === '--watch') opts.watch = true;
|
|
52
|
-
else if (a === '-
|
|
90
|
+
else if (a === '--no-color') opts.color = 'never';
|
|
91
|
+
else if (a === '--color') {
|
|
92
|
+
const v = takeValue(a);
|
|
93
|
+
if (!['auto', 'always', 'never'].includes(v)) throw new Error('--color must be auto, always, or never');
|
|
94
|
+
opts.color = v;
|
|
95
|
+
} else if (a === '--disable') {
|
|
96
|
+
opts.disable = opts.disable.concat(takeValue(a).split(',').map((s) => s.trim()).filter(Boolean));
|
|
97
|
+
} else if (a === '-o' || a === '--output') opts.output = takeValue(a);
|
|
53
98
|
else if (a === '--title') opts.title = takeValue(a);
|
|
54
99
|
else if (a === '-') opts.input = '-';
|
|
55
100
|
else if (a.startsWith('-')) throw new Error(`unknown option: ${a}`);
|
|
@@ -62,6 +107,60 @@ function titleFor(file) {
|
|
|
62
107
|
return basename(file, extname(file));
|
|
63
108
|
}
|
|
64
109
|
|
|
110
|
+
/** Read source from a file, `-`, or piped stdin. Returns { src, path } or an
|
|
111
|
+
* error shape ({ error } / { empty }) the command can turn into an exit code. */
|
|
112
|
+
function readInput(opts) {
|
|
113
|
+
if (opts.input === '-' || (opts.input == null && !process.stdin.isTTY)) {
|
|
114
|
+
const src = readStdin();
|
|
115
|
+
if (opts.input !== '-' && !src.trim()) return { empty: true };
|
|
116
|
+
return { src, path: '<stdin>' };
|
|
117
|
+
}
|
|
118
|
+
if (opts.input == null) return null;
|
|
119
|
+
return { src: readFileSync(opts.input, 'utf8'), path: opts.input };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Render ChromaMark to ANSI on stdout, from a file, `-`, or piped stdin. */
|
|
123
|
+
function runRender(opts) {
|
|
124
|
+
let input;
|
|
125
|
+
try {
|
|
126
|
+
input = readInput(opts);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
if (!input || input.empty) {
|
|
132
|
+
process.stderr.write(`error: render needs an input file or piped stdin\n\n${HELP}`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write(renderAnsi(input.src, { color: opts.color }));
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Lint ChromaMark and print diagnostics; exit non-zero when any are found. */
|
|
140
|
+
function runLint(opts) {
|
|
141
|
+
let input;
|
|
142
|
+
try {
|
|
143
|
+
input = readInput(opts);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
if (!input || input.empty) {
|
|
149
|
+
process.stderr.write(`error: lint needs an input file or piped stdin\n\n${HELP}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
const diags = lint(input.src, { disable: opts.disable });
|
|
153
|
+
for (const d of diags) {
|
|
154
|
+
process.stdout.write(`${input.path}:${d.line}:${d.column} ${d.severity} ${d.rule} ${d.message}\n`);
|
|
155
|
+
}
|
|
156
|
+
if (diags.length) {
|
|
157
|
+
process.stderr.write(`✗ ${diags.length} problem${diags.length === 1 ? '' : 's'}\n`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
process.stderr.write(`✓ ${input.path}: no problems\n`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
65
164
|
function listCmFiles(dir) {
|
|
66
165
|
const out = [];
|
|
67
166
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -109,8 +208,15 @@ export function run(argv = process.argv.slice(2)) {
|
|
|
109
208
|
if (opts.help) { process.stdout.write(HELP); return 0; }
|
|
110
209
|
if (opts.version) { process.stdout.write(`${version()}\n`); return 0; }
|
|
111
210
|
|
|
211
|
+
if (opts.command === 'render') {
|
|
212
|
+
return runRender(opts);
|
|
213
|
+
}
|
|
214
|
+
if (opts.command === 'lint') {
|
|
215
|
+
return runLint(opts);
|
|
216
|
+
}
|
|
217
|
+
|
|
112
218
|
if (opts.input === '-' || (opts.input == null && !process.stdin.isTTY)) {
|
|
113
|
-
const src =
|
|
219
|
+
const src = readStdin();
|
|
114
220
|
if (opts.input !== '-' && !src.trim()) {
|
|
115
221
|
process.stderr.write(`error: no input file given and stdin was empty\n\n${HELP}`);
|
|
116
222
|
return 1;
|
package/src/index.js
CHANGED
|
@@ -5,13 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
|
-
import { render as renderFragment } from '@chromamark/renderer';
|
|
8
|
+
import { render as renderFragment, renderAnsi, lint } from '@chromamark/renderer';
|
|
9
9
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
|
|
12
12
|
/** Render ChromaMark to an HTML fragment (no page chrome). */
|
|
13
13
|
export const render = renderFragment;
|
|
14
14
|
|
|
15
|
+
/** Render ChromaMark to ANSI-styled text for a terminal. */
|
|
16
|
+
export { renderAnsi };
|
|
17
|
+
|
|
18
|
+
/** Lint ChromaMark source, returning an array of diagnostics. */
|
|
19
|
+
export { lint };
|
|
20
|
+
|
|
15
21
|
let cachedTheme;
|
|
16
22
|
|
|
17
23
|
/** The ChromaMark theme stylesheet, read from @chromamark/renderer. */
|