Más allá de las expresiones regulares: Mejora del análisis del valor de CSS en las Herramientas para desarrolladores de Chrome

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

¿Has notado las propiedades de CSS en las Herramientas para desarrolladores de Chrome ¿La pestaña Estilos se ve un poco más prolija últimamente? Estas actualizaciones, que se lanzaron entre la versión 121 y la 128 de Chrome, son el resultado de una mejora significativa en la forma en que analizamos y presentamos los valores de CSS. En este artículo, te explicaremos los detalles técnicos de esta transformación: pasar de un sistema de coincidencia de expresiones regulares a un analizador más sólido.

Comparemos las Herramientas para desarrolladores actuales con la versión anterior:

Arriba: la versión más reciente de Chrome, Abajo: Chrome 121.

Una gran diferencia, ¿verdad? Este es un desglose de las mejoras clave:

  • color-mix Una vista previa práctica que representa visualmente los dos argumentos de color dentro de la función color-mix.
  • pink Una vista previa del color en la que se puede hacer clic para el color con nombre pink. Haz clic en él para abrir un selector de color y realizar ajustes fácilmente.
  • var(--undefined, [fallback value]) Se mejoró el manejo de variables no definidas, con la variable no definida inhabilitada y el valor de resguardo activo (en este caso, un color HSL) que se muestra con una vista previa de color en la que se puede hacer clic.
  • hsl(…): Otra vista previa de color en la que se puede hacer clic para la función de color hsl, que proporciona acceso rápido al selector de color.
  • 177deg: Es un reloj en ángulo en el que se puede hacer clic y que te permite arrastrar y modificar de forma interactiva el valor del ángulo.
  • var(--saturation, …): Es un vínculo en el que se puede hacer clic a la definición de propiedad personalizada, que facilita ir a la declaración relevante.

La diferencia es sorprendente. Para lograrlo, tuvimos que enseñarle a las Herramientas para desarrolladores a comprender los valores de propiedad de las CSS mucho mejor de lo que lo hacía antes.

Si estas vistas previas ya estaban disponibles,

Aunque estos íconos de vista previa pueden parecer familiares, no siempre se han mostrado de manera coherente, especialmente en la sintaxis de CSS compleja, como el ejemplo anterior. Incluso en los casos en los que sí funcionaban, a menudo se requería un esfuerzo significativo para que funcionaran correctamente.

Esto se debe a que el sistema de análisis de valores ha crecido de manera orgánica desde los primeros días de Herramientas para desarrolladores. Sin embargo, no ha podido estar a la altura de las increíbles funciones nuevas que obtenemos de CSS ni del aumento correspondiente en la complejidad del lenguaje. El sistema requirió un rediseño completo para mantenerse al día con la evolución, y eso es exactamente lo que hicimos.

Cómo se procesan los valores de las propiedades de CSS

En las Herramientas para desarrolladores, el proceso de renderización y decoración de declaraciones de propiedad en la pestaña Estilos se divide en dos fases distintas:

  1. Análisis estructural. En esta fase inicial, se analiza la declaración de propiedad para identificar los componentes subyacentes y las relaciones. Por ejemplo, en la declaración border: 1px solid red, reconocería 1px como una longitud, solid como una cadena y red como un color.
  2. Renderización. La fase de renderización, que se basa en el análisis estructural, transforma estos componentes en una representación HTML. De esta manera, se enriquece el texto de las propiedades que se muestran con elementos interactivos y señales visuales. Por ejemplo, el valor de color red se renderiza con un ícono de color en el que se puede hacer clic y que, al hacer clic en él, revela un selector de color que facilita la modificación.

Expresiones regulares

Antes, nos basamos en expresiones regulares (regexes) para desglosar los valores de las propiedades a fin de realizar el análisis estructural. Mantuvimos una lista de regex que coincidió con los bits de los valores de propiedad que consideramos decoradores. Por ejemplo, había expresiones que coincidían con los colores, las longitudes y los ángulos de CSS, con subexpresiones más complicadas, como las llamadas a función var, etcétera. Analizamos el texto de izquierda a derecha para realizar un análisis de valor y buscamos continuamente la primera expresión de la lista que coincida con la siguiente parte del texto.

Aunque esto funcionó la mayor parte del tiempo, la cantidad de casos en los que no siguió creciendo. A lo largo de los años, hemos recibido una buena cantidad de informes de errores en los que las coincidencias no eran correctas. A medida que las corregimos (algunas soluciones simples y otras bastante elaboradas) tuvimos que repensar nuestro enfoque para mantener bajo control nuestra deuda técnica. Veamos algunos de los problemas.

Coincidencias con color-mix()

La regex que usamos para la función color-mix() era la siguiente:

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

Esto coincide con su sintaxis:

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

Intenta ejecutar el siguiente ejemplo para visualizar las coincidencias.

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

Haz coincidir el resultado con la función de combinación de colores.

El ejemplo más simple funciona bien. Sin embargo, en el ejemplo más complejo, la coincidencia <firstColor> es hsl(177deg var(--saturation y la coincidencia <secondColor> es 100%) 50%)), lo que no tiene sentido.

Sabíamos que esto era un problema. Después de todo, CSS como lenguaje formal no es normal, por lo que ya incluimos manejo especial para tratar argumentos de funciones más complicados, como las funciones var. Sin embargo, como puedes ver en la primera captura de pantalla, eso no funciona en todos los casos.

Coincidencias con tan()

Uno de los errores informados más graciosos se relacionaba con la función trigonométrica tan() . La regex que usamos para la coincidencia de colores incluyó una subexpresión \b[a-zA-Z]+\b(?!-) para hacer coincidir los colores con nombre, como la palabra clave red. Luego, verificamos si la parte coincidente es en realidad un color con nombre y adivina qué, tan también es un color con nombre. Por lo tanto, interpretamos incorrectamente las expresiones tan() como colores.

Coincidencias con var()

Observemos otro ejemplo: funciones var() con un resguardo que contiene otras referencias de var(): var(--non-existent, var(--margin-vertical)).

Nuestra regex para var() coincidiría con este valor. Excepto, dejaría de coincidir en el primer paréntesis de cierre. Por lo tanto, el texto anterior coincide con var(--non-existent, var(--margin-vertical). Esta es una limitación de libro de texto de la coincidencia de expresiones regulares. En esencia, los idiomas que requieren el paréntesis de coincidencia no son regulares.

Transición a un analizador de CSS

Cuando el análisis de texto con expresiones regulares deja de funcionar (porque el lenguaje analizado no es regular), hay un próximo paso canónico: usar un analizador para una gramática de tipo superior. En el caso de CSS, significa un analizador para lenguajes sin contexto. De hecho, ese sistema de analizador ya existía en la base de código de Herramientas para desarrolladores: Lezer de CodeMirror, que es la base, por ejemplo, del resaltado de sintaxis en CodeMirror, el editor que se encuentra en el panel Sources. El analizador de CSS de Lezer nos permitió producir árboles de sintaxis (no abstractos) para las reglas de CSS y estaba listo para que lo usáramos. Victoria.

Un árbol de sintaxis para el valor de propiedad &quot;hsl(177deg var(--saturation, 100%) 50%)&quot;. Es una versión simplificada del resultado producido por el analizador Lezer, que no incluye nodos puramente sintácticos para las comas y los paréntesis.

Excepto que, desde el primer momento, nos pareció inviable migrar directamente de la coincidencia basada en regex a la basada en analizadores: los dos enfoques funcionan desde direcciones opuestas. Al hacer coincidir partes de valores con expresiones regulares, las Herramientas para desarrolladores analizaban la entrada de izquierda a derecha, tratando de encontrar repetidamente la primera coincidencia de una lista ordenada de patrones. Con un árbol de sintaxis, la coincidencia comenzará desde abajo hacia arriba; por ejemplo, analizando primero los argumentos de una llamada, antes de intentar hacer coincidir la llamada a función. Considéralo evaluando una expresión aritmética, en la que primero considerarías expresiones entre paréntesis, luego operadores multiplicativos y, por último, operadores aditivos. En este encuadre, la coincidencia basada en regex corresponde a la evaluación de la expresión aritmética de izquierda a derecha. Realmente no queríamos reescribir todo el sistema de coincidencias desde cero: había 15 pares de comparadores y procesadores diferentes, con miles de líneas de código, lo que hacía que fuera improbable que pudiéramos enviarlo en un solo evento importante.

Así que se nos ocurrió una solución que nos permitió realizar cambios incrementales, que describiremos a continuación con más detalle. En resumen, mantuvimos el enfoque de dos fases, pero en la primera fase tratamos de hacer coincidir las subexpresiones de abajo hacia arriba (por lo tanto, rompiendo con el flujo de regex) y, en la segunda fase, renderizamos de arriba abajo. En ambas fases, podríamos usar los comparadores y renderizados basados en regex existentes, prácticamente sin cambios, y, por lo tanto, pudimos migrarlos uno por uno.

Fase 1: Coincidencia ascendente

La primera fase de manera más o menos exacta y exclusiva hace lo que dice en la portada. Desviamos el árbol en orden de abajo hacia arriba y tratamos de hacer coincidir las subexpresiones en cada nodo del árbol de sintaxis que visitamos. Para hacer coincidir una subexpresión específica, un comparador puede usar una regex del mismo modo que lo hizo en el sistema existente. A partir de la versión 128, seguimos haciendo en algunos casos, por ejemplo, para las longitudes coincidentes. Como alternativa, un comparador puede analizar la estructura del subárbol con permisos de administrador en el nodo actual. Esto le permite detectar errores sintácticos y registrar la información estructural al mismo tiempo.

Considera el ejemplo de árbol de sintaxis anterior:

Fase 1: Coincidencia ascendente con el árbol de sintaxis.

Para este árbol, nuestros comparadores se aplicarían en el siguiente orden:

  1. hsl(177degvar(--saturation, 100%) 50%): Primero, descubrimos el primer argumento de la llamada a función hsl, el ángulo de matiz. Lo hacemos coincidir con un comparador de ángulos para poder decorar el valor de ángulo con el ícono de ángulo.
  2. hsl(177degvar(--saturation, 100%)50%): En segundo lugar, descubrimos la llamada a la función var con un comparador de variables. Para este tipo de llamadas, debemos realizar principalmente dos cosas:
    • Busca la declaración de la variable y calcula su valor. Luego, agrega un vínculo y una ventana emergente al nombre de la variable para conectarte a ellas, respectivamente.
    • Decora la llamada con un ícono de color si el valor calculado es un color. En realidad, hay una tercera cosa, pero hablaremos de eso más adelante.
  3. hsl(177deg var(--saturation, 100%) 50%): Por último, hacemos coincidir la expresión de llamada de la función hsl para que podamos decorarla con el ícono de color.

Además de buscar subexpresiones que nos gustaría decorar, hay un segundo atributo que estamos ejecutando como parte del proceso de segmentación. Ten en cuenta que en el paso 2 dijimos que buscamos el valor calculado para el nombre de una variable. De hecho, damos un paso más y propagamos los resultados en el árbol. Y no solo para la variable, sino también para el valor de resguardo. Se garantiza que, cuando se visita un nodo de función var, se visitaron sus elementos secundarios de antemano, por lo que ya conocemos los resultados de cualquier función var que pueda aparecer en el valor de resguardo. Por lo tanto, podemos sustituir de manera fácil y económica las funciones var con sus resultados sobre la marcha, lo que nos permite responder trivialmente a preguntas como "¿El resultado de esta llamada de var es un color?", como lo hicimos en el paso 2.

Fase 2: Renderización descendente

Para la segunda fase, invertimos la dirección. Tomando los resultados de la coincidencia de la fase 1, renderizamos el árbol en HTML recorriéndolo en orden de arriba abajo. Para cada nodo visitado, verificamos si coincidió y, de ser así, llamamos al procesador correspondiente del comparador. Evitamos la necesidad de un control especial para los nodos que contienen solo texto (como el "50%" de NumberLiteral) mediante la inclusión de un comparador y un renderizador predeterminados para nodos de texto. Los procesadores simplemente generan nodos HTML que, cuando se combinan, producen la representación del valor de la propiedad, incluidas sus decoraciones.

Fase 2: Renderización descendente en el árbol de sintaxis

Para el árbol de ejemplo, este es el orden en el que se renderiza el valor de la propiedad:

  1. Visita la llamada a función hsl. Coincidió, así que llama a la función de renderizador de color. Hace dos cosas:
    • Calcula el valor de color real con el mecanismo de sustitución sobre la marcha para cualquier argumento var y, luego, dibuja un ícono de color.
    • Renderiza de forma recurrente los elementos secundarios de CallExpression. Esto automáticamente se encarga de renderizar el nombre de la función, los paréntesis y las comas, que son solo texto.
  2. Visita el primer argumento de la llamada a hsl. Coincidió, así que llama al renderizador de ángulos, que dibuja el ícono de ángulo y el texto del ángulo.
  3. Visita el segundo argumento, que es la llamada var. Coincidió, así que llama al renderer de la variable, que genera lo siguiente:
    • El texto var( al comienzo.
    • El nombre de la variable y la decora con un vínculo a la definición de la variable o con un texto gris para indicar que no estaba definido. También agrega una ventana emergente a la variable para mostrar información sobre su valor.
    • La coma se renderizan de forma recursiva en el valor de resguardo.
    • Paréntesis de cierre.
  4. Visita el último argumento de la llamada a hsl. Como no hubo coincidencia, solo muestra el contenido de texto.

¿Notaste que, en este algoritmo, una renderización controla por completo la forma en que se representan los elementos secundarios de un nodo coincidente? La renderización recurrente de los elementos secundarios es proactiva. Este truco es lo que habilitó una migración paso a paso de la renderización basada en regex a la renderización basada en árboles de sintaxis. En el caso de los nodos que coinciden con un comparador de regex heredado, se podría usar el renderizador correspondiente en su forma original. En términos del árbol de sintaxis, tendría la responsabilidad de renderizar todo el subárbol, y su resultado (un nodo HTML) podría conectarse de manera clara al proceso de renderización que lo rodea. Esto nos dio la opción de realizar la portabilidad de los comparadores y renderizadores en pares, y de intercambiarlos uno por uno.

Otra gran función de los procesadores que controlan la renderización de los elementos secundarios de sus nodos coincidentes es que nos da la capacidad de razonar sobre las dependencias entre los íconos que estamos agregando. En el ejemplo anterior, el color producido por la función hsl depende, obviamente, del valor de matiz. Esto significa que el color que muestra el ícono de color depende del ángulo que muestre el ícono de ángulo. Si el usuario abre el editor de ángulos a través de ese ícono y modifica el ángulo, ahora podemos actualizar el color del ícono de color en tiempo real:

Como puedes ver en el ejemplo anterior, también usamos este mecanismo para otras vinculaciones de íconos, como color-mix() y sus dos canales de color, o funciones var que muestran un color desde su resguardo.

Impacto en el rendimiento

Cuando nos adentramos en este problema para mejorar la confiabilidad y solucionar problemas de larga data, esperábamos una regresión de rendimiento, teniendo en cuenta que empezamos a ejecutar un analizador completo. Para probarlo, creamos una comparativa que renderiza alrededor de 3,500 declaraciones de propiedad y generamos perfiles de las versiones basadas en regex y en analizadores con una limitación 6x en una máquina M1.

Como esperábamos, el enfoque basado en análisis resultó ser un 27% más lento que el enfoque basado en regex para ese caso. El enfoque basado en regex tardó 11 segundos en renderizarse, y el enfoque basado en analizadores tardó 15 segundos en renderizarse.

Teniendo en cuenta los beneficios que obtenemos del nuevo enfoque, decidimos avanzar con él.

Agradecimientos

Queremos agradecer más profundamente a Sofia Emelianova y Jecelyn Yeen por su invaluable ayuda durante la edición de esta publicación.

Descarga los canales de vista previa

Considera usar Chrome Canary, Dev o Beta como navegadores de desarrollo predeterminados. Estos canales de vista previa te brindan acceso a las funciones más recientes de Herramientas para desarrolladores, prueban API de plataforma web de vanguardia y detectan problemas en tu sitio antes que los usuarios.

Comunicarse con el equipo de Herramientas para desarrolladores de Chrome

Usa las siguientes opciones para hablar sobre las nuevas funciones y los cambios en la publicación, o cualquier otra cosa relacionada con Herramientas para desarrolladores.

  • Para enviarnos sugerencias o comentarios, accede a crbug.com.
  • Informa un problema en Herramientas para desarrolladores con Más opciones   Más > Ayuda > Informa problemas de Herramientas para desarrolladores en Herramientas para desarrolladores.
  • Twittea a @ChromeDevTools.
  • Deja comentarios en nuestros videos de YouTube de Herramientas para desarrolladores o en videos de YouTube de las Sugerencias de las Herramientas para desarrolladores.