linegauge 0.5.4 → 1.0.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 +189 -88
- package/dist/check.js +2 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/slice.d.ts +5 -0
- package/dist/slice.js +266 -45
- package/dist/style.d.ts +8 -0
- package/dist/style.js +5 -7
- package/dist/truncate.js +18 -3
- package/dist/width.d.ts +27 -0
- package/dist/width.js +30 -22
- package/dist/wrap.js +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,14 +7,36 @@
|
|
|
7
7
|
</a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
+
<p align="center">
|
|
11
|
+
Measuring, wrapping, truncating and slicing styled terminal text — without the edge fraying.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/linegauge"><img src="https://img.shields.io/npm/v/linegauge?style=flat-square&color=0a6b47" alt="linegauge on npm: the latest version" /></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/linegauge"><img src="https://img.shields.io/npm/dm/linegauge?style=flat-square" alt="linegauge downloads per month on npm" /></a>
|
|
17
|
+
<a href="https://github.com/ofri-peretz/burgee/actions/workflows/quality.yml?query=branch%3Amain"><img src="https://img.shields.io/github/actions/workflow/status/ofri-peretz/burgee/quality.yml?branch=main&style=flat-square&label=Quality%20Gate" alt="Quality Gate: the CI status of main" /></a>
|
|
18
|
+
<a href="https://app.codecov.io/gh/ofri-peretz/burgee/components"><img src="https://img.shields.io/codecov/c/github/ofri-peretz/burgee/main?component=linegauge&style=flat-square" alt="linegauge line coverage: its Codecov component" /></a>
|
|
19
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/ofri-peretz/burgee"><img src="https://img.shields.io/ossf-scorecard/github.com/ofri-peretz/burgee?style=flat-square&label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard for the repository" /></a>
|
|
20
|
+
<a href="https://www.npmjs.com/package/linegauge?activeTab=code"><img src="https://img.shields.io/npm/unpacked-size/linegauge?style=flat-square" alt="Unpacked size of the latest linegauge release on npm" /></a>
|
|
21
|
+
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/package.json"><img src="https://img.shields.io/badge/dependencies-0-0a6b47?style=flat-square" alt="Zero dependencies" /></a>
|
|
22
|
+
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/package.json"><img src="https://img.shields.io/badge/types-included-blue?style=flat-square" alt="TypeScript types included for every entry point" /></a>
|
|
23
|
+
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/package.json"><img src="https://img.shields.io/badge/Node.js-20.19%2B%20%7C%2022.13%2B-green?style=flat-square" alt="Node.js 20.19+ or 22.13+" /></a>
|
|
24
|
+
<a href="https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=flat-square" alt="License: MIT" /></a>
|
|
25
|
+
<a href="https://www.npmjs.com/package/linegauge#provenance"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.npmjs.org%2Flinegauge%2Flatest&query=%24.dist.attestations.provenance~&label=npm&style=flat-square&color=0a6b47" alt="npm provenance of the latest release, read live from its registry attestation" /></a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a href="https://burgee.interlace.tools/docs/compatibility"><img src="https://img.shields.io/badge/slice--ansi%20suite-104%2F104-0a6b47?style=flat-square" alt="linegauge/slice passes 104 of 104 cases of the slice-ansi test suite" /></a>
|
|
30
|
+
<a href="https://burgee.interlace.tools/docs/compatibility"><img src="https://img.shields.io/badge/string--width%20suite-233%2F233-0a6b47?style=flat-square" alt="linegauge passes 233 of 233 cases of the string-width test suite" /></a>
|
|
31
|
+
<a href="https://burgee.interlace.tools/docs/compatibility"><img src="https://img.shields.io/badge/strip--ansi%20suite-8%2F8-0a6b47?style=flat-square" alt="linegauge/strip passes 8 of 8 cases of the strip-ansi test suite" /></a>
|
|
32
|
+
<a href="https://burgee.interlace.tools/docs/compatibility"><img src="https://img.shields.io/badge/wrap--ansi%20suite-85%2F85-0a6b47?style=flat-square" alt="linegauge/wrap passes 85 of 85 cases of the wrap-ansi test suite" /></a>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
10
35
|
<p align="center">
|
|
11
36
|
Docs: <a href="https://linegauge.interlace.tools">https://linegauge.interlace.tools</a><br />
|
|
12
37
|
Migrating from: <a href="https://linegauge.interlace.tools/docs/coming-from/string-width">string-width</a> · <a href="https://linegauge.interlace.tools/docs/coming-from/wrap-ansi">wrap-ansi</a> · <a href="https://linegauge.interlace.tools/docs/coming-from/strip-ansi">strip-ansi</a> · <a href="https://linegauge.interlace.tools/docs/coming-from/slice-ansi">slice-ansi</a>
|
|
13
38
|
</p>
|
|
14
39
|
|
|
15
|
-
**Measuring, wrapping, truncating and slicing styled terminal text — without the edge
|
|
16
|
-
fraying.**
|
|
17
|
-
|
|
18
40
|
A printer's line gauge is the steel rule marked in picas and points: a compositor holds it
|
|
19
41
|
against a line of type and checks it fits the measure it was set to.
|
|
20
42
|
|
|
@@ -24,25 +46,16 @@ Drop-in paths for **string-width** (the default export), **wrap-ansi**, **strip-
|
|
|
24
46
|
**slice-ansi**. Nothing here reads `process`, so a pipe, `--json` and an agent get the same
|
|
25
47
|
columns a terminal does — burgee, caique and flagstaff measure their output with it.
|
|
26
48
|
|
|
49
|
+
## Install
|
|
50
|
+
|
|
27
51
|
```bash
|
|
28
|
-
npm
|
|
52
|
+
npm install linegauge
|
|
53
|
+
pnpm add linegauge
|
|
54
|
+
yarn add linegauge
|
|
55
|
+
bun add linegauge
|
|
29
56
|
```
|
|
30
57
|
|
|
31
|
-
##
|
|
32
|
-
|
|
33
|
-
`width` · `wrap` · `truncate` · `slice` · `widest`
|
|
34
|
-
|
|
35
|
-
They look like five utilities. They are one: **cutting styled text without letting the edge
|
|
36
|
-
come apart** — no dangling escape sequence, no half a grapheme, no severed emoji cluster.
|
|
37
|
-
Each has to know where the ANSI is and where the cluster boundaries are, and once you know
|
|
38
|
-
that, you may as well answer all five.
|
|
39
|
-
|
|
40
|
-
The ecosystem splits it across twelve packages — `strip-ansi`, `string-width`,
|
|
41
|
-
`ansi-regex`, `wrap-ansi`, `emoji-regex`, `slice-ansi`, `get-east-asian-width`,
|
|
42
|
-
`eastasianwidth`, `string-length`, `wcwidth`, `cli-truncate` and `widest-line` — which
|
|
43
|
-
between them sit under most of the terminal ecosystem.
|
|
44
|
-
|
|
45
|
-
## Use
|
|
58
|
+
## Quick start
|
|
46
59
|
|
|
47
60
|
```js
|
|
48
61
|
import { width, wrap, truncate, slice, widest } from 'linegauge';
|
|
@@ -56,13 +69,19 @@ slice(styled, 2, 4); // columns 2 and 3, styles intact
|
|
|
56
69
|
widest(['a', 'bbb', 'cc']); // 3
|
|
57
70
|
```
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
## One problem wearing five names
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
`width` · `wrap` · `truncate` · `slice` · `widest`
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
They look like five utilities. They are one: **cutting styled text without letting the edge
|
|
77
|
+
come apart** — no dangling escape sequence, no half a grapheme, no severed emoji cluster.
|
|
78
|
+
Each has to know where the ANSI is and where the cluster boundaries are, and once you know
|
|
79
|
+
that, you may as well answer all five.
|
|
80
|
+
|
|
81
|
+
The ecosystem splits it across twelve packages — `strip-ansi`, `string-width`,
|
|
82
|
+
`ansi-regex`, `wrap-ansi`, `emoji-regex`, `slice-ansi`, `get-east-asian-width`,
|
|
83
|
+
`eastasianwidth`, `string-length`, `wcwidth`, `cli-truncate` and `widest-line` — which
|
|
84
|
+
between them sit under most of the terminal ecosystem.
|
|
66
85
|
|
|
67
86
|
## What "without the edge fraying" means
|
|
68
87
|
|
|
@@ -81,27 +100,6 @@ it wrong is how a layout gains a phantom column under one input.
|
|
|
81
100
|
**`widest` takes lines, not a blob.** It accepts any iterable of strings, so the caller says
|
|
82
101
|
where the boundaries are rather than having a newline convention assumed for them.
|
|
83
102
|
|
|
84
|
-
## Graded by the packages it replaces
|
|
85
|
-
|
|
86
|
-
The incumbent is the specification. `width` runs against `string-width`, `wrap` against
|
|
87
|
-
`wrap-ansi`, `slice` against `slice-ansi` and `truncate` against `cli-truncate`.
|
|
88
|
-
|
|
89
|
-
## API
|
|
90
|
-
|
|
91
|
-
| | |
|
|
92
|
-
| :-- | :-- |
|
|
93
|
-
| `width(text, { ambiguousIsNarrow, countAnsiEscapeCodes })` | terminal columns the text occupies |
|
|
94
|
-
| `wrap(text, columns, options)` | fold to a width, styles preserved across rows |
|
|
95
|
-
| `truncate(text, columns, { position, ellipsis })` | cut to a budget, ellipsis counted inside it |
|
|
96
|
-
| `slice(text, start, end)` | the columns `[start, end)`, self-contained |
|
|
97
|
-
| `strip(text)` | the text with its escape sequences removed (`linegauge/strip`) |
|
|
98
|
-
| `widest(lines)` | the width of the widest line of any iterable |
|
|
99
|
-
| `lineCount(text, columns)` | rows the text occupies at that width |
|
|
100
|
-
| `measure(text)` | columns of plain text, no escape scan |
|
|
101
|
-
|
|
102
|
-
Non-strings answer `0` rather than throwing, because a width function is usually reached
|
|
103
|
-
with whatever a template produced.
|
|
104
|
-
|
|
105
103
|
## Design notes
|
|
106
104
|
|
|
107
105
|
**Ambiguous-width characters count narrow by default**, which is what a terminal does unless
|
|
@@ -122,64 +120,167 @@ so the segmenter is reached only when it earns its cost.
|
|
|
122
120
|
|
|
123
121
|
## Plugins
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
123
|
+
linegauge hosts one key, **`widths`**, and it answers one question: how many columns a code
|
|
124
|
+
point occupies *on this terminal*, when that terminal disagrees with Unicode.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { register } from 'linegauge/plugin';
|
|
128
|
+
|
|
129
|
+
register({
|
|
130
|
+
name: 'nerd-font',
|
|
131
|
+
widths: {
|
|
132
|
+
icons: { ranges: [[0xe000, 0xf8ff]], columns: 2, why: 'this Nerd Font draws Private Use icons two columns wide' },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The disagreements it exists for are real and local: a Nerd Font that put a two-column icon in
|
|
138
|
+
the Private Use Area, a code point newer than the table compiled into this release, a font that
|
|
139
|
+
draws box-drawing characters wide. Each is a fact about one terminal, so the honest shape for it
|
|
140
|
+
is data the user supplies rather than a constant somebody argues about upstream.
|
|
141
|
+
|
|
142
|
+
- **Plain data, no functions.** `{ ranges, columns, why }` — inclusive code-point pairs, a
|
|
143
|
+
column count of 0, 1 or 2, and a sentence — so a plugin can arrive as JSON, be diffed, and be
|
|
144
|
+
printed by `npx linegauge check` without running its author's code.
|
|
145
|
+
- **`why` is required**, which no other key in the family asks for: a width table with no
|
|
146
|
+
provenance cannot be audited when it turns out wrong, and for ambiguous width, wrong is the
|
|
147
|
+
normal outcome.
|
|
148
|
+
- **Later registrations win**, over earlier ones and over the built-in table — the user is the
|
|
149
|
+
authority on their terminal. A reversed range or a column count of 3 is refused at
|
|
150
|
+
`register()` with a code and a fix.
|
|
151
|
+
- **Nothing is registered by default**, and the seam is installed only while something is, so
|
|
152
|
+
what the incumbent suites grade is Unicode's answer, and a program with no plugin pays nothing.
|
|
153
|
+
|
|
154
|
+
The ambiguous-width policy a CJK terminal needs is not a plugin: it is `ambiguousIsNarrow`, an
|
|
155
|
+
option with tests behind it.
|
|
156
|
+
|
|
157
|
+
## Migrating
|
|
158
|
+
|
|
159
|
+
One import per incumbent — each default export is the incumbent's:
|
|
160
|
+
|
|
161
|
+
```diff
|
|
162
|
+
- import stringWidth from 'string-width';
|
|
163
|
+
+ import stringWidth from 'linegauge';
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```diff
|
|
167
|
+
- import wrapAnsi from 'wrap-ansi';
|
|
168
|
+
+ import wrapAnsi from 'linegauge/wrap';
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```diff
|
|
172
|
+
- import stripAnsi from 'strip-ansi';
|
|
173
|
+
+ import stripAnsi from 'linegauge/strip';
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```diff
|
|
177
|
+
- import sliceAnsi from 'slice-ansi';
|
|
178
|
+
+ import sliceAnsi from 'linegauge/slice';
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The default export is `string-width`, byte-for-byte call-compatible, so a transitive copy
|
|
182
|
+
resolves without a code change too:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{ "overrides": { "string-width": "npm:linegauge@^1" } }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Or let the codemod make the import change: `npx burgee migrate --dry-run` lists every import it
|
|
189
|
+
would rewrite — only drop-ins graded level with their incumbent — and `npx burgee migrate` makes
|
|
190
|
+
it ([Migrate](https://burgee.interlace.tools/docs/migrate)).
|
|
191
|
+
|
|
192
|
+
## Compatibility
|
|
193
|
+
|
|
194
|
+
The incumbent is the specification. `width` runs against `string-width`, `wrap` against
|
|
195
|
+
`wrap-ansi`, `slice` against `slice-ansi` and `truncate` against `cli-truncate`.
|
|
196
|
+
|
|
197
|
+
The four drop-in paths are graded by each incumbent's own suite, unedited, through
|
|
198
|
+
`compat-oracle`; the grades are generated under *Benchmarks* below and published on the
|
|
199
|
+
[compatibility page](https://burgee.interlace.tools/docs/compatibility). `truncate`, which has no
|
|
200
|
+
drop-in path, is checked case by case against `cli-truncate` in `truncate.test.ts`.
|
|
158
201
|
|
|
159
202
|
## Benchmarks
|
|
160
203
|
|
|
161
|
-
Every number here is produced by `npm run bench` and published at [/docs/benchmarks](/docs/benchmarks).
|
|
204
|
+
Every number here is produced by `npm run bench` and published at [burgee.interlace.tools/docs/benchmarks](https://burgee.interlace.tools/docs/benchmarks).
|
|
162
205
|
|
|
163
206
|
Graded by the incumbent's own test suite:
|
|
164
207
|
|
|
165
208
|
| suite | passing |
|
|
166
209
|
| :-- | --: |
|
|
167
|
-
| `slice-ansi` |
|
|
168
|
-
| `string-width` |
|
|
210
|
+
| `slice-ansi` | 104 / 104 |
|
|
211
|
+
| `string-width` | 233 / 233 |
|
|
169
212
|
| `strip-ansi` | 8 / 8 |
|
|
170
|
-
| `wrap-ansi` |
|
|
213
|
+
| `wrap-ansi` | 85 / 85 |
|
|
171
214
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
215
|
+
Weight, installed and tree-inclusive: **102,653 bytes** against **194,329** for the incumbents it replaces — a ratio of **0.5282**.
|
|
216
|
+
|
|
217
|
+
## For agents
|
|
218
|
+
|
|
219
|
+
- **The same columns everywhere.** Nothing here reads `process`, so a pipe, `--json` and an agent
|
|
220
|
+
get the columns a terminal does — which is why burgee, caique and flagstaff measure their
|
|
221
|
+
output with it.
|
|
222
|
+
- **Non-strings answer `0`** rather than throwing, so a width call reached with whatever a
|
|
223
|
+
template produced never takes a program down.
|
|
224
|
+
- **A width plugin can be checked before it ships.** `npx linegauge check ./widths.mjs`
|
|
225
|
+
validates it against the family schema and exits 0, 1 with a code and a fix, or 2 on a usage
|
|
226
|
+
error.
|
|
227
|
+
- **The docs are machine-readable** at
|
|
228
|
+
[linegauge.interlace.tools/llms.txt](https://linegauge.interlace.tools/llms.txt) and
|
|
229
|
+
[llms-full.txt](https://linegauge.interlace.tools/llms-full.txt).
|
|
230
|
+
|
|
231
|
+
## API
|
|
232
|
+
|
|
233
|
+
| | |
|
|
234
|
+
| :-- | :-- |
|
|
235
|
+
| `width(text, { ambiguousIsNarrow, countAnsiEscapeCodes })` | terminal columns the text occupies |
|
|
236
|
+
| `wrap(text, columns, options)` | fold to a width, styles preserved across rows |
|
|
237
|
+
| `truncate(text, columns, { position, ellipsis })` | cut to a budget, ellipsis counted inside it |
|
|
238
|
+
| `slice(text, start, end)` | the columns `[start, end)`, self-contained |
|
|
239
|
+
| `strip(text)` | the text with its escape sequences removed (`linegauge/strip`) |
|
|
240
|
+
| `widest(lines)` | the width of the widest line of any iterable |
|
|
241
|
+
| `lineCount(text, columns)` | rows the text occupies at that width |
|
|
242
|
+
| `measure(text)` | columns of plain text, no escape scan |
|
|
243
|
+
|
|
244
|
+
Non-strings answer `0` rather than throwing, because a width function is usually reached
|
|
245
|
+
with whatever a template produced.
|
|
246
|
+
|
|
247
|
+
Every export, with its types, is on [linegauge.interlace.tools](https://linegauge.interlace.tools/docs).
|
|
176
248
|
|
|
177
|
-
Weight, installed and tree-inclusive: **86,464 bytes** against **194,329** for the incumbents it replaces — a ratio of **0.4449**.
|
|
178
249
|
## Where it sits
|
|
179
250
|
|
|
180
251
|
Plugins register under the `widths` key, against the one schema the whole family shares.
|
|
181
252
|
|
|
182
253
|
`burgee`, `caique`, `flagstaff` build on it, and it builds on nothing in this family.
|
|
254
|
+
|
|
255
|
+
## The family
|
|
256
|
+
|
|
257
|
+
Ten packages, one repository, one release pipeline. A CLI on burgee declares what it is, roundel
|
|
258
|
+
carries its colours, flagstaff flies it and caique answers back; each installs on its own, and none
|
|
259
|
+
takes a dependency from outside the family.
|
|
260
|
+
|
|
261
|
+
| Package | What it is | Replaces |
|
|
262
|
+
| :-- | :-- | :-- |
|
|
263
|
+
| [burgee](https://burgee.interlace.tools/docs/packages/burgee) | The CLI framework: one declaration, every surface | commander and yargs |
|
|
264
|
+
| [roundel](https://roundel.interlace.tools/docs) | Colour: one output policy, semantic tokens, a theme | chalk |
|
|
265
|
+
| [flagstaff](https://flagstaff.interlace.tools/docs) | The frame loop: spinners, progress, boxes and tables | ora, log-update, boxen and cli-table3 |
|
|
266
|
+
| [caique](https://caique.interlace.tools/docs) | Prompts that are flags first, and never hang | inquirer and clack |
|
|
267
|
+
| **linegauge** (this package) | Measuring, wrapping, truncating and slicing styled text | string-width, wrap-ansi, strip-ansi and slice-ansi |
|
|
268
|
+
| [paratext](https://paratext.interlace.tools/docs) | Hyperlinks, images, title, clipboard and notifications | ansi-escapes, terminal-link and term-img |
|
|
269
|
+
| [seniority](https://seniority.interlace.tools/docs) | Configuration precedence and discovery, with provenance | cosmiconfig, dotenv and rc |
|
|
270
|
+
| [closeout](https://closeout.interlace.tools/docs) | Exit handlers, terminal restore and a bounded shutdown | signal-exit, exit-hook and restore-cursor |
|
|
271
|
+
| [bellpull](https://bellpull.interlace.tools/docs) | Subprocesses, and which executable actually ran | cross-spawn and which |
|
|
272
|
+
| [controlroom](https://burgee.interlace.tools/docs/packages/controlroom) | Reserved, not usable yet — planned: full-screen, keyboard-driven terminal screens | ink, planned |
|
|
273
|
+
|
|
274
|
+
Every migration guide, and the family-wide [compatibility](https://burgee.interlace.tools/docs/compatibility)
|
|
275
|
+
and [benchmarks](https://burgee.interlace.tools/docs/benchmarks) pages, are on
|
|
276
|
+
[burgee.interlace.tools](https://burgee.interlace.tools/docs/packages).
|
|
277
|
+
|
|
278
|
+
## Contributing
|
|
279
|
+
|
|
280
|
+
Issues and pull requests are welcome at [ofri-peretz/burgee](https://github.com/ofri-peretz/burgee/issues); read
|
|
281
|
+
[CONTRIBUTING.md](https://github.com/ofri-peretz/burgee/blob/main/CONTRIBUTING.md) first. Report a vulnerability privately, as
|
|
282
|
+
[SECURITY.md](https://github.com/ofri-peretz/burgee/blob/main/SECURITY.md) describes — never in a public issue.
|
|
283
|
+
|
|
183
284
|
## Licence
|
|
184
285
|
|
|
185
|
-
MIT
|
|
286
|
+
MIT © Ofri Peretz — see [LICENSE](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/LICENSE).
|
package/dist/check.js
CHANGED
|
@@ -45,7 +45,6 @@ const overridesOf = (plugin) => {
|
|
|
45
45
|
return typeof widths === 'object' && widths !== null ? Object.entries(widths) : [];
|
|
46
46
|
};
|
|
47
47
|
const firstOf = (plugin) => overridesOf(plugin).flatMap(([, o]) => (o.ranges ?? []).map(([low]) => low));
|
|
48
|
-
const shadows = (names) => (names.length === 0 ? '' : ` (replaces ${names.join(', ')})`);
|
|
49
48
|
export async function check(argv, write) {
|
|
50
49
|
try {
|
|
51
50
|
return await inspect(argv, write);
|
|
@@ -68,9 +67,9 @@ async function inspect(argv, write) {
|
|
|
68
67
|
validate(plugin);
|
|
69
68
|
register(plugin);
|
|
70
69
|
const name = plugin.name;
|
|
71
|
-
const rows = overridesOf(plugin).flatMap(([label, o]) =>
|
|
70
|
+
const rows = overridesOf(plugin).flatMap(([label, o]) => o.ranges.map(([low, high]) => {
|
|
72
71
|
const span = low === high ? codePoint(low) : `${codePoint(low)}..${codePoint(high)}`;
|
|
73
|
-
return `${label} ${span} built-in ${String(builtIn.get(low)
|
|
72
|
+
return `${label} ${span} built-in ${String(builtIn.get(low))} → ${String(width(String.fromCodePoint(low)))} — ${o.why}`;
|
|
74
73
|
}));
|
|
75
74
|
write(`${name} — ${String(rows.length)} widths\n`);
|
|
76
75
|
if (rows.length === 0) {
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* R2's fast path: locked by `differential.test.ts`.
|
|
27
27
|
*/
|
|
28
28
|
export { lineCount, measure, width, width as default, type WidthOptions, type WidthOptions as Options } from './width.js';
|
|
29
|
-
export
|
|
29
|
+
export * from './slice.js';
|
|
30
30
|
export { strip } from './strip.js';
|
|
31
31
|
export { truncate, type TruncateOptions } from './truncate.js';
|
|
32
32
|
export { widest } from './widest.js';
|
package/dist/index.js
CHANGED
package/dist/slice.d.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* `[start, end)` in display columns. A negative or reversed range is empty rather than an
|
|
8
8
|
* error, matching `String.prototype.slice`'s temperament if not its units.
|
|
9
|
+
*
|
|
10
|
+
* The walk's state is locals of this function rather than fields of an object, and that is
|
|
11
|
+
* measured, not taste: a minifier renames a local and cannot touch a property name, and the
|
|
12
|
+
* object form — `cut.pendingAt`, `cut.linkAt` and eight more, each spelled out on every use —
|
|
13
|
+
* cost the bundled `linegauge/slice` more than this does (see `ceilings.json`).
|
|
9
14
|
*/
|
|
10
15
|
export declare function slice(string: string, start?: number, end?: number): string;
|
|
11
16
|
/**
|
package/dist/slice.js
CHANGED
|
@@ -1,57 +1,278 @@
|
|
|
1
|
-
import { applyParameters,
|
|
1
|
+
import { applyParameters, applyToken, ASCII_PRINTABLE, closingSequence, segmenter, sgrTokens } from './style.js';
|
|
2
2
|
import { measure } from './width.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
3
|
+
const ESC = '\u001B';
|
|
4
|
+
const BELL = '\u0007';
|
|
5
|
+
const C1_DCS = '\u0090';
|
|
6
|
+
const C1_SOS = '\u0098';
|
|
7
|
+
const C1_CSI = '\u009B';
|
|
8
|
+
const C1_ST = '\u009C';
|
|
9
|
+
const C1_OSC = '\u009D';
|
|
10
|
+
const C1_PM = '\u009E';
|
|
11
|
+
const C1_APC = '\u009F';
|
|
12
|
+
const ST = `${ESC}\\`;
|
|
13
|
+
const ESC_CSI = `${ESC}[`;
|
|
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
|
+
const STRING_COMMANDS = new Set([']', 'P', 'X', '^', '_']);
|
|
17
|
+
const C1_STRINGS = new Set([C1_DCS, C1_SOS, C1_PM, C1_APC]);
|
|
18
|
+
const CSI_PARAMETER = /[0-?]/u;
|
|
19
|
+
const CSI_INTERMEDIATE = /[ -/]/u;
|
|
20
|
+
const CSI_FINAL = /[@-~]/u;
|
|
21
|
+
const SGR_PARAMETER = /[\d:;]/u;
|
|
22
|
+
const control = (code) => ({ kind: 'control', code });
|
|
23
|
+
function parseLink(string, index) {
|
|
24
|
+
const prefix = LINK_PREFIXES.find((candidate) => string.startsWith(candidate, index));
|
|
25
|
+
if (prefix === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
const uri = string.indexOf(';', index + prefix.length);
|
|
28
|
+
if (uri === -1)
|
|
29
|
+
return control(string.slice(index));
|
|
30
|
+
for (let at = uri + 1; at < string.length; at += 1) {
|
|
31
|
+
const terminator = string[at] === BELL || string[at] === C1_ST ? string[at] : string.startsWith(ST, at) ? ST : undefined;
|
|
32
|
+
if (terminator !== undefined) {
|
|
33
|
+
return { kind: 'link', code: string.slice(index, at + terminator.length), open: at !== uri + 1, close: `${prefix};${terminator}` };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return control(string.slice(index));
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
function parseControlString(string, index) {
|
|
39
|
+
const first = string[index];
|
|
40
|
+
let body;
|
|
41
|
+
let bell = false;
|
|
42
|
+
if (first === ESC) {
|
|
43
|
+
const command = string[index + 1] ?? '';
|
|
44
|
+
if (command === '\\')
|
|
45
|
+
return control(ST);
|
|
46
|
+
if (!STRING_COMMANDS.has(command))
|
|
47
|
+
return undefined;
|
|
48
|
+
body = index + 2;
|
|
49
|
+
bell = command === ']';
|
|
50
|
+
}
|
|
51
|
+
else if (first === C1_ST) {
|
|
52
|
+
return control(C1_ST);
|
|
53
|
+
}
|
|
54
|
+
else if (first === C1_OSC || C1_STRINGS.has(first)) {
|
|
55
|
+
body = index + 1;
|
|
56
|
+
bell = first === C1_OSC;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
for (let at = body; at < string.length; at += 1) {
|
|
62
|
+
if ((bell && string[at] === BELL) || string[at] === C1_ST)
|
|
63
|
+
return control(string.slice(index, at + 1));
|
|
64
|
+
if (string.startsWith(ST, at))
|
|
65
|
+
return control(string.slice(index, at + ST.length));
|
|
66
|
+
}
|
|
67
|
+
return control(string.slice(index));
|
|
68
|
+
}
|
|
69
|
+
function parseCsi(string, index) {
|
|
70
|
+
let prefix;
|
|
71
|
+
if (string.startsWith(ESC_CSI, index))
|
|
72
|
+
prefix = ESC_CSI;
|
|
73
|
+
else if (string[index] === C1_CSI)
|
|
74
|
+
prefix = C1_CSI;
|
|
75
|
+
else
|
|
76
|
+
return undefined;
|
|
77
|
+
let canonical = true;
|
|
78
|
+
for (let at = index + prefix.length; at < string.length; at += 1) {
|
|
79
|
+
const character = string[at];
|
|
80
|
+
if (CSI_FINAL.test(character)) {
|
|
81
|
+
const code = string.slice(index, at + 1);
|
|
82
|
+
if (character !== 'm' || !canonical)
|
|
83
|
+
return control(code);
|
|
84
|
+
return { kind: 'sgr', code, prefix, parameters: string.slice(index + prefix.length, at) };
|
|
85
|
+
}
|
|
86
|
+
if (CSI_PARAMETER.test(character)) {
|
|
87
|
+
canonical &&= SGR_PARAMETER.test(character);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (CSI_INTERMEDIATE.test(character)) {
|
|
91
|
+
canonical = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
return control(string.slice(index, at));
|
|
95
|
+
}
|
|
96
|
+
return control(string.slice(index));
|
|
97
|
+
}
|
|
98
|
+
function parseEscape(string, index) {
|
|
99
|
+
return parseLink(string, index) ?? parseControlString(string, index) ?? parseCsi(string, index);
|
|
100
|
+
}
|
|
101
|
+
const LONE_REGIONAL_INDICATOR = /^[\u{1F1E6}-\u{1F1FF}]$/u;
|
|
102
|
+
function positions(cluster) {
|
|
103
|
+
const columns = measure(cluster);
|
|
104
|
+
if (columns === 0)
|
|
105
|
+
return 1;
|
|
106
|
+
return LONE_REGIONAL_INDICATOR.test(cluster) ? 2 : columns;
|
|
107
|
+
}
|
|
108
|
+
function tokenize(string) {
|
|
109
|
+
const tokens = [];
|
|
110
|
+
const visible = [];
|
|
111
|
+
let text = '';
|
|
37
112
|
let index = 0;
|
|
38
113
|
while (index < string.length) {
|
|
39
|
-
const escape =
|
|
40
|
-
if (escape
|
|
41
|
-
|
|
42
|
-
index += escape
|
|
114
|
+
const escape = INTRODUCERS.has(string[index]) ? parseEscape(string, index) : undefined;
|
|
115
|
+
if (escape) {
|
|
116
|
+
tokens.push(escape);
|
|
117
|
+
index += escape.code.length;
|
|
43
118
|
continue;
|
|
44
119
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
120
|
+
const value = String.fromCodePoint(string.codePointAt(index));
|
|
121
|
+
const token = { kind: 'text', value, columns: 1, continuation: false };
|
|
122
|
+
tokens.push(token);
|
|
123
|
+
visible.push(token);
|
|
124
|
+
text += value;
|
|
125
|
+
index += value.length;
|
|
126
|
+
}
|
|
127
|
+
if (ASCII_PRINTABLE.test(text))
|
|
128
|
+
return tokens;
|
|
129
|
+
let at = 0;
|
|
130
|
+
for (const { segment } of segmenter().segment(text)) {
|
|
131
|
+
const points = [...segment].length;
|
|
132
|
+
const columns = positions(segment);
|
|
133
|
+
for (let offset = 0; offset < points; offset += 1) {
|
|
134
|
+
const token = visible[at + offset];
|
|
135
|
+
token.columns = offset === 0 ? columns : 0;
|
|
136
|
+
token.continuation = offset > 0;
|
|
49
137
|
}
|
|
50
|
-
|
|
138
|
+
at += points;
|
|
139
|
+
}
|
|
140
|
+
return tokens;
|
|
141
|
+
}
|
|
142
|
+
function opensStyle(parameters) {
|
|
143
|
+
return sgrTokens(parameters).some((token) => {
|
|
144
|
+
const probe = [];
|
|
145
|
+
applyToken(token, probe);
|
|
146
|
+
return probe.length > 0;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function closesStyle(parameters, active) {
|
|
150
|
+
const after = [...active];
|
|
151
|
+
applyParameters(parameters, after);
|
|
152
|
+
return active.some((style) => !after.includes(style));
|
|
153
|
+
}
|
|
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
|
+
export function slice(string, start = 0, end = Number.POSITIVE_INFINITY) {
|
|
166
|
+
if (end <= start || string.length === 0)
|
|
167
|
+
return '';
|
|
168
|
+
const tokens = tokenize(string);
|
|
169
|
+
const ahead = continuationAhead(tokens);
|
|
170
|
+
let active = [];
|
|
171
|
+
let link;
|
|
172
|
+
let linked = false;
|
|
173
|
+
let linkAt = 0;
|
|
174
|
+
let pendingAt;
|
|
175
|
+
let pendingActive = active;
|
|
176
|
+
let column = 0;
|
|
177
|
+
let body = '';
|
|
178
|
+
let started = false;
|
|
179
|
+
const forgetLink = () => {
|
|
180
|
+
link = undefined;
|
|
181
|
+
linked = false;
|
|
182
|
+
};
|
|
183
|
+
const discardLink = (open) => {
|
|
184
|
+
const length = open.code.length;
|
|
185
|
+
body = body.slice(0, linkAt) + body.slice(linkAt + length);
|
|
186
|
+
if (pendingAt !== undefined && pendingAt > linkAt)
|
|
187
|
+
pendingAt -= length;
|
|
188
|
+
forgetLink();
|
|
189
|
+
};
|
|
190
|
+
const settleLink = (open) => {
|
|
191
|
+
if (linked)
|
|
192
|
+
body += open.close;
|
|
193
|
+
else
|
|
194
|
+
discardLink(open);
|
|
195
|
+
};
|
|
196
|
+
const takeSgr = (token, pastEnd) => {
|
|
197
|
+
const opens = opensStyle(token.parameters);
|
|
198
|
+
if (pastEnd && (opens || !closesStyle(token.parameters, active)))
|
|
199
|
+
return;
|
|
200
|
+
if (started && opens && pendingAt === undefined) {
|
|
201
|
+
pendingAt = body.length;
|
|
202
|
+
pendingActive = [...active];
|
|
203
|
+
}
|
|
204
|
+
const before = new Set(active);
|
|
205
|
+
applyParameters(token.parameters, active);
|
|
206
|
+
if (token.prefix.length === 1) {
|
|
207
|
+
for (const style of active)
|
|
208
|
+
if (!before.has(style))
|
|
209
|
+
style.prefix = token.prefix;
|
|
210
|
+
}
|
|
211
|
+
if (started)
|
|
212
|
+
body += token.code;
|
|
213
|
+
};
|
|
214
|
+
const takeLink = (token, pastEnd) => {
|
|
215
|
+
if (pastEnd && (token.open || link === undefined))
|
|
216
|
+
return;
|
|
217
|
+
if (token.open) {
|
|
218
|
+
if (link !== undefined)
|
|
219
|
+
settleLink(link);
|
|
220
|
+
link = token;
|
|
221
|
+
linked = false;
|
|
222
|
+
linkAt = body.length;
|
|
223
|
+
}
|
|
224
|
+
else if (started && link !== undefined && !linked) {
|
|
225
|
+
discardLink(link);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
forgetLink();
|
|
230
|
+
}
|
|
231
|
+
if (started)
|
|
232
|
+
body += token.code;
|
|
233
|
+
};
|
|
234
|
+
const takeText = (token) => {
|
|
235
|
+
if (!started && column >= start && !token.continuation) {
|
|
236
|
+
started = true;
|
|
237
|
+
body = active.map((style) => `${style.prefix ?? ESC_CSI}${style.open}m`).join('');
|
|
238
|
+
if (link !== undefined) {
|
|
239
|
+
linkAt = body.length;
|
|
240
|
+
body += link.code;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (started) {
|
|
244
|
+
body += token.value;
|
|
245
|
+
pendingAt = undefined;
|
|
246
|
+
if (link !== undefined)
|
|
247
|
+
linked = true;
|
|
248
|
+
}
|
|
249
|
+
column += token.columns;
|
|
250
|
+
};
|
|
251
|
+
for (const [index, token] of tokens.entries()) {
|
|
252
|
+
const cluster = token.kind === 'text' && !token.continuation;
|
|
253
|
+
let pastEnd = column >= end || (cluster && column + token.columns > end);
|
|
254
|
+
if (pastEnd && token.kind !== 'text' && ahead[index] === true)
|
|
255
|
+
pastEnd = false;
|
|
256
|
+
if (pastEnd && cluster) {
|
|
257
|
+
if (pendingAt !== undefined) {
|
|
258
|
+
body = body.slice(0, pendingAt);
|
|
259
|
+
active = pendingActive;
|
|
260
|
+
}
|
|
51
261
|
break;
|
|
262
|
+
}
|
|
263
|
+
if (token.kind === 'sgr')
|
|
264
|
+
takeSgr(token, pastEnd);
|
|
265
|
+
else if (token.kind === 'link')
|
|
266
|
+
takeLink(token, pastEnd);
|
|
267
|
+
else if (token.kind === 'text')
|
|
268
|
+
takeText(token);
|
|
269
|
+
else if (!pastEnd && started)
|
|
270
|
+
body += token.code;
|
|
52
271
|
}
|
|
53
|
-
if (!
|
|
272
|
+
if (!started)
|
|
54
273
|
return '';
|
|
55
|
-
|
|
274
|
+
if (link !== undefined)
|
|
275
|
+
settleLink(link);
|
|
276
|
+
return body + closingSequence(active);
|
|
56
277
|
}
|
|
57
278
|
export { slice as default };
|
package/dist/style.d.ts
CHANGED
|
@@ -61,6 +61,14 @@ export interface ActiveStyle {
|
|
|
61
61
|
family: string;
|
|
62
62
|
open: string;
|
|
63
63
|
close: number;
|
|
64
|
+
/**
|
|
65
|
+
* The introducer the opener arrived with, when it was not `ESC [`. Only `slice` sets and
|
|
66
|
+
* reads it — a C1 `CSI` (`U+009B`) opener is reopened as it was written, which is what
|
|
67
|
+
* `slice-ansi` 9 does and grades (`keeps C1 SGR CSI behavior`) — so it is a field here and
|
|
68
|
+
* a branch in `slice.ts`, not in `openingSequence`: every entry reaches this file, and
|
|
69
|
+
* the branch would have cost `plugin.js` 81 bytes for a case only `slice` meets.
|
|
70
|
+
*/
|
|
71
|
+
prefix?: string;
|
|
64
72
|
}
|
|
65
73
|
export declare function sgrTokens(parameters: string): SgrToken[];
|
|
66
74
|
export declare function applyToken(token: SgrToken, active: ActiveStyle[]): void;
|
package/dist/style.js
CHANGED
|
@@ -53,7 +53,7 @@ export const segmenter = () => (cached ??= new Intl.Segmenter());
|
|
|
53
53
|
export const sgr = (code) => `${ESC}${CSI}${code}${SGR_TERMINATOR}`;
|
|
54
54
|
export const hyperlink = (url, parameters = '') => `${ESC}${OSC}8;${parameters};${url}${BELL}`;
|
|
55
55
|
export function matchEscape(string, index) {
|
|
56
|
-
if (!ESCAPES.has(string[index]
|
|
56
|
+
if (!ESCAPES.has(string[index]))
|
|
57
57
|
return undefined;
|
|
58
58
|
ANSI_ESCAPE.lastIndex = index;
|
|
59
59
|
return ANSI_ESCAPE.exec(string) ?? undefined;
|
|
@@ -83,11 +83,11 @@ export function forEachSegment(string, onPlainText, onEscape = () => undefined)
|
|
|
83
83
|
const isDigits = (value) => /^\d+$/.test(value);
|
|
84
84
|
function colonColorToken(parameter) {
|
|
85
85
|
const parts = parameter.split(':');
|
|
86
|
-
const code = Number.parseInt(parts[0]
|
|
87
|
-
const mode = Number.parseInt(parts[1]
|
|
86
|
+
const code = Number.parseInt(parts[0], 10);
|
|
87
|
+
const mode = Number.parseInt(parts[1], 10);
|
|
88
88
|
if (![SGR_FOREGROUND_EXTENDED, SGR_BACKGROUND_EXTENDED, SGR_UNDERLINE_COLOR_EXTENDED].includes(code))
|
|
89
89
|
return undefined;
|
|
90
|
-
if (mode === SGR_COLOR_MODE_256 && parts.length === COLOR_256_PARTS && isDigits(parts[2]
|
|
90
|
+
if (mode === SGR_COLOR_MODE_256 && parts.length === COLOR_256_PARTS && isDigits(parts[2])) {
|
|
91
91
|
return { code, open: parameter, hasArguments: true };
|
|
92
92
|
}
|
|
93
93
|
if (mode !== SGR_COLOR_MODE_RGB)
|
|
@@ -118,7 +118,7 @@ export function sgrTokens(parameters) {
|
|
|
118
118
|
const parts = parameters.split(';');
|
|
119
119
|
const tokens = [];
|
|
120
120
|
for (let index = 0; index < parts.length; index += 1) {
|
|
121
|
-
const parameter = parts[index]
|
|
121
|
+
const parameter = parts[index];
|
|
122
122
|
if (parameter.includes(':')) {
|
|
123
123
|
const token = colonColorToken(parameter);
|
|
124
124
|
if (token !== undefined)
|
|
@@ -129,8 +129,6 @@ export function sgrTokens(parameters) {
|
|
|
129
129
|
if (!Number.isFinite(code))
|
|
130
130
|
continue;
|
|
131
131
|
if (isExtendedColor(code)) {
|
|
132
|
-
if (index + 1 >= parts.length)
|
|
133
|
-
break;
|
|
134
132
|
const extended = extendedColorToken(code, parts, index);
|
|
135
133
|
if (extended === undefined)
|
|
136
134
|
break;
|
package/dist/truncate.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { slice } from './slice.js';
|
|
2
2
|
import { width } from './width.js';
|
|
3
|
+
function fit(string, keep, end, total) {
|
|
4
|
+
const head = end === 'head';
|
|
5
|
+
const cut = (at) => (head ? slice(string, 0, at) : slice(string, at));
|
|
6
|
+
let low = head ? keep : total - keep;
|
|
7
|
+
let high = string.length * 2;
|
|
8
|
+
while (low < high) {
|
|
9
|
+
const mid = (low + high + (head ? 1 : 0)) >> 1;
|
|
10
|
+
const fits = width(cut(mid)) <= keep;
|
|
11
|
+
if (head === fits)
|
|
12
|
+
low = head ? mid : mid + 1;
|
|
13
|
+
else
|
|
14
|
+
high = head ? mid - 1 : mid;
|
|
15
|
+
}
|
|
16
|
+
return cut(low);
|
|
17
|
+
}
|
|
3
18
|
export function truncate(string, columns, options = {}) {
|
|
4
19
|
const { position = 'end', ellipsis = '\u2026' } = options;
|
|
5
20
|
if (columns <= 0)
|
|
@@ -12,9 +27,9 @@ export function truncate(string, columns, options = {}) {
|
|
|
12
27
|
return mark === columns ? ellipsis : '';
|
|
13
28
|
const keep = columns - mark;
|
|
14
29
|
if (position === 'start')
|
|
15
|
-
return ellipsis +
|
|
30
|
+
return ellipsis + fit(string, keep, 'tail', total);
|
|
16
31
|
if (position === 'end')
|
|
17
|
-
return
|
|
32
|
+
return fit(string, keep, 'head', total) + ellipsis;
|
|
18
33
|
const left = Math.ceil(keep / 2);
|
|
19
|
-
return
|
|
34
|
+
return fit(string, left, 'head', total) + ellipsis + fit(string, keep - left, 'tail', total);
|
|
20
35
|
}
|
package/dist/width.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ignorable/control/format/mark/surrogate set: the code points that occupy no column.
|
|
3
|
+
*
|
|
4
|
+
* Exported for `width.test.ts`, which grades `leadingInvisible` against the two regexes this
|
|
5
|
+
* set used to be spelled as. Joined with nothing, because it goes inside `[…]`: joined with
|
|
6
|
+
* `|` it once made the class match a literal pipe, and `width()` answered 15 for a
|
|
7
|
+
* three-column string.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INVISIBLE_CLASSES: readonly ["\\p{Default_Ignorable_Code_Point}", "\\p{Control}", "\\p{Format}", "\\p{Nonspacing_Mark}", "\\p{Enclosing_Mark}", "\\p{Surrogate}"];
|
|
10
|
+
/**
|
|
11
|
+
* How many code units at the start of `text` are invisible — a code point loop, linear in the
|
|
12
|
+
* length, where this used to be two regexes.
|
|
13
|
+
*
|
|
14
|
+
* **The regexes could hang `width()`.** A cluster was zero-width when it matched
|
|
15
|
+
* `^(?:DI|Control|Format|Mn|Me|Surrogate)+$`, and the six classes overlap: `U+034F`
|
|
16
|
+
* COMBINING GRAPHEME JOINER is both Default_Ignorable and Nonspacing_Mark, so a run of them
|
|
17
|
+
* followed by one visible character made the engine try every way of assigning each joiner
|
|
18
|
+
* to one of its two alternatives before failing. Measured on Node 24: 24 joiners took 0.6 s,
|
|
19
|
+
* 26 took 2.4 s, 1,000 did not finish in ten minutes. `Intl.Segmenter` hands the whole run
|
|
20
|
+
* to this function as one cluster, because every joiner extends it. string-width 8.3.0's
|
|
21
|
+
* suite added the case (1,000 and 3,000,000 joiners), which is how it was found. The
|
|
22
|
+
* character-class spelling, `^[…]+`, does not backtrack like that but grows V8's backtrack
|
|
23
|
+
* stack by one entry per code point and threw `RangeError` at three million. Asking about one
|
|
24
|
+
* code point at a time has neither problem, and answers the same on every input either regex
|
|
25
|
+
* finishes on — `width.test.ts` compares them exhaustively over short strings.
|
|
26
|
+
*/
|
|
27
|
+
export declare function leadingInvisible(text: string): number;
|
|
1
28
|
/** Installed by `plugin.ts` when the first override registers, and cleared when the last one goes. */
|
|
2
29
|
export declare function setClaim(fn: ((code: number) => number | undefined) | undefined): void;
|
|
3
30
|
/**
|
package/dist/width.js
CHANGED
|
@@ -12,19 +12,20 @@ const WIDE = [
|
|
|
12
12
|
0x3190, 0x31E5, 0x31EF, 0x321E, 0x3220, 0x3247, 0x3250, 0xA48C, 0xA490, 0xA4C6,
|
|
13
13
|
0xA960, 0xA97C, 0xAC00, 0xD7A3, 0xF900, 0xFAFF, 0xFE10, 0xFE19, 0xFE30, 0xFE52,
|
|
14
14
|
0xFE54, 0xFE66, 0xFE68, 0xFE6B, 0xFF01, 0xFF60, 0xFFE0, 0xFFE6, 0x16FE0, 0x16FE4,
|
|
15
|
-
0x16FF0,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
0x16FF0, 0x16FF6, 0x17000, 0x18CDA, 0x18CFF, 0x18D20, 0x18D80, 0x18DF2, 0x18E00, 0x19191,
|
|
16
|
+
0x191A0, 0x191D2, 0x1AFF0, 0x1AFF3, 0x1AFF5, 0x1AFFB, 0x1AFFD, 0x1AFFE, 0x1B000, 0x1B128,
|
|
17
|
+
0x1B132, 0x1B132, 0x1B150, 0x1B152, 0x1B155, 0x1B155, 0x1B164, 0x1B168, 0x1B170, 0x1B2FB,
|
|
18
|
+
0x1D300, 0x1D356, 0x1D360, 0x1D376, 0x1F004, 0x1F004, 0x1F0CF, 0x1F0CF, 0x1F18E, 0x1F18E,
|
|
19
|
+
0x1F191, 0x1F19A, 0x1F1AE, 0x1F1AE, 0x1F200, 0x1F202, 0x1F210, 0x1F23B, 0x1F240, 0x1F248,
|
|
20
|
+
0x1F250, 0x1F251, 0x1F260, 0x1F265, 0x1F300, 0x1F320, 0x1F32D, 0x1F335, 0x1F337, 0x1F37C,
|
|
21
|
+
0x1F37E, 0x1F393, 0x1F3A0, 0x1F3CA, 0x1F3CF, 0x1F3D3, 0x1F3E0, 0x1F3F0, 0x1F3F4, 0x1F3F4,
|
|
22
|
+
0x1F3F8, 0x1F43E, 0x1F440, 0x1F440, 0x1F442, 0x1F4FC, 0x1F4FF, 0x1F53D, 0x1F54B, 0x1F54E,
|
|
23
|
+
0x1F550, 0x1F567, 0x1F57A, 0x1F57A, 0x1F595, 0x1F596, 0x1F5A4, 0x1F5A4, 0x1F5FB, 0x1F64F,
|
|
24
|
+
0x1F680, 0x1F6C5, 0x1F6CC, 0x1F6CC, 0x1F6D0, 0x1F6D2, 0x1F6D5, 0x1F6D9, 0x1F6DC, 0x1F6DF,
|
|
25
|
+
0x1F6EB, 0x1F6EC, 0x1F6F4, 0x1F6FC, 0x1F7DA, 0x1F7DA, 0x1F7E0, 0x1F7EB, 0x1F7F0, 0x1F7F0,
|
|
26
|
+
0x1F90C, 0x1F93A, 0x1F93C, 0x1F945, 0x1F947, 0x1F9FF, 0x1FA70, 0x1FA7C, 0x1FA80, 0x1FAC6,
|
|
27
|
+
0x1FAC8, 0x1FAC8, 0x1FACC, 0x1FADD, 0x1FADF, 0x1FAEB, 0x1FAEF, 0x1FAFA, 0x20000, 0x2FFFD,
|
|
28
|
+
0x30000, 0x3FFFD,
|
|
28
29
|
];
|
|
29
30
|
const NARROW = 1;
|
|
30
31
|
const WIDE_COLUMNS = 2;
|
|
@@ -89,19 +90,25 @@ function isWide(codePoint) {
|
|
|
89
90
|
function isAmbiguous(codePoint) {
|
|
90
91
|
return inTable(AMBIGUOUS, codePoint);
|
|
91
92
|
}
|
|
92
|
-
let
|
|
93
|
-
let leadingClass;
|
|
93
|
+
let invisibleClass;
|
|
94
94
|
let rgiClass;
|
|
95
95
|
let spacingClass;
|
|
96
96
|
let pictographicClass;
|
|
97
|
-
const INVISIBLE_CLASSES = ['\\p{Default_Ignorable_Code_Point}', '\\p{Control}', '\\p{Format}', '\\p{Nonspacing_Mark}', '\\p{Enclosing_Mark}', '\\p{Surrogate}'];
|
|
98
|
-
const INVISIBLE_ALTERNATION = INVISIBLE_CLASSES.join('|');
|
|
97
|
+
export const INVISIBLE_CLASSES = ['\\p{Default_Ignorable_Code_Point}', '\\p{Control}', '\\p{Format}', '\\p{Nonspacing_Mark}', '\\p{Enclosing_Mark}', '\\p{Surrogate}'];
|
|
99
98
|
const INVISIBLE_SET = INVISIBLE_CLASSES.join('');
|
|
100
|
-
const
|
|
101
|
-
const LEADING_NON_PRINTING = () => (leadingClass ??= new RegExp(`^[${INVISIBLE_SET}]+`, 'v'));
|
|
99
|
+
const INVISIBLE = () => (invisibleClass ??= new RegExp(`^[${INVISIBLE_SET}]$`, 'v'));
|
|
102
100
|
const RGI_EMOJI = () => (rgiClass ??= new RegExp('^\\p{RGI_Emoji}$', 'v'));
|
|
103
101
|
const SPACING_MARK = () => (spacingClass ??= new RegExp('^\\p{Spacing_Mark}$', 'v'));
|
|
104
102
|
const EXTENDED_PICTOGRAPHIC = () => (pictographicClass ??= new RegExp('^\\p{Extended_Pictographic}$', 'u'));
|
|
103
|
+
export function leadingInvisible(text) {
|
|
104
|
+
let index = 0;
|
|
105
|
+
for (const character of text) {
|
|
106
|
+
if (!INVISIBLE().test(character))
|
|
107
|
+
break;
|
|
108
|
+
index += character.length;
|
|
109
|
+
}
|
|
110
|
+
return index;
|
|
111
|
+
}
|
|
105
112
|
const UNQUALIFIED_KEYCAP = /^[\d#*]\u20E3$/u;
|
|
106
113
|
const ZWJ = '\u200D';
|
|
107
114
|
const EMOJI_ZWJ_PICTOGRAPHS = 2;
|
|
@@ -155,7 +162,7 @@ function isJamo(codePoint) {
|
|
|
155
162
|
function hangulColumns(visible, ambiguousIsWide) {
|
|
156
163
|
const codePoints = [];
|
|
157
164
|
for (const character of visible) {
|
|
158
|
-
if (
|
|
165
|
+
if (INVISIBLE().test(character))
|
|
159
166
|
continue;
|
|
160
167
|
codePoints.push(character.codePointAt(0) ?? 0);
|
|
161
168
|
}
|
|
@@ -190,13 +197,14 @@ export function measure(text, ambiguousIsWide = false) {
|
|
|
190
197
|
columns += claimed;
|
|
191
198
|
continue;
|
|
192
199
|
}
|
|
193
|
-
|
|
200
|
+
const skipped = leadingInvisible(segment);
|
|
201
|
+
if (skipped === segment.length)
|
|
194
202
|
continue;
|
|
195
203
|
if (RGI_EMOJI().test(segment) || isUnqualifiedEmojiSequence(segment)) {
|
|
196
204
|
columns += WIDE_COLUMNS;
|
|
197
205
|
continue;
|
|
198
206
|
}
|
|
199
|
-
const visible = segment.
|
|
207
|
+
const visible = segment.slice(skipped);
|
|
200
208
|
const hangul = hangulColumns(visible, ambiguousIsWide);
|
|
201
209
|
if (hangul !== undefined) {
|
|
202
210
|
columns += hangul;
|
package/dist/wrap.js
CHANGED
|
@@ -213,9 +213,17 @@ function wrapLine(string, columns, options) {
|
|
|
213
213
|
rows = rows.map((row) => trimVisibleEnd(row));
|
|
214
214
|
return restoreStylesAcrossRows(rows.join('\n'));
|
|
215
215
|
}
|
|
216
|
+
function normalizeText(string) {
|
|
217
|
+
let normalized = '';
|
|
218
|
+
forEachSegment(string, (text) => {
|
|
219
|
+
normalized += text.normalize();
|
|
220
|
+
}, (escape) => {
|
|
221
|
+
normalized += escape;
|
|
222
|
+
});
|
|
223
|
+
return normalized;
|
|
224
|
+
}
|
|
216
225
|
export function wrap(string, columns, options = {}) {
|
|
217
|
-
return String(string)
|
|
218
|
-
.normalize()
|
|
226
|
+
return normalizeText(String(string))
|
|
219
227
|
.replaceAll('\r\n', '\n')
|
|
220
228
|
.split('\n')
|
|
221
229
|
.map((line) => wrapLine(expandTabs(line), columns, options))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linegauge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|