Construirea de biblioteci de modele cu Shadow DOM în Markdown
Publicat: 2022-03-10Fluxul meu obișnuit de lucru folosind un procesor de text desktop merge cam așa:
- Selectați un text pe care vreau să-l copiez într-o altă parte a documentului.
- Rețineți că aplicația a selectat puțin mai mult sau mai puțin decât i-am spus.
- Încearcă din nou.
- Renunțați și hotărâți-vă să adăugați partea care lipsește (sau să eliminați partea suplimentară) din selecția dorită mai târziu.
- Copiați și lipiți selecția.
- Rețineți că formatarea textului lipit este oarecum diferită de cea originală.
- Încercați să găsiți presetarea de stil care se potrivește cu textul original.
- Încercați să aplicați presetarea.
- Renunțați și aplicați manual familia de fonturi și dimensiunea.
- Rețineți că există prea mult spațiu alb deasupra textului lipit și apăsați „Backspace” pentru a închide decalajul.
- Rețineți că textul în cauză și-a ridicat mai multe rânduri simultan, s-a alăturat textului titlului de deasupra lui și și-a adoptat stilul.
- Gândește-te la mortalitatea mea.
Când scrieți documentație web tehnică (a se citi: biblioteci de modele), procesoarele de text nu sunt doar neascultătoare, ci și inadecvate. În mod ideal, vreau un mod de scriere care să-mi permită să includ componentele pe care le documentez în linie, iar acest lucru nu este posibil decât dacă documentația în sine este făcută din HTML, CSS și JavaScript. În acest articol, voi împărtăși o metodă pentru includerea cu ușurință a demonstrațiilor de cod în Markdown, cu ajutorul codurilor scurte și a încapsulării shadow DOM.

CSS și Markdown
Spuneți ce vreți despre CSS, dar este cu siguranță un instrument de compunere mai consistent și mai fiabil decât orice editor WYSIWYG sau procesor de text de pe piață. De ce? Pentru că nu există un algoritm de tip cutie neagră de nivel înalt care să încerce să ghicească ce stiluri ai intenționat cu adevărat să ajungi unde. În schimb, este foarte explicit: definiți ce elemente iau ce stiluri în ce circumstanțe și respectă acele reguli.
Singura problemă cu CSS este că vă cere să scrieți omologul său, HTML. Chiar și marii iubitori de HTML ar admite probabil că scrierea manuală a acestuia este dificilă atunci când doriți doar să produceți conținut în proză. Aici intervine Markdown. Cu sintaxa sa concisă și setul de caracteristici redus, oferă un mod de scriere care este ușor de învățat, dar poate încă – odată convertit în HTML în mod programatic – să valorifice funcțiile puternice și previzibile de tipare CSS. Există un motiv pentru care a devenit formatul de facto pentru generatoarele de site-uri statice și platformele moderne de blogging, cum ar fi Ghost.
Acolo unde este necesară o markup mai complexă, personalizată, majoritatea analizoarelor Markdown vor accepta HTML brut în intrare. Cu toate acestea, cu cât se bazează mai mult pe un marcaj complex, cu atât sistemul de creație este mai puțin accesibil pentru cei care sunt mai puțin tehnici sau cei cu timp și răbdare. Aici intervin codurile scurte.
Shortcodes în Hugo
Hugo este un generator de site-uri static scris în Go - un limbaj multifuncțional, compilat, dezvoltat de Google. Datorită concurenței (și, fără îndoială, a altor caracteristici de limbaj de nivel scăzut pe care nu le înțeleg pe deplin), Go face din Hugo un generator de conținut web static foarte rapid. Acesta este unul dintre numeroasele motive pentru care Hugo a fost ales pentru noua versiune a Smashing Magazine.
Pe lângă performanță, funcționează într-un mod similar cu generatoarele bazate pe Ruby și Node.js cu care este posibil să fiți deja familiarizați: Markdown plus metadate (YAML sau TOML) procesate prin șabloane. Sara Soueidan a scris un prim excelent despre funcționalitatea de bază a lui Hugo.
Pentru mine, caracteristica ucigașă a lui Hugo este implementarea sa de coduri scurte. Cei care vin de la WordPress pot fi deja familiarizați cu conceptul: o sintaxă scurtată folosită în principal pentru includerea codurilor complexe de încorporare ale serviciilor terțe. De exemplu, WordPress include un cod scurt Vimeo care ia doar ID-ul videoclipului Vimeo în cauză.
[vimeo 44633289]
Parantezele semnifică faptul că conținutul lor ar trebui procesat ca un shortcode și extins în markup HTML complet încorporat atunci când conținutul este analizat.
Folosind funcțiile șablonului Go, Hugo oferă un API extrem de simplu pentru crearea de shortcodes personalizate. De exemplu, am creat un cod scurt Codepen simplu pentru a-l include printre conținutul meu 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 caută automat un șablon numit codePen.html
în subdosarul shortcodes
pentru a analiza shortcode-ul în timpul compilării. Implementarea mea arată astfel:
{{ 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 }}
Pentru a vă face o idee mai bună despre cum funcționează pachetul de șabloane Go, veți dori să consultați „Primul șablon Go” al lui Hugo. Între timp, rețineți următoarele:
- Este destul de nenorocit, dar totuși puternic.
- Partea
{{ .Get 0 }}
este pentru a prelua primul (și, în acest caz, singurul) argument furnizat - ID-ul Codepen. Hugo acceptă, de asemenea, argumente numite, care sunt furnizate ca atribute HTML. -
.
sintaxa se referă la contextul curent. Deci,.Get 0
înseamnă „Obțineți primul argument furnizat pentru codul scurt curent”.
În orice caz, cred că shortcode-urile sunt cel mai bun lucru de la shortbread, iar implementarea lui Hugo pentru scrierea shortcode-urilor personalizate este impresionantă. Ar trebui să remarc din cercetările mele că este posibil să utilizați Jekyll includes cu un efect similar, dar le consider mai puțin flexibile și puternice.
Demo de cod fără terți
Am mult timp pentru Codepen (și pentru celelalte locuri de joacă de cod disponibile), dar există probleme inerente cu includerea unui astfel de conținut într-o bibliotecă de modele:
- Utilizează un API, așa că nu poate fi ușor sau eficient făcut să funcționeze offline.
- Nu reprezintă doar modelul sau componenta; este propria sa interfață complexă, învelită în propriul branding. Acest lucru creează zgomot inutile și distragere a atenției atunci când accentul ar trebui să fie pe componentă.
De ceva timp, am încercat să încorporez demonstrații ale componentelor folosind propriile mele cadre iframe. Aș indica iframe-ul către un fișier local care conține demonstrația ca propria pagină web. Folosind iframe, am putut să încapsulez stilul și comportamentul fără a mă baza pe o terță parte.
Din păcate, cadrele iframe sunt destul de greoaie și dificil de redimensionat dinamic. În ceea ce privește complexitatea creației, aceasta implică, de asemenea, menținerea fișierelor separate și obligația de a le lega. Aș prefera să-mi scriu componentele la locul lor, inclusiv doar codul necesar pentru a le face să funcționeze. Vreau să pot scrie demonstrații în timp ce scriu documentația lor.
Shortcode- demo
Din fericire, Hugo vă permite să creați coduri scurte care includ conținut între deschiderea și închiderea etichetelor de coduri scurte. Conținutul este disponibil în fișierul shortcode folosind {{ .Inner }}
. Deci, să presupunem că ar trebui să folosesc un shortcode demo
ca acesta:
{{<demo>}} This is the content! {{</demo>}}
„Acesta este conținutul!” ar fi disponibil ca {{ .Inner }}
în șablonul demo.html
care îl analizează. Acesta este un bun punct de plecare pentru sprijinirea demonstrațiilor de cod inline, dar trebuie să abordez încapsularea.
Încapsularea stilului
Când vine vorba de încapsularea stilurilor, există trei lucruri de care să vă faceți griji:
- stilurile fiind moștenite de componentă din pagina părinte,
- pagina părinte moștenind stiluri de la componentă,
- stilurile fiind împărtășite neintenționat între componente.
O soluție este gestionarea cu atenție a selectoarelor CSS, astfel încât să nu existe suprapunere între componente și între componente și pagină. Acest lucru ar însemna folosirea selectoarelor ezoterice pe componentă și nu este ceva ce m-ar interesa să trebuiască să iau în considerare atunci când aș putea scrie cod concis și ușor de citit. Unul dintre avantajele iframe-urilor este că stilurile sunt încapsulate implicit, așa că aș putea scrie button { background: blue }
și să fiu sigur că se va aplica numai în interiorul iframe-ului.
O modalitate mai puțin intensivă de a preveni componentele să moștenească stiluri din pagină este de a folosi proprietatea all
cu valoarea initial
a unui element părinte ales. Pot seta acest element în fișierul demo.html
:
<div class="demo"> {{ .Inner }} </div>
Apoi, trebuie să aplic all: initial
la instanțe ale acestui element, care se propagă la copiii fiecărei instanțe.
.demo { all: initial }
Comportamentul initial
este destul de... idiosincratic. În practică, toate elementele afectate revin la adoptarea doar a stilurilor lor de agent utilizator (cum ar fi display: block
pentru elementele <h2>
). Cu toate acestea, elementul la care este aplicat — class=“demo”
— trebuie să aibă anumite stiluri de agent de utilizator reinstalate în mod explicit. În cazul nostru, acesta este doar display: block
, deoarece class=“demo”
este un <div>
.

.demo { all: initial; display: block; }
Notă: all
nu este acceptat până acum în Microsoft Edge, dar este în considerare. Suportul este, în caz contrar, liniștitor de larg. Pentru scopurile noastre, valoarea revert
ar fi mai robustă și mai fiabilă, dar nu este încă acceptată nicăieri.
Shadow DOM'ing The Shortcode
Utilizarea all: initial
nu face componentele noastre inline complet imune la influența exterioară (specificitatea încă se aplică), dar putem fi siguri că stilurile sunt nesetate deoarece avem de-a face cu numele clasei demo
rezervate. În mare parte, doar stilurile moștenite de la selectoare cu specificitate scăzută, cum ar fi html
și body
vor fi eliminate.
Cu toate acestea, aceasta se ocupă doar de stilurile care vin de la părinte în componente. Pentru a preveni ca stilurile scrise pentru componente să afecteze alte părți ale paginii, va trebui să folosim shadow DOM pentru a crea un subarboresc încapsulat.
Imaginați-vă că vreau să documentez un element <button>
cu stil. Aș dori să pot scrie pur și simplu ceva de genul următor, fără teama că selectorul de elemente de button
se va aplica elementelor <button>
din biblioteca de modele în sine sau în alte componente din aceeași pagină de bibliotecă.
{{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}
Trucul este să luați partea {{ .Inner }}
a șablonului de cod scurt și să o includeți ca ShadowRoot
innerHTML
Aș putea implementa asta așa:
{{ $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
este setat ca o variabilă pentru a identifica containerul de componente. Conectează unele funcții de șablon Go pentru a crea un șir unic... sperăm (!) — aceasta nu este o metodă antiglonț; este doar pentru ilustrare. -
root.attachShadow
face din containerul de componente o gazdă DOM umbră. - Am populat
innerHTML
alShadowRoot
folosind{{ .Inner }}
, care include CSS-ul acum încapsulat.
Permiterea comportamentului JavaScript
De asemenea, aș dori să includ comportamentul JavaScript în componentele mele. La început, am crezut că va fi ușor; Din păcate, JavaScript inserat prin innerHTML
nu este analizat sau executat. Acest lucru poate fi rezolvat prin importul din conținutul unui element <template>
. Mi-am modificat implementarea în consecință.
{{ $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>
Acum, pot include o demonstrație inline a, să zicem, un buton de comutare funcțional:
{{<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>}}
Notă: am scris în detaliu despre butoanele de comutare și accesibilitatea pentru componentele inclusive.
Încapsulare JavaScript
Spre surprinderea mea, JavaScript nu este încapsulat automat, așa cum CSS este în DOM umbră. Adică, dacă a existat un alt buton [aria-pressed]
în pagina părinte înainte de exemplul acestei componente, atunci document.querySelector
l-ar viza în schimb.
Ceea ce am nevoie este un echivalent cu document
doar pentru subarborele demo-ului. Acest lucru este definibil, deși destul de pronunțat:
document.getElementById('demo-{{ $uniq }}').shadowRoot;
Nu am vrut să fiu nevoit să scriu această expresie ori de câte ori a trebuit să țintesc elemente în interiorul containerelor demo. Așadar, am venit cu un hack prin care am atribuit expresia unei variabile demo
locale și scripturi prefixate furnizate prin codul scurt cu această atribuire:
if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));
Odată cu acest lucru, demo
devine echivalentul document
pentru orice subarboresc al componentelor și pot folosi demo.querySelector
pentru a-mi viza cu ușurință butonul de comutare.
var toggle = demo.querySelector('[aria-pressed]');
Rețineți că am inclus conținutul scriptului demo-ului într-o expresie a funcției imediat invocată (IIFE), astfel încât variabila demo
- și toate variabilele de procedură utilizate pentru componentă - nu sunt în domeniul global. În acest fel, demo
poate fi folosită în scriptul oricărui cod scurt, dar se va referi doar la codul scurt în mână.
Acolo unde ECMAScript6 este disponibil, este posibil să se realizeze localizarea folosind „bloc scoping”, cu doar acolade care includ instrucțiunile let
sau const
. Cu toate acestea, toate celelalte definiții din bloc ar trebui să folosească și let
sau const
(evitând var
).
{ let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }
Suport pentru Shadow DOM
Desigur, toate cele de mai sus sunt posibile numai acolo unde shadow DOM versiunea 1 este acceptată. Chrome, Safari, Opera și Android arată toate destul de bine, dar browserele Firefox și Microsoft sunt problematice. Este posibil să detectați funcții de asistență și să furnizați un mesaj de eroare în cazul în care attachShadow
nu este disponibil:
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'; }
Sau puteți include Shady DOM și extensia Shady CSS, ceea ce înseamnă o dependență oarecum mare (60 KB+) și un API diferit. Rob Dodson a fost destul de amabil să-mi ofere o demonstrație de bază, pe care sunt bucuros să o împărtășesc pentru a vă ajuta să începeți.
Legende pentru componente
Cu funcționalitatea de bază a demonstrației inline, scrierea rapidă a demonstrațiilor de lucru în conformitate cu documentația lor este din fericire simplă. Acest lucru ne oferă luxul de a putea pune întrebări de genul „Dar dacă vreau să ofer o legendă pentru a eticheta demonstrația?” Acest lucru este deja perfect posibil, deoarece – după cum sa menționat anterior – Markdown acceptă HTML brut.
<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>
Cu toate acestea, singura parte nouă a acestei structuri modificate este formularea titlului în sine. Mai bine să ofer o interfață simplă pentru furnizarea acesteia la ieșire, salvându-mi viitorul meu – și oricine altcineva care folosește codul scurt – timp și efort și reducând riscul greșelilor de tipar. Acest lucru este posibil prin furnizarea unui parametru numit codului scurt - în acest caz, caption
numită simplu:
{{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}
Parametrii numiți sunt accesibili în șablon, cum ar fi {{ .Get "caption" }}
, ceea ce este destul de simplu. Vreau ca legenda și, prin urmare, <figure>
și <figcaption>
din jur să fie opționale. Folosind clauze if
, pot furniza conținutul relevant numai în cazul în care shortcode-ul oferă un argument subtitrare:
{{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}
Iată cum arată acum șablonul demo.html
complet (desigur, este un pic cam dezordonat, dar funcționează):
{{ $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>
O ultimă notă: dacă vreau să accept sintaxa markdownify
în valoarea subtitrării, o pot trece prin funcția Markdownify a lui Hugo. În acest fel, autorul poate furniza markdown (și HTML), dar nu este forțat să facă nici unul.
{{ .Get "caption" | markdownify }}
Concluzie
Pentru performanța și numeroasele sale caracteristici excelente, Hugo este în prezent o potrivire confortabilă pentru mine atunci când vine vorba de generarea de site-uri statice. Dar includerea de coduri scurte este ceea ce mi se pare cel mai convingător. În acest caz, am reușit să creez o interfață simplă pentru o problemă de documentare pe care am încercat să o rezolv de ceva timp.
Ca și în componentele web, o mulțime de complexitate a marcajului (uneori exacerbată prin ajustarea pentru accesibilitate) poate fi ascunsă în spatele codurilor scurte. În acest caz, mă refer la includerea mea a role="group"
și a relației aria-labelledby
, care oferă o „etichetă de grup" mai bine acceptată pentru <figure>
- nu lucruri pe care oricine le place să le codifice de mai multe ori, în special unde valorile unice ale atributelor trebuie luate în considerare în fiecare caz.
Cred că shortcode-urile sunt pentru Markdown și conținut ceea ce sunt componentele web pentru HTML și funcționalitate: o modalitate de a face mai ușor, mai fiabil și mai consecvent calitatea de autor. Aștept cu nerăbdare evoluția ulterioară în acest mic domeniu curios al web-ului.
Resurse
- documentație Hugo
- „Șablon de pachet”, limbajul de programare Go
- „Coduri scurte”, Hugo
- „toate” (proprietate scurtă CSS), Mozilla Developer Network
- „inițial (cuvânt cheie CSS), Mozilla Developer Network
- „Shadow DOM v1: Componente web autonome”, Eric Bidelman, Web Fundamentals, Google Developers
- „Introducere în elementele șablonului”, Eiji Kitamura, WebComponents.org
- „Include”, Jekyll