Voir CSS comme un langage de requête
(evdc.me)- La structure de CSS, qui sélectionne un ensemble cible avec des sélecteurs et des règles puis lui applique des propriétés, ressemble formellement à Datalog, qui fonctionne avec des ensembles et des règles
- La combinaison de sélecteurs comme
div.awesomecrée une intersection et, en Datalog, un join similaire se produit en répétant la même variable - Le CSS actuel ne peut pas réutiliser le résultat d’un style calculé comme condition de sélection, ce qui rend difficile l’expression directe de requêtes transitives récursives ou de la propagation répétée d’états dérivés
- Datalog étend les relations à l’aide de règles récursives et d’une évaluation au point fixe jusqu’à ce qu’aucun nouveau fait n’apparaisse, et grâce à la monotonie, le calcul peut se terminer sur un domaine fini
- Le CSS réel peut lire des informations sur les ancêtres avec des fonctionnalités comme les Container Queries, mais il a choisi d’éviter les boucles de rétroaction et les cycles, tout en laissant ouverte la possibilité d’appliquer la syntaxe CSS à des requêtes récursives
La ressemblance structurelle entre CSS et Datalog
- CSS a une structure fondée sur la sélection d’un ensemble cible et l’application de règles aux éléments sélectionnés
- Des « Things » comme les éléments HTML existent d’abord, puis un selector désigne un ensemble partageant des propriétés communes
- On peut décrire des ensembles avec des selector comme
div,#child,.awesome,[data-custom-attribute="foo"] - On peut combiner des selector, comme dans
div.awesome, pour former une intersection
- Une règle CSS associe un selector et des declarations pour définir sur les éléments sélectionnés des propriétés comme
coloroufont-size- Mais ces propriétés modifient généralement un état extérieur au langage et leur résultat ne peut pas ensuite servir de condition de selector
- Une forme comme
div[color=red], qui requerrait à nouveau le résultat du style, n’est pas acceptée par le navigateur
- Datalog fonctionne de manière similaire avec des ensembles de faits et une déduction fondée sur des règles
- Des atomes et relations comme
parent(alice, bob)en sont l’unité de base - On peut utiliser des variables
X,Ypour sélectionner des ensembles d’éléments correspondant à des conditions - Répéter la même variable pour relier des conditions produit un join proche de la combinaison de selector en CSS
- Des atomes et relations comme
- La forme
head(X, Y) :- body1(X, Z), body2(Z, Y)ressemble à une règle CSS, avec simplement la direction inversée- Le selector en CSS est proche du body en Datalog, et la declaration est proche du head
div.awesome { color: red; }correspond àcolor(X, red) :- div(X), class(X, awesome).
Les requêtes récursives que CSS ne sait pas faire
- La condition consistant à appliquer un style inversé à tous les éléments focalisés à l’intérieur de
data-theme="dark", tout en s’arrêtant sidata-theme="light"apparaît au milieu, nécessite une requête transitive- En CSS réel, on ne peut traiter qu’une partie du problème avec des règles comme
[data-theme="dark"] :focuset[data-theme="dark"] [data-theme="light"] :focus - Quand le niveau d’imbrication augmente, il faut continuer à ajouter des règles, et il devient difficile d’exprimer directement la relation récursive
- En CSS réel, on ne peut traiter qu’une partie du problème avec des règles comme
- La condition nécessaire consiste à déterminer récursivement si un élément est effectively-dark
- S’il a lui-même
data-theme="dark", il devient effectively-dark - Un enfant sous un ancêtre effectively-dark devient lui aussi effectively-dark s’il n’y a pas de
data-theme="light"entre les deux - C’est sur la base de cet état qu’il faut appliquer un style à
.effectively-dark :focus
- S’il a lui-même
- Dans une syntaxe hypothétique CSSLog, une règle pourrait ajouter un état dérivé comme
class: +effectively-dark.effectively-dark > :not([data-theme="light"])propage l’état aux enfants- Les règles devraient être répétées récursivement jusqu’à atteindre l’état cible
- Ce type de propagation récursive est difficile à exprimer avec le CSS actuel
- La fin de l’article montre quelques méthodes pour l’imiter partiellement, mais ce n’est pas une solution générale fondée sur le même principe
La récursion et le point fixe en Datalog
- Datalog fonctionne en déduisant de nouveaux faits à partir des faits existants, et traite nativement la récursion
ancestor(X, Y) :- parent(X, Y).ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
- La règle
ancestorétend progressivement la relation d’ascendance à partir de la relation parent- À partir de
parent(alice, bob), on obtient d’abordancestor(alice, bob) - Puis on déduit aussi des chemins comme
alice -> bob -> caroletalice -> bob -> dave
- À partir de
- Ce calcul va jusqu’au bout grâce à une évaluation au point fixe, sans boucle
forexplicite- Au départ, on n’utilise que les faits de base explicitement déclarés
- On applique le body de toutes les règles à l’ensemble courant de faits pour ajouter le head
- On s’arrête quand aucun nouveau fait n’apparaît
- Si cette approche se termine, c’est grâce à la monotonie
- On n’ajoute que des faits, on n’en retire pas, donc l’ensemble des faits connus ne peut que croître
- Si l’on part d’un ensemble fini de faits, le nombre de faits déductibles reste lui aussi borné
- À l’inverse, si l’on pouvait retirer des faits, des conclusions précédentes pourraient être invalidées et provoquer une boucle infinie
Container Queries et les limites du CSS réel
- Les Container Queries du CSS réel permettent d’appliquer des règles en fonction du style d’un ancêtre ou d’un conteneur
- Une forme comme
@container style(--theme: dark) { .card { background: royalblue; color: white; } }est prise en charge
- Une forme comme
- Mais l’exemple du mode sombre transitif exige des conditions plus fortes qu’une simple lecture d’ancêtre
- Chaque élément doit savoir s’il est lui-même effectively-dark
- Cet état doit être propagé transitivement à l’ensemble de ses descendants
- La propagation doit s’arrêter à la frontière
data-theme="light"
- Les Container Queries ne peuvent pas traiter la deuxième condition
- On peut lire une custom property héritée d’un ancêtre, mais on ne peut pas requerir à nouveau un état dérivé déjà calculé par une autre règle
- On peut voir les informations présentes à l’origine dans le DOM, mais pas utiliser le résultat d’un calcul récursif comme condition de selector
- Un article lié de 2015 notait déjà que les element queries se heurtaient au même problème
- Si une propriété définie par une requête peut elle-même être à nouveau requêtée, le risque de boucles et de répétitions infinies augmente
- Le CSS Working Group a évité ce problème en limitant la direction du flux d’information
- Il autorise un descendant à requêter des informations sur ses ancêtres
- Il bloque la rétroaction dans le sens inverse ou les cycles impliquant le style de l’élément lui-même
- Ainsi, le calcul reste fini sans avoir besoin d’une sémantique de point fixe
La possibilité d’inverser la syntaxe CSS en langage de requête récursif
- Plutôt que d’injecter la sémantique de Datalog dans CSS, une piste plus réaliste serait de poser la syntaxe CSS au-dessus de Datalog
- La syntaxe de Datalog, avec
:-, les points et les atomes sans déclaration, constitue une barrière d’entrée importante pour les utilisateurs de langages modernes - CSS dispose déjà d’une syntaxe de selector riche pour manipuler des structures arborescentes
- La syntaxe de Datalog, avec
- De nombreuses données réelles ont justement une forme arborescente
- JSON
- AST
- système de fichiers
- organigramme
- XML
- Dans ces domaines, combiner une syntaxe de type CSS qui traite implicitement les relations parent/enfant avec une récursion au point fixe pourrait être utile
- En Datalog classique, il faut réécrire les structures arborescentes sous forme relationnelle, ce qui est assez lourd
- Si l’on réutilise directement l’intuition des selector CSS pour des requêtes récursives, davantage de programmeurs pourraient s’en emparer facilement
- Ce type d’outil n’existe pas encore vraiment de manière nette
- Le nom « CSSLog » n’est que provisoire, et un langage mieux nommé pourrait apparaître
- Il reste de la place pour traiter les requêtes récursives sur des arbres avec une notation plus familière
Points complémentaires et liens de référence
- Datalog est apparu dans les années 1970 dans le contexte des bases de données relationnelles et de la recherche en IA de l’époque, puis a réémergé à plusieurs reprises sous différentes formes
- Une forme simple de calcul au point fixe est présentée sous le nom de naive evaluation, mais elle peut être inefficace car elle recalcule sans cesse les faits déjà connus
- Une amélioration classique mentionnée ici est la semi-naive evaluation, qui n’utilise à chaque étape que les faits nouvellement produits
- La monotonie est aussi une propriété utile dans les systèmes distribués
- Il existe aussi une manière d’imiter partiellement le mode sombre transitif via l’héritage des custom properties
[data-theme="dark"] { --effective-theme: dark; }[data-theme="light"] { --effective-theme: light; }@container style(--effective-theme: dark) { :focus { outline-color: white; } }- Cette méthode fonctionne globalement pour ce cas précis, mais ne fournit pas en général une véritable fermeture transitive
1 commentaires
Commentaires sur Hacker News
Les sélecteurs CSS sont bien plus faciles à écrire que XPath
Il y a eu récemment une présentation expliquant que la nouvelle API DOM de PHP permet de manipuler HTML et les sélecteurs CSS de manière native et très simplement. Avant, il fallait convertir le CSS en XPath
[1] https://speakerdeck.com/keyvan/parsing-html-with-php-8-dot-4...
C’est dommage que, comme tout cela s’est développé autour du stylage dans le navigateur, il manque des fonctionnalités comme la sélection basée sur le contenu textuel, présente dans XPath
Je crois qu’il y avait eu une proposition autrefois, mais qu’elle n’est pas entrée dans la spec à cause de problèmes de performances dans le contexte du rendu navigateur
En construisant un agent d’édition de documents, j’affichais le document en HTML et je laissais le LLM indiquer uniquement un sélecteur CSS pour récupérer les fragments nécessaires dans le contexte, et ça marchait presque comme par magie
Les gens peuvent utiliser une approche qui leur est déjà familière
J’aimerais qu’il existe un nom distinct pour séparer la syntaxe CSS de tout l’ensemble des règles, fonctions et unités définies par le CSSWG
Il y a pas mal de potentiel ici, mais pour parler d’autres cas d’usage ou enquêter dessus, on dirait qu’il faut au final fouiller du code sur GitHub intégrant des parseurs CSS pour voir quelles choses étranges les gens fabriquent
Je bricole aussi une sorte d’étrange moteur de templates, mélangeant un langage de balisage léger basé sur des nœuds, des sélecteurs CSS pour exprimer ce qui doit entrer dans les templates, et une syntaxe proche de CSS pour contrôler comment assembler ces morceaux
https://www.w3.org/TR/selectors-3/
La spec DOM y fait aussi référence
https://dom.spec.whatwg.org/#selectors
Donc l’appellation générique sélecteur CSS est déjà correcte, et on peut aussi simplement dire sélecteur
Le nom sélecteur DOM serait peut-être plus net, mais si on pense aussi aux sélecteurs utilisés dans le CSS statique ou dans d’autres moteurs DOM hors moteur JS (parseur XML, API DOM PHP, etc.), cela risque au contraire d’être plus confus
Il existe aussi des sélecteurs spéciaux directement liés au rendu ou à la navigation dans le navigateur, comme
:hoverou::target-textCela dit, un nom séparé pourrait être utile pour un sous-ensemble minimal de syntaxe de requête moins couplé au navigateur ou à CSS
Ça me rappelle https://github.com/braposo/graphql-css que j’avais vu à une conférence il y a quelque temps
C’était un projet pour rire, mais je l’avais bien aimé parce qu’il montrait bien comment le fait de transplanter un pattern dans un autre contexte et de le réutiliser peut rendre possibles des choses inattendues
C’est justement ce genre de réutilisation de patterns entre contextes différents que j’essaie d’explorer
Même si la plupart de ces idées ne vont pas très loin, ça reste assez intéressant dans un esprit hacker
pyastgrep, comme on peut le voir sur https://pyastgrep.readthedocs.io/en/latest/, permet d’utiliser des sélecteurs CSS pour interroger la syntaxe Python
XPath est la valeur par défaut, mais on peut par exemple faire
pyastgrep --css 'Call > func > Name#main'C’est presque exactement la direction que je voulais désigner
Je ne vois pas très bien quel scénario cela résout
Aujourd’hui déjà, on peut modifier conditionnellement un parent selon ses enfants. Par exemple,
prea un padding par défaut de 16px et, s’il acodecomme enfant direct, on peut le mettre à 0 avec&:has(> code)La conclusion est moins « il faut corriger les limites du CSS moderne » que « si on appliquait une syntaxe proche de CSS à un système proche de Datalog, on pourrait peut-être rendre le traitement de données arborescentes plus familier pour davantage d’ingénieurs »
Autrement dit, il s’agit d’ajouter de nouveaux éléments enfants ou attributs au DOM
Les LLM actuels ont plutôt du mal avec CSS, donc ça me donne au contraire envie d’essayer ça pour voir si cela permettrait aux LLM de raisonner plus simplement
Je ne vois pas très bien l’utilité concrète, mais c’est quand même cool
Hum... j’ai l’impression que ce n’est juste que JQ
J’aime assez CSS jusqu’à un certain point, mais je déteste la dérive de complexité qui ne cesse de s’aggraver
Je comprends l’argument selon lequel les langages de programmation deviennent plus puissants que les non-langages de programmation, mais au lieu de continuer à complexifier HTML, CSS et JavaScript, j’ai plutôt l’impression qu’il vaudrait mieux voir apparaître autre chose pour remplacer l’ensemble
Je n’utilise presque jamais non plus les nouveaux éléments de HTML5, parce que je ne vois pas vraiment pourquoi la plupart sont nécessaires. J’en suis venu à penser qu’au fond beaucoup de conteneurs ne sont que des
divavec un ID unique, et j’aurais même aimé qu’il existe une sorte d’alias pour ces ID afin de naviguer avechrefdans les liens internesDes trucs comme
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }me prennent bien trop de temps à interpréter mentalement, au point que ça ne me paraît plus élégant ni simpleEn revanche,
h2 { color: red; }reste simpleUne expression comme
ancestor(X, Y) :- parent(X, Y).me donne déjà envie de ne plus y penser. C’est quoi,:-? On dirait un visage qui souritJe me suis arrêté de lire à
@container style(--theme: dark) { .card { background: royalblue; color: white; } }C’est étrange de voir un standard qui fonctionnait bien autrefois sembler se dégrader avec le temps
Par exemple, si on développe
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }en pseudo-code de type anglais, cela revient à peu près à dire : s’il existe un X avecdata-theme="dark"et que son enfant Y adata-theme="light"et est dans l’état focus, alors mettreoutline-colorde Y à blackOn peut donc l’écrire à la manière de Datalog comme
outline-color(Y, black) if data-theme(X, "dark") and parent(X, Y) and data-theme(Y, "light") and focused(Y)En gros, on remplace
:-parifet les virgules parandOn peut aller plus loin et écrire quelque chose comme
Y.outline_color := black if X.data-theme == dark and Y.parent == X and Y.data-theme == dark and Y.focused, pour faire apparaîtreattr(X, val)comme une sorte de sucre syntaxique proche de l’UFCS tel queX.attr == valSi on veut quelque chose qui ressemble davantage à la famille ALGOL, on pourrait aussi écrire
forall Y { Y.outline_color := black if Y.data_theme == "dark" and Y.focused and Y.parent.data_theme == "light" }Ici, Y est introduit explicitement et une jointure est implicite, ce qui donne une apparence plus proche de la programmation générale, mais en réalité c’est toujours le moteur Datalog qui exécute efficacement ce type de boucle à chaque changement de dépendance