linegauge 0.2.0 → 0.3.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 +60 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/slice.d.ts +8 -0
- package/dist/slice.js +9 -0
- package/dist/strip.d.ts +18 -0
- package/dist/strip.js +19 -0
- package/dist/style.js +11 -0
- package/dist/width.js +147 -13
- package/dist/wrap.d.ts +10 -0
- package/dist/wrap.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,66 @@ text.
|
|
|
100
100
|
scan when the string has no non-ASCII code unit, so the segmenter is reached only when it
|
|
101
101
|
earns its cost.
|
|
102
102
|
|
|
103
|
+
## Plugins
|
|
104
|
+
|
|
105
|
+
**linegauge hosts no plugin key, and that is a decision rather than an omission.** Every
|
|
106
|
+
other package in the family hosts one — `tokens` in roundel, `spinners` and `borders` and
|
|
107
|
+
`glyphs` and `components` in flagstaff, `capabilities` in paratext, `sources` in seniority,
|
|
108
|
+
`handlers` in closeout, `resolvers` in bellpull, `widgets` in caique. Each of those keys sits
|
|
109
|
+
over a question with more than one right answer: which colour, which glyph, which terminal,
|
|
110
|
+
where configuration lives, how an executable is found. A plugin settles it for one program
|
|
111
|
+
without making anybody else wrong.
|
|
112
|
+
|
|
113
|
+
These six functions are not that kind of question. `width('古代')` is 4 because Unicode
|
|
114
|
+
classes those code points East Asian Wide and a terminal gives each of them two columns;
|
|
115
|
+
`slice` returns the columns it was asked for or it returns the wrong string. A plugin key
|
|
116
|
+
here would not extend what linegauge does — it would let a caller redefine what the terminal
|
|
117
|
+
does, silently, for everything above it. The failure would not even surface as an error: a
|
|
118
|
+
box comes out a column short, a table gains a phantom column, and nothing throws.
|
|
119
|
+
|
|
120
|
+
There is a second reason, and it is the one that decides it. This package's correctness is
|
|
121
|
+
differential — `width` is graded against `string-width`, `wrap` against `wrap-ansi`, `slice`
|
|
122
|
+
against `slice-ansi`, `truncate` against `cli-truncate`. A registered contribution would put
|
|
123
|
+
answers under the published pass rate that no grader ever saw, so the number would stop
|
|
124
|
+
meaning what it says.
|
|
125
|
+
|
|
126
|
+
The two things that genuinely vary are already handled without a registry:
|
|
127
|
+
|
|
128
|
+
- **The Unicode data.** The Wide and Fullwidth table is Unicode's, and cluster boundaries
|
|
129
|
+
come from the platform's `Intl.Segmenter`. When Unicode ships a version the table changes —
|
|
130
|
+
that is a release of this package, re-graded, not a registration a caller can make.
|
|
131
|
+
- **The environment.** How wide the terminal is, and whether there is one, are the caller's
|
|
132
|
+
to pass; nothing here reads `process`. That is a parameter, not a plugin.
|
|
133
|
+
|
|
134
|
+
The family's plugin contract records this refusal next to the other layers' keys (R5a), so
|
|
135
|
+
"no key" is one of the contract's answers rather than a hole in it. If a real second answer
|
|
136
|
+
ever arrives — an ambiguous-width policy some terminal actually needs — it lands as an option
|
|
137
|
+
with a differential test behind it, because the graders have to see it.
|
|
138
|
+
|
|
139
|
+
## Benchmarks
|
|
140
|
+
|
|
141
|
+
Every number here is produced by `npm run bench` and published at [/docs/benchmarks](/docs/benchmarks).
|
|
142
|
+
|
|
143
|
+
Graded by the incumbent's own test suite:
|
|
144
|
+
|
|
145
|
+
| suite | passing |
|
|
146
|
+
| :-- | --: |
|
|
147
|
+
| `slice-ansi` | 15 / 15 ¹ |
|
|
148
|
+
| `string-width` | 229 / 229 |
|
|
149
|
+
| `strip-ansi` | 8 / 8 |
|
|
150
|
+
| `wrap-ansi` | 80 / 80 |
|
|
151
|
+
|
|
152
|
+
¹ A case the incumbent marks `test.failing()` — it cannot do the thing and says so in
|
|
153
|
+
its own suite — which this package passes. The runner reports that as a failure, because
|
|
154
|
+
to the incumbent an unexpected pass means a stale annotation; it is counted here as the
|
|
155
|
+
pass it is, and marked rather than left to look like the ones beside it.
|
|
156
|
+
|
|
157
|
+
Weight, installed and tree-inclusive: **83,538 bytes** against **170,342** for the incumbents it replaces — a ratio of **0.4904**.
|
|
158
|
+
## Where it sits
|
|
159
|
+
|
|
160
|
+
It hosts no plugin key of its own.
|
|
161
|
+
|
|
162
|
+
`burgee` and `flagstaff` build on it, and it builds on nothing in this family.
|
|
103
163
|
## Licence
|
|
104
164
|
|
|
105
165
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `stripVTControlCharacters` is recorded — one shape in sixteen, and it was a live bug in
|
|
24
24
|
* `width()`.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* R2's fast path: locked by `differential.test.ts`.
|
|
27
27
|
*/
|
|
28
28
|
export { lineCount, measure, width, width as default, type WidthOptions } from './width.js';
|
|
29
29
|
export { slice } from './slice.js';
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `stripVTControlCharacters` is recorded — one shape in sixteen, and it was a live bug in
|
|
24
24
|
* `width()`.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* R2's fast path: locked by `differential.test.ts`.
|
|
27
27
|
*/
|
|
28
28
|
export { lineCount, measure, width, width as default } from './width.js';
|
|
29
29
|
export { slice } from './slice.js';
|
package/dist/slice.d.ts
CHANGED
|
@@ -8,3 +8,11 @@
|
|
|
8
8
|
* error, matching `String.prototype.slice`'s temperament if not its units.
|
|
9
9
|
*/
|
|
10
10
|
export declare function slice(string: string, start?: number, end?: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* The default export, for the reason `strip.ts` gives at length: `slice-ansi`'s suite
|
|
13
|
+
* imports its entry point's **default**, and that suite now grades this file.
|
|
14
|
+
*
|
|
15
|
+
* The specifier is described rather than quoted on purpose — see the note on `wrap`'s
|
|
16
|
+
* default: `subpath-isolation.test.ts` reads the emitted text, comments and all.
|
|
17
|
+
*/
|
|
18
|
+
export { slice as default };
|
package/dist/slice.js
CHANGED
|
@@ -97,4 +97,13 @@ export function slice(string, start = 0, end = Number.POSITIVE_INFINITY) {
|
|
|
97
97
|
return '';
|
|
98
98
|
return cut.body + (cut.link === undefined ? '' : hyperlink('')) + closingSequence(cut.active);
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The default export, for the reason `strip.ts` gives at length: `slice-ansi`'s suite
|
|
102
|
+
* imports its entry point's **default**, and that suite now grades this file.
|
|
103
|
+
*
|
|
104
|
+
* The specifier is described rather than quoted on purpose — see the note on `wrap`'s
|
|
105
|
+
* default: `subpath-isolation.test.ts` reads the emitted text, comments and all.
|
|
106
|
+
*/
|
|
107
|
+
// eslint-disable-next-line import-next/no-default-export -- the incumbent's own suite imports a default; see above. This is the drop-in surface, not a style choice.
|
|
108
|
+
export { slice as default };
|
|
100
109
|
//# sourceMappingURL=slice.js.map
|
package/dist/strip.d.ts
CHANGED
|
@@ -24,3 +24,21 @@
|
|
|
24
24
|
* would have nothing left to match.
|
|
25
25
|
*/
|
|
26
26
|
export declare function strip(string: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* The same function again, as the default export, because `strip-ansi`'s own suite imports
|
|
29
|
+
* a default — and that suite is now this module's grader
|
|
30
|
+
* (`compat-oracle/vendor/strip-ansi`, `baseline/strip-ansi.json`).
|
|
31
|
+
*
|
|
32
|
+
* It is a *subpath* default rather than the package's, and it has to be: R8 spends the root
|
|
33
|
+
* default on `width`, so `overrides: { "string-width": "npm:linegauge@^1" }` resolves. A
|
|
34
|
+
* `strip-ansi` façade can therefore only ever be `linegauge/strip`, and this is the line
|
|
35
|
+
* that makes `import stripAnsi from 'linegauge/strip'` read exactly like the import it
|
|
36
|
+
* replaces. `truncate` and `widest` deliberately do not have one yet: an export is a
|
|
37
|
+
* contract forever, and neither has a vendored suite holding it to the incumbent's shape.
|
|
38
|
+
*
|
|
39
|
+
* Spelled `strip as default` rather than `export default strip` to match how `index.ts`
|
|
40
|
+
* publishes `width as default`: the alias is a live binding to the same declaration, so
|
|
41
|
+
* there is exactly one `strip` in the module however it is imported — which is the property
|
|
42
|
+
* `facade-defaults.test.ts` asserts with `toBe`, not `toEqual`.
|
|
43
|
+
*/
|
|
44
|
+
export { strip as default };
|
package/dist/strip.js
CHANGED
|
@@ -59,4 +59,23 @@ export function strip(string) {
|
|
|
59
59
|
});
|
|
60
60
|
return nodeStrip(out);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* The same function again, as the default export, because `strip-ansi`'s own suite imports
|
|
64
|
+
* a default — and that suite is now this module's grader
|
|
65
|
+
* (`compat-oracle/vendor/strip-ansi`, `baseline/strip-ansi.json`).
|
|
66
|
+
*
|
|
67
|
+
* It is a *subpath* default rather than the package's, and it has to be: R8 spends the root
|
|
68
|
+
* default on `width`, so `overrides: { "string-width": "npm:linegauge@^1" }` resolves. A
|
|
69
|
+
* `strip-ansi` façade can therefore only ever be `linegauge/strip`, and this is the line
|
|
70
|
+
* that makes `import stripAnsi from 'linegauge/strip'` read exactly like the import it
|
|
71
|
+
* replaces. `truncate` and `widest` deliberately do not have one yet: an export is a
|
|
72
|
+
* contract forever, and neither has a vendored suite holding it to the incumbent's shape.
|
|
73
|
+
*
|
|
74
|
+
* Spelled `strip as default` rather than `export default strip` to match how `index.ts`
|
|
75
|
+
* publishes `width as default`: the alias is a live binding to the same declaration, so
|
|
76
|
+
* there is exactly one `strip` in the module however it is imported — which is the property
|
|
77
|
+
* `facade-defaults.test.ts` asserts with `toBe`, not `toEqual`.
|
|
78
|
+
*/
|
|
79
|
+
// eslint-disable-next-line import-next/no-default-export -- the incumbent's own suite imports a default; see above. This is the drop-in surface, not a style choice.
|
|
80
|
+
export { strip as default };
|
|
62
81
|
//# sourceMappingURL=strip.js.map
|
package/dist/style.js
CHANGED
|
@@ -249,7 +249,18 @@ export function applyToken(token, active) {
|
|
|
249
249
|
const family = `modifier-${token.code}`;
|
|
250
250
|
removeFamily(active, family);
|
|
251
251
|
active.push({ family, open: token.open, close });
|
|
252
|
+
return;
|
|
252
253
|
}
|
|
254
|
+
// An SGR parameter this file has no close code for — `ESC[20m`, `ESC[1001m`. It used to
|
|
255
|
+
// be dropped here, which is the one failure on the `slice-ansi` row that was ours
|
|
256
|
+
// (`design.md` § R10 category E): the text survived a cut and its style did not, silently.
|
|
257
|
+
// The sequence is the caller's, not this library's to vet, so it is carried through and
|
|
258
|
+
// reopened like any other style. `SGR_RESET` is its closer because it is the only one
|
|
259
|
+
// that is correct for a parameter whose meaning is unknown — there is nothing to derive a
|
|
260
|
+
// narrower close from — and it is what `slice-ansi` emits for the same input.
|
|
261
|
+
const family = `unknown-${token.open}`;
|
|
262
|
+
removeFamily(active, family);
|
|
263
|
+
active.push({ family, open: token.open, close: SGR_RESET });
|
|
253
264
|
}
|
|
254
265
|
export const applyParameters = (parameters, active) => {
|
|
255
266
|
for (const token of sgrTokens(parameters))
|
package/dist/width.js
CHANGED
|
@@ -131,24 +131,154 @@ function isWide(codePoint) {
|
|
|
131
131
|
function isAmbiguous(codePoint) {
|
|
132
132
|
return inTable(AMBIGUOUS, codePoint);
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
/**
|
|
135
|
+
* `v`-mode properties: the whole point of using them is that Node ships the tables.
|
|
136
|
+
*
|
|
137
|
+
* The mark classes are spelled out as `Nonspacing_Mark` and `Enclosing_Mark` rather than
|
|
138
|
+
* the `\p{Mark}` that stood here, because `\p{Mark}` is those two **and** `Spacing_Mark` —
|
|
139
|
+
* and a spacing mark is exactly the kind that does occupy a column. `ा`, Devanagari
|
|
140
|
+
* vowel sign AA, answered 0 under the wider class and a terminal draws it one column wide.
|
|
141
|
+
*
|
|
142
|
+
* `\p{Format}` joins the zero-width class for the mirror-image reason. A prepended
|
|
143
|
+
* concatenation mark — `U+0600`, `U+06DD`, `U+070F` — is `Format` but *not*
|
|
144
|
+
* `Default_Ignorable`, so it fell through to the base-scalar path below, which stripped it
|
|
145
|
+
* as leading non-printing, found an empty remainder, read code point 0 and charged a column
|
|
146
|
+
* for it. Charging a column for a character the cursor never advances past is the shape of
|
|
147
|
+
* bug that stays invisible until a box comes out a column short.
|
|
148
|
+
*/
|
|
149
|
+
const ZERO_WIDTH_CLUSTER = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Nonspacing_Mark}|\p{Enclosing_Mark}|\p{Surrogate})+$/v;
|
|
150
|
+
const LEADING_NON_PRINTING = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Nonspacing_Mark}\p{Enclosing_Mark}\p{Surrogate}]+/v;
|
|
137
151
|
const RGI_EMOJI = /^\p{RGI_Emoji}$/v;
|
|
152
|
+
const SPACING_MARK = /^\p{Spacing_Mark}$/v;
|
|
153
|
+
const EXTENDED_PICTOGRAPHIC = /^\p{Extended_Pictographic}$/u;
|
|
154
|
+
/**
|
|
155
|
+
* An **unqualified keycap**: the base, then `U+20E3`, with the `U+FE0F` that would have made
|
|
156
|
+
* it fully qualified missing. The base class is explicit — a digit, `#` or `*` — because
|
|
157
|
+
* `U+260E U+FE0F U+20E3` is *not* a keycap, and `string-width`'s suite grades that too.
|
|
158
|
+
*/
|
|
159
|
+
const UNQUALIFIED_KEYCAP = /^[\d#*]\u20E3$/u;
|
|
160
|
+
const ZWJ = '\u200D';
|
|
161
|
+
/** Two pictographs is what separates an emoji ZWJ sequence from an Indic conjunct. */
|
|
162
|
+
const EMOJI_ZWJ_PICTOGRAPHS = 2;
|
|
163
|
+
/**
|
|
164
|
+
* Longest cluster worth testing for an emoji sequence. Real ones run well under thirty code
|
|
165
|
+
* units; the cap is here so a pathological cluster cannot turn a width call into a scan.
|
|
166
|
+
*/
|
|
167
|
+
const EMOJI_SCAN_LIMIT = 50;
|
|
138
168
|
/** The Halfwidth and Fullwidth Forms block, which a cluster can carry after its base. */
|
|
139
169
|
const FORMS_FIRST = 0xff00;
|
|
140
170
|
const FORMS_LAST = 0xffef;
|
|
171
|
+
/**
|
|
172
|
+
* Conjoining jamo, in the three classes modern Hangul composes from: leading (L), vowel (V)
|
|
173
|
+
* and trailing (T). Each has an archaic extension block beside the main one.
|
|
174
|
+
*/
|
|
175
|
+
const JAMO_LEADING = [0x1100, 0x115f, 0xa960, 0xa97c];
|
|
176
|
+
const JAMO_VOWEL = [0x1160, 0x11a7, 0xd7b0, 0xd7c6];
|
|
177
|
+
const JAMO_TRAILING = [0x11a8, 0x11ff, 0xd7cb, 0xd7fb];
|
|
141
178
|
const segmenter = new Intl.Segmenter();
|
|
142
|
-
/**
|
|
143
|
-
function
|
|
179
|
+
/** East Asian Width of one code point, in columns, under the caller's ambiguous policy. */
|
|
180
|
+
function columnsOf(codePoint, ambiguousIsWide) {
|
|
181
|
+
return isWide(codePoint) || (ambiguousIsWide && isAmbiguous(codePoint)) ? WIDE_COLUMNS : NARROW;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Columns a cluster's trailing **spacing marks and fullwidth forms** add. `ガ` is a base
|
|
185
|
+
* plus a wide mark; `क` + `ा` is a base plus a spacing mark. Both advance the cursor past
|
|
186
|
+
* the base, and neither is a separate cluster, so neither can be counted anywhere else.
|
|
187
|
+
*
|
|
188
|
+
* The walk is over the *visible* cluster — what is left after the leading non-printing run
|
|
189
|
+
* has been removed — so that the character being skipped as "the base" is the base, not a
|
|
190
|
+
* format character standing in front of it.
|
|
191
|
+
*/
|
|
192
|
+
function trailingColumns(visible, ambiguousIsWide) {
|
|
144
193
|
let extra = 0;
|
|
145
|
-
for (const character of [...
|
|
194
|
+
for (const character of [...visible].slice(1)) {
|
|
146
195
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
147
|
-
|
|
148
|
-
|
|
196
|
+
const isForm = codePoint >= FORMS_FIRST && codePoint <= FORMS_LAST;
|
|
197
|
+
if (isForm || SPACING_MARK.test(character))
|
|
198
|
+
extra += columnsOf(codePoint, ambiguousIsWide);
|
|
149
199
|
}
|
|
150
200
|
return extra;
|
|
151
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether a cluster is an emoji sequence that `\p{RGI_Emoji}` refuses because it is
|
|
204
|
+
* **minimally qualified or unqualified** — the same sequence with its `U+FE0F` left off.
|
|
205
|
+
* A terminal renders `U+2764 ZWJ U+1F525` as one two-column emoji whether or not the
|
|
206
|
+
* variation selector is there; the regex only matches the fully-qualified spelling.
|
|
207
|
+
*
|
|
208
|
+
* Two shapes, and both need their guard. A ZWJ sequence counts only when **two or more**
|
|
209
|
+
* pictographs are joined, which is what keeps `क् ZWJ ष` — an Indic conjunct using the same
|
|
210
|
+
* joiner — one column. A keycap counts only over a digit, `#` or `*`, which is what keeps
|
|
211
|
+
* the invalid `U+260E U+FE0F U+20E3` one column. Both of those are cases in the suite that
|
|
212
|
+
* grades this file, and both passed before this function existed.
|
|
213
|
+
*/
|
|
214
|
+
function isUnqualifiedEmojiSequence(cluster) {
|
|
215
|
+
if (cluster.length > EMOJI_SCAN_LIMIT)
|
|
216
|
+
return false;
|
|
217
|
+
if (UNQUALIFIED_KEYCAP.test(cluster))
|
|
218
|
+
return true;
|
|
219
|
+
if (!cluster.includes(ZWJ))
|
|
220
|
+
return false;
|
|
221
|
+
let pictographs = 0;
|
|
222
|
+
for (const character of cluster) {
|
|
223
|
+
if (EXTENDED_PICTOGRAPHIC.test(character))
|
|
224
|
+
pictographs += 1;
|
|
225
|
+
if (pictographs >= EMOJI_ZWJ_PICTOGRAPHS)
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
/** Whether `codePoint` falls in one of a flat `[low, high]` pair list. */
|
|
231
|
+
function inPairs(pairs, codePoint) {
|
|
232
|
+
for (let i = 0; i < pairs.length; i += PAIR) {
|
|
233
|
+
if (codePoint >= (pairs[i] ?? 0) && codePoint <= (pairs[i + 1] ?? 0))
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function isJamo(codePoint) {
|
|
239
|
+
return inPairs(JAMO_LEADING, codePoint) || inPairs(JAMO_VOWEL, codePoint) || inPairs(JAMO_TRAILING, codePoint);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Columns a cluster of **conjoining Hangul jamo** occupies, or `undefined` when the cluster
|
|
243
|
+
* does not start with one — in which case the caller's ordinary base-scalar path is right.
|
|
244
|
+
*
|
|
245
|
+
* This is the one category that needs a walk rather than a table lookup. `Intl.Segmenter`
|
|
246
|
+
* joins a whole run of jamo into a single grapheme cluster (GB6, GB7, GB8), so measuring the
|
|
247
|
+
* cluster by its first code point answered **2** for a six-jamo run a terminal draws **12**
|
|
248
|
+
* columns wide. Modern Hangul composes L + V, or L + V + T, into one syllable block two
|
|
249
|
+
* columns wide; jamo that find no partner stay additive at their own East Asian Width, which
|
|
250
|
+
* makes a leading jamo 2 and a vowel or trailing jamo 1.
|
|
251
|
+
*
|
|
252
|
+
* A cluster that begins with jamo and then turns into something else — a leading jamo
|
|
253
|
+
* followed by a precomposed syllable — measures the jamo by this rule and the remainder by
|
|
254
|
+
* East Asian Width, which is how `U+1100 U+AC00` comes to 4 rather than 2.
|
|
255
|
+
*/
|
|
256
|
+
function hangulColumns(visible, ambiguousIsWide) {
|
|
257
|
+
const codePoints = [];
|
|
258
|
+
for (const character of visible) {
|
|
259
|
+
if (ZERO_WIDTH_CLUSTER.test(character))
|
|
260
|
+
continue;
|
|
261
|
+
codePoints.push(character.codePointAt(0) ?? 0);
|
|
262
|
+
}
|
|
263
|
+
if (codePoints.length === 0 || !isJamo(codePoints[0] ?? 0))
|
|
264
|
+
return undefined;
|
|
265
|
+
let columns = 0;
|
|
266
|
+
for (let index = 0; index < codePoints.length; index += 1) {
|
|
267
|
+
const codePoint = codePoints[index] ?? 0;
|
|
268
|
+
if (!isJamo(codePoint)) {
|
|
269
|
+
for (let rest = index; rest < codePoints.length; rest += 1)
|
|
270
|
+
columns += columnsOf(codePoints[rest] ?? 0, ambiguousIsWide);
|
|
271
|
+
return columns;
|
|
272
|
+
}
|
|
273
|
+
if (inPairs(JAMO_LEADING, codePoint) && inPairs(JAMO_VOWEL, codePoints[index + 1] ?? -1)) {
|
|
274
|
+
columns += WIDE_COLUMNS;
|
|
275
|
+
index += inPairs(JAMO_TRAILING, codePoints[index + PAIR] ?? -1) ? PAIR : 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
columns += columnsOf(codePoint, ambiguousIsWide);
|
|
279
|
+
}
|
|
280
|
+
return columns;
|
|
281
|
+
}
|
|
152
282
|
/**
|
|
153
283
|
* Columns a string of *plain* text occupies — no escape scan. The wrapper below has
|
|
154
284
|
* already split its input into text runs and complete sequences, so rescanning would only
|
|
@@ -159,14 +289,18 @@ export function measure(text, ambiguousIsWide = false) {
|
|
|
159
289
|
for (const { segment } of segmenter.segment(text)) {
|
|
160
290
|
if (ZERO_WIDTH_CLUSTER.test(segment))
|
|
161
291
|
continue;
|
|
162
|
-
if (RGI_EMOJI.test(segment)) {
|
|
292
|
+
if (RGI_EMOJI.test(segment) || isUnqualifiedEmojiSequence(segment)) {
|
|
163
293
|
columns += WIDE_COLUMNS;
|
|
164
294
|
continue;
|
|
165
295
|
}
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
296
|
+
const visible = segment.replace(LEADING_NON_PRINTING, '');
|
|
297
|
+
const hangul = hangulColumns(visible, ambiguousIsWide);
|
|
298
|
+
if (hangul !== undefined) {
|
|
299
|
+
columns += hangul;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
columns += columnsOf(visible.codePointAt(0) ?? 0, ambiguousIsWide);
|
|
303
|
+
columns += trailingColumns(visible, ambiguousIsWide);
|
|
170
304
|
}
|
|
171
305
|
return columns;
|
|
172
306
|
}
|
package/dist/wrap.d.ts
CHANGED
|
@@ -10,3 +10,13 @@ export interface WrapOptions {
|
|
|
10
10
|
}
|
|
11
11
|
/** Wrap `string` to `columns`, keeping its ANSI intact and each row self-contained. */
|
|
12
12
|
export declare function wrap(string: string, columns: number, options?: WrapOptions): string;
|
|
13
|
+
/**
|
|
14
|
+
* The default export, for the reason `strip.ts` gives at length: `wrap-ansi` 10's suite
|
|
15
|
+
* imports its entry point's **default**, and that suite now grades this file.
|
|
16
|
+
*
|
|
17
|
+
* Written without quoting the specifier, deliberately. `subpath-isolation.test.ts` scans the
|
|
18
|
+
* *emitted text* of `dist/wrap.js` for relative imports, comments included, so a doc comment
|
|
19
|
+
* that spells one out makes the entry look as though it reaches a sibling. Caught by that
|
|
20
|
+
* lock on 2026-09-14, which is the lock doing exactly its job on the wrong input.
|
|
21
|
+
*/
|
|
22
|
+
export { wrap as default };
|
package/dist/wrap.js
CHANGED
|
@@ -268,4 +268,15 @@ export function wrap(string, columns, options = {}) {
|
|
|
268
268
|
.map((line) => wrapLine(expandTabs(line), columns, options))
|
|
269
269
|
.join('\n');
|
|
270
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* The default export, for the reason `strip.ts` gives at length: `wrap-ansi` 10's suite
|
|
273
|
+
* imports its entry point's **default**, and that suite now grades this file.
|
|
274
|
+
*
|
|
275
|
+
* Written without quoting the specifier, deliberately. `subpath-isolation.test.ts` scans the
|
|
276
|
+
* *emitted text* of `dist/wrap.js` for relative imports, comments included, so a doc comment
|
|
277
|
+
* that spells one out makes the entry look as though it reaches a sibling. Caught by that
|
|
278
|
+
* lock on 2026-09-14, which is the lock doing exactly its job on the wrong input.
|
|
279
|
+
*/
|
|
280
|
+
// eslint-disable-next-line import-next/no-default-export -- the incumbent's own suite imports a default; see above. This is the drop-in surface, not a style choice.
|
|
281
|
+
export { wrap as default };
|
|
271
282
|
//# sourceMappingURL=wrap.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linegauge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|