| |
| |
| |
|
|
|
|
| class UnicodeIREngine {
|
| constructor() {
|
| this.encoder = new TextEncoder();
|
| this.decoder = new TextDecoder('utf-8');
|
| this.normalizationForm = 'NFC';
|
| }
|
|
|
| |
| |
|
|
| normalize(input, form = this.normalizationForm) {
|
| if (typeof input !== 'string') return '';
|
| try {
|
| return input.normalize(form);
|
| } catch (e) {
|
| console.error('Normalization failed:', e);
|
| return input;
|
| }
|
| }
|
|
|
| |
| |
| |
|
|
| encode(input) {
|
| const normalized = this.normalize(input);
|
| const codePoints = [];
|
| const utf8Bytes = [];
|
|
|
|
|
| for (const char of normalized) {
|
| const codePoint = char.codePointAt(0);
|
| codePoints.push(codePoint);
|
| }
|
|
|
|
|
| const utf8Array = this.encoder.encode(normalized);
|
| for (let i = 0; i < utf8Array.length; i++) {
|
| utf8Bytes.push(utf8Array[i]);
|
| }
|
|
|
| return {
|
| normalized: normalized,
|
| codePoints: codePoints,
|
| utf8Bytes: Array.from(utf8Bytes),
|
| length: codePoints.length,
|
| byteLength: utf8Bytes.length,
|
| };
|
| }
|
|
|
| |
| |
| |
|
|
| decode(irObject) {
|
| if (!irObject || !irObject.utf8Bytes) {
|
| console.error('Invalid IR object');
|
| return '';
|
| }
|
|
|
| try {
|
| const uint8Array = new Uint8Array(irObject.utf8Bytes);
|
| const decoded = this.decoder.decode(uint8Array);
|
| return this.normalize(decoded);
|
| } catch (e) {
|
| console.error('Decoding failed:', e);
|
| return '';
|
| }
|
| }
|
|
|
| |
| |
|
|
| verifyRoundtrip(input) {
|
| const encoded = this.encode(input);
|
| const decoded = this.decode(encoded);
|
| const reencoded = this.encode(decoded);
|
|
|
| return {
|
| success: encoded.codePoints.length === reencoded.codePoints.length &&
|
| encoded.utf8Bytes.length === reencoded.utf8Bytes.length,
|
| original: input,
|
| encoded: encoded,
|
| decoded: decoded,
|
| reencoded: reencoded,
|
| };
|
| }
|
|
|
| |
| |
| |
|
|
| graphemeClusters(input) {
|
| const normalized = this.normalize(input);
|
| const clusters = [];
|
|
|
|
|
| if (typeof Intl !== 'undefined' && Intl.Segmenter) {
|
| const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
| const segments = segmenter.segment(normalized);
|
| for (const segment of segments) {
|
| clusters.push(segment.segment);
|
| }
|
| } else {
|
|
|
| for (const char of normalized) {
|
| clusters.push(char);
|
| }
|
| }
|
|
|
| return clusters;
|
| }
|
|
|
| |
| |
|
|
| detectBidiLevel(input) {
|
|
|
| const rtlRanges = [
|
| [0x0590, 0x08FF],
|
| [0xFB1D, 0xFB4F],
|
| [0xFB50, 0xFDFF],
|
| [0xFE70, 0xFEFF],
|
| ];
|
|
|
| for (const char of input) {
|
| const codePoint = char.codePointAt(0);
|
| for (const [start, end] of rtlRanges) {
|
| if (codePoint >= start && codePoint <= end) {
|
| return 'rtl';
|
| }
|
| }
|
| }
|
| return 'ltr';
|
| }
|
|
|
| |
| |
|
|
| hasAstralCharacters(input) {
|
| for (const char of input) {
|
| if (char.codePointAt(0) > 0xFFFF) {
|
| return true;
|
| }
|
| }
|
| return false;
|
| }
|
|
|
| |
| |
|
|
| findAstralCharacters(input) {
|
| const astral = [];
|
| for (const char of input) {
|
| const codePoint = char.codePointAt(0);
|
| if (codePoint > 0xFFFF) {
|
| astral.push({
|
| char: char,
|
| codePoint: codePoint,
|
| hex: '0x' + codePoint.toString(16).toUpperCase(),
|
| });
|
| }
|
| }
|
| return astral;
|
| }
|
|
|
| |
| |
|
|
| sanitizeForDisplay(input, removeControls = false) {
|
| let result = input;
|
|
|
| if (removeControls) {
|
|
|
| result = result.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
|
| }
|
|
|
| return result;
|
| }
|
|
|
| |
| |
|
|
| hasCombiningMarks(input) {
|
|
|
| for (const char of input) {
|
| const codePoint = char.codePointAt(0);
|
| if (codePoint >= 0x0300 && codePoint <= 0x036F) {
|
| return true;
|
| }
|
| }
|
| return false;
|
| }
|
|
|
| |
| |
|
|
| toJSON(input) {
|
| const ir = this.encode(input);
|
| return {
|
| normalized: ir.normalized,
|
| codePoints: ir.codePoints,
|
| utf8Bytes: ir.utf8Bytes,
|
| metadata: {
|
| length: ir.length,
|
| byteLength: ir.byteLength,
|
| hasAstral: this.hasAstralCharacters(input),
|
| hasCombining: this.hasCombiningMarks(input),
|
| bidiLevel: this.detectBidiLevel(input),
|
| },
|
| };
|
| }
|
|
|
| |
| |
|
|
| fromJSON(obj) {
|
| if (!obj || !obj.utf8Bytes) {
|
| throw new Error('Invalid JSON-safe IR object');
|
| }
|
| return this.decode(obj);
|
| }
|
| }
|
|
|
|
|
| if (typeof module !== 'undefined' && module.exports) {
|
| module.exports = UnicodeIREngine;
|
| }
|
|
|