Javascript и строковые манипуляции с суррогатными парами utf-16
Я работаю над твиттер-приложением и просто наткнулся на мир utf-8 (16). Похоже, что большинство функций строки javascript так же слепы к суррогатным парам, как и я. Мне нужно перекодировать некоторые вещи, чтобы они были осведомлены о широком знании.
У меня есть эта функция для синтаксического анализа строк в массивах при сохранении суррогатных пар. Затем я перекодирую несколько функций для обработки массивов, а не строк.
function sortSurrogates(str){
var cp = []; // array to hold code points
while(str.length){ // loop till we've done the whole string
if(/[\uD800-\uDFFF]/.test(str.substr(0,1))){ // test the first character
// High surrogate found low surrogate follows
cp.push(str.substr(0,2)); // push the two onto array
str = str.substr(2); // clip the two off the string
}else{ // else BMP code point
cp.push(str.substr(0,1)); // push one onto array
str = str.substr(1); // clip one from string
}
} // loop
return cp; // return the array
}
Мой вопрос: есть ли что-то более простое, что мне не хватает? Я вижу, что многие люди повторяют, что javascript имеет дело с utf-16 изначально, но мое тестирование заставляет меня поверить, что это может быть формат данных, но функции еще этого не знают. Я пропустил что-то простое?
EDIT:
Чтобы помочь проиллюстрировать проблему:
var a = "0123456789"; // U+0030 - U+0039 2 bytes each
var b = "𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡"; // U+1D7D8 - U+1D7E1 4 bytes each
alert(a.length); // javascript shows 10
alert(b.length); // javascript shows 20
Twitter видит и считает, что оба из них имеют длину 10 символов.
Ответы
Ответ 1
Javascript использует UCS-2 внутренне, что не является UTF-16. Из-за этого очень сложно обрабатывать Unicode в Javascript, и я не предлагаю этого сделать.
Что касается того, что делает Twitter, вы, кажется, говорите, что это безопасный подсчет по кодовой точке не безумно с помощью блока кода.
Если у вас нет выбора, вы должны использовать язык программирования, который фактически поддерживает Unicode и который имеет интерфейс с кодовой точки, а не интерфейс кода. Javascript недостаточно хорош для этого, как вы обнаружили.
У этого есть Проклятие UCS-2, что даже хуже, чем Проклятие UTF-16, которое уже достаточно плохо. Я говорю обо всем этом в разговоре OSCON, 🔫 Unicode Support Shootout: 👍 The Good, the Bad и (в основном) Ugly 👎.
Из-за его ужасного проклятия вам нужно вручную имитировать UTF-16 с UCS-2 в Javascript, что является просто орехом.
Javascript страдает от всех других ужасных проблем Unicode. У него нет поддержки графем или нормализации или сортировки, которые вам действительно нужны. И его регулярные выражения сломаны, иногда из-за Проклятия, иногда только потому, что люди ошибались. Например, Javascript не способен выражать регулярные выражения типа [𝒜-𝒵]
. Javascript даже не поддерживает casefolding, поэтому вы не можете написать шаблон, подобный /ΣΤΙΓΜΑΣ/i
, и правильно его сопоставить с στιγμας.
Вы можете попытаться использовать плагин XRegEXp, но вы не будете изгонять проклятие таким образом. Только переход на язык с поддержкой Unicode сделает это, а 𝒥𝒶𝓋𝒶𝓈𝒸𝓇𝒾𝓅𝓉 просто не является одним из них.
Ответ 2
Я сбил начальную точку для объекта обработки строки Unicode. Он создает функцию под названием UnicodeString()
, которая принимает либо строку JavaScript, либо массив целых чисел, представляющих кодовые точки Unicode, и предоставляет свойства length
и codePoints
и методы toString()
и slice()
. Добавление поддержки регулярных выражений будет очень сложным, но такие вещи, как indexOf()
и split()
(без поддержки регулярных выражений), должны быть довольно легко реализованы.
var UnicodeString = (function() {
function surrogatePairToCodePoint(charCode1, charCode2) {
return ((charCode1 & 0x3FF) << 10) + (charCode2 & 0x3FF) + 0x10000;
}
function stringToCodePointArray(str) {
var codePoints = [], i = 0, charCode;
while (i < str.length) {
charCode = str.charCodeAt(i);
if ((charCode & 0xF800) == 0xD800) {
codePoints.push(surrogatePairToCodePoint(charCode, str.charCodeAt(++i)));
} else {
codePoints.push(charCode);
}
++i;
}
return codePoints;
}
function codePointArrayToString(codePoints) {
var stringParts = [];
for (var i = 0, len = codePoints.length, codePoint, offset, codePointCharCodes; i < len; ++i) {
codePoint = codePoints[i];
if (codePoint > 0xFFFF) {
offset = codePoint - 0x10000;
codePointCharCodes = [0xD800 + (offset >> 10), 0xDC00 + (offset & 0x3FF)];
} else {
codePointCharCodes = [codePoint];
}
stringParts.push(String.fromCharCode.apply(String, codePointCharCodes));
}
return stringParts.join("");
}
function UnicodeString(arg) {
if (this instanceof UnicodeString) {
this.codePoints = (typeof arg == "string") ? stringToCodePointArray(arg) : arg;
this.length = this.codePoints.length;
} else {
return new UnicodeString(arg);
}
}
UnicodeString.prototype = {
slice: function(start, end) {
return new UnicodeString(this.codePoints.slice(start, end));
},
toString: function() {
return codePointArrayToString(this.codePoints);
}
};
return UnicodeString;
})();
var ustr = UnicodeString("f𝌆𝌆bar");
document.getElementById("output").textContent = "String: '" + ustr + "', length: " + ustr.length + ", slice(2, 4): " + ustr.slice(2, 4);
<div id="output"></div>
Ответ 3
Вот несколько сценариев, которые могут быть полезны при работе с суррогатными парами в JavaScript:
Ответ 4
Этот соответствует тем, что я искал. Он нуждается в лучшей поддержке для различных строковых функций. По мере добавления к нему я обновлю этот ответ.
function wString(str){
var T = this; //makes 'this' visible in functions
T.cp = []; //code point array
T.length = 0; //length attribute
T.wString = true; // (item.wString) tests for wString object
//member functions
sortSurrogates = function(s){ //returns array of utf-16 code points
var chrs = [];
while(s.length){ // loop till we've done the whole string
if(/[\uD800-\uDFFF]/.test(s.substr(0,1))){ // test the first character
// High surrogate found low surrogate follows
chrs.push(s.substr(0,2)); // push the two onto array
s = s.substr(2); // clip the two off the string
}else{ // else BMP code point
chrs.push(s.substr(0,1)); // push one onto array
s = s.substr(1); // clip one from string
}
} // loop
return chrs;
};
//end member functions
//prototype functions
T.substr = function(start,len){
if(len){
return T.cp.slice(start,start+len).join('');
}else{
return T.cp.slice(start).join('');
}
};
T.substring = function(start,end){
return T.cp.slice(start,end).join('');
};
T.replace = function(target,str){
//allow wStrings as parameters
if(str.wString) str = str.cp.join('');
if(target.wString) target = target.cp.join('');
return T.toString().replace(target,str);
};
T.equals = function(s){
if(!s.wString){
s = sortSurrogates(s);
T.cp = s;
}else{
T.cp = s.cp;
}
T.length = T.cp.length;
};
T.toString = function(){return T.cp.join('');};
//end prototype functions
T.equals(str)
};
Результаты тестирования:
// plain string
var x = "0123456789";
alert(x); // 0123456789
alert(x.substr(4,5)) // 45678
alert(x.substring(2,4)) // 23
alert(x.replace("456","x")); // 0123x789
alert(x.length); // 10
// wString object
x = new wString("𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡");
alert(x); // 𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡
alert(x.substr(4,5)) // 𝟜𝟝𝟞𝟟𝟠
alert(x.substring(2,4)) // 𝟚𝟛
alert(x.replace("𝟜𝟝𝟞","x")); // 𝟘𝟙𝟚𝟛x𝟟𝟠𝟡
alert(x.length); // 10
Ответ 5
Итераторы строки Javascript могут давать вам фактические символы вместо суррогатных кодовых точек:
>>> [..."0123456789"]
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
>>> [..."𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡"]
["𝟘", "𝟙", "𝟚", "𝟛", "𝟜", "𝟝", "𝟞", "𝟟", "𝟠", "𝟡"]
>>> [..."0123456789"].length
10
>>> [..."𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡"].length
10