RenderingNG deep-dive: LayoutNG-blokfragmentatie

Blokfragmentatie in LayoutNG is nu voltooid. Ontdek in dit artikel hoe het werkt en waarom het belangrijk is.

Morten Stenshorne
Morten Stenshorne

Ik ben Morten Stenshorne, een lay-outingenieur in het Blink-renderingteam bij Google. Ik ben sinds het begin van de jaren 2000 betrokken geweest bij de ontwikkeling van browserengines en heb veel plezier gehad, zoals het helpen slagen van de acid2-test in de Presto-engine (Opera 12 en eerder), en het reverse-engineeren van andere browsers om tabelindeling repareren in Presto. Ik heb ook meer van die jaren besteed dan ik zou willen toegeven aan blokfragmentatie, en in het bijzonder aan multicol in Presto, WebKit en Blink. De afgelopen jaren bij Google heb ik mij vooral geconcentreerd op het leiden van het werk van het toevoegen van ondersteuning voor blokfragmentatie aan LayoutNG . Ga met mij mee in deze diepe duik in de implementatie van blokfragmentatie, aangezien het heel goed de laatste keer kan zijn dat ik blokfragmentatie implementeer. :)

Wat is blokfragmentatie?

Bij blokfragmentatie gaat het om het splitsen van een CSS-vak op blokniveau (zoals een sectie of alinea) in meerdere fragmenten wanneer het niet als geheel past in één fragmentcontainer die een fragmentainer wordt genoemd. 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.

Een alinea tekst opgesplitst in twee kolommen.
In dit voorbeeld is een alinea in twee kolommen gesplitst met behulp van een indeling met meerdere kolommen. Elke kolom is een fragmentainer, die een fragment van de gefragmenteerde stroom vertegenwoordigt.

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.

Wat is LayoutNG-blokfragmentatie?

LayoutNGBlockFragmentation is een herschrijving van de fragmentatie-engine voor LayoutNG, en na vele jaren werken werden de eerste onderdelen eerder dit jaar eindelijk in Chrome 102 uitgebracht. Hiermee werden al lang bestaande problemen opgelost die in wezen niet konden worden opgelost in onze "verouderde" engine. In termen van datastructuren vervangt het meerdere pre-NG-datastructuren 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 over het algemeen niet goed uit als het laatste wat op een pagina wordt geplaatst een koptekst is, terwijl de inhoud van de sectie op de volgende pagina begint. In plaats daarvan is het beter om vóór de kopbal te breken. Zie onderstaande figuur voor een voorbeeld.

Het eerste voorbeeld toont een kop onderaan de pagina, het tweede toont deze bovenaan de volgende pagina met de bijbehorende inhoud.

Chrome 102 ondersteunt ook fragmentatie-overflow, zodat monolithische (zogenaamd onbreekbaar zijnde) inhoud niet in meerdere kolommen wordt opgedeeld en verfeffecten zoals schaduwen en transformaties correct worden toegepast.

Blokfragmentatie in LayoutNG is nu voltooid

Op het moment dat we dit schrijven, hebben we de volledige ondersteuning voor blokfragmentatie in LayoutNG 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. Dit betekent dat vanaf Chrome 108 de oude engine niet langer wordt gebruikt om de lay-out uit te voeren.

Naast het daadwerkelijk opmaken van de inhoud, ondersteunen LayoutNG-datastructuren ook schilderen en hit-testen, maar we vertrouwen nog steeds op een aantal oudere datastructuren voor JavaScript-API's die lay-outinformatie lezen, zoals offsetLeft en offsetTop .

Door alles met NG in te delen, 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.

Geloof het of niet: medio 2019 was het merendeel van de kernfunctionaliteit van de layoutNG-blokfragmentatie-indeling 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. Zie de volgende details voor het lange antwoord.

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 layout-engine toen er inhoud in zat die nog niet kon worden verwerkt door LayoutNG-blokfragmentatie. Op het moment van verzending was de kernfragmentatie van LayoutNG (voorjaar 2022) inclusief flex, raster, tabellen en alles wat maar 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 valse positieven zijn (terugvallen op de erfenis als dat eigenlijk niet nodig is), is dat geen probleem, want alle bugs in dat lay-outgedrag zijn fouten. 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 feitelijk 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 wordt zelfs nog ingewikkelder 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 helaas een probleem dat moest worden opgelost om zonder al te veel problemen naast de oudere motor te kunnen bestaan. In de toekomst zouden we veel van deze code moeten kunnen vereenvoudigen, omdat LayoutNG is ontworpen om op flexibele wijze alle moderne lay-outmodi te ondersteunen.

De problemen met de oudere fragmentatie-engine

De oudere engine, ontworpen in een vroeger tijdperk van het web, kent niet echt een concept van fragmentatie, ook al bestond fragmentatie toen 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 zou passen 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):

De interne weergave als één kolom met pagineringsbalken waar de inhoud breekt, en de weergave op het scherm als drie kolommen.

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 verkeerd worden toegepast, en vakschaduwen die worden afgesneden aan de randen van de kolommen.

Hier is een eenvoudig voorbeeld met tekstschaduw:

De oudere engine kan dit niet goed aan:

Uitgeknipte tekstschaduwen geplaatst in de tweede kolom.

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 lay-outengine geen fragmentatie begrijpt!

Het zou er zo uit moeten zien (en zo wordt het weergegeven met NG):

Twee tekstkolommen waarbij de schaduwen correct worden weergegeven.

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 kolombleed. Dat komt omdat transformaties volgens de specificaties moeten worden toegepast als een post-lay-out, 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.

Kaders zijn verkeerd verdeeld over twee kolommen.

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):

ALT_TEXT_HIER

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:

Tekst opgesplitst in twee kolommen.

Hier heeft het #multicol -element ruimte voor 5 regels in elke kolom (omdat het 100 px hoog is en de regelhoogte 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 onderbreking 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 voorkomen 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 bij 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 root van de fragmentatiecontext, 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. Er wordt een NGBlockBreakToken gekoppeld aan het NGPysicalBoxFragment dat wordt 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 simpelweg 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 de specificatiesectie voor ongeforceerde onderbrekingen correct te implementeren, moeten we tijdens de lay-out daarom 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 indeling 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 lay-out 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 lay-out 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. Voorbeeld:

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 regels voor overtredingen het eerst worden genegeerd als we niet genoeg breekpunten hebben om fragmentainer-overflow te voorkomen.

Samenvatting

Het belangrijkste functionele doel van het LayoutNG-blokfragmentatieproject was het bieden van een LayoutNG-architectuurondersteunende implementatie van alles wat de oudere engine ondersteunt, en zo min mogelijk anders, afgezien van bugfixes. De belangrijkste uitzondering hier 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 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.

Bedankt voor het lezen!

Dankbetuigingen