Além das expressões regulares: melhoria da análise de valores CSS no Chrome DevTools

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Você notou as propriedades do CSS no Chrome DevTools A guia Estilos está um pouco mais sofisticada ultimamente? Essas atualizações, lançadas entre o Chrome 121 e 128, resultaram de uma melhoria significativa na forma como analisamos e apresentamos os valores CSS. Neste artigo, abordaremos os detalhes técnicos dessa transformação, passando de um sistema de correspondência de expressões regulares para um analisador mais robusto.

Vamos comparar o DevTools atual com a versão anterior:

Em cima: é a versão mais recente do Chrome. Embaixo: o Chrome 121.

Completamente diferença, certo? Estas são as principais melhorias:

  • color-mix: Uma prévia útil que representa visualmente os dois argumentos de cor na função color-mix.
  • pink: Uma visualização de cor clicável para a cor nomeada pink. Clique nele para abrir um seletor de cores e fazer ajustes fáceis.
  • var(--undefined, [fallback value]): Processamento aprimorado de variáveis indefinidas, com a variável indefinida esmaecida e o valor substituto ativo (nesse caso, uma cor HSL) exibidos com uma visualização de cor clicável.
  • hsl(…): outra visualização de cor clicável para a função de cor hsl, que fornece acesso rápido ao seletor de cores.
  • 177deg: um relógio angular clicável que permite arrastar e modificar o valor do ângulo de maneira interativa.
  • var(--saturation, …): um link clicável para a definição de propriedade personalizada que facilita o acesso à declaração relevante.

A diferença é impressionante. Para isso, tivemos que ensinar o DevTools a entender os valores de propriedade CSS muito melhor do que antes.

Essas visualizações já não estavam disponíveis?

Embora esses ícones de visualização possam parecer familiares, eles nem sempre são exibidos de forma consistente, especialmente em sintaxe CSS complexa, como o exemplo acima. Mesmo nos casos em que funcionavam, muitas vezes era necessário um esforço significativo para que funcionassem corretamente.

O motivo é que o sistema de análise de valores tem crescido organicamente desde os primeiros dias do DevTools. No entanto, ele não conseguiu acompanhar os novos e incríveis recursos que estamos obtendo do CSS e o aumento correspondente na complexidade da linguagem. O sistema exigiu uma reformulação completa para acompanhar a evolução e foi exatamente isso que fizemos!

Como os valores da propriedade CSS são processados

No DevTools, o processo de renderização e decoração de declarações de propriedade na guia Estilos é dividido em duas fases distintas:

  1. Análise estrutural. Essa fase inicial analisa a declaração de propriedade para identificar os componentes subjacentes e as relações entre eles. Por exemplo, na declaração border: 1px solid red, ele reconhece 1px como um comprimento, solid como uma string e red como uma cor.
  2. Renderização. Com base na análise estrutural, a fase de renderização transforma esses componentes em uma representação HTML. Isso enriquece o texto da propriedade exibida com elementos interativos e dicas visuais. Por exemplo, o valor de cor red é renderizado com um ícone de cor clicável que, quando clicado, revela um seletor de cores para fácil modificação.

Expressões regulares

Antes, usávamos expressões regulares (regexes) para dissecar os valores das propriedades para análise estrutural. Mantivemos uma lista de regex para corresponder aos bits de valores de propriedade que consideramos decorativos. Por exemplo, havia expressões que correspondiam a cores, comprimentos, ângulos e subexpressões mais complicadas do CSS, como chamadas de função var e assim por diante. Analisamos o texto da esquerda para a direita para fazer uma análise de valor, continuamente procurando a primeira expressão na lista que corresponde à próxima parte do texto.

Embora isso tenha funcionado bem na maioria das vezes, o número de casos em que não continuou crescendo. Ao longo dos anos, recebemos diversos relatórios de bugs em que as informações correspondentes não estavam corretas. À medida que os corrigimos (algumas simples e outras muito elaborados), tivemos que repensar nossa abordagem para evitar dívidas técnicas. Vamos analisar alguns dos problemas.

color-mix() correspondente

A regex que usamos para a função color-mix() era a seguinte:

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

Que corresponde à sintaxe:

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

Tente executar o exemplo a seguir para visualizar as correspondências.

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

Resultado da correspondência para a função de mistura de cores.

O exemplo mais simples funciona bem. No entanto, no exemplo mais complexo, a correspondência de <firstColor> é hsl(177deg var(--saturation e <secondColor> é 100%) 50%)), o que não faz sentido.

Sabíamos que isso era um problema. Afinal, o CSS como linguagem formal não é regular, então já incluímos o tratamento especial para lidar com argumentos de função mais complicados, como funções var. No entanto, como você pode notar na primeira captura de tela, isso ainda não funcionou em todos os casos.

tan() correspondente

Um dos bugs informados mais engraçados era a função trigonométrica tan() . O regex que estávamos usando para corresponder cores incluía uma subexpressão \b[a-zA-Z]+\b(?!-) para corresponder cores nomeadas, como a palavra-chave red. Depois, verificamos se a parte correspondente é realmente uma cor com nome e adivinhe: tan também é uma cor com nome. Portanto, interpretamos incorretamente as expressões tan() como cores.

var() correspondente

Confira outro exemplo, as funções var() com um substituto que contém outras referências do var(): var(--non-existent, var(--margin-vertical)).

Nosso regex para var() corresponderia a esse valor. No entanto, a correspondência deixa de fazer a correspondência no primeiro parêntese de fechamento. Portanto, a correspondência do texto acima é feita como var(--non-existent, var(--margin-vertical). Essa é uma limitação didática da correspondência de expressões regulares. Os idiomas que exigem correspondência de parênteses, fundamentalmente, não são regulares.

Transição para um analisador de CSS

Quando a análise de texto com expressões regulares deixa de funcionar (porque a linguagem analisada não é regular), há uma próxima etapa canônica: usar um analisador para uma gramática de tipo superior. Para CSS, isso significa um analisador para linguagens livres de contexto. Na verdade, esse sistema de analisadores já existia na base de código do DevTools: o Lezer do CodeMirror, que é a base, por exemplo, do destaque de sintaxe no CodeMirror, o editor que você encontra no painel Sources. O analisador CSS da Lezer permitiu a produção de árvores de sintaxe (não abstratas) para regras de CSS e estava pronto para uso. Vitória.

Uma árvore de sintaxe para o valor da propriedade `hsl(177deg var(--). Synature, 100%) 50%`. É uma versão simplificada do resultado produzido pelo analisador Lezer, excluindo nós puramente sintáticos para vírgulas e parênteses.

A diferença é que, desde o início, consideramos inviável migrar diretamente da correspondência baseada em regex para a correspondência baseada em analisador. As duas abordagens funcionam com direções opostas. Ao combinar partes de valores com expressões regulares, o DevTools verifica a entrada da esquerda para a direita, tentando repetidamente encontrar a correspondência mais antiga em uma lista ordenada de padrões. Em uma árvore de sintaxe, a correspondência começa de baixo para cima, por exemplo, analisando primeiro os argumentos de uma chamada antes de tentar fazer a correspondência com a chamada de função. Pense nisso como avaliar uma expressão aritmética, em que primeiro você consideraria expressões entre parênteses, depois operadores multiplicativos e, por fim, operadores aditivos. Nesse enquadramento, a correspondência com base em regex corresponde à avaliação da expressão aritmética da esquerda para a direita. Realmente não queríamos reescrever todo o sistema de correspondência do zero. Havia 15 pares de matchers e renderizador diferentes, com milhares de linhas de código para eles, o que tornava improvável que pudéssemos enviá-lo em um único marco.

Assim, encontramos uma solução que nos permitiu fazer alterações incrementais, que vamos descrever com mais detalhes abaixo. Em resumo, mantivemos a abordagem de duas fases, mas na primeira fase tentamos fazer a correspondência de subexpressões de baixo para cima, o que interrompe o fluxo de regex. Na segunda fase, a renderização de cima para baixo é feita. Em ambas as fases, foi possível usar os correspondentes e renderizações com base em regex, praticamente inalterados, e migrá-los um a um.

Fase 1: correspondência ascendente

A primeira fase faz, de forma mais ou menos exata e exclusiva, o que diz na capa. Percorremos a árvore em ordem, de baixo para cima, e tentamos combinar subexpressões em cada nó da árvore de sintaxe que visitamos. Para corresponder a uma subexpressão específica, um matcher pode usar regex como fez no sistema atual. A partir da versão 128, ainda fazemos isso em alguns casos, por exemplo, para comprimentos correspondentes. Como alternativa, um matcher pode analisar a estrutura da subárvore com raízes no nó atual. Isso permite que ele capture erros de sintaxe e registre informações estruturais ao mesmo tempo.

Considere o exemplo de árvore de sintaxe acima:

Fase 1: correspondência ascendente na árvore de sintaxe.

Para essa árvore, nossos correspondentes se aplicariam na seguinte ordem:

  1. hsl(177degvar(--saturation, 100%) 50%): primeiro, descobrimos o primeiro argumento da chamada de função hsl, o ângulo de matiz. Combinamos com um matcher de ângulo para poder decorar o valor do ângulo com o ícone correspondente.
  2. hsl(177degvar(--saturation, 100%)50%): segundo, descobrimos a chamada de função var com um matcher var. Para essas chamadas, queremos fazer duas coisas:
    • Procure a declaração da variável e calcule seu valor. Em seguida, adicione um link e um pop-over ao nome da variável para se conectar a eles, respectivamente.
    • Decorar a chamada com um ícone de cor se o valor calculado for uma cor Há um terceiro elemento, mas falaremos sobre ele mais tarde.
  3. hsl(177deg var(--saturation, 100%) 50%): por fim, combinamos a expressão de chamada da função hsl para que ela possa ser decorada com o ícone de cor.

Além de pesquisar subexpressões que gostaríamos de decorar, há um segundo recurso que estamos executando como parte do processo de correspondência. Observe que, na etapa 2, dissemos que pesquisamos o valor calculado de um nome de variável. Na verdade, levamos esse passo adiante e propagamos os resultados até a árvore. E não apenas para a variável, mas também para o valor substituto. É garantido que, ao visitar um nó da função var, os filhos tenham sido visitados com antecedência. Portanto, já sabemos os resultados de qualquer função var que possa aparecer no valor substituto. Portanto, podemos substituir as funções var de maneira fácil e econômica pelos resultados, o que nos permite responder a perguntas triviais como "O resultado dessa chamada de var é uma cor?", como fizemos na etapa 2.

Fase 2: renderização de cima para baixo

Na segunda fase, invertemos a direção. Usando os resultados de correspondência da fase 1, processamos a árvore em HTML passando-a em ordem, de cima para baixo. Para cada nó visitado, verificamos se ele corresponde e, em caso afirmativo, chamamos o renderizador correspondente do correspondente. Evitamos a necessidade de tratamento especial para nós que contêm apenas texto (como o NumberLiteral "50%") incluindo um matcher e renderizador padrão para nós de texto. Os renderizadores simplesmente geram nós HTML que, quando reunidos, produzem a representação do valor da propriedade, incluindo as decorações.

Fase 2: renderização de cima para baixo na árvore de sintaxe.

Para o exemplo de árvore, esta é a ordem em que o valor da propriedade é renderizado:

  1. Acesse a chamada de função hsl. Ela correspondeu, então chame o renderizador da função de cor. Ele faz duas coisas:
    • Calcula o valor real da cor usando o mecanismo de substituição dinâmico para qualquer argumento var e desenha um ícone de cor.
    • Renderiza de maneira recursiva os filhos da CallExpression. Ela cuida automaticamente da renderização do nome da função, parênteses e vírgulas, que são apenas texto.
  2. Acesse o primeiro argumento da chamada hsl. Ele correspondeu, então chame o renderizador de ângulo, que desenha o ícone e o texto dele.
  3. Acesse o segundo argumento, que é a chamada var. Ele correspondeu, então chame a var renderer, que gera o seguinte:
    • O texto var( no início.
    • O nome da variável e a decora com um link para a definição da variável ou com uma cor de texto cinza para indicar que ela estava indefinida. Ele também adiciona um pop-over à variável para mostrar informações sobre seu valor.
    • A vírgula e depois renderiza o valor de substituição de forma recursiva.
    • Um parêntese de fechamento.
  4. Acesse o último argumento da chamada hsl. Como não há correspondência, exiba apenas o conteúdo de texto.

Você notou que, nesse algoritmo, uma renderização controla totalmente como os filhos de um nó correspondente são renderizados? A renderização recursiva dos filhos é proativa. Foi esse truque que permitiu a migração em etapas da renderização baseada em regex para a renderização baseada em árvore de sintaxe. Para nós que correspondem a uma correspondência de regex legado, o renderizador correspondente poderia ser usado na forma original. Em termos de árvore de sintaxe, ele seria responsável por renderizar toda a subárvore e seu resultado (um nó HTML) poderia ser conectado de forma limpa ao processo de renderização circundante. Isso nos deu a opção de transferir matchers e renderizadores em pares e trocá-los um por um.

Outro recurso interessante dos renderizadores que controlam a renderização dos filhos do nó correspondente é a capacidade de determinar as dependências entre os ícones que estamos adicionando. No exemplo acima, a cor produzida pela função hsl obviamente depende do valor de matiz. Isso significa que a cor mostrada pelo ícone de cor depende do ângulo mostrado pelo ícone de ângulo. Se o usuário abrir o editor de ângulo por meio desse ícone e modificar o ângulo, agora poderemos atualizar a cor do ícone de cor em tempo real:

Como mostrado no exemplo acima, também usamos esse mecanismo para outros pares de ícones, como para color-mix() e seus dois canais de cores ou para funções var que retornam uma cor do substituto.

Impacto no desempenho

Ao investigar esse problema para melhorar a confiabilidade e corrigir problemas antigos, esperávamos uma regressão de desempenho, considerando que começamos a executar um analisador completo. Para testar isso, criamos uma comparação que renderiza cerca de 3,5 mil declarações de propriedade e criamos um perfil das versões baseadas em regex e em análise com limitação de seis vezes em uma máquina M1.

Como esperávamos, a abordagem com base em análise acabou sendo 27% mais lenta do que a baseada em regex para esse caso. A abordagem baseada em regex levou 11 segundos para renderizar, e a baseada em analisadores levou 15 segundos.

Considerando as vitórias que obtemos com a nova abordagem, decidimos continuar com ela.

Agradecimentos

Agradecemos muito Sofia Emelianova e Jecelyn Yeen pela ajuda inestimável para editar esta postagem.

Fazer o download dos canais de visualização

Use o Chrome Canary, Dev ou Beta como seu navegador de desenvolvimento padrão. Esses canais de pré-lançamento dão acesso aos recursos mais recentes do DevTools, testam APIs modernas da plataforma Web e encontram problemas no seu site antes que os usuários o façam!

Entrar em contato com a equipe do Chrome DevTools

Use as opções a seguir para discutir os novos recursos e mudanças na postagem ou qualquer outro assunto relacionado ao DevTools.

  • Envie uma sugestão ou feedback pelo site crbug.com.
  • Informe um problema do DevTools usando Mais opções   Mais > Ajuda > Relate problemas no DevTools no DevTools.
  • Envie um tweet em @ChromeDevTools.
  • Deixe comentários nos vídeos do YouTube sobre as novidades do DevTools ou nos vídeos do YouTube com dicas sobre o DevTools.