La fragmentation de bloc consiste à diviser une zone CSS au niveau du bloc (telle qu'une section ou un paragraphe) en plusieurs fragments lorsqu'elle ne rentre pas dans son ensemble dans un seul conteneur de fragments, appelée fragmenteur. Un fragmenteur n'est pas un élément, mais représente une colonne dans une mise en page multicolonne ou une page dans un contenu multimédia paginé.
Pour que la fragmentation se produise, le contenu doit se trouver dans un contexte de fragmentation. Un contexte de fragmentation est généralement établi par un conteneur multicolonne (le contenu est divisé en colonnes) ou lors de l'impression (le contenu est divisé en pages). Un long paragraphe comportant de nombreuses lignes peut nécessiter d'être divisé en plusieurs fragments, de sorte que les premières lignes soient placées dans le premier fragment et les lignes restantes dans les fragments suivants.
La fragmentation de bloc est semblable à un autre type de fragmentation bien connu: la fragmentation de ligne, également appelée "coupure de ligne". Tout élément intégré composé de plusieurs mots (nœud de texte, élément <a>
, etc.) et permettant les retours à la ligne peut être divisé en plusieurs fragments. Chaque fragment est placé dans une zone de ligne différente. Une zone de ligne est la fragmentation intégrée équivalente à un fragmenteur pour les colonnes et les pages.
Fragmentation du bloc LayoutNG
LayoutNGBlockFragmentation est une réécriture du moteur de fragmentation pour LayoutNG, initialement publié dans Chrome 102. En termes de structures de données, il a remplacé plusieurs structures de données pré-NG par des fragments NG représentés directement dans l'arborescence des fragments.
Par exemple, nous acceptons désormais la valeur "avoid" pour les propriétés CSS "break-before" et "break-after", qui permettent aux auteurs d'éviter les coupures juste après un en-tête. Il est souvent peu pratique de terminer une page par un en-tête, alors que le contenu de la section commence sur la page suivante. Il est préférable de faire un saut avant l'en-tête.
Chrome est également compatible avec le débordement de fragmentation, de sorte que le contenu monolithique (censé être incassable) ne soit pas découpé en plusieurs colonnes, et que les effets de peinture tels que les ombres et les transformations soient appliqués correctement.
La fragmentation des blocs dans LayoutNG est maintenant terminée
La fragmentation de base (conteneurs de blocs, y compris la mise en page de ligne, les flottants et le positionnement hors flux) est disponible dans Chrome 102. La fragmentation Flex et de la grille a été publiée dans Chrome 103, et la fragmentation de tableau dans Chrome 106. Enfin, l'impression est disponible dans Chrome 108. La fragmentation de blocs était la dernière fonctionnalité qui dépendait de l'ancien moteur pour effectuer la mise en page.
À partir de Chrome 108, l'ancien moteur n'est plus utilisé pour effectuer la mise en page.
De plus, les structures de données LayoutNG sont compatibles avec la peinture et les tests de contact, mais nous nous appuyons sur certaines anciennes structures de données pour les API JavaScript qui lisent des informations de mise en page, telles que offsetLeft
et offsetTop
.
En effectuant la mise en page avec NG, vous pourrez implémenter et déployer de nouvelles fonctionnalités qui n'ont que des implémentations LayoutNG (et pas de contrepartie dans l'ancien moteur), telles que les requêtes de conteneur CSS, le positionnement des ancres, MathML et la mise en page personnalisée (Houdini). Pour les requêtes de conteneur, nous l'avons publié un peu à l'avance, en avertissant les développeurs que l'impression n'était pas encore prise en charge.
Nous avons lancé la première partie de LayoutNG en 2019, qui se composait d'une mise en page standard de conteneurs de blocs, d'une mise en page intégrée, de floats et d'un positionnement hors flux, mais pas de compatibilité avec les éléments flexibles, les grilles et les tables, ni la fragmentation des blocs. Nous recourrons à l'ancien moteur de mise en page pour les éléments Flex, les grilles, les tables et tout ce qui implique la fragmentation des blocs. Cela était vrai même pour les éléments de bloc, en ligne, flottants et hors flux dans un contenu fragmenté. Comme vous pouvez le constater, la mise à niveau d'un moteur de mise en page aussi complexe en place est une danse très délicate.
Par ailleurs, à la mi-2019, la majorité des fonctionnalités de base de la mise en page de fragmentation des blocs LayoutNG étaient déjà implémentées (derrière un indicateur). Pourquoi la livraison a-t-elle pris autant de temps ? Pour faire court, la fragmentation doit coexister correctement avec les différentes parties obsolètes du système, qui ne peuvent pas être supprimées ni mises à niveau tant que toutes les dépendances ne sont pas mises à niveau.
Interaction avec l'ancien moteur
Les anciennes structures de données sont toujours chargées des API JavaScript qui lisent les informations de mise en page. Nous devons donc réécrire les données dans l'ancien moteur de manière à ce qu'il les comprenne. Cela inclut la mise à jour correcte des anciennes structures de données multicolonnes, telles que LayoutMultiColumnFlowThread.
Détection et gestion du remplacement d'anciens moteurs
Nous avons dû revenir à l'ancien moteur de mise en page lorsque le contenu ne pouvait pas encore être géré par la fragmentation de blocs LayoutNG. Au moment de la fragmentation des blocs de base LayoutNG, elle incluait les éléments flex, grille, tableaux et tout ce qui est imprimé. Cela était particulièrement délicat, car nous devions détecter la nécessité d'anciennes créations de remplacement avant de créer des objets dans l'arborescence de mise en page. Par exemple, nous devions détecter avant de savoir s'il existait un ancêtre de conteneur multicolonne, et avant de savoir quels nœuds DOM deviendront ou non un contexte de mise en forme. Il s'agit d'un problème de poule et d'œuf qui n'a pas de solution parfaite, mais tant que le seul comportement incorrect est un faux positif (recours à l'ancien lorsque ce n'est pas nécessaire), ce n'est pas un problème, car les bugs de ce comportement de mise en page sont déjà présents dans Chromium, et non nouveaux.
Promenade dans les arbres avant la peinture
La pré-peinture est une étape que nous effectuons après la mise en page, mais avant la peinture. Le principal défi est que nous devons toujours parcourir l'arborescence des objets de mise en page, mais nous avons maintenant des fragments NG. Comment y faire face ? Nous parcourons simultanément l'objet de mise en page et les arbres de fragments NG. Cette opération est assez complexe, car la mise en correspondance entre les deux arbres n'est pas simple.
Bien que la structure arborescente de l'objet de mise en page ressemble étroitement à celle de l'arborescence DOM, l'arborescence de fragments est une sortie de la mise en page, et non une entrée. En plus de refléter l'effet de toute fragmentation, y compris la fragmentation en ligne (fragments de ligne) et la fragmentation par bloc (fragments de colonne ou de page), l'arborescence des fragments présente également une relation parent-enfant directe entre un bloc contenant et les descendants DOM qui ont ce fragment comme bloc contenant. Par exemple, dans l'arborescence des fragments, un fragment généré par un élément positionné de manière absolue est un enfant direct du fragment de bloc contenant, même s'il existe d'autres nœuds dans la chaîne d'ascendance entre le descendant hors flux et son bloc contenant.
Cela peut s'avérer encore plus compliqué lorsqu'un élément est positionné hors de la fragmentation, car les fragments hors flux deviennent alors des enfants directs du fragmentainer (et non un enfant de ce que le CSS considère comme le bloc conteneur). Ce problème devait être résolu pour coexister avec l'ancien moteur. À l'avenir, nous devrions être en mesure de simplifier ce code, car LayoutNG est conçu pour prendre en charge de manière flexible tous les modes de mise en page modernes.
Problèmes liés à l'ancien moteur de fragmentation
L'ancien moteur, conçu à une époque antérieure du Web, ne comporte pas vraiment de concept de fragmentation, même si la fragmentation existait techniquement à l'époque (pour prendre en charge l'impression). La prise en charge de la fragmentation n'était qu'un élément ajouté (impression) ou rétrofité (multicolonne).
Lors de la mise en page du contenu fragmentable, l'ancien moteur le met en page dans une bande haute dont la largeur correspond à la taille intégrée d'une colonne ou d'une page, et dont la hauteur est aussi haute que nécessaire pour contenir son contenu. Cette bande haute n'est pas affichée sur la page. Imaginez qu'elle soit affichée sur une page virtuelle qui est ensuite réorganisée pour l'affichage final. Ce processus est conceptuellement similaire à l'impression d'un article de journal papier entier dans une colonne, puis à l'utilisation de ciseaux pour le découper en plusieurs éléments lors de la deuxième étape. (À l'époque, certains journaux utilisaient des techniques similaires.)
L'ancien moteur suit une limite de page ou de colonne imaginaire dans la bande. Cela permet de déplacer le contenu qui ne dépasse pas la limite vers la page ou la colonne suivante. Par exemple, si seule la moitié supérieure d'une ligne peut tenir sur ce que le moteur considère comme la page actuelle, il insère une "entretoise de pagination" pour la pousser vers le bas à la position où le moteur suppose que se trouve le haut de la page suivante. Ensuite, la majeure partie du travail de fragmentation réel (le "découpage avec des ciseaux et le placement") a lieu après la mise en page lors de la pré-peinture et de la peinture, en découpant la longue bande de contenu en pages ou en colonnes (en coupant et en traduisant des parties). Cela rendait certaines choses essentiellement impossibles, comme l'application de transformations et le positionnement relatif après la fragmentation (ce qui est requis par la spécification). De plus, bien que le moteur précédent prenne en charge la fragmentation de table, il n'est pas du tout compatible avec la fragmentation flex ou de grille.
Voici une illustration de la façon dont une mise en page à trois colonnes est représentée en interne dans l'ancien moteur, avant d'utiliser des ciseaux, un emplacement et de la colle (nous avons spécifié une hauteur, de sorte que seules quatre lignes s'adaptent, mais il reste de l'espace en trop en bas):
Étant donné que l'ancien moteur de mise en page ne fragmente pas le contenu lors de la mise en page, il existe de nombreux artefacts étranges, tels que des positionnements relatifs et des transformations qui ne sont pas correctement appliquées, et des ombres de boîte qui sont rognées au bord des colonnes.
Voici un exemple avec text-shadow:
L'ancien moteur ne gère pas correctement ces opérations:
Voyez-vous comment l'ombre du texte de la ligne de la première colonne est rognée et placée en haut de la deuxième colonne ? En effet, l'ancien moteur de mise en page ne comprend pas la fragmentation.
Voici le résultat attendu :
Ensuite, rendons les choses un peu plus complexes avec des transformations et une ombre portée. Notez que dans l'ancien moteur, le recadrage et le débordement de colonne sont incorrects. En effet, selon les spécifications, les transformations sont censées être appliquées en tant qu'effet post-mise en page et post-fragmentation. Avec la fragmentation LayoutNG, les deux fonctionnent correctement. Cela augmente l'interopérabilité avec Firefox, qui a déjà bien pris en charge la fragmentation depuis un certain temps, la plupart des tests dans ce domaine étant également passés par là.
L'ancien moteur rencontre également des problèmes avec le contenu monolithique de grande taille. Un contenu est monolithique s'il ne peut pas être divisé en plusieurs fragments. Les éléments avec défilement en cas de dépassement sont monolithiques, car il n'est pas logique pour les utilisateurs de faire défiler une région non rectangulaire. Les zones de ligne et les images sont d'autres exemples de contenu monolithique. Exemple :
Si le contenu monolithique est trop haut pour tenir dans une colonne, l'ancien moteur le découpe de manière brutale (ce qui entraîne un comportement très "intéressant" lorsque vous essayez de faire défiler le conteneur à faire défiler):
Au lieu de laisser le contenu déborder dans la première colonne (comme c'est le cas avec la fragmentation de blocs LayoutNG):
L'ancien moteur est compatible avec les pauses forcées. Par exemple, <div style="break-before:page;">
insère un saut de page avant l'élément DIV. Cependant, sa compatibilité avec la recherche de sauts de page non forcés optimaux est limitée. Il est compatible avec break-inside:avoid
et les orphelins et les veuves, mais il n'est pas possible d'éviter les coupures entre les blocs, par exemple si elles sont demandées via break-before:avoid
. Considérez l'exemple suivant :
Ici, l'élément #multicol
peut contenir cinq lignes dans chaque colonne (car sa hauteur est de 100 pixels et sa hauteur de 20 pixels). Ainsi, tout #firstchild
peut tenir dans la première colonne. Toutefois, son frère #secondchild
a break-before:avoid, ce qui signifie que le contenu ne souhaite pas qu'une coupure se produise entre eux. Étant donné que la valeur de widows
est 2, nous devons insérer deux lignes de #firstchild
dans la deuxième colonne pour respecter toutes les demandes d'évitement des coupures. Chromium est le premier moteur de navigateur entièrement compatible avec cette combinaison de fonctionnalités.
Fonctionnement de la fragmentation NG
Le moteur de mise en page NG met généralement en page le document en parcourant l'arborescence des boîtes CSS en profondeur. Lorsque tous les descendants d'un nœud sont mis en page, la mise en page de ce nœud peut être finalisée en créant un NGPhysicalFragment et en revenant à l'algorithme de mise en page parent. Cet algorithme ajoute le fragment à sa liste de fragments enfants et, une fois que tous les enfants sont terminés, génère un fragment pour lui-même avec tous ses fragments enfants à l'intérieur. Cette méthode crée une arborescence de fragments pour l'ensemble du document. Il s'agit toutefois d'une simplification excessive: par exemple, les éléments positionnés en dehors du flux doivent remonter de l'endroit où ils se trouvent dans l'arborescence DOM vers leur bloc contenant avant de pouvoir être mis en page. Je vais ignorer ce détail avancé pour plus de simplicité.
En plus de la zone CSS elle-même, LayoutNG fournit un espace de contrainte à un algorithme de mise en page. Cela fournit à l'algorithme des informations telles que l'espace disponible pour la mise en page, si un nouveau contexte de mise en forme est établi et les résultats de la réduction des marges intermédiaires du contenu précédent. L'espace de contraintes connaît également la taille de bloc mise en page du fragmenteur et le décalage de bloc actuel dans celui-ci. Cela indique où le saut de route.
Lorsque la fragmentation de bloc est impliquée, la mise en page des descendants doit s'arrêter à une coupure. Les raisons de cette coupure peuvent être diverses : manque d'espace sur la page ou dans la colonne, ou coupure forcée. Nous produisons ensuite des fragments pour les nœuds que nous avons visités, et nous remontons jusqu'à la racine du contexte de fragmentation (le conteneur multicolonne ou, en cas d'impression, la racine du document). Ensuite, à la racine du contexte de fragmentation, nous nous préparons pour un nouveau fragmentainer, puis nous redescendons dans l'arborescence, en reprenant là où nous nous sommes arrêtés avant la pause.
La structure de données essentielle pour fournir les moyens de reprendre la mise en page après une pause est appelée NGBlockBreakToken. Il contient toutes les informations nécessaires pour reprendre la mise en page correctement dans le prochain fragmentainer. Un NGBlockBreakToken est associé à un nœud et forme un arbre NGBlockBreakToken, de sorte que chaque nœud à reprendre soit représenté. Un NGBlockBreakToken est associé au NGPhysicalBoxFragment généré pour les nœuds qui sont intrusifs. Les jetons de rupture sont propagés aux parents, formant un arbre de jetons de rupture. Si nous devons effectuer une coupure avant un nœud (plutôt que dedans), aucun fragment ne sera généré, mais le nœud parent doit toujours créer un jeton de coupure "break-before" pour le nœud, afin que nous puissions commencer à le mettre en page lorsque nous atteignons la même position dans l'arborescence des nœuds du fragmenteur suivant.
Des coupures sont insérées lorsque nous manquons d'espace dans le fragmenteur (coupure non forcée) ou lorsqu'une coupure forcée est demandée.
La spécification contient des règles pour les coupures involontaires optimales. Il n'est pas toujours judicieux d'insérer une coupure exactement là où nous manquons d'espace. Par exemple, diverses propriétés CSS telles que break-before
influencent le choix de l'emplacement de la coupure.
Lors de la mise en page, afin d'implémenter correctement la section de spécifications sur les coupures forcées, nous devons suivre les points de rupture potentiellement appropriés. Cet enregistrement nous permet de revenir en arrière et d'utiliser le dernier point d'arrêt possible trouvé, si nous manquons d'espace à un point où nous ne respecterions pas les requêtes d'évitement des interruptions (par exemple, break-before:avoid
ou orphans:7
). Chaque point d'arrêt possible reçoit un score, allant de "ne le faire qu'en dernier recours" à "endroit idéal pour l'arrêt", avec des valeurs intermédiaires. Si un emplacement est marqué comme étant "parfait", cela signifie qu'aucune règle contraire ne sera enfreinte si nous ne la respectons pas (et si nous obtenons ce score exactement au moment où nous manquons d'espace, il n'est pas nécessaire de chercher quelque chose de mieux). Si le score est "dernier recours", le point d'arrêt n'est même pas valide, mais nous pouvons toujours y interrompre le processus si nous ne trouvons rien de mieux, afin d'éviter tout débordement de fragmenteur.
Les points d'arrêt valides ne se produisent généralement que entre des frères et sœurs (boîtes de ligne ou blocs), et non, par exemple, entre un parent et son premier enfant (les points d'arrêt de classe C constituent une exception, mais nous n'avons pas besoin d'en parler ici). Il existe un point d'arrêt valide, par exemple, avant un frère de bloc avec break-before:avoid, mais il se situe quelque part entre "parfait" et "dernier recours".
Lors de la mise en page, nous enregistrons le meilleur point d'arrêt trouvé jusqu'à présent dans une structure appelée NGEarlyBreak. Un "early-break" est un point d'arrêt possible avant ou dans un nœud de bloc, ou avant une ligne (ligne de conteneur de bloc ou ligne flex). Nous pouvons former une chaîne ou un chemin d'objets NGEarlyBreak, au cas où le meilleur point d'arrêt se trouverait dans un élément que nous avons ignoré précédemment lorsque nous avons manqué d'espace. Exemple :
Dans ce cas, nous manquons d'espace juste avant #second
, mais il contient "break-before:avoid", qui obtient un score d'emplacement de coupure de "non-respect de l'évitement de coupure". À ce stade, nous avons une chaîne NGEarlyBreak de "inside #outer
> inside #middle
> inside #inner
> before "line 3"', avec "perfect". Nous préférons donc interrompre le processus à ce stade. Nous devons donc revenir en arrière et réexécuter la mise en page depuis le début de #outer (et cette fois transmettre le NGEarlyBreak que nous avons trouvé), afin de pouvoir interrompre avant la ligne 3 dans #inner. (Nous interrompons avant la ligne 3 afin que les quatre lignes restantes se retrouvent dans le fragmenteur suivant et pour respecter widows:4
.)
L'algorithme est conçu pour s'arrêter toujours au meilleur point d'arrêt possible, tel que défini dans les spécifications, en abandonnant les règles dans l'ordre approprié si toutes ne peuvent pas être satisfaites. Notez que nous ne devons effectuer une nouvelle mise en page qu'une seule fois par flux de fragmentation. Au moment de la deuxième étape de mise en page, le meilleur emplacement de coupure a déjà été transmis aux algorithmes de mise en page. Il s'agit de l'emplacement de coupure découvert lors de la première étape de mise en page et fourni dans la sortie de mise en page de ce cycle. Lors de la deuxième étape de mise en page, nous ne laissons pas l'espace se réduire jusqu'à ce que nous n'ayons plus de place. En fait, nous ne devrions pas manquer d'espace (ce serait une erreur), car nous avons un emplacement super pratique (le plus pratique possible) pour insérer une coupure anticipée, afin d'éviter de violer inutilement les règles de rupture. Nous nous arrêtons à ce point.
À ce sujet, nous devons parfois ne pas respecter certaines des requêtes d'évitement des coupures si cela permet d'éviter le débordement du fragmenteur. Exemple :
Ici, nous manquons d'espace juste avant #second
, mais la valeur "break-before:avoid" est spécifiée. Cela se traduit par "non-respect de l'évitement de la coupure", comme dans le dernier exemple. Nous avons également un NGEarlyBreak avec "orphans and widows" (orphelins et veuves non conformes) (dans #first
> avant "line 2"), qui n'est toujours pas parfait, mais mieux que "violating break avoid" (non-respect de l'évitement de la coupure). Nous allons donc faire une pause avant "ligne 2", ce qui enfreint la requête "orphelines / veuves". La spécification traite de ce point dans la section 4.4. "Unforced Breaks" (Pauses non forcées), qui définit les règles de rupture ignorées en premier si nous ne disposons pas de suffisamment de points d'arrêt pour éviter le débordement du fragmenteur.
Conclusion
L'objectif fonctionnel du projet de fragmentation de blocs LayoutNG était de fournir une implémentation compatible avec l'architecture LayoutNG, et le moins possible d'éléments compatibles avec l'ancien moteur, hormis les corrections de bugs. La principale exception concerne une meilleure prise en charge de l'évitement des coupures (break-before:avoid
, par exemple), car il s'agit d'un élément essentiel du moteur de fragmentation. Il devait donc être présent dès le départ, car l'ajouter plus tard impliquerait une nouvelle réécriture.
Maintenant que la fragmentation des blocs LayoutNG est terminée, nous pouvons commencer à ajouter de nouvelles fonctionnalités, telles que la prise en charge de tailles de page mixtes lors de l'impression, des marges @page
lors de l'impression, box-decoration-break:clone
, etc. Comme pour LayoutNG en général, nous nous attendons à ce que le taux de bugs et la charge de maintenance du nouveau système soient considérablement inférieurs au fil du temps.
Remerciements
- Una Kravets pour la belle "capture d'écran faite à la main".
- Chris Harrelson pour la relecture, les commentaires et les suggestions.
- Philip Jägenstedt pour obtenir des commentaires et des suggestions.
- Rachel Andrew pour modifier le premier exemple de figure à plusieurs colonnes.