Wie findet man Stellen zum Textumbruch an Zeichen- oder Wortgrenzen?
Sichere Textumbruchpositionen für Kürzungen, Umbrüche und Cursoroperationen lokalisieren
Einführung
Wenn Sie Text kürzen, einen Cursor positionieren oder Klicks in einem Texteditor verarbeiten, müssen Sie herausfinden, wo ein Zeichen endet und ein anderes beginnt, oder wo Wörter anfangen und enden. Wenn Sie Text an der falschen Position trennen, werden Emojis gespalten, Kombinationszeichen durchschnitten oder Wörter falsch geteilt.
Die JavaScript-API Intl.Segmenter bietet die Methode containing(), um das Textsegment an einer beliebigen Position in einer Zeichenkette zu finden. Diese teilt Ihnen mit, welches Zeichen oder Wort einen bestimmten Index enthält, wo dieses Segment beginnt und wo es endet. Sie können diese Informationen nutzen, um sichere Trennpunkte zu finden, die Graphemcluster-Grenzen und linguistische Wortgrenzen in allen Sprachen respektieren.
Dieser Artikel erklärt, warum das Trennen von Text an beliebigen Positionen fehlschlägt, wie man Textgrenzen mit Intl.Segmenter findet und wie man Grenzinformationen für Kürzungen, Cursor-Positionierung und Textauswahl verwendet.
Warum Sie Text nicht an beliebiger Stelle trennen können
JavaScript-Strings bestehen aus Code-Einheiten, nicht aus vollständigen Zeichen. Ein einzelnes Emoji, ein Buchstabe mit Akzent oder eine Flagge kann mehrere Code-Einheiten umfassen. Wenn Sie eine Zeichenkette an einem beliebigen Index schneiden, riskieren Sie, ein Zeichen in der Mitte zu teilen.
Betrachten Sie dieses Beispiel:
const text = "Hello 👨👩👧👦 world";
const truncated = text.slice(0, 10);
console.log(truncated); // "Hello 👨�"
Das Familien-Emoji verwendet 11 Code-Einheiten. Das Schneiden an Position 10 teilt das Emoji und erzeugt eine fehlerhafte Ausgabe mit einem Ersatzzeichen.
Bei Wörtern erzeugt das Trennen an der falschen Position Fragmente, die nicht den Erwartungen der Benutzer entsprechen:
const text = "Hello world";
const fragment = text.slice(0, 7);
console.log(fragment); // "Hello w"
Benutzer erwarten, dass Text zwischen Wörtern getrennt wird, nicht mitten in einem Wort. Das Finden der Grenze vor oder nach Position 7 liefert bessere Ergebnisse.
Finden des Textsegments an einer bestimmten Position
Die Methode containing() liefert Informationen über das Textsegment, das einen bestimmten Index enthält:
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "Hello 👋🏽";
const segments = segmenter.segment(text);
const segment = segments.containing(6);
console.log(segment);
// { segment: "👋🏽", index: 6, input: "Hello 👋🏽" }
Das Emoji an Position 6 umfasst vier Code-Einheiten (von Index 6 bis 9). Die Methode containing() gibt zurück:
segment: den vollständigen Graphem-Cluster als Stringindex: wo dieses Segment im ursprünglichen String beginntinput: Referenz auf den ursprünglichen String
Dies zeigt, dass Position 6 innerhalb des Emojis liegt, das Emoji bei Index 6 beginnt und das vollständige Emoji "👋🏽" ist.
Finden sicherer Kürzungspunkte für Text
Um Text zu kürzen, ohne Zeichen zu unterbrechen, finden Sie die Graphemgrenze vor Ihrer Zielposition:
function truncateAtPosition(text, maxIndex) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const segments = segmenter.segment(text);
const segment = segments.containing(maxIndex);
// Kürzen vor diesem Segment, um es nicht zu unterbrechen
return text.slice(0, segment.index);
}
truncateAtPosition("Hello 👨👩👧👦 world", 10);
// "Hello " (stoppt vor dem Emoji, nicht in der Mitte)
truncateAtPosition("café", 3);
// "caf" (stoppt vor é)
Diese Funktion findet das Segment an der Zielposition und kürzt davor, wodurch sichergestellt wird, dass Sie niemals einen Graphem-Cluster teilen.
Um stattdessen nach dem Segment zu kürzen:
function truncateAfterPosition(text, minIndex) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const segments = segmenter.segment(text);
const segment = segments.containing(minIndex);
const endIndex = segment.index + segment.segment.length;
return text.slice(0, endIndex);
}
truncateAfterPosition("Hello 👨👩👧👦 world", 10);
// "Hello 👨👩👧👦 " (enthält das vollständige Emoji)
Dies schließt das gesamte Segment ein, das die Zielposition enthält.
Wortgrenzen für Textumbruch finden
Beim Umbrechen von Text an einer maximalen Breite möchten Sie zwischen Wörtern umbrechen, nicht mitten in einem Wort. Verwenden Sie die Wortsegmentierung, um die Wortgrenze vor Ihrer Zielposition zu finden:
function findWordBreakBefore(text, position, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
const segment = segments.containing(position);
// If we're in a word, break before it
if (segment.isWordLike) {
return segment.index;
}
// If we're in whitespace or punctuation, break here
return position;
}
const text = "Hello world";
findWordBreakBefore(text, 7, "en");
// 5 (the space before "world")
const textZh = "你好世界";
findWordBreakBefore(textZh, 6, "zh");
// 6 (the boundary before "世界")
Diese Funktion findet den Anfang des Wortes, das die Zielposition enthält. Wenn die Position bereits in einem Leerzeichen ist, gibt sie die Position unverändert zurück.
Für Textumbruch, der Wortgrenzen respektiert:
function wrapTextAtWidth(text, maxLength, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = Array.from(segmenter.segment(text));
const lines = [];
let currentLine = "";
for (const { segment, isWordLike } of segments) {
const potentialLine = currentLine + segment;
if (potentialLine.length <= maxLength) {
currentLine = potentialLine;
} else {
if (currentLine) {
lines.push(currentLine.trim());
}
currentLine = isWordLike ? segment : "";
}
}
if (currentLine) {
lines.push(currentLine.trim());
}
return lines;
}
wrapTextAtWidth("Hello world from JavaScript", 12, "en");
// ["Hello world", "from", "JavaScript"]
wrapTextAtWidth("你好世界欢迎使用", 6, "zh");
// ["你好世界", "欢迎使用"]
Diese Funktion teilt Text in Zeilen auf, die Wortgrenzen respektieren und innerhalb der maximalen Länge bleiben.
Ermitteln, welches Wort eine Cursorposition enthält
In Texteditoren müssen Sie wissen, in welchem Wort sich der Cursor befindet, um Funktionen wie Doppelklick-Auswahl, Rechtschreibprüfung oder Kontextmenüs zu implementieren:
function getWordAtPosition(text, position, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
const segment = segments.containing(position);
if (!segment.isWordLike) {
return null;
}
return {
word: segment.segment,
start: segment.index,
end: segment.index + segment.segment.length
};
}
const text = "Hello world";
getWordAtPosition(text, 7, "en");
// { word: "world", start: 6, end: 11 }
getWordAtPosition(text, 5, "en");
// null (position 5 is the space, not a word)
Dies gibt das Wort an der Cursorposition zusammen mit seinen Start- und Endindizes zurück, oder null, wenn sich der Cursor nicht in einem Wort befindet.
Verwenden Sie dies für die Implementierung der Textauswahl per Doppelklick:
function selectWordAtPosition(text, position, locale) {
const wordInfo = getWordAtPosition(text, position, locale);
if (!wordInfo) {
return { start: position, end: position };
}
return { start: wordInfo.start, end: wordInfo.end };
}
selectWordAtPosition("Hello world", 7, "en");
// { start: 6, end: 11 } (selects "world")
Ermitteln von Satzgrenzen für die Navigation
Für die Dokumentnavigation oder Text-zu-Sprache-Segmentierung können Sie ermitteln, welcher Satz eine bestimmte Position enthält:
function getSentenceAtPosition(text, position, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
const segments = segmenter.segment(text);
const segment = segments.containing(position);
return {
sentence: segment.segment,
start: segment.index,
end: segment.index + segment.segment.length
};
}
const text = "Hello world. How are you? Fine thanks.";
getSentenceAtPosition(text, 15, "en");
// { sentence: "How are you? ", start: 13, end: 26 }
Dies findet den vollständigen Satz, der die Zielposition enthält, einschließlich seiner Grenzen.
Die nächste Grenze nach einer Position finden
Um ein Graphem, Wort oder Satz vorwärts zu bewegen, iterieren Sie durch Segmente, bis Sie eines finden, das nach Ihrer aktuellen Position beginnt:
function findNextBoundary(text, position, granularity, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity });
const segments = Array.from(segmenter.segment(text));
for (const segment of segments) {
if (segment.index > position) {
return segment.index;
}
}
return text.length;
}
const text = "Hello 👨👩👧👦 world";
findNextBoundary(text, 0, "grapheme", "en");
// 1 (Grenze nach "H")
findNextBoundary(text, 6, "grapheme", "en");
// 17 (Grenze nach dem Familien-Emoji)
findNextBoundary(text, 0, "word", "en");
// 5 (Grenze nach "Hello")
Dies findet heraus, wo das nächste Segment beginnt, was die sichere Position ist, um den Cursor zu bewegen oder Text zu kürzen.
Die vorherige Grenze vor einer Position finden
Um ein Graphem, Wort oder Satz rückwärts zu bewegen, finden Sie das Segment vor Ihrer aktuellen Position:
function findPreviousBoundary(text, position, granularity, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity });
const segments = Array.from(segmenter.segment(text));
let previousIndex = 0;
for (const segment of segments) {
if (segment.index >= position) {
return previousIndex;
}
previousIndex = segment.index;
}
return previousIndex;
}
const text = "Hello 👨👩👧👦 world";
findPreviousBoundary(text, 17, "grapheme", "en");
// 6 (Grenze vor dem Familien-Emoji)
findPreviousBoundary(text, 11, "word", "en");
// 6 (Grenze vor "world")
Dies findet heraus, wo das vorherige Segment beginnt, was die sichere Position ist, um den Cursor rückwärts zu bewegen.
Implementierung der Cursorbewegung mit Grenzen
Kombinieren Sie die Grenzfindung mit der Cursorposition, um eine korrekte Cursorbewegung zu implementieren:
function moveCursorForward(text, cursorPosition, locale) {
return findNextBoundary(text, cursorPosition, "grapheme", locale);
}
function moveCursorBackward(text, cursorPosition, locale) {
return findPreviousBoundary(text, cursorPosition, "grapheme", locale);
}
function moveWordForward(text, cursorPosition, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = Array.from(segmenter.segment(text));
for (const segment of segments) {
if (segment.index > cursorPosition && segment.isWordLike) {
return segment.index;
}
}
return text.length;
}
function moveWordBackward(text, cursorPosition, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = Array.from(segmenter.segment(text));
let previousWordIndex = 0;
for (const segment of segments) {
if (segment.index >= cursorPosition) {
return previousWordIndex;
}
if (segment.isWordLike) {
previousWordIndex = segment.index;
}
}
return previousWordIndex;
}
const text = "Hello 👨👩👧👦 world";
moveCursorForward(text, 6, "en");
// 17 (bewegt sich über das gesamte Emoji)
moveWordForward(text, 0, "en");
// 6 (bewegt sich zum Anfang von "world")
Diese Funktionen implementieren die standardmäßige Cursorbewegung eines Texteditors, die Graphem- und Wortgrenzen respektiert.
Alle Trennmöglichkeiten im Text finden
Um jede Position zu finden, an der Text sicher getrennt werden kann, iteriere durch alle Segmente und sammle deren Startindizes:
function getBreakOpportunities(text, granularity, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity });
const segments = Array.from(segmenter.segment(text));
return segments.map(segment => segment.index);
}
const text = "Hello 👨👩👧👦 world";
getBreakOpportunities(text, "grapheme", "en");
// [0, 1, 2, 3, 4, 5, 6, 17, 18, 19, 20, 21, 22]
getBreakOpportunities(text, "word", "en");
// [0, 5, 6, 17, 18, 22]
Dies gibt ein Array mit jeder gültigen Trennposition im Text zurück. Verwende dies für die Implementierung erweiterter Textlayout- oder Analysefunktionen.
Umgang mit Grenzfällen bei Begrenzungen
Wenn die Position am Ende des Textes liegt, gibt containing() das letzte Segment zurück:
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "Hello";
const segments = segmenter.segment(text);
const segment = segments.containing(5);
console.log(segment);
// { segment: "o", index: 4, input: "Hello" }
Die Position ist am Ende, daher wird das letzte Graphem zurückgegeben.
Wenn die Position vor dem ersten Zeichen liegt, gibt containing() das erste Segment zurück:
const segment = segments.containing(0);
console.log(segment);
// { segment: "H", index: 0, input: "Hello" }
Bei leeren Strings gibt es keine Segmente, daher gibt der Aufruf von containing() bei einem leeren String undefined zurück. Überprüfe auf leere Strings, bevor du containing() verwendest:
function safeContaining(text, position, granularity, locale) {
if (text.length === 0) {
return null;
}
const segmenter = new Intl.Segmenter(locale, { granularity });
const segments = segmenter.segment(text);
return segments.containing(position);
}
Die richtige Granularität für Grenzen wählen
Verwenden Sie unterschiedliche Granularitäten basierend auf Ihren Anforderungen:
-
Graphem: Verwenden Sie diese Option bei der Implementierung von Cursorbewegungen, Zeichenlöschung oder anderen Operationen, die respektieren müssen, was Benutzer als einzelne Zeichen wahrnehmen. Dies verhindert die Aufteilung von Emojis, kombinierenden Zeichen oder anderen komplexen Graphem-Clustern.
-
Wort: Verwenden Sie diese Option für Wortauswahl, Rechtschreibprüfung, Wortzählung oder andere Operationen, die linguistische Wortgrenzen benötigen. Dies funktioniert in allen Sprachen, einschließlich solcher ohne Leerzeichen zwischen Wörtern.
-
Satz: Verwenden Sie diese Option für Satznavigation, Text-zu-Sprache-Segmentierung oder andere Operationen, die Text satzweise verarbeiten. Dies berücksichtigt Abkürzungen und andere Kontexte, in denen Punkte keine Sätze beenden.
Verwenden Sie keine Wortgrenzen, wenn Sie Zeichengrenzen benötigen, und verwenden Sie keine Graphemgrenzen, wenn Sie Wortgrenzen benötigen. Jede dient einem spezifischen Zweck.
Browser-Unterstützung für Grenzoperationen
Die Intl.Segmenter-API und ihre containing()-Methode haben im April 2024 den Baseline-Status erreicht. Aktuelle Versionen von Chrome, Firefox, Safari und Edge unterstützen sie. Ältere Browser nicht.
Überprüfen Sie die Unterstützung vor der Verwendung:
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const segments = segmenter.segment(text);
const segment = segments.containing(position);
// Segment-Informationen verwenden
} else {
// Fallback für ältere Browser
// Verwenden Sie ungefähre Grenzen basierend auf der Stringlänge
}
Für Anwendungen, die auf ältere Browser abzielen, stellen Sie ein Fallback-Verhalten mit ungefähren Grenzen bereit oder verwenden Sie einen Polyfill, der die Intl.Segmenter-API implementiert.
Häufige Fehler bei der Suche nach Grenzen
Nehmen Sie nicht an, dass jede Codeeinheit ein gültiger Trennpunkt ist. Viele Positionen teilen Graphem-Cluster oder Wörter und erzeugen ungültige oder unerwartete Ergebnisse.
Verwenden Sie nicht string.length, um die Endgrenze zu finden. Verwenden Sie stattdessen den Index des letzten Segments plus dessen Länge.
Vergessen Sie nicht, isWordLike zu überprüfen, wenn Sie mit Wortgrenzen arbeiten. Nicht-Wort-Segmente wie Leerzeichen und Interpunktion werden ebenfalls vom Segmenter zurückgegeben.
Nehmen Sie nicht an, dass Wortgrenzen in allen Sprachen gleich sind. Verwenden Sie lokalisierungsbewusste Segmentierung für korrekte Ergebnisse.
Rufen Sie containing() nicht wiederholt für leistungskritische Operationen auf. Wenn Sie mehrere Grenzen benötigen, iterieren Sie einmal durch die Segmente und erstellen Sie einen Index.
Leistungsüberlegungen für Grenzoperationen
Das Erstellen eines Segmentierers ist schnell, aber das Durchlaufen aller Segmente kann bei sehr langen Texten langsam sein. Für Operationen, die mehrere Grenzen benötigen, sollten Sie Segmentinformationen zwischenspeichern:
class TextBoundaryCache {
constructor(text, granularity, locale) {
this.text = text;
const segmenter = new Intl.Segmenter(locale, { granularity });
this.segments = Array.from(segmenter.segment(text));
}
containing(position) {
for (const segment of this.segments) {
const end = segment.index + segment.segment.length;
if (position >= segment.index && position < end) {
return segment;
}
}
return this.segments[this.segments.length - 1];
}
nextBoundary(position) {
for (const segment of this.segments) {
if (segment.index > position) {
return segment.index;
}
}
return this.text.length;
}
previousBoundary(position) {
let previous = 0;
for (const segment of this.segments) {
if (segment.index >= position) {
return previous;
}
previous = segment.index;
}
return previous;
}
}
const cache = new TextBoundaryCache("Hello world", "grapheme", "en");
cache.containing(7);
cache.nextBoundary(7);
cache.previousBoundary(7);
Dies speichert alle Segmente einmal zwischen und ermöglicht schnelle Nachschlagefunktionen für mehrere Operationen.
Praktisches Beispiel: Textkürzung mit Auslassungszeichen
Kombinieren Sie die Grenzfindung mit Kürzung, um eine Funktion zu erstellen, die Text am letzten vollständigen Wort vor einer maximalen Länge abschneidet:
function truncateAtWordBoundary(text, maxLength, locale) {
if (text.length <= maxLength) {
return text;
}
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = Array.from(segmenter.segment(text));
let lastWordEnd = 0;
for (const segment of segments) {
const segmentEnd = segment.index + segment.segment.length;
if (segmentEnd > maxLength) {
break;
}
if (segment.isWordLike) {
lastWordEnd = segmentEnd;
}
}
if (lastWordEnd === 0) {
return "";
}
return text.slice(0, lastWordEnd).trim() + "…";
}
truncateAtWordBoundary("Hello world from JavaScript", 15, "en");
// "Hello world…"
truncateAtWordBoundary("你好世界欢迎使用", 9, "zh");
// "你好世界…"
Diese Funktion findet das letzte vollständige Wort vor der maximalen Länge und fügt ein Auslassungszeichen hinzu, wodurch ein sauberer gekürzter Text entsteht, der keine Wörter abschneidet.