манипулирование 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, но мое тестирование заставляет меня поверить, что это может быть формат данных, но функции еще не знают этого. Я пропустил что-то простое?

РЕДАКТИРОВАТЬ: Чтобы проиллюстрировать проблему:

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 символов.


person BentFX    schedule 30.07.2011    source источник
comment
Что вам на самом деле нужно сделать?   -  person Tim Down    schedule 31.07.2011
comment
Базовая манипуляция. Твиттер не возвращает встроенные ссылки, а просто текст, URL-адреса и индексы, где URL-адреса принадлежат. Индексы основаны на кодовых точках, а не на 16-битных символах. Также у меня есть текстовое поле для форматирования твитов. Javascript обрабатывает простое количество символов как количество 16-битных фрагментов, а не отдельных кодовых точек. Я могу решить это, просто не хочу идти в неправильном направлении, не спросив профи, нет ли чего-то проще.   -  person BentFX    schedule 31.07.2011
comment
Я сижу здесь, обдумывая это, и я думаю, что понял, если только у кого-то нет чего-то попроще. При небольшом творческом прототипировании массивы должны почти вписаться в мой существующий код, а также прекрасно впишутся в мой сундук с сокровищами функций. Если самый простой способ справиться с сочетанием 2- и 4-байтовых символов — разбить их на массивы, то мне просто нужно прототипировать массивы, чтобы они больше походили на строки. Если никто не придет с элегантным ответом, я вернусь через пару дней с ответом, который будет почти на 1/4 приличным.   -  person BentFX    schedule 31.07.2011
comment
Javascript внутри использует UCS-2, который не является UTF-16. Из-за этого очень сложно обрабатывать Unicode в Javascript, и я не предлагаю пытаться это сделать. . Что касается того, что делает Twitter, вы, кажется, говорите, что он разумно считает по кодовой точке, а не безумно по кодовой единице.   -  person tchrist    schedule 31.07.2011
comment
@tchrist: Что ты имеешь в виду? Строки JavaScript, которые видны разработчикам, имеют кодировку UTF-16.   -  person Tim Down    schedule 31.07.2011
comment
Да! Спасибо тхрист. Прочитав вики, я хотел сказать, что javascript использует ucs-2, но не знал об этом достаточно, чтобы уверенно говорить об этом. Да! Твиттер считает кодовые точки. Я долго думал об этом. Это должен быть объект, который хранит строку в виде массива кодовых точек с прототипами, соответствующими основным функциям обработки строк. Думаю, я могу это сделать. :)   -  person BentFX    schedule 31.07.2011
comment
@Tim Они видны как строки отдельных кодовых единиц UCS-2, а не как строки кодовых точек Unicode. Вы можете доказать это себе с помощью регулярных выражений. Попробуйте написать [????-????] по шаблону и посмотрите, что получится. Он просто сломан. Если бы Javascript действительно использовал UTF-16, я мог бы написать document.write(String.fromCharCode(0x1D49C)), и мне не пришлось бы писать и не было бы разрешено писать document.write(String.fromCharCode(0xD835,0xDC9C)) вместо него. Это чепуха сломанная UCS-2.   -  person tchrist    schedule 31.07.2011
comment
@tchrist: Ты прав, извини.   -  person Tim Down    schedule 31.07.2011
comment
@BentFX: я нашел недавний отчет об ошибке, который кажется связанным, но я не совсем знаю, что с этим делать.   -  person tchrist    schedule 31.07.2011
comment
@tchrist Я посмотрел на этот отчет об ошибке и не получил никакой радости. Когда я это прочитал, codePointAt(pos); функция по-прежнему нуждается в pos, определенном в кодовых единицах.   -  person BentFX    schedule 31.07.2011


Ответы (5)


Javascript внутри использует UCS-2, а не UTF-16. Из-за этого очень сложно работать с Unicode в Javascript, и я не предлагаю пытаться это делать.

Что касается того, что делает Twitter, вы, кажется, говорите, что он разумно считает по кодовой точке, а не безумно по кодовой единице.

Если у вас нет выбора, вы должны использовать язык программирования, который действительно поддерживает Unicode и имеет интерфейс кодовой точки, а не интерфейс кодовой единицы. Javascript недостаточно хорош для этого, как вы обнаружили.

У него есть Проклятие UCS-2, которое даже хуже, чем Проклятие UTF-16, которое уже достаточно плохо. Я рассказываю обо всем этом в докладе OSCON, ???? Тест поддержки Unicode: ???? Хороший, плохой и (в основном) плохой ???? .

Из-за его ужасного Проклятия вам придется вручную имитировать UTF-16 с UCS-2 в Javascript, что просто безумие.

Javascript также страдает от множества других ужасных проблем с Unicode. Он не поддерживает графемы, нормализацию или сопоставление, которые вам действительно нужны. И его регулярные выражения сломаны, иногда из-за Проклятия, иногда просто потому, что люди ошиблись. Например, Javascript не может выражать регулярные выражения типа [????-????]. Javascript даже не поддерживает сворачивание регистра, поэтому вы не можете написать такой шаблон, как /ΣΤΙΓΜΑΣ/i, чтобы он правильно соответствовал στιγμας.

Вы можете попробовать использовать плагин XRegEXp, но таким образом вы не изгоните Проклятие. Это можно сделать только при переходе на язык с поддержкой Unicode, а ???????????????????????????????????????? просто не один из них.

person tchrist    schedule 30.07.2011
comment
Я ничего не знаю о графанемах или номинализации, но если бы я мог смоделировать элементарную строку с широким символом в javascript, моя текущая проблема была бы решена :) - person BentFX; 31.07.2011
comment
@BentFX: Этот ответ предполагает, что вас нелегко осчастливить. Мне жаль. Похоже, что стандарт ᴇᴄᴍᴀScript злонамеренно определяет строковые значения не как последовательности символов Unicode, а скорее как последовательности 16-битных «единиц кода». Кажется, это не было обновлено на этой стороне тысячелетия. Мы знаем, что для символа Unicode требуется 21 бит уже около 15 лет. Если я найду способ сделать это, я обновлю свой ответ. - person tchrist; 31.07.2011
comment
@BentFX Я исправил ссылки. Боюсь, мне нечего сказать хорошего, потому что Javascript оказался худшим из всех семи языков, которые я исследовал. Как я уже сказал, я очень сожалею об этом; Я не могу придумать причины, по которой они так долго тянули с этим, и обновлю свой ответ, если найду решение. - person tchrist; 31.07.2011
comment
Да, я не могу видеть это с вашей точки зрения, но у меня есть хорошее представление о том, где вы стоите, и у вас гораздо лучший обзор ландшафта, чем у меня. подсчитывать символы и выбирать подстроки на основе кодовых точек, а не 16-битных фрагментов. У меня нет надежды заставить регулярное выражение или String.fromCharCode() работать. Просто хочу иметь возможность вырезать и вставлять связно. - person BentFX; 31.07.2011
comment
@BentFX: я набросал код, чтобы сделать основы того, что вы хотите. Смотрите мой ответ. - person Tim Down; 31.07.2011
comment
EcmaScript 5 говорит, что реализации могут быть либо UTF-16, либо UCS-2. Соответствующая реализация настоящего стандарта должна интерпретировать символы в соответствии со стандартом Unicode версии 3.0 или более поздней и ISO/IEC 10646-1 с либо UCS-2, либо UTF-16 в качестве принятой формы кодирования, реализация уровень 3. из глава 2 параграф 2 - person Mike Samuel; 31.07.2011
comment
@Mike Пожалуйста, объясните, как правильно интерпретировать все кодовые точки Unicode из Unicode версии 3.0 или более поздней, используя UCS-2. Я не верю, что вы можете это сделать. - person tchrist; 31.07.2011
comment
@tchrist, я никогда не утверждал, что могу. Я просто указал, что ваше первое предложение неверно: Javascript использует UCS-2 внутри... - person Mike Samuel; 31.07.2011
comment
@Mike: Хорошо, тогда вы бы сказали, что иногда Javascript использует UCS-2 внутри, а иногда использует UTF-16, и поэтому вы не можете полагаться на наличие UTF-16? - person tchrist; 31.07.2011
comment
@tchrist, я согласен. Если вы хотите работать со многими интерпретаторами, вы не можете полагаться на то, что все они представляют дополнительные кодовые точки как UTF-16. Если вам нужно работать только с одним или несколькими интерпретаторами, вы можете протестировать: var div = document.createElement("DIV"); div.innerHTML = "&#0x10000;"; var isUtf16 = div.firstChild.nodeValue.charCodeAt(0) == 0xd800; - person Mike Samuel; 31.07.2011
comment
@Mike: Отличный трюк! Я добавлю его в свои слайды Unicode, потому что я пытаюсь дать каждому языку некоторые лакомые кусочки, которые помогут сделать Unicode менее разочаровывающим при использовании этого языка. - person tchrist; 31.07.2011
comment
/ΣΤΙΓΜΑΣ/i.test('στιγμας') возвращает true в моих более-менее современных версиях Chrome, Firefox, Edge и даже Internet Explorer. Текущий ECMA-262 v9.0 определяет, что эта строка должна использовать UTF-16. Также в Current Javascript есть String.fromCodePoint и String.prototype.codePointAt, которые на самом деле работают с кодовыми точками выше BMP. Может быть, вы могли бы обновить свой ответ и упомянуть, что современный javascript использует UTF-16? - person T S; 05.07.2018
comment
@tchrist Также ответ Румпеля показывает, что современный javascript может внутренне обрабатывать заданный вариант использования, поэтому вы должны упомянуть, что ваш ответ применим только в том случае, если вам все еще нужно поддерживать старые движки javascript. - person T S; 05.07.2018

Я собрал отправную точку для объекта обработки строки 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>

person Tim Down    schedule 31.07.2011
comment
Спасибо за попытку. Я действительно новичок в структуре объекта javascript и многое возьму из вашего примера. На самом деле мое программирование носит развлекательный характер, и мне нравится решать головоломки. Похоже, что Unicode в Javascript похож на кубик Рубика, за исключением того, что правильных решений меньше. :) - person BentFX; 31.07.2011
comment
Это печальное заявление о текущем состоянии Javascript, когда ответ StackOverflow с 4 голосами за является лучшим способом обработки Unicode UTF-16. Однако отличная работа над этим! Отлично работает для моей текущей задачи (нарезка твитов, содержащих значки Emoji). - person Matt Vukas; 11.07.2014

Вот пара скриптов, которые могут быть полезны при работе с суррогатными парами в JavaScript:

person slevithan    schedule 28.05.2012

Итераторы строк Javascript могут дать вам фактические символы вместо суррогатных кодовых точек:

>>> [..."0123456789"]
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
>>> [..."????????????????????????????????????????"]
["????", "????", "????", "????", "????", "????", "????", "????", "????", "????"]
>>> [..."0123456789"].length
10
>>> [..."????????????????????????????????????????"].length
10
person rumpel    schedule 05.06.2016

Это соответствует тому, что я искал. Требуется лучшая поддержка различных строковых функций. По мере добавления я буду обновлять этот ответ.

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
person BentFX    schedule 31.07.2011
comment
Это выглядит достаточно похоже на мои усилия. - person Tim Down; 31.07.2011
comment
@Tim Да, структуры разные, но главное - прототипировать необходимые функции. Основное отличие заключается в том, что вы кодируете кодовые единицы в кодовые точки. Я решил не делать этого, потому что мне не нужны истинные кодовые точки, javascript не может их отображать, так зачем беспокоиться. Для моего использования достаточно просто разделить их, чтобы их можно было посчитать и разделить в разумных точках. Развлекайся! - person BentFX; 31.07.2011
comment
Достаточно справедливо, если вам не нужны настоящие кодовые точки, не используйте их. Они могут вам понадобиться, если вам нужно отправить строку Unicode на сервер. - person Tim Down; 31.07.2011
comment
Из angular js: ....replace(/[���-���][���-���]/g, function(value) { var hi = value.charCodeAt(0); var low = value.charCodeAt( 1); return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; }) Это создает закодированное значение объекта, безопасное для вставки в атрибуты или тела элементов . - person Ajax; 04.01.2016