Skip to content

Commit 0895c86

Browse files
committed
Make endSlice exclude partial wide graphemes
Fixes #43 Closes #44
1 parent 2ea51ac commit 0895c86

File tree

4 files changed

+185
-92
lines changed

4 files changed

+185
-92
lines changed

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Slice a string with [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escap
33
44
@param string - A string with ANSI escape codes. Like one styled by [`chalk`](https://github.com/chalk/chalk).
55
@param startSlice - Zero-based visible-column index at which to start the slice. Grapheme clusters are kept intact.
6-
@param endSlice - Zero-based visible-column index at which to end the slice.
6+
@param endSlice - Zero-based visible-column index at which to end the slice. If a full grapheme cluster would cross `endSlice`, it is excluded.
77
88
@example
99
```

index.js

100755100644
Lines changed: 135 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -59,106 +59,122 @@ function shouldIncludeSgrAfterEnd(token, activeStyles) {
5959
return hasClosingEffect && !hasStartFragment;
6060
}
6161

62-
function applySgrToken({token, isPastEnd, activeStyles, returnValue, include, activeHyperlink, position}) {
63-
if (isPastEnd && !shouldIncludeSgrAfterEnd(token, activeStyles)) {
64-
return {
65-
activeStyles,
66-
activeHyperlink,
67-
position,
68-
returnValue,
69-
include,
70-
};
62+
function hasSgrStartFragment(token) {
63+
return token.fragments.some(fragment => fragment.type === 'start');
64+
}
65+
66+
function clearPendingHyperlink(parameters) {
67+
if (
68+
parameters.activeHyperlink
69+
&& !parameters.activeHyperlinkHasVisibleText
70+
&& parameters.activeHyperlinkOutputIndex !== undefined
71+
) {
72+
parameters.returnValue = parameters.returnValue.slice(0, parameters.activeHyperlinkOutputIndex);
7173
}
7274

73-
activeStyles = applySgrFragments(activeStyles, token.fragments);
74-
if (include) {
75-
returnValue += token.code;
75+
parameters.activeHyperlink = undefined;
76+
parameters.activeHyperlinkHasVisibleText = false;
77+
parameters.activeHyperlinkOutputIndex = undefined;
78+
}
79+
80+
function applySgrToken(parameters) {
81+
if (
82+
parameters.isPastEnd
83+
&& !shouldIncludeSgrAfterEnd(parameters.token, parameters.activeStyles)
84+
) {
85+
return parameters;
7686
}
7787

78-
return {
79-
activeStyles,
80-
activeHyperlink,
81-
position,
82-
returnValue,
83-
include,
84-
};
88+
if (
89+
parameters.include
90+
&& hasSgrStartFragment(parameters.token)
91+
&& parameters.pendingSgrOutputIndex === undefined
92+
) {
93+
parameters.pendingSgrOutputIndex = parameters.returnValue.length;
94+
parameters.pendingSgrActiveStyles = new Map(parameters.activeStyles);
95+
}
96+
97+
parameters.activeStyles = applySgrFragments(parameters.activeStyles, parameters.token.fragments);
98+
if (parameters.include) {
99+
parameters.returnValue += parameters.token.code;
100+
}
101+
102+
return parameters;
85103
}
86104

87-
function applyHyperlinkToken({token, isPastEnd, activeStyles, activeHyperlink, position, returnValue, include}) {
105+
function applyHyperlinkToken(parameters) {
88106
if (
89-
isPastEnd
107+
parameters.isPastEnd
90108
&& (
91-
token.action !== 'close'
92-
|| !activeHyperlink
109+
parameters.token.action !== 'close'
110+
|| !parameters.activeHyperlink
93111
)
94112
) {
95-
return {
96-
activeStyles,
97-
activeHyperlink,
98-
position,
99-
returnValue,
100-
include,
101-
};
113+
return parameters;
102114
}
103115

104-
if (token.action === 'open') {
105-
activeHyperlink = token;
106-
} else if (token.action === 'close') {
107-
activeHyperlink = undefined;
116+
if (parameters.token.action === 'open') {
117+
parameters.activeHyperlink = parameters.token;
118+
parameters.activeHyperlinkHasVisibleText = false;
119+
parameters.activeHyperlinkOutputIndex = undefined;
120+
if (parameters.include) {
121+
parameters.activeHyperlinkOutputIndex = parameters.returnValue.length;
122+
}
123+
} else if (parameters.token.action === 'close') {
124+
if (
125+
parameters.include
126+
&& parameters.activeHyperlink
127+
&& !parameters.activeHyperlinkHasVisibleText
128+
) {
129+
clearPendingHyperlink(parameters);
130+
return parameters;
131+
}
132+
133+
parameters.activeHyperlink = undefined;
134+
parameters.activeHyperlinkHasVisibleText = false;
135+
parameters.activeHyperlinkOutputIndex = undefined;
108136
}
109137

110-
if (include) {
111-
returnValue += token.code;
138+
if (parameters.include) {
139+
parameters.returnValue += parameters.token.code;
112140
}
113141

114-
return {
115-
activeStyles,
116-
activeHyperlink,
117-
position,
118-
returnValue,
119-
include,
120-
};
142+
return parameters;
121143
}
122144

123-
function applyControlToken({token, isPastEnd, activeStyles, activeHyperlink, position, returnValue, include}) {
124-
if (!isPastEnd && include) {
125-
returnValue += token.code;
145+
function applyControlToken(parameters) {
146+
if (!parameters.isPastEnd && parameters.include) {
147+
parameters.returnValue += parameters.token.code;
126148
}
127149

128-
return {
129-
activeStyles,
130-
activeHyperlink,
131-
position,
132-
returnValue,
133-
include,
134-
};
150+
return parameters;
135151
}
136152

137-
function applyCharacterToken({token, start, activeStyles, activeHyperlink, position, returnValue, include}) {
153+
function applyCharacterToken(parameters) {
138154
if (
139-
!include
140-
&& position >= start
141-
&& !token.isGraphemeContinuation
155+
!parameters.include
156+
&& parameters.position >= parameters.start
157+
&& !parameters.token.isGraphemeContinuation
142158
) {
143-
include = true;
144-
returnValue = [...activeStyles.values()].join('');
145-
if (activeHyperlink) {
146-
returnValue += activeHyperlink.code;
159+
parameters.include = true;
160+
parameters.returnValue = [...parameters.activeStyles.values()].join('');
161+
if (parameters.activeHyperlink) {
162+
parameters.activeHyperlinkOutputIndex = parameters.returnValue.length;
163+
parameters.returnValue += parameters.activeHyperlink.code;
147164
}
148165
}
149166

150-
if (include) {
151-
returnValue += token.value;
167+
if (parameters.include) {
168+
parameters.returnValue += parameters.token.value;
169+
parameters.pendingSgrOutputIndex = undefined;
170+
parameters.pendingSgrActiveStyles = undefined;
171+
if (parameters.activeHyperlink) {
172+
parameters.activeHyperlinkHasVisibleText = true;
173+
}
152174
}
153175

154-
position += token.visibleWidth;
155-
return {
156-
activeStyles,
157-
activeHyperlink,
158-
position,
159-
returnValue,
160-
include,
161-
};
176+
parameters.position += parameters.token.visibleWidth;
177+
return parameters;
162178
}
163179

164180
const tokenHandlers = {
@@ -171,21 +187,7 @@ const tokenHandlers = {
171187
function applyToken(parameters) {
172188
const tokenHandler = tokenHandlers[parameters.token.type];
173189
if (!tokenHandler) {
174-
const {
175-
activeStyles,
176-
activeHyperlink,
177-
position,
178-
returnValue,
179-
include,
180-
} = parameters;
181-
182-
return {
183-
activeStyles,
184-
activeHyperlink,
185-
position,
186-
returnValue,
187-
include,
188-
};
190+
return parameters;
189191
}
190192

191193
return tokenHandler(parameters);
@@ -206,17 +208,35 @@ function createHasContinuationAheadMap(tokens) {
206208
return hasContinuationAhead;
207209
}
208210

211+
function isPastEndBoundary(token, position, end) {
212+
if (end === undefined) {
213+
return false;
214+
}
215+
216+
if (position >= end) {
217+
return true;
218+
}
219+
220+
return token.type === 'character'
221+
&& !token.isGraphemeContinuation
222+
&& position + token.visibleWidth > end;
223+
}
224+
209225
export default function sliceAnsi(string, start, end) {
210226
const tokens = tokenizeAnsi(string, {endCharacter: end});
211227
const hasContinuationAhead = createHasContinuationAheadMap(tokens);
212228
let activeStyles = new Map();
213229
let activeHyperlink;
230+
let activeHyperlinkHasVisibleText = false;
231+
let activeHyperlinkOutputIndex;
232+
let pendingSgrOutputIndex;
233+
let pendingSgrActiveStyles;
214234
let position = 0;
215235
let returnValue = '';
216236
let include = false;
217237

218238
for (const [tokenIndex, token] of tokens.entries()) {
219-
let isPastEnd = end !== undefined && position >= end;
239+
let isPastEnd = isPastEndBoundary(token, position, end);
220240
if (
221241
isPastEnd
222242
&& token.type !== 'character'
@@ -230,15 +250,42 @@ export default function sliceAnsi(string, start, end) {
230250
&& token.type === 'character'
231251
&& !token.isGraphemeContinuation
232252
) {
253+
if (activeHyperlink && !activeHyperlinkHasVisibleText) {
254+
const hyperlinkState = {
255+
activeHyperlink,
256+
activeHyperlinkHasVisibleText,
257+
activeHyperlinkOutputIndex,
258+
returnValue,
259+
};
260+
clearPendingHyperlink(hyperlinkState);
261+
({
262+
activeHyperlink,
263+
activeHyperlinkHasVisibleText,
264+
activeHyperlinkOutputIndex,
265+
returnValue,
266+
} = hyperlinkState);
267+
}
268+
269+
if (pendingSgrOutputIndex !== undefined) {
270+
returnValue = returnValue.slice(0, pendingSgrOutputIndex);
271+
activeStyles = pendingSgrActiveStyles;
272+
pendingSgrOutputIndex = undefined;
273+
pendingSgrActiveStyles = undefined;
274+
}
275+
233276
break;
234277
}
235278

236-
({activeStyles, activeHyperlink, position, returnValue, include} = applyToken({
279+
({activeStyles, activeHyperlink, activeHyperlinkHasVisibleText, activeHyperlinkOutputIndex, pendingSgrOutputIndex, pendingSgrActiveStyles, position, returnValue, include} = applyToken({
237280
token,
238281
isPastEnd,
239282
start,
240283
activeStyles,
241284
activeHyperlink,
285+
activeHyperlinkHasVisibleText,
286+
activeHyperlinkOutputIndex,
287+
pendingSgrOutputIndex,
288+
pendingSgrActiveStyles,
242289
position,
243290
returnValue,
244291
include,

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Zero-based visible-column index at which to start the slice. Grapheme clusters (
4141
Type: `number`
4242

4343
Zero-based visible-column index at which to end the slice.
44+
If a full grapheme cluster would cross `endSlice`, it is excluded.
4445

4546
## Related
4647

0 commit comments

Comments
 (0)