Introduction aux Décorateurs dans l'Éditeur Slate
Après avoir examiné la conception de la structure des données et le système de typage des éditeurs basés sur Slate, nous allons maintenant nous pencher sur l'implémentation des décorateurs dans cet environnement. Les décorateurs jouent un rôle crucial dans Slate, offrant un moyen pratique de gérer le rendu des étendues (Range) lors du cycle de rafraîchissement de l'éditeur.
Le Concept de Décoration
La fonctionnalité de décoration de Slate est particulièrement intéressante. Imaginons un scénario où vous devez implémenter la coloration syntaxique d'un bloc de code. Plusieurs approches sont possibles :
- Stocker les catégories de mots-clés directement dans la structure de données lors de l'analyse du code. Cela permet un rendu direct des informations de surbrillance, mais augmente la taille de la structure de données.
- Stocker uniquement le code et anaylser le texte pour générer des marques (
Marks) lors du rendu côté client. Cette méthode est plus complexe si la collaboration est impliquée, car elle nécessiterait des opérations non collaboratives ou des opérations purement clientes non stockées côté serveur. - Utiliser les décorateurs. En substance, Slate simplifie la deuxième approche en permettant de rendre du contenu supplémentaire (comme des
Marks) sans modifier la structure de données sous-jacente.
Les décorateurs ne sont pas limités à la coloration syntaxique. Toute situation où vous souhaitez afficher des informations visuelles au rendu sans les persister dans la structure de données bénéficie de l'utilisation des décorateurs. Des exemples courants incluent la mise en évidence des résultats de recherche dans l'éditeur ou la transformation automatique d'URLs collées en liens hypertexte cliquables.
Comprendre le Mécanisme decorate
Lors de l'examen de l'exemple de recherche et de surbrillance de Slate, un problème apparaît avec la recherche inter-nœuds. Si une recherche s'étend sur plusieurs nœuds textuels, la mise en évidence peut échouer. Par exemple, avec la structure suivante :
[
{ text: "12345" },
{ text: "67890" }
]
Rechercher "12345" ou "67890" fonctionne parfaitement. Cependant, rechercher "123456" échoue, car l'étendue construite pour cette recherche (par exemple, path: [0], offset: [0-6]) chevauche le nœud [0] et le nœud [1]. Le mécanisme de décoration par défaut ne gère pas cela correctement.
Le résultat de la fonction decorate d'un niveau parent est transmis aux étapes de rendu suivantes. Au niveau de chaque nœud enfant, la fonction decorate est appelée pour générer de nouvelles décorations, et les décorations du parent sont filtrées par Range.intersection. Si une intersection existe entre la décoration parente et l'étendue du nœud courant, la décoration est propagée.
Le cœur du problème réside ici. Pour trouver l'index de "123456", il faut concaténer le contenu de tous les nœuds textuels du tableau parent. Ensuite, la fonction Range.intersection est utilisée pour déterminer si une décoration parentale chevauche l'étendue d'un nœud enfant. La logique d'intersection est la suivante :
// Fonction conceptuelle Range.intersection(range1, range2)
// Elle vérifie si les étendues se chevauchent.
// Soient deux étendues : [début1, fin1] et [début2, fin2]
// Cas sans chevauchement : fin1 < début2
// Ex: [0, 5] et [6, 10]
// Cas avec chevauchement : début2 < fin1
// Ex: [0, 8] et [5, 12] => Chevauchement sur [5, 8]
Pour le nœud text: "12345" avec une recherche "123456", la première partie "12345" serait correctement capturée par une étendue de path: [0], offset: [0-5], car Range.intersection déterminerait un chevauchement.
Le Problème de la Traversal Inter-Nœuds
Le véritable défi survient lors de la recherche du "6" restant dans le nœud suivant. L'étendue de recherche originale était path: [0], offset: [0-6]. Le deuxième nœud textuel a une étendue de base path: [1], offset: [0-5]. Selon la logique Range.intersection, il n'y aurait pas de chevauchement entre path: [0], offset: [0-6] et path: [1], offset: [0-5] car les chemins sont différents. Pour gérer cela, il faudrait que la logique de Range.intersection examine les étendues des nœuds précédents, ce qui serait coûteux en performances si de nombreux nœuds sont impliqués.
La solution la plus efficace consiste à modifier la manière dont les étendues sont construites lors de l'analyse de la recherche. Si une correspondance de recherche s'étend sur plusieurs nœuds, nous devons la diviser en plusieurs étendues, une pour chaque partie de la correspondance au sein de son nœud textuel respectif. En reprenant l'exemple précédent de "123456", les résultats de la recherche seraient divisés en path: [0], offset: [0, 5] et path: [1], offset: [0, 1]. De cette manière, Range.intersection peut fonctionner correctement car chaque étendue est parfaitement alignée avec le chemin de son nœud.
Voici une implémentation conceptuelle de cette logique pour générer les étandues fragmentées :
/**
* Génère des étendues (Ranges) pour toutes les occurrences d'un texte de recherche
* au sein des nœuds textuels enfants d'un élément parent.
*
* @param {Element} parentNode L'élément parent contenant les nœuds textuels.
* @param {string[]} parentPath Le chemin du nœud parent dans l'arbre Slate.
* @param {string} searchText Le texte à rechercher.
* @returns {Array<range>} Un tableau d'objets Range de Slate.
*/
function genererEtenduesDeRecherche(parentNode, parentPath, searchText) {
const childTextContents = parentNode.children.map(child => child.text);
const concatenatedText = childTextContents.join('');
const searchLength = searchText.length;
let currentSearchIndex = concatenatedText.indexOf(searchText);
const resultRanges = [];
// Nous suivons ces deux variables pour repositionner notre recherche dans les nœuds enfants
let currentNodeIdx = 0;
let accumulatedLengthOfNodes = 0;
while (currentSearchIndex !== -1) {
// Avancer currentNodeIdx et accumulatedLengthOfNodes jusqu'à atteindre le nœud de départ de la correspondance
while (currentNodeIdx < childTextContents.length && currentSearchIndex >= accumulatedLengthOfNodes + childTextContents[currentNodeIdx].length) {
accumulatedLengthOfNodes += childTextContents[currentNodeIdx].length;
currentNodeIdx++;
}
let offsetInCurrentNode = currentSearchIndex - accumulatedLengthOfNodes;
let remainingMatchLength = searchLength;
// Parcourir les nœuds suivants pour couvrir la correspondance complète
let tempNodeIdx = currentNodeIdx; // Index temporaire pour cette boucle interne
let tempAccumulatedLength = accumulatedLengthOfNodes; // Longueur accumulée temporaire
while (tempNodeIdx < childTextContents.length && remainingMatchLength > 0) {
const currentChildText = childTextContents[tempNodeIdx];
const currentChildPath = [...parentPath, tempNodeIdx]; // Créer le chemin complet du nœud enfant
const lengthToCapture = Math.min(remainingMatchLength, currentChildText.length - offsetInCurrentNode);
// Construire l'objet Range pour ce segment de la correspondance
resultRanges.push({
anchor: { path: currentChildPath, offset: offsetInCurrentNode },
focus: { path: currentChildPath, offset: offsetInCurrentNode + lengthToCapture }
});
remainingMatchLength -= lengthToCapture;
// S'il reste une partie de la correspondance, passer au nœud suivant
if (remainingMatchLength > 0) {
tempAccumulatedLength += currentChildText.length;
offsetInCurrentNode = 0; // Réinitialiser l'offset pour le prochain nœud
tempNodeIdx++;
}
}
// Rechercher la prochaine occurrence après la correspondance actuelle
currentSearchIndex = concatenatedText.indexOf(searchText, currentSearchIndex + searchLength);
// Réinitialiser les pointeurs pour la prochaine itération de recherche
currentNodeIdx = 0;
accumulatedLengthOfNodes = 0;
}
return resultRanges;
}
</range>
leaf versus text dans renderLeaf
Lors de la personnalisation du rendu des feuilles (leaf) avec renderLeaf, il est important de noter qu'il existe deux types de propriétés de texte passées : leaf: Text et text: Text. Les décorations sont appliquées au niveau du nœud text, mais Slate le divise ensuite en plusieurs leaves basées sur ces décorations et les propriétés de texte intrinsèques. Chaque leaf est ensuite transmise à renderLeaf.
Par conséquent, lorsque vous rendez du contenu, comme un bloc de code surligné ou des marques en gras, vous devez vous baser sur la propriété leaf. C'est la granularité la plus fine du contenu textuel, enrichie de toutes les marques (provenant des décorations et des propriétés natives du texte) qui doivent être appliquées. La propriété text représente le nœud textuel original non segmenté, tandis que leaf est un segment de ce text avec un ensemble spécifique de propriétés de rendu. Sans décorateurs, leaf et text peuvent être identiques en termes de contenu et de propriétés, mais les décorateurs créent intentionnellement ces segments plus petits.
// Extrait conceptuel de la gestion des feuilles dans Slate-React
const allLeaves = SlateText.decorations(textNode, activeDecorations);
const elementKey = ReactEditor.findKey(editor, textNode);
const childComponents = [];
for (let i = 0; i < allLeaves.length; i++) {
const currentLeaf = allLeaves[i];
childComponents.push(
<Leaf
key={`${elementKey.id}-${i}`}
leaf={currentLeaf}
text={textNode} // Le nœud de texte original est également passé
parent={parentNode}
renderLeaf={renderLeafFunction}
// ... autres props
/>
);
}
Ce mécanisme permet à Slate de rendre efficacement des portions de texte avec des styles différents sans altérer la structure de données fondamentale, en s'appuyant sur la division logique des nœuds text en leaves au moment du rendu.