@cavelang/highlight 0.34.0 → 0.36.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 +38 -1
- package/dist/src/browser.d.ts +2 -2
- package/dist/src/browser.d.ts.map +1 -1
- package/dist/src/browser.js.map +1 -1
- package/dist/src/core.d.ts +6 -1
- package/dist/src/core.d.ts.map +1 -1
- package/dist/src/core.js +65 -10
- package/dist/src/core.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -3
- package/dist/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/browser.ts +2 -2
- package/src/core.ts +54 -11
- package/src/index.ts +7 -3
package/README.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
CAVE syntax highlighting for terminals — web-tree-sitter over the
|
|
4
4
|
[`@cavelang/tree-sitter-cave`](../tree-sitter-cave) grammar, colored by the
|
|
5
5
|
grammar's own `queries/highlights.scm` so terminal output and editor
|
|
6
|
-
highlighting share one source of truth.
|
|
6
|
+
highlighting share one source of truth. Explicit full-claim `@claim`
|
|
7
|
+
markers use keyword highlighting; reserved and numeric-looking subjects remain
|
|
8
|
+
entities, while a
|
|
9
|
+
trailing `@claim` remains a context label.
|
|
7
10
|
|
|
8
11
|
```ts
|
|
9
12
|
import { highlighter } from '@cavelang/highlight'
|
|
@@ -21,3 +24,37 @@ process.stdout.write(ansi('auth/middleware USES jwt @ 90% #security\n'))
|
|
|
21
24
|
|
|
22
25
|
The default theme leaves entities uncolored — they are the bulk of every
|
|
23
26
|
line; color carries the structure (verbs, values, metadata, comments).
|
|
27
|
+
Theme lookup uses only the object's own entries, with the longest dotted prefix
|
|
28
|
+
winning; inherited properties never supply styles. Custom `paint` spans use
|
|
29
|
+
JavaScript UTF-16 offsets and must be ordered, non-overlapping integer ranges
|
|
30
|
+
within `0..text.length`, with `start <= end`. Adjacent and empty ranges are
|
|
31
|
+
allowed. Invalid ranges throw `TypeError` rather than silently duplicating or
|
|
32
|
+
truncating source text. The grammar-backed `spans()` already supplies valid
|
|
33
|
+
ordered ranges.
|
|
34
|
+
`paint()` captures the supplied range values before looking up theme styles.
|
|
35
|
+
Theme getters cannot rewrite those validated ranges or extend the in-progress
|
|
36
|
+
render by changing the caller's span array.
|
|
37
|
+
|
|
38
|
+
`highlighter()` shares one in-flight or successful initialization per process.
|
|
39
|
+
A failed initialization rejects its callers and clears that cache, allowing a
|
|
40
|
+
later call to retry after the underlying problem is repaired. It does not retry
|
|
41
|
+
automatically or create a retry loop.
|
|
42
|
+
|
|
43
|
+
The shared factory releases its compiled query and allocated parser if parser
|
|
44
|
+
setup fails. Per-call parse trees are released even when capture extraction
|
|
45
|
+
throws. `createHighlighter(language, querySource)` and
|
|
46
|
+
`createBrowserHighlighter(options)` return an `OwnedHighlighter`. Call its
|
|
47
|
+
`close()` when finished to release the parser and compiled query. Closing twice
|
|
48
|
+
is harmless; `spans()` and `ansi()` throw after closure. The initialized language
|
|
49
|
+
and WASM runtime are not unloaded.
|
|
50
|
+
|
|
51
|
+
When setup or capture fails and releasing its resources also fails, an
|
|
52
|
+
`AggregateError` retains the original error as its cause and first entry and
|
|
53
|
+
the cleanup error as its second entry. Multiple owned-resource cleanup failures
|
|
54
|
+
are retained in parser, then query order. Single errors propagate unchanged;
|
|
55
|
+
every resource release is attempted, and `close()` never retries failed cleanup.
|
|
56
|
+
|
|
57
|
+
`highlighter()` returns the shared `Highlighter` interface without `close()`;
|
|
58
|
+
its parser and query remain available for the process lifetime. The website's
|
|
59
|
+
cached browser highlighter likewise lives for the page lifetime. Components
|
|
60
|
+
borrowing that cache must not close it on unmount.
|
package/dist/src/browser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Browser entry point for CAVE's Tree-sitter highlighter. */
|
|
2
|
-
import { type
|
|
2
|
+
import { type OwnedHighlighter } from './core.ts';
|
|
3
3
|
export * from './core.ts';
|
|
4
4
|
export type BrowserHighlighterOptions = {
|
|
5
5
|
/** URL emitted for `web-tree-sitter/web-tree-sitter.wasm`. */
|
|
@@ -10,5 +10,5 @@ export type BrowserHighlighterOptions = {
|
|
|
10
10
|
readonly querySource: string;
|
|
11
11
|
};
|
|
12
12
|
/** Loads the parser and CAVE grammar WASM, then creates a browser highlighter. */
|
|
13
|
-
export declare const createBrowserHighlighter: ({ parserWasmUrl, languageWasmUrl, querySource, }: BrowserHighlighterOptions) => Promise<
|
|
13
|
+
export declare const createBrowserHighlighter: ({ parserWasmUrl, languageWasmUrl, querySource, }: BrowserHighlighterOptions) => Promise<OwnedHighlighter>;
|
|
14
14
|
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAG9D,OAAO,EAAqB,KAAK,
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAG9D,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAEpE,cAAc,WAAW,CAAA;AAEzB,MAAM,MAAM,yBAAyB,GAAG;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,yDAAyD;IACzD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,wBAAwB,GAAU,kDAI5C,yBAAyB,KAAG,OAAO,CAAC,gBAAgB,CAItD,CAAA"}
|
package/dist/src/browser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAyB,MAAM,WAAW,CAAA;AAEpE,cAAc,WAAW,CAAA;AAWzB,kFAAkF;AAClF,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,EAAE,EAC7C,aAAa,EACb,eAAe,EACf,WAAW,GACe,EAA6B,EAAE;IACzD,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,CAAA;IACtD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;IACrD,OAAO,iBAAiB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;AACjD,CAAC,CAAA"}
|
package/dist/src/core.d.ts
CHANGED
|
@@ -18,8 +18,13 @@ export type Highlighter = {
|
|
|
18
18
|
/** `text` with ANSI colors applied per `theme` (default {@link defaultTheme}). */
|
|
19
19
|
readonly ansi: (text: string, theme?: Theme) => string;
|
|
20
20
|
};
|
|
21
|
+
/** A factory-created highlighter whose parser and query belong to the caller. */
|
|
22
|
+
export type OwnedHighlighter = Highlighter & {
|
|
23
|
+
/** Release the parser and query once; subsequent spans/ansi calls throw. */
|
|
24
|
+
readonly close: () => void;
|
|
25
|
+
};
|
|
21
26
|
/** Renders `spans` over `text` as ANSI; exported for custom span sources. */
|
|
22
27
|
export declare const paint: (text: string, spans: readonly Span[], theme?: Theme) => string;
|
|
23
28
|
/** Creates a highlighter from an initialized Tree-sitter language and query. */
|
|
24
|
-
export declare const createHighlighter: (language: Language, querySource: string) =>
|
|
29
|
+
export declare const createHighlighter: (language: Language, querySource: string) => OwnedHighlighter;
|
|
25
30
|
//# sourceMappingURL=core.d.ts.map
|
package/dist/src/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE9D,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;AAeD,6EAA6E;AAC7E,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,EAAE,OAAO,SAAS,IAAI,EAAE,EAAE,QAAO,KAAoB,KAAG,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE9D,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;AAED,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG;IAC3C,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AAeD,6EAA6E;AAC7E,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,EAAE,OAAO,SAAS,IAAI,EAAE,EAAE,QAAO,KAAoB,KAAG,MAgBzF,CAAA;AAeD,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,GAAI,UAAU,QAAQ,EAAE,aAAa,MAAM,KAAG,gBAoD3E,CAAA"}
|
package/dist/src/core.js
CHANGED
|
@@ -21,7 +21,7 @@ export const defaultTheme = {
|
|
|
21
21
|
/** Longest dotted prefix of `capture` present in `theme`. */
|
|
22
22
|
const styleOf = (theme, capture) => {
|
|
23
23
|
for (let name = capture;; name = name.slice(0, name.lastIndexOf('.'))) {
|
|
24
|
-
const style = theme[name];
|
|
24
|
+
const style = Object.hasOwn(theme, name) ? theme[name] : undefined;
|
|
25
25
|
if (style !== undefined) {
|
|
26
26
|
return style;
|
|
27
27
|
}
|
|
@@ -32,9 +32,14 @@ const styleOf = (theme, capture) => {
|
|
|
32
32
|
};
|
|
33
33
|
/** Renders `spans` over `text` as ANSI; exported for custom span sources. */
|
|
34
34
|
export const paint = (text, spans, theme = defaultTheme) => {
|
|
35
|
+
const ranges = spans.map(({ start, end, capture }) => ({ start, end, capture }));
|
|
35
36
|
let out = '';
|
|
36
37
|
let at = 0;
|
|
37
|
-
for (const span of
|
|
38
|
+
for (const span of ranges) {
|
|
39
|
+
if (!Number.isSafeInteger(span.start) || !Number.isSafeInteger(span.end) ||
|
|
40
|
+
span.start < at || span.end < span.start || span.end > text.length) {
|
|
41
|
+
throw new TypeError('Highlight spans must be ordered, non-overlapping integer ranges within the source');
|
|
42
|
+
}
|
|
38
43
|
const style = styleOf(theme, span.capture);
|
|
39
44
|
out += text.slice(at, span.start);
|
|
40
45
|
const piece = text.slice(span.start, span.end);
|
|
@@ -43,17 +48,64 @@ export const paint = (text, spans, theme = defaultTheme) => {
|
|
|
43
48
|
}
|
|
44
49
|
return out + text.slice(at);
|
|
45
50
|
};
|
|
51
|
+
/** Release an owned resource without replacing a primary failure. */
|
|
52
|
+
const withCleanup = (body, cleanup, message) => {
|
|
53
|
+
let result;
|
|
54
|
+
try {
|
|
55
|
+
result = body();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
try {
|
|
59
|
+
cleanup();
|
|
60
|
+
}
|
|
61
|
+
catch (cleanupError) {
|
|
62
|
+
throw new AggregateError([error, cleanupError], message, { cause: error });
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
cleanup();
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
46
69
|
/** Creates a highlighter from an initialized Tree-sitter language and query. */
|
|
47
70
|
export const createHighlighter = (language, querySource) => {
|
|
48
71
|
const query = new Query(language, querySource);
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
let parser;
|
|
73
|
+
const release = () => {
|
|
74
|
+
const errors = [];
|
|
75
|
+
try {
|
|
76
|
+
parser?.delete();
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
errors.push(error);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
query.delete();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
errors.push(error);
|
|
86
|
+
}
|
|
87
|
+
if (errors.length === 1)
|
|
88
|
+
throw errors[0];
|
|
89
|
+
if (errors.length > 1) {
|
|
90
|
+
throw new AggregateError(errors, 'Highlighter cleanup failed', { cause: errors[0] });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
parser = new Parser();
|
|
95
|
+
parser.setLanguage(language);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
withCleanup(() => { throw error; }, release, 'Highlighter setup failed and cleanup also failed');
|
|
99
|
+
}
|
|
100
|
+
let closed = false;
|
|
51
101
|
const spans = (text) => {
|
|
102
|
+
if (closed)
|
|
103
|
+
throw new Error('Highlighter is closed');
|
|
52
104
|
const tree = parser.parse(text);
|
|
53
105
|
if (tree === null) {
|
|
54
106
|
return [];
|
|
55
107
|
}
|
|
56
|
-
|
|
108
|
+
return withCleanup(() => {
|
|
57
109
|
const all = query.captures(tree.rootNode)
|
|
58
110
|
.map(({ name, node }) => ({ start: node.startIndex, end: node.endIndex, capture: name }))
|
|
59
111
|
.filter(span => span.end > span.start)
|
|
@@ -69,14 +121,17 @@ export const createHighlighter = (language, querySource) => {
|
|
|
69
121
|
}
|
|
70
122
|
}
|
|
71
123
|
return disjoint;
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
tree.delete();
|
|
75
|
-
}
|
|
124
|
+
}, () => tree.delete(), 'Highlight capture failed and tree cleanup also failed');
|
|
76
125
|
};
|
|
77
126
|
return {
|
|
78
127
|
spans,
|
|
79
|
-
ansi: (text, theme = defaultTheme) => paint(text, spans(text), theme)
|
|
128
|
+
ansi: (text, theme = defaultTheme) => paint(text, spans(text), theme),
|
|
129
|
+
close: () => {
|
|
130
|
+
if (closed)
|
|
131
|
+
return;
|
|
132
|
+
closed = true;
|
|
133
|
+
release();
|
|
134
|
+
}
|
|
80
135
|
};
|
|
81
136
|
};
|
|
82
137
|
//# sourceMappingURL=core.js.map
|
package/dist/src/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAiB,MAAM,iBAAiB,CAAA;AAY9D;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAU;IACjC,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,IAAI;IACb,kBAAkB,EAAE,IAAI;IACxB,QAAQ,EAAE,IAAI;IACd,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,IAAI;IACV,MAAM,EAAE,IAAI;IACZ,gBAAgB,EAAE,IAAI;IACtB,KAAK,EAAE,IAAI;IACX,QAAQ,EAAE,IAAI;IACd,QAAQ,EAAE,IAAI;IACd,GAAG,EAAE,IAAI;IACT,WAAW,EAAE,IAAI;CAClB,CAAA;
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type Highlighter } from './core.ts';
|
|
15
15
|
export * from './core.ts';
|
|
16
|
-
/** The process-wide highlighter; loads
|
|
16
|
+
/** The process-wide highlighter; failed loads may be retried by later calls. */
|
|
17
17
|
export declare const highlighter: () => Promise<Highlighter>;
|
|
18
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAE/D,cAAc,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAE/D,cAAc,WAAW,CAAA;AAczB,gFAAgF;AAChF,eAAO,MAAM,WAAW,QAAO,OAAO,CAAC,WAAW,CAI9C,CAAA"}
|
package/dist/src/index.js
CHANGED
|
@@ -20,9 +20,13 @@ const resolvePath = (specifier) => fileURLToPath(import.meta.resolve(specifier))
|
|
|
20
20
|
const create = async () => {
|
|
21
21
|
await Parser.init();
|
|
22
22
|
const language = await Language.load(resolvePath('@cavelang/tree-sitter-cave/wasm'));
|
|
23
|
-
|
|
23
|
+
const { spans, ansi } = createHighlighter(language, readFileSync(resolvePath('@cavelang/tree-sitter-cave/highlights'), 'utf8'));
|
|
24
|
+
return { spans, ansi };
|
|
24
25
|
};
|
|
25
26
|
let cached;
|
|
26
|
-
/** The process-wide highlighter; loads
|
|
27
|
-
export const highlighter = () => cached ??= create()
|
|
27
|
+
/** The process-wide highlighter; failed loads may be retried by later calls. */
|
|
28
|
+
export const highlighter = () => cached ??= create().catch(error => {
|
|
29
|
+
cached = undefined;
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
28
32
|
//# sourceMappingURL=index.js.map
|
package/dist/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAoB,MAAM,WAAW,CAAA;AAE/D,cAAc,WAAW,CAAA;AAEzB,MAAM,WAAW,GAAG,CAAC,SAAiB,EAAU,EAAE,CAChD,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAA;AAE/C,MAAM,MAAM,GAAG,KAAK,IAA0B,EAAE;IAC9C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;IACnB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,iCAAiC,CAAC,CAAC,CAAA;IACpF,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAoB,MAAM,WAAW,CAAA;AAE/D,cAAc,WAAW,CAAA;AAEzB,MAAM,WAAW,GAAG,CAAC,SAAiB,EAAU,EAAE,CAChD,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAA;AAE/C,MAAM,MAAM,GAAG,KAAK,IAA0B,EAAE;IAC9C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;IACnB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,iCAAiC,CAAC,CAAC,CAAA;IACpF,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,QAAQ,EAAE,YAAY,CAAC,WAAW,CAAC,uCAAuC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;IAC/H,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;AACxB,CAAC,CAAA;AAED,IAAI,MAAwC,CAAA;AAE5C,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,GAAyB,EAAE,CACpD,MAAM,KAAK,MAAM,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;IAChC,MAAM,GAAG,SAAS,CAAA;IAClB,MAAM,KAAK,CAAA;AACb,CAAC,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavelang/highlight",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CAVE syntax highlighting — web-tree-sitter over the @cavelang/tree-sitter-cave grammar, rendered to ANSI for terminals.",
|
|
6
6
|
"keywords": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"directory": "packages/highlight"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": "^
|
|
24
|
+
"node": "^24.16.0 || ^26.1.0"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@types/emscripten": "^1.41.
|
|
51
|
-
"web-tree-sitter": "^0.
|
|
52
|
-
"@cavelang/tree-sitter-cave": "0.
|
|
50
|
+
"@types/emscripten": "^1.41.6",
|
|
51
|
+
"web-tree-sitter": "^0.27.0",
|
|
52
|
+
"@cavelang/tree-sitter-cave": "0.36.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsc -b",
|
package/src/browser.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Browser entry point for CAVE's Tree-sitter highlighter. */
|
|
2
2
|
|
|
3
3
|
import { Language, Parser } from 'web-tree-sitter'
|
|
4
|
-
import { createHighlighter, type
|
|
4
|
+
import { createHighlighter, type OwnedHighlighter } from './core.ts'
|
|
5
5
|
|
|
6
6
|
export * from './core.ts'
|
|
7
7
|
|
|
@@ -19,7 +19,7 @@ export const createBrowserHighlighter = async ({
|
|
|
19
19
|
parserWasmUrl,
|
|
20
20
|
languageWasmUrl,
|
|
21
21
|
querySource,
|
|
22
|
-
}: BrowserHighlighterOptions): Promise<
|
|
22
|
+
}: BrowserHighlighterOptions): Promise<OwnedHighlighter> => {
|
|
23
23
|
await Parser.init({ locateFile: () => parserWasmUrl })
|
|
24
24
|
const language = await Language.load(languageWasmUrl)
|
|
25
25
|
return createHighlighter(language, querySource)
|
package/src/core.ts
CHANGED
|
@@ -37,10 +37,16 @@ export type Highlighter = {
|
|
|
37
37
|
readonly ansi: (text: string, theme?: Theme) => string
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** A factory-created highlighter whose parser and query belong to the caller. */
|
|
41
|
+
export type OwnedHighlighter = Highlighter & {
|
|
42
|
+
/** Release the parser and query once; subsequent spans/ansi calls throw. */
|
|
43
|
+
readonly close: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
/** Longest dotted prefix of `capture` present in `theme`. */
|
|
41
47
|
const styleOf = (theme: Theme, capture: string): undefined | string => {
|
|
42
48
|
for (let name = capture; ; name = name.slice(0, name.lastIndexOf('.'))) {
|
|
43
|
-
const style = theme[name]
|
|
49
|
+
const style = Object.hasOwn(theme, name) ? theme[name] : undefined
|
|
44
50
|
if (style !== undefined) {
|
|
45
51
|
return style
|
|
46
52
|
}
|
|
@@ -52,9 +58,14 @@ const styleOf = (theme: Theme, capture: string): undefined | string => {
|
|
|
52
58
|
|
|
53
59
|
/** Renders `spans` over `text` as ANSI; exported for custom span sources. */
|
|
54
60
|
export const paint = (text: string, spans: readonly Span[], theme: Theme = defaultTheme): string => {
|
|
61
|
+
const ranges = spans.map(({ start, end, capture }) => ({ start, end, capture }))
|
|
55
62
|
let out = ''
|
|
56
63
|
let at = 0
|
|
57
|
-
for (const span of
|
|
64
|
+
for (const span of ranges) {
|
|
65
|
+
if (!Number.isSafeInteger(span.start) || !Number.isSafeInteger(span.end) ||
|
|
66
|
+
span.start < at || span.end < span.start || span.end > text.length) {
|
|
67
|
+
throw new TypeError('Highlight spans must be ordered, non-overlapping integer ranges within the source')
|
|
68
|
+
}
|
|
58
69
|
const style = styleOf(theme, span.capture)
|
|
59
70
|
out += text.slice(at, span.start)
|
|
60
71
|
const piece = text.slice(span.start, span.end)
|
|
@@ -64,17 +75,46 @@ export const paint = (text: string, spans: readonly Span[], theme: Theme = defau
|
|
|
64
75
|
return out + text.slice(at)
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
/** Release an owned resource without replacing a primary failure. */
|
|
79
|
+
const withCleanup = <T>(body: () => T, cleanup: () => void, message: string): T => {
|
|
80
|
+
let result: T
|
|
81
|
+
try { result = body() } catch (error) {
|
|
82
|
+
try { cleanup() } catch (cleanupError) {
|
|
83
|
+
throw new AggregateError([error, cleanupError], message, { cause: error })
|
|
84
|
+
}
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
cleanup()
|
|
88
|
+
return result
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
/** Creates a highlighter from an initialized Tree-sitter language and query. */
|
|
68
|
-
export const createHighlighter = (language: Language, querySource: string):
|
|
92
|
+
export const createHighlighter = (language: Language, querySource: string): OwnedHighlighter => {
|
|
69
93
|
const query = new Query(language, querySource)
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
let parser: Parser | undefined
|
|
95
|
+
const release = (): void => {
|
|
96
|
+
const errors: unknown[] = []
|
|
97
|
+
try { parser?.delete() } catch (error) { errors.push(error) }
|
|
98
|
+
try { query.delete() } catch (error) { errors.push(error) }
|
|
99
|
+
if (errors.length === 1) throw errors[0]
|
|
100
|
+
if (errors.length > 1) {
|
|
101
|
+
throw new AggregateError(errors, 'Highlighter cleanup failed', { cause: errors[0] })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
parser = new Parser()
|
|
106
|
+
parser.setLanguage(language)
|
|
107
|
+
} catch (error) {
|
|
108
|
+
withCleanup(() => { throw error }, release, 'Highlighter setup failed and cleanup also failed')
|
|
109
|
+
}
|
|
110
|
+
let closed = false
|
|
72
111
|
const spans = (text: string): readonly Span[] => {
|
|
73
|
-
|
|
112
|
+
if (closed) throw new Error('Highlighter is closed')
|
|
113
|
+
const tree = parser!.parse(text)
|
|
74
114
|
if (tree === null) {
|
|
75
115
|
return []
|
|
76
116
|
}
|
|
77
|
-
|
|
117
|
+
return withCleanup(() => {
|
|
78
118
|
const all = query.captures(tree.rootNode)
|
|
79
119
|
.map(({ name, node }) => ({ start: node.startIndex, end: node.endIndex, capture: name }))
|
|
80
120
|
.filter(span => span.end > span.start)
|
|
@@ -90,12 +130,15 @@ export const createHighlighter = (language: Language, querySource: string): High
|
|
|
90
130
|
}
|
|
91
131
|
}
|
|
92
132
|
return disjoint
|
|
93
|
-
}
|
|
94
|
-
tree.delete()
|
|
95
|
-
}
|
|
133
|
+
}, () => tree.delete(), 'Highlight capture failed and tree cleanup also failed')
|
|
96
134
|
}
|
|
97
135
|
return {
|
|
98
136
|
spans,
|
|
99
|
-
ansi: (text, theme = defaultTheme) => paint(text, spans(text), theme)
|
|
137
|
+
ansi: (text, theme = defaultTheme) => paint(text, spans(text), theme),
|
|
138
|
+
close: () => {
|
|
139
|
+
if (closed) return
|
|
140
|
+
closed = true
|
|
141
|
+
release()
|
|
142
|
+
}
|
|
100
143
|
}
|
|
101
144
|
}
|
package/src/index.ts
CHANGED
|
@@ -25,11 +25,15 @@ const resolvePath = (specifier: string): string =>
|
|
|
25
25
|
const create = async (): Promise<Highlighter> => {
|
|
26
26
|
await Parser.init()
|
|
27
27
|
const language = await Language.load(resolvePath('@cavelang/tree-sitter-cave/wasm'))
|
|
28
|
-
|
|
28
|
+
const { spans, ansi } = createHighlighter(language, readFileSync(resolvePath('@cavelang/tree-sitter-cave/highlights'), 'utf8'))
|
|
29
|
+
return { spans, ansi }
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
let cached: undefined | Promise<Highlighter>
|
|
32
33
|
|
|
33
|
-
/** The process-wide highlighter; loads
|
|
34
|
+
/** The process-wide highlighter; failed loads may be retried by later calls. */
|
|
34
35
|
export const highlighter = (): Promise<Highlighter> =>
|
|
35
|
-
cached ??= create()
|
|
36
|
+
cached ??= create().catch(error => {
|
|
37
|
+
cached = undefined
|
|
38
|
+
throw error
|
|
39
|
+
})
|