@cavelang/highlight 0.5.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/License.md ADDED
@@ -0,0 +1,6 @@
1
+ # CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
2
+
3
+ To the extent possible under law, the authors listed in [Authors.md](./Authors.md) have waived all copyright and related or neighboring rights to this software and associated documentation files (the "Software").
4
+
5
+ For more information, please see:
6
+ https://creativecommons.org/publicdomain/zero/1.0/
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @cavelang/highlight
2
+
3
+ CAVE syntax highlighting for terminals — web-tree-sitter over the
4
+ [`@cavelang/tree-sitter-cave`](../tree-sitter-cave) grammar, colored by the
5
+ grammar's own `queries/highlights.scm` so terminal output and editor
6
+ highlighting share one source of truth.
7
+
8
+ ```ts
9
+ import { highlighter } from '@cavelang/highlight'
10
+
11
+ const { ansi, spans } = await highlighter()
12
+ process.stdout.write(ansi('auth/middleware USES jwt @ 90% #security\n'))
13
+ ```
14
+
15
+ - `spans(text)` — flat, non-overlapping `{ start, end, capture }` ranges
16
+ - `ansi(text, theme?)` — ANSI-colored text; themes map capture names (or
17
+ dotted prefixes, longest wins) to SGR parameters
18
+ - `paint(text, spans, theme?)` — renderer, exported for custom span sources
19
+
20
+ The default theme leaves entities uncolored — they are the bulk of every
21
+ line; color carries the structure (verbs, values, metadata, comments).
@@ -0,0 +1,37 @@
1
+ /**
2
+ * CAVE syntax highlighting.
3
+ *
4
+ * Single-source: parses with the `@cavelang/tree-sitter-cave` WASM grammar
5
+ * via web-tree-sitter and colors the captures of its `queries/highlights.scm`
6
+ * — the same query editors use — so terminal output and editor highlighting
7
+ * can never drift apart.
8
+ *
9
+ * `highlighter()` loads the grammar once per process; `spans` yields flat,
10
+ * non-overlapping capture ranges and `ansi` renders them with a
11
+ * capture-name-keyed theme (longest dotted prefix wins, unstyled text passes
12
+ * through untouched).
13
+ */
14
+ /** One highlighted range: `capture` is a `highlights.scm` name like `keyword`. */
15
+ export type Span = {
16
+ readonly start: number;
17
+ readonly end: number;
18
+ readonly capture: string;
19
+ };
20
+ /** Capture name (or dotted prefix) to ANSI SGR parameters, e.g. `keyword: '35'`. */
21
+ export type Theme = Readonly<Record<string, string>>;
22
+ /**
23
+ * Terminal-default-friendly theme: entities (`variable`) stay uncolored on
24
+ * purpose — they are the bulk of every line; color carries the structure.
25
+ */
26
+ export declare const defaultTheme: Theme;
27
+ export type Highlighter = {
28
+ /** Non-overlapping capture spans of `text`, in document order. */
29
+ readonly spans: (text: string) => readonly Span[];
30
+ /** `text` with ANSI colors applied per `theme` (default {@link defaultTheme}). */
31
+ readonly ansi: (text: string, theme?: Theme) => string;
32
+ };
33
+ /** Renders `spans` over `text` as ANSI; exported for custom span sources. */
34
+ export declare const paint: (text: string, spans: readonly Span[], theme?: Theme) => string;
35
+ /** The process-wide highlighter; loads the grammar WASM on first call. */
36
+ export declare const highlighter: () => Promise<Highlighter>;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,kFAAkF;AAClF,MAAM,MAAM,IAAI,GAAG;IACjB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,oFAAoF;AACpF,MAAM,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAEpD;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,KAc1B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,IAAI,EAAE,CAAA;IACjD,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,KAAK,MAAM,CAAA;CACvD,CAAA;AAkBD,6EAA6E;AAC7E,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,EAAE,OAAO,SAAS,IAAI,EAAE,EAAE,QAAO,KAAoB,KAAG,MAWzF,CAAA;AAyCD,0EAA0E;AAC1E,eAAO,MAAM,WAAW,QAAO,OAAO,CAAC,WAAW,CAC7B,CAAA"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * CAVE syntax highlighting.
3
+ *
4
+ * Single-source: parses with the `@cavelang/tree-sitter-cave` WASM grammar
5
+ * via web-tree-sitter and colors the captures of its `queries/highlights.scm`
6
+ * — the same query editors use — so terminal output and editor highlighting
7
+ * can never drift apart.
8
+ *
9
+ * `highlighter()` loads the grammar once per process; `spans` yields flat,
10
+ * non-overlapping capture ranges and `ansi` renders them with a
11
+ * capture-name-keyed theme (longest dotted prefix wins, unstyled text passes
12
+ * through untouched).
13
+ */
14
+ import { readFileSync } from 'node:fs';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { Language, Parser, Query } from 'web-tree-sitter';
17
+ /**
18
+ * Terminal-default-friendly theme: entities (`variable`) stay uncolored on
19
+ * purpose — they are the bulk of every line; color carries the structure.
20
+ */
21
+ export const defaultTheme = {
22
+ comment: '90',
23
+ keyword: '35',
24
+ 'keyword.operator': '95',
25
+ property: '33',
26
+ number: '36',
27
+ type: '36',
28
+ string: '32',
29
+ 'string.special': '96',
30
+ label: '34',
31
+ constant: '33',
32
+ operator: '91',
33
+ tag: '94',
34
+ punctuation: '90'
35
+ };
36
+ const resolvePath = (specifier) => fileURLToPath(import.meta.resolve(specifier));
37
+ /** Longest dotted prefix of `capture` present in `theme`. */
38
+ const styleOf = (theme, capture) => {
39
+ for (let name = capture;; name = name.slice(0, name.lastIndexOf('.'))) {
40
+ const style = theme[name];
41
+ if (style !== undefined) {
42
+ return style;
43
+ }
44
+ if (!name.includes('.')) {
45
+ return undefined;
46
+ }
47
+ }
48
+ };
49
+ /** Renders `spans` over `text` as ANSI; exported for custom span sources. */
50
+ export const paint = (text, spans, theme = defaultTheme) => {
51
+ let out = '';
52
+ let at = 0;
53
+ for (const span of spans) {
54
+ const style = styleOf(theme, span.capture);
55
+ out += text.slice(at, span.start);
56
+ const piece = text.slice(span.start, span.end);
57
+ out += style === undefined ? piece : `\u001B[${style}m${piece}\u001B[0m`;
58
+ at = span.end;
59
+ }
60
+ return out + text.slice(at);
61
+ };
62
+ const create = async () => {
63
+ await Parser.init();
64
+ const language = await Language.load(resolvePath('@cavelang/tree-sitter-cave/wasm'));
65
+ const query = new Query(language, readFileSync(resolvePath('@cavelang/tree-sitter-cave/highlights'), 'utf8'));
66
+ const parser = new Parser();
67
+ parser.setLanguage(language);
68
+ const spans = (text) => {
69
+ const tree = parser.parse(text);
70
+ if (tree === null) {
71
+ return [];
72
+ }
73
+ try {
74
+ const all = query.captures(tree.rootNode)
75
+ .map(({ name, node }) => ({ start: node.startIndex, end: node.endIndex, capture: name }))
76
+ .filter(span => span.end > span.start)
77
+ .sort((a, b) => a.start - b.start || b.end - a.end);
78
+ // One capture per node keeps these disjoint already; guard anyway so a
79
+ // future query with nested captures degrades to outermost-wins.
80
+ const disjoint = [];
81
+ let at = 0;
82
+ for (const span of all) {
83
+ if (span.start >= at) {
84
+ disjoint.push(span);
85
+ at = span.end;
86
+ }
87
+ }
88
+ return disjoint;
89
+ }
90
+ finally {
91
+ tree.delete();
92
+ }
93
+ };
94
+ return {
95
+ spans,
96
+ ansi: (text, theme = defaultTheme) => paint(text, spans(text), theme)
97
+ };
98
+ };
99
+ let cached;
100
+ /** The process-wide highlighter; loads the grammar WASM on first call. */
101
+ export const highlighter = () => cached ??= create();
102
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cavelang/highlight",
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "description": "CAVE syntax highlighting — web-tree-sitter over the @cavelang/tree-sitter-cave grammar, rendered to ANSI for terminals.",
6
+ "license": "CC0-1.0",
7
+ "author": "Mirek Rusin (https://github.com/mirek)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mirek/cave.git",
11
+ "directory": "packages/highlight"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.18"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/src/index.d.ts",
19
+ "default": "./dist/src/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/test",
25
+ "!dist/tsconfig.tsbuildinfo",
26
+ "src",
27
+ "!src/**/*.test.ts",
28
+ "README.md",
29
+ "License.md"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "web-tree-sitter": "^0.26.10",
36
+ "@cavelang/tree-sitter-cave": "0.5.0"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -b",
40
+ "test": "node --disable-warning=ExperimentalWarning --test 'test/*.test.ts'",
41
+ "typecheck": "tsc -b"
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * CAVE syntax highlighting.
3
+ *
4
+ * Single-source: parses with the `@cavelang/tree-sitter-cave` WASM grammar
5
+ * via web-tree-sitter and colors the captures of its `queries/highlights.scm`
6
+ * — the same query editors use — so terminal output and editor highlighting
7
+ * can never drift apart.
8
+ *
9
+ * `highlighter()` loads the grammar once per process; `spans` yields flat,
10
+ * non-overlapping capture ranges and `ansi` renders them with a
11
+ * capture-name-keyed theme (longest dotted prefix wins, unstyled text passes
12
+ * through untouched).
13
+ */
14
+
15
+ import { readFileSync } from 'node:fs'
16
+ import { fileURLToPath } from 'node:url'
17
+ import { Language, Parser, Query } from 'web-tree-sitter'
18
+
19
+ /** One highlighted range: `capture` is a `highlights.scm` name like `keyword`. */
20
+ export type Span = {
21
+ readonly start: number
22
+ readonly end: number
23
+ readonly capture: string
24
+ }
25
+
26
+ /** Capture name (or dotted prefix) to ANSI SGR parameters, e.g. `keyword: '35'`. */
27
+ export type Theme = Readonly<Record<string, string>>
28
+
29
+ /**
30
+ * Terminal-default-friendly theme: entities (`variable`) stay uncolored on
31
+ * purpose — they are the bulk of every line; color carries the structure.
32
+ */
33
+ export const defaultTheme: Theme = {
34
+ comment: '90',
35
+ keyword: '35',
36
+ 'keyword.operator': '95',
37
+ property: '33',
38
+ number: '36',
39
+ type: '36',
40
+ string: '32',
41
+ 'string.special': '96',
42
+ label: '34',
43
+ constant: '33',
44
+ operator: '91',
45
+ tag: '94',
46
+ punctuation: '90'
47
+ }
48
+
49
+ export type Highlighter = {
50
+ /** Non-overlapping capture spans of `text`, in document order. */
51
+ readonly spans: (text: string) => readonly Span[]
52
+ /** `text` with ANSI colors applied per `theme` (default {@link defaultTheme}). */
53
+ readonly ansi: (text: string, theme?: Theme) => string
54
+ }
55
+
56
+ const resolvePath = (specifier: string): string =>
57
+ fileURLToPath(import.meta.resolve(specifier))
58
+
59
+ /** Longest dotted prefix of `capture` present in `theme`. */
60
+ const styleOf = (theme: Theme, capture: string): undefined | string => {
61
+ for (let name = capture; ; name = name.slice(0, name.lastIndexOf('.'))) {
62
+ const style = theme[name]
63
+ if (style !== undefined) {
64
+ return style
65
+ }
66
+ if (!name.includes('.')) {
67
+ return undefined
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Renders `spans` over `text` as ANSI; exported for custom span sources. */
73
+ export const paint = (text: string, spans: readonly Span[], theme: Theme = defaultTheme): string => {
74
+ let out = ''
75
+ let at = 0
76
+ for (const span of spans) {
77
+ const style = styleOf(theme, span.capture)
78
+ out += text.slice(at, span.start)
79
+ const piece = text.slice(span.start, span.end)
80
+ out += style === undefined ? piece : `\u001B[${style}m${piece}\u001B[0m`
81
+ at = span.end
82
+ }
83
+ return out + text.slice(at)
84
+ }
85
+
86
+ const create = async (): Promise<Highlighter> => {
87
+ await Parser.init()
88
+ const language = await Language.load(resolvePath('@cavelang/tree-sitter-cave/wasm'))
89
+ const query = new Query(language, readFileSync(resolvePath('@cavelang/tree-sitter-cave/highlights'), 'utf8'))
90
+ const parser = new Parser()
91
+ parser.setLanguage(language)
92
+ const spans = (text: string): readonly Span[] => {
93
+ const tree = parser.parse(text)
94
+ if (tree === null) {
95
+ return []
96
+ }
97
+ try {
98
+ const all = query.captures(tree.rootNode)
99
+ .map(({ name, node }) => ({ start: node.startIndex, end: node.endIndex, capture: name }))
100
+ .filter(span => span.end > span.start)
101
+ .sort((a, b) => a.start - b.start || b.end - a.end)
102
+ // One capture per node keeps these disjoint already; guard anyway so a
103
+ // future query with nested captures degrades to outermost-wins.
104
+ const disjoint: Span[] = []
105
+ let at = 0
106
+ for (const span of all) {
107
+ if (span.start >= at) {
108
+ disjoint.push(span)
109
+ at = span.end
110
+ }
111
+ }
112
+ return disjoint
113
+ } finally {
114
+ tree.delete()
115
+ }
116
+ }
117
+ return {
118
+ spans,
119
+ ansi: (text, theme = defaultTheme) => paint(text, spans(text), theme)
120
+ }
121
+ }
122
+
123
+ let cached: undefined | Promise<Highlighter>
124
+
125
+ /** The process-wide highlighter; loads the grammar WASM on first call. */
126
+ export const highlighter = (): Promise<Highlighter> =>
127
+ cached ??= create()