Costruire librerie di modelli con Shadow DOM in Markdown
Pubblicato: 2022-03-10Il mio flusso di lavoro tipico che utilizza un elaboratore di testi desktop funziona in questo modo:
- Seleziona del testo che voglio copiare in un'altra parte del documento.
- Si noti che l'applicazione ha selezionato leggermente più o meno di quanto le avevo indicato.
- Riprova.
- Rinuncia e decidi di aggiungere la parte mancante (o rimuovere la parte extra) della mia selezione prevista in un secondo momento.
- Copia e incolla la selezione.
- Si noti che la formattazione del testo incollato è in qualche modo diversa dall'originale.
- Prova a trovare lo stile predefinito che corrisponde al testo originale.
- Prova ad applicare il preset.
- Rinuncia e applica manualmente la famiglia e la dimensione dei caratteri.
- Nota che c'è troppo spazio bianco sopra il testo incollato e premi "Backspace" per chiudere lo spazio.
- Si noti che il testo in questione si è elevato di più righe contemporaneamente, si è unito al testo del titolo sopra di esso e ne ha adottato lo stile.
- Rifletti sulla mia mortalità.
Quando si scrive documentazione web tecnica (leggi: librerie di modelli), i word processor non sono solo disobbedienti, ma inappropriati. Idealmente, voglio una modalità di scrittura che mi permetta di includere i componenti che sto documentando in linea, e questo non è possibile a meno che la documentazione stessa non sia composta da HTML, CSS e JavaScript. In questo articolo, condividerò un metodo per includere facilmente demo di codice in Markdown, con l'aiuto di shortcode e incapsulamento shadow DOM.

CSS e riduzione
Dì quello che vuoi sui CSS, ma è sicuramente uno strumento di composizione più coerente e affidabile di qualsiasi editor WYSIWYG o word processor sul mercato. Come mai? Perché non esiste un algoritmo black-box di alto livello che tenti di indovinare quali stili volevi davvero andare dove. Invece, è molto esplicito: definisci quali elementi prendono quali stili in quali circostanze e rispetta quelle regole.
L'unico problema con CSS è che richiede di scrivere la sua controparte, HTML. Anche i grandi amanti dell'HTML probabilmente ammetterebbero che scriverlo manualmente è arduo quando vuoi solo produrre contenuti in prosa. È qui che entra in gioco Markdown. Con la sua sintassi concisa e il set di funzionalità ridotto, offre una modalità di scrittura facile da imparare ma può ancora, una volta convertito in HTML a livello di codice, sfruttare le potenti e prevedibili funzionalità di composizione dei CSS. C'è un motivo per cui è diventato il formato de facto per i generatori di siti Web statici e le moderne piattaforme di blogging come Ghost.
Laddove è richiesto un markup più complesso e personalizzato, la maggior parte dei parser Markdown accetterà HTML grezzo nell'input. Tuttavia, più ci si affida a markup complessi, meno accessibile è il proprio sistema di authoring a coloro che sono meno tecnici o che hanno poco tempo e pazienza. È qui che entrano in gioco gli shortcode.
Shortcode in Hugo
Hugo è un generatore di siti statici scritto in Go, un linguaggio compilato multiuso sviluppato da Google. A causa della concorrenza (e, senza dubbio, di altre funzionalità del linguaggio di basso livello che non comprendo completamente), Go rende Hugo un generatore velocissimo di contenuti web statici. Questo è uno dei tanti motivi per cui Hugo è stato scelto per la nuova versione di Smashing Magazine.
Prestazioni a parte, funziona in modo simile ai generatori basati su Ruby e Node.js con cui potresti già avere familiarità: Markdown più metadati (YAML o TOML) elaborati tramite modelli. Sara Soueidan ha scritto un eccellente primer sulle funzionalità principali di Hugo.
Per me, la caratteristica killer di Hugo è la sua implementazione di codici brevi. Chi proviene da WordPress potrebbe già avere familiarità con il concetto: una sintassi abbreviata utilizzata principalmente per includere i complessi codici di incorporamento di servizi di terze parti. Ad esempio, WordPress include uno shortcode Vimeo che prende solo l'ID del video Vimeo in questione.
[vimeo 44633289]
Le parentesi indicano che il loro contenuto deve essere elaborato come shortcode ed espanso nel markup di incorporamento HTML completo quando il contenuto viene analizzato.
Utilizzando le funzioni del modello Go, Hugo fornisce un'API estremamente semplice per la creazione di codici brevi personalizzati. Ad esempio, ho creato un semplice shortcode Codepen da includere tra i miei contenuti 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 cerca automaticamente un modello chiamato codePen.html
nella sottocartella shortcodes
per analizzare lo shortcode durante la compilazione. La mia implementazione è simile a questa:
{{ 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 }}
Per avere un'idea migliore di come funziona il pacchetto di modelli Go, ti consigliamo di consultare "Go Template Primer" di Hugo. Nel frattempo, basta notare quanto segue:
- È piuttosto vaga ma comunque potente.
- La parte
{{ .Get 0 }}
serve per recuperare il primo (e, in questo caso, l'unico) argomento fornito: l'ID Codepen. Hugo supporta anche argomenti denominati, forniti come attributi HTML. - Il
.
la sintassi si riferisce al contesto corrente. Quindi,.Get 0
significa "Ottieni il primo argomento fornito per lo shortcode corrente".
In ogni caso, penso che gli shortcode siano la cosa migliore dopo lo shortbread e l'implementazione di Hugo per la scrittura di shortcode personalizzati è impressionante. Dovrei notare dalla mia ricerca che è possibile utilizzare Jekyll include con effetti simili, ma li trovo meno flessibili e potenti.
Demo del codice senza terze parti
Ho molto tempo per Codepen (e gli altri giochi di codice disponibili), ma ci sono problemi inerenti all'inclusione di tali contenuti in una libreria di modelli:
- Utilizza un'API, quindi non può essere facilmente o in modo efficiente fatto funzionare offline.
- Non rappresenta solo il modello o il componente; è la sua complessa interfaccia avvolta nel proprio marchio. Questo crea rumore e distrazione inutili quando l'attenzione dovrebbe essere sul componente.
Per qualche tempo, ho provato a incorporare demo di componenti usando i miei iframe. Indirizzerei l'iframe a un file locale contenente la demo come propria pagina web. Utilizzando gli iframe, sono stato in grado di incapsulare stile e comportamento senza fare affidamento su terze parti.
Sfortunatamente, gli iframe sono piuttosto ingombranti e difficili da ridimensionare dinamicamente. In termini di complessità di creazione, comporta anche il mantenimento di file separati e la necessità di collegarsi ad essi. Preferirei scrivere i miei componenti sul posto, includendo solo il codice necessario per farli funzionare. Voglio essere in grado di scrivere demo mentre scrivo la loro documentazione.
Lo Shortcode demo
Fortunatamente, Hugo ti consente di creare shortcode che includono contenuti tra l'apertura e la chiusura di tag shortcode. Il contenuto è disponibile nel file shortcode utilizzando {{ .Inner }}
. Quindi, supponiamo di dover usare uno shortcode demo
come questo:
{{<demo>}} This is the content! {{</demo>}}
"Questo è il contenuto!" sarebbe disponibile come {{ .Inner }}
nel modello demo.html
che lo analizza. Questo è un buon punto di partenza per supportare le demo di codice inline, ma ho bisogno di affrontare l'incapsulamento.
Incapsulamento di stile
Quando si tratta di incapsulare gli stili, ci sono tre cose di cui preoccuparsi:
- stili ereditati dal componente dalla pagina principale,
- la pagina principale che eredita gli stili dal componente,
- stili condivisi involontariamente tra i componenti.
Una soluzione è gestire con attenzione i selettori CSS in modo che non ci siano sovrapposizioni tra i componenti e tra i componenti e la pagina. Ciò significherebbe utilizzare selettori esoterici per componente, e non è qualcosa che sarei interessato a dover considerare quando potrei scrivere un codice conciso e leggibile. Uno dei vantaggi degli iframe è che gli stili sono incapsulati per impostazione predefinita, quindi potrei scrivere button { background: blue }
ed essere sicuro che si applicherebbe solo all'interno dell'iframe.
Un modo meno intensivo per impedire ai componenti di ereditare gli stili dalla pagina consiste nell'usare la proprietà all
con il valore initial
su un elemento padre scelto. Posso impostare questo elemento nel file demo.html
:
<div class="demo"> {{ .Inner }} </div>
Quindi, devo applicare all: initial
alle istanze di questo elemento, che si propaga ai figli di ogni istanza.
.demo { all: initial }
Il comportamento initial
è piuttosto... idiosincratico. In pratica, tutti gli elementi interessati tornano ad adottare solo i loro stili di user agent (come display: block
per gli elementi <h2>
). Tuttavia, l'elemento a cui è applicato — class=“demo”
— necessita di alcuni stili di user agent esplicitamente reintegrati. Nel nostro caso, questo è solo display: block
, poiché class=“demo”
è un <div>
.

.demo { all: initial; display: block; }
Nota: finora all
non è supportato in Microsoft Edge ma è in esame. Il supporto è, per il resto, rassicurante ampio. Per i nostri scopi, il valore di revert
sarebbe più robusto e affidabile ma non è ancora supportato da nessuna parte.
Shadow DOM'ing The Shortcode
Usando all: initial
non rende i nostri componenti inline completamente immuni da influenze esterne (la specificità si applica ancora), ma possiamo essere certi che gli stili non sono impostati perché abbiamo a che fare con il nome della classe demo
riservata. Per lo più verranno eliminati gli stili appena ereditati da selettori a bassa specificità come html
e body
.
Tuttavia, questo riguarda solo gli stili provenienti dal genitore nei componenti. Per evitare che gli stili scritti per i componenti influiscano su altre parti della pagina, dovremo utilizzare shadow DOM per creare un sottoalbero incapsulato.
Immagina di voler documentare un elemento <button>
in stile. Mi piacerebbe essere in grado di scrivere semplicemente qualcosa come il seguente, senza temere che il selettore dell'elemento del button
si applichi agli elementi <button>
nella libreria di modelli stessa o in altri componenti nella stessa pagina della libreria.
{{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}
Il trucco è prendere la parte {{ .Inner }}
del modello shortcode e includerlo come innerHTML
di un nuovo ShadowRoot
. Potrei implementarlo in questo modo:
{{ $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
viene impostato come variabile per identificare il contenitore dei componenti. Convoglia alcune funzioni del modello Go per creare una stringa univoca... si spera (!) — questo non è un metodo a prova di proiettile; è solo a scopo illustrativo. -
root.attachShadow
rende il contenitore del componente un host DOM shadow. - Popolo l'
innerHTML
diShadowRoot
usando{{ .Inner }}
, che include il CSS ora incapsulato.
Consentire il comportamento JavaScript
Vorrei anche includere il comportamento JavaScript nei miei componenti. All'inizio, ho pensato che sarebbe stato facile; sfortunatamente, JavaScript inserito tramite innerHTML
non viene analizzato o eseguito. Questo può essere risolto importando dal contenuto di un elemento <template>
. Ho modificato la mia implementazione di conseguenza.
{{ $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>
Ora, sono in grado di includere una demo in linea di, ad esempio, un pulsante di commutazione funzionante:
{{<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>}}
Nota: ho scritto in modo approfondito i pulsanti di commutazione e l'accessibilità per i componenti inclusi.
Incapsulamento JavaScript
JavaScript è, con mia sorpresa, non incapsulato automaticamente come CSS è in shadow DOM. Cioè, se c'era un altro pulsante [aria-pressed]
nella pagina principale prima dell'esempio di questo componente, document.querySelector
lo sceglierebbe invece.
Quello di cui ho bisogno è un equivalente per document
solo il sottoalbero della demo. Questo è definibile, anche se abbastanza prolisso:
document.getElementById('demo-{{ $uniq }}').shadowRoot;
Non volevo dover scrivere questa espressione ogni volta che dovevo indirizzare elementi all'interno di contenitori demo. Quindi, ho escogitato un trucco in base al quale ho assegnato l'espressione a una variabile demo
locale e agli script prefissi forniti tramite lo shortcode con questo compito:
if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));
Con questo in atto, demo
diventa l'equivalente di document
per qualsiasi sottoalbero dei componenti e posso usare demo.querySelector
per indirizzare facilmente il mio pulsante di attivazione/disattivazione.
var toggle = demo.querySelector('[aria-pressed]');
Si noti che ho racchiuso il contenuto dello script della demo in un'espressione di funzione richiamata immediatamente (IIFE), in modo che la variabile demo
- e tutte le variabili successive utilizzate per il componente - non siano nell'ambito globale. In questo modo, la demo
può essere utilizzata in qualsiasi script di shortcode ma si riferirà solo allo shortcode in mano.
Laddove è disponibile ECMAScript6, è possibile ottenere la localizzazione utilizzando "l'ambito del blocco", con solo parentesi graffe che racchiudono le istruzioni let
o const
. Tuttavia, tutte le altre definizioni all'interno del blocco dovrebbero utilizzare anche let
o const
(eschewing var
).
{ let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }
Supporto DOM ombra
Ovviamente, tutto quanto sopra è possibile solo dove è supportato il DOM ombra versione 1. Chrome, Safari, Opera e Android sembrano tutti abbastanza buoni, ma i browser Firefox e Microsoft sono problematici. È possibile rilevare la funzionalità di supporto e fornire un messaggio di errore in cui attachShadow
non è disponibile:
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'; }
Oppure puoi includere Shady DOM e l'estensione Shady CSS, il che significa una dipendenza piuttosto ampia (60 KB+) e un'API diversa. Rob Dodson è stato così gentile da fornirmi una demo di base, che sono felice di condividere per aiutarti a iniziare.
Didascalie per componenti
Con la funzionalità demo in linea di base attiva, scrivere rapidamente demo funzionanti in linea con la loro documentazione è misericordiosamente semplice. Questo ci offre il lusso di poter porre domande del tipo: "E se volessi fornire una didascalia per etichettare la demo?" Questo è già perfettamente possibile poiché, come notato in precedenza, Markdown supporta HTML grezzo.
<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>
Tuttavia, l'unica parte nuova di questa struttura modificata è la formulazione della didascalia stessa. È meglio fornire un'interfaccia semplice per fornirlo all'output, risparmiando a me stesso futuro - e a chiunque altro utilizzi lo shortcode - tempo e fatica e riducendo il rischio di errori di battitura. Ciò è possibile fornendo allo shortcode un parametro denominato, in questo caso semplicemente denominato caption
:
{{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}
I parametri denominati sono accessibili nel modello come {{ .Get "caption" }}
, il che è abbastanza semplice. Voglio che la didascalia e, quindi, la <figure>
e <figcaption>
siano opzionali. Utilizzando le clausole if
, posso fornire il contenuto pertinente solo dove lo shortcode fornisce un argomento didascalia:
{{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}
Ecco come appare ora il modello demo.html
completo (è vero, è un po' un pasticcio, ma fa il trucco):
{{ $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>
Un'ultima nota: se voglio supportare la sintassi markdown nel valore della didascalia, posso inviarlo tramite la funzione markdownify
di Hugo. In questo modo, l'autore è in grado di fornire markdown (e HTML) ma non è nemmeno obbligato a farlo.
{{ .Get "caption" | markdownify }}
Conclusione
Per le sue prestazioni e le sue numerose caratteristiche eccellenti, Hugo è attualmente una soluzione comoda per me quando si tratta di generazione di siti statici. Ma l'inclusione di codici brevi è ciò che trovo più avvincente. In questo caso, sono stato in grado di creare una semplice interfaccia per un problema di documentazione che stavo cercando di risolvere da tempo.
Come nei componenti Web, dietro gli shortcode può essere nascosta molta complessità di markup (a volte esacerbata dall'adeguamento per l'accessibilità). In questo caso, mi riferisco alla mia inclusione di role="group"
e alla relazione aria-labelledby
, che fornisce un'"etichetta di gruppo" meglio supportata alla <figure>
— non cose che a qualcuno piace codificare più di una volta, specialmente dove i valori di attributo univoci devono essere considerati in ogni istanza.
Credo che gli shortcode stiano a Markdown e content ciò che i componenti web stanno a HTML e funzionalità: un modo per rendere la paternità più semplice, più affidabile e più coerente. Non vedo l'ora di un'ulteriore evoluzione in questo curioso piccolo campo del web.
Risorse
- documentazione di Hugo
- "Modello di pacchetto", il linguaggio di programmazione Go
- "Codici brevi", Hugo
- "tutto" (proprietà abbreviata CSS), Mozilla Developer Network
- “iniziale (parola chiave CSS), Mozilla Developer Network
- "Shadow DOM v1: componenti Web autonomi", Eric Bidelman, Web Fundamentals, Google Developers
- "Introduzione agli elementi del modello", Eiji Kitamura, WebComponents.org
- "Include", Jekyll