react-native-encoding-api
Version:
A React Native package for handling text encoding and decoding
150 lines (135 loc) • 4.67 kB
text/typescript
/**
* A drop-in replacement for the native TextEncoder/TextDecoder that's missing in React Native.
* Provides the exact same API and behavior as the Web API's TextEncoder/TextDecoder.
*/
export class TextEncoder {
/**
* Encodes a string into UTF-8 bytes
* @param text The text to encode
* @returns A Uint8Array containing the UTF-8 encoded bytes
*/
encode(text: string): Uint8Array {
const bytes = new Uint8Array(text.length * 4); // Maximum possible size
let i = 0;
let j = 0;
while (i < text.length) {
const charCode = text.charCodeAt(i++);
if (charCode < 0x80) {
bytes[j++] = charCode;
} else if (charCode < 0x800) {
bytes[j++] = 0xC0 | (charCode >> 6);
bytes[j++] = 0x80 | (charCode & 0x3F);
} else if (charCode < 0xD800 || charCode >= 0xE000) {
bytes[j++] = 0xE0 | (charCode >> 12);
bytes[j++] = 0x80 | ((charCode >> 6) & 0x3F);
bytes[j++] = 0x80 | (charCode & 0x3F);
} else {
// Surrogate pair
const nextCharCode = text.charCodeAt(i++);
const codePoint = ((charCode - 0xD800) << 10) + (nextCharCode - 0xDC00) + 0x10000;
bytes[j++] = 0xF0 | (codePoint >> 18);
bytes[j++] = 0x80 | ((codePoint >> 12) & 0x3F);
bytes[j++] = 0x80 | ((codePoint >> 6) & 0x3F);
bytes[j++] = 0x80 | (codePoint & 0x3F);
}
}
return bytes.slice(0, j);
}
}
export class TextDecoder {
private encoding: string;
private fatal: boolean;
private ignoreBOM: boolean;
constructor(encoding: string = 'utf-8', options: { fatal?: boolean; ignoreBOM?: boolean } = {}) {
this.encoding = encoding.toLowerCase();
this.fatal = options.fatal || false;
this.ignoreBOM = options.ignoreBOM || false;
if (this.encoding !== 'utf-8') {
throw new Error('Only UTF-8 encoding is supported');
}
}
/**
* Decodes a Uint8Array into a string using UTF-8 encoding
* @param bytes The input array to decode
* @returns The decoded string
*/
decode(bytes: Uint8Array): string {
if (!(bytes instanceof Uint8Array)) {
throw new TypeError('Expected Uint8Array');
}
let result = '';
let i = 0;
// Handle BOM if present and not ignored
if (!this.ignoreBOM && bytes.length >= 3 &&
bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) {
i = 3;
}
while (i < bytes.length) {
const byte1 = bytes[i++];
if (byte1 < 0x80) {
result += String.fromCharCode(byte1);
} else if (byte1 < 0xE0) {
if (i >= bytes.length) {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
break;
}
const byte2 = bytes[i++];
if ((byte2 & 0xC0) !== 0x80) {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
continue;
}
result += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
} else if (byte1 < 0xF0) {
if (i + 1 >= bytes.length) {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
break;
}
const byte2 = bytes[i++];
const byte3 = bytes[i++];
if ((byte2 & 0xC0) !== 0x80 || (byte3 & 0xC0) !== 0x80) {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
continue;
}
result += String.fromCharCode(
((byte1 & 0x0F) << 12) |
((byte2 & 0x3F) << 6) |
(byte3 & 0x3F)
);
} else if (byte1 < 0xF5) {
if (i + 2 >= bytes.length) {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
break;
}
const byte2 = bytes[i++];
const byte3 = bytes[i++];
const byte4 = bytes[i++];
if ((byte2 & 0xC0) !== 0x80 || (byte3 & 0xC0) !== 0x80 || (byte4 & 0xC0) !== 0x80) {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
continue;
}
const codepoint =
((byte1 & 0x07) << 18) |
((byte2 & 0x3F) << 12) |
((byte3 & 0x3F) << 6) |
(byte4 & 0x3F);
if (codepoint > 0x10FFFF) {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
continue;
}
if (codepoint < 0x10000) {
result += String.fromCharCode(codepoint);
} else {
const offset = codepoint - 0x10000;
result += String.fromCharCode(
0xD800 + (offset >> 10),
0xDC00 + (offset & 0x3FF)
);
}
} else {
if (this.fatal) throw new Error('Invalid UTF-8 sequence');
continue;
}
}
return result;
}
}