neo-neo-bblessed
Version:
A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.
292 lines (247 loc) • 7.42 kB
text/typescript
/**
* unicode.ts - modern unicode utilities using npm packages
* Copyright (c) 2025, Contributors (MIT License).
* Replaces embedded unicode libraries with proper npm dependencies
*/
import * as eastasianwidth from 'eastasianwidth';
import * as unicode from 'unicode-properties';
/**
* Wide, Surrogates, and Combining
*/
function charWidth(str: string | number, i?: number): number {
const point = typeof str !== 'number' ? codePointAt(str, i || 0) : str;
// nul
if (point === 0) return 0;
// tab
if (point === 0x09) {
// Default tab width is 8, can be overridden by environment variable
return +(process.env['BLESSED_TAB_WIDTH'] || 8);
}
// 8-bit control characters (2-width according to unicode??)
if (point < 32 || (point >= 0x7f && point < 0xa0)) {
return 0;
}
// Use unicode-properties to check for combining characters
if (
unicode.getCategory(point) === 'Mn' ||
unicode.getCategory(point) === 'Me' ||
unicode.getCategory(point) === 'Mc'
) {
return 0;
}
// Use eastasianwidth package for proper width detection
const width = eastasianwidth.eastAsianWidth(String.fromCodePoint(point));
switch (width) {
case 'F': // Fullwidth
case 'W': // Wide
return 2;
case 'H': // Halfwidth
case 'Na': // Narrow
case 'N': // Neutral
return 1;
case 'A': // Ambiguous
// Handle CJK Ambiguous based on environment variable
if (process.env['NCURSES_CJK_WIDTH']) {
return +process.env['NCURSES_CJK_WIDTH'] || 1;
}
return 1;
default:
return 1;
}
}
function strWidth(str: string): number {
let width = 0;
for (let i = 0; i < str.length; i++) {
width += charWidth(str, i);
if (isSurrogate(str, i)) i++;
}
return width;
}
function isSurrogate(str: string | number, i?: number): boolean {
const point = typeof str !== 'number' ? codePointAt(str, i || 0) : str;
return point > 0x00ffff;
}
/**
* Use unicode-properties for combining character detection
*/
function isCombining(str: string | number, i?: number): boolean {
const point = typeof str !== 'number' ? codePointAt(str, i || 0) : str;
const category = unicode.getCategory(point);
return category === 'Mn' || category === 'Me' || category === 'Mc';
}
/**
* Code Point Helpers - using native implementations when available
*/
function codePointAt(str: string, position?: number): number {
if (str == null) {
throw TypeError();
}
const string = String(str);
// Use native implementation if available
if (string.codePointAt) {
return string.codePointAt(position || 0) || 0;
}
// Fallback implementation
const size = string.length;
const index = position ? Number(position) : 0;
if (index !== index) {
// better `isNaN`
return 0;
}
// Account for out-of-bounds indices:
if (index < 0 || index >= size) {
return 0;
}
// Get the first code unit
const first = string.charCodeAt(index);
let second;
if (
// check if it's the start of a surrogate pair
first >= 0xd800 &&
first <= 0xdbff && // high surrogate
size > index + 1 // there is a next code unit
) {
second = string.charCodeAt(index + 1);
if (second >= 0xdc00 && second <= 0xdfff) {
// low surrogate
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
return (first - 0xd800) * 0x400 + second - 0xdc00 + 0x10000;
}
}
return first;
}
function fromCodePoint(...codePoints: number[]): string {
// Use native implementation if available
if (String.fromCodePoint) {
return String.fromCodePoint(...codePoints);
}
// Fallback implementation
const MAX_SIZE = 0x4000;
const codeUnits: number[] = [];
let highSurrogate: number;
let lowSurrogate: number;
let index = -1;
const length = codePoints.length;
if (!length) {
return '';
}
let result = '';
while (++index < length) {
const codePoint = Number(codePoints[index]);
if (
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
codePoint < 0 || // not a valid Unicode code point
codePoint > 0x10ffff || // not a valid Unicode code point
Math.floor(codePoint) !== codePoint // not an integer
) {
throw RangeError('Invalid code point: ' + codePoint);
}
if (codePoint <= 0xffff) {
// BMP code point
codeUnits.push(codePoint);
} else {
// Astral code point; split in surrogate halves
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
const adjustedCodePoint = codePoint - 0x10000;
highSurrogate = (adjustedCodePoint >> 10) + 0xd800;
lowSurrogate = (adjustedCodePoint % 0x400) + 0xdc00;
codeUnits.push(highSurrogate, lowSurrogate);
}
if (index + 1 === length || codeUnits.length > MAX_SIZE) {
result += String.fromCharCode(...codeUnits);
codeUnits.length = 0;
}
}
return result;
}
/**
* Regexes for compatibility
*/
const chars = {
// Double width characters that are _not_ surrogate pairs.
wide: new RegExp(
'([' +
'\\u1100-\\u115f' + // Hangul Jamo init. consonants
'\\u2329\\u232a' +
'\\u2e80-\\u303e\\u3040-\\ua4cf' + // CJK ... Yi
'\\uac00-\\ud7a3' + // Hangul Syllables
'\\uf900-\\ufaff' + // CJK Compatibility Ideographs
'\\ufe10-\\ufe19' + // Vertical forms
'\\ufe30-\\ufe6f' + // CJK Compatibility Forms
'\\uff00-\\uff60' + // Fullwidth Forms
'\\uffe0-\\uffe6' +
'])',
'g'
),
// All surrogate pair wide chars.
swide: new RegExp(
'(' +
// 0x20000 - 0x2fffd:
'[\\ud840-\\ud87f][\\udc00-\\udffd]' +
'|' +
// 0x30000 - 0x3fffd:
'[\\ud880-\\ud8bf][\\udc00-\\udffd]' +
')',
'g'
),
// Regex to detect a surrogate pair.
surrogate: /[\ud800-\udbff][\udc00-\udfff]/g,
// Basic combining regex (simplified, using unicode-properties for actual detection)
combining:
/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed]/g,
};
// All wide chars including surrogate pairs.
const charsWithAll = {
...chars,
all: new RegExp(
'(' +
chars.swide.source.slice(1, -1) +
'|' +
chars.wide.source.slice(1, -1) +
')',
'g'
),
};
// Backward compatibility - these were exports in the original
// Legacy export compatibility
const combining = {};
const combiningTable: number[][] = [];
// Initialize combining lookup for backward compatibility
function initializeCombining() {
for (let i = 0; i <= 0x10ffff; i++) {
const category = unicode.getCategory(i);
if (category === 'Mn' || category === 'Me' || category === 'Mc') {
(combining as any)[i] = true;
}
}
}
// Lazy initialization
let combiningInitialized = false;
function ensureCombiningInitialized() {
if (!combiningInitialized) {
initializeCombining();
combiningInitialized = true;
}
}
// Legacy exports for CommonJS compatibility
const combiningProxy = new Proxy(
{},
{
get(_target, prop) {
ensureCombiningInitialized();
return (combining as any)[prop];
},
}
);
// Export everything properly for ESM
export {
charWidth,
strWidth,
isSurrogate,
isCombining,
codePointAt,
fromCodePoint,
charsWithAll as chars,
combiningProxy as combining,
combiningTable,
};