@ali.mazaheri/nanocat 0.0.0-stage → 0.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.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # nanocat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Syntax-highlight files or piped `stdin` directly in your terminal using GNU nano's `.nanorc` syntax definitions—so your CLI output matches the exact colours you see inside `nano`.
4
+
5
+ Zero external dependencies (Python 3 standard library only).
6
+
7
+ ## Features
8
+
9
+ - Evaluates `.nanorc` `color` and `icolor` rules (including multi-line `start=... end=...` blocks) in nano's exact top-to-bottom precedence order
10
+ - Reads `~/.nanorc` `include` directives and `numbercolor` / `titlecolor` theme settings (plus `--theme` overrides)
11
+ - Line numbers (`-n` / `--number`), Git diff gutter markers (`-g` / `--git`), and line-range slicing (`-r 10:40`)
12
+ - Soft-wrapping (`-w` / `--wrap`), multi-file headers, and auto-paging through `less -R` (`--paging=auto|always|never`)
13
+
14
+ ## Installation
15
+
16
+ ### Quick Install (One-Liner)
17
+
18
+ ```bash
19
+ curl -fsSL https://githubusercontent.com | bash
20
+ ```
21
+
22
+ ### Via pipx or uv
23
+
24
+ ```bash
25
+ pipx install git+https://github.com
26
+ ```
27
+
28
+ ### Manual Install
29
+
30
+ ```bash
31
+ git clone https://github.com
32
+ mkdir -p ~/.local/bin
33
+ ln -sf "\$(pwd)/nanocat/nanocat" ~/.local/bin/nanocat
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ # Highlight one or more files directly (auto-detects extension or shebang)
40
+ nanocat path/to/file.tsx
41
+ nanocat -n -g src/App.tsx src/index.tsx
42
+
43
+ # Show line numbers, git diff markers, and slice lines 10 to 40
44
+ nanocat -n -g -r 10:40 path/to/file.go
45
+
46
+ # Highlight piped stdin using a specific language
47
+ grep -n "func " main.go | nanocat -l go
48
+
49
+ # List all detected .nanorc syntax definitions
50
+ nanocat --list
51
+ ```
@@ -0,0 +1,13 @@
1
+ export interface NanorcRule {
2
+ type: "single" | "block";
3
+ color: string;
4
+ start: RegExp;
5
+ end?: RegExp;
6
+ }
7
+ export interface NanorcSyntax {
8
+ name: string;
9
+ extensions: RegExp[];
10
+ rules: NanorcRule[];
11
+ }
12
+ export declare function parseNanorc(content: string): NanorcSyntax[];
13
+ export declare function highlight(code: string, syntax: NanorcSyntax, format?: "ansi" | "html"): string;
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ const ANSI_FG = {
2
+ black: "30", red: "31", green: "32", yellow: "33",
3
+ blue: "34", magenta: "35", cyan: "36", white: "37",
4
+ brightblack: "90", brightred: "91", brightgreen: "92",
5
+ brightyellow: "93", brightblue: "94", brightmagenta: "95",
6
+ brightcyan: "96", brightwhite: "97", orange: "38;5;208",
7
+ };
8
+ function posixToJsRegex(pat) {
9
+ return pat
10
+ .replace(/\\</g, "\\b")
11
+ .replace(/\\>/g, "\\b")
12
+ .replace(/\[\[:space:\]\]|\[:space:\]/g, "\\s")
13
+ .replace(/\[\[:digit:\]\]|\[:digit:\]/g, "[0-9]")
14
+ .replace(/\[\[:alpha:\]\]|\[:alpha:\]/g, "[A-Za-z]")
15
+ .replace(/\[\[:alnum:\]\]|\[:alnum:\]/g, "[A-Za-z0-9]");
16
+ }
17
+ function colorToAnsi(spec) {
18
+ const parts = spec.toLowerCase().split(",").map((s) => s.trim());
19
+ const codes = [];
20
+ for (const p of parts) {
21
+ if (p === "bold")
22
+ codes.push("1");
23
+ else if (ANSI_FG[p])
24
+ codes.push(ANSI_FG[p]);
25
+ }
26
+ return codes.length ? `\x1b[${codes.join(";")}m` : "";
27
+ }
28
+ function extractQuoted(line) {
29
+ const out = [];
30
+ const re = /"((?:\\"|[^"])*)"/g;
31
+ let m;
32
+ while ((m = re.exec(line)) !== null) {
33
+ out.push(m[1].replace(/\\"/g, '"'));
34
+ }
35
+ return out;
36
+ }
37
+ export function parseNanorc(content) {
38
+ const syntaxes = [];
39
+ let current = null;
40
+ for (const raw of content.split(/\r?\n/)) {
41
+ const line = raw.trim();
42
+ if (!line || line.startsWith("#"))
43
+ continue;
44
+ if (line.startsWith("syntax ")) {
45
+ const quoted = extractQuoted(line.slice(7));
46
+ current = {
47
+ name: quoted[0] ?? "unknown",
48
+ extensions: quoted.slice(1).map((p) => new RegExp(posixToJsRegex(p))),
49
+ rules: [],
50
+ };
51
+ syntaxes.push(current);
52
+ }
53
+ else if (current && (line.startsWith("color ") || line.startsWith("icolor "))) {
54
+ const icase = line.startsWith("icolor ");
55
+ const rest = line.slice(icase ? 7 : 6).trim();
56
+ const spaceIdx = rest.search(/\s/);
57
+ if (spaceIdx === -1)
58
+ continue;
59
+ const colorSpec = rest.slice(0, spaceIdx);
60
+ const body = rest.slice(spaceIdx).trim();
61
+ const flags = icase ? "gm" : "gm";
62
+ const quoted = extractQuoted(body);
63
+ if (body.startsWith("start=") && quoted.length >= 2) {
64
+ current.rules.push({
65
+ type: "block",
66
+ color: colorSpec,
67
+ start: new RegExp(posixToJsRegex(quoted[0]), icase ? "gim" : "gm"),
68
+ end: new RegExp(posixToJsRegex(quoted[1]), icase ? "gim" : "gm"),
69
+ });
70
+ }
71
+ else {
72
+ for (const pat of quoted) {
73
+ current.rules.push({
74
+ type: "single",
75
+ color: colorSpec,
76
+ start: new RegExp(posixToJsRegex(pat), icase ? "gim" : "gm"),
77
+ });
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return syntaxes;
83
+ }
84
+ export function highlight(code, syntax, format = "ansi") {
85
+ const n = code.length;
86
+ const styles = new Array(n).fill("");
87
+ for (const rule of syntax.rules) {
88
+ if (rule.type === "single") {
89
+ rule.start.lastIndex = 0;
90
+ let m;
91
+ while ((m = rule.start.exec(code)) !== null) {
92
+ if (m[0].length === 0) {
93
+ rule.start.lastIndex++;
94
+ continue;
95
+ }
96
+ for (let i = m.index; i < m.index + m[0].length; i++)
97
+ styles[i] = rule.color;
98
+ }
99
+ }
100
+ else if (rule.type === "block" && rule.end) {
101
+ let pos = 0;
102
+ while (pos < n) {
103
+ rule.start.lastIndex = pos;
104
+ const ms = rule.start.exec(code);
105
+ if (!ms)
106
+ break;
107
+ rule.end.lastIndex = ms.index + ms[0].length;
108
+ const me = rule.end.exec(code);
109
+ const endPos = me ? me.index + me[0].length : n;
110
+ for (let i = ms.index; i < endPos; i++)
111
+ styles[i] = rule.color;
112
+ pos = Math.max(endPos, ms.index + 1);
113
+ }
114
+ }
115
+ }
116
+ let out = "";
117
+ let cur = "";
118
+ for (let i = 0; i < n; i++) {
119
+ const st = styles[i];
120
+ if (st !== cur) {
121
+ if (cur)
122
+ out += format === "ansi" ? "\x1b[0m" : "</span>";
123
+ if (st) {
124
+ out += format === "ansi"
125
+ ? colorToAnsi(st)
126
+ : `<span class="nc-${st.replace(/[^a-z0-9]/gi, "-")}">`;
127
+ }
128
+ cur = st;
129
+ }
130
+ const ch = code[i];
131
+ out += format === "html"
132
+ ? ch.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
133
+ : ch;
134
+ }
135
+ if (cur)
136
+ out += format === "ansi" ? "\x1b[0m" : "</span>";
137
+ return out;
138
+ }
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "@ali.mazaheri/nanocat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "Zero-dependency .nanorc syntax highlighter for Node.js CI and browser HTML output.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "node --test tests/nanocat.test.mjs"
11
+ },
12
+ "keywords": ["nano", "nanorc", "syntax-highlighting", "ansi", "ci", "typescript"],
13
+ "license": "MIT"
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,150 @@
1
+ export interface NanorcRule {
2
+ type: "single" | "block";
3
+ color: string;
4
+ start: RegExp;
5
+ end?: RegExp;
6
+ }
7
+
8
+ export interface NanorcSyntax {
9
+ name: string;
10
+ extensions: RegExp[];
11
+ rules: NanorcRule[];
12
+ }
13
+
14
+ const ANSI_FG: Record<string, string> = {
15
+ black: "30", red: "31", green: "32", yellow: "33",
16
+ blue: "34", magenta: "35", cyan: "36", white: "37",
17
+ brightblack: "90", brightred: "91", brightgreen: "92",
18
+ brightyellow: "93", brightblue: "94", brightmagenta: "95",
19
+ brightcyan: "96", brightwhite: "97", orange: "38;5;208",
20
+ };
21
+
22
+ function posixToJsRegex(pat: string): string {
23
+ return pat
24
+ .replace(/\\</g, "\\b")
25
+ .replace(/\\>/g, "\\b")
26
+ .replace(/\[\[:space:\]\]|\[:space:\]/g, "\\s")
27
+ .replace(/\[\[:digit:\]\]|\[:digit:\]/g, "[0-9]")
28
+ .replace(/\[\[:alpha:\]\]|\[:alpha:\]/g, "[A-Za-z]")
29
+ .replace(/\[\[:alnum:\]\]|\[:alnum:\]/g, "[A-Za-z0-9]");
30
+ }
31
+
32
+ function colorToAnsi(spec: string): string {
33
+ const parts = spec.toLowerCase().split(",").map((s) => s.trim());
34
+ const codes: string[] = [];
35
+ for (const p of parts) {
36
+ if (p === "bold") codes.push("1");
37
+ else if (ANSI_FG[p]) codes.push(ANSI_FG[p]);
38
+ }
39
+ return codes.length ? `\x1b[${codes.join(";")}m` : "";
40
+ }
41
+
42
+ function extractQuoted(line: string): string[] {
43
+ const out: string[] = [];
44
+ const re = /"((?:\\"|[^"])*)"/g;
45
+ let m: RegExpExecArray | null;
46
+ while ((m = re.exec(line)) !== null) {
47
+ out.push(m[1].replace(/\\"/g, '"'));
48
+ }
49
+ return out;
50
+ }
51
+
52
+ export function parseNanorc(content: string): NanorcSyntax[] {
53
+ const syntaxes: NanorcSyntax[] = [];
54
+ let current: NanorcSyntax | null = null;
55
+
56
+ for (const raw of content.split(/\r?\n/)) {
57
+ const line = raw.trim();
58
+ if (!line || line.startsWith("#")) continue;
59
+
60
+ if (line.startsWith("syntax ")) {
61
+ const quoted = extractQuoted(line.slice(7));
62
+ current = {
63
+ name: quoted[0] ?? "unknown",
64
+ extensions: quoted.slice(1).map((p) => new RegExp(posixToJsRegex(p))),
65
+ rules: [],
66
+ };
67
+ syntaxes.push(current);
68
+ } else if (current && (line.startsWith("color ") || line.startsWith("icolor "))) {
69
+ const icase = line.startsWith("icolor ");
70
+ const rest = line.slice(icase ? 7 : 6).trim();
71
+ const spaceIdx = rest.search(/\s/);
72
+ if (spaceIdx === -1) continue;
73
+ const colorSpec = rest.slice(0, spaceIdx);
74
+ const body = rest.slice(spaceIdx).trim();
75
+ const flags = icase ? "gm" : "gm";
76
+ const quoted = extractQuoted(body);
77
+
78
+ if (body.startsWith("start=") && quoted.length >= 2) {
79
+ current.rules.push({
80
+ type: "block",
81
+ color: colorSpec,
82
+ start: new RegExp(posixToJsRegex(quoted[0]), icase ? "gim" : "gm"),
83
+ end: new RegExp(posixToJsRegex(quoted[1]), icase ? "gim" : "gm"),
84
+ });
85
+ } else {
86
+ for (const pat of quoted) {
87
+ current.rules.push({
88
+ type: "single",
89
+ color: colorSpec,
90
+ start: new RegExp(posixToJsRegex(pat), icase ? "gim" : "gm"),
91
+ });
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return syntaxes;
97
+ }
98
+
99
+ export function highlight(
100
+ code: string,
101
+ syntax: NanorcSyntax,
102
+ format: "ansi" | "html" = "ansi"
103
+ ): string {
104
+ const n = code.length;
105
+ const styles = new Array<string>(n).fill("");
106
+
107
+ for (const rule of syntax.rules) {
108
+ if (rule.type === "single") {
109
+ rule.start.lastIndex = 0;
110
+ let m: RegExpExecArray | null;
111
+ while ((m = rule.start.exec(code)) !== null) {
112
+ if (m[0].length === 0) { rule.start.lastIndex++; continue; }
113
+ for (let i = m.index; i < m.index + m[0].length; i++) styles[i] = rule.color;
114
+ }
115
+ } else if (rule.type === "block" && rule.end) {
116
+ let pos = 0;
117
+ while (pos < n) {
118
+ rule.start.lastIndex = pos;
119
+ const ms = rule.start.exec(code);
120
+ if (!ms) break;
121
+ rule.end.lastIndex = ms.index + ms[0].length;
122
+ const me = rule.end.exec(code);
123
+ const endPos = me ? me.index + me[0].length : n;
124
+ for (let i = ms.index; i < endPos; i++) styles[i] = rule.color;
125
+ pos = Math.max(endPos, ms.index + 1);
126
+ }
127
+ }
128
+ }
129
+
130
+ let out = "";
131
+ let cur = "";
132
+ for (let i = 0; i < n; i++) {
133
+ const st = styles[i];
134
+ if (st !== cur) {
135
+ if (cur) out += format === "ansi" ? "\x1b[0m" : "</span>";
136
+ if (st) {
137
+ out += format === "ansi"
138
+ ? colorToAnsi(st)
139
+ : `<span class="nc-${st.replace(/[^a-z0-9]/gi, "-")}">`;
140
+ }
141
+ cur = st;
142
+ }
143
+ const ch = code[i];
144
+ out += format === "html"
145
+ ? ch.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
146
+ : ch;
147
+ }
148
+ if (cur) out += format === "ansi" ? "\x1b[0m" : "</span>";
149
+ return out;
150
+ }
@@ -0,0 +1,22 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseNanorc, highlight } from "../dist/index.js";
4
+
5
+ test("parseNanorc and highlight ANSI and HTML", () => {
6
+ const rc = `
7
+ syntax "demo" "\\.demo$"
8
+ color green "\\<(func|return)\\>"
9
+ color brightblue start="/\\*" end="\\*/"
10
+ `;
11
+ const syntaxes = parseNanorc(rc);
12
+ assert.equal(syntaxes.length, 1);
13
+
14
+ const code = "func main() { /* return */ return }";
15
+ const ansi = highlight(code, syntaxes[0], "ansi");
16
+ const html = highlight(code, syntaxes[0], "html");
17
+
18
+ assert.match(ansi, /\x1b\[32mfunc\x1b\[0m/);
19
+ assert.match(ansi, /\x1b\[94m\/\* return \*\/\x1b\[0m/);
20
+ assert.match(html, /<span class="nc-green">func<\/span>/);
21
+ assert.match(html, /<span class="nc-brightblue">\/\* return \*\/<\/span>/);
22
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "strict": true
10
+ },
11
+ "include": ["src/**/*"]
12
+ }