// Full CSI: ESC [ <params> <final byte> covers cursor movement, erase, and SGR.
const ANSI_CSI_PATTERN = "\\x1b\\[[\\x20-\\x3f]*[\\x40-\\x7e]" ;
// OSC-8 hyperlinks: ESC ] 8 ; ; url ST ... ESC ] 8 ; ; ST.
// ST can be either ESC \ or BEL.
const OSC8_PATTERN = "\\x1b\\]8;;.*?(?:\\x1b\\\\|\\x07)|\\x1b\\]8;;(?:\\x1b\\\\|\\x07)" ;
const ANSI_CSI_REGEX = new RegExp(ANSI_CSI_PATTERN, "g" );
const OSC8_REGEX = new RegExp(OSC8_PATTERN, "g" );
const graphemeSegmenter =
typeof Intl !== "undefined" && "Segmenter" in Intl
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
: null ;
export function stripAnsi(input: string): string {
return input.replace(OSC8_REGEX, "" ).replace(ANSI_CSI_REGEX, "" );
}
export function splitGraphemes(input: string): string[] {
if (!input) {
return [];
}
if (!graphemeSegmenter) {
return Array.from(input);
}
try {
return Array.from(graphemeSegmenter.segment(input), (segment) => segment.segment);
} catch {
return Array.from(input);
}
}
/**
* Sanitize a value for safe interpolation into log messages .
* Strips ANSI escape sequences , C0 / C1 control characters , and DEL to
* prevent log forging / terminal escape injection ( CWE - 117 ) .
*/
export function sanitizeForLog(v: string): string {
// Pattern built at runtime so the source file stays free of literal control
// characters AND the linter cannot statically detect them (no-control-regex).
const c0Start = String.fromCharCode(0 x00);
const c0End = String.fromCharCode(0 x1f);
const del = String.fromCharCode(0 x7f);
const c1Start = String.fromCharCode(0 x80);
const c1End = String.fromCharCode(0 x9f);
const controlCharsRegex = new RegExp(`[${c0Start}-${c0End}${del}${c1Start}-${c1End}]`, "g" );
return stripAnsi(v).replace(controlCharsRegex, "" );
}
function isZeroWidthCodePoint(codePoint: number): boolean {
return (
(codePoint >= 0 x0300 && codePoint <= 0 x036f) ||
(codePoint >= 0 x1ab0 && codePoint <= 0 x1aff) ||
(codePoint >= 0 x1dc0 && codePoint <= 0 x1dff) ||
(codePoint >= 0 x20d0 && codePoint <= 0 x20ff) ||
(codePoint >= 0 xfe20 && codePoint <= 0 xfe2f) ||
(codePoint >= 0 xfe00 && codePoint <= 0 xfe0f) ||
codePoint === 0 x200d
);
}
function isFullWidthCodePoint(codePoint: number): boolean {
if (codePoint < 0 x1100) {
return false ;
}
return (
codePoint <= 0 x115f ||
codePoint === 0 x2329 ||
codePoint === 0 x232a ||
(codePoint >= 0 x2e80 && codePoint <= 0 x3247 && codePoint !== 0 x303f) ||
(codePoint >= 0 x3250 && codePoint <= 0 x4dbf) ||
(codePoint >= 0 x4e00 && codePoint <= 0 xa4c6) ||
(codePoint >= 0 xa960 && codePoint <= 0 xa97c) ||
(codePoint >= 0 xac00 && codePoint <= 0 xd7a3) ||
(codePoint >= 0 xf900 && codePoint <= 0 xfaff) ||
(codePoint >= 0 xfe10 && codePoint <= 0 xfe19) ||
(codePoint >= 0 xfe30 && codePoint <= 0 xfe6b) ||
(codePoint >= 0 xff01 && codePoint <= 0 xff60) ||
(codePoint >= 0 xffe0 && codePoint <= 0 xffe6) ||
(codePoint >= 0 x1aff0 && codePoint <= 0 x1aff3) ||
(codePoint >= 0 x1aff5 && codePoint <= 0 x1affb) ||
(codePoint >= 0 x1affd && codePoint <= 0 x1affe) ||
(codePoint >= 0 x1b000 && codePoint <= 0 x1b2ff) ||
(codePoint >= 0 x1f200 && codePoint <= 0 x1f251) ||
(codePoint >= 0 x20000 && codePoint <= 0 x3fffd)
);
}
const emojiLikePattern = /[\p{Extended_Pictographic}\p{Regional_Indicator}\u20e3]/u;
function graphemeWidth(grapheme: string): number {
if (!grapheme) {
return 0 ;
}
if (emojiLikePattern.test(grapheme)) {
return 2 ;
}
let sawPrintable = false ;
for (const char of grapheme) {
const codePoint = char .codePointAt(0 );
if (codePoint == null ) {
continue ;
}
if (isZeroWidthCodePoint(codePoint)) {
continue ;
}
if (isFullWidthCodePoint(codePoint)) {
return 2 ;
}
sawPrintable = true ;
}
return sawPrintable ? 1 : 0 ;
}
export function visibleWidth(input: string): number {
return splitGraphemes(stripAnsi(input)).reduce(
(sum, grapheme) => sum + graphemeWidth(grapheme),
0 ,
);
}
Messung V0.5 in Prozent C=96 H=97 G=96
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland