Home Reference Source

src/encode/Unicode.js

/**
 * The script is part of MojiJS.
 * 
 * AUTHOR:
 *  natade (http://twitter.com/natadea)
 * 
 * LICENSE:
 *  The MIT license https://opensource.org/licenses/MIT
 */

/**
 * Unicode を扱うクラス
 * @ignore
 */
export default class Unicode {

	/**
	 * 上位のサロゲートペアの判定
	 * @param {String} text - 対象テキスト
	 * @param {Number} index - インデックス
	 * @returns {Boolean} 確認結果
	 */
	static isHighSurrogateAt(text, index) {
		const ch = text.charCodeAt(index);
		return ((0xD800 <= ch) && (ch <= 0xDBFF));
	}

	/**
	 * 下位のサロゲートペアの判定
	 * @param {String} text - 対象テキスト
	 * @param {Number} index - インデックス
	 * @returns {Boolean} 確認結果
	 */
	static isLowSurrogateAt(text, index) {
		const ch = text.charCodeAt(index);
		return ((0xDC00 <= ch) && (ch <= 0xDFFF));
	}
	
	/**
	 * サロゲートペアの判定
	 * @param {String} text - 対象テキスト
	 * @param {Number} index - インデックス
	 * @returns {Boolean} 確認結果
	 */
	static isSurrogatePairAt(text, index) {
		const ch = text.charCodeAt(index);
		return ((0xD800 <= ch) && (ch <= 0xDFFF));
	}
	
	/**
	 * サロゲートペア対応のコードポイント取得
	 * @param {String} text - 対象テキスト
	 * @param {Number} [index = 0] - インデックス
	 * @returns {Number} コードポイント
	 */
	static codePointAt(text, index) {
		const index_ = (index !== undefined) ? index : 0;
		if(Unicode.isHighSurrogateAt(text, index_)) {
			const high = text.charCodeAt(index_);
			const low  = text.charCodeAt(index_ + 1);
			return ((((high - 0xD800) << 10) | (low - 0xDC00)) + 0x10000);
		}
		else {
			return (text.charCodeAt(index_));
		}
	}

	/**
	 * インデックスの前にあるコードポイント
	 * @param {String} text - 対象テキスト
	 * @param {Number} index - インデックス
	 * @returns {Number} コードポイント
	 */
	static codePointBefore(text, index) {
		if(!Unicode.isLowSurrogateAt(text, index - 1)) {
			return (text.charCodeAt(index - 1));
		}
		else {
			return (text.codePointAt(index - 2));
		}
	}

	/**
	 * コードポイント換算で文字列数をカウント
	 * @param {String} text - 対象テキスト
	 * @param {Number} [beginIndex=0] - 最初のインデックス(省略可)
	 * @param {Number} [endIndex] - 最後のインデックス(ここは含めない)(省略可)
	 * @returns {Number} 文字数
	 */
	static codePointCount(text, beginIndex, endIndex) {
		if(beginIndex === undefined) {
			beginIndex = 0;
		}
		if(endIndex === undefined) {
			endIndex = text.length;
		}
		let count = 0;
		for(;beginIndex < endIndex;beginIndex++) {
			count++;
			if(Unicode.isSurrogatePairAt(text, beginIndex)) {
				beginIndex++;
			}
		}
		return count;
	}

	/**
	 * コードポイント換算で文字列配列の位置を計算
	 * @param {String} text - 対象テキスト
	 * @param {Number} index - オフセット
	 * @param {Number} codePointOffset - ずらすコードポイント数
	 * @returns {Number} ずらしたインデックス
	 */
	static offsetByCodePoints(text, index, codePointOffset) {
		let count = 0;
		if(codePointOffset === 0) {
			return (index);
		}
		if(codePointOffset > 0) {
			for(;index < text.length;index++) {
				count++;
				if(Unicode.isHighSurrogateAt(text, index)) {
					index++;
				}
				if(count === codePointOffset) {
					return (index + 1);
				}
			}

		}
		else {
			codePointOffset = -codePointOffset;
			for(;index >= 0;index--) {
				count++;
				if(Unicode.isLowSurrogateAt(text, index - 1)) {
					index--;
				}
				if(count === codePointOffset) {
					return (index - 1);
				}
			}
		}
		throw "error offsetByCodePoints";
	}

	/**
	 * コードポイントの数値データをUTF16の配列に変換
	 * @param {...(number|Array<number>)} codepoint - 変換したいUTF-32の配列、又はコードポイントを並べた可変引数
	 * @returns {Array<number>} 変換後のテキスト
	 */
	static toUTF16ArrayFromCodePoint() {
		/**
		 * @type {Array<number>}
		 */
		const utf16_array = [];
		/**
		 * @type {Array<number>}
		 */
		let codepoint_array = [];
		if(arguments[0].length) {
			codepoint_array = arguments[0];
		}
		else {
			for(let i = 0;i < arguments.length;i++) {
				codepoint_array[i] = arguments[i];
			}
		}
		for(let i = 0;i < codepoint_array.length;i++) {
			const codepoint = codepoint_array[i];
			if(0x10000 <= codepoint) {
				const high = (( codepoint - 0x10000 ) >> 10) + 0xD800;
				const low  = (codepoint & 0x3FF) + 0xDC00;
				utf16_array.push(high);
				utf16_array.push(low);
			}
			else {
				utf16_array.push(codepoint);
			}
		}
		return utf16_array;
	}

	/**
	 * コードポイントの数値データを文字列に変換
	 * @param {...(number|Array<number>)} codepoint - 変換したいコードポイントの数値配列、又は数値を並べた可変引数
	 * @returns {String} 変換後のテキスト
	 */
	static fromCodePoint(codepoint) {
		let utf16_array = null;
		if(codepoint instanceof Array) {
			utf16_array = Unicode.toUTF16ArrayFromCodePoint(codepoint);
		}
		else {
			const codepoint_array = [];
			for(let i = 0;i < arguments.length;i++) {
				codepoint_array[i] = arguments[i];
			}
			utf16_array = Unicode.toUTF16ArrayFromCodePoint(codepoint_array);
		}
		const text = [];
		for(let i = 0;i < utf16_array.length;i++) {
			text[text.length] = String.fromCharCode(utf16_array[i]);
		}
		return(text.join(""));
	}

	/**
	 * 文字列をUTF32(コードポイント)の配列に変換
	 * @param {String} text - 変換したいテキスト
	 * @returns {Array<number>} UTF32(コードポイント)のデータが入った配列
	 */
	static toUTF32Array(text) {
		const utf32 = [];
		for(let i = 0; i < text.length; i = Unicode.offsetByCodePoints(text, i, 1)) {
			utf32.push(Unicode.codePointAt(text, i));
		}
		return utf32;
	}

	/**
	 * UTF32の配列から文字列に変換
	 * @param {Array<number>} utf32 - 変換したいテキスト
	 * @returns {String} 変換後のテキスト
	 */
	static fromUTF32Array(utf32) {
		return Unicode.fromCodePoint(utf32);
	}

	/**
	 * 文字列をUTF16の配列に変換
	 * @param {String} text - 変換したいテキスト
	 * @returns {Array<number>} UTF16のデータが入った配列
	 */
	static toUTF16Array(text) {
		const utf16 = [];
		for(let i = 0; i < text.length; i++) {
			utf16[i] = text.charCodeAt(i);
		}
		return utf16;
	}

	/**
	 * UTF16の配列から文字列に変換
	 * @param {Array<number>} utf16 - 変換したいテキスト
	 * @returns {String} 変換後のテキスト
	 */
	static fromUTF16Array(utf16) {
		const text = [];
		for(let i = 0; i < utf16.length; i++) {
			text[i] = String.fromCharCode(utf16[i]);
		}
		return text.join("");
	}

	/**
	 * 文字列をUTF8の配列に変換
	 * @param {String} text - 変換したいテキスト
	 * @returns {Array<number>} UTF8のデータが入った配列
	 */
	static toUTF8Array(text) {
		return Unicode.toUTFBinaryFromCodePoint(Unicode.toUTF32Array(text), "utf-8", false);
	}

	/**
	 * UTF8の配列から文字列に変換
	 * @param {Array<number>} utf8 - 変換したいテキスト
	 * @returns {String} 変換後のテキスト
	 */
	static fromUTF8Array(utf8) {
		return Unicode.fromCodePoint(Unicode.toCodePointFromUTFBinary(utf8, "utf-8"));
	}

	/**
	 * 指定したテキストを切り出す
	 * - 単位は文字数
	 * @param {String} text - 切り出したいテキスト
	 * @param {Number} offset - 切り出し位置
	 * @param {Number} size - 切り出す長さ
	 * @returns {String} 切り出したテキスト
	 */
	static cutTextForCodePoint(text, offset, size) {
		const utf32 = Unicode.toUTF32Array(text);
		const cut = [];
		for(let i = 0, point = offset; ((i < size) && (point < utf32.length)); i++, point++) {
			cut.push(utf32[point]);
		}
		return Unicode.fromUTF32Array(cut);
	}

	/**
	 * UTFのバイナリ配列からバイトオーダーマーク(BOM)を調査する
	 * @param {Array<number>} utfbinary - 調査するバイナリ配列
	 * @returns {string} 符号化形式(不明時はnull)
	 */
	static getCharsetFromBOM(utfbinary) {
		if(utfbinary.length >= 4) {
			if((utfbinary[0] === 0x00) && (utfbinary[1] === 0x00) && (utfbinary[2] === 0xFE) && (utfbinary[3] === 0xFF)) {
				return "UTF-32BE";
			}
			if((utfbinary[0] === 0xFF) && (utfbinary[1] === 0xFE) && (utfbinary[2] === 0x00) && (utfbinary[3] === 0x00)) {
				return "UTF-32LE";
			}
		}
		if(utfbinary.length >= 3) {
			if((utfbinary[0] === 0xEF) && (utfbinary[1] === 0xBB) && (utfbinary[2] === 0xBF)) {
				return "UTF-8";
			}
		}
		if(utfbinary.length >= 2) {
			if((utfbinary[0] === 0xFE) && (utfbinary[1] === 0xFF)) {
				return "UTF-16BE";
			}
			if((utfbinary[0] === 0xFF) && (utfbinary[1] === 0xFE)) {
				return "UTF-16LE";
			}
		}
		return null;
	}

	/**
	 * UTFのバイナリ配列からコードポイントに変換
	 * @param {Array<number>} binary - 変換したいバイナリ配列
	 * @param {String} [charset] - UTFの種類(省略した場合はBOM付きを期待する)
	 * @returns {Array<number>} コードポイントの配列(失敗時はnull)
	 */
	static toCodePointFromUTFBinary(binary, charset) {
		const utf32_array = [];
		let check_charset = charset;
		let offset = 0;
		// バイトオーダーマーク(BOM)がある場合は BOM を優先
		const charset_for_bom = Unicode.getCharsetFromBOM(binary);
		if(charset_for_bom) {
			check_charset = charset_for_bom;
			if(/utf-?8/i.test(charset_for_bom)) {
				offset = 3;
			}
			else if(/utf-?16/i.test(charset_for_bom)) {
				offset = 2;
			}
			else if(/utf-?32/i.test(charset_for_bom)) {
				offset = 4;
			}
		}
		// BOM付きではない+指定もしていないので変換失敗
		if(!charset_for_bom && !charset) {
			return null;
		}
		// UTF-8
		if(/utf-?8n?/i.test(check_charset)) {
			let size = 0;
			let write = 0;
			for(let i = offset; i < binary.length; i++) {
				const bin = binary[i];
				if(size === 0) {
					if(bin < 0x80) {
						utf32_array.push(bin);
					}
					else if(bin < 0xE0) {
						size = 1;
						write = bin & 0x1F; // 0001 1111
					}
					else if(bin < 0xF0) {
						size = 2;
						write = bin & 0xF; // 0000 1111
					}
					else {
						size = 3;
						write = bin & 0x7; // 0000 0111
					}
				}
				else {
					write <<= 6;
					write |= bin & 0x3F; // 0011 1111
					size--;
					if(size === 0) {
						utf32_array.push(write);
					}
				}
			}
			return utf32_array;
		}
		// UTF-16
		else if(/utf-?16/i.test(check_charset)) {
			// UTF-16 につめる
			const utf16 = [];
			// UTF-16BE
			if(/utf-?16(be)/i.test(check_charset)) {
				for(let i = offset; i < binary.length; i += 2) {
					utf16.push((binary[i] << 8) | binary[i + 1]);
				}
			}
			// UTF-16LE
			else if(/utf-?16(le)?/i.test(check_charset)) {
				for(let i = offset; i < binary.length; i += 2) {
					utf16.push(binary[i] | (binary[i + 1] << 8));
				}
			}
			// UTF-32 につめる
			for(let i = 0; i < utf16.length; i++) {
				if((0xD800 <= utf16[i]) && (utf16[i] <= 0xDBFF)) {
					if(i + 2 <= utf16.length) {
						const high = utf16[i];
						const low  = utf16[i + 1];
						utf32_array.push((((high - 0xD800) << 10) | (low - 0xDC00)) + 0x10000);
					}
					i++;
				}
				else {
					utf32_array.push(utf16[i]);
				}
			}
			return utf32_array;
		}
		// UTF-32
		else {
			// UTF-32BE
			if(/utf-?32(be)/i.test(check_charset)) {
				for(let i = offset; i < binary.length; i += 4) {
					utf32_array.push((binary[i] << 24) | (binary[i + 1] << 16) | (binary[i + 2] << 8) | binary[i + 3]);
				}
				return utf32_array;
			}
			// UTF-32LE
			else if(/utf-?32(le)?/i.test(check_charset)) {
				for(let i = offset; i < binary.length; i += 4) {
					utf32_array.push(binary[i] | (binary[i + 1] << 8) | (binary[i + 2] << 16) | (binary[i + 3] << 24));
				}
				return utf32_array;
			}
		}
		return null;
	}

	/**
	 * UTF32配列からバイナリ配列に変換
	 * @param {Array<number>} utf32_array - 変換したいUTF-32配列
	 * @param {String} charset - UTFの種類
	 * @param {boolean} [is_with_bom=true] - BOMをつけるかどうか
	 * @returns {Array<number>} バイナリ配列(失敗時はnull)
	 */
	static toUTFBinaryFromCodePoint(utf32_array, charset, is_with_bom) {
		let is_with_bom_ = is_with_bom !== undefined ? is_with_bom : true;
		// charset に" with BOM" が入っている場合はBOM付きとする
		if(/\s+with\s+bom$/i.test(charset)) {
			is_with_bom_ = true;
		}
		/**
		 * @type {Array<number>}
		 */
		const binary = [];
		// UTF-8
		if(/utf-?8n?/i.test(charset)) {
			// bom をつける
			if(is_with_bom_) {
				binary.push(0xEF);
				binary.push(0xBB);
				binary.push(0xBF);
			}
			for(let i = 0; i < utf32_array.length; i++) {
				let codepoint = utf32_array[i];
				// 1バイト文字
				if(codepoint <= 0x7F) {
					binary.push(codepoint);
					continue;
				}
				const buffer = [];
				let size = 0;
				// 2バイト以上
				if(codepoint < 0x800) {
					size = 2;
				}
				else if(codepoint < 0x10000) {
					size = 3;
				}
				else {
					size = 4;
				}
				for(let j = 0; j < size; j++) {
					let write = codepoint & ((1 << 6) - 1);
					if(j === size - 1) {
						if(size === 2) {
							write |= 0xC0; // 1100 0000
						}
						else if(size === 3) {
							write |= 0xE0; // 1110 0000
						}
						else {
							write |= 0xF0; // 1111 0000
						}
						buffer.push(write);
						break;
					}
					buffer.push(write | 0x80); // 1000 0000
					codepoint = codepoint >> 6;
				}
				// 反転
				for(let j = buffer.length - 1; j >= 0; j--) {
					binary.push(buffer[j]);
				}
			}
			return binary;
		}
		// UTF-16
		else if(/utf-?16/i.test(charset)) {
			// UTF-16 に詰め替える
			const utf16_array = Unicode.toUTF16ArrayFromCodePoint(utf32_array);
			// UTF-16BE
			if(/utf-?16(be)/i.test(charset)) {
				// bom をつける
				if(is_with_bom_) {
					binary.push(0xFE);
					binary.push(0xFF);
				}
				for(let i = 0; i < utf16_array.length; i++ ) {
					binary.push(utf16_array[i] >> 8);
					binary.push(utf16_array[i] & 0xff);
				}
			}
			// UTF-16LE
			else if(/utf-?16(le)?/i.test(charset)) {
				// bom をつける
				if(is_with_bom_) {
					binary.push(0xFF);
					binary.push(0xFE);
				}
				for(let i = 0; i < utf16_array.length; i++ ) {
					binary.push(utf16_array[i] & 0xff);
					binary.push(utf16_array[i] >> 8);
				}
			}
			return binary;
		}
		// UTF-32
		else if(/utf-?32/i.test(charset)) {
			// UTF-32BE
			if(/utf-?32(be)/i.test(charset)) {
				// bom をつける
				if(is_with_bom_) {
					binary.push(0x00);
					binary.push(0x00);
					binary.push(0xFE);
					binary.push(0xFF);
				}
				for(let i = 0; i < utf32_array.length; i++) {
					binary.push((utf32_array[i] >> 24) & 0xff);
					binary.push((utf32_array[i] >> 16) & 0xff);
					binary.push((utf32_array[i] >> 8) & 0xff);
					binary.push(utf32_array[i] & 0xff);
				}
			}
			// UTF-32LE
			else if(/utf-?32(le)?/i.test(charset)) {
				// bom をつける
				if(is_with_bom_) {
					binary.push(0xFF);
					binary.push(0xFE);
					binary.push(0x00);
					binary.push(0x00);
				}
				for(let i = 0; i < utf32_array.length; i++) {
					binary.push(utf32_array[i] & 0xff);
					binary.push((utf32_array[i] >> 8) & 0xff);
					binary.push((utf32_array[i] >> 16) & 0xff);
					binary.push((utf32_array[i] >> 24) & 0xff);
				}
			}
			return binary;
		}
		return null;
	}

	/**
	 * コードポイントから異体字セレクタの判定
	 * @param {Number} codepoint - コードポイント
	 * @returns {boolean} 確認結果
	 */
	static isVariationSelectorFromCodePoint(codepoint) {
		return (
			// モンゴル自由字形選択子 U+180B〜U+180D (3個)
			((0x180B <= codepoint) && (codepoint <= 0x180D)) ||
			// SVSで利用される異体字セレクタ U+FE00〜U+FE0F (VS1~VS16) (16個)
			((0xFE00 <= codepoint) && (codepoint <= 0xFE0F)) ||
			// IVSで利用される異体字セレクタ U+E0100〜U+E01EF (VS17~VS256) (240個)
			((0xE0100 <= codepoint) && (codepoint <= 0xE01EF))
		);
	}

	/**
	 * コードポイントから結合文字の判定
	 * @param {Number} codepoint - コードポイント
	 * @returns {boolean} 確認結果
	 */
	static isCombiningMarkFromCodePoint(codepoint) {
		return (
			// Combining Diacritical Marks
			((0x0300 <= codepoint) && (codepoint <= 0x036F)) ||
			// Combining Diacritical Marks Extended
			((0x1AB0 <= codepoint) && (codepoint <= 0x1AFF)) ||
			// Combining Diacritical Marks Supplement
			((0x1DC0 <= codepoint) && (codepoint <= 0x1DFF)) ||
			// Combining Diacritical Marks for Symbols
			((0x20D0 <= codepoint) && (codepoint <= 0x20FF)) ||
			// Hiragana 含まれる4種類の文字
			((0x3099 <= codepoint) && (codepoint <= 0x309C)) ||
			// Combining Half Marks
			((0xFE20 <= codepoint) && (codepoint <= 0xFE2F))
		);
	}



}