Verder dan reguliere expressies: Verbetering van het parseren van CSS-waarden in Chrome DevTools

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Is het je opgevallen dat de CSS-eigenschappen op het tabblad Stijlen van Chrome DevTools er de laatste tijd wat gepolijster uitzien? Deze updates, uitgerold tussen Chrome 121 en 128, zijn het resultaat van een aanzienlijke verbetering in de manier waarop we CSS-waarden parseren en presenteren. In dit artikel leiden we u door de technische details van deze transformatie, waarbij we overgaan van een systeem voor het matchen van reguliere expressies naar een robuustere parser.

Laten we de huidige DevTools vergelijken met de vorige versie:

Boven: het is de nieuwste Chrome, onder: Chrome 121.

Een behoorlijk verschil, toch? Hier volgt een overzicht van de belangrijkste verbeteringen:

  • color-mix . Een handig voorbeeld dat de twee kleurargumenten binnen de color-mix visueel weergeeft.
  • pink . Een klikbaar kleurvoorbeeld voor de genoemde kleur pink . Klik erop om een ​​kleurkiezer te openen voor eenvoudige aanpassingen.
  • var(--undefined, [fallback value]) . Verbeterde verwerking van ongedefinieerde variabelen, waarbij de ongedefinieerde variabele grijs wordt weergegeven en de actieve fallback-waarde (in dit geval een HSL-kleur) wordt weergegeven met een klikbaar kleurvoorbeeld.
  • hsl(…) : Nog een klikbaar kleurvoorbeeld voor de hsl -kleurfunctie, die snelle toegang tot de kleurkiezer biedt.
  • 177deg : een klikbare hoekklok waarmee u de hoekwaarde interactief kunt slepen en wijzigen.
  • var(--saturation, …) : Een klikbare link naar de aangepaste eigenschapsdefinitie, waardoor u gemakkelijk naar de relevante declaratie kunt springen.

Het verschil is opvallend. Om dit te bereiken moesten we DevTools leren de CSS-eigenschapswaarden veel beter te begrijpen dan voorheen.

Waren deze previews niet al beschikbaar?

Hoewel deze voorbeeldpictogrammen misschien bekend voorkomen, worden ze niet altijd consistent weergegeven, vooral niet in complexe CSS-syntaxis zoals in het bovenstaande voorbeeld. Zelfs als ze wel werkten, waren er vaak aanzienlijke inspanningen nodig om ze correct te laten functioneren.

De reden daarvoor is dat het systeem voor het analyseren van waarden sinds de eerste dagen van DevTools organisch is gegroeid. Het is echter niet in staat gebleken gelijke tred te houden met de recente verbazingwekkende nieuwe functies die we van CSS krijgen, en de daarmee gepaard gaande toename in taalcomplexiteit. Het systeem vereiste een volledig herontwerp om gelijke tred te houden met de evolutie en dat is precies wat we deden!

Hoe CSS-eigenschapswaarden worden verwerkt

In DevTools is het proces van het weergeven en decoreren van eigendomsdeclaraties op het tabblad Stijlen opgesplitst in twee afzonderlijke fasen:

  1. Structurele analyse. In deze eerste fase wordt de eigendomsverklaring ontleed om de onderliggende componenten en hun relaties te identificeren. In de border: 1px solid red herkent het bijvoorbeeld 1px als een lengte, solid als een string en red als een kleur.
  2. Weergave. Voortbouwend op de structurele analyse transformeert de weergavefase deze componenten in een HTML-representatie. Dit verrijkt de weergegeven eigendomstekst met interactieve elementen en visuele aanwijzingen. De kleurwaarde red wordt bijvoorbeeld weergegeven met een klikbaar kleurpictogram dat, wanneer erop wordt geklikt, een kleurkiezer onthult voor eenvoudige aanpassing.

Reguliere expressies

Voorheen vertrouwden we op reguliere expressies (regexes) om de eigenschapswaarden voor structurele analyse te ontleden. We hebben een lijst met regexes bijgehouden die overeenkomen met de stukjes eigenschapswaarde die we als decoratie beschouwden. Er waren bijvoorbeeld expressies die overeenkwamen met CSS-kleuren, lengtes, hoeken, ingewikkelder subexpressies zoals var functieaanroepen, enzovoort. We hebben de tekst van links naar rechts gescand om waardeanalyses uit te voeren, waarbij we voortdurend op zoek waren naar de eerste uitdrukking uit de lijst die overeenkomt met het volgende stuk tekst.

Hoewel dit meestal prima werkte, bleef het aantal gevallen waarin dit niet gebeurde groeien. Door de jaren heen hebben we een groot aantal bugrapporten ontvangen waarin de matching niet helemaal klopte. Toen we deze oplosten – sommige eenvoudig, andere behoorlijk uitgebreid – moesten we onze aanpak heroverwegen om onze technische schulden op afstand te houden. Laten we eens een paar van de problemen bekijken!

Overeenkomende color-mix()

De regex die we gebruikten voor de color-mix() functie was de volgende:

/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g

Wat overeenkomt met de syntaxis:

color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})

Probeer het volgende voorbeeld uit te voeren om de overeenkomsten te visualiseren.

const re = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;

// it works - simpler example
const simpler = re.exec('color-mix(in srgb, pink, hsl(127deg 100% 50%))');
console.table(simpler.groups);

re.exec('');

// it doesn't work - complex example
const complex = re.exec('color-mix(in srgb, pink, var(--undefined, hsl(127deg var(--saturation, 100%) 50%)))');
console.table(complex.groups);

Matchresultaat voor de kleurenmixfunctie.

Het eenvoudigere voorbeeld werkt prima. In het complexere voorbeeld is de <firstColor> -match echter hsl(177deg var(--saturation and <secondColor> match is 100%) 50%)) , wat volkomen zinloos is.

We wisten dat dit een probleem was. CSS als formele taal is tenslotte niet regulier , dus hebben we al een speciale behandeling toegevoegd om met ingewikkeldere functieargumenten om te gaan, zoals var -functies. Zoals je in de eerste schermafbeelding kunt zien, werkte dat echter nog steeds niet in alle gevallen.

Bijpassende tan()

Een van de meest hilarische gerapporteerde bugs betrof de trigonometrische tan() functie. De regex die we gebruikten voor het matchen van kleuren bevatte een subexpressie \b[a-zA-Z]+\b(?!-) voor het matchen van benoemde kleuren, zoals het red trefwoord. Vervolgens hebben we gecontroleerd of het overeenkomende deel daadwerkelijk een benoemde kleur is, en raad eens: tan is ook een benoemde kleur! We hebben dus tan() -expressies ten onrechte geïnterpreteerd als kleuren.

Overeenkomende var()

Laten we een ander voorbeeld bekijken, var() functies met een fallback die andere var() referenties bevat: var(--non-existent, var(--margin-vertical)) .

Onze regex voor var() zou graag overeenkomen met deze waarde. Alleen stopt het matchen bij het eerste haakje sluiten. Dus de bovenstaande tekst wordt gematcht als var(--non-existent, var(--margin-vertical) . Dit is een beperking uit het leerboek van het matchen van reguliere expressies. Talen die matchende haakjes vereisen, zijn fundamenteel niet regulier.

Overgang naar een CSS-parser

Wanneer tekstanalyse met reguliere expressies niet meer werkt (omdat de geanalyseerde taal niet regulier is), is er een canonieke volgende stap: gebruik een parser voor grammatica van een hoger type. Voor CSS betekent dat een parser voor contextvrije talen. In feite bestond een dergelijk parsersysteem al in de codebase van DevTools: Lezer van CodeMirror, wat de basis vormt voor bijvoorbeeld syntaxisaccentuering in CodeMirror, de editor die je in het paneel Bronnen vindt. Met de CSS-parser van Lezer konden we (niet-abstracte) syntaxisbomen voor CSS-regels produceren en konden we deze gebruiken. Overwinning.

Een syntaxisboom voor de eigenschapswaarde `hsl(177deg var(--saturation, 100%) 50%)`. Het is een vereenvoudigde versie van het resultaat geproduceerd door de Lezer-parser, waarbij puur syntactische knooppunten voor komma's en haakjes worden weggelaten.

Alleen vonden we het out-of-the-box onhaalbaar om rechtstreeks van regex-gebaseerde matching naar parser-gebaseerde matching te migreren: de twee benaderingen werken vanuit tegengestelde richtingen. Bij het matchen van waarden met reguliere expressies scande DevTools de invoer van links naar rechts, waarbij herhaaldelijk werd geprobeerd de vroegste overeenkomst te vinden in een geordende lijst met patronen. Met een syntaxisboom zou het matchen van onderaf beginnen, bijvoorbeeld door eerst de argumenten van een aanroep te analyseren, voordat wordt geprobeerd de functieaanroep te matchen. Zie het als het evalueren van een rekenkundige uitdrukking, waarbij u eerst uitdrukkingen tussen haakjes zou overwegen, dan vermenigvuldigende operatoren en vervolgens additieve operatoren. In dit kader komt de op regex gebaseerde matching overeen met het evalueren van de rekenkundige expressie van links naar rechts. We wilden echt niet het hele matchingsysteem helemaal opnieuw schrijven: er waren 15 verschillende matchers en rendererparen, met duizenden regels code, waardoor het onwaarschijnlijk was dat we het in één keer konden verzenden.

Daarom hebben we een oplossing bedacht waarmee we stapsgewijze wijzigingen konden aanbrengen, die we hieronder in meer detail zullen beschrijven. Kortom, we hebben de tweefasige aanpak behouden, maar in de eerste fase proberen we subexpressies bottom-up te matchen (en zo te breken met de regex-stroom), en in de tweede fase renderen we top-down. In beide fasen konden we de bestaande op regex gebaseerde matchers en renders vrijwel ongewijzigd gebruiken en konden we ze dus één voor één migreren.

Fase 1: Bottom-up matching

De eerste fase doet min of meer precies en uitsluitend wat er op de cover staat. We doorkruisen de boom in volgorde van onder naar boven en proberen subexpressies te matchen bij elk syntaxisboomknooppunt dat we bezoeken. Om een ​​specifieke subexpressie te matchen, kan een matcher regex gebruiken, net zoals in het bestaande systeem. Vanaf versie 128 doen we dat in enkele gevallen zelfs nog, bijvoorbeeld voor het matchen van lengtes . Als alternatief kan een matcher de structuur analyseren van de subboom die is geworteld in het huidige knooppunt. Hierdoor kan het syntaxisfouten opsporen en tegelijkertijd structurele informatie vastleggen.

Beschouw het voorbeeld van de syntaxisboom hierboven:

Fase 1: Bottom-up matching op de syntaxisboom.

Voor deze boom zouden onze matchers in de volgende volgorde een aanvraag indienen:

  1. hsl( 177deg var(--saturation, 100%) 50%) : Eerst ontdekken we het eerste argument van de hsl functieaanroep, de tinthoek. We matchen het met een hoekmatcher, zodat we de hoekwaarde kunnen versieren met het hoekpictogram.
  2. hsl(177deg var(--saturation, 100%) 50%) : Ten tweede ontdekken we de var functieaanroep met een var-matcher. Bij zulke oproepen willen we vooral twee dingen doen:
    • Zoek de declaratie van de variabele op en bereken de waarde ervan, en voeg respectievelijk een link en een popover toe aan de naam van de variabele om er verbinding mee te maken.
    • Versier het gesprek met een kleurenpictogram als de berekende waarde een kleur is. Er is eigenlijk nog een derde ding, maar daar zullen we later over praten.
  3. hsl(177deg var(--saturation, 100%) 50%) : Ten slotte matchen we de aanroepexpressie voor de hsl functie, zodat we deze kunnen decoreren met het kleurenpictogram.

Naast het zoeken naar subexpressies die we willen verfraaien, is er eigenlijk een tweede functie die we gebruiken als onderdeel van het matchingproces. Merk op dat we in stap #2 zeiden dat we de berekende waarde voor een variabelenaam moesten opzoeken. Sterker nog, we gaan nog een stap verder en verspreiden de resultaten hogerop. En niet alleen voor de variabele, maar ook voor de fallback-waarde! Het is gegarandeerd dat bij het bezoeken van een var -functieknooppunt de onderliggende elementen ervan vooraf zijn bezocht, zodat we al de resultaten kennen van eventuele var functies die mogelijk in de fallback-waarde voorkomen. Daarom kunnen we var functies eenvoudig en goedkoop direct vervangen door hun resultaten, waardoor we triviaal vragen kunnen beantwoorden als "Is het resultaat van deze var een kleur?", zoals we deden in stap #2.

Fase 2: Rendering van bovenaf

Voor de tweede fase keren we de richting om. Op basis van de wedstrijdresultaten uit fase 1 renderen we de boom in HTML door deze in volgorde van boven naar beneden te doorlopen. Voor elk bezocht knooppunt controleren we of het overeenkomt en zo ja, dan bellen we de corresponderende renderer van de matcher. We vermijden de noodzaak van speciale afhandeling voor knooppunten die alleen tekst bevatten (zoals de NumberLiteral "50%") door een standaard matcher en renderer voor tekstknooppunten op te nemen. Renderers voeren eenvoudigweg HTML-knooppunten uit, die, wanneer ze worden samengevoegd, de weergave van de waarde van het onroerend goed produceren, inclusief de decoraties ervan.

Fase 2: Top-down weergave in de syntaxisboom.

Voor de voorbeeldstructuur is dit de volgorde waarin de eigenschapswaarde wordt weergegeven:

  1. Bezoek de hsl -functieaanroep. Het kwam overeen, dus roep de kleurfunctie-renderer aan. Het doet twee dingen:
    • Berekent de werkelijke kleurwaarde met behulp van het directe vervangingsmechanisme voor eventuele var argumenten, en tekent vervolgens een kleurenpictogram.
    • Geeft recursief de onderliggende elementen van de CallExpression weer. Dit zorgt automatisch voor het weergeven van de functienaam, haakjes en komma's, die alleen maar tekst zijn.
  2. Bezoek het eerste argument van de hsl oproep. Het kwam overeen, dus roep de hoekrenderer aan, die het hoekpictogram en de tekst van de hoek tekent.
  3. Bezoek het tweede argument, de var aanroep. Het kwam overeen, dus roep de var renderer aan, die het volgende uitvoert:
    • De tekst var( aan het begin.
    • De variabele krijgt een naam en versiert deze met een link naar de definitie van de variabele of met een grijze tekstkleur om aan te geven dat deze niet gedefinieerd is. Er wordt ook een popover aan de variabele toegevoegd om informatie over de waarde ervan weer te geven.
    • De komma en geeft vervolgens recursief de fallback-waarde weer.
    • Een sluitend haakje.
  4. Bezoek het laatste argument van de hsl oproep. Het kwam niet overeen, dus voer gewoon de tekstinhoud uit.

Is het je opgevallen dat in dit algoritme een render volledig bepaalt hoe de kinderen van een overeenkomend knooppunt worden weergegeven? Het recursief weergeven van de kinderen is proactief. Deze truc heeft een stapsgewijze migratie mogelijk gemaakt van op regex gebaseerde weergave naar op syntaxisboom gebaseerde weergave. Voor knooppunten die overeenkomen met een oudere regex-matcher, kan de overeenkomstige renderer in zijn oorspronkelijke vorm worden gebruikt. In syntaxisboomtermen zou het de verantwoordelijkheid op zich nemen voor het weergeven van de gehele subboom, en het resultaat ervan (een HTML-knooppunt) zou netjes in het omliggende weergaveproces kunnen worden aangesloten. Dit gaf ons de mogelijkheid om matchers en renderers in paren te porten en ze één voor één uit te wisselen.

Een andere leuke eigenschap van renderers die de weergave van de kinderen van hun overeenkomende knooppunten regelen, is dat het ons de mogelijkheid geeft om te redeneren over de afhankelijkheden tussen de pictogrammen die we toevoegen. In het bovenstaande voorbeeld hangt de kleur die door de hsl functie wordt geproduceerd uiteraard af van de tintwaarde. Dat betekent dat de kleur die door het kleurenpictogram wordt weergegeven, afhangt van de hoek die wordt weergegeven door het hoekpictogram. Als de gebruiker de hoekeditor via dat pictogram opent en de hoek wijzigt, kunnen we nu de kleur van het kleurenpictogram in realtime bijwerken:

Zoals je in het bovenstaande voorbeeld kunt zien, gebruiken we dit mechanisme ook voor andere pictogramcombinaties, zoals voor color-mix() en de twee kleurkanalen ervan, of var functies die een kleur retourneren uit de fallback.

Prestatie-impact

Toen we ons in dit probleem verdiepten om de betrouwbaarheid te verbeteren en langdurige problemen op te lossen, verwachtten we enige prestatievermindering, aangezien we een volwaardige parser waren gaan gebruiken. Om dit uit te testen hebben we een benchmark gemaakt die ongeveer 3,5k eigendomsdeclaraties weergeeft en zowel de regex-gebaseerde als parser-gebaseerde versies geprofileerd met 6x throttling op een M1-machine.

Zoals we hadden verwacht, bleek de op parsing gebaseerde aanpak in dat geval 27% langzamer te zijn dan de op regex gebaseerde aanpak. Het renderen van de op regex gebaseerde aanpak duurde 11 seconden en het renderen op basis van een parser duurde 15 seconden.

Gezien de overwinningen die we behalen met de nieuwe aanpak, hebben we besloten hiermee verder te gaan.

Dankbetuigingen

Onze diepste dank gaat uit naar Sofia Emelianova en Jecelyn Yeen voor hun onschatbare hulp bij het bewerken van dit bericht!

Download de voorbeeldkanalen

Overweeg om Chrome Canary , Dev of Beta te gebruiken als uw standaard ontwikkelingsbrowser. Deze preview-kanalen geven u toegang tot de nieuwste DevTools-functies, testen geavanceerde webplatform-API's en ontdekken problemen op uw site voordat uw gebruikers dat doen!

Neem contact op met het Chrome DevTools-team

Gebruik de volgende opties om de nieuwe functies en wijzigingen in het bericht te bespreken, of iets anders gerelateerd aan DevTools.

  • Stuur ons een suggestie of feedback via crbug.com .
  • Rapporteer een DevTools-probleem met behulp van de opties MeerMeer > Help > Rapporteer een DevTools-probleem in DevTools.
  • Tweet op @ChromeDevTools .
  • Laat reacties achter op onze Wat is er nieuw in DevTools YouTube-video's of DevTools Tips YouTube-video's .