2024-04-08 04:32:55 +00:00
|
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2024-04-10 02:43:44 +00:00
|
|
|
|
// This module is browser compatible.
|
2024-04-08 04:32:55 +00:00
|
|
|
|
// Ported from unicode_width rust crate, Copyright (c) 2015 The Rust Project Developers. MIT license.
|
|
|
|
|
|
|
|
|
|
import data from "./_data.json" with { type: "json" };
|
|
|
|
|
import { runLengthDecode } from "./_run_length.ts";
|
|
|
|
|
|
|
|
|
|
let tables: Uint8Array[] | null = null;
|
|
|
|
|
function lookupWidth(cp: number) {
|
|
|
|
|
if (!tables) tables = data.tables.map(runLengthDecode);
|
2024-06-17 07:06:49 +00:00
|
|
|
|
const t1Offset = tables[0]![(cp >> 13) & 0xff]!;
|
|
|
|
|
const t2Offset = tables[1]![128 * t1Offset + ((cp >> 6) & 0x7f)]!;
|
|
|
|
|
const packedWidths = tables[2]![16 * t2Offset + ((cp >> 2) & 0xf)]!;
|
2024-04-08 04:32:55 +00:00
|
|
|
|
|
|
|
|
|
const width = (packedWidths >> (2 * (cp & 0b11))) & 0b11;
|
|
|
|
|
|
|
|
|
|
return width === 3 ? 1 : width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cache = new Map<string, number | null>();
|
2024-06-17 07:06:49 +00:00
|
|
|
|
function charWidth(char: string) {
|
|
|
|
|
if (cache.has(char)) return cache.get(char)!;
|
2024-04-08 04:32:55 +00:00
|
|
|
|
|
2024-06-17 07:06:49 +00:00
|
|
|
|
const codePoint = char.codePointAt(0)!;
|
|
|
|
|
let width: number | null = null;
|
2024-04-08 04:32:55 +00:00
|
|
|
|
|
2024-06-17 07:06:49 +00:00
|
|
|
|
if (codePoint < 0x7f) {
|
|
|
|
|
width = codePoint >= 0x20 ? 1 : codePoint === 0 ? 0 : null;
|
|
|
|
|
} else if (codePoint >= 0xa0) {
|
|
|
|
|
width = lookupWidth(codePoint);
|
2024-04-08 04:32:55 +00:00
|
|
|
|
} else {
|
2024-06-17 07:06:49 +00:00
|
|
|
|
width = null;
|
2024-04-08 04:32:55 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-17 07:06:49 +00:00
|
|
|
|
cache.set(char, width);
|
|
|
|
|
return width;
|
2024-04-08 04:32:55 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the physical width of a string in a TTY-like environment. This is
|
|
|
|
|
* useful for cases such as calculating where a line-wrap will occur and
|
|
|
|
|
* underlining strings.
|
|
|
|
|
*
|
|
|
|
|
* The physical width is given by the number of columns required to display
|
|
|
|
|
* the string. The number of columns a given unicode character occupies can
|
|
|
|
|
* vary depending on the character itself.
|
|
|
|
|
*
|
|
|
|
|
* @param str The string to measure.
|
|
|
|
|
* @returns The unicode width of the string.
|
|
|
|
|
*
|
|
|
|
|
* @example Calculating the unicode width of a string
|
|
|
|
|
* ```ts
|
2024-04-29 02:57:30 +00:00
|
|
|
|
* import { unicodeWidth } from "@std/cli/unicode-width";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-04-08 04:32:55 +00:00
|
|
|
|
*
|
2024-06-03 04:10:27 +00:00
|
|
|
|
* assertEquals(unicodeWidth("hello world"), 11);
|
|
|
|
|
* assertEquals(unicodeWidth("天地玄黃宇宙洪荒"), 16);
|
|
|
|
|
* assertEquals(unicodeWidth("fullwidth"), 18);
|
2024-04-08 04:32:55 +00:00
|
|
|
|
* ```
|
|
|
|
|
*
|
|
|
|
|
* @example Calculating the unicode width of a color-encoded string
|
|
|
|
|
* ```ts
|
2024-04-29 02:57:30 +00:00
|
|
|
|
* import { unicodeWidth } from "@std/cli/unicode-width";
|
|
|
|
|
* import { stripAnsiCode } from "@std/fmt/colors";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-04-08 04:32:55 +00:00
|
|
|
|
*
|
2024-06-03 04:10:27 +00:00
|
|
|
|
* assertEquals(unicodeWidth(stripAnsiCode("\x1b[36mголубой\x1b[39m")), 7);
|
|
|
|
|
* assertEquals(unicodeWidth(stripAnsiCode("\x1b[31m紅色\x1b[39m")), 4);
|
|
|
|
|
* assertEquals(unicodeWidth(stripAnsiCode("\x1B]8;;https://deno.land\x07🦕\x1B]8;;\x07")), 2);
|
2024-04-08 04:32:55 +00:00
|
|
|
|
* ```
|
|
|
|
|
*
|
|
|
|
|
* Use
|
|
|
|
|
* {@linkcode https://jsr.io/@std/fmt/doc/colors/~/stripAnsiCode | stripAnsiCode}
|
|
|
|
|
* to remove ANSI escape codes from a string before passing it to
|
|
|
|
|
* {@linkcode unicodeWidth}.
|
|
|
|
|
*/
|
|
|
|
|
export function unicodeWidth(str: string): number {
|
|
|
|
|
return [...str].map((ch) => charWidth(ch) ?? 0).reduce((a, b) => a + b, 0);
|
|
|
|
|
}
|