CSS als Abfragesprache betrachten
(evdc.me)- Die Struktur von CSS, das mit Selektoren und Regeln eine Zielmenge auswählt und Eigenschaften anwendet, ähnelt formal Datalog, das mit Mengen und Regeln arbeitet
- Eine Kombination von Selektoren wie
div.awesomebildet eine Schnittmenge; in Datalog entsteht ein ähnlicher Join, indem dieselbe Variable mehrfach verwendet wird - Aktuelles CSS kann das Ergebnis berechneter Styles nicht erneut als Auswahlbedingung verwenden, daher lassen sich rekursive transitive Abfragen oder die wiederholte Weitergabe abgeleiteter Zustände nur schwer direkt ausdrücken
- Datalog erweitert Beziehungen mit rekursiven Regeln und Fixpunkt-Auswertung, bis keine neuen Fakten mehr entstehen; dank Monotonie lässt sich die Berechnung in einem endlichen Bereich abschließen
- Reales CSS kann mit Funktionen wie Container Queries zwar Informationen von Vorfahren lesen, wählt aber einen Ansatz, der Feedback-Schleifen und Zyklen verhindert; zugleich bleibt Spielraum, CSS-Syntax mit rekursiven Abfragen zu verbinden
Die ähnliche Struktur von CSS und Datalog
- CSS hat die Struktur aus Auswahl einer Zielmenge und Anwendung von Regeln auf die ausgewählten Ziele
- „Things“ wie HTML-Elemente existieren zunächst, und ein Selektor verweist auf eine Menge mit gemeinsamen Eigenschaften
- Mit Selektoren wie
div,#child,.awesome,[data-custom-attribute="foo"]lässt sich eine Menge beschreiben - Selektoren lassen sich wie in
div.awesomekombinieren, um eine Schnittmenge zu bilden
- CSS-Regeln verbinden Selektor und Declaration und setzen bei ausgewählten Elementen Eigenschaften wie
coloroderfont-size- Solche Eigenschaften verändern jedoch meist einen Zustand außerhalb der Sprache; ihr Ergebnis kann nicht erneut als Selektorbedingung verwendet werden
- Eine Form wie
div[color=red], die das Styling-Ergebnis erneut abfragt, akzeptiert der Browser nicht
- Datalog funktioniert ähnlich mit Faktenmengen und regelbasierter Ableitung
- Atome und Relationen wie
parent(alice, bob)bilden die Grundeinheit - Mit Variablen
X,Ylassen sich Mengen passender Einträge auswählen - Wird dieselbe Variable wiederholt, um Bedingungen zu verknüpfen, entsteht ein Join, ähnlich wie bei der Kombination von CSS-Selektoren
- Atome und Relationen wie
- Die Struktur
head(X, Y) :- body1(X, Z), body2(Z, Y)ist der Form nach ähnlich zu einer CSS-Regel, nur in umgekehrter Richtung- Der CSS-Selektor entspricht eher dem Body von Datalog, die Declaration eher dem Head
div.awesome { color: red; }entsprichtcolor(X, red) :- div(X), class(X, awesome).
Rekursive Abfragen, die CSS nicht kann
- Die Bedingung, allen fokussierten Elementen innerhalb von
data-theme="dark"einen invertierten Stil zu geben, aber bei einem zwischengeschaltetendata-theme="light"anzuhalten, erfordert eine transitive Abfrage- In realem CSS lässt sich ein Teil davon mit Regeln wie
[data-theme="dark"] :focusund[data-theme="dark"] [data-theme="light"] :focusbehandeln - Nimmt die Schachtelungstiefe zu, müssen ständig weitere Regeln ergänzt werden; eine rekursive Beziehung lässt sich schwer direkt ausdrücken
- In realem CSS lässt sich ein Teil davon mit Regeln wie
- Die benötigte Bedingung ist eine rekursive Bestimmung, ob ein Element effectively-dark ist
- Hat es selbst
data-theme="dark", ist es effectively-dark - Auch ein Kind unter einem effectively-dark-Vorfahren ist effectively-dark, sofern dazwischen kein
data-theme="light"liegt - Auf Basis dieses Zustands müsste dann ein Stil auf
.effectively-dark :focusangewendet werden
- Hat es selbst
- In einer hypothetischen CSSLog-Syntax könnten Regeln mit
class: +effectively-darkabgeleitete Zustände hinzufügen.effectively-dark > :not([data-theme="light"])würde den Zustand auf Kinder weitergeben- Die Regeln müssten rekursiv wiederholt werden, bis der Zielzustand erreicht ist
- Eine solche rekursive Weitergabe lässt sich im heutigen CSS schwer ausdrücken
- Gegen Ende des Textes werden auch einige Methoden erwähnt, die etwas Ähnliches nachahmen, aber sie sind keine allgemeine Lösung nach demselben Prinzip
Rekursion und Fixpunkt in Datalog
- Datalog arbeitet, indem aus bestehenden Fakten neue Fakten abgeleitet werden, und behandelt Rekursion als Grundprinzip
ancestor(X, Y) :- parent(X, Y).ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
- Die
ancestor-Regeln erweitern die Vorfahrenbeziehung schrittweise auf Basis der Elternbeziehung- Aus
parent(alice, bob)entsteht zunächstancestor(alice, bob) - Anschließend werden auch Pfade wie
alice -> bob -> carolundalice -> bob -> davezusätzlich abgeleitet
- Aus
- Diese Berechnung läuft auch ohne explizite
for-Schleife per Fixpunkt-Auswertung bis zum Ende- Zunächst werden nur die angegebenen Basisfakten verwendet
- Der Body aller Regeln wird auf die aktuelle Faktenmenge angewendet und der Head ergänzt
- Sobald keine neuen Fakten mehr entstehen, stoppt der Prozess
- Warum das endet, liegt an der Monotonie
- Fakten werden nur hinzugefügt, nicht entfernt; die Menge bekannter Fakten wächst also nur weiter
- Beginnt man mit einer endlichen Faktenmenge, ist auch die Zahl ableitbarer Fakten endlich begrenzt
- Könnten Fakten dagegen entfernt werden, würden frühere Schlussfolgerungen rückgängig und es könnte zu Endlosschleifen kommen
Container Queries und die Grenzen von realem CSS
- Die realen Container Queries in CSS können Regeln auf Basis des Stils von Vorfahren oder Containern anwenden
- Unterstützt wird eine Form wie
@container style(--theme: dark) { .card { background: royalblue; color: white; } }
- Unterstützt wird eine Form wie
- Das Beispiel mit transitive dark mode verlangt jedoch stärkere Bedingungen als eine einfache Vorfahrenabfrage
- Jedes Element müsste wissen, ob es selbst effectively-dark ist
- Dieser Zustand müsste transitiv weitergegeben werden, und zwar an alle Nachfahren
- An einer
data-theme="light"-Grenze müsste die Weitergabe stoppen
- Container Queries können die zweite Bedingung nicht erfüllen
- Man kann zwar Custom Properties von Vorfahren lesen, aber keinen abgeleiteten Zustand, den andere Regeln bereits berechnet haben, erneut abfragen
- Informationen, die ursprünglich im DOM vorhanden waren, sind sichtbar, aber das Ergebnis rekursiver Berechnungen kann nicht als Selektorbedingung dienen
- Ein einschlägiger Artikel von 2015 weist ebenfalls darauf hin, dass Element Queries an demselben Problem scheiterten
- Wenn man per Abfrage gesetzte Eigenschaften erneut abfragbar macht, steigt das Risiko für Schleifen und unendliche Wiederholungen
- Die CSS Working Group hat dieses Problem bislang durch eine Beschränkung der Informationsflussrichtung umgangen
- Dass Nachfahren Informationen über Vorfahren abfragen, ist erlaubt
- Feedback in die Gegenrichtung oder Zyklen mit dem eigenen Stil werden verhindert
- Dadurch bleibt die Berechnung auch ohne Fixpunkt-Semantik endlich
Die Möglichkeit, CSS-Syntax in eine rekursive Abfragesprache umzudrehen
- Statt Datalog-Semantik in CSS einzubauen, wird ein neuer Weg als realistischer vorgeschlagen: CSS-Syntax auf Datalog aufzusetzen
- Die Syntax von Datalog mit
:-, Punkten und Atomen ohne Deklaration ist für Nutzer moderner Sprachen eine hohe Einstiegshürde - CSS besitzt bereits eine ausdrucksstarke Selektorsyntax für Baumstrukturen
- Die Syntax von Datalog mit
- Es wird darauf hingewiesen, dass viele reale Daten baumförmig sind
- JSON
- AST
- Dateisysteme
- Organigramme
- XML
- In solchen Bereichen könnte die Kombination aus CSS-artiger Syntax, die Eltern-/Kind-Beziehungen implizit behandelt, und Fixpunkt-Rekursion nützlich sein
- Allgemeines Datalog ist umständlich, weil Baumstrukturen erst in relationale Darstellungen übersetzt werden müssen
- Wenn man das Gefühl von CSS-Selektoren direkt auf rekursive Abfragen überträgt, könnte das für mehr Programmierer leichter zugänglich werden
- Ein solches Werkzeug ist bislang noch nicht klar erkennbar
- Der Name „CSSLog“ ist nur vorläufig; eine Sprache mit besserem Namen könnte noch entstehen
- Es bleibt Raum, rekursive Baumabfragen mit vertrauterer Notation zu behandeln
Ergänzende Punkte und weiterführende Links
- Datalog entstand seit den 1970er Jahren im Kontext relationaler Datenbanken und der damaligen KI-Forschung und tauchte später in verschiedenen Formen immer wieder auf
- Eine einfache Form der Fixpunktberechnung wird als naive evaluation eingeführt, kann aber ineffizient sein, weil bekannte Fakten immer wieder neu berechnet werden
- Als typische Verbesserung wird semi-naive evaluation genannt, die in jedem Schritt nur neu entstandene Fakten nutzt
- Monotonie führt auch in verteilten Systemen zu nützlichen Eigenschaften
- Mit Vererbung von Custom Properties lässt sich transitive dark mode teilweise nachahmen
[data-theme="dark"] { --effective-theme: dark; }[data-theme="light"] { --effective-theme: light; }@container style(--effective-theme: dark) { :focus { outline-color: white; } }- Diese Methode funktioniert in diesem speziellen Fall meist, bietet aber keinen echten transitiven Abschluss im Allgemeinen
1 Kommentare
Hacker-News-Kommentare
CSS-Selektoren sind viel einfacher zu schreiben als XPath
Kürzlich gab es auch einen Vortrag darüber, dass die neue DOM-API in PHP HTML und CSS-Selektoren nativ sehr einfach handhabbar macht. Früher musste man CSS nach XPath umwandeln.
[1] https://speakerdeck.com/keyvan/parsing-html-with-php-8-dot-4...
Schade ist, dass sich CSS vor allem rund um Browser-Styling entwickelt hat und deshalb Funktionen wie Auswahl anhand von Textinhalten fehlen, wie man sie aus XPath kennt.
Soweit ich weiß, gab es früher Vorschläge dazu, sie kamen aber wegen möglicher Performance-Probleme im Browser-Rendering-Kontext nicht in die Spezifikation.
Beim Bauen eines Agenten zur Dokumentbearbeitung habe ich Dokumente als HTML dargestellt und das LLM nur CSS-Selektoren angeben lassen, um die nötigen Fragmente als Kontext zu holen — das funktionierte fast magisch gut.
So können Leute genau die Arbeitsweise nutzen, die sie bereits kennen.
Es wäre schön, wenn es einen Namen gäbe, der CSS-Syntax von dem gesamten System aus Regeln, Funktionen und Einheiten trennt, das die CSSWG definiert
Da steckt einiges an Potenzial drin, aber wenn man über andere Anwendungsfälle sprechen oder sie untersuchen will, scheint man am Ende doch GitHub-Code mit eingebautem CSS-Parser durchforsten zu müssen, um zu sehen, welche seltsamen Dinge Leute damit bauen.
Ich bastle auch an etwas, das fast wie eine seltsame Template-Engine wirkt: eine Mischung aus einer leichten knotenbasierten Markup-Sprache, CSS-Selektoren zur Beschreibung dessen, was in Templates hineinkommt, und einer CSS-ähnlichen Syntax zur Steuerung, wie diese Teile kombiniert werden.
https://www.w3.org/TR/selectors-3/
Die DOM-Spezifikation verweist ebenfalls darauf
https://dom.spec.whatwg.org/#selectors
Daher ist CSS-Selektor als Sammelbegriff bereits korrekt, und man kann auch einfach Selektor sagen.
DOM-Selektor könnte als Bezeichnung sauberer wirken, aber wenn man auch Selektoren berücksichtigt, die in statischem CSS oder in anderen DOM-Engines außerhalb von JS-Engines verwendet werden (XML-Parser, PHP-DOM-API usw.), könnte das eher noch verwirrender sein.
Außerdem gibt es besondere Selektoren wie
:hoveroder::target-text, die direkt an Browser-Rendering und Navigation gebunden sind.Für eine minimale Teilmenge einer Abfragesyntax, die weniger eng an Browser oder CSS gekoppelt ist, könnte ein eigener Name aber nützlich sein.
Das erinnert mich an https://github.com/braposo/graphql-css, das ich einmal auf einer Konferenz gesehen habe
Es war ein Spaßprojekt, aber ich mochte es, weil es gut zeigte, wie das Verpflanzen und Wiederverwenden von Mustern in andere Kontexte unerwartete Dinge möglich machen kann.
Genau auf diese Weise versuche ich gerade, Muster aus verschiedenen Kontexten zu übernehmen und auszuprobieren.
Meistens führt das zwar nicht besonders weit, aber vom Hacker-Spirit her ist es ziemlich spannend.
pyastgrep kann, wie unter https://pyastgrep.readthedocs.io/en/latest/ zu sehen ist, CSS-Selektoren zum Abfragen von Python-Syntax verwenden
Standardmäßig wird XPath genutzt, aber möglich ist zum Beispiel
pyastgrep --css 'Call > func > Name#main'.Das trifft fast genau die Richtung, auf die ich hinauswollte.
Ich bin mir nicht sicher, welches Szenario das eigentlich lösen soll
Schon jetzt kann man Eltern abhängig von ihren Kindern konditional verändern. Zum Beispiel hat
prestandardmäßig 16px Padding, und wenn das direkte Kindcodeist, kann man es mit&:has(> code)auf 0 setzen.Die Schlussfolgerung ist weniger „Wir sollten die Grenzen von modernem CSS reparieren“, sondern eher: Wenn man eine CSS-ähnliche Syntax auf ein Datalog-ähnliches System setzt, könnte der Umgang mit baumförmigen Daten für mehr Ingenieure vertrauter werden.
Also eher das Hinzufügen neuer Kindelemente oder Attribute im DOM.
Die heutigen LLMs sind im Umgang mit CSS eher nicht besonders gut, daher würde ich das fast gerade deshalb gern ausprobieren, um zu sehen, ob LLMs damit einfacher schlussfolgern können.
Mir fällt kein klarer praktischer Nutzen ein, aber cool ist es trotzdem.
Hm ... ist das nicht einfach JQ?
Ich mag CSS bis zu einem gewissen Grad, aber ich mag nicht, wie stark der Complexity Creep zunimmt
Ich verstehe die Logik, dass Programmiersprachen mächtiger werden als Nicht-Programmiersprachen, aber statt HTML, CSS und JavaScript immer weiter aufzublähen, fände ich es besser, wenn etwas anderes käme, das das Ganze ersetzt.
Auch die neuen Elemente in HTML5 verstehe ich größtenteils nicht und benutze sie fast nie. Am Ende denke ich ohnehin oft, dass viele Container einfach nur
div-Elemente mit einer eindeutigen ID sind, und ich hätte mir sogar so etwas wie Aliasse für diese IDs gewünscht, damithref-Navigation zu internen Links leichter wird.Dinge wie
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }brauchen bei mir viel zu lange, bis ich sie im Kopf aufgelöst habe, und fühlen sich dadurch nicht mehr elegant und einfach an.Dagegen ist
h2 { color: red; }immer noch simpel.Bei Ausdrücken wie
ancestor(X, Y) :- parent(X, Y).habe ich schon jetzt keine Lust mehr weiterzudenken. Was soll:-überhaupt sein, das sieht aus wie ein Smiley.Bei
@container style(--theme: dark) { .card { background: royalblue; color: white; } }habe ich aufgehört zu lesen.Es wirkt seltsam, dass ein Standard, der früher gut funktioniert hat, mit der Zeit immer kaputter zu werden scheint.
Zum Beispiel bedeutet
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }in englischartigem Pseudocode ungefähr: Wenn es ein X mitdata-theme="dark"gibt und dessen Kind Ydata-theme="light"hat und fokussiert ist, dann setzeoutline-colorvon Y auf black.In Datalog-Form könnte man das also als
outline-color(Y, black) if data-theme(X, "dark") and parent(X, Y) and data-theme(Y, "light") and focused(Y)schreiben.Dabei wurde
:-einfach durchifund das Komma durchandersetzt.Man könnte noch weitergehen und es als
Y.outline_color := black if X.data-theme == dark and Y.parent == X and Y.data-theme == dark and Y.focusedschreiben, sodassattr(X, val)wie UFCS-artiger Syntax-Zucker in der FormX.attr == valaussieht.Wenn es noch mehr nach ALGOL-Familie aussehen soll, wäre auch etwas wie
forall Y { Y.outline_color := black if Y.data_theme == "dark" and Y.focused and Y.parent.data_theme == "light" }möglich.Hier wird Y explizit eingeführt und ein Join implizit gemacht, sodass es mehr wie allgemeine Programmierung aussieht, aber tatsächlich würde eine Datalog-Engine solche Schleifen effizient immer dann neu ausführen, wenn sich Abhängigkeiten ändern.