Erstellen von Musterbibliotheken mit Shadow DOM in Markdown
Veröffentlicht: 2022-03-10Mein typischer Arbeitsablauf mit einem Desktop-Textverarbeitungsprogramm sieht ungefähr so aus:
- Wählen Sie einen Text aus, den ich in einen anderen Teil des Dokuments kopieren möchte.
- Beachten Sie, dass die Anwendung etwas mehr oder weniger ausgewählt hat, als ich ihr gesagt habe.
- Versuchen Sie es nochmal.
- Geben Sie auf und beschließen Sie, den fehlenden Teil meiner beabsichtigten Auswahl später hinzuzufügen (oder den zusätzlichen Teil zu entfernen).
- Kopieren Sie die Auswahl und fügen Sie sie ein.
- Beachten Sie, dass sich die Formatierung des eingefügten Textes irgendwie vom Original unterscheidet.
- Versuchen Sie, die Stilvorgabe zu finden, die dem Originaltext entspricht.
- Versuchen Sie, die Voreinstellung anzuwenden.
- Geben Sie auf und wenden Sie die Schriftfamilie und -größe manuell an.
- Beachten Sie, dass über dem eingefügten Text zu viel Leerraum vorhanden ist, und drücken Sie die „Rücktaste“, um die Lücke zu schließen.
- Beachten Sie, dass sich der betreffende Text um mehrere Zeilen auf einmal erhöht hat, sich mit dem darüber liegenden Überschriftentext verbunden und dessen Stil übernommen hat.
- Denke über meine Sterblichkeit nach.
Beim Schreiben von technischer Webdokumentation (sprich: Musterbibliotheken) sind Textverarbeitungen nicht nur ungehorsam, sondern unangemessen. Idealerweise möchte ich eine Schreibweise, die es mir erlaubt, die Komponenten, die ich dokumentiere, inline einzufügen, und das ist nicht möglich, es sei denn, die Dokumentation selbst besteht aus HTML, CSS und JavaScript. In diesem Artikel teile ich eine Methode zum einfachen Einbinden von Code-Demos in Markdown mit Hilfe von Shortcodes und Shadow-DOM-Kapselung.
CSS und Markdown
Über CSS können Sie sagen, was Sie wollen, aber es ist sicherlich ein konsistenteres und zuverlässigeres Satzwerkzeug als jeder WYSIWYG-Editor oder jede Textverarbeitung auf dem Markt. Warum? Weil es keinen High-Level-Blackbox-Algorithmus gibt, der versucht, im Nachhinein zu erraten, welche Stile Sie wirklich wohin bringen wollten. Stattdessen ist es sehr explizit: Sie definieren, welche Elemente unter welchen Umständen welchen Stil annehmen, und es respektiert diese Regeln.
Das einzige Problem mit CSS ist, dass Sie dafür das Gegenstück HTML schreiben müssen. Selbst große Liebhaber von HTML würden wahrscheinlich zugeben, dass das manuelle Schreiben auf der mühsamen Seite ist, wenn Sie nur Prosa-Inhalte erstellen möchten. Hier kommt Markdown ins Spiel. Mit seiner knappen Syntax und dem reduzierten Funktionsumfang bietet es eine Schreibweise, die leicht zu erlernen ist, aber dennoch – einmal programmgesteuert in HTML konvertiert – die leistungsstarken und vorhersagbaren Satzfunktionen von CSS nutzen kann. Es hat einen Grund, warum es zum De-facto -Format für statische Website-Generatoren und moderne Blogging-Plattformen wie Ghost geworden ist.
Wenn komplexeres, maßgeschneidertes Markup erforderlich ist, akzeptieren die meisten Markdown-Parser rohes HTML in der Eingabe. Je mehr man sich jedoch auf komplexes Markup verlässt, desto weniger zugänglich ist das eigene Autorensystem für diejenigen, die weniger technisch versiert sind oder wenig Zeit und Geduld haben. Hier kommen Shortcodes ins Spiel.
Shortcodes in Hugo
Hugo ist ein statischer Website-Generator, der in Go geschrieben ist – einer von Google entwickelten kompilierten Mehrzwecksprache. Aufgrund der Nebenläufigkeit (und zweifellos anderer Low-Level-Sprachfunktionen, die ich nicht vollständig verstehe) macht Go Hugo zu einem blitzschnellen Generator statischer Webinhalte. Dies ist einer der vielen Gründe, warum Hugo für die neue Version des Smashing Magazine ausgewählt wurde.
Abgesehen von der Leistung funktioniert es ähnlich wie die Ruby- und Node.js-basierten Generatoren, die Sie vielleicht bereits kennen: Markdown plus Metadaten (YAML oder TOML), die über Vorlagen verarbeitet werden. Sara Soueidan hat eine hervorragende Einführung in die Kernfunktionalität von Hugo geschrieben.
Für mich ist das Killer-Feature von Hugo die Implementierung von Shortcodes. Wer von WordPress kommt, kennt das Konzept vielleicht schon: eine verkürzte Syntax, die vor allem zum Einbinden der komplexen Einbettungscodes von Drittanbieterdiensten verwendet wird. Zum Beispiel enthält WordPress einen Vimeo-Shortcode, der nur die ID des betreffenden Vimeo-Videos verwendet.
[vimeo 44633289]
Die Klammern bedeuten, dass ihr Inhalt als Shortcode verarbeitet und in das vollständige HTML-Einbettungs-Markup erweitert werden sollte, wenn der Inhalt geparst wird.
Unter Verwendung der Go-Vorlagenfunktionen bietet Hugo eine extrem einfache API zum Erstellen benutzerdefinierter Shortcodes. Zum Beispiel habe ich einen einfachen Codepen-Shortcode erstellt, der in meinen Markdown-Inhalt aufgenommen werden soll:
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 sucht automatisch nach einer Vorlage namens codePen.html
im shortcodes
-Unterordner, um den Shortcode während der Kompilierung zu parsen. Meine Implementierung sieht so aus:
{{ 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 }}
Um eine bessere Vorstellung davon zu bekommen, wie das Go-Vorlagenpaket funktioniert, sollten Sie Hugos „Go Template Primer“ konsultieren. Beachten Sie in der Zwischenzeit einfach Folgendes:
- Es ist ziemlich flüchtig, aber dennoch mächtig.
- Der Teil
{{ .Get 0 }}
dient zum Abrufen des ersten (und in diesem Fall einzigen) Arguments – der Codepen-ID. Hugo unterstützt auch benannte Argumente, die wie HTML-Attribute bereitgestellt werden. - Die
.
Syntax bezieht sich auf den aktuellen Kontext..Get 0
bedeutet also „Erstes für den aktuellen Shortcode bereitgestelltes Argument abrufen“.
Auf jeden Fall denke ich, dass Shortcodes das Beste seit Shortbread sind, und Hugos Implementierung zum Schreiben von benutzerdefinierten Shortcodes ist beeindruckend. Ich sollte aus meinen Recherchen anmerken, dass es möglich ist, Jekyll-Includes mit ähnlichem Effekt zu verwenden, aber ich finde sie weniger flexibel und leistungsfähig.
Code-Demos ohne Drittanbieter
Ich habe viel Zeit für Codepen (und die anderen verfügbaren Code-Spielplätze), aber es gibt inhärente Probleme mit der Aufnahme solcher Inhalte in eine Musterbibliothek:
- Es verwendet eine API, so dass es nicht einfach oder effizient gemacht werden kann, offline zu arbeiten.
- Es stellt nicht nur das Muster oder die Komponente dar; Es ist eine eigene komplexe Schnittstelle, die in ein eigenes Branding gehüllt ist. Dies erzeugt unnötigen Lärm und Ablenkung, wenn der Fokus auf dem Bauteil liegen sollte.
Ich habe einige Zeit versucht, Komponentendemos mit meinen eigenen Iframes einzubetten. Ich würde den Iframe auf eine lokale Datei verweisen, die die Demo als eigene Webseite enthält. Durch die Verwendung von Iframes konnte ich Stil und Verhalten kapseln, ohne mich auf Dritte verlassen zu müssen.
Leider sind Iframes ziemlich unhandlich und lassen sich nur schwer dynamisch in der Größe anpassen. In Bezug auf die Komplexität des Authorings bedeutet dies auch, separate Dateien zu verwalten und mit ihnen zu verknüpfen. Ich würde es vorziehen, meine Komponenten an Ort und Stelle zu schreiben, einschließlich nur des Codes, der benötigt wird, damit sie funktionieren. Ich möchte in der Lage sein, Demos zu schreiben, während ich ihre Dokumentation schreibe.
Der demo
-Shortcode
Glücklicherweise können Sie mit Hugo Shortcodes erstellen, die Inhalte zwischen öffnenden und schließenden Shortcode-Tags enthalten. Der Inhalt ist in der Shortcode-Datei mit {{ .Inner }}
verfügbar. Angenommen, ich würde einen demo
-Shortcode wie diesen verwenden:
{{<demo>}} This is the content! {{</demo>}}
„Das ist der Inhalt!“ wäre als {{ .Inner }}
in der demo.html
Vorlage verfügbar, die es analysiert. Dies ist ein guter Ausgangspunkt für die Unterstützung von Inline-Code-Demos, aber ich muss mich mit der Kapselung befassen.
Stilkapselung
Wenn es um die Einkapselung von Stilen geht, gibt es drei Dinge, über die Sie sich Sorgen machen müssen:
- Stile, die von der Komponente von der übergeordneten Seite geerbt werden,
- die übergeordnete Seite, die Stile von der Komponente erbt,
- Stile, die unbeabsichtigt zwischen Komponenten geteilt werden.
Eine Lösung besteht darin, CSS-Selektoren sorgfältig zu verwalten, sodass es keine Überschneidungen zwischen Komponenten und zwischen Komponenten und der Seite gibt. Dies würde bedeuten, esoterische Selektoren pro Komponente zu verwenden, und es ist nichts, was mich interessieren würde, wenn ich knappen, lesbaren Code schreiben könnte. Einer der Vorteile von Iframes besteht darin, dass Stile standardmäßig gekapselt sind, sodass ich button { background: blue }
schreiben und sicher sein kann, dass es nur innerhalb des Iframes angewendet wird.
Eine weniger intensive Methode, um zu verhindern, dass Komponenten Stile von der Seite erben, besteht darin, die Eigenschaft all
mit dem initial
für ein ausgewähltes übergeordnetes Element zu verwenden. Ich kann dieses Element in der Datei demo.html
:
<div class="demo"> {{ .Inner }} </div>
Dann muss ich all: initial
auf Instanzen dieses Elements anwenden, das an die untergeordneten Elemente jeder Instanz weitergegeben wird.
.demo { all: initial }
Das Verhalten von initial
ist ziemlich… eigenwillig. In der Praxis nehmen alle betroffenen Elemente wieder nur ihre User-Agent-Stile an (wie display: block
für <h2>
-Elemente). Für das Element, auf das es angewendet wird – class=“demo”
– müssen jedoch bestimmte Benutzeragentenstile explizit wiederhergestellt werden. In unserem Fall ist dies nur display: block
, da class=“demo”
ein <div>
ist.
.demo { all: initial; display: block; }
Hinweis: all
wird in Microsoft Edge bisher nicht unterstützt, wird aber in Erwägung gezogen. Die Unterstützung ist ansonsten beruhigend breit. Für unsere Zwecke wäre der revert
Wert robuster und zuverlässiger, wird aber noch nirgendwo unterstützt.
Shadow DOM'ing The Shortcode
Die Verwendung von all: initial
macht unsere Inline-Komponenten nicht vollständig immun gegen äußere Einflüsse (Spezifität gilt immer noch), aber wir können sicher sein, dass Stile nicht festgelegt sind, da wir es mit dem reservierten demo
-Klassennamen zu tun haben. Meistens werden nur geerbte Stile von Selektoren mit geringer Spezifität wie html
und body
eliminiert.
Trotzdem behandelt dies nur Stile, die vom Elternteil in die Komponenten kommen. Um zu verhindern, dass Stile, die für Komponenten geschrieben wurden, andere Teile der Seite beeinflussen, müssen wir Shadow-DOM verwenden, um einen gekapselten Teilbaum zu erstellen.
Stellen Sie sich vor, ich möchte ein formatiertes <button>
-Element dokumentieren. Ich möchte in der Lage sein, einfach so etwas wie das Folgende zu schreiben, ohne befürchten zu müssen, dass die button
auf <button>
-Elemente in der Musterbibliothek selbst oder in anderen Komponenten auf derselben Bibliotheksseite angewendet wird.
{{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}
Der Trick besteht darin, den Teil {{ .Inner }}
der Shortcode-Vorlage zu nehmen und ihn als innerHTML
eines neuen ShadowRoot
. Ich könnte das so umsetzen:
{{ $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
wird als Variable gesetzt, um den Komponentencontainer zu identifizieren. Es leitet einige Go-Vorlagenfunktionen ein, um eine eindeutige Zeichenfolge zu erstellen … hoffentlich (!) – dies ist keine kugelsichere Methode; es ist nur zur Veranschaulichung. -
root.attachShadow
macht den Komponentencontainer zu einem Schatten-DOM-Host. - Ich
innerHTML
das innere HTML vonShadowRoot
mit{{ .Inner }}
, das das jetzt gekapselte CSS enthält.
JavaScript-Verhalten zulassen
Ich möchte auch JavaScript-Verhalten in meine Komponenten einbeziehen. Zuerst dachte ich, das wäre einfach; Leider wird über innerHTML
eingefügtes JavaScript nicht geparst oder ausgeführt. Dies kann durch Importieren aus dem Inhalt eines <template>
-Elements gelöst werden. Ich habe meine Implementierung entsprechend angepasst.
{{ $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>
Jetzt kann ich eine Inline-Demo von beispielsweise einer funktionierenden Umschaltfläche einfügen:
{{<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>}}
Hinweis: Ich habe ausführlich über Umschaltflächen und Barrierefreiheit für inklusive Komponenten geschrieben.
JavaScript-Kapselung
JavaScript wird zu meiner Überraschung nicht automatisch gekapselt wie CSS im Schatten-DOM. Das heißt, wenn auf der übergeordneten Seite vor dem Beispiel dieser Komponente eine weitere [aria-pressed]
-Schaltfläche vorhanden wäre, würde document.querySelector
stattdessen darauf abzielen.
Was ich brauche, ist ein Äquivalent zum document
nur für den Teilbaum der Demo. Dies ist definierbar, wenn auch ziemlich ausführlich:
document.getElementById('demo-{{ $uniq }}').shadowRoot;
Ich wollte diesen Ausdruck nicht immer dann schreiben müssen, wenn ich auf Elemente innerhalb von Demo-Containern abzielen musste. Also habe ich mir einen Hack ausgedacht, bei dem ich den Ausdruck einer lokalen demo
-Variablen zugewiesen und Skripten vorangestellt habe, die über den Shortcode mit dieser Zuweisung bereitgestellt werden:
if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));
Wenn dies vorhanden ist, wird demo
zum Äquivalent von document
für alle Komponenten-Teilbäume, und ich kann demo.querySelector
verwenden, um meine Umschaltfläche einfach anzusprechen.
var toggle = demo.querySelector('[aria-pressed]');
Beachten Sie, dass ich den Inhalt des Demo-Skripts in einen sofort aufgerufenen Funktionsausdruck (IIFE) eingeschlossen habe, sodass die demo
-Variable – und alle für die Komponente verwendeten Variablen – nicht im globalen Gültigkeitsbereich liegen. Auf diese Weise kann demo
in jedem Shortcode-Skript verwendet werden, bezieht sich jedoch nur auf den jeweiligen Shortcode.
Wo ECMAScript6 verfügbar ist, ist es möglich, die Lokalisierung mithilfe von „Block Scoping“ zu erreichen, wobei let
oder const
-Anweisungen nur in geschweifte Klammern eingeschlossen werden. Alle anderen Definitionen innerhalb des Blocks müssten jedoch auch let
oder const
verwenden (ohne var
).
{ let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }
Shadow-DOM-Unterstützung
All dies ist natürlich nur möglich, wenn Shadow DOM Version 1 unterstützt wird. Chrome, Safari, Opera und Android sehen alle ziemlich gut aus, aber Firefox und Microsoft-Browser sind problematisch. Es ist möglich, die Unterstützung von Funktionen zu erkennen und eine Fehlermeldung bereitzustellen, wenn attachShadow
nicht verfügbar ist:
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'; }
Oder Sie können Shady DOM und die Shady CSS-Erweiterung einbinden, was eine etwas große Abhängigkeit (60 KB+) und eine andere API bedeutet. Rob Dodson war so freundlich, mir eine einfache Demo zur Verfügung zu stellen, die ich gerne mit Ihnen teile, um Ihnen den Einstieg zu erleichtern.
Beschriftungen für Komponenten
Mit der grundlegenden Inline-Demo-Funktionalität ist das schnelle Schreiben von funktionierenden Demos inline mit ihrer Dokumentation glücklicherweise unkompliziert. Dies bietet uns den Luxus, Fragen stellen zu können wie: „Was ist, wenn ich eine Bildunterschrift zur Kennzeichnung der Demo bereitstellen möchte?“ Dies ist bereits möglich, da – wie bereits erwähnt – Markdown rohes HTML unterstützt.
<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>
Der einzige neue Teil dieser geänderten Struktur ist jedoch der Wortlaut der Bildunterschrift selbst. Es ist besser, eine einfache Schnittstelle für die Bereitstellung an der Ausgabe bereitzustellen, um meinem zukünftigen Ich – und allen anderen, die den Shortcode verwenden – Zeit und Mühe zu ersparen und das Risiko von Codierungsfehlern zu verringern. Dies ist möglich, indem dem Shortcode ein benannter Parameter übergeben wird – in diesem Fall einfach benannt caption
:
{{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}
Auf benannte Parameter kann in der Vorlage wie {{ .Get "caption" }}
werden, was einfach genug ist. Ich möchte, dass die Beschriftung und damit die umgebenden <figure>
und <figcaption>
optional sind. Mit if
-Klauseln kann ich den relevanten Inhalt nur dann bereitstellen, wenn der Shortcode ein Untertitel-Argument bereitstellt:
{{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}
So sieht die vollständige demo.html
Vorlage jetzt aus (zugegeben, es ist ein bisschen chaotisch, aber es funktioniert):
{{ $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>
Eine letzte Anmerkung: Sollte ich die Markdown-Syntax im Beschriftungswert unterstützen wollen, kann ich sie durch Hugos markdownify
-Funktion leiten. Auf diese Weise kann der Autor Markdown (und HTML) bereitstellen, ist aber nicht dazu gezwungen.
{{ .Get "caption" | markdownify }}
Fazit
Aufgrund seiner Leistung und seiner vielen hervorragenden Funktionen passt Hugo derzeit gut zu mir, wenn es um die Generierung statischer Websites geht. Aber die Aufnahme von Shortcodes finde ich am überzeugendsten. In diesem Fall konnte ich eine einfache Schnittstelle für ein Dokumentationsproblem erstellen, das ich seit einiger Zeit zu lösen versuche.
Wie bei Webkomponenten kann sich hinter Shortcodes viel Markup-Komplexität verbergen (die manchmal durch Anpassungen an die Zugänglichkeit noch verschärft wird). In diesem Fall beziehe ich mich auf meine Einbeziehung von role="group"
und der aria-labelledby
Beziehung, die der <figure>
eine besser unterstützte "Gruppenbezeichnung" verleiht – besonders Dinge, die niemand gerne mehr als einmal codiert wobei eindeutige Attributwerte in jedem Fall berücksichtigt werden müssen.
Ich glaube, Shortcodes sind für Markdown und Content das, was Webkomponenten für HTML und Funktionalität sind: eine Möglichkeit, die Autorenschaft einfacher, zuverlässiger und konsistenter zu machen. Ich freue mich auf die weitere Entwicklung in diesem merkwürdigen kleinen Bereich des Internets.
Ressourcen
- Hugo-Dokumentation
- „Paketvorlage“, die Go-Programmiersprache
- „Shortcodes“, Hugo
- „all“ (CSS-Kurzschrifteigenschaft), Mozilla Developer Network
- „initial (CSS-Schlüsselwort), Mozilla Developer Network
- „Shadow DOM v1: Eigenständige Webkomponenten“, Eric Bidelman, Web Fundamentals, Google Developers
- „Einführung in Vorlagenelemente“, Eiji Kitamura, WebComponents.org
- „Beinhaltet“, Jekyll