@peteanderson/ansi 0.2.1 → 0.2.3
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 +13 -3
- package/dist/ansi.d.ts +4 -2
- package/dist/ansi.js +5 -1
- package/dist/index.d.ts +3 -1
- package/dist/patterns.d.ts +3 -0
- package/dist/patterns.js +43 -0
- package/dist/simplify/atom.d.ts +3 -0
- package/dist/simplify/atom.js +28 -0
- package/dist/simplify/index.d.ts +2 -0
- package/dist/simplify/index.js +2 -0
- package/dist/simplify/lookup.d.ts +5 -0
- package/dist/simplify/lookup.js +77 -0
- package/dist/simplify/simplify.d.ts +1 -0
- package/dist/simplify/simplify.js +25 -0
- package/dist/simplify/state.d.ts +10 -0
- package/dist/simplify/state.js +57 -0
- package/dist/simplify/utils.d.ts +2 -0
- package/dist/simplify/utils.js +31 -0
- package/dist/slice.d.ts +1 -0
- package/dist/slice.js +83 -0
- package/dist/strip.d.ts +5 -1
- package/dist/strip.js +9 -49
- package/package.json +1 -1
- package/src/ansi.js +9 -5
- package/src/patterns.js +44 -0
- package/src/simplify/atom.js +34 -0
- package/src/simplify/index.js +1 -0
- package/src/simplify/lookup.js +86 -0
- package/src/simplify/simplify.js +33 -0
- package/src/simplify/state.js +73 -0
- package/src/simplify/types.d.ts +28 -0
- package/src/simplify/utils.js +41 -0
- package/src/slice.js +99 -0
- package/src/strip.js +10 -56
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ console.log(ansi.bg.blue.combine(ansi.fg.white)("highlighted"));
|
|
|
18
18
|
console.log(ansi.style.bold("important"));
|
|
19
19
|
console.log(ansi.fg.rgb(5, 2, 0)("custom color"));
|
|
20
20
|
console.log(ansi.strip("styled text")); // remove all ANSI codes
|
|
21
|
+
console.log(ansi.visibleLength(styledText)); // get display width
|
|
21
22
|
console.log(ansi.slice("styled text", 0, 5)); // slice by visible length
|
|
23
|
+
console.log(ansi.simplify(styledText)); // combine adjacent SGR sequences
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
## API
|
|
@@ -52,8 +54,7 @@ Functions that wrap text with style codes:
|
|
|
52
54
|
`bold` `dim` `italic` `underline` `inverse` `hidden` `strikethrough` `doubleUnderline` `framed`
|
|
53
55
|
`encircled` `overline`
|
|
54
56
|
|
|
55
|
-
All return functions with `open` and `close` properties for raw sequences.
|
|
56
|
-
`combine()` method for appending additional styles.
|
|
57
|
+
All return functions with `open` and `close` properties for raw sequences.
|
|
57
58
|
|
|
58
59
|
### `caret`
|
|
59
60
|
|
|
@@ -84,12 +85,21 @@ Removes all ANSI CSI sequences from a string.
|
|
|
84
85
|
### `slice(text, start, end)`
|
|
85
86
|
|
|
86
87
|
Slices a string by visible characters, ignoring ANSI sequences. Works like `String.prototype.slice`
|
|
87
|
-
but counts only visible characters.
|
|
88
|
+
but counts only visible characters. Properly maintains terminal state at slice boundaries.
|
|
88
89
|
|
|
89
90
|
### `sanitize(text)`
|
|
90
91
|
|
|
91
92
|
Removes "unsafe" CSI sequences (non-SGR), leaving only color and style codes.
|
|
92
93
|
|
|
94
|
+
### `simplify(text)`
|
|
95
|
+
|
|
96
|
+
Simplifies ANSI SGR sequences by combining adjacent sequences and removing redundant codes. Useful
|
|
97
|
+
for optimizing styled strings.
|
|
98
|
+
|
|
99
|
+
### `visibleLength(text)`
|
|
100
|
+
|
|
101
|
+
Returns the visible length of a string with all ANSI sequences removed.
|
|
102
|
+
|
|
93
103
|
## License
|
|
94
104
|
|
|
95
105
|
MIT
|
package/dist/ansi.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { style } from "./sgr/style";
|
|
|
6
6
|
import { fg } from "./sgr/color";
|
|
7
7
|
import { bg } from "./sgr/color";
|
|
8
8
|
import { strip } from "./strip";
|
|
9
|
+
import { visibleLength } from "./strip";
|
|
9
10
|
import { sanitize } from "./strip";
|
|
10
|
-
import { slice } from "./
|
|
11
|
-
|
|
11
|
+
import { slice } from "./slice";
|
|
12
|
+
import { simplify } from "./simplify/simplify";
|
|
13
|
+
export { caret, erase, scroll, reset, style, fg, bg, strip, visibleLength, sanitize, slice, simplify };
|
package/dist/ansi.js
CHANGED
|
@@ -3,7 +3,9 @@ const caret = require("./caret");
|
|
|
3
3
|
const erase = require("./erase");
|
|
4
4
|
const scroll = require("./scroll");
|
|
5
5
|
const { bg, fg, reset, style } = require("./sgr");
|
|
6
|
-
const {
|
|
6
|
+
const { simplify } = require("./simplify");
|
|
7
|
+
const { slice } = require("./slice");
|
|
8
|
+
const { sanitize, strip, visibleLength } = require("./strip");
|
|
7
9
|
const ansi = {
|
|
8
10
|
caret,
|
|
9
11
|
erase,
|
|
@@ -13,7 +15,9 @@ const ansi = {
|
|
|
13
15
|
fg,
|
|
14
16
|
bg,
|
|
15
17
|
strip,
|
|
18
|
+
visibleLength,
|
|
16
19
|
sanitize,
|
|
17
20
|
slice,
|
|
21
|
+
simplify,
|
|
18
22
|
};
|
|
19
23
|
module.exports = ansi;
|
package/dist/index.d.ts
CHANGED
|
@@ -99,7 +99,9 @@ declare const _exports: {
|
|
|
99
99
|
x256: (arg0: number, arg1?: number, arg2?: number) => (text: string) => string;
|
|
100
100
|
};
|
|
101
101
|
strip: (text: string) => string;
|
|
102
|
+
visibleLength: (text: string) => number;
|
|
102
103
|
sanitize: (text: string) => string;
|
|
103
|
-
slice: typeof import("./
|
|
104
|
+
slice: typeof import("./slice").slice;
|
|
105
|
+
simplify: typeof import("./simplify/simplify").simplify;
|
|
104
106
|
};
|
|
105
107
|
export = _exports;
|
package/dist/patterns.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
I'm freezing these to prevent accidental mutation of the `lastIndex` property, which could keep
|
|
4
|
+
future calls from starting at the beginning of the string.
|
|
5
|
+
|
|
6
|
+
Code using these objects will have to clone them (with `new RegExp(rx)`) before using functions
|
|
7
|
+
that modify `lastIndex` (such as `exec`). But according to my benchmarks, cloning the regex takes
|
|
8
|
+
mere nanoseconds, and it wasn't even measurable when operating on longer strings. V8 clearly uses a
|
|
9
|
+
global cache of compiled regexes. I assume that SpiderMonkey does the same thing.
|
|
10
|
+
|
|
11
|
+
Here are my `timeit` benchmark results:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const {timeit} = require("$timeit");
|
|
15
|
+
|
|
16
|
+
const rxSGR = /\x1b\[[\d;]*m/g;
|
|
17
|
+
const short = "\x1b[31mred \x1b[32mgreen \x1b[34mblue \x1b[0m";
|
|
18
|
+
const long = short.repeat(1000);
|
|
19
|
+
|
|
20
|
+
timeit(() => short.replace(rxSGR, ""), {description: "short - no clone "});
|
|
21
|
+
timeit(() => short.replace(new RegExp(rxSGR), ""), {description: "short - clone object "});
|
|
22
|
+
timeit(() => short.replace(new RegExp(rxSGR.source, "g"), ""), {description: "short - clone source "});
|
|
23
|
+
|
|
24
|
+
console.log();
|
|
25
|
+
|
|
26
|
+
timeit(() => long .replace(rxSGR, ""), {description: "long - no clone "});
|
|
27
|
+
timeit(() => long .replace(new RegExp(rxSGR), ""), {description: "long - clone object "});
|
|
28
|
+
timeit(() => long .replace(new RegExp(rxSGR.source, "g"), ""), {description: "long - clone source "});
|
|
29
|
+
|
|
30
|
+
// Output:
|
|
31
|
+
// short - no clone : 82.73 ns (12,087,731 in 1.000016982 s)
|
|
32
|
+
// short - clone object : 142.132 ns (7,035,692 in 1.0000001770000002 s)
|
|
33
|
+
// short - clone source : 127.872 ns (7,820,293 in 1.00000017 s)
|
|
34
|
+
//
|
|
35
|
+
// long - no clone : 47.953 ųs (20,854 in 1.0000098149999999 s)
|
|
36
|
+
// long - clone object : 46.713 ųs (21,408 in 1.0000285520000003 s)
|
|
37
|
+
// long - clone source : 46.491 ųs (21,510 in 1.000026492 s)
|
|
38
|
+
```
|
|
39
|
+
*/
|
|
40
|
+
const rxSGR = Object.freeze(/\x1b\[[\d;]*m/g);
|
|
41
|
+
const rxCSI = Object.freeze(/\x1b\[[0-?]*[ -/]*[@-~]/g);
|
|
42
|
+
const rxUnsafe = Object.freeze(/\x1b\[[0-?]*[ -/]*[@-ln-~]/g); // Everything except SGR codes (m).
|
|
43
|
+
module.exports = { rxSGR, rxCSI, rxUnsafe };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type BuildAtom = import("./types").BuildAtom;
|
|
2
|
+
export function buildExtendedColorAtom(attribute: import("./types").Attribute, codes: number[], index: number, state: import("./types").AttributeMap): import("./types").Atom;
|
|
3
|
+
export function buildIntensityAtom(attribute: import("./types").Attribute, codes: number[], index: number, state: import("./types").AttributeMap): import("./types").Atom;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** @typedef {import("./types").BuildAtom} BuildAtom */
|
|
3
|
+
/** @type {BuildAtom} */
|
|
4
|
+
function buildExtendedColorAtom(attribute, codes, index) {
|
|
5
|
+
switch (codes[index + 1]) {
|
|
6
|
+
case 2:
|
|
7
|
+
if (index + 5 >= codes.length)
|
|
8
|
+
return { attribute, code: codes.slice(index), skip: codes.length - index - 1 };
|
|
9
|
+
return { attribute, code: codes.slice(index, index + 5), skip: 4 };
|
|
10
|
+
case 5:
|
|
11
|
+
if (index + 3 >= codes.length)
|
|
12
|
+
return { attribute, code: codes.slice(index), skip: codes.length - index - 1 };
|
|
13
|
+
return { attribute, code: codes.slice(index, index + 3), skip: 2 };
|
|
14
|
+
default:
|
|
15
|
+
return { attribute, code: codes.slice(index, index + 2), skip: 1 };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** @type {BuildAtom} */
|
|
19
|
+
function buildIntensityAtom(attribute, codes, index, state) {
|
|
20
|
+
const code = codes[index];
|
|
21
|
+
if (code === 22)
|
|
22
|
+
return { attribute, code: [code] };
|
|
23
|
+
const current = state.get("intensity");
|
|
24
|
+
if (!current || current.length == 1 && current[0] === code || current[0] === 22)
|
|
25
|
+
return { attribute, code: [code] };
|
|
26
|
+
return { attribute, code: [1, 2] };
|
|
27
|
+
}
|
|
28
|
+
module.exports = { buildExtendedColorAtom, buildIntensityAtom };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type Atom = import("./types").Atom;
|
|
2
|
+
export type Attribute = import("./types").Attribute;
|
|
3
|
+
export type AttributeMap = import("./types").AttributeMap;
|
|
4
|
+
export type BuildAtom = import("./types").BuildAtom;
|
|
5
|
+
export function lookUpAtom(codes: number[], index: number, state: AttributeMap): Atom;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { buildExtendedColorAtom, buildIntensityAtom } = require("./atom");
|
|
3
|
+
/** @typedef {import("./types").Atom} Atom */
|
|
4
|
+
/** @typedef {import("./types").Attribute} Attribute */
|
|
5
|
+
/** @typedef {import("./types").AttributeMap} AttributeMap */
|
|
6
|
+
/** @typedef {import("./types").BuildAtom} BuildAtom */
|
|
7
|
+
/** @type {Record<number, [attribute: Attribute, buildAtom?: BuildAtom]>} */
|
|
8
|
+
const codeMap = {
|
|
9
|
+
0: ["reset"],
|
|
10
|
+
1: ["intensity", buildIntensityAtom],
|
|
11
|
+
2: ["intensity", buildIntensityAtom],
|
|
12
|
+
3: ["italic"],
|
|
13
|
+
4: ["underline"],
|
|
14
|
+
7: ["inverse"],
|
|
15
|
+
8: ["hidden"],
|
|
16
|
+
9: ["strikethrough"],
|
|
17
|
+
21: ["underline"],
|
|
18
|
+
22: ["intensity", buildIntensityAtom],
|
|
19
|
+
23: ["italic"],
|
|
20
|
+
24: ["underline"],
|
|
21
|
+
27: ["inverse"],
|
|
22
|
+
28: ["hidden"],
|
|
23
|
+
29: ["strikethrough"],
|
|
24
|
+
30: ["fg"],
|
|
25
|
+
31: ["fg"],
|
|
26
|
+
32: ["fg"],
|
|
27
|
+
33: ["fg"],
|
|
28
|
+
34: ["fg"],
|
|
29
|
+
35: ["fg"],
|
|
30
|
+
36: ["fg"],
|
|
31
|
+
37: ["fg"],
|
|
32
|
+
38: ["fg", buildExtendedColorAtom],
|
|
33
|
+
39: ["fg"],
|
|
34
|
+
40: ["bg"],
|
|
35
|
+
41: ["bg"],
|
|
36
|
+
42: ["bg"],
|
|
37
|
+
43: ["bg"],
|
|
38
|
+
44: ["bg"],
|
|
39
|
+
45: ["bg"],
|
|
40
|
+
46: ["bg"],
|
|
41
|
+
47: ["bg"],
|
|
42
|
+
48: ["bg", buildExtendedColorAtom],
|
|
43
|
+
49: ["bg"],
|
|
44
|
+
51: ["frame"],
|
|
45
|
+
52: ["frame"],
|
|
46
|
+
53: ["overline"],
|
|
47
|
+
54: ["frame"],
|
|
48
|
+
55: ["overline"],
|
|
49
|
+
90: ["fg"],
|
|
50
|
+
91: ["fg"],
|
|
51
|
+
92: ["fg"],
|
|
52
|
+
93: ["fg"],
|
|
53
|
+
94: ["fg"],
|
|
54
|
+
95: ["fg"],
|
|
55
|
+
96: ["fg"],
|
|
56
|
+
97: ["fg"],
|
|
57
|
+
100: ["bg"],
|
|
58
|
+
101: ["bg"],
|
|
59
|
+
102: ["bg"],
|
|
60
|
+
103: ["bg"],
|
|
61
|
+
104: ["bg"],
|
|
62
|
+
105: ["bg"],
|
|
63
|
+
106: ["bg"],
|
|
64
|
+
107: ["bg"],
|
|
65
|
+
};
|
|
66
|
+
/** @type {(codes: number[], index: number, state: AttributeMap) => Atom} */
|
|
67
|
+
function lookUpAtom(codes, index, state) {
|
|
68
|
+
const code = codes[index];
|
|
69
|
+
const entry = codeMap[code];
|
|
70
|
+
if (!entry)
|
|
71
|
+
return { attribute: code, code: [code] };
|
|
72
|
+
const [attribute, buildAtom] = entry;
|
|
73
|
+
if (buildAtom)
|
|
74
|
+
return buildAtom(attribute, codes, index, state);
|
|
75
|
+
return { attribute, code: [code] };
|
|
76
|
+
}
|
|
77
|
+
module.exports = { lookUpAtom };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function simplify(text: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { rxSGR } = require("../patterns");
|
|
3
|
+
const { State } = require("./state");
|
|
4
|
+
/** @type {(text: string) => string} */
|
|
5
|
+
function simplify(text) {
|
|
6
|
+
// `.split` doesn't modify `lastIndex`, so no clone is necessary here.
|
|
7
|
+
const parts = text.split(rxSGR);
|
|
8
|
+
const state = new State();
|
|
9
|
+
let result = parts[0];
|
|
10
|
+
for (let i = 1; i < parts.length;) {
|
|
11
|
+
let codes = parts[i++].split(";").map(Number);
|
|
12
|
+
let chunk = parts[i++];
|
|
13
|
+
while (!chunk && i < parts.length) {
|
|
14
|
+
// Merge consecutive SGR sequences.
|
|
15
|
+
codes.push(...parts[i++].split(";").map(Number));
|
|
16
|
+
chunk = parts[i++];
|
|
17
|
+
}
|
|
18
|
+
codes = state.update(codes);
|
|
19
|
+
if (codes.length)
|
|
20
|
+
result += `\x1b[${codes.join(";")}m`;
|
|
21
|
+
result += chunk;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
module.exports = { simplify };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Attribute = import("./types").Attribute;
|
|
2
|
+
export type AttributeMap = import("./types").AttributeMap;
|
|
3
|
+
/** @type {AttributeMap} */
|
|
4
|
+
export class State extends Map<any, any> {
|
|
5
|
+
constructor();
|
|
6
|
+
constructor(entries?: readonly (readonly [any, any])[] | null | undefined);
|
|
7
|
+
constructor();
|
|
8
|
+
constructor(iterable?: Iterable<readonly [any, any]> | null | undefined);
|
|
9
|
+
update(codes: number[]): number[];
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { lookUpAtom } = require("./lookup");
|
|
3
|
+
const { arraysEqual, transitionIntensity } = require("./utils");
|
|
4
|
+
/** @typedef {import("./types").Attribute} Attribute */
|
|
5
|
+
/** @typedef {import("./types").AttributeMap} AttributeMap */
|
|
6
|
+
/** @type {Map<Attribute, number[]>} */
|
|
7
|
+
const defaultCodes = new Map([
|
|
8
|
+
["intensity", [22]],
|
|
9
|
+
["italic", [23]],
|
|
10
|
+
["underline", [24]],
|
|
11
|
+
["inverse", [27]],
|
|
12
|
+
["hidden", [28]],
|
|
13
|
+
["strikethrough", [29]],
|
|
14
|
+
["fg", [39]],
|
|
15
|
+
["bg", [49]],
|
|
16
|
+
["frame", [54]],
|
|
17
|
+
["overline", [55]],
|
|
18
|
+
]);
|
|
19
|
+
/** @type {AttributeMap} */
|
|
20
|
+
class State extends Map {
|
|
21
|
+
/** @type {(codes: number[]) => number[]} */
|
|
22
|
+
update(codes) {
|
|
23
|
+
/** @type {AttributeMap} */
|
|
24
|
+
let snapshot = new Map(this);
|
|
25
|
+
/** @type {Set<Attribute>} */
|
|
26
|
+
const attributes = new Set();
|
|
27
|
+
for (let i = 0; i < codes.length; i++) {
|
|
28
|
+
const { attribute, code, skip } = lookUpAtom(codes, i, this);
|
|
29
|
+
if (attribute === "reset") {
|
|
30
|
+
attributes.clear();
|
|
31
|
+
this.clear();
|
|
32
|
+
attributes.add("reset");
|
|
33
|
+
for (const [attr, defaultCode] of defaultCodes)
|
|
34
|
+
this.set(attr, defaultCode);
|
|
35
|
+
snapshot = new Map(this);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
attributes.add(attribute);
|
|
39
|
+
this.set(attribute, code);
|
|
40
|
+
if (skip)
|
|
41
|
+
i += skip;
|
|
42
|
+
}
|
|
43
|
+
return Array.from(attributes).flatMap(attribute => {
|
|
44
|
+
if (attribute === "reset")
|
|
45
|
+
return [0];
|
|
46
|
+
const code = this.get(attribute);
|
|
47
|
+
if (arraysEqual(code, snapshot.get(attribute)))
|
|
48
|
+
return [];
|
|
49
|
+
if (attribute === "intensity") {
|
|
50
|
+
const previous = snapshot.get(attribute);
|
|
51
|
+
return transitionIntensity(previous, code);
|
|
52
|
+
}
|
|
53
|
+
return code;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
module.exports = { State };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** @type {<T>(a: T[] | undefined, b: T[] | undefined) => boolean} */
|
|
3
|
+
function arraysEqual(a, b) {
|
|
4
|
+
if (a === b)
|
|
5
|
+
return true;
|
|
6
|
+
if (!a || !b || a.length !== b.length)
|
|
7
|
+
return false;
|
|
8
|
+
for (let i = 0; i < a.length; i++) {
|
|
9
|
+
if (a[i] !== b[i])
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
/** @type {(previous: number[] | undefined, current: number[]) => number[]} */
|
|
15
|
+
function transitionIntensity(previous, current) {
|
|
16
|
+
if (!previous || previous[0] === 22 || current[0] === 22)
|
|
17
|
+
return current;
|
|
18
|
+
const oldBold = previous[0] === 1;
|
|
19
|
+
const newBold = current[0] === 1;
|
|
20
|
+
const oldDim = previous[previous.length - 1] === 2;
|
|
21
|
+
const newDim = current[current.length - 1] === 2;
|
|
22
|
+
const result = [];
|
|
23
|
+
if (oldBold && !newBold || oldDim && !newDim)
|
|
24
|
+
result.push(22);
|
|
25
|
+
if (newBold && (!oldBold || result[0] === 22))
|
|
26
|
+
result.push(1);
|
|
27
|
+
if (newDim && (!oldDim || result[0] === 22))
|
|
28
|
+
result.push(2);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
module.exports = { arraysEqual, transitionIntensity };
|
package/dist/slice.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function slice(text: string, start: number, end: number): string;
|
package/dist/slice.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { rxCSI, rxSGR } = require("./patterns");
|
|
3
|
+
const { simplify } = require("./simplify");
|
|
4
|
+
const { visibleLength } = require("./strip");
|
|
5
|
+
/**
|
|
6
|
+
@type {
|
|
7
|
+
(
|
|
8
|
+
text : string,
|
|
9
|
+
fromStringIndex : number,
|
|
10
|
+
visibleCount : number,
|
|
11
|
+
) => {index: number, length: number}
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
function scanCSI(text, fromStringIndex, visibleCount) {
|
|
15
|
+
// We intentionally mutate `lastIndex` here, so we need a clone.
|
|
16
|
+
const rx = new RegExp(rxCSI);
|
|
17
|
+
let length = 0;
|
|
18
|
+
let index = fromStringIndex;
|
|
19
|
+
while (index < text.length && length < visibleCount) {
|
|
20
|
+
if (text[index] === "\x1b") {
|
|
21
|
+
rx.lastIndex = index;
|
|
22
|
+
const match = rx.exec(text);
|
|
23
|
+
if (match?.index === index) {
|
|
24
|
+
index += match[0].length;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
index++;
|
|
29
|
+
length++;
|
|
30
|
+
}
|
|
31
|
+
return { index, length };
|
|
32
|
+
}
|
|
33
|
+
/** @type {(text: string, start: number, end?: number) => string} */
|
|
34
|
+
function skippedSequences(text, start, end) {
|
|
35
|
+
const sequences = text.slice(start, end).match(new RegExp(rxSGR));
|
|
36
|
+
return sequences ? simplify(sequences.join("")) : "";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Return a slice of the given text, counting only visible characters (i.e. ignoring ANSI escape
|
|
40
|
+
* codes). The returned slice will include any ANSI escape codes from within the slice. In addition,
|
|
41
|
+
* SGR codes (m) from before and after the slice will be combined, simplified, and included so that
|
|
42
|
+
* the terminal state before and after the slice will be the same as if the whole string were
|
|
43
|
+
* printed. If you compose two or more slices together, there may be redundant SGR sequences at the
|
|
44
|
+
* boundaries, but the result will be correct. You can use `simplify` to remove the redundancies.
|
|
45
|
+
*
|
|
46
|
+
* Note the following caveats:
|
|
47
|
+
*
|
|
48
|
+
* * Any non-SGR ANSI codes prior to or after the slice will be ignored.
|
|
49
|
+
* * Control characters (such as newlines and backspaces) both outside and inside the slice are
|
|
50
|
+
* counted as visible characters.
|
|
51
|
+
*
|
|
52
|
+
* These caveats mean that text may not appear on screen to be the same number of characters as the
|
|
53
|
+
* length of the slice, and that the terminal state may not be exactly the same at the start and end
|
|
54
|
+
* of the slice as it would be if the whole string were printed. This function is intended mostly
|
|
55
|
+
* for text that contains SGR codes, but no other ANSI codes or control characters.
|
|
56
|
+
*
|
|
57
|
+
* @type {(text: string, start: number, end: number) => string}
|
|
58
|
+
*/
|
|
59
|
+
function slice(text, start, end) {
|
|
60
|
+
/** @type {number | undefined} */
|
|
61
|
+
let textVisibleLength;
|
|
62
|
+
const getVisibleLength = () => textVisibleLength ??= visibleLength(text);
|
|
63
|
+
if (start < 0 || end < 0) {
|
|
64
|
+
textVisibleLength = getVisibleLength();
|
|
65
|
+
if (start < 0)
|
|
66
|
+
start = Math.max(0, textVisibleLength + start);
|
|
67
|
+
if (end < 0)
|
|
68
|
+
end = Math.max(0, textVisibleLength + end);
|
|
69
|
+
}
|
|
70
|
+
if (start > end)
|
|
71
|
+
return "";
|
|
72
|
+
if (start === 0 && (end >= text.length || end >= getVisibleLength()))
|
|
73
|
+
return text;
|
|
74
|
+
const { index: startIndex, length: visibleSkipped } = scanCSI(text, 0, start);
|
|
75
|
+
const endIndex = startIndex < text.length && visibleSkipped < end
|
|
76
|
+
? scanCSI(text, startIndex, end - visibleSkipped).index
|
|
77
|
+
: text.length;
|
|
78
|
+
// Start and end the string in the same terminal state it would be in if the string weren't sliced.
|
|
79
|
+
const leadingSequences = skippedSequences(text, 0, startIndex);
|
|
80
|
+
const trailingSequences = skippedSequences(text, endIndex);
|
|
81
|
+
return leadingSequences + text.slice(startIndex, endIndex) + trailingSequences;
|
|
82
|
+
}
|
|
83
|
+
module.exports = { slice };
|
package/dist/strip.d.ts
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* @type {(text: string) => string}
|
|
4
4
|
*/
|
|
5
5
|
export const strip: (text: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Calculate the visible length of a string with ANSI escape codes stripped.
|
|
8
|
+
* @type {(text: string) => number}
|
|
9
|
+
*/
|
|
10
|
+
export const visibleLength: (text: string) => number;
|
|
6
11
|
/**
|
|
7
12
|
* Remove all ANSI escape codes except SGR codes (m), which are used for color and styling.
|
|
8
13
|
* @type {(text: string) => string}
|
|
9
14
|
*/
|
|
10
15
|
export const sanitize: (text: string) => string;
|
|
11
|
-
export function slice(text: string, start: number, end: number): string;
|
package/dist/strip.js
CHANGED
|
@@ -1,58 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const {
|
|
3
|
-
const rxCSI = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
4
|
-
const rxUnsafe = /\x1b\[[0-?]*[ -/]*[@-ln-~]/g; // Everything except SGR codes (m).
|
|
2
|
+
const { rxCSI, rxUnsafe } = require("./patterns");
|
|
5
3
|
/**
|
|
6
4
|
* Remove all ANSI escape codes from the given text.
|
|
7
5
|
* @type {(text: string) => string}
|
|
8
6
|
*/
|
|
9
|
-
const strip = text => text.replace(rxCSI,
|
|
7
|
+
const strip = text => text.replace(new RegExp(rxCSI), "");
|
|
8
|
+
/**
|
|
9
|
+
* Calculate the visible length of a string with ANSI escape codes stripped.
|
|
10
|
+
* @type {(text: string) => number}
|
|
11
|
+
*/
|
|
12
|
+
const visibleLength = text => strip(text).length;
|
|
10
13
|
/**
|
|
11
14
|
* Remove all ANSI escape codes except SGR codes (m), which are used for color and styling.
|
|
12
15
|
* @type {(text: string) => string}
|
|
13
16
|
*/
|
|
14
|
-
const sanitize = text => text.replace(rxUnsafe,
|
|
15
|
-
|
|
16
|
-
function slice(text, start, end) {
|
|
17
|
-
if (start < 0 || end < 0) {
|
|
18
|
-
const visibleLength = strip(text).length;
|
|
19
|
-
if (start < 0)
|
|
20
|
-
start = Math.max(0, visibleLength + start);
|
|
21
|
-
if (end < 0)
|
|
22
|
-
end = Math.max(0, visibleLength + end);
|
|
23
|
-
}
|
|
24
|
-
if (start > end)
|
|
25
|
-
return "";
|
|
26
|
-
let visibleIndex = 0;
|
|
27
|
-
let startIndex = 0;
|
|
28
|
-
while (startIndex < text.length && visibleIndex < start) {
|
|
29
|
-
if (text[startIndex] === "\x1b") {
|
|
30
|
-
rxCSI.lastIndex = startIndex;
|
|
31
|
-
const match = rxCSI.exec(text);
|
|
32
|
-
if (match?.index === startIndex) {
|
|
33
|
-
startIndex += match[0].length;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
startIndex++;
|
|
38
|
-
visibleIndex++;
|
|
39
|
-
}
|
|
40
|
-
let endIndex = startIndex;
|
|
41
|
-
let hasAnsi = false;
|
|
42
|
-
while (endIndex < text.length && visibleIndex < end) {
|
|
43
|
-
if (text[endIndex] === "\x1b") {
|
|
44
|
-
rxCSI.lastIndex = endIndex;
|
|
45
|
-
const match = rxCSI.exec(text);
|
|
46
|
-
if (match?.index === endIndex) {
|
|
47
|
-
endIndex += match[0].length;
|
|
48
|
-
hasAnsi = true;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
endIndex++;
|
|
53
|
-
visibleIndex++;
|
|
54
|
-
}
|
|
55
|
-
return text.slice(startIndex, endIndex) + (hasAnsi ? reset : "");
|
|
56
|
-
}
|
|
57
|
-
;
|
|
58
|
-
module.exports = { strip, sanitize, slice };
|
|
17
|
+
const sanitize = text => text.replace(new RegExp(rxUnsafe), "");
|
|
18
|
+
module.exports = { strip, visibleLength, sanitize };
|
package/package.json
CHANGED
package/src/ansi.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const caret
|
|
4
|
-
const erase
|
|
5
|
-
const scroll
|
|
6
|
-
const {bg, fg, reset, style}
|
|
7
|
-
const {
|
|
3
|
+
const caret = require("./caret");
|
|
4
|
+
const erase = require("./erase");
|
|
5
|
+
const scroll = require("./scroll");
|
|
6
|
+
const {bg, fg, reset, style} = require("./sgr");
|
|
7
|
+
const {simplify} = require("./simplify");
|
|
8
|
+
const {slice} = require("./slice");
|
|
9
|
+
const {sanitize, strip, visibleLength} = require("./strip");
|
|
8
10
|
|
|
9
11
|
const ansi = {
|
|
10
12
|
caret,
|
|
@@ -17,8 +19,10 @@ const ansi = {
|
|
|
17
19
|
bg,
|
|
18
20
|
|
|
19
21
|
strip,
|
|
22
|
+
visibleLength,
|
|
20
23
|
sanitize,
|
|
21
24
|
slice,
|
|
25
|
+
simplify,
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
module.exports = ansi;
|
package/src/patterns.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
I'm freezing these to prevent accidental mutation of the `lastIndex` property, which could keep
|
|
3
|
+
future calls from starting at the beginning of the string.
|
|
4
|
+
|
|
5
|
+
Code using these objects will have to clone them (with `new RegExp(rx)`) before using functions
|
|
6
|
+
that modify `lastIndex` (such as `exec`). But according to my benchmarks, cloning the regex takes
|
|
7
|
+
mere nanoseconds, and it wasn't even measurable when operating on longer strings. V8 clearly uses a
|
|
8
|
+
global cache of compiled regexes. I assume that SpiderMonkey does the same thing.
|
|
9
|
+
|
|
10
|
+
Here are my `timeit` benchmark results:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
const {timeit} = require("$timeit");
|
|
14
|
+
|
|
15
|
+
const rxSGR = /\x1b\[[\d;]*m/g;
|
|
16
|
+
const short = "\x1b[31mred \x1b[32mgreen \x1b[34mblue \x1b[0m";
|
|
17
|
+
const long = short.repeat(1000);
|
|
18
|
+
|
|
19
|
+
timeit(() => short.replace(rxSGR, ""), {description: "short - no clone "});
|
|
20
|
+
timeit(() => short.replace(new RegExp(rxSGR), ""), {description: "short - clone object "});
|
|
21
|
+
timeit(() => short.replace(new RegExp(rxSGR.source, "g"), ""), {description: "short - clone source "});
|
|
22
|
+
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
timeit(() => long .replace(rxSGR, ""), {description: "long - no clone "});
|
|
26
|
+
timeit(() => long .replace(new RegExp(rxSGR), ""), {description: "long - clone object "});
|
|
27
|
+
timeit(() => long .replace(new RegExp(rxSGR.source, "g"), ""), {description: "long - clone source "});
|
|
28
|
+
|
|
29
|
+
// Output:
|
|
30
|
+
// short - no clone : 82.73 ns (12,087,731 in 1.000016982 s)
|
|
31
|
+
// short - clone object : 142.132 ns (7,035,692 in 1.0000001770000002 s)
|
|
32
|
+
// short - clone source : 127.872 ns (7,820,293 in 1.00000017 s)
|
|
33
|
+
//
|
|
34
|
+
// long - no clone : 47.953 ųs (20,854 in 1.0000098149999999 s)
|
|
35
|
+
// long - clone object : 46.713 ųs (21,408 in 1.0000285520000003 s)
|
|
36
|
+
// long - clone source : 46.491 ųs (21,510 in 1.000026492 s)
|
|
37
|
+
```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const rxSGR = Object.freeze(/\x1b\[[\d;]*m/g);
|
|
41
|
+
const rxCSI = Object.freeze(/\x1b\[[0-?]*[ -/]*[@-~]/g);
|
|
42
|
+
const rxUnsafe = Object.freeze(/\x1b\[[0-?]*[ -/]*[@-ln-~]/g); // Everything except SGR codes (m).
|
|
43
|
+
|
|
44
|
+
module.exports = {rxSGR, rxCSI, rxUnsafe};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./types").BuildAtom} BuildAtom */
|
|
4
|
+
|
|
5
|
+
/** @type {BuildAtom} */
|
|
6
|
+
function buildExtendedColorAtom(attribute, codes, index) {
|
|
7
|
+
switch (codes[index + 1]) {
|
|
8
|
+
case 2:
|
|
9
|
+
if (index + 5 >= codes.length)
|
|
10
|
+
return {attribute, code: codes.slice(index), skip: codes.length - index - 1};
|
|
11
|
+
return {attribute, code: codes.slice(index, index + 5), skip: 4};
|
|
12
|
+
case 5:
|
|
13
|
+
if (index + 3 >= codes.length)
|
|
14
|
+
return {attribute, code: codes.slice(index), skip: codes.length - index - 1};
|
|
15
|
+
return {attribute, code: codes.slice(index, index + 3), skip: 2};
|
|
16
|
+
default:
|
|
17
|
+
return {attribute, code: codes.slice(index, index + 2), skip: 1};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @type {BuildAtom} */
|
|
22
|
+
function buildIntensityAtom(attribute, codes, index, state) {
|
|
23
|
+
const code = codes[index];
|
|
24
|
+
if (code === 22)
|
|
25
|
+
return {attribute, code: [code]};
|
|
26
|
+
|
|
27
|
+
const current = state.get("intensity");
|
|
28
|
+
if (!current || current.length == 1 && current[0] === code || current[0] === 22)
|
|
29
|
+
return {attribute, code: [code]};
|
|
30
|
+
|
|
31
|
+
return {attribute, code: [1, 2]};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {buildExtendedColorAtom, buildIntensityAtom};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./simplify");
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {buildExtendedColorAtom, buildIntensityAtom} = require("./atom");
|
|
4
|
+
|
|
5
|
+
/** @typedef {import("./types").Atom} Atom */
|
|
6
|
+
/** @typedef {import("./types").Attribute} Attribute */
|
|
7
|
+
/** @typedef {import("./types").AttributeMap} AttributeMap */
|
|
8
|
+
/** @typedef {import("./types").BuildAtom} BuildAtom */
|
|
9
|
+
|
|
10
|
+
/** @type {Record<number, [attribute: Attribute, buildAtom?: BuildAtom]>} */
|
|
11
|
+
const codeMap = {
|
|
12
|
+
0 : ["reset"],
|
|
13
|
+
1 : ["intensity", buildIntensityAtom],
|
|
14
|
+
2 : ["intensity", buildIntensityAtom],
|
|
15
|
+
3 : ["italic"],
|
|
16
|
+
4 : ["underline"],
|
|
17
|
+
7 : ["inverse"],
|
|
18
|
+
8 : ["hidden"],
|
|
19
|
+
9 : ["strikethrough"],
|
|
20
|
+
21 : ["underline"],
|
|
21
|
+
22 : ["intensity", buildIntensityAtom],
|
|
22
|
+
23 : ["italic"],
|
|
23
|
+
24 : ["underline"],
|
|
24
|
+
27 : ["inverse"],
|
|
25
|
+
28 : ["hidden"],
|
|
26
|
+
29 : ["strikethrough"],
|
|
27
|
+
30 : ["fg"],
|
|
28
|
+
31 : ["fg"],
|
|
29
|
+
32 : ["fg"],
|
|
30
|
+
33 : ["fg"],
|
|
31
|
+
34 : ["fg"],
|
|
32
|
+
35 : ["fg"],
|
|
33
|
+
36 : ["fg"],
|
|
34
|
+
37 : ["fg"],
|
|
35
|
+
38 : ["fg", buildExtendedColorAtom],
|
|
36
|
+
39 : ["fg"],
|
|
37
|
+
40 : ["bg"],
|
|
38
|
+
41 : ["bg"],
|
|
39
|
+
42 : ["bg"],
|
|
40
|
+
43 : ["bg"],
|
|
41
|
+
44 : ["bg"],
|
|
42
|
+
45 : ["bg"],
|
|
43
|
+
46 : ["bg"],
|
|
44
|
+
47 : ["bg"],
|
|
45
|
+
48 : ["bg", buildExtendedColorAtom],
|
|
46
|
+
49 : ["bg"],
|
|
47
|
+
51 : ["frame"],
|
|
48
|
+
52 : ["frame"],
|
|
49
|
+
53 : ["overline"],
|
|
50
|
+
54 : ["frame"],
|
|
51
|
+
55 : ["overline"],
|
|
52
|
+
90 : ["fg"],
|
|
53
|
+
91 : ["fg"],
|
|
54
|
+
92 : ["fg"],
|
|
55
|
+
93 : ["fg"],
|
|
56
|
+
94 : ["fg"],
|
|
57
|
+
95 : ["fg"],
|
|
58
|
+
96 : ["fg"],
|
|
59
|
+
97 : ["fg"],
|
|
60
|
+
100 : ["bg"],
|
|
61
|
+
101 : ["bg"],
|
|
62
|
+
102 : ["bg"],
|
|
63
|
+
103 : ["bg"],
|
|
64
|
+
104 : ["bg"],
|
|
65
|
+
105 : ["bg"],
|
|
66
|
+
106 : ["bg"],
|
|
67
|
+
107 : ["bg"],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** @type {(codes: number[], index: number, state: AttributeMap) => Atom} */
|
|
71
|
+
function lookUpAtom(codes, index, state) {
|
|
72
|
+
const code = codes[index];
|
|
73
|
+
const entry = codeMap[code];
|
|
74
|
+
|
|
75
|
+
if (!entry)
|
|
76
|
+
return {attribute: code, code: [code]};
|
|
77
|
+
|
|
78
|
+
const [attribute, buildAtom] = entry;
|
|
79
|
+
|
|
80
|
+
if (buildAtom)
|
|
81
|
+
return buildAtom(attribute, codes, index, state);
|
|
82
|
+
|
|
83
|
+
return {attribute, code: [code]};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {lookUpAtom};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {rxSGR} = require("../patterns");
|
|
4
|
+
const {State} = require("./state");
|
|
5
|
+
|
|
6
|
+
/** @type {(text: string) => string} */
|
|
7
|
+
function simplify(text) {
|
|
8
|
+
// `.split` doesn't modify `lastIndex`, so no clone is necessary here.
|
|
9
|
+
const parts = text.split(rxSGR);
|
|
10
|
+
|
|
11
|
+
const state = new State();
|
|
12
|
+
let result = parts[0];
|
|
13
|
+
|
|
14
|
+
for (let i = 1; i < parts.length; ) {
|
|
15
|
+
let codes = parts[i++].split(";").map(Number);
|
|
16
|
+
let chunk = parts[i++];
|
|
17
|
+
|
|
18
|
+
while (!chunk && i < parts.length) {
|
|
19
|
+
// Merge consecutive SGR sequences.
|
|
20
|
+
codes.push(...parts[i++].split(";").map(Number));
|
|
21
|
+
chunk = parts[i++];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
codes = state.update(codes);
|
|
25
|
+
if (codes.length)
|
|
26
|
+
result += `\x1b[${codes.join(";")}m`;
|
|
27
|
+
result += chunk;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {simplify};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {lookUpAtom} = require("./lookup");
|
|
4
|
+
const {arraysEqual, transitionIntensity} = require("./utils");
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("./types").Attribute} Attribute */
|
|
7
|
+
/** @typedef {import("./types").AttributeMap} AttributeMap */
|
|
8
|
+
|
|
9
|
+
/** @type {Map<Attribute, number[]>} */
|
|
10
|
+
const defaultCodes = new Map([
|
|
11
|
+
["intensity", [22]],
|
|
12
|
+
["italic", [23]],
|
|
13
|
+
["underline", [24]],
|
|
14
|
+
["inverse", [27]],
|
|
15
|
+
["hidden", [28]],
|
|
16
|
+
["strikethrough",[29]],
|
|
17
|
+
["fg", [39]],
|
|
18
|
+
["bg", [49]],
|
|
19
|
+
["frame", [54]],
|
|
20
|
+
["overline", [55]],
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** @type {AttributeMap} */
|
|
24
|
+
class State extends Map {
|
|
25
|
+
/** @type {(codes: number[]) => number[]} */
|
|
26
|
+
update(codes) {
|
|
27
|
+
/** @type {AttributeMap} */
|
|
28
|
+
let snapshot = new Map(this);
|
|
29
|
+
|
|
30
|
+
/** @type {Set<Attribute>} */
|
|
31
|
+
const attributes = new Set();
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < codes.length; i++) {
|
|
34
|
+
const {attribute, code, skip} = lookUpAtom(codes, i, this);
|
|
35
|
+
|
|
36
|
+
if (attribute === "reset") {
|
|
37
|
+
attributes.clear();
|
|
38
|
+
this.clear();
|
|
39
|
+
attributes.add("reset");
|
|
40
|
+
for (const [attr, defaultCode] of defaultCodes)
|
|
41
|
+
this.set(attr, defaultCode);
|
|
42
|
+
|
|
43
|
+
snapshot = new Map(this);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
attributes.add(attribute);
|
|
48
|
+
this.set(attribute, code);
|
|
49
|
+
|
|
50
|
+
if (skip)
|
|
51
|
+
i += skip;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Array.from(attributes).flatMap(attribute => {
|
|
55
|
+
if (attribute === "reset")
|
|
56
|
+
return [0];
|
|
57
|
+
|
|
58
|
+
const code = this.get(attribute);
|
|
59
|
+
|
|
60
|
+
if (arraysEqual(code, snapshot.get(attribute)))
|
|
61
|
+
return [];
|
|
62
|
+
|
|
63
|
+
if (attribute === "intensity") {
|
|
64
|
+
const previous = snapshot.get(attribute);
|
|
65
|
+
return transitionIntensity(previous, code);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return code;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {State};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type Attribute =
|
|
2
|
+
| "intensity"
|
|
3
|
+
| "italic"
|
|
4
|
+
| "underline"
|
|
5
|
+
| "inverse"
|
|
6
|
+
| "hidden"
|
|
7
|
+
| "strikethrough"
|
|
8
|
+
| "fg"
|
|
9
|
+
| "bg"
|
|
10
|
+
| "frame"
|
|
11
|
+
| "overline"
|
|
12
|
+
| "reset"
|
|
13
|
+
| number;
|
|
14
|
+
|
|
15
|
+
type AttributeMap = Map<Attribute, number[]>;
|
|
16
|
+
|
|
17
|
+
export interface Atom {
|
|
18
|
+
attribute : Attribute;
|
|
19
|
+
code : number[];
|
|
20
|
+
skip? : number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type BuildAtom = (
|
|
24
|
+
attribute: Attribute,
|
|
25
|
+
codes : number[],
|
|
26
|
+
index : number,
|
|
27
|
+
state : AttributeMap,
|
|
28
|
+
) => Atom;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/** @type {<T>(a: T[] | undefined, b: T[] | undefined) => boolean} */
|
|
4
|
+
function arraysEqual(a, b) {
|
|
5
|
+
if (a === b)
|
|
6
|
+
return true;
|
|
7
|
+
|
|
8
|
+
if (!a || !b || a.length !== b.length)
|
|
9
|
+
return false;
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < a.length; i++) {
|
|
12
|
+
if (a[i] !== b[i])
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @type {(previous: number[] | undefined, current: number[]) => number[]} */
|
|
20
|
+
function transitionIntensity(previous, current) {
|
|
21
|
+
if (!previous || previous[0] === 22 || current[0] === 22)
|
|
22
|
+
return current;
|
|
23
|
+
|
|
24
|
+
const oldBold = previous[0] === 1;
|
|
25
|
+
const newBold = current[0] === 1;
|
|
26
|
+
const oldDim = previous[previous.length - 1] === 2;
|
|
27
|
+
const newDim = current[current.length - 1] === 2;
|
|
28
|
+
|
|
29
|
+
const result = [];
|
|
30
|
+
|
|
31
|
+
if (oldBold && !newBold || oldDim && !newDim)
|
|
32
|
+
result.push(22);
|
|
33
|
+
if (newBold && (!oldBold || result[0] === 22))
|
|
34
|
+
result.push(1);
|
|
35
|
+
if (newDim && (!oldDim || result[0] === 22))
|
|
36
|
+
result.push(2);
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {arraysEqual, transitionIntensity};
|
package/src/slice.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {rxCSI, rxSGR} = require("./patterns");
|
|
4
|
+
const {simplify} = require("./simplify");
|
|
5
|
+
const {visibleLength} = require("./strip");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
@type {
|
|
9
|
+
(
|
|
10
|
+
text : string,
|
|
11
|
+
fromStringIndex : number,
|
|
12
|
+
visibleCount : number,
|
|
13
|
+
) => {index: number, length: number}
|
|
14
|
+
}
|
|
15
|
+
*/
|
|
16
|
+
function scanCSI(text, fromStringIndex, visibleCount) {
|
|
17
|
+
// We intentionally mutate `lastIndex` here, so we need a clone.
|
|
18
|
+
const rx = new RegExp(rxCSI);
|
|
19
|
+
|
|
20
|
+
let length = 0;
|
|
21
|
+
let index = fromStringIndex;
|
|
22
|
+
|
|
23
|
+
while (index < text.length && length < visibleCount) {
|
|
24
|
+
if (text[index] === "\x1b") {
|
|
25
|
+
rx.lastIndex = index;
|
|
26
|
+
const match = rx.exec(text);
|
|
27
|
+
if (match?.index === index) {
|
|
28
|
+
index += match[0].length;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
index++;
|
|
34
|
+
length++;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {index, length};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @type {(text: string, start: number, end?: number) => string} */
|
|
41
|
+
function skippedSequences(text, start, end) {
|
|
42
|
+
const sequences = text.slice(start, end).match(new RegExp(rxSGR));
|
|
43
|
+
return sequences ? simplify(sequences.join("")) : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return a slice of the given text, counting only visible characters (i.e. ignoring ANSI escape
|
|
48
|
+
* codes). The returned slice will include any ANSI escape codes from within the slice. In addition,
|
|
49
|
+
* SGR codes (m) from before and after the slice will be combined, simplified, and included so that
|
|
50
|
+
* the terminal state before and after the slice will be the same as if the whole string were
|
|
51
|
+
* printed. If you compose two or more slices together, there may be redundant SGR sequences at the
|
|
52
|
+
* boundaries, but the result will be correct. You can use `simplify` to remove the redundancies.
|
|
53
|
+
*
|
|
54
|
+
* Note the following caveats:
|
|
55
|
+
*
|
|
56
|
+
* * Any non-SGR ANSI codes prior to or after the slice will be ignored.
|
|
57
|
+
* * Control characters (such as newlines and backspaces) both outside and inside the slice are
|
|
58
|
+
* counted as visible characters.
|
|
59
|
+
*
|
|
60
|
+
* These caveats mean that text may not appear on screen to be the same number of characters as the
|
|
61
|
+
* length of the slice, and that the terminal state may not be exactly the same at the start and end
|
|
62
|
+
* of the slice as it would be if the whole string were printed. This function is intended mostly
|
|
63
|
+
* for text that contains SGR codes, but no other ANSI codes or control characters.
|
|
64
|
+
*
|
|
65
|
+
* @type {(text: string, start: number, end: number) => string}
|
|
66
|
+
*/
|
|
67
|
+
function slice(text, start, end) {
|
|
68
|
+
/** @type {number | undefined} */
|
|
69
|
+
let textVisibleLength;
|
|
70
|
+
const getVisibleLength = () => textVisibleLength ??= visibleLength(text);
|
|
71
|
+
|
|
72
|
+
if (start < 0 || end < 0) {
|
|
73
|
+
textVisibleLength = getVisibleLength();
|
|
74
|
+
if (start < 0)
|
|
75
|
+
start = Math.max(0, textVisibleLength + start);
|
|
76
|
+
if (end < 0)
|
|
77
|
+
end = Math.max(0, textVisibleLength + end);
|
|
78
|
+
}
|
|
79
|
+
if (start > end)
|
|
80
|
+
return "";
|
|
81
|
+
|
|
82
|
+
if (start === 0 && (end >= text.length || end >= getVisibleLength()))
|
|
83
|
+
return text;
|
|
84
|
+
|
|
85
|
+
const {index: startIndex, length: visibleSkipped} = scanCSI(text, 0, start);
|
|
86
|
+
|
|
87
|
+
const endIndex =
|
|
88
|
+
startIndex < text.length && visibleSkipped < end
|
|
89
|
+
? scanCSI(text, startIndex, end - visibleSkipped).index
|
|
90
|
+
: text.length;
|
|
91
|
+
|
|
92
|
+
// Start and end the string in the same terminal state it would be in if the string weren't sliced.
|
|
93
|
+
const leadingSequences = skippedSequences(text, 0, startIndex);
|
|
94
|
+
const trailingSequences = skippedSequences(text, endIndex);
|
|
95
|
+
|
|
96
|
+
return leadingSequences + text.slice(startIndex, endIndex) + trailingSequences;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {slice};
|
package/src/strip.js
CHANGED
|
@@ -1,69 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
const rxCSI = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
6
|
-
const rxUnsafe = /\x1b\[[0-?]*[ -/]*[@-ln-~]/g; // Everything except SGR codes (m).
|
|
3
|
+
const {rxCSI, rxUnsafe} = require("./patterns");
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Remove all ANSI escape codes from the given text.
|
|
10
7
|
* @type {(text: string) => string}
|
|
11
8
|
*/
|
|
12
|
-
const strip = text => text.replace(rxCSI,
|
|
9
|
+
const strip = text => text.replace(new RegExp(rxCSI), "");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Calculate the visible length of a string with ANSI escape codes stripped.
|
|
13
|
+
* @type {(text: string) => number}
|
|
14
|
+
*/
|
|
15
|
+
const visibleLength = text => strip(text).length;
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Remove all ANSI escape codes except SGR codes (m), which are used for color and styling.
|
|
16
19
|
* @type {(text: string) => string}
|
|
17
20
|
*/
|
|
18
|
-
const sanitize = text => text.replace(rxUnsafe,
|
|
19
|
-
|
|
20
|
-
/** @type {(text: string, start: number, end: number) => string} */
|
|
21
|
-
function slice(text, start, end) {
|
|
22
|
-
if (start < 0 || end < 0) {
|
|
23
|
-
const visibleLength = strip(text).length;
|
|
24
|
-
if (start < 0)
|
|
25
|
-
start = Math.max(0, visibleLength + start);
|
|
26
|
-
if (end < 0)
|
|
27
|
-
end = Math.max(0, visibleLength + end);
|
|
28
|
-
}
|
|
29
|
-
if (start > end)
|
|
30
|
-
return "";
|
|
31
|
-
|
|
32
|
-
let visibleIndex = 0;
|
|
33
|
-
let startIndex = 0;
|
|
34
|
-
|
|
35
|
-
while (startIndex < text.length && visibleIndex < start) {
|
|
36
|
-
if (text[startIndex] === "\x1b") {
|
|
37
|
-
rxCSI.lastIndex = startIndex;
|
|
38
|
-
const match = rxCSI.exec(text);
|
|
39
|
-
if (match?.index === startIndex) {
|
|
40
|
-
startIndex += match[0].length;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
startIndex++;
|
|
46
|
-
visibleIndex++;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let endIndex = startIndex;
|
|
50
|
-
let hasAnsi = false;
|
|
51
|
-
while (endIndex < text.length && visibleIndex < end) {
|
|
52
|
-
if (text[endIndex] === "\x1b") {
|
|
53
|
-
rxCSI.lastIndex = endIndex;
|
|
54
|
-
const match = rxCSI.exec(text);
|
|
55
|
-
if (match?.index === endIndex) {
|
|
56
|
-
endIndex += match[0].length;
|
|
57
|
-
hasAnsi = true;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
endIndex++;
|
|
63
|
-
visibleIndex++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return text.slice(startIndex, endIndex) + (hasAnsi ? reset : "");
|
|
67
|
-
};
|
|
21
|
+
const sanitize = text => text.replace(new RegExp(rxUnsafe), "");
|
|
68
22
|
|
|
69
|
-
module.exports = {strip,
|
|
23
|
+
module.exports = {strip, visibleLength, sanitize};
|