mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
add utf8 decoder
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"@types/jsdom": "11.0.1",
|
||||
"@types/mocha": "^2.2.33",
|
||||
"@types/node": "6.0.108",
|
||||
"@types/utf8": "^2.1.6",
|
||||
"@types/webpack": "^4.4.11",
|
||||
"browserify": "^13.3.0",
|
||||
"chai": "3.5.0",
|
||||
@@ -39,6 +40,7 @@
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
"typescript": "3.1",
|
||||
"utf8": "^3.0.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^4.17.1",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint } from './TextDecoder';
|
||||
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from './TextDecoder';
|
||||
import { encode } from 'utf8';
|
||||
|
||||
|
||||
// convert UTF32 codepoints to string
|
||||
@@ -19,7 +20,30 @@ function toString(data: Uint32Array, length: number): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('StringToUtf32 Decoder', () => {
|
||||
// convert "bytestring" (charCode 0-255) to bytes
|
||||
function fromByteString(s: string): Uint8Array {
|
||||
const result = new Uint8Array(s.length);
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
result[i] = s.charCodeAt(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
const TEST_STRINGS = [
|
||||
'Лорем ипсум долор сит амет, ех сеа аццусам диссентиет. Ан еос стет еирмод витуперата. Иус дицерет урбанитас ет. Ан при алтера долорес сплендиде, цу яуо интегре денияуе, игнота волуптариа инструцтиор цу вим.',
|
||||
'ლორემ იფსუმ დოლორ სით ამეთ, ფაცერ მუციუს ცონსეთეთურ ყუო იდ, ფერ ვივენდუმ ყუაერენდუმ ეა, ესთ ამეთ მოვეთ სუავითათე ცუ. ვითაე სენსიბუს ან ვიხ. ეხერცი დეთერრუისსეთ უთ ყუი. ვოცენთ დებითის ადიფისცი ეთ ფერ. ნეც ან ფეუგაით ფორენსიბუს ინთერესსეთ. იდ დიცო რიდენს იუს. დისსენთიეთ ცონსეყუუნთურ სედ ნე, ნოვუმ მუნერე ეუმ ათ, ნე ეუმ ნიჰილ ირაცუნდია ურბანითას.',
|
||||
'अधिकांश अमितकुमार प्रोत्साहित मुख्य जाने प्रसारन विश्लेषण विश्व दारी अनुवादक अधिकांश नवंबर विषय गटकउसि गोपनीयता विकास जनित परस्पर गटकउसि अन्तरराष्ट्रीयकरन होसके मानव पुर्णता कम्प्युटर यन्त्रालय प्रति साधन',
|
||||
'覧六子当聞社計文護行情投身斗来。増落世的況上席備界先関権能万。本物挙歯乳全事携供板栃果以。頭月患端撤競見界記引去法条公泊候。決海備駆取品目芸方用朝示上用報。講申務紙約週堂出応理田流団幸稿。起保帯吉対阜庭支肯豪彰属本躍。量抑熊事府募動極都掲仮読岸。自続工就断庫指北速配鳴約事新住米信中験。婚浜袋著金市生交保他取情距。',
|
||||
'八メル務問へふらく博辞説いわょ読全タヨムケ東校どっ知壁テケ禁去フミ人過を装5階がねぜ法逆はじ端40落ミ予竹マヘナセ任1悪た。省ぜりせ製暇ょへそけ風井イ劣手はぼまず郵富法く作断タオイ取座ゅょが出作ホシ月給26島ツチ皇面ユトクイ暮犯リワナヤ断連こうでつ蔭柔薄とレにの。演めけふぱ損田転10得観びトげぎ王物鉄夜がまけ理惜くち牡提づ車惑参ヘカユモ長臓超漫ぼドかわ。',
|
||||
'모든 국민은 행위시의 법률에 의하여 범죄를 구성하지 아니하는 행위로 소추되지 아니하며. 전직대통령의 신분과 예우에 관하여는 법률로 정한다, 국회는 헌법 또는 법률에 특별한 규정이 없는 한 재적의원 과반수의 출석과 출석의원 과반수의 찬성으로 의결한다. 군인·군무원·경찰공무원 기타 법률이 정하는 자가 전투·훈련등 직무집행과 관련하여 받은 손해에 대하여는 법률이 정하는 보상외에 국가 또는 공공단체에 공무원의 직무상 불법행위로 인한 배상은 청구할 수 없다.',
|
||||
'كان فشكّل الشرقي مع, واحدة للمجهود تزامناً بعض بل. وتم جنوب للصين غينيا لم, ان وبدون وكسبت الأمور ذلك, أسر الخاسر الانجليزية هو. نفس لغزو مواقعها هو. الجو علاقة الصعداء انه أي, كما مع بمباركة للإتحاد الوزراء. ترتيب الأولى أن حدى, الشتوية باستحداث مدن بل, كان قد أوسع عملية. الأوضاع بالمطالبة كل قام, دون إذ شمال الربيع،. هُزم الخاصّة ٣٠ أما, مايو الصينية مع قبل.',
|
||||
'או סדר החול מיזמי קרימינולוגיה. קהילה בגרסה לויקיפדים אל היא, של צעד ציור ואלקטרוניקה. מדע מה ברית המזנון ארכיאולוגיה, אל טבלאות מבוקשים כלל. מאמרשיחהצפה העריכהגירסאות שכל אל, כתב עיצוב מושגי של. קבלו קלאסיים ב מתן. נבחרים אווירונאוטיקה אם מלא, לוח למנוע ארכיאולוגיה מה. ארץ לערוך בקרבת מונחונים או, עזרה רקטות לויקיפדים אחר גם.',
|
||||
'Лорем ლორემ अधिकांश 覧六子 八メル 모든 בקרבת 💮 😂 äggg 123€ 𝄞.'
|
||||
];
|
||||
|
||||
|
||||
describe('StringToUtf32 decoder', () => {
|
||||
describe('full codepoint test', () => {
|
||||
it('0..65535', () => {
|
||||
const decoder = new StringToUtf32();
|
||||
@@ -51,6 +75,15 @@ describe('StringToUtf32 Decoder', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
it('test strings', () => {
|
||||
const decoder = new StringToUtf32();
|
||||
const target = new Uint32Array(500);
|
||||
for (let i = 0; i < TEST_STRINGS.length; ++i) {
|
||||
const length = decoder.decode(TEST_STRINGS[i], target);
|
||||
assert.equal(toString(target, length), TEST_STRINGS[i]);
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
describe('stream handling', () => {
|
||||
it('surrogates mixed advance by 1', () => {
|
||||
const decoder = new StringToUtf32();
|
||||
@@ -65,3 +98,102 @@ describe('StringToUtf32 Decoder', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Utf8ToUtf32 decoder', () => {
|
||||
describe('full codepoint test', () => {
|
||||
it('0..65535 (1/2/3 byte sequences)', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = 0; i < 65536; ++i) {
|
||||
// skip surrogate pairs
|
||||
if (i >= 0xD800 && i <= 0xDFFF) {
|
||||
continue;
|
||||
}
|
||||
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 1);
|
||||
assert.equal(toString(target, length), String.fromCharCode(i));
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
it('65536..0x10FFFF (4 byte sequences)', function(): void {
|
||||
this.timeout(20000);
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = 65536; i < 0x10FFFF; ++i) {
|
||||
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 1);
|
||||
assert.equal(target[0], i);
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
it('test strings', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(500);
|
||||
for (let i = 0; i < TEST_STRINGS.length; ++i) {
|
||||
const utf8Data = fromByteString(encode(TEST_STRINGS[i]));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(toString(target, length), TEST_STRINGS[i]);
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
describe('stream handling', () => {
|
||||
it('2 byte sequences - advance by 1', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString('\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f\xc3\xb6\xc3\xa4\xc3\xbc');
|
||||
let decoded = '';
|
||||
for (let i = 0; i < utf8Data.length; ++i) {
|
||||
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
|
||||
decoded += toString(target, written);
|
||||
}
|
||||
assert(decoded, 'ÄÖÜßöäü');
|
||||
});
|
||||
it('2/3 byte sequences - advance by 1', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xc3\x96\xe2\x82\xac\xc3\x9c\xe2\x82\xac\xc3\x9f\xe2\x82\xac\xc3\xb6\xe2\x82\xac\xc3\xa4\xe2\x82\xac\xc3\xbc');
|
||||
let decoded = '';
|
||||
for (let i = 0; i < utf8Data.length; ++i) {
|
||||
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
|
||||
decoded += toString(target, written);
|
||||
}
|
||||
assert(decoded, 'Āր܀߀ö€ä€ü');
|
||||
});
|
||||
it('2/3/4 byte sequences - advance by 1', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
|
||||
let decoded = '';
|
||||
for (let i = 0; i < utf8Data.length; ++i) {
|
||||
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
|
||||
decoded += toString(target, written);
|
||||
}
|
||||
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
|
||||
});
|
||||
it('2/3/4 byte sequences - advance by 2', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
|
||||
let decoded = '';
|
||||
for (let i = 0; i < utf8Data.length; i += 2) {
|
||||
const written = decoder.decode(utf8Data.slice(i, i + 2), target);
|
||||
decoded += toString(target, written);
|
||||
}
|
||||
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
|
||||
});
|
||||
it('2/3/4 byte sequences - advance by 3', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
|
||||
let decoded = '';
|
||||
for (let i = 0; i < utf8Data.length; i += 3) {
|
||||
const written = decoder.decode(utf8Data.slice(i, i + 3), target);
|
||||
decoded += toString(target, written);
|
||||
}
|
||||
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Polyfill - Convert UTF32 codepoint into JS string.
|
||||
*/
|
||||
export function stringFromCodePoint(codePoint: number): string {
|
||||
if (codePoint > 0xFFFF) {
|
||||
codePoint -= 0x10000;
|
||||
return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
|
||||
}
|
||||
return String.fromCharCode(codePoint);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.
|
||||
* To keep the decoder in line with JS strings it handles single surrogates as UCS2.
|
||||
@@ -73,12 +86,232 @@ export class StringToUtf32 {
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill - Convert UTF32 codepoint into JS string.
|
||||
* Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.
|
||||
*/
|
||||
export function stringFromCodePoint(codePoint: number): string {
|
||||
if (codePoint > 0xFFFF) {
|
||||
codePoint -= 0x10000;
|
||||
return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
|
||||
export class Utf8ToUtf32 {
|
||||
public interim: Uint8Array = new Uint8Array(3);
|
||||
|
||||
/**
|
||||
* Clears interim bytes and resets decoder to clean state.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.interim.fill(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.
|
||||
* The methods assumes stream input and will store partly transmitted bytes
|
||||
* and decode them with the next data chunk.
|
||||
* Note: The method does no bound checks for target, therefore make sure
|
||||
* the provided data chunk does not exceed the size of `target`.
|
||||
* Returns the number of written codepoints in `target`.
|
||||
*/
|
||||
decode(input: Uint8Array, target: Uint32Array): number {
|
||||
const length = input.length;
|
||||
|
||||
if (!length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
let byte1;
|
||||
let byte2;
|
||||
let byte3;
|
||||
let byte4;
|
||||
let codepoint = 0;
|
||||
let startPos = 0;
|
||||
|
||||
// handle leftover bytes
|
||||
if (this.interim[0]) {
|
||||
let discardInterim = false;
|
||||
let cp = this.interim[0];
|
||||
cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);
|
||||
let pos = 0;
|
||||
let tmp;
|
||||
while ((tmp = this.interim[++pos] & 0x3F) && pos < 4) {
|
||||
cp <<= 6;
|
||||
cp |= tmp;
|
||||
}
|
||||
// missing bytes - read ahead from input
|
||||
const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;
|
||||
const missing = type - pos;
|
||||
while (startPos < missing) {
|
||||
if (startPos >= length) {
|
||||
return 0;
|
||||
}
|
||||
tmp = input[startPos++];
|
||||
if ((tmp & 0xC0) !== 0x80) {
|
||||
// wrong continuation, discard interim bytes completely
|
||||
startPos--;
|
||||
discardInterim = true;
|
||||
break;
|
||||
} else {
|
||||
// need to save so we can continue short inputs in next call
|
||||
this.interim[pos++] = tmp;
|
||||
cp <<= 6;
|
||||
cp |= tmp & 0x3F;
|
||||
}
|
||||
}
|
||||
if (!discardInterim) {
|
||||
// final test is type dependent
|
||||
if (type === 2) {
|
||||
if (cp < 0x80) {
|
||||
// wrong starter byte
|
||||
startPos--;
|
||||
} else {
|
||||
target[size++] = cp;
|
||||
}
|
||||
} else if (type === 3) {
|
||||
if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF)) {
|
||||
// illegal codepoint
|
||||
} else {
|
||||
target[size++] = cp;
|
||||
}
|
||||
} else {
|
||||
if (codepoint < 0x010000 || codepoint > 0x10FFFF) {
|
||||
// illegal codepoint
|
||||
} else {
|
||||
target[size++] = cp;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.interim.fill(0);
|
||||
}
|
||||
|
||||
// loop through input
|
||||
const fourStop = length - 4;
|
||||
let i = startPos;
|
||||
while (i < length) {
|
||||
|
||||
/**
|
||||
* ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.
|
||||
* This is a compromise between speed gain for ASCII
|
||||
* and penalty for non ASCII:
|
||||
* For best ASCII performance the char should be stored directly into target,
|
||||
* but even a single attempt to write to target and compare afterwards
|
||||
* penalizes non ASCII really bad (-50%), thus we load the char into byteX first,
|
||||
* which reduces ASCII performance by ~15%.
|
||||
* This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible
|
||||
* compared to the gains.
|
||||
* Note that this optimization only takes place for 4 consecutive ASCII chars,
|
||||
* for any shorter it bails out. Worst case - all 4 bytes being read but
|
||||
* thrown away due to the last being a non ASCII char (-10% performance).
|
||||
*/
|
||||
while (i < fourStop
|
||||
&& !((byte1 = input[i]) & 0x80)
|
||||
&& !((byte2 = input[i + 1]) & 0x80)
|
||||
&& !((byte3 = input[i + 2]) & 0x80)
|
||||
&& !((byte4 = input[i + 3]) & 0x80))
|
||||
{
|
||||
target[size++] = byte1;
|
||||
target[size++] = byte2;
|
||||
target[size++] = byte3;
|
||||
target[size++] = byte4;
|
||||
i += 4;
|
||||
}
|
||||
|
||||
// reread byte1
|
||||
byte1 = input[i++];
|
||||
|
||||
// 1 byte
|
||||
if (byte1 < 0x80) {
|
||||
target[size++] = byte1;
|
||||
|
||||
// 2 bytes
|
||||
} else if ((byte1 & 0xE0) === 0xC0) {
|
||||
if (i >= length) {
|
||||
this.interim[0] = byte1;
|
||||
return size;
|
||||
}
|
||||
byte2 = input[i++];
|
||||
if ((byte2 & 0xC0) !== 0x80) {
|
||||
// wrong continuation
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F);
|
||||
if (codepoint < 0x80) {
|
||||
// wrong starter byte
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
target[size++] = codepoint;
|
||||
|
||||
// 3 bytes
|
||||
} else if ((byte1 & 0xF0) === 0xE0) {
|
||||
if (i >= length) {
|
||||
this.interim[0] = byte1;
|
||||
return size;
|
||||
}
|
||||
byte2 = input[i++];
|
||||
if ((byte2 & 0xC0) !== 0x80) {
|
||||
// wrong continuation
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (i >= length) {
|
||||
this.interim[0] = byte1;
|
||||
this.interim[1] = byte2;
|
||||
return size;
|
||||
}
|
||||
byte3 = input[i++];
|
||||
if ((byte3 & 0xC0) !== 0x80) {
|
||||
// wrong continuation
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);
|
||||
if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) {
|
||||
// illegal codepoint, no i-- here
|
||||
continue;
|
||||
}
|
||||
target[size++] = codepoint;
|
||||
|
||||
// 4 bytes
|
||||
} else if ((byte1 & 0xF8) === 0xF0) {
|
||||
if (i >= length) {
|
||||
this.interim[0] = byte1;
|
||||
return size;
|
||||
}
|
||||
byte2 = input[i++];
|
||||
if ((byte2 & 0xC0) !== 0x80) {
|
||||
// wrong continuation
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (i >= length) {
|
||||
this.interim[0] = byte1;
|
||||
this.interim[1] = byte2;
|
||||
return size;
|
||||
}
|
||||
byte3 = input[i++];
|
||||
if ((byte3 & 0xC0) !== 0x80) {
|
||||
// wrong continuation
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (i >= length) {
|
||||
this.interim[0] = byte1;
|
||||
this.interim[1] = byte2;
|
||||
this.interim[2] = byte3;
|
||||
return size;
|
||||
}
|
||||
byte4 = input[i++];
|
||||
if ((byte4 & 0xC0) !== 0x80) {
|
||||
// wrong continuation
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F);
|
||||
if (codepoint < 0x010000 || codepoint > 0x10FFFF) {
|
||||
// illegal codepoint, no i-- here
|
||||
continue;
|
||||
}
|
||||
target[size++] = codepoint;
|
||||
} else {
|
||||
// illegal byte, just skip
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
return String.fromCharCode(codePoint);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user