Как сделать toLowerCase() и toUpperCase() согласованными в разных браузерах
Существуют ли реализации 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, такими как /questions/tagged/google-cloud-firestore, это может значительно упростить взаимодействие, когда вы по сути обрабатываете внутренние данные на клиенте.
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/ состояний:
Позволять
cuList
быть списком, где элементы являются результатомtoLowercase(cpList)
согласно алгоритму преобразования регистра по умолчанию в Юникоде.
Этот алгоритм преобразования регистра по умолчанию в Юникоде указан в разделе 3.13 Алгоритмы регистра по умолчанию в стандарте Юникод.
Полные сопоставления регистров для символов Юникода получаются с помощью сопоставлений из
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/a37e3f3138069729aa434ea90eea4a3c В зависимости от конкретного случая использования вам может не понадобиться 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));