Blokfragmentatie splitst een CSS-blok op blokniveau (zoals een sectie of alinea) in meerdere fragmenten wanneer het niet als geheel in één fragmentcontainer past, een zogenaamde fragmentainer . Een fragmentainer is geen element, maar vertegenwoordigt een kolom in een lay-out met meerdere kolommen, of een pagina in gepagineerde media.
Om fragmentatie te laten plaatsvinden, moet de inhoud zich binnen een fragmentatiecontext bevinden. Een fragmentatiecontext wordt meestal tot stand gebracht door een container met meerdere kolommen (inhoud wordt opgesplitst in kolommen) of tijdens het afdrukken (inhoud wordt opgesplitst in pagina's). Een lange alinea met veel regels moet mogelijk in meerdere fragmenten worden gesplitst, zodat de eerste regels in het eerste fragment worden geplaatst en de overige regels in daaropvolgende fragmenten.
Blokfragmentatie is analoog aan een ander bekend type fragmentatie: lijnfragmentatie, ook wel bekend als 'regelbreuk'. Elk inline-element dat uit meer dan één woord bestaat (elk tekstknooppunt, elk <a>
-element, enzovoort) en regeleinden toestaat, kan in meerdere fragmenten worden gesplitst. Elk fragment wordt in een ander regelvak geplaatst. Een regelvak is de inline-fragmentatie die equivalent is aan een fragmentainer voor kolommen en pagina's.
LayoutNG-blokfragmentatie
LayoutNGBlockFragmentation is een herschrijving van de fragmentatie-engine voor LayoutNG, aanvankelijk geleverd in Chrome 102. In termen van datastructuren heeft het meerdere pre-NG-datastructuren vervangen door NG-fragmenten die rechtstreeks in de fragmentboom worden weergegeven.
We ondersteunen nu bijvoorbeeld de waarde 'avoid' voor de CSS-eigenschappen 'break-before' en 'break-after' , waarmee auteurs pauzes direct na een header kunnen vermijden. Het ziet er vaak vreemd uit als het laatste op een pagina een koptekst is, terwijl de inhoud van de sectie op de volgende pagina begint. Het is beter om vóór de kopbal te breken.
Chrome ondersteunt ook fragmentatie-overflow, zodat monolithische (zogenaamd onbreekbare) inhoud niet in meerdere kolommen wordt opgedeeld en verfeffecten zoals schaduwen en transformaties correct worden toegepast.
Blokfragmentatie in LayoutNG is nu voltooid
Kernfragmentatie (blokcontainers, inclusief lijnindeling, floats en out-of-flow-positionering) geleverd in Chrome 102. Flex- en rasterfragmentatie geleverd in Chrome 103, en tabelfragmentatie geleverd in Chrome 106. Ten slotte wordt afdrukken geleverd in Chrome 108. Blokfragmentatie was de laatste functie die voor het uitvoeren van de lay-out afhankelijk was van de oudere engine.
Vanaf Chrome 108 wordt de oude engine niet langer gebruikt om de lay-out uit te voeren.
Bovendien ondersteunen LayoutNG-datastructuren schilderen en hit-testen, maar we vertrouwen wel op een aantal oudere datastructuren voor JavaScript-API's die lay-outinformatie lezen, zoals offsetLeft
en offsetTop
.
Door alles met NG in te richten, wordt het mogelijk nieuwe functies te implementeren en te leveren die alleen LayoutNG-implementaties hebben (en geen tegenhanger van de oudere engine), zoals CSS-containerquery's , ankerpositionering, MathML en aangepaste lay-out (Houdini) . Voor containervragen hebben we het iets van tevoren verzonden, met een waarschuwing aan ontwikkelaars dat afdrukken nog niet werd ondersteund.
We hebben het eerste deel van LayoutNG in 2019 verzonden, dat bestond uit de reguliere blokcontainerindeling, inline-indeling, floats en out-of-flow-positionering, maar geen ondersteuning voor flex, grid of tabellen, en helemaal geen ondersteuning voor blokfragmentatie. We zouden terugvallen op het gebruik van de oude layout-engine voor flex, grid, tabellen en alles wat met blokfragmentatie te maken had. Dat gold zelfs voor blok-, inline-, zwevende en out-of-flow-elementen binnen gefragmenteerde inhoud. Zoals je kunt zien, is het ter plekke upgraden van zo'n complexe layout-engine een heel delicate dans.
Bovendien was medio 2019 het grootste deel van de kernfunctionaliteit van de lay-out voor blokfragmentatie van LayoutNG al geïmplementeerd (achter een vlag). Dus waarom duurde het zo lang voordat het werd verzonden? Het korte antwoord is: fragmentatie moet correct naast elkaar bestaan met verschillende oudere delen van het systeem, die niet kunnen worden verwijderd of geüpgraded totdat alle afhankelijkheden zijn geüpgraded.
Interactie met oudere motoren
Oudere datastructuren zijn nog steeds verantwoordelijk voor JavaScript-API's die lay-outinformatie lezen, dus we moeten gegevens terugschrijven naar de oudere engine op een manier die deze begrijpt. Dit omvat het correct bijwerken van de oudere gegevensstructuren met meerdere kolommen, zoals LayoutMultiColumnFlowThread .
Detectie en verwerking van fallback van verouderde motoren
We moesten terugvallen op de oudere lay-outengine toen er inhoud in zat die nog niet kon worden verwerkt door LayoutNG-blokfragmentatie. Op het moment van verzending was de kern van LayoutNG blokfragmentatie, inclusief flex, raster, tabellen en alles wat werd afgedrukt. Dit was vooral lastig omdat we de noodzaak van verouderde fallback moesten detecteren voordat we objecten in de lay-outstructuur konden maken. We moesten bijvoorbeeld detecteren voordat we wisten of er een voorouder van een container met meerdere kolommen was, en voordat we wisten welke DOM-knooppunten een opmaakcontext zouden worden of niet. Het is een kip-en-ei-probleem waarvoor geen perfecte oplossing bestaat, maar zolang het enige wangedrag bestaat uit valse positieven (terugvallen op de erfenis terwijl dat eigenlijk niet nodig is), is dat geen probleem, want alle bugs in dat lay-outgedrag zijn die. Chromium heeft dat al, geen nieuwe.
Boomwandeling vooraf schilderen
Voorschilderen is iets dat we doen na het opmaken , maar vóór het schilderen. De grootste uitdaging is dat we nog steeds door de lay-outobjectboom moeten lopen, maar we hebben nu NG-fragmenten – dus hoe gaan we daarmee om? We lopen tegelijkertijd door het lay-outobject en de NG-fragmentbomen! Dit is behoorlijk ingewikkeld, omdat het in kaart brengen van de twee bomen niet triviaal is.
Hoewel de boomstructuur van het lay-outobject sterk lijkt op die van de DOM-boom, is de fragmentboom een uitvoer van de lay-out en geen invoer daarvoor. Behalve dat het daadwerkelijk het effect weergeeft van elke fragmentatie, inclusief inline-fragmentatie (lijnfragmenten) en blokfragmentatie (kolom- of paginafragmenten), heeft de fragmentboom ook een directe ouder-kindrelatie tussen een bevattend blok en de DOM-afstammelingen die dat fragment als hun bevattende blok. In de fragmentboom is een fragment dat wordt gegenereerd door een absoluut gepositioneerd element bijvoorbeeld een direct kind van het bevattende blokfragment, zelfs als er andere knooppunten in de voorouderketen zijn tussen de uit de stroom gepositioneerde afstammeling en het bevattende blok.
Het kan zelfs nog ingewikkelder zijn als er een uit de stroom gepositioneerd element binnen de fragmentatie zit, omdat de uit de stroom komende fragmenten dan directe kinderen worden van de fragmentainer (en niet een kind van wat CSS denkt dat het het bevattende blok is). Dit was een probleem dat moest worden opgelost om naast de oudere motor te kunnen blijven bestaan. In de toekomst zouden we deze code moeten kunnen vereenvoudigen, omdat LayoutNG is ontworpen om alle moderne lay-outmodi flexibel te ondersteunen.
De problemen met de oudere fragmentatie-engine
De oudere engine, ontworpen in een eerder tijdperk van het web, kent niet echt een concept van fragmentatie, ook al bestond fragmentatie destijds technisch gezien ook (om afdrukken te ondersteunen). Ondersteuning voor fragmentatie was gewoon iets dat er bovenop werd vastgeschroefd (afdrukken) of achteraf werd ingebouwd (meerdere kolommen).
Bij het opmaken van fragmenteerbare inhoud verdeelt de oudere engine alles in een hoge strook waarvan de breedte de inline-grootte is van een kolom of pagina, en de hoogte zo groot is als nodig is om de inhoud ervan te bevatten. Deze hoge strook wordt niet op de pagina weergegeven. Zie het als een weergave op een virtuele pagina die vervolgens opnieuw wordt gerangschikt voor definitieve weergave. Het is conceptueel vergelijkbaar met het afdrukken van een heel papieren krantenartikel in één kolom, en het vervolgens met een schaar in meerdere kolommen knippen als tweede stap. (Vroeger gebruikten sommige kranten soortgelijke technieken!)
De oudere engine houdt een denkbeeldige pagina- of kolomgrens in de strip bij. Hierdoor kan inhoud die niet voorbij de grens past, naar de volgende pagina of kolom worden verplaatst. Als bijvoorbeeld alleen de bovenste helft van een regel past op wat de engine denkt dat de huidige pagina is, zal deze een "pagineringssteun" invoegen om deze naar beneden te duwen naar de positie waar de engine aanneemt dat de bovenkant van de volgende pagina is . Vervolgens vindt het grootste deel van het eigenlijke fragmentatiewerk (het "knippen met een schaar en plaatsing") plaats na de lay-out tijdens het voorverven en schilderen, door de lange strook inhoud in pagina's of kolommen te snijden (door gedeelten te knippen en te vertalen). Dit maakte een aantal dingen feitelijk onmogelijk, zoals het toepassen van transformaties en relatieve positionering na fragmentatie (wat de specificatie vereist). Bovendien is er, hoewel er enige ondersteuning is voor tabelfragmentatie in de oudere engine, helemaal geen ondersteuning voor flex- of rasterfragmentatie.
Hier is een illustratie van hoe een lay-out met drie kolommen intern wordt weergegeven in de oudere engine, voordat schaar, plaatsing en lijm worden gebruikt (we hebben een gespecificeerde hoogte, zodat er slechts vier lijnen passen, maar er is wat overtollige ruimte onderaan):
Omdat de oudere lay-outengine de inhoud niet daadwerkelijk fragmenteert tijdens de lay-out, zijn er veel vreemde artefacten, zoals relatieve positionering en transformaties die onjuist worden toegepast, en vakschaduwen die aan de randen van de kolommen worden afgekapt.
Hier is een voorbeeld met tekstschaduw:
De oudere engine kan dit niet goed aan:
Zie je hoe de tekstschaduw van de lijn in de eerste kolom wordt afgesneden en in plaats daarvan bovenaan de tweede kolom wordt geplaatst? Dat komt omdat de oude layout-engine fragmentatie niet begrijpt.
Het zou er als volgt uit moeten zien:
Laten we het vervolgens een beetje ingewikkelder maken, met transformaties en doosschaduw. Merk op dat er in de oudere engine sprake is van onjuiste clipping en kolombloedingen. Dat komt omdat transformaties volgens de specificaties moeten worden toegepast als een post-layout, post-fragmentatie-effect. Met LayoutNG-fragmentatie werken beide correct. Dit vergroot de interoperabiliteit met Firefox, dat al enige tijd goede fragmentatieondersteuning heeft en de meeste tests op dit gebied daar ook passeren.
De oudere engine heeft ook problemen met hoge monolithische inhoud. Inhoud is monolithisch als deze niet in meerdere fragmenten kan worden opgesplitst. Elementen met overflow-scrollen zijn monolithisch, omdat het voor gebruikers geen zin heeft om in een niet-rechthoekig gebied te scrollen. Lijnvakken en afbeeldingen zijn andere voorbeelden van monolithische inhoud. Hier is een voorbeeld:
Als het stuk monolithische inhoud te groot is om in een kolom te passen, zal de oudere engine het op brute wijze in stukken snijden (wat leidt tot zeer "interessant" gedrag bij pogingen om door de scrollbare container te scrollen):
In plaats van het de eerste kolom te laten overlopen (zoals bij LayoutNG-blokfragmentatie):
De oudere engine ondersteunt gedwongen pauzes. <div style="break-before:page;">
zal bijvoorbeeld een pagina-einde invoegen vóór de DIV. Het biedt echter slechts beperkte ondersteuning voor het vinden van optimale, ongeforceerde pauzes. Het ondersteunt wel break-inside:avoid
en orphans and weduwen , maar er is geen ondersteuning voor het vermijden van pauzes tussen blokken, indien aangevraagd via bijvoorbeeld break-before:avoid
. Beschouw dit voorbeeld:
Hier heeft het #multicol
element ruimte voor 5 regels in elke kolom (omdat het 100 px hoog is en de lijnhoogte 20 px), dus #firstchild
zou in de eerste kolom kunnen passen. Zijn broer of zus #secondchild
heeft echter break-before:avoid, wat betekent dat de inhoud wenst dat er geen pauze tussen hen plaatsvindt. Omdat de waarde van widows
2 is, moeten we 2 regels #firstchild
naar de tweede kolom schuiven om alle verzoeken om pauze te vermijden te honoreren. Chromium is de eerste browserengine die deze combinatie van functies volledig ondersteunt.
Hoe NG-fragmentatie werkt
De NG-lay-outengine legt het document over het algemeen op door de CSS-boxboom eerst in de diepte te doorkruisen. Wanneer alle afstammelingen van een knooppunt zijn ingedeeld, kan de indeling van dat knooppunt worden voltooid door een NGPysicalFragment te produceren en terug te keren naar het bovenliggende indelingsalgoritme. Dat algoritme voegt het fragment toe aan de lijst met onderliggende fragmenten en genereert, zodra alle onderliggende fragmenten zijn voltooid, een fragment voor zichzelf met alle onderliggende fragmenten erin. Door deze methode wordt een fragmentboom voor het hele document gemaakt. Dit is echter een overdreven vereenvoudiging: uit de stroom gepositioneerde elementen zullen bijvoorbeeld moeten opborrelen van de plaats waar ze in de DOM-boom aanwezig zijn naar hun bevattende blok voordat ze kunnen worden opgemaakt. Ik negeer dit geavanceerde detail hier omwille van de eenvoud.
Samen met het CSS-vak zelf biedt LayoutNG een beperkingsruimte voor een lay-outalgoritme. Dit voorziet het algoritme van informatie zoals de beschikbare ruimte voor lay-out, of er een nieuwe opmaakcontext tot stand is gebracht en het instorten van de tussenliggende marge als gevolg van voorgaande inhoud. De beperkingsruimte kent ook de opgemaakte blokgrootte van de fragmentainer, en de huidige blokverschuiving daarin. Dit geeft aan waar te breken.
Als er sprake is van blokfragmentatie, moet de lay-out van afstammelingen op een pauze stoppen. De redenen voor het afbreken zijn onder meer onvoldoende ruimte op de pagina of kolom, of een geforceerde afbreking. Vervolgens produceren we fragmenten voor de knooppunten die we hebben bezocht, en keren helemaal terug naar de fragmentatiecontextroot (de multicol-container, of, in het geval van afdrukken, de documentroot). Vervolgens bereiden we ons bij de fragmentatiecontextwortel voor op een nieuwe fragmentainer en dalen we weer af in de boom, waarbij we verdergaan waar we waren gebleven vóór de pauze.
De cruciale gegevensstructuur voor het verschaffen van de middelen om de lay-out na een pauze te hervatten, wordt NGBlockBreakToken genoemd. Het bevat alle informatie die nodig is om de lay-out correct te hervatten in de volgende fragmentainer. Een NGBlockBreakToken is gekoppeld aan een knooppunt en vormt een NGBlockBreakToken-boom, zodat elk knooppunt dat moet worden hervat, wordt weergegeven. Een NGBlockBreakToken is gekoppeld aan het NGPysicalBoxFragment dat is gegenereerd voor knooppunten die binnenin breken. De breekfiches worden doorgegeven aan de ouders en vormen zo een boom van breekfiches. Als we vóór een knooppunt moeten breken (in plaats van erbinnen), wordt er geen fragment geproduceerd, maar moet het bovenliggende knooppunt nog steeds een "break-before" break-token voor het knooppunt maken, zodat we kunnen beginnen met het opmaken ervan wanneer we komen op dezelfde positie in de knooppuntenboom in de volgende fragmentainer.
Pauzes worden ingevoegd wanneer we geen fragmentainerruimte meer hebben (een ongeforceerde pauze), of wanneer een geforceerde pauze wordt aangevraagd.
Er zijn regels in de specificatie voor optimale, ongeforceerde pauzes en het invoegen van een pauze precies daar waar we geen ruimte meer hebben, is niet altijd het juiste om te doen. Er zijn bijvoorbeeld verschillende CSS-eigenschappen zoals break-before
die de keuze van de pauzelocatie beïnvloeden.
Om tijdens de lay-out de specificatiesectie voor ongeforceerde onderbrekingen correct te implementeren, moeten we mogelijk goede breekpunten bijhouden. Deze record betekent dat we terug kunnen gaan en het laatst gevonden best mogelijke breekpunt kunnen gebruiken als we geen ruimte meer hebben op een punt waarop we verzoeken om pauzes te vermijden schenden (bijvoorbeeld break-before:avoid
of orphans:7
). Elk mogelijk breekpunt krijgt een score, variërend van "doe dit alleen als laatste redmiddel" tot "perfecte plek om te breken", met enkele waarden daartussenin. Als een pauzelocatie als "perfect" scoort, betekent dit dat er geen regels worden overtreden als we daar breken (en als we deze score precies krijgen op het punt waar we geen ruimte meer hebben, is het niet nodig om terug te kijken naar iets beters ). Als de score 'laatste redmiddel' is, is het breekpunt niet eens geldig, maar we kunnen daar nog steeds breken als we niets beters vinden, om fragmentainer overflow te voorkomen.
Geldige breekpunten komen over het algemeen alleen voor tussen broers en zussen (lijnvakken of blokken), en bijvoorbeeld niet tussen een ouder en zijn eerste kind ( klasse C-breekpunten vormen een uitzondering, maar die hoeven we hier niet te bespreken). Er is bijvoorbeeld een geldig breekpunt vóór een blokbroer of zus met break-before:avoid, maar dit ligt ergens tussen "perfect" en "laatste redmiddel".
Tijdens de lay-out houden we het beste breekpunt tot nu toe bij in een structuur genaamd NGEarlyBreak . Een vroege doorbraak is een mogelijk breekpunt vóór of binnen een blokknooppunt, of vóór een lijn (een blokcontainerlijn of een flexlijn). We kunnen een keten of pad van NGEarlyBreak-objecten vormen, voor het geval het beste breekpunt zich ergens diep in iets bevindt waar we eerder langs liepen op het moment dat er geen ruimte meer was. Hier is een voorbeeld:
In dit geval hebben we vlak voor #second
geen ruimte meer, maar er staat "break-before:avoid", wat een pauzelocatiescore krijgt van "overtredende breakvermijden". Op dat punt hebben we een NGEarlyBreak-keten van "inside #outer
> inside #middle
> inside #inner
> before "line 3"', met "perfect", dus daar zouden we liever breken. We moeten dus terugkeren en opnieuw uitvoeren layout vanaf het begin van #outer (en geef deze keer de NGEarlyBreak door die we hebben gevonden), zodat we kunnen breken vóór "regel 3" in #inner in de volgende fragmentainer, en om widows:4
.)
Het algoritme is ontworpen om altijd te breken op het best mogelijke breekpunt (zoals gedefinieerd in de specificatie ) door regels in de juiste volgorde te laten vallen, als niet aan alle regels kan worden voldaan. Houd er rekening mee dat we de lay-out slechts één keer per fragmentatiestroom opnieuw hoeven in te delen. Tegen de tijd dat we in de tweede layout-pass zijn, is de beste break-locatie al doorgegeven aan de layout-algoritmen. Dit is de break-locatie die werd ontdekt in de eerste layout-pass en die werd opgegeven als onderdeel van de layout-uitvoer in die ronde. Bij de tweede layout-pass gaan we pas aan de slag als we geen ruimte meer hebben. Sterker nog, er wordt niet van ons verwacht dat we geen ruimte meer hebben (dat zou eigenlijk een fout zijn), omdat we een superleuke layout hebben gekregen. (nou ja, zo leuk als er beschikbaar was) plaats om een vroege pauze in te lassen, om te voorkomen dat je onnodig de regels overtreedt. Dus we gaan gewoon naar dat punt toe en breken.
Wat dat betreft moeten we soms enkele van de verzoeken om onderbrekingen te vermijden schenden, als dat helpt om fragmentainer-overflow te voorkomen. Bijvoorbeeld:
Hier hebben we vlak voor #second
geen ruimte meer, maar er staat "break-before:avoid". Dat wordt vertaald naar "het overtreden van break-vermijding", net als het laatste voorbeeld. We hebben ook een NGEarlyBreak met "het overtreden van wezen en weduwen" (binnen #first
> vóór "regel 2"), wat nog steeds niet perfect is, maar beter dan "het overtreden van pauze vermijden". We breken dus vóór "regel 2" en schenden daarmee het wezen-/weduwenverzoek. De specificatie behandelt dit in 4.4. Unforced Breaks , waar het definieert welke overtredende regels als eerste worden genegeerd als we niet genoeg breekpunten hebben om fragmentainer-overflow te voorkomen.
Conclusie
Het functionele doel van het LayoutNG-blokfragmentatieproject was om een LayoutNG-architectuur-ondersteunende implementatie te bieden van alles wat de oudere engine ondersteunt, en zo min mogelijk anders, afgezien van bugfixes. De belangrijkste uitzondering is betere ondersteuning voor het vermijden van onderbrekingen ( bijvoorbeeld break-before:avoid
), omdat dit een kernonderdeel is van de fragmentatie-engine, dus het moest er vanaf het begin in zitten, omdat het later toevoegen ervan opnieuw herschrijven zou betekenen.
Nu de blokfragmentatie van LayoutNG is voltooid, kunnen we beginnen met het toevoegen van nieuwe functionaliteit, zoals het ondersteunen van gemengde paginaformaten bij het afdrukken, @page
margin-vakken bij het afdrukken, box-decoration-break:clone
en meer. En net als bij de LayoutNG in het algemeen verwachten we dat het aantal bugs en de onderhoudslast van het nieuwe systeem in de loop van de tijd aanzienlijk lager zal zijn.
Dankbetuigingen
- Una Kravets voor de mooie "handgemaakte screenshot".
- Chris Harrelson voor proeflezen, feedback en suggesties.
- Philip Jägenstedt voor feedback en suggesties.
- Rachel Andrew voor het bewerken en de eerste voorbeeldfiguur met meerdere kolommen.