Construindo bibliotecas de padrões com Shadow DOM em Markdown

Publicados: 2022-03-10
Resumo rápido ↬ Algumas pessoas odeiam escrever documentação, e outras simplesmente odeiam escrever. Acontece que adoro escrever; caso contrário, você não estaria lendo isso. Ajuda que eu adore escrever porque, como consultor de design que oferece orientação profissional, escrever é uma grande parte do que faço. Mas eu odeio, odeio, odeio processadores de texto. Ao escrever documentação técnica da web (leia: bibliotecas de padrões), os processadores de texto não são apenas desobedientes, mas inadequados. Idealmente, eu quero um modo de escrita que me permita incluir os componentes que estou documentando inline, e isso não é possível a menos que a documentação em si seja feita de HTML, CSS e JavaScript. Neste artigo, compartilharei um método para incluir facilmente demonstrações de código no Markdown, com a ajuda de códigos de acesso e encapsulamento de shadow DOM.

Meu fluxo de trabalho típico usando um processador de texto de desktop é mais ou menos assim:

  1. Selecione algum texto que desejo copiar para outra parte do documento.
  2. Observe que o aplicativo selecionou um pouco mais ou menos do que eu disse.
  3. Tente novamente.
  4. Desista e resolva adicionar a parte que falta (ou remover a parte extra) da minha seleção pretendida mais tarde.
  5. Copie e cole a seleção.
  6. Observe que a formatação do texto colado é um pouco diferente do original.
  7. Tente encontrar a predefinição de estilo que corresponda ao texto original.
  8. Tente aplicar a predefinição.
  9. Desista e aplique a família e o tamanho da fonte manualmente.
  10. Observe que há muito espaço em branco acima do texto colado e pressione “Backspace” para fechar a lacuna.
  11. Observe que o texto em questão se elevou várias linhas ao mesmo tempo, juntou o texto do cabeçalho acima dele e adotou seu estilo.
  12. Pondere minha mortalidade.

Ao escrever documentação técnica da web (leia: bibliotecas de padrões), os processadores de texto não são apenas desobedientes, mas inadequados. Idealmente, eu quero um modo de escrita que me permita incluir os componentes que estou documentando inline, e isso não é possível a menos que a documentação em si seja feita de HTML, CSS e JavaScript. Neste artigo, compartilharei um método para incluir facilmente demonstrações de código no Markdown, com a ajuda de códigos de acesso e encapsulamento de shadow DOM.

Um M, uma seta para baixo mais um detive escondido no escuro simbolizando Markdown e Shadown Dom
Mais depois do salto! Continue lendo abaixo ↓

CSS e Markdown

Diga o que quiser sobre CSS, mas certamente é uma ferramenta de composição tipográfica mais consistente e confiável do que qualquer editor WYSIWYG ou processador de texto no mercado. Por quê? Porque não existe um algoritmo de caixa preta de alto nível que tente adivinhar quais estilos você realmente pretendia ir para onde. Em vez disso, é muito explícito: você define quais elementos adotam quais estilos em quais circunstâncias e respeita essas regras.

O único problema com CSS é que ele requer que você escreva sua contraparte, HTML. Mesmo os grandes amantes do HTML provavelmente admitiriam que escrevê-lo manualmente é uma tarefa árdua quando você quer apenas produzir conteúdo em prosa. É aí que entra o Markdown. Com sua sintaxe concisa e conjunto de recursos reduzido, ele oferece um modo de escrita que é fácil de aprender, mas ainda pode - uma vez convertido em HTML programaticamente - aproveitar os recursos de composição de texto poderosos e previsíveis do CSS. Há uma razão pela qual ele se tornou o formato de fato para geradores de sites estáticos e plataformas modernas de blogs, como o Ghost.

Onde uma marcação mais complexa e personalizada for necessária, a maioria dos analisadores Markdown aceitará HTML bruto na entrada. No entanto, quanto mais se confia em marcação complexa, menos acessível é o sistema de autoria para aqueles que são menos técnicos ou com pouco tempo e paciência. É aqui que entram os códigos de acesso.

Códigos de acesso em Hugo

Hugo é um gerador de site estático escrito em Go — uma linguagem compilada multifuncional desenvolvida no Google. Devido à simultaneidade (e, sem dúvida, outros recursos de linguagem de baixo nível que eu não entendo completamente), Go faz do Hugo um gerador extremamente rápido de conteúdo da web estático. Esta é uma das muitas razões pelas quais Hugo foi escolhido para a nova versão da Smashing Magazine.

Além do desempenho, ele funciona de maneira semelhante aos geradores baseados em Ruby e Node.js com os quais você já deve estar familiarizado: Markdown mais metadados (YAML ou TOML) processados ​​por meio de modelos. Sara Soueidan escreveu uma excelente cartilha sobre a funcionalidade central do Hugo.

Para mim, o recurso matador do Hugo é a implementação de códigos de acesso. Aqueles que vêm do WordPress já podem estar familiarizados com o conceito: uma sintaxe abreviada usada principalmente para incluir os códigos de incorporação complexos de serviços de terceiros. Por exemplo, o WordPress inclui um shortcode do Vimeo que leva apenas o ID do vídeo do Vimeo em questão.

 [vimeo 44633289]

Os colchetes significam que seu conteúdo deve ser processado como um código de acesso e expandido para a marcação de incorporação HTML completa quando o conteúdo for analisado.

Fazendo uso das funções do modelo Go, o Hugo fornece uma API extremamente simples para criar códigos de acesso personalizados. Por exemplo, criei um código de acesso simples do Codepen para incluir no meu conteúdo do Markdown:

 Some Markdown content before the shortcode. Aliquam sodales rhoncus dui, sed congue velit semper ut. Class aptent taciti sociosqu ad litora torquent. {{<codePen VpVNKW>}} Some Markdown content after the shortcode. Nulla vel magna sit amet dui lobortis commodo vitae vel nulla sit amet ante hendrerit tempus.

Hugo procura automaticamente um modelo chamado codePen.html na subpasta de shortcodes de acesso para analisar o código de acesso durante a compilação. Minha implementação está assim:

 {{ if .Site.Params.codePenUser }} <iframe height='300' scrolling='no' title="code demonstration with codePen" src='//codepen.io/{{ .Site.Params.codepenUser | lower }}/embed/{{ .Get 0 }}/?height=265&theme-id=dark&default-tab=result,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true'> <div> <a href="//codepen.io/{{ .Site.Params.codePenUser | lower }}/pen/{{ .Get 0 }}">See the demo on codePen</a> </div> </iframe> {{ else }} <p class="site-error"><strong>Site error:</strong> The <code>codePenUser</code> param has not been set in <code>config.toml</code></p> {{ end }}

Para ter uma ideia melhor de como o pacote de modelos Go funciona, você deve consultar o “Go Template Primer” de Hugo. Enquanto isso, observe apenas o seguinte:

  • É bastante fugly, mas poderoso, no entanto.
  • A parte {{ .Get 0 }} é para recuperar o primeiro (e, neste caso, apenas) argumento fornecido — o Codepen ID. Hugo também suporta argumentos nomeados, que são fornecidos como atributos HTML.
  • O . sintaxe refere-se ao contexto atual. Portanto, .Get 0 significa “Obter o primeiro argumento fornecido para o shortcode atual”.

De qualquer forma, acho que os shortcodes são a melhor coisa desde o shortbread, e a implementação de Hugo para escrever shortcodes personalizados é impressionante. Devo observar em minha pesquisa que é possível usar Jekyll inclui para efeitos semelhantes, mas acho-os menos flexíveis e poderosos.

Demonstrações de código sem terceiros

Eu tenho muito tempo para o Codepen (e outros playgrounds de código disponíveis), mas há problemas inerentes ao incluir esse conteúdo em uma biblioteca de padrões:

  • Ele usa uma API, portanto, não pode ser feito com facilidade ou eficiência para funcionar offline.
  • Ele não representa apenas o padrão ou componente; é sua própria interface complexa envolta em sua própria marca. Isso cria ruído e distração desnecessários quando o foco deveria estar no componente.

Por algum tempo, tentei incorporar demonstrações de componentes usando meus próprios iframes. Eu apontaria o iframe para um arquivo local contendo a demonstração como sua própria página da web. Ao usar iframes, consegui encapsular estilo e comportamento sem depender de terceiros.

Infelizmente, os iframes são bastante complicados e difíceis de redimensionar dinamicamente. Em termos de complexidade de autoria, também implica manter arquivos separados e ter que vinculá-los. Prefiro escrever meus componentes no lugar, incluindo apenas o código necessário para fazê-los funcionar. Eu quero ser capaz de escrever demos enquanto escrevo sua documentação.

O Shortcode de demo

Felizmente, Hugo permite que você crie códigos de acesso que incluem conteúdo entre abrir e fechar tags de código de acesso. O conteúdo está disponível no arquivo shortcode usando {{ .Inner }} . Então, suponha que eu use um shortcode de demo como este:

 {{<demo>}} This is the content! {{</demo>}}

“Este é o conteúdo!” estaria disponível como {{ .Inner }} no modelo demo.html que o analisa. Este é um bom ponto de partida para dar suporte a demonstrações de código embutido, mas preciso abordar o encapsulamento.

Encapsulamento de estilo

Quando se trata de encapsular estilos, há três coisas com as quais se preocupar:

  • estilos sendo herdados pelo componente da página pai,
  • a página pai herdando estilos do componente,
  • estilos sendo compartilhados não intencionalmente entre os componentes.

Uma solução é gerenciar cuidadosamente os seletores CSS para que não haja sobreposição entre os componentes e entre os componentes e a página. Isso significaria usar seletores esotéricos por componente, e não é algo que eu estaria interessado em considerar quando pudesse escrever um código conciso e legível. Uma das vantagens dos iframes é que os estilos são encapsulados por padrão, então eu poderia escrever button { background: blue } e ter certeza de que ele só se aplicaria dentro do iframe.

Uma maneira menos intensiva de impedir que os componentes herdem estilos da página é usar a propriedade all com o valor initial em um elemento pai eleito. Eu posso definir este elemento no arquivo demo.html :

 <div class="demo"> {{ .Inner }} </div>

Então, preciso aplicar all: initial às instâncias desse elemento, que se propaga para os filhos de cada instância.

 .demo { all: initial }

O comportamento da initial é bastante… idiossincrático. Na prática, todos os elementos afetados voltam a adotar apenas seus estilos de agente de usuário (como display: block para elementos <h2> ). No entanto, o elemento ao qual ele é aplicado — class=“demo” — precisa ter certos estilos de agente do usuário explicitamente restabelecidos. No nosso caso, isso é apenas display: block , já que class=“demo” é um <div> .

 .demo { all: initial; display: block; }

Observação: até agora, all não é suportado no Microsoft Edge, mas está sendo considerado. O suporte é, de outra forma, tranquilizadoramente amplo. Para nossos propósitos, o valor de revert seria mais robusto e confiável, mas ainda não é suportado em nenhum lugar.

Shadow DOM'ing The Shortcode

Usar all: initial não torna nossos componentes inline completamente imunes à influência externa (a especificidade ainda se aplica), mas podemos ter certeza de que os estilos não estão definidos porque estamos lidando com o nome da classe de demo reservada. Principalmente estilos herdados de seletores de baixa especificidade, como html e body , serão eliminados.

No entanto, isso lida apenas com estilos provenientes do pai em componentes. Para evitar que estilos escritos para componentes afetem outras partes da página, precisaremos usar shadow DOM para criar uma subárvore encapsulada.

Imagine que eu queira documentar um elemento <button> com estilo. Eu gostaria de poder simplesmente escrever algo como o seguinte, sem medo de que o seletor de elemento de button seja aplicado aos elementos <button> na própria biblioteca de padrões ou em outros componentes na mesma página da biblioteca.

 {{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}

O truque é pegar a parte {{ .Inner }} do modelo de código de acesso e incluí-la como innerHTML de um novo ShadowRoot . Eu poderia implementar isso assim:

 {{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} <div class="demo"></div> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); root.innerHTML = '{{ .Inner }}'; })(); </script>
  • $uniq é definido como uma variável para identificar o contêiner do componente. Ele canaliza algumas funções de template Go para criar uma string única... esperançosamente(!) — este não é um método à prova de balas; é apenas para ilustração.
  • root.attachShadow torna o contêiner do componente um host DOM de sombra.
  • Eu preencho o innerHTML do ShadowRoot usando {{ .Inner }} , que inclui o CSS agora encapsulado.

Permitindo Comportamento JavaScript

Eu também gostaria de incluir o comportamento JavaScript em meus componentes. A princípio, pensei que seria fácil; infelizmente, o JavaScript inserido via innerHTML não é analisado ou executado. Isso pode ser resolvido importando do conteúdo de um elemento <template> . Eu alterei minha implementação de acordo.

 {{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} <div class="demo"></div> <template> {{ .Inner }} </template> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); var template = document.getElementById('template-{{ $uniq }}'); root.shadowRoot.appendChild(document.importNode(template.content, true)); })(); </script>

Agora, posso incluir uma demonstração em linha de, digamos, um botão de alternância funcional:

 {{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } [aria-pressed="true"] { box-shadow: inset 0 0 5px #000; } </style> <script> var toggle = document.querySelector('[aria-pressed]'); toggle.addEventListener('click', (e) => { let pressed = e.target.getAttribute('aria-pressed') === 'true'; e.target.setAttribute('aria-pressed', !pressed); }); </script> {{</demo>}}

Observação: escrevi detalhadamente sobre botões de alternância e acessibilidade para componentes inclusivos.

Encapsulamento de JavaScript

O JavaScript, para minha surpresa, não é encapsulado automaticamente como o CSS está no shadow DOM. Ou seja, se houvesse outro botão [aria-pressed] na página pai antes do exemplo deste componente, então document.querySelector o direcionaria.

O que eu preciso é de um document equivalente apenas para a subárvore da demonstração. Isso é definível, embora bastante detalhado:

 document.getElementById('demo-{{ $uniq }}').shadowRoot;

Eu não queria ter que escrever essa expressão sempre que tivesse que direcionar elementos dentro de contêineres de demonstração. Então, eu criei um hack pelo qual atribuí a expressão a uma variável de demo local e scripts prefixados fornecidos por meio do shortcode com esta atribuição:

 if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));

Com isso em vigor, demo se torna o equivalente a document para qualquer subárvore de componente, e posso usar demo.querySelector para direcionar facilmente meu botão de alternância.

 var toggle = demo.querySelector('[aria-pressed]');

Observe que incluí o conteúdo do script da demonstração em uma expressão de função imediatamente invocada (IIFE), para que a variável demo — e todas as variáveis ​​de procedimento usadas para o componente — não estejam no escopo global. Desta forma, demo pode ser usado em qualquer script de shortcode, mas apenas se referirá ao shortcode em mãos.

Onde o ECMAScript6 está disponível, é possível obter a localização usando "escopo de bloco", com apenas chaves entre as instruções let ou const . No entanto, todas as outras definições dentro do bloco teriam que usar let ou const (evitando var ) também.

 { let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }

Suporte ao Shadow DOM

Obviamente, todas as opções acima só são possíveis onde o shadow DOM versão 1 é suportado. Chrome, Safari, Opera e Android parecem muito bons, mas os navegadores Firefox e Microsoft são problemáticos. É possível detectar o suporte de detecção de recursos e fornecer uma mensagem de erro quando attachShadow não estiver disponível:

 if (document.head.attachShadow) { // Do shadow DOM stuff here } else { root.innerHTML = 'Shadow DOM is needed to display encapsulated demos. The browser does not have an issue with the demo code itself'; }

Ou você pode incluir Shady DOM e a extensão Shady CSS, o que significa uma dependência um pouco grande (60 KB+) e uma API diferente. Rob Dodson teve a gentileza de me fornecer uma demonstração básica, que estou feliz em compartilhar para ajudá-lo a começar.

Legendas para componentes

Com a funcionalidade básica de demonstração em linha, escrever rapidamente demonstrações de trabalho em linha com sua documentação é misericordiosamente simples. Isso nos dá o luxo de poder fazer perguntas como: "E se eu quiser fornecer uma legenda para rotular a demonstração?" Isso já é perfeitamente possível, pois - como observado anteriormente - o Markdown suporta HTML bruto.

 <figure role="group" aria-labelledby="caption-button"> {{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}} <figcaption>A standard button</figcaption> </figure>

No entanto, a única novidade desta estrutura alterada é a própria redação da legenda. É melhor fornecer uma interface simples para fornecê-la à saída, economizando meu futuro eu – e qualquer outra pessoa usando o shortcode – tempo e esforço e reduzindo o risco de erros de codificação. Isso é possível fornecendo um parâmetro nomeado para o shortcode — neste caso, simplesmente chamado caption :

 {{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}

Parâmetros nomeados são acessíveis no modelo como {{ .Get "caption" }} , que é bastante simples. Eu quero que a legenda e, portanto, a <figure> e a <figcaption> ao redor sejam opcionais. Usando cláusulas if , posso fornecer o conteúdo relevante apenas onde o shortcode fornece um argumento de legenda:

 {{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}

Veja como o modelo demo.html completo agora se parece (reconhecidamente, é um pouco confuso, mas funciona):

 {{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} {{ if .Get "caption" }} <figure role="group" aria-labelledby="caption-{{ $uniq }}"> {{ end }} <div class="demo"></div> {{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }} {{ if .Get "caption" }} </figure> {{ end }} <template> {{ .Inner }} </template> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); var template = document.getElementById('template-{{ $uniq }}'); var script = template.content.querySelector('script'); if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true)); })(); </script>

Uma última observação: se eu quiser dar suporte à sintaxe de remarcação no valor da legenda, posso canalizá-la por meio da função markdownify de Hugo. Dessa forma, o autor pode fornecer markdown (e HTML), mas não é obrigado a fazer isso.

 {{ .Get "caption" | markdownify }}

Conclusão

Por seu desempenho e seus muitos recursos excelentes, Hugo é atualmente um ajuste confortável para mim quando se trata de geração de sites estáticos. Mas a inclusão de códigos de acesso é o que acho mais atraente. Neste caso, consegui criar uma interface simples para um problema de documentação que venho tentando resolver há algum tempo.

Como nos componentes da Web, muita complexidade de marcação (às vezes exacerbada pelo ajuste de acessibilidade) pode ser ocultada por códigos de acesso. Neste caso, estou me referindo à minha inclusão de role="group" e o relacionamento aria-labelledby , que fornece um "rótulo de grupo" com melhor suporte para o <figure> - não coisas que alguém gosta de codificar mais de uma vez, especialmente em que valores de atributos exclusivos precisam ser considerados em cada instância.

Acredito que os códigos de acesso são para Markdown e conteúdo o que os componentes da Web são para HTML e funcionalidade: uma maneira de tornar a autoria mais fácil, mais confiável e mais consistente. Aguardo ansiosamente por mais evolução neste pequeno campo curioso da web.

Recursos

  • Documentação Hugo
  • “Package Template,” A Linguagem de Programação Go
  • “Códigos de acesso”, Hugo
  • “all” (propriedade abreviada de CSS), Mozilla Developer Network
  • “inicial (palavra-chave CSS), Mozilla Developer Network
  • “Shadow DOM v1: Componentes Web Autocontidos”, Eric Bidelman, Web Fundamentals, Google Developers
  • “Introdução aos elementos de modelo”, Eiji Kitamura, WebComponents.org
  • “Inclui”, Jekyll