Существуют ли в JavaScript полифил-реализации String.toLowerCase () и String.toUpperCase () или другие методы в JavaScript, которые могут работать с символами Unicode и согласованы во всех браузерах?

Справочная информация

Выполнение следующих действий приведет к разнице результатов в браузерах или даже между версиями браузеров (например, FireFox 54 против 55):

document.write(String.fromCodePoint(223).normalize("NFKC").toLowerCase().toUpperCase().toLowerCase())

В Firefox 55 он дает вам ss, в Firefox 54 он дает вам ß.

Обычно это нормально, и такие механизмы, как Locales, обрабатывают множество случаев, которые вам нужны; однако, когда вам нужно согласованное поведение на разных платформах, например, общение с системами BaaS, такими как он может значительно упростить взаимодействие, когда вы, по сути, обрабатываете внутренние данные на клиенте.

3
Dan McGrath 26 Ноя 2018 в 22:48

1 ответ

Лучший ответ

Обратите внимание, что эта проблема, похоже, затрагивает только устаревшие версии Firefox, поэтому, если вам явно не нужно поддерживать эти старые версии, вы можете просто не беспокоиться. Поведение для вашего примера одинаково во всех современных браузерах (с момента изменения в Firefox). Это можно проверить с помощью jsvu + eshost:

$ jsvu # Update installed JavaScript engine binaries to the latest version.

$ eshost -e '"\xDF".normalize("NFKC").toLowerCase().toUpperCase().toLowerCase()'
#### Chakra
ss

#### V8 --harmony
ss

#### JavaScriptCore
ss

#### V8
ss

#### SpiderMonkey
ss

#### xs
ss

Но вы спросили, как решить эту проблему, давайте продолжим.

Шаг 4 https://tc39.github.io/ecma262/#sec -string.prototype.tolowercase утверждает:

Пусть cuList будет списком, элементы которого являются результатом toLowercase(cpList) в соответствии с алгоритмом преобразования регистра Unicode по умолчанию.

Этот алгоритм преобразования регистра Unicode по умолчанию указан в разделе 3.13 Регистр по умолчанию Алгоритмы стандарта Unicode.

Полные сопоставления регистров для символов Юникода получаются с помощью сопоставлений из SpecialCasing.txt плюс сопоставления из UnicodeData.txt , исключая любое из последних сопоставлений, которые могут конфликтовать. Считается, что любой символ, для которого нет сопоставления в этих файлах, сопоставлен сам с собой.

[…]

Следующие правила определяют операции преобразования регистра по умолчанию для строк Unicode. Эти правила используют операции полного преобразования регистра, Uppercase_Mapping(C), Lowercase_Mapping(C) и Titlecase_Mapping(C), а также контекстно-зависимые сопоставления на основе контекста регистра, как указано в таблице 3-17.

Для строки X:

  • R1 toUppercase(X): сопоставить каждый символ C в X с Uppercase_Mapping(C).
  • R2 toLowercase(X): сопоставить каждый символ C в X с Lowercase_Mapping(C).

Вот пример из SpecialCasing.txt с добавленной моей аннотацией ниже:

00DF  ; 00DF   ; 0053 0073; 0053 0053;                      # LATIN SMALL LETTER SHARP S
<code>; <lower>; <title>  ; <upper>  ; (<condition_list>;)? # <comment>

В этой строке указано, что U + 00DF ('ß') преобразует нижний регистр в U + 00DF (ß) и заглавные буквы в U + 0053 U + 0053 (SS).

Вот пример из UnicodeData.txt с добавленной моей аннотацией ниже:

0041  ; LATIN CAPITAL LETTER A; Lu;0;L;;;;;N;;;; 0061   ;
<code>; <name>                ; <ignore>       ; <lower>; <upper>

В этой строке говорится, что U + 0041 ('A') переводится в нижний регистр до U + 0061 ('a'). У него нет явного преобразования в верхний регистр, то есть в верхнем регистре самому себе.

Вот еще один пример из UnicodeData.txt:

0061  ; LATIN SMALL LETTER A; Ll;0;L;;;;;N;; ;0041;        ; 0041
<code>; <name>              ; <ignore>            ; <lower>; <upper>

В этой строке говорится, что U + 0061 ('a') преобразуется в верхний регистр до U + 0041 ('A'). У него нет явного преобразования в нижний регистр, то есть он сам с нижним регистром.

Вы можете написать сценарий, который анализирует эти два файла, читает каждую строку, следующую за этими примерами, и строит сопоставления в нижнем / верхнем регистре. Затем вы можете превратить эти сопоставления в небольшую библиотеку JavaScript, которая обеспечивает совместимые со спецификациями функции toLowerCase / toUpperCase.

Похоже, это большая работа. В зависимости от старого поведения в Firefox и того, что именно изменилось (?), Вы, вероятно, могли бы ограничить работу только специальными сопоставлениями в SpecialCasing.txt. (Я предполагаю, что в Firefox 55 изменились только специальные оболочки, на основе приведенного вами примера.)

// Instead of…
function normalize(string) {
  const normalized = string.normalize('NFKC');
  const lowercased = normalized.toLowerCase();
  return lowercased;
}

// …one could do something like:
function lowerCaseSpecialCases(string) {
  // TODO: replace all SpecialCasing.txt characters with their lowercase
  // mapping.
  return string.replace(/TODO/g, fn);
}
function normalize(string) {
  const normalized = string.normalize('NFKC');
  const fixed = lowerCaseSpecialCases(normalized); // Workaround for old Firefox 54 behavior.
  const lowercased = fixed.toLowerCase();
  return lowercased;
}

Я написал сценарий, который анализирует SpecialCasing.txt и генерирует библиотеку JS, которая реализует упомянутую выше функциональность lowerCaseSpecialCases (как toLower), а также toUpper. Вот он: https://gist.github.com/mathiasbynens/a37e3f3138069a390aee точный вариант использования, вам может вообще не понадобиться toUpper и соответствующее ему регулярное выражение и карта. Вот полная сгенерированная библиотека:

const reToLower = /[\u0130\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FCC\u1FFC]/g;
const toLowerMap = new Map([
  ['\u0130', 'i\u0307'],
  ['\u1F88', '\u1F80'],
  ['\u1F89', '\u1F81'],
  ['\u1F8A', '\u1F82'],
  ['\u1F8B', '\u1F83'],
  ['\u1F8C', '\u1F84'],
  ['\u1F8D', '\u1F85'],
  ['\u1F8E', '\u1F86'],
  ['\u1F8F', '\u1F87'],
  ['\u1F98', '\u1F90'],
  ['\u1F99', '\u1F91'],
  ['\u1F9A', '\u1F92'],
  ['\u1F9B', '\u1F93'],
  ['\u1F9C', '\u1F94'],
  ['\u1F9D', '\u1F95'],
  ['\u1F9E', '\u1F96'],
  ['\u1F9F', '\u1F97'],
  ['\u1FA8', '\u1FA0'],
  ['\u1FA9', '\u1FA1'],
  ['\u1FAA', '\u1FA2'],
  ['\u1FAB', '\u1FA3'],
  ['\u1FAC', '\u1FA4'],
  ['\u1FAD', '\u1FA5'],
  ['\u1FAE', '\u1FA6'],
  ['\u1FAF', '\u1FA7'],
  ['\u1FBC', '\u1FB3'],
  ['\u1FCC', '\u1FC3'],
  ['\u1FFC', '\u1FF3']
]);
const toLower = (string) => string.replace(reToLower, (match) => toLowerMap.get(match));

const reToUpper = /[\xDF\u0149\u01F0\u0390\u03B0\u0587\u1E96-\u1E9A\u1F50\u1F52\u1F54\u1F56\u1F80-\u1FAF\u1FB2-\u1FB4\u1FB6\u1FB7\u1FBC\u1FC2-\u1FC4\u1FC6\u1FC7\u1FCC\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2-\u1FE4\u1FE6\u1FE7\u1FF2-\u1FF4\u1FF6\u1FF7\u1FFC\uFB00-\uFB06\uFB13-\uFB17]/g;
const toUpperMap = new Map([
  ['\xDF', 'SS'],
  ['\uFB00', 'FF'],
  ['\uFB01', 'FI'],
  ['\uFB02', 'FL'],
  ['\uFB03', 'FFI'],
  ['\uFB04', 'FFL'],
  ['\uFB05', 'ST'],
  ['\uFB06', 'ST'],
  ['\u0587', '\u0535\u0552'],
  ['\uFB13', '\u0544\u0546'],
  ['\uFB14', '\u0544\u0535'],
  ['\uFB15', '\u0544\u053B'],
  ['\uFB16', '\u054E\u0546'],
  ['\uFB17', '\u0544\u053D'],
  ['\u0149', '\u02BCN'],
  ['\u0390', '\u0399\u0308\u0301'],
  ['\u03B0', '\u03A5\u0308\u0301'],
  ['\u01F0', 'J\u030C'],
  ['\u1E96', 'H\u0331'],
  ['\u1E97', 'T\u0308'],
  ['\u1E98', 'W\u030A'],
  ['\u1E99', 'Y\u030A'],
  ['\u1E9A', 'A\u02BE'],
  ['\u1F50', '\u03A5\u0313'],
  ['\u1F52', '\u03A5\u0313\u0300'],
  ['\u1F54', '\u03A5\u0313\u0301'],
  ['\u1F56', '\u03A5\u0313\u0342'],
  ['\u1FB6', '\u0391\u0342'],
  ['\u1FC6', '\u0397\u0342'],
  ['\u1FD2', '\u0399\u0308\u0300'],
  ['\u1FD3', '\u0399\u0308\u0301'],
  ['\u1FD6', '\u0399\u0342'],
  ['\u1FD7', '\u0399\u0308\u0342'],
  ['\u1FE2', '\u03A5\u0308\u0300'],
  ['\u1FE3', '\u03A5\u0308\u0301'],
  ['\u1FE4', '\u03A1\u0313'],
  ['\u1FE6', '\u03A5\u0342'],
  ['\u1FE7', '\u03A5\u0308\u0342'],
  ['\u1FF6', '\u03A9\u0342'],
  ['\u1F80', '\u1F08\u0399'],
  ['\u1F81', '\u1F09\u0399'],
  ['\u1F82', '\u1F0A\u0399'],
  ['\u1F83', '\u1F0B\u0399'],
  ['\u1F84', '\u1F0C\u0399'],
  ['\u1F85', '\u1F0D\u0399'],
  ['\u1F86', '\u1F0E\u0399'],
  ['\u1F87', '\u1F0F\u0399'],
  ['\u1F88', '\u1F08\u0399'],
  ['\u1F89', '\u1F09\u0399'],
  ['\u1F8A', '\u1F0A\u0399'],
  ['\u1F8B', '\u1F0B\u0399'],
  ['\u1F8C', '\u1F0C\u0399'],
  ['\u1F8D', '\u1F0D\u0399'],
  ['\u1F8E', '\u1F0E\u0399'],
  ['\u1F8F', '\u1F0F\u0399'],
  ['\u1F90', '\u1F28\u0399'],
  ['\u1F91', '\u1F29\u0399'],
  ['\u1F92', '\u1F2A\u0399'],
  ['\u1F93', '\u1F2B\u0399'],
  ['\u1F94', '\u1F2C\u0399'],
  ['\u1F95', '\u1F2D\u0399'],
  ['\u1F96', '\u1F2E\u0399'],
  ['\u1F97', '\u1F2F\u0399'],
  ['\u1F98', '\u1F28\u0399'],
  ['\u1F99', '\u1F29\u0399'],
  ['\u1F9A', '\u1F2A\u0399'],
  ['\u1F9B', '\u1F2B\u0399'],
  ['\u1F9C', '\u1F2C\u0399'],
  ['\u1F9D', '\u1F2D\u0399'],
  ['\u1F9E', '\u1F2E\u0399'],
  ['\u1F9F', '\u1F2F\u0399'],
  ['\u1FA0', '\u1F68\u0399'],
  ['\u1FA1', '\u1F69\u0399'],
  ['\u1FA2', '\u1F6A\u0399'],
  ['\u1FA3', '\u1F6B\u0399'],
  ['\u1FA4', '\u1F6C\u0399'],
  ['\u1FA5', '\u1F6D\u0399'],
  ['\u1FA6', '\u1F6E\u0399'],
  ['\u1FA7', '\u1F6F\u0399'],
  ['\u1FA8', '\u1F68\u0399'],
  ['\u1FA9', '\u1F69\u0399'],
  ['\u1FAA', '\u1F6A\u0399'],
  ['\u1FAB', '\u1F6B\u0399'],
  ['\u1FAC', '\u1F6C\u0399'],
  ['\u1FAD', '\u1F6D\u0399'],
  ['\u1FAE', '\u1F6E\u0399'],
  ['\u1FAF', '\u1F6F\u0399'],
  ['\u1FB3', '\u0391\u0399'],
  ['\u1FBC', '\u0391\u0399'],
  ['\u1FC3', '\u0397\u0399'],
  ['\u1FCC', '\u0397\u0399'],
  ['\u1FF3', '\u03A9\u0399'],
  ['\u1FFC', '\u03A9\u0399'],
  ['\u1FB2', '\u1FBA\u0399'],
  ['\u1FB4', '\u0386\u0399'],
  ['\u1FC2', '\u1FCA\u0399'],
  ['\u1FC4', '\u0389\u0399'],
  ['\u1FF2', '\u1FFA\u0399'],
  ['\u1FF4', '\u038F\u0399'],
  ['\u1FB7', '\u0391\u0342\u0399'],
  ['\u1FC7', '\u0397\u0342\u0399'],
  ['\u1FF7', '\u03A9\u0342\u0399']
]);
const toUpper = (string) => string.replace(reToUpper, (match) => toUpperMap.get(match));
3
Mathias Bynens 27 Ноя 2018 в 19:08