Comment trouver où couper le texte aux limites de caractères ou de mots ?
Localiser les positions de coupure sûres pour la troncature, le retour à la ligne et les opérations de curseur
Introduction
Lorsque vous tronquez du texte, positionnez un curseur ou gérez des clics dans un éditeur de texte, vous devez trouver où un caractère se termine et où un autre commence, ou où les mots commencent et se terminent. Couper le texte à la mauvaise position divise les emoji, coupe à travers les caractères combinés ou divise les mots de manière incorrecte.
L'API Intl.Segmenter de JavaScript fournit la méthode containing() pour trouver le segment de texte à n'importe quelle position dans une chaîne. Cela vous indique quel caractère ou mot contient un index spécifique, où ce segment commence et où il se termine. Vous pouvez utiliser ces informations pour trouver des points de coupure sûrs qui respectent les limites de groupes de graphèmes et les limites de mots linguistiques dans toutes les langues.
Cet article explique pourquoi couper le texte à des positions arbitraires échoue, comment trouver les limites de texte avec Intl.Segmenter, et comment utiliser les informations de limites pour la troncature, le positionnement du curseur et la sélection de texte.
Pourquoi vous ne pouvez pas couper le texte à n'importe quelle position
Les chaînes JavaScript sont constituées d'unités de code, pas de caractères complets. Un seul emoji, une lettre accentuée ou un drapeau peut s'étendre sur plusieurs unités de code. Si vous coupez une chaîne à un index arbitraire, vous risquez de diviser un caractère au milieu.
Considérez cet exemple :
const text = "Hello 👨👩👧👦 world";
const truncated = text.slice(0, 10);
console.log(truncated); // "Hello 👨�"
L'emoji famille utilise 11 unités de code. Couper à la position 10 divise l'emoji, produisant une sortie cassée avec un caractère de remplacement.
Pour les mots, couper à la mauvaise position crée des fragments qui ne correspondent pas aux attentes des utilisateurs :
const text = "Hello world";
const fragment = text.slice(0, 7);
console.log(fragment); // "Hello w"
Les utilisateurs s'attendent à ce que le texte se coupe entre les mots, et non au milieu d'un mot. Trouver la limite avant ou après la position 7 produit de meilleurs résultats.
Trouver le segment de texte à une position spécifique
La méthode containing() renvoie des informations sur le segment de texte qui inclut un index spécifique :
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 👋🏽" }
L'emoji à la position 6 s'étend sur quatre unités de code (de l'index 6 à 9). La méthode containing() renvoie :
segment: le cluster de graphèmes complet sous forme de chaîneindex: où ce segment commence dans la chaîne d'origineinput: référence à la chaîne d'origine
Cela vous indique que la position 6 se trouve à l'intérieur de l'emoji, que l'emoji commence à l'index 6, et que l'emoji complet est "👋🏽".
Trouver des points de troncature sûrs pour le texte
Pour tronquer le texte sans casser les caractères, trouvez la limite de graphème avant votre position cible :
function truncateAtPosition(text, maxIndex) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const segments = segmenter.segment(text);
const segment = segments.containing(maxIndex);
// Truncate before this segment to avoid breaking it
return text.slice(0, segment.index);
}
truncateAtPosition("Hello 👨👩👧👦 world", 10);
// "Hello " (stops before the emoji, not in the middle)
truncateAtPosition("café", 3);
// "caf" (stops before é)
Cette fonction trouve le segment à la position cible et tronque avant celui-ci, garantissant que vous ne divisez jamais un cluster de graphèmes.
Pour tronquer après le segment au lieu d'avant :
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 👨👩👧👦 " (includes the complete emoji)
Cela inclut l'intégralité du segment qui contient la position cible.
Trouver les limites de mots pour le retour à la ligne
Lors du retour à la ligne du texte à une largeur maximale, vous souhaitez couper entre les mots, et non au milieu d'un mot. Utilisez la segmentation de mots pour trouver la limite de mot avant votre position cible :
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 "世界")
Cette fonction trouve le début du mot qui contient la position cible. Si la position se trouve déjà dans un espace blanc, elle renvoie la position inchangée.
Pour le retour à la ligne qui respecte les limites des mots :
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");
// ["你好世界", "欢迎使用"]
Cette fonction divise le texte en lignes qui respectent les limites des mots et s'adaptent à la longueur maximale.
Trouver quel mot contient une position de curseur
Dans les éditeurs de texte, vous devez savoir dans quel mot se trouve le curseur pour implémenter des fonctionnalités telles que la sélection par double-clic, la vérification orthographique ou les menus contextuels :
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)
Cela renvoie le mot à la position du curseur ainsi que ses indices de début et de fin, ou null si le curseur n'est pas dans un mot.
Utilisez ceci pour implémenter la sélection de texte par double-clic :
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")
Trouver les limites de phrase pour la navigation
Pour la navigation dans les documents ou la segmentation de synthèse vocale, trouvez quelle phrase contient une position spécifique :
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 }
Cela trouve la phrase complète qui contient la position cible, y compris ses limites.
Trouver la limite suivante après une position
Pour avancer d'un graphème, mot ou phrase, parcourez les segments jusqu'à en trouver un qui commence après votre position actuelle :
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 (boundary after "H")
findNextBoundary(text, 6, "grapheme", "en");
// 17 (boundary after the family emoji)
findNextBoundary(text, 0, "word", "en");
// 5 (boundary after "Hello")
Cela trouve où commence le segment suivant, qui est la position sûre pour déplacer le curseur ou tronquer le texte.
Trouver la limite précédente avant une position
Pour reculer d'un graphème, mot ou phrase, trouvez le segment avant votre position actuelle :
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 (boundary before the family emoji)
findPreviousBoundary(text, 11, "word", "en");
// 6 (boundary before "world")
Cela trouve où commence le segment précédent, qui est la position sûre pour déplacer le curseur vers l'arrière.
Implémenter le déplacement du curseur avec les limites
Combinez la recherche de limites avec la position du curseur pour implémenter un déplacement correct du curseur :
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 (moves over the entire emoji)
moveWordForward(text, 0, "en");
// 6 (moves to the start of "world")
Ces fonctions implémentent le déplacement standard du curseur dans un éditeur de texte qui respecte les limites de graphèmes et de mots.
Trouver toutes les opportunités de coupure dans le texte
Pour trouver chaque position où vous pouvez couper le texte en toute sécurité, parcourez tous les segments et collectez leurs indices de départ :
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]
Cela renvoie un tableau de chaque position de coupure valide dans le texte. Utilisez ceci pour implémenter des fonctionnalités avancées de mise en page ou d'analyse de texte.
Gérer les cas limites avec les limites
Lorsque la position est à la toute fin du texte, containing() renvoie le dernier segment :
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" }
La position est à la fin, donc elle renvoie le dernier graphème.
Lorsque la position est avant le premier caractère, containing() renvoie le premier segment :
const segment = segments.containing(0);
console.log(segment);
// { segment: "H", index: 0, input: "Hello" }
Pour les chaînes vides, il n'y a pas de segments, donc appeler containing() sur une chaîne vide renvoie undefined. Vérifiez les chaînes vides avant d'utiliser containing() :
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);
}
Choisir la bonne granularité pour les limites
Utilisez différentes granularités en fonction de ce que vous devez trouver :
-
Graphème : utilisez lors de l'implémentation du déplacement du curseur, de la suppression de caractères ou de toute opération qui doit respecter ce que les utilisateurs voient comme des caractères uniques. Cela empêche la division des emoji, des caractères combinés ou d'autres clusters de graphèmes complexes.
-
Mot : utilisez pour la sélection de mots, la vérification orthographique, le comptage de mots ou toute opération nécessitant des limites de mots linguistiques. Cela fonctionne dans toutes les langues, y compris celles sans espaces entre les mots.
-
Phrase : utilisez pour la navigation entre phrases, la segmentation de synthèse vocale ou toute opération qui traite le texte phrase par phrase. Cela respecte les abréviations et autres contextes où les points ne terminent pas les phrases.
N'utilisez pas les limites de mots lorsque vous avez besoin de limites de caractères, et n'utilisez pas les limites de graphèmes lorsque vous avez besoin de limites de mots. Chacune sert un objectif spécifique.
Prise en charge des opérations de limites par les navigateurs
L'API Intl.Segmenter et sa méthode containing() ont atteint le statut Baseline en avril 2024. Les versions actuelles de Chrome, Firefox, Safari et Edge la prennent en charge. Les navigateurs plus anciens ne la prennent pas en charge.
Vérifiez la prise en charge avant utilisation :
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const segments = segmenter.segment(text);
const segment = segments.containing(position);
// Use segment information
} else {
// Fallback for older browsers
// Use approximate boundaries based on string length
}
Pour les applications ciblant des navigateurs plus anciens, fournissez un comportement de repli utilisant des limites approximatives, ou utilisez un polyfill qui implémente l'API Intl.Segmenter.
Erreurs courantes lors de la recherche de limites
Ne supposez pas que chaque unité de code est un point de rupture valide. De nombreuses positions divisent des groupes de graphèmes ou des mots, produisant des résultats invalides ou inattendus.
N'utilisez pas string.length pour trouver la limite de fin. Utilisez plutôt l'index du dernier segment plus sa longueur.
N'oubliez pas de vérifier isWordLike lorsque vous travaillez avec des limites de mots. Les segments non-mots comme les espaces et la ponctuation sont également renvoyés par le segmenteur.
Ne supposez pas que les limites de mots sont identiques dans toutes les langues. Utilisez une segmentation tenant compte de la locale pour obtenir des résultats corrects.
N'appelez pas containing() de manière répétée pour les opérations critiques en termes de performances. Si vous avez besoin de plusieurs limites, parcourez les segments une seule fois et construisez un index.
Considérations de performance pour les opérations de limites
La création d'un segmenteur est rapide, mais l'itération à travers tous les segments peut être lente pour des textes très longs. Pour les opérations nécessitant plusieurs limites, envisagez de mettre en cache les informations de segment :
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);
Cela met en cache tous les segments une fois et fournit des recherches rapides pour plusieurs opérations.
Exemple pratique : troncature de texte avec points de suspension
Combinez la recherche de limites avec la troncature pour créer une fonction qui coupe le texte au dernier mot complet avant une longueur maximale :
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");
// "你好世界…"
Cette fonction trouve le dernier mot complet avant la longueur maximale et ajoute des points de suspension, produisant un texte tronqué propre qui ne coupe pas les mots.