В текстовых данных символ не всегда соответствует одному отображаемому элементу. В Unicode один «видимый символ» может состоять из нескольких кодовых точек: базового знака, диакритических модификаторов, соединителей нулевой ширины, региональных индикаторов или последовательностей сложения. Такие визуальные единицы называются графемными кластерами.
Графемный кластер — минимальная единица текста, которую пользователь воспринимает как один символ при чтении и редактировании. Например, буква «e» с диакритикой может быть представлена как:
e◌́С точки зрения строки это два Unicode-символа, но визуально и семантически это одна графема.
Аналогично сложные письменности и системы письма используют
соединение нескольких кодовых точек для формирования одного
отображаемого знака. Поэтому операции split('') в
JavaScript не соответствуют реальному понятию символов в тексте.
Стандартная операция разбиения строки в Jav * aScript:
const chars = "école".split("");
разбивает строку по UTF-16 кодовым единицам, а не по графемам. Результат может содержать:
Это приводит к ошибкам в:
Unicode определяет правила сегментации текста на графемные кластеры в стандарте Unicode Text Segmentation (UAX #29). Согласно этим правилам:
Графема может включать:
Примеры:
a + ◌́ → одна графемаn + ◌̃ → одна графемаСовременный JavaScript предоставляет стандартный API для корректной
сегментации текста — Intl.Segmenter.
Он позволяет разбивать текст на:
Для графем используется режим
granularity: "grapheme".
const segmenter = new Intl.Segmenter("en", {
granularity: "grapheme"
});
Параметр локали влияет на языковые правила, но графемная сегментация в основном опирается на Unicode Standard, а не на язык.
Метод segment() возвращает итерируемую структуру
сегментов:
const text = "école";
const segmenter = new Intl.Segmenter("en", {
granularity: "grapheme"
});
const segments = segmenter.segment(text);
for (const part of segments) {
console.log(part.segment);
}
Каждый элемент содержит:
segment — строка графемыindex — позиция началаinput — исходная строка"école".split("")
Разбивает строку по UTF-16 кодам, не учитывая графемные кластеры.
[..."école"]
Поведение аналогично split(""), также не учитывает
комбинируемые символы.
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
[...segmenter.segment("école")].map(s => s.segment);
Возвращает корректные графемы.
const text = "a\u0301"; // a + combining acute accent
Визуально: «á»
Некоторые символы (например, редкие иероглифы или математические символы) кодируются двумя UTF-16 единицами.
const text = "?";
Используются для объединения нескольких символов в один сложный знак в некоторых письменностях.
const text = "क्\u200Dष";
Такие последовательности должны трактоваться как единая графема.
Intl.Segmenter возвращает ленивый итератор, что
позволяет эффективно обрабатывать большие строки.
const segmenter = new Intl.Segmenter("ru", {
granularity: "grapheme"
});
const iterator = segmenter.segment("пример текста");
for (const { segment, index } of iterator) {
console.log(segment, index);
}
Каждый сегмент содержит точную позицию в исходной строке, что важно для текстовых редакторов.
Ограничение длины текста часто должно учитывать не кодовые единицы, а графемы.
function graphemeLength(text) {
const segmenter = new Intl.Segmenter("en", {
granularity: "grapheme"
});
let count = 0;
for (const _ of segmenter.segment(text)) {
count++;
}
return count;
}
Это корректно обрабатывает:
split("") — быстро/./gu учитывает суррогатные парыГрафемная сегментация используется в:
Пример безопасной обрезки:
function truncateByGraphemes(text, max) {
const segmenter = new Intl.Segmenter("en", {
granularity: "grapheme"
});
let result = "";
let count = 0;
for (const { segment } of segmenter.segment(text)) {
if (count >= max) break;
result += segment;
count++;
}
return result;
}
Хотя графемная сегментация основана на Unicode, локаль влияет на:
Для grapheme локаль обычно не изменяет результат, но
влияет на совместимость и поведение API в будущем.
В JavaScript «символ» строки — это UTF-16 code unit. Это техническая единица хранения, а не лингвистическая единица.
Графема же — пользовательская единица восприятия текста.
Различие:
Intl.Segmenter оперирует последним уровнем.
Каждый сегмент содержит index, который позволяет
сопоставлять графемы с исходной строкой:
const segmenter = new Intl.Segmenter("en", {
granularity: "grapheme"
});
for (const part of segmenter.segment("test")) {
console.log(part.segment, part.index);
}
Это полезно для:
Переход от кодовых единиц к графемам устраняет фундаментальные проблемы работы с текстом:
Intl.Segmenter формализует этот уровень представления
текста в стандартизированном API JavaScript.