Vendo CSS como uma linguagem de consulta
(evdc.me)- A estrutura do CSS, que seleciona um conjunto-alvo com seletores e regras e aplica propriedades, se parece em forma com o Datalog, que opera com conjuntos e regras
- A combinação de seletores como
div.awesomecria uma interseção, e no Datalog ocorre um join parecido ao repetir a mesma variável - O CSS atual não consegue reutilizar o resultado do estilo calculado como condição de seleção, então é difícil expressar diretamente consultas transitivas recursivas ou a propagação iterativa de estados derivados
- O Datalog expande relações com regras recursivas e avaliação por ponto fixo até que não surjam mais fatos novos e, graças à monotonicidade, consegue terminar o cálculo em um escopo finito
- O CSS real consegue ler informações de ancestrais com recursos como Container Queries, mas escolheu evitar loops de feedback e ciclos, embora ainda exista espaço para incorporar consultas recursivas à sintaxe do CSS
Estruturas semelhantes entre CSS e Datalog
- O CSS tem a estrutura de seleção de conjunto-alvo e aplicação de regras ao alvo selecionado
- Primeiro existem “coisas” como elementos HTML, e o selector aponta para um conjunto com propriedades em comum
- É possível descrever conjuntos com selectors como
div,#child,.awesome,[data-custom-attribute="foo"] - É possível combinar selectors como
div.awesomepara criar uma interseção
- As regras CSS unem selector e declaration para definir propriedades como
coloroufont-sizenos elementos selecionados- Porém, essas propriedades em geral alteram um estado fora da linguagem, e o resultado não pode voltar a ser usado como condição em selectors
- Uma forma como
div[color=red], consultando novamente o resultado do estilo, não é aceita pelo navegador
- O Datalog funciona de forma parecida, com conjuntos de fatos e derivação baseada em regras
- Átomos e relations como
parent(alice, bob)são a unidade básica - Variáveis
XeYpermitem selecionar conjuntos de itens que atendem às condições - Ao repetir a mesma variável para ligar condições, ocorre um join parecido com a combinação de selectors no CSS
- Átomos e relations como
- A estrutura
head(X, Y) :- body1(X, Z), body2(Z, Y)é parecida com uma regra CSS, apenas com a direção invertida- O selector do CSS se aproxima do body do Datalog, e a declaration se aproxima do head
div.awesome { color: red; }corresponde acolor(X, red) :- div(X), class(X, awesome).
As consultas recursivas que o CSS não consegue fazer
- A condição de aplicar um estilo invertido a todos os elementos em foco dentro de
data-theme="dark", mas parar quando aparecerdata-theme="light"no meio do caminho, exige uma consulta transitiva- No CSS real, dá para cobrir só parte do caso com regras como
[data-theme="dark"] :focuse[data-theme="dark"] [data-theme="light"] :focus - Se o nível de aninhamento aumenta, é preciso continuar adicionando regras, e fica difícil expressar diretamente a relação recursiva
- No CSS real, dá para cobrir só parte do caso com regras como
- A condição necessária é determinar recursivamente se um elemento é effectively-dark
- Se ele próprio tiver
data-theme="dark", então se torna effectively-dark - Filhos sob um ancestral effectively-dark também se tornam effectively-dark, desde que não haja
data-theme="light"no meio - Com base nesse estado, seria preciso aplicar estilo a
.effectively-dark :focus
- Se ele próprio tiver
- Em uma sintaxe hipotética chamada CSSLog, as regras poderiam adicionar estados derivados com algo como
class: +effectively-dark.effectively-dark > :not([data-theme="light"])propagaria o estado para os filhos- As regras precisariam se repetir recursivamente até alcançar o estado-alvo
- Esse tipo de propagação recursiva é difícil de expressar no CSS atual
- No fim do texto aparecem formas de imitar algo parecido, mas não são uma solução geral baseada no mesmo princípio
Recursão e ponto fixo no Datalog
- O Datalog funciona derivando fatos novos a partir de fatos existentes e trata recursão como algo básico
ancestor(X, Y) :- parent(X, Y).ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
- As regras de
ancestorexpandem gradualmente a relação de ancestralidade com base na relação de paternidade- De
parent(alice, bob)surge primeiroancestor(alice, bob) - Depois também são derivados caminhos como
alice -> bob -> carolealice -> bob -> dave
- De
- Esse cálculo avança até o fim por meio de avaliação por ponto fixo, mesmo sem um loop
forexplícito- No começo, usa apenas os fatos-base declarados
- O body de todas as regras é aplicado ao conjunto atual de fatos para acrescentar o head
- O processo para quando não surgem mais fatos novos
- O motivo de isso terminar está na monotonicidade
- O sistema só adiciona fatos, sem removê-los, então o conjunto de fatos conhecidos só cresce
- Se começa com um conjunto finito de fatos, o número de fatos deriváveis também fica finitamente limitado
- Em contrapartida, se fosse possível remover fatos, conclusões anteriores poderiam ser revertidas e o processo poderia cair em loop infinito
Container Queries e os limites do CSS real
- As Container Queries do CSS real permitem aplicar regras com base no estilo de um ancestral ou contêiner
- Há suporte para formas como
@container style(--theme: dark) { .card { background: royalblue; color: white; } }
- Há suporte para formas como
- Mas o exemplo de dark mode transitivo exige algo mais forte do que apenas consultar ancestrais
- Cada elemento precisa saber se ele mesmo é effectively-dark
- Esse estado precisa ser propagado de forma transitiva para todos os descendentes
- A propagação precisa parar na fronteira
data-theme="light"
- Container Queries não conseguem tratar a segunda condição
- Dá para ler custom properties de ancestrais, mas não consultar de volta um estado derivado já calculado por outras regras
- É possível ver informações que já existiam no DOM, mas não usar o resultado de um cálculo recursivo como condição de selector
- Um texto relacionado de 2015 também aponta que element queries esbarravam no mesmo problema
- Se uma propriedade definida por uma consulta puder ser consultada novamente, cresce o risco de loops e repetição infinita
- O CSS Working Group vem evitando esse problema por meio de restrições na direção do fluxo de informação
- É permitido que descendentes consultem informações de ancestrais
- Mas feedback no sentido oposto ou ciclos envolvendo o próprio estilo do elemento são bloqueados
- Assim, o cálculo permanece finito mesmo sem semântica de ponto fixo
A possibilidade de inverter a sintaxe do CSS em uma linguagem de consulta recursiva
- Em vez de colocar a semântica do Datalog dentro do CSS, o texto sugere como caminho mais realista colocar a sintaxe do CSS sobre o Datalog
- A sintaxe do Datalog, com
:-, pontos finais e átomos sem declaração, representa uma barreira de entrada para muitos usuários de linguagens modernas - O CSS já tem uma sintaxe rica de selectors para lidar com estruturas em árvore
- A sintaxe do Datalog, com
- O texto observa que muitos dados reais têm forma de árvore
- JSON
- AST
- sistema de arquivos
- organograma
- XML
- Nesses domínios, pode ser útil combinar sintaxe ao estilo CSS, que lida implicitamente com relações entre pai e filho, com recursão por ponto fixo
- No Datalog comum, é trabalhoso reescrever a estrutura em árvore como representação relacional
- Se a intuição dos selectors CSS puder ser levada diretamente para consultas recursivas, mais programadores poderão se aproximar disso com facilidade
- Ainda não há uma ferramenta claramente estabelecida nesse formato
- O nome “CSSLog” é provisório, e pode surgir uma linguagem melhor batizada
- Ainda existe espaço para tratar consultas recursivas em árvores com uma notação mais familiar
Pontos complementares e links de referência
- O Datalog surgiu nos anos 1970 no contexto de bancos de dados relacionais e da pesquisa em IA da época, e desde então reapareceu repetidamente em várias formas
- Uma forma simples de cálculo de ponto fixo é apresentada como naive evaluation, mas ela pode ser ineficiente por recalcular repetidamente fatos já conhecidos
- Como melhoria representativa, também é mencionada a semi-naive evaluation, que usa apenas os fatos novos de cada etapa
- A monotonicidade também leva a propriedades úteis em sistemas distribuídos
- Também existe uma forma de imitar parcialmente o dark mode transitivo com herança de custom properties
[data-theme="dark"] { --effective-theme: dark; }[data-theme="light"] { --effective-theme: light; }@container style(--effective-theme: dark) { :focus { outline-color: white; } }- Esse método em geral funciona para esse caso específico, mas não fornece um fechamento transitivo real de forma geral
1 comentários
Comentários do Hacker News
Seletores CSS são muito mais fáceis de escrever do que XPath
Recentemente até houve uma apresentação sobre como a nova API DOM do PHP lida com HTML e seletores CSS de forma nativa e muito simples. Antes, era preciso converter CSS para XPath
[1] https://speakerdeck.com/keyvan/parsing-html-with-php-8-dot-4...
Como isso evoluiu com foco em estilização de navegador, é uma pena que não tenha recursos como seleção baseada no conteúdo de texto, como no XPath
Pelo que sei, já houve propostas no passado, mas elas acabaram não entrando na spec por possíveis problemas de desempenho no contexto de renderização do navegador
Ao criar um agente de edição de documentos, exibimos o documento em HTML e deixamos o LLM indicar apenas o CSS selector para puxar os trechos necessários para o contexto, e funcionou quase como mágica
As pessoas podem usar exatamente o padrão com que já estão acostumadas
Seria bom existir um nome para separar a sintaxe do CSS de todo o sistema de regras, funções e unidades definido pelo CSSWG
Há bastante potencial aí, mas para discutir ou pesquisar outros casos de uso, no fim parece que só resta vasculhar código no GitHub com parser de CSS embutido para ver que tipo de coisa estranha as pessoas estão criando
Também estou mexendo com algo parecido com um mecanismo de templates estranho, misturando uma linguagem de marcação leve baseada em nós, seletores CSS para expressar o que entra no template, e uma sintaxe parecida com CSS para controlar como esses pedaços se combinam
https://www.w3.org/TR/selectors-3/
A spec do DOM também faz referência a isso
https://dom.spec.whatwg.org/#selectors
Então o termo genérico CSS selector já está correto, e também dá para chamar só de selector
O nome DOM selector talvez pareça mais limpo, mas se você considerar seletores usados em CSS estático ou em outros motores DOM fora de engines JavaScript, como parser XML, API DOM do PHP etc., isso pode acabar sendo ainda mais confuso
Além disso, existem seletores especiais ligados diretamente à renderização e navegação do navegador, como
:hovere::target-textAinda assim, pode ser útil ter um nome separado para o subconjunto mínimo da sintaxe de consulta menos acoplado ao navegador ou ao CSS
Isso me lembra https://github.com/braposo/graphql-css, que vi numa conferência há algum tempo
Era um projeto de brincadeira, mas gostei porque mostrava bem como transplantar e reutilizar padrões em outros contextos pode tornar possíveis coisas inesperadas
Estou justamente tentando reaproveitar padrões de contextos diferentes desse jeito
Mesmo que a maior parte não vá muito longe, isso tem um apelo bem interessante para o espírito hacker
pyastgrep, como mostrado em https://pyastgrep.readthedocs.io/en/latest/, permite usar seletores CSS para consultar sintaxe Python
O padrão é XPath e, por exemplo, dá para fazer
pyastgrep --css 'Call > func > Name#main'Vai quase exatamente na direção que eu queria apontar
Não entendi bem que tipo de cenário isso resolve
Hoje já dá para alterar condicionalmente um pai com base no filho. Por exemplo,
pretem padding padrão de 16px e, se o filho direto forcode, dá para zerar isso com&:has(> code)A conclusão fica mais próxima de “precisamos corrigir os limites do CSS moderno” do que de imaginar se colocar uma sintaxe parecida com CSS sobre um sistema parecido com Datalog poderia tornar o trabalho com dados em árvore mais familiar para mais engenheiros
Ou seja, a ideia é adicionar novos elementos filhos ou atributos ao DOM
Os LLMs atuais não costumam lidar tão bem com CSS, então isso até dá vontade de testar para ver se, desse jeito, eles conseguiriam raciocinar de forma mais simples
Não consigo pensar em muita utilidade prática, mas ainda assim é legal
Hum... isso não é simplesmente JQ?
Até gosto de CSS até certo ponto, mas odeio como o complexity creep só piora
Entendo a lógica de que linguagens de programação acabam ficando mais poderosas que linguagens não programáveis, mas em vez de continuar aumentando a complexidade de HTML, CSS e JavaScript, eu preferiria que surgisse outra coisa para substituir tudo isso
Também quase nunca uso os novos elementos do HTML5, porque na maioria dos casos nem entendo por que eles são necessários. No fim, muitos containers parecem ser só
divcom IDs únicos e eu até gostaria que existisse algo como um alias para esses IDs para navegaçãohrefinternaCoisas como
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }demoram demais para eu interpretar mentalmente e já não parecem mais elegantes nem simplesPor outro lado,
h2 { color: red; }ainda é simplesUma expressão como
ancestor(X, Y) :- parent(X, Y).já me faz perder a vontade de pensar. Afinal, o que é:-? Parece uma carinha sorrindoParei de ler em
@container style(--theme: dark) { .card { background: royalblue; color: white; } }É estranho ver padrões que antes funcionavam bem parecerem se deteriorar com o tempo
Por exemplo,
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }em pseudocódigo mais próximo do inglês seria algo como: existe um X comdata-theme="dark", seu filho Y temdata-theme="light"e está em foco, entãooutline-colorde Y deve ser blackAssim, em estilo Datalog, isso poderia ser escrito como
outline-color(Y, black) if data-theme(X, "dark") and parent(X, Y) and data-theme(Y, "light") and focused(Y)Ou seja, trocando
:-porife vírgulas porandIndo além, também daria para escrever algo como
Y.outline_color := black if X.data-theme == dark and Y.parent == X and Y.data-theme == dark and Y.focused, fazendoattr(X, val)parecer um açúcar sintático parecido com UFCS na formaX.attr == valSe quisesse algo com mais cara de família ALGOL, também poderia ser
forall Y { Y.outline_color := black if Y.data_theme == "dark" and Y.focused and Y.parent.data_theme == "light" }Aqui Y é introduzido explicitamente e um dos joins fica implícito, então isso parece mais programação geral, mas na prática ainda seria um motor Datalog executando esse tipo de loop com eficiência sempre que as dependências mudassem