linegauge 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/slice.js +56 -47
- package/dist/strip.d.ts +6 -17
- package/dist/strip.js +2 -9
- package/dist/width.d.ts +18 -0
- package/dist/width.js +26 -44
- package/dist/wrap.js +2 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
*
|
|
26
26
|
* R2's fast path: locked by `differential.test.ts`.
|
|
27
27
|
*/
|
|
28
|
-
export { lineCount, measure, width, width as default, type WidthOptions, type WidthOptions as Options } from './width.js';
|
|
29
28
|
export * from './slice.js';
|
|
29
|
+
export { lineCount, measure, width, width as default, type WidthOptions, type WidthOptions as Options } from './width.js';
|
|
30
30
|
export { strip } from './strip.js';
|
|
31
|
-
export
|
|
31
|
+
export * from './truncate.js';
|
|
32
32
|
export { widest } from './widest.js';
|
|
33
33
|
export { wrap, type WrapOptions } from './wrap.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { lineCount, measure, width, width as default } from './width.js';
|
|
2
1
|
export * from './slice.js';
|
|
2
|
+
export { lineCount, measure, width, width as default } from './width.js';
|
|
3
3
|
export { strip } from './strip.js';
|
|
4
|
-
export
|
|
4
|
+
export * from './truncate.js';
|
|
5
5
|
export { widest } from './widest.js';
|
|
6
6
|
export { wrap } from './wrap.js';
|
package/dist/slice.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applyParameters, applyToken, ASCII_PRINTABLE, closingSequence, segmenter, sgrTokens } from './style.js';
|
|
2
|
-
import {
|
|
2
|
+
import { clusterColumns } from './width.js';
|
|
3
3
|
const ESC = '\u001B';
|
|
4
4
|
const BELL = '\u0007';
|
|
5
5
|
const C1_DCS = '\u0090';
|
|
@@ -12,7 +12,6 @@ const C1_APC = '\u009F';
|
|
|
12
12
|
const ST = `${ESC}\\`;
|
|
13
13
|
const ESC_CSI = `${ESC}[`;
|
|
14
14
|
const LINK_PREFIXES = [`${ESC}]8;`, `${C1_OSC}8;`];
|
|
15
|
-
const INTRODUCERS = new Set([ESC, C1_DCS, C1_SOS, C1_CSI, C1_ST, C1_OSC, C1_PM, C1_APC]);
|
|
16
15
|
const STRING_COMMANDS = new Set([']', 'P', 'X', '^', '_']);
|
|
17
16
|
const C1_STRINGS = new Set([C1_DCS, C1_SOS, C1_PM, C1_APC]);
|
|
18
17
|
const CSI_PARAMETER = /[0-?]/u;
|
|
@@ -100,44 +99,66 @@ function parseEscape(string, index) {
|
|
|
100
99
|
}
|
|
101
100
|
const LONE_REGIONAL_INDICATOR = /^[\u{1F1E6}-\u{1F1FF}]$/u;
|
|
102
101
|
function positions(cluster) {
|
|
103
|
-
const columns =
|
|
102
|
+
const columns = clusterColumns(cluster);
|
|
104
103
|
if (columns === 0)
|
|
105
104
|
return 1;
|
|
106
105
|
return LONE_REGIONAL_INDICATOR.test(cluster) ? 2 : columns;
|
|
107
106
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
107
|
+
const INTRODUCER = /[\u001B\u0090\u0098\u009B-\u009F]/g;
|
|
108
|
+
function walk(string, visit) {
|
|
109
|
+
const parts = [];
|
|
111
110
|
let text = '';
|
|
112
|
-
let index = 0;
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
index
|
|
111
|
+
for (let index = 0, from = 0; index <= string.length;) {
|
|
112
|
+
INTRODUCER.lastIndex = index;
|
|
113
|
+
const at = INTRODUCER.exec(string)?.index ?? string.length;
|
|
114
|
+
const escape = at < string.length ? parseEscape(string, at) : undefined;
|
|
115
|
+
if (at < string.length && escape === undefined) {
|
|
116
|
+
index = at + 1;
|
|
118
117
|
continue;
|
|
119
118
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
const run = string.slice(from, at);
|
|
120
|
+
if (run !== '')
|
|
121
|
+
parts.push(run);
|
|
122
|
+
text += run;
|
|
123
|
+
if (escape === undefined)
|
|
124
|
+
break;
|
|
125
|
+
parts.push(escape);
|
|
126
|
+
index = from = at + escape.code.length;
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
const clusters = ASCII_PRINTABLE.test(text) ? undefined : segmenter().segment(text)[Symbol.iterator]();
|
|
129
|
+
let part = 0;
|
|
130
|
+
let run;
|
|
131
|
+
const unit = () => {
|
|
132
|
+
for (;;) {
|
|
133
|
+
const point = run?.next();
|
|
134
|
+
if (point?.done === false)
|
|
135
|
+
return point.value;
|
|
136
|
+
const current = parts[part++];
|
|
137
|
+
if (typeof current !== 'string')
|
|
138
|
+
return current;
|
|
139
|
+
run = current[Symbol.iterator]();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
let next = unit();
|
|
143
|
+
const escapes = (ahead) => {
|
|
144
|
+
for (; next !== undefined && typeof next !== 'string'; next = unit())
|
|
145
|
+
visit({ ...next, ahead });
|
|
146
|
+
};
|
|
147
|
+
for (escapes(false); next !== undefined; escapes(false)) {
|
|
148
|
+
const cluster = clusters?.next();
|
|
149
|
+
const whole = cluster?.done === false;
|
|
150
|
+
const segment = whole ? cluster.value.segment : next;
|
|
151
|
+
const columns = whole ? positions(segment) : 1;
|
|
152
|
+
let offset = 0;
|
|
153
|
+
for (const _ of segment) {
|
|
154
|
+
if (offset > 0)
|
|
155
|
+
escapes(true);
|
|
156
|
+
if (visit({ kind: 'text', value: next, columns: offset === 0 ? columns : 0, continuation: offset > 0 }))
|
|
157
|
+
return;
|
|
158
|
+
next = unit();
|
|
159
|
+
offset += 1;
|
|
137
160
|
}
|
|
138
|
-
at += points;
|
|
139
161
|
}
|
|
140
|
-
return tokens;
|
|
141
162
|
}
|
|
142
163
|
function opensStyle(parameters) {
|
|
143
164
|
return sgrTokens(parameters).some((token) => {
|
|
@@ -151,22 +172,9 @@ function closesStyle(parameters, active) {
|
|
|
151
172
|
applyParameters(parameters, after);
|
|
152
173
|
return active.some((style) => !after.includes(style));
|
|
153
174
|
}
|
|
154
|
-
function continuationAhead(tokens) {
|
|
155
|
-
const ahead = [];
|
|
156
|
-
let next = false;
|
|
157
|
-
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
|
158
|
-
ahead[index] = next;
|
|
159
|
-
const token = tokens[index];
|
|
160
|
-
if (token?.kind === 'text')
|
|
161
|
-
next = token.continuation;
|
|
162
|
-
}
|
|
163
|
-
return ahead;
|
|
164
|
-
}
|
|
165
175
|
export function slice(string, start = 0, end = Number.POSITIVE_INFINITY) {
|
|
166
176
|
if (end <= start || string.length === 0)
|
|
167
177
|
return '';
|
|
168
|
-
const tokens = tokenize(string);
|
|
169
|
-
const ahead = continuationAhead(tokens);
|
|
170
178
|
let active = [];
|
|
171
179
|
let link;
|
|
172
180
|
let linked = false;
|
|
@@ -248,17 +256,17 @@ export function slice(string, start = 0, end = Number.POSITIVE_INFINITY) {
|
|
|
248
256
|
}
|
|
249
257
|
column += token.columns;
|
|
250
258
|
};
|
|
251
|
-
|
|
259
|
+
walk(string, (token) => {
|
|
252
260
|
const cluster = token.kind === 'text' && !token.continuation;
|
|
253
261
|
let pastEnd = column >= end || (cluster && column + token.columns > end);
|
|
254
|
-
if (pastEnd && token.kind !== 'text' && ahead
|
|
262
|
+
if (pastEnd && token.kind !== 'text' && token.ahead)
|
|
255
263
|
pastEnd = false;
|
|
256
264
|
if (pastEnd && cluster) {
|
|
257
265
|
if (pendingAt !== undefined) {
|
|
258
266
|
body = body.slice(0, pendingAt);
|
|
259
267
|
active = pendingActive;
|
|
260
268
|
}
|
|
261
|
-
|
|
269
|
+
return true;
|
|
262
270
|
}
|
|
263
271
|
if (token.kind === 'sgr')
|
|
264
272
|
takeSgr(token, pastEnd);
|
|
@@ -268,7 +276,8 @@ export function slice(string, start = 0, end = Number.POSITIVE_INFINITY) {
|
|
|
268
276
|
takeText(token);
|
|
269
277
|
else if (!pastEnd && started)
|
|
270
278
|
body += token.code;
|
|
271
|
-
|
|
279
|
+
return false;
|
|
280
|
+
});
|
|
272
281
|
if (!started)
|
|
273
282
|
return '';
|
|
274
283
|
if (link !== undefined)
|
package/dist/strip.d.ts
CHANGED
|
@@ -5,23 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Everything a terminal would print, with the escape sequences removed: CSI (SGR and the
|
|
8
|
-
* cursor and erase forms), OSC including `OSC 8` hyperlinks under
|
|
9
|
-
*
|
|
10
|
-
* after it is text and is kept — the same answer `strip-ansi` gives
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
15
|
-
* 1. The local scan, over `style.ts`'s `ANSI_ESCAPE`. It removes CSI and OSC **including
|
|
16
|
-
* the colon form** of an extended colour, which is the one shape Node gets wrong.
|
|
17
|
-
* 2. Node's stripper on what is left — the single-character escapes (`ESC c`), the charset
|
|
18
|
-
* selections (`ESC ( B`) and a truncated sequence at the end of a string. `ANSI_ESCAPE`
|
|
19
|
-
* matches none of those, because `wrap` never needed them: it was built to find the
|
|
20
|
-
* sequences it has to *reopen*, and a charset selection is not one.
|
|
21
|
-
*
|
|
22
|
-
* Order is load-bearing. Ours runs first so the colon form is already gone by the time Node
|
|
23
|
-
* sees the string; reversed, pass 2 would leave `:2::255:0:0m` behind as text and pass 1
|
|
24
|
-
* would have nothing left to match.
|
|
8
|
+
* cursor and erase forms), OSC including `OSC 8` hyperlinks under all three terminators, the
|
|
9
|
+
* C1 introducers, and the single-character escapes. A lone `ESC` with nothing that parses
|
|
10
|
+
* after it is text and is kept — the same answer `strip-ansi` gives, because it is the same
|
|
11
|
+
* pattern. A string with neither `ESC` nor `0x9B` is returned as it is, without a replace —
|
|
12
|
+
* strip-ansi's own fast path, including what it leaves alone: a C1 OSC (`0x9D`) with no `ESC`
|
|
13
|
+
* or `0x9B` beside it is kept by the incumbent, and so it is kept here.
|
|
25
14
|
*/
|
|
26
15
|
export declare function strip(string: string): string;
|
|
27
16
|
/**
|
package/dist/strip.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import { forEachSegment } from './style.js';
|
|
1
|
+
const ANSI = /(?:(?:\u001B\]|\u009D)[^\u0007\u001B\u009C\u009D]*(?:\u0007|\u001B\\|\u009C))|[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]/g;
|
|
3
2
|
export function strip(string) {
|
|
4
|
-
|
|
5
|
-
return '';
|
|
6
|
-
let out = '';
|
|
7
|
-
forEachSegment(string, (text) => {
|
|
8
|
-
out += text;
|
|
9
|
-
});
|
|
10
|
-
return nodeStrip(out);
|
|
3
|
+
return string.includes('\u001B') || string.includes('\u009B') ? string.replace(ANSI, '') : string;
|
|
11
4
|
}
|
|
12
5
|
export { strip as default };
|
package/dist/width.d.ts
CHANGED
|
@@ -33,6 +33,24 @@ export declare function setClaim(fn: ((code: number) => number | undefined) | un
|
|
|
33
33
|
* give a malformed sequence a second chance to be mistaken for one.
|
|
34
34
|
*/
|
|
35
35
|
export declare function measure(text: string, ambiguousIsWide?: boolean): number;
|
|
36
|
+
/**
|
|
37
|
+
* Columns one grapheme cluster occupies — `measure`'s body, for callers that already hold a
|
|
38
|
+
* cluster: `slice` and `wrap` segment their text once and used to hand each cluster back to
|
|
39
|
+
* `measure`, which segmented it again (B5).
|
|
40
|
+
*/
|
|
41
|
+
export declare function clusterColumns(segment: string, ambiguousIsWide?: boolean): number;
|
|
42
|
+
/**
|
|
43
|
+
* Printable ASCII is one column per code unit, and nothing that makes `measure` correct can
|
|
44
|
+
* change that answer: there are no escape sequences, no combining marks and no emoji between
|
|
45
|
+
* 0x20 and 0x7E. `countAnsiEscapeCodes` cannot change it either — `ESC` is 0x1B, below the
|
|
46
|
+
* range, so a string this accepts has no escapes to count.
|
|
47
|
+
*
|
|
48
|
+
* It is not a micro-optimisation. `widest` over many lines is one `Intl.Segmenter` walk per
|
|
49
|
+
* line, and `truncate.test.ts`'s 200,000-line case — the one proving `widest` survives where
|
|
50
|
+
* `Math.max(...)` throws — timed out at five seconds without this. ASCII is the common line.
|
|
51
|
+
* A class and not a loop since 2026-09-30: the same answer in a quarter of the bytes, which
|
|
52
|
+
* `measure` now pays for too (B5).
|
|
53
|
+
*/
|
|
36
54
|
/**
|
|
37
55
|
* What `width` accepts beyond the string. Graded against `string-width`'s own suite, so the
|
|
38
56
|
* names and the defaults are its names and its defaults, not ours.
|
package/dist/width.js
CHANGED
|
@@ -90,24 +90,20 @@ function isWide(codePoint) {
|
|
|
90
90
|
function isAmbiguous(codePoint) {
|
|
91
91
|
return inTable(AMBIGUOUS, codePoint);
|
|
92
92
|
}
|
|
93
|
-
let
|
|
93
|
+
let visibleClass;
|
|
94
94
|
let rgiClass;
|
|
95
95
|
let spacingClass;
|
|
96
96
|
let pictographicClass;
|
|
97
97
|
export const INVISIBLE_CLASSES = ['\\p{Default_Ignorable_Code_Point}', '\\p{Control}', '\\p{Format}', '\\p{Nonspacing_Mark}', '\\p{Enclosing_Mark}', '\\p{Surrogate}'];
|
|
98
98
|
const INVISIBLE_SET = INVISIBLE_CLASSES.join('');
|
|
99
|
-
const
|
|
99
|
+
const ASCII = /^[ -~]*$/u;
|
|
100
|
+
const VISIBLE = () => (visibleClass ??= new RegExp(`[^${INVISIBLE_SET}]`, 'v'));
|
|
100
101
|
const RGI_EMOJI = () => (rgiClass ??= new RegExp('^\\p{RGI_Emoji}$', 'v'));
|
|
101
102
|
const SPACING_MARK = () => (spacingClass ??= new RegExp('^\\p{Spacing_Mark}$', 'v'));
|
|
102
103
|
const EXTENDED_PICTOGRAPHIC = () => (pictographicClass ??= new RegExp('^\\p{Extended_Pictographic}$', 'u'));
|
|
103
104
|
export function leadingInvisible(text) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!INVISIBLE().test(character))
|
|
107
|
-
break;
|
|
108
|
-
index += character.length;
|
|
109
|
-
}
|
|
110
|
-
return index;
|
|
105
|
+
const at = text.search(VISIBLE());
|
|
106
|
+
return at === -1 ? text.length : at;
|
|
111
107
|
}
|
|
112
108
|
const UNQUALIFIED_KEYCAP = /^[\d#*]\u20E3$/u;
|
|
113
109
|
const ZWJ = '\u200D';
|
|
@@ -160,14 +156,14 @@ function isJamo(codePoint) {
|
|
|
160
156
|
return inPairs(JAMO_LEADING, codePoint) || inPairs(JAMO_VOWEL, codePoint) || inPairs(JAMO_TRAILING, codePoint);
|
|
161
157
|
}
|
|
162
158
|
function hangulColumns(visible, ambiguousIsWide) {
|
|
159
|
+
if (!isJamo(visible.codePointAt(0) ?? 0))
|
|
160
|
+
return undefined;
|
|
163
161
|
const codePoints = [];
|
|
164
162
|
for (const character of visible) {
|
|
165
|
-
if (
|
|
163
|
+
if (!VISIBLE().test(character))
|
|
166
164
|
continue;
|
|
167
165
|
codePoints.push(character.codePointAt(0) ?? 0);
|
|
168
166
|
}
|
|
169
|
-
if (codePoints.length === 0 || !isJamo(codePoints[0] ?? 0))
|
|
170
|
-
return undefined;
|
|
171
167
|
let columns = 0;
|
|
172
168
|
for (let index = 0; index < codePoints.length; index += 1) {
|
|
173
169
|
const codePoint = codePoints[index] ?? 0;
|
|
@@ -190,45 +186,31 @@ export function setClaim(fn) {
|
|
|
190
186
|
claim = fn;
|
|
191
187
|
}
|
|
192
188
|
export function measure(text, ambiguousIsWide = false) {
|
|
189
|
+
if (claim === undefined && ASCII.test(text))
|
|
190
|
+
return text.length;
|
|
193
191
|
let columns = 0;
|
|
194
|
-
for (const { segment } of segmenter().segment(text))
|
|
195
|
-
|
|
196
|
-
if (claimed !== undefined) {
|
|
197
|
-
columns += claimed;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const skipped = leadingInvisible(segment);
|
|
201
|
-
if (skipped === segment.length)
|
|
202
|
-
continue;
|
|
203
|
-
if (RGI_EMOJI().test(segment) || isUnqualifiedEmojiSequence(segment)) {
|
|
204
|
-
columns += WIDE_COLUMNS;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const visible = segment.slice(skipped);
|
|
208
|
-
const hangul = hangulColumns(visible, ambiguousIsWide);
|
|
209
|
-
if (hangul !== undefined) {
|
|
210
|
-
columns += hangul;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
columns += columnsOf(visible.codePointAt(0) ?? 0, ambiguousIsWide);
|
|
214
|
-
columns += trailingColumns(visible, ambiguousIsWide);
|
|
215
|
-
}
|
|
192
|
+
for (const { segment } of segmenter().segment(text))
|
|
193
|
+
columns += clusterColumns(segment, ambiguousIsWide);
|
|
216
194
|
return columns;
|
|
217
195
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
196
|
+
const MAY_BE_EMOJI = /[\u2000-\u24FF\u25FD-\u2FFF\uD800-\u{10FFFF}]/u;
|
|
197
|
+
export function clusterColumns(segment, ambiguousIsWide = false) {
|
|
198
|
+
const claimed = claim?.(segment.codePointAt(0) ?? 0);
|
|
199
|
+
if (claimed !== undefined)
|
|
200
|
+
return claimed;
|
|
201
|
+
const skipped = segment.search(VISIBLE());
|
|
202
|
+
if (skipped === -1)
|
|
203
|
+
return 0;
|
|
204
|
+
if (MAY_BE_EMOJI.test(segment) && (RGI_EMOJI().test(segment) || isUnqualifiedEmojiSequence(segment)))
|
|
205
|
+
return WIDE_COLUMNS;
|
|
206
|
+
const visible = segment.slice(skipped);
|
|
207
|
+
return hangulColumns(visible, ambiguousIsWide) ?? columnsOf(visible.codePointAt(0) ?? 0, ambiguousIsWide) + trailingColumns(visible, ambiguousIsWide);
|
|
225
208
|
}
|
|
226
209
|
export function width(input, options = {}) {
|
|
227
210
|
if (typeof input !== 'string' || input === '')
|
|
228
211
|
return 0;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return ascii;
|
|
212
|
+
if (ASCII.test(input))
|
|
213
|
+
return input.length;
|
|
232
214
|
return measure(options.countAnsiEscapeCodes === true ? input : strip(input), options.ambiguousIsNarrow === false);
|
|
233
215
|
}
|
|
234
216
|
export function lineCount(text, columns) {
|
package/dist/wrap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ASCII_PRINTABLE, ROW_BOUNDARY, TAB_SIZE, applyLeadingResets, applyParameters, closingSequence, forEachSegment, hyperlink, matchEscape, openingSequence, segmenter, sgr } from './style.js';
|
|
2
|
-
import { measure } from './width.js';
|
|
2
|
+
import { clusterColumns, measure } from './width.js';
|
|
3
3
|
export function visibleWidth(string) {
|
|
4
4
|
let plainText = '';
|
|
5
5
|
forEachSegment(string, (part) => {
|
|
@@ -16,7 +16,7 @@ function tokenize(string) {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
for (const { segment } of segmenter().segment(plainText))
|
|
19
|
-
tokens.push({ value: segment, width:
|
|
19
|
+
tokens.push({ value: segment, width: clusterColumns(segment) });
|
|
20
20
|
}, (escape) => tokens.push({ value: escape, width: 0 }));
|
|
21
21
|
return tokens;
|
|
22
22
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linegauge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A printer's line gauge \u2014 the steel rule marked in picas and points. Measuring, wrapping, truncating and slicing styled terminal text without the edge fraying \u2014 grapheme-correct over Intl.Segmenter. Drop-in paths for string-width, wrap-ansi, strip-ansi and slice-ansi. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|