
























@@ -70,9 +70,10 @@ function wrapLine(text: string, width: number): string[] {
7070}
71717272// ANSI-aware wrapping: never split inside ANSI SGR/OSC-8 sequences.
73-// We don't attempt to re-open styling per line; terminals keep SGR state
74-// across newlines, so as long as we don't corrupt escape sequences we're safe.
73+// Table cells are padded and bordered per physical line, so wrapped lines
74+// must not leak styling into padding while the next continuation keeps it.
7575const ESC = "\u001b";
76+const SGR_RESET = `${ESC}[0m`;
76777778type Token = { kind: "ansi" | "char"; value: string };
7879const tokens: Token[] = [];
@@ -170,9 +171,140 @@ function wrapLine(text: string, width: number): string[] {
170171const bufVisibleWidth = (slice: Token[]) =>
171172slice.reduce((acc, t) => acc + (t.kind === "char" ? visibleWidth(t.value) : 0), 0);
172173174+const parseSgrParams = (value: string): number[] | null => {
175+if (!value.startsWith(`${ESC}[`) || !value.endsWith("m")) {
176+return null;
177+}
178+const raw = value.slice(2, -1);
179+if (!raw) {
180+return [0];
181+}
182+const params = raw.split(";").map((part) => (part === "" ? 0 : Number(part)));
183+return params.every((param) => Number.isInteger(param)) ? params : null;
184+};
185+186+const activeSgrAfter = (tokens: Token[]) => {
187+type SgrCategory =
188+| "background"
189+| "blink"
190+| "conceal"
191+| "foreground"
192+| "intensity"
193+| "inverse"
194+| "italic"
195+| "strike"
196+| "underline";
197+const active: Array<{ value: string; categories: Set<SgrCategory> }> = [];
198+const resetCategoriesFor = (params: number[]) => {
199+const categories = new Set<SgrCategory>();
200+for (const param of params) {
201+if (param === 22) {
202+categories.add("intensity");
203+} else if (param === 23) {
204+categories.add("italic");
205+} else if (param === 24) {
206+categories.add("underline");
207+} else if (param === 25) {
208+categories.add("blink");
209+} else if (param === 27) {
210+categories.add("inverse");
211+} else if (param === 28) {
212+categories.add("conceal");
213+} else if (param === 29) {
214+categories.add("strike");
215+} else if (param === 39) {
216+categories.add("foreground");
217+} else if (param === 49) {
218+categories.add("background");
219+}
220+}
221+return categories;
222+};
223+const activeCategoriesFor = (params: number[]) => {
224+const categories = new Set<SgrCategory>();
225+for (let i = 0; i < params.length; i += 1) {
226+const param = params[i] ?? 0;
227+if (param === 1 || param === 2) {
228+categories.add("intensity");
229+} else if (param === 3) {
230+categories.add("italic");
231+} else if (param === 4) {
232+categories.add("underline");
233+} else if (param === 5 || param === 6) {
234+categories.add("blink");
235+} else if (param === 7) {
236+categories.add("inverse");
237+} else if (param === 8) {
238+categories.add("conceal");
239+} else if (param === 9) {
240+categories.add("strike");
241+} else if ((param >= 30 && param <= 37) || (param >= 90 && param <= 97)) {
242+categories.add("foreground");
243+} else if (param === 38) {
244+categories.add("foreground");
245+if (params[i + 1] === 2) {
246+i += 4;
247+} else if (params[i + 1] === 5) {
248+i += 2;
249+}
250+} else if ((param >= 40 && param <= 47) || (param >= 100 && param <= 107)) {
251+categories.add("background");
252+} else if (param === 48) {
253+categories.add("background");
254+if (params[i + 1] === 2) {
255+i += 4;
256+} else if (params[i + 1] === 5) {
257+i += 2;
258+}
259+}
260+}
261+return categories;
262+};
263+const intersects = (left: Set<SgrCategory>, right: Set<SgrCategory>) => {
264+for (const value of left) {
265+if (right.has(value)) {
266+return true;
267+}
268+}
269+return false;
270+};
271+for (const token of tokens) {
272+if (token.kind !== "ansi") {
273+continue;
274+}
275+const params = parseSgrParams(token.value);
276+if (!params) {
277+continue;
278+}
279+if (params.includes(0)) {
280+active.length = 0;
281+}
282+const resetCategories = resetCategoriesFor(params);
283+if (resetCategories.size > 0) {
284+for (let i = active.length - 1; i >= 0; i -= 1) {
285+const entry = active[i];
286+if (entry && intersects(entry.categories, resetCategories)) {
287+active.splice(i, 1);
288+}
289+}
290+}
291+const activeCategories = activeCategoriesFor(params);
292+if (activeCategories.size > 0) {
293+for (let i = active.length - 1; i >= 0; i -= 1) {
294+const entry = active[i];
295+if (entry && intersects(entry.categories, activeCategories)) {
296+active.splice(i, 1);
297+}
298+}
299+active.push({ value: token.value, categories: activeCategories });
300+}
301+}
302+return active.map((entry) => entry.value).join("");
303+};
304+173305const pushLine = (value: string) => {
174306const cleaned = value.replace(/\s+$/, "");
175-if (cleaned.trim().length === 0) {
307+if (visibleWidth(cleaned) === 0) {
176308return;
177309}
178310lines.push(cleaned);
@@ -197,17 +329,25 @@ function wrapLine(text: string, width: number): string[] {
197329return;
198330}
199331if (breakAt == null || breakAt <= 0) {
200-pushLine(bufToString());
332+const activeSgr = activeSgrAfter(buf);
333+pushLine(activeSgr ? `${bufToString()}${SGR_RESET}` : bufToString());
201334buf.length = 0;
335+if (activeSgr) {
336+buf.push({ kind: "ansi", value: activeSgr });
337+}
202338bufVisible = 0;
203339lastBreakIndex = null;
204340return;
205341}
206342207343const left = buf.slice(0, breakAt);
208344const rest = buf.slice(breakAt);
209-pushLine(bufToString(left));
345+const activeSgr = activeSgrAfter(left);
346+pushLine(activeSgr ? `${bufToString(left)}${SGR_RESET}` : bufToString(left));
210347trimLeadingSpaces(rest);
348+if (activeSgr) {
349+rest.unshift({ kind: "ansi", value: activeSgr });
350+}
211351212352buf.length = 0;
213353buf.push(...rest);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。