Budowanie bibliotek wzorców z Shadow DOM w Markdown

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Niektórzy ludzie nienawidzą pisania dokumentacji, a inni po prostu nienawidzą pisania. Tak się składa, że ​​uwielbiam pisać; w przeciwnym razie nie czytałbyś tego. Pomaga mi to, że uwielbiam pisać, ponieważ jako konsultantka ds. projektowania oferująca profesjonalne doradztwo, pisanie jest dużą częścią tego, co robię. Ale nienawidzę, nienawidzę, nienawidzę edytorów tekstu. Podczas pisania technicznej dokumentacji internetowej (czytaj: biblioteki wzorców) edytory tekstu są nie tylko nieposłuszne, ale wręcz nieodpowiednie. Najlepiej byłoby, gdybym potrzebował trybu pisania, który pozwalałby mi dołączyć komponenty, które dokumentuję, a nie jest to możliwe, chyba że sama dokumentacja jest wykonana z HTML, CSS i JavaScript. W tym artykule przedstawię metodę łatwego włączania demonstracji kodu w Markdown, za pomocą skrótów i enkapsulacji shadow DOM.

Mój typowy przepływ pracy z komputerowym edytorem tekstu wygląda mniej więcej tak:

  1. Zaznacz tekst, który chcę skopiować do innej części dokumentu.
  2. Zauważ, że aplikacja wybrała nieco więcej lub mniej niż kazałem.
  3. Spróbuj ponownie.
  4. Zrezygnuj i postanów dodać brakującą część (lub usunąć dodatkową część) mojego zamierzonego wyboru później.
  5. Skopiuj i wklej zaznaczenie.
  6. Zwróć uwagę, że formatowanie wklejonego tekstu różni się w jakiś sposób od oryginału.
  7. Spróbuj znaleźć ustawienie stylizacji, które pasuje do oryginalnego tekstu.
  8. Spróbuj zastosować ustawienie wstępne.
  9. Zrezygnuj i ręcznie zastosuj rodzinę i rozmiar czcionek.
  10. Zwróć uwagę, że nad wklejonym tekstem jest zbyt dużo białego miejsca i naciśnij „Backspace”, aby zamknąć lukę.
  11. Zauważ, że tekst, o którym mowa, podniósł się o kilka linijek naraz, dołączył do tekstu nagłówka nad nim i przyjął jego stylizację.
  12. Zastanów się nad moją śmiertelnością.

Podczas pisania technicznej dokumentacji internetowej (czytaj: biblioteki wzorców) edytory tekstu są nie tylko nieposłuszne, ale wręcz nieodpowiednie. Najlepiej byłoby, gdybym potrzebował trybu pisania, który pozwalałby mi dołączyć komponenty, które dokumentuję, i nie jest to możliwe, chyba że sama dokumentacja składa się z HTML, CSS i JavaScript. W tym artykule przedstawię metodę łatwego włączania demonstracji kodu w Markdown, za pomocą skrótów i enkapsulacji shadow DOM.

M, strzałka w dół plus detektyw ukryty w ciemności symbolizujący Markdown i Shadown Dom
Więcej po skoku! Kontynuuj czytanie poniżej ↓

CSS i przeceny

Mów, co chcesz o CSS, ale z pewnością jest to bardziej spójne i niezawodne narzędzie do składu niż jakikolwiek edytor WYSIWYG lub edytor tekstu na rynku. Czemu? Ponieważ nie ma wysokopoziomowego algorytmu czarnej skrzynki, który próbuje odgadnąć, jakie style naprawdę zamierzałeś i gdzie. Zamiast tego jest to bardzo jednoznaczne: określasz, które elementy przybierają jakie style w jakich okolicznościach, i przestrzega tych zasad.

Jedyny problem z CSS polega na tym, że wymaga napisania swojego odpowiednika, HTML. Nawet wielcy miłośnicy HTML prawdopodobnie przyznają, że ręczne pisanie jest uciążliwe, gdy chcesz po prostu tworzyć treści prozatorskie. W tym miejscu wkracza Markdown. Dzięki zwięzłej składni i zredukowanemu zestawowi funkcji oferuje tryb pisania, który jest łatwy do nauczenia, ale nadal może — po programowej konwersji na HTML — wykorzystać potężne i przewidywalne funkcje CSS. Nie bez powodu stał się de facto formatem dla generatorów statycznych stron internetowych i nowoczesnych platform blogowych, takich jak Ghost.

Tam, gdzie wymagane są bardziej złożone, spersonalizowane znaczniki, większość parserów Markdown zaakceptuje surowy kod HTML w danych wejściowych. Jednak im bardziej ktoś polega na złożonych znacznikach, tym mniej dostępny jest system autorski dla tych, którzy są mniej techniczni lub mają mało czasu i cierpliwości. Tutaj pojawiają się skróty.

Skróty w Hugo

Hugo to statyczny generator witryn napisany w Go — uniwersalnym, skompilowanym języku opracowanym przez Google. Ze względu na współbieżność (i bez wątpienia inne funkcje języka niskiego poziomu, których w pełni nie rozumiem), Go sprawia, że ​​Hugo jest błyskawicznym generatorem statycznych treści internetowych. To jeden z wielu powodów, dla których Hugo został wybrany do nowej wersji Smashing Magazine.

Pomijając wydajność, działa w podobny sposób do generatorów opartych na Ruby i Node.js, które być może już znasz: Markdown plus metadane (YAML lub TOML) przetwarzane za pomocą szablonów. Sara Soueidan napisała doskonały podkład na temat podstawowej funkcjonalności Hugo.

Dla mnie zabójczą funkcją Hugo jest implementacja skrótów. Ci, którzy wywodzą się z WordPressa, mogą już znać tę koncepcję: skróconą składnię używaną głównie do dołączania złożonych kodów do osadzania usług stron trzecich. Na przykład WordPress zawiera krótki kod Vimeo, który pobiera tylko identyfikator danego filmu Vimeo.

 [vimeo 44633289]

Nawiasy oznaczają, że ich treść powinna być przetwarzana jako krótki kod i rozszerzona do pełnego kodu HTML, gdy treść jest analizowana.

Korzystając z funkcji szablonów Go, Hugo zapewnia niezwykle prosty interfejs API do tworzenia niestandardowych krótkich kodów. Na przykład stworzyłem prosty shortcode Codepen, aby uwzględnić w mojej zawartości 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 automatycznie szuka szablonu o nazwie codePen.html w podfolderze shortcodes , aby przeanalizować krótki kod podczas kompilacji. Moja implementacja wygląda tak:

 {{ 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 }}

Aby lepiej zrozumieć, jak działa pakiet szablonów Go, zapoznaj się z „Go Template Primer” firmy Hugo. W międzyczasie pamiętaj tylko o następujących kwestiach:

  • Jest dość brzydki, ale mimo to potężny.
  • Część {{ .Get 0 }} służy do pobrania pierwszego (i w tym przypadku jedynego) dostarczonego argumentu — identyfikatora Codepen. Hugo obsługuje również nazwane argumenty, które są dostarczane jak atrybuty HTML.
  • . składnia odnosi się do bieżącego kontekstu. Tak więc .Get 0 oznacza „Pobierz pierwszy argument dostarczony dla bieżącego kodu skrótu”.

W każdym razie uważam, że shortcodes są najlepszą rzeczą od czasu shortbread, a implementacja Hugo do pisania niestandardowych shortcodes jest imponująca. Powinienem zauważyć z moich badań, że możliwe jest użycie Jekyll zawiera podobny efekt, ale uważam, że są one mniej elastyczne i mocne.

Demonstracje kodu bez stron trzecich

Mam dużo czasu na Codepen (i inne dostępne place zabaw), ale są nieodłączne problemy z umieszczaniem takiej zawartości w bibliotece wzorców:

  • Wykorzystuje interfejs API, więc nie można go łatwo i efektywnie wykorzystać do pracy w trybie offline.
  • Nie tylko reprezentuje wzór lub komponent; jest to własny, złożony interfejs owinięty własnym brandingiem. Powoduje to niepotrzebny hałas i rozproszenie uwagi, gdy uwaga powinna być skupiona na komponencie.

Przez jakiś czas próbowałem osadzać dema komponentów przy użyciu własnych ramek iframe. Wskażę iframe do lokalnego pliku zawierającego demo jako własną stronę internetową. Korzystając z ramek iframe, mogłem zawrzeć styl i zachowanie bez polegania na osobach trzecich.

Niestety ramki iframe są raczej nieporęczne i trudne do dynamicznego zmieniania rozmiaru. Jeśli chodzi o złożoność tworzenia, wiąże się to również z utrzymywaniem oddzielnych plików i koniecznością linkowania do nich. Wolałbym pisać moje komponenty w miejscu, w tym tylko kod potrzebny do ich działania. Chcę mieć możliwość pisania dem, kiedy piszę ich dokumentację.

demo Shortcode

Na szczęście Hugo umożliwia tworzenie krótkich kodów, które zawierają treść między otwieraniem i zamykaniem tagów shortcode. Treść jest dostępna w pliku shortcode za pomocą {{ .Inner }} . Załóżmy więc, że miałbym użyć takiego krótkiego kodu demo :

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

„To jest treść!” będzie dostępny jako {{ .Inner }} w szablonie demo.html , który go analizuje. To dobry punkt wyjścia do obsługi demonstracji kodu wbudowanego, ale muszę zająć się enkapsulacją.

Enkapsulacja stylu

Jeśli chodzi o enkapsulację stylów, należy się martwić o trzy rzeczy:

  • style dziedziczone przez komponent ze strony nadrzędnej,
  • strona nadrzędna dziedzicząca style z komponentu,
  • style nieumyślnie współdzielone między komponentami.

Jednym z rozwiązań jest ostrożne zarządzanie selektorami CSS, tak aby nie zachodziły nakładanie się komponentów oraz pomiędzy komponentami i stroną. Oznaczałoby to użycie ezoterycznych selektorów dla każdego komponentu i nie jest to coś, o czym byłbym zainteresowany, gdy mógłbym pisać zwięzły, czytelny kod. Jedną z zalet ramek iframe jest to, że style są domyślnie enkapsulowane, więc mogę napisać button { background: blue } i mieć pewność, że będzie on stosowany tylko wewnątrz ramki iframe.

Mniej intensywnym sposobem zapobiegania dziedziczeniu stylów przez komponenty ze strony jest użycie właściwości all z wartością initial na wybranym elemencie nadrzędnym. Mogę ustawić ten element w pliku demo.html :

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

Następnie muszę zastosować all: initial do instancji tego elementu, które propagują się do dzieci każdej instancji.

 .demo { all: initial }

Zachowanie initial jest dość… idiosynkratyczne. W praktyce wszystkie elementy, których to dotyczy, wracają do przyjmowania tylko swoich stylów agenta użytkownika (takich jak display: block dla elementów <h2> ). Jednak element, do którego jest stosowany — class=“demo” — musi mieć wyraźnie przywrócone określone style agenta użytkownika. W naszym przypadku jest to po prostu display: block , ponieważ class=“demo” to <div> .

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

Uwaga: all jest jak dotąd nieobsługiwane w Microsoft Edge, ale jest rozważane. Poza tym wsparcie jest uspokajająco szerokie. Dla naszych celów wartość revert byłaby bardziej niezawodna i niezawodna, ale nie jest jeszcze nigdzie obsługiwana.

Shadow DOM'ing The Shortcode

Użycie all: initial nie czyni naszych komponentów wbudowanych całkowicie odpornymi na wpływy z zewnątrz (wciąż obowiązuje specyficzność), ale możemy być pewni, że style są nieustawione, ponieważ mamy do czynienia z zarezerwowaną nazwą klasy demo . Wyeliminowane zostaną głównie style odziedziczone z selektorów o niskiej specyficzności, takich jak html i body .

Niemniej jednak dotyczy to tylko stylów pochodzących od rodzica do komponentów. Aby zapobiec wpływowi stylów napisanych dla komponentów na inne części strony, musimy użyć shadow DOM do utworzenia hermetyzowanego poddrzewa.

Wyobraź sobie, że chcę udokumentować stylizowany element <button> . Chciałbym móc po prostu napisać coś takiego jak poniżej, bez obawy, że selektor elementu button będzie miał zastosowanie do elementów <button> w samej bibliotece wzorców lub w innych komponentach na tej samej stronie biblioteki.

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

Sztuczka polega na tym, aby wziąć {{ .Inner }} część szablonu shortcode i dołączyć ją jako wewnętrzny ShadowRoot innerHTML Mógłbym to zaimplementować w ten sposób:

 {{ $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 jest ustawiana jako zmienna identyfikująca kontener komponentu. Potoki w niektórych funkcjach szablonu Go tworzą unikalny ciąg… miejmy nadzieję (!) — nie jest to metoda kuloodporna; to tylko dla ilustracji.
  • root.attachShadow sprawia, że ​​kontener komponentu staje się hostem cienia DOM.
  • ShadowRoot innerHTML pomocą {{ .Inner }} , który zawiera teraz enkapsulowany CSS.

Zezwalanie na zachowanie JavaScript

Chciałbym również uwzględnić zachowanie JavaScript w moich komponentach. Na początku myślałem, że to będzie łatwe; niestety JavaScript wstawiony przez innerHTML nie jest analizowany ani wykonywany. Można to rozwiązać importując z zawartości elementu <template> . Odpowiednio zmieniłem moją implementację.

 {{ $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>

Teraz mogę dołączyć wbudowane demo, powiedzmy, działającego przycisku przełączania:

 {{<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>}}

Uwaga: szczegółowo opisałem przyciski przełączania i ułatwienia dostępu dla komponentów dołączanych.

Enkapsulacja JavaScript

JavaScript, ku mojemu zdziwieniu, nie jest enkapsulowany automatycznie, tak jak CSS jest w cieniu DOM. Oznacza to, że jeśli na stronie nadrzędnej przed przykładem tego komponentu znajdował się inny [aria-pressed] , to document.querySelector wybrałby go zamiast tego.

To, czego potrzebuję, to odpowiednik document tylko dla poddrzewa demo. Można to zdefiniować, aczkolwiek dość szczegółowo:

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

Nie chciałem pisać tego wyrażenia za każdym razem, gdy musiałem celować w elementy wewnątrz kontenerów demonstracyjnych. Wymyśliłem więc hack, w którym przypisałem wyrażenie do lokalnej zmiennej demo i skrypty z prefiksem dostarczone przez krótki kod z tym przypisaniem:

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

Dzięki temu demo staje się odpowiednikiem document dla dowolnego poddrzewa komponentów i mogę użyć demo.querySelector , aby łatwo wskazać mój przycisk przełączania.

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

Zwróć uwagę, że zawartość skryptu demonstracyjnego umieściłem w natychmiast wywołanym wyrażeniu funkcyjnym (IIFE), tak że zmienna demo — i wszystkie kolejne zmienne użyte dla komponentu — nie znajdują się w zasięgu globalnym. W ten sposób demo może być użyte w dowolnym skrypcie shortcode, ale będzie odnosić się tylko do tego shortcode.

Tam, gdzie dostępny jest ECMAScript6, możliwe jest osiągnięcie lokalizacji za pomocą „zakresu blokowego”, z nawiasami klamrowymi zawierającymi instrukcje let lub const . Jednak wszystkie inne definicje w bloku musiałyby również używać let lub const (pomijanie var ).

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

Wsparcie Shadow DOM

Oczywiście wszystko to jest możliwe tylko wtedy, gdy obsługiwany jest shadow DOM w wersji 1. Chrome, Safari, Opera i Android wyglądają całkiem nieźle, ale przeglądarki Firefox i Microsoft są problematyczne. Możliwe jest wykrycie funkcji wsparcia i wyświetlenie komunikatu o błędzie, attachShadow nie jest dostępny:

 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'; }

Możesz też dołączyć Shady DOM i rozszerzenie Shady CSS, co oznacza dość dużą zależność (60 KB+) i inny interfejs API. Rob Dodson był na tyle uprzejmy, że udostępnił mi podstawowe demo, którym chętnie się podzielę, aby pomóc Ci zacząć.

Podpisy dla komponentów

Dzięki podstawowej funkcjonalności demonstracji inline, szybkie pisanie działających wersji demonstracyjnych w połączeniu z ich dokumentacją jest na szczęście proste. Daje nam to luksus zadawania pytań typu: „A jeśli chcę dodać podpis, aby oznaczyć demo?” Jest to całkowicie możliwe, ponieważ — jak już wcześniej wspomniano — Markdown obsługuje surowy HTML.

 <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>

Jednak jedyną nową częścią tej zmienionej struktury jest samo brzmienie podpisu. Lepiej zapewnić prosty interfejs do dostarczania go do wyjścia, oszczędzając mojemu przyszłemu ja — i wszystkim innym korzystającym z shortcode — czas i wysiłek oraz zmniejszając ryzyko literówek. Jest to możliwe poprzez dostarczenie nazwanego parametru do shortcode — w tym przypadku po prostu o nazwie caption :

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

Nazwane parametry są dostępne w szablonie, np. {{ .Get "caption" }} , co jest dość proste. Chcę, aby podpis, a zatem otaczające <figure> i <figcaption> były opcjonalne. Używając klauzul if , mogę dostarczyć odpowiednią treść tylko wtedy, gdy shortcode zawiera argument podpisu:

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

Oto jak teraz wygląda pełny szablon demo.html (co prawda trochę bałaganu, ale to załatwia sprawę):

 {{ $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>

Ostatnia uwaga: jeśli chcę obsługiwać składnię markdown w wartości podpisu, mogę ją przepuścić przez funkcję markdownify Hugo. W ten sposób autor może dostarczyć przeceny (i HTML), ale nie jest do tego zmuszony.

 {{ .Get "caption" | markdownify }}

Wniosek

Ze względu na swoją wydajność i wiele doskonałych funkcji, Hugo jest obecnie dla mnie wygodnym rozwiązaniem, jeśli chodzi o generowanie stron statycznych. Ale włączenie skrótów jest tym, co uważam za najbardziej przekonujące. W tym przypadku udało mi się stworzyć prosty interfejs dla problemu z dokumentacją, który od jakiegoś czasu staram się rozwiązać.

Podobnie jak w przypadku komponentów internetowych, za skrótami może kryć się duża złożoność znaczników (czasami pogarszana przez dostosowanie pod kątem dostępności). W tym przypadku mam na myśli włączenie przeze mnie zmiennej role="group" i relacji aria-labelledby , która zapewnia lepiej obsługiwaną "etykietę grupy" dla <figure> — nie są to rzeczy, które każdy lubi kodować więcej niż raz, zwłaszcza gdzie unikalne wartości atrybutów muszą być brane pod uwagę w każdym przypadku.

Uważam, że skróty są dla Markdowna i treści tym, czym komponenty sieciowe dla HTML i funkcjonalności: sposobem na ułatwienie tworzenia, bardziej niezawodne i spójne. Nie mogę się doczekać dalszej ewolucji w tej ciekawej, małej dziedzinie sieci.

Zasoby

  • Dokumentacja Hugo
  • „Szablon pakietu”, język programowania Go
  • „Skróty”, Hugo
  • „all” (właściwość skrócona CSS), Mozilla Developer Network
  • „początkowe (słowo kluczowe CSS), Mozilla Developer Network
  • „Shadow DOM v1: samodzielne składniki internetowe”, Eric Bidelman, Web Fundamentals, Google Developers
  • „Wprowadzenie do elementów szablonów”, Eiji Kitamura, WebComponents.org
  • „Zawiera”, Jekyll