@chromamark/cli 0.1.3 → 0.2.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/LICENSE.md +5 -11
- package/README.md +32 -4
- package/package.json +9 -5
- package/src/cli.js +140 -17
- package/src/index.d.ts +25 -0
- package/src/index.js +7 -1
package/LICENSE.md
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# MIT License
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Copyright (c) 2026 CJ Fravel
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
8
|
|
|
9
|
-
THE SOFTWARE IS PROVIDED
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Third-party components
|
|
14
|
-
|
|
15
|
-
The published browser bundles in `packages/renderer/dist/` and the bundled VS Code extension embed [markdown-it](https://github.com/markdown-it/markdown-it), which is licensed under the [MIT License](https://github.com/markdown-it/markdown-it/blob/master/LICENSE).
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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,21 +22,48 @@ 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
|
+
Commands accept one input source. Extra positional arguments, including a second
|
|
39
|
+
`-` stdin marker, are rejected instead of being silently ignored.
|
|
40
|
+
|
|
41
|
+
The `render` command turns tones into terminal colors, pills into bracketed
|
|
42
|
+
icon chips (`[✓ PASS]`), blocks into a colored left bar, and meters into a
|
|
43
|
+
unicode bar. Color is automatic on a TTY and can be forced with `--color`; it
|
|
44
|
+
also honors the [`NO_COLOR`](https://no-color.org) convention.
|
|
45
|
+
|
|
46
|
+
The `lint` command flags mistakes that otherwise degrade silently — a construct
|
|
47
|
+
wrapped in backticks (`CM001`), an unknown tone (`CM002`), an unknown block kind
|
|
48
|
+
(`CM003`), a bad meter value (`CM004`), or an unclosed container (`CM005`) — and
|
|
49
|
+
exits non-zero when any are found. Suppress rules with `--disable CM001,CM003`.
|
|
50
|
+
|
|
29
51
|
## Programmatic
|
|
30
52
|
|
|
31
53
|
```js
|
|
32
|
-
import { compile, render } from '@chromamark/cli';
|
|
54
|
+
import { compile, render, renderAnsi, lint } from '@chromamark/cli';
|
|
33
55
|
|
|
34
56
|
const page = compile('::: success\nAll good [!pass]\n:::', { title: 'Report' });
|
|
35
57
|
// → complete <!DOCTYPE html> document with the theme inlined
|
|
36
58
|
const fragment = render('[!pass]'); // just the HTML fragment
|
|
59
|
+
const ansi = renderAnsi('[!pass]', { color: 'always' }); // terminal-styled text
|
|
60
|
+
const problems = lint('Use `[!pass]` here'); // [{ line, column, rule, ... }]
|
|
37
61
|
```
|
|
38
62
|
|
|
63
|
+
TypeScript declarations ship with the package.
|
|
64
|
+
|
|
39
65
|
## License
|
|
40
66
|
|
|
41
|
-
|
|
67
|
+
This software package is licensed under the MIT License; see LICENSE.md. The
|
|
68
|
+
[ChromaMark specification](https://github.com/cjfravel-dev/ChromaMark/blob/main/LICENSE-SPEC.md)
|
|
69
|
+
is licensed separately under CC BY-SA 4.0.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromamark/cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "ChromaMark command-line
|
|
5
|
-
"license": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "ChromaMark command-line tool — compile .cm to self-contained HTML, render to a color terminal, or lint.",
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"author": "cjfravel-dev",
|
|
7
7
|
"homepage": "https://github.com/cjfravel-dev/ChromaMark#readme",
|
|
8
8
|
"repository": {
|
|
@@ -24,8 +24,12 @@
|
|
|
24
24
|
"chromamark": "./bin/chromamark.js"
|
|
25
25
|
},
|
|
26
26
|
"main": "./src/index.js",
|
|
27
|
+
"types": "./src/index.d.ts",
|
|
27
28
|
"exports": {
|
|
28
|
-
".":
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./src/index.d.ts",
|
|
31
|
+
"default": "./src/index.js"
|
|
32
|
+
}
|
|
29
33
|
},
|
|
30
34
|
"files": [
|
|
31
35
|
"bin/",
|
|
@@ -42,6 +46,6 @@
|
|
|
42
46
|
"html"
|
|
43
47
|
],
|
|
44
48
|
"dependencies": {
|
|
45
|
-
"@chromamark/renderer": "^0.
|
|
49
|
+
"@chromamark/renderer": "^0.3.1"
|
|
46
50
|
}
|
|
47
51
|
}
|
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,11 +87,20 @@ 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
|
-
else if (a === '-') opts.input = '-';
|
|
99
|
+
else if (a === '-' && opts.input == null) opts.input = '-';
|
|
100
|
+
else if (a === '-') throw new Error('unexpected argument: -');
|
|
55
101
|
else if (a.startsWith('-')) throw new Error(`unknown option: ${a}`);
|
|
56
102
|
else if (opts.input == null) opts.input = a;
|
|
103
|
+
else throw new Error(`unexpected argument: ${a}`);
|
|
57
104
|
}
|
|
58
105
|
return opts;
|
|
59
106
|
}
|
|
@@ -62,6 +109,60 @@ function titleFor(file) {
|
|
|
62
109
|
return basename(file, extname(file));
|
|
63
110
|
}
|
|
64
111
|
|
|
112
|
+
/** Read source from a file, `-`, or piped stdin. Returns { src, path } or an
|
|
113
|
+
* error shape ({ error } / { empty }) the command can turn into an exit code. */
|
|
114
|
+
function readInput(opts) {
|
|
115
|
+
if (opts.input === '-' || (opts.input == null && !process.stdin.isTTY)) {
|
|
116
|
+
const src = readStdin();
|
|
117
|
+
if (opts.input !== '-' && !src.trim()) return { empty: true };
|
|
118
|
+
return { src, path: '<stdin>' };
|
|
119
|
+
}
|
|
120
|
+
if (opts.input == null) return null;
|
|
121
|
+
return { src: readFileSync(opts.input, 'utf8'), path: opts.input };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Render ChromaMark to ANSI on stdout, from a file, `-`, or piped stdin. */
|
|
125
|
+
function runRender(opts) {
|
|
126
|
+
let input;
|
|
127
|
+
try {
|
|
128
|
+
input = readInput(opts);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
if (!input || input.empty) {
|
|
134
|
+
process.stderr.write(`error: render needs an input file or piped stdin\n\n${HELP}`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
process.stdout.write(renderAnsi(input.src, { color: opts.color }));
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Lint ChromaMark and print diagnostics; exit non-zero when any are found. */
|
|
142
|
+
function runLint(opts) {
|
|
143
|
+
let input;
|
|
144
|
+
try {
|
|
145
|
+
input = readInput(opts);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
if (!input || input.empty) {
|
|
151
|
+
process.stderr.write(`error: lint needs an input file or piped stdin\n\n${HELP}`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
const diags = lint(input.src, { disable: opts.disable });
|
|
155
|
+
for (const d of diags) {
|
|
156
|
+
process.stdout.write(`${input.path}:${d.line}:${d.column} ${d.severity} ${d.rule} ${d.message}\n`);
|
|
157
|
+
}
|
|
158
|
+
if (diags.length) {
|
|
159
|
+
process.stderr.write(`✗ ${diags.length} problem${diags.length === 1 ? '' : 's'}\n`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
process.stderr.write(`✓ ${input.path}: no problems\n`);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
65
166
|
function listCmFiles(dir) {
|
|
66
167
|
const out = [];
|
|
67
168
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -109,8 +210,15 @@ export function run(argv = process.argv.slice(2)) {
|
|
|
109
210
|
if (opts.help) { process.stdout.write(HELP); return 0; }
|
|
110
211
|
if (opts.version) { process.stdout.write(`${version()}\n`); return 0; }
|
|
111
212
|
|
|
213
|
+
if (opts.command === 'render') {
|
|
214
|
+
return runRender(opts);
|
|
215
|
+
}
|
|
216
|
+
if (opts.command === 'lint') {
|
|
217
|
+
return runLint(opts);
|
|
218
|
+
}
|
|
219
|
+
|
|
112
220
|
if (opts.input === '-' || (opts.input == null && !process.stdin.isTTY)) {
|
|
113
|
-
const src =
|
|
221
|
+
const src = readStdin();
|
|
114
222
|
if (opts.input !== '-' && !src.trim()) {
|
|
115
223
|
process.stderr.write(`error: no input file given and stdin was empty\n\n${HELP}`);
|
|
116
224
|
return 1;
|
|
@@ -128,23 +236,38 @@ export function run(argv = process.argv.slice(2)) {
|
|
|
128
236
|
|
|
129
237
|
try {
|
|
130
238
|
info = statSync(opts.input);
|
|
131
|
-
rebuild();
|
|
132
239
|
} catch (err) {
|
|
133
240
|
process.stderr.write(`error: ${err.message}\n`);
|
|
134
241
|
return 1;
|
|
135
242
|
}
|
|
136
243
|
|
|
137
244
|
if (opts.watch && !opts.stdout) {
|
|
138
|
-
|
|
139
|
-
|
|
245
|
+
const onChange = (_eventType, filename) => {
|
|
246
|
+
if (info.isDirectory() && filename && extname(String(filename)) !== '.cm') return;
|
|
140
247
|
try { rebuild(); } catch (err) { process.stderr.write(`error: ${err.message}\n`); }
|
|
141
248
|
};
|
|
249
|
+
let watcher;
|
|
142
250
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
251
|
+
try {
|
|
252
|
+
watcher = watch(opts.input, { recursive: info.isDirectory() }, onChange);
|
|
253
|
+
} catch {
|
|
254
|
+
watcher = watch(opts.input, onChange); // non-recursive fallback (older Node / platforms)
|
|
255
|
+
}
|
|
256
|
+
process.stderr.write('watching for changes… (Ctrl+C to stop)\n');
|
|
257
|
+
rebuild();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (watcher) watcher.close();
|
|
260
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
261
|
+
return 1;
|
|
146
262
|
}
|
|
147
263
|
return 0;
|
|
148
264
|
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
rebuild();
|
|
268
|
+
} catch (err) {
|
|
269
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
149
272
|
return 0;
|
|
150
273
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LintDiagnostic,
|
|
3
|
+
LintOptions,
|
|
4
|
+
RendererOptions,
|
|
5
|
+
RenderAnsiOptions,
|
|
6
|
+
} from '@chromamark/renderer';
|
|
7
|
+
|
|
8
|
+
export interface CompileOptions {
|
|
9
|
+
title?: string;
|
|
10
|
+
theme?: string;
|
|
11
|
+
rendererOptions?: RendererOptions;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function render(source: unknown, options?: RendererOptions): string;
|
|
15
|
+
export function renderAnsi(source: unknown, options?: RenderAnsiOptions): string;
|
|
16
|
+
export function lint(source: unknown, options?: LintOptions): LintDiagnostic[];
|
|
17
|
+
export function theme(): string;
|
|
18
|
+
export function compile(source: unknown, options?: CompileOptions): string;
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
LintDiagnostic,
|
|
22
|
+
LintOptions,
|
|
23
|
+
RendererOptions,
|
|
24
|
+
RenderAnsiOptions,
|
|
25
|
+
};
|
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. */
|