MarkdownでShadowDOMを使用してパターンライブラリを構築する
公開: 2022-03-10デスクトップワードプロセッサを使用する私の典型的なワークフローは次のようになります。
- ドキュメントの別の部分にコピーするテキストを選択します。
- アプリケーションが私が言ったよりもわずかに多いか少ないかを選択していることに注意してください。
- 再試行。
- 後で、あきらめて、意図した選択の欠落している部分を追加する(または余分な部分を削除する)ことを決心します。
- 選択範囲をコピーして貼り付けます。
- 貼り付けたテキストのフォーマットは、元のテキストとは多少異なることに注意してください。
- 元のテキストに一致するスタイリングプリセットを見つけてください。
- プリセットを適用してみてください。
- フォントファミリーとサイズを手動で放棄して適用します。
- 貼り付けたテキストの上に空白が多すぎることに注意してください。「Backspace」を押してギャップを閉じます。
- 問題のテキストは、一度に数行高くなり、その上の見出しテキストに結合され、そのスタイルが採用されていることに注意してください。
- 私の死を考えてください。
技術的なWebドキュメント(パターンライブラリを読む)を書くとき、ワードプロセッサは単に不従順であるだけでなく不適切です。 理想的には、ドキュメント化するコンポーネントをインラインで含めることができる書き込みモードが必要です。これは、ドキュメント自体がHTML、CSS、およびJavaScriptで作成されていない限り不可能です。 この記事では、ショートコードとシャドウDOMカプセル化を利用して、Markdownにコードデモを簡単に含める方法を紹介します。

CSSとマークダウン
CSSについて何を言うか、それは確かに市場に出回っているどのWYSIWYGエディターやワードプロセッサーよりも一貫性があり信頼性の高い植字ツールです。 なんで? なぜなら、実際にどこに行くつもりだったのかを二番目に推測しようとする高レベルのブラックボックスアルゴリズムがないからです。 代わりに、それは非常に明確です。どの要素がどの状況でどのスタイルをとるかを定義し、それらのルールを尊重します。
CSSの唯一の問題は、対応するHTMLを作成する必要があることです。 HTMLの偉大な愛好家でさえ、散文コンテンツを作成したいだけの場合、HTMLを手動で作成することは困難な側面であると認めるでしょう。 そこでMarkdownが登場します。簡潔な構文と機能セットの削減により、習得が容易でありながら、プログラムでHTMLに変換した後でも、CSSの強力で予測可能な植字機能を利用できる書き込みモードを提供します。 静的なウェブサイトジェネレーターやGhostなどの最新のブログプラットフォームの事実上の形式になっているのには理由があります。
より複雑な特注のマークアップが必要な場合、ほとんどのMarkdownパーサーは入力で生のHTMLを受け入れます。 ただし、複雑なマークアップに依存するほど、技術的でない人や時間と忍耐力が不足している人にとっては、オーサリングシステムにアクセスしにくくなります。 これがショートコードの出番です。
Hugoのショートコード
Hugoは、Googleで開発された多目的コンパイル言語であるGoで記述された静的サイトジェネレーターです。 並行性(そして間違いなく、私が完全には理解していない他の低水準言語機能)のために、GoはHugoを静的Webコンテンツの超高速ジェネレーターにします。 これが、HugoがSmashingMagazineの新しいバージョンに選ばれた多くの理由の1つです。
パフォーマンスはさておき、これは、すでにおなじみのRubyおよびNode.jsベースのジェネレーターと同じように機能します。Markdownとテンプレートを介して処理されるメタデータ(YAMLまたはTOML)です。 Sara Soueidanは、Hugoのコア機能に関する優れた入門書を作成しました。
私にとって、Hugoのキラー機能はショートコードの実装です。 WordPressのユーザーは、この概念にすでに精通している可能性があります。これは、主にサードパーティサービスの複雑な埋め込みコードを含めるために使用される短縮構文です。 たとえば、WordPressには、問題のVimeoビデオのIDだけを取得するVimeoショートコードが含まれています。
[vimeo 44633289]
角かっこは、コンテンツがショートコードとして処理され、コンテンツが解析されるときに完全なHTML埋め込みマークアップに展開される必要があることを示します。
Hugoは、Goテンプレート関数を利用して、カスタムショートコードを作成するための非常にシンプルなAPIを提供します。 たとえば、Markdownコンテンツに含める簡単なCodepenショートコードを作成しました。
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は、コンパイル中にショートコードを解析するために、 shortcodes
codePen.html
名前のテンプレートを自動的に検索します。 私の実装は次のようになります。
{{ 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 }}
Goテンプレートパッケージがどのように機能するかをよりよく理解するには、Hugoの「GoTemplatePrimer」を参照してください。 それまでの間、次の点に注意してください。
- それはかなり醜いですが、それでも強力です。
-
{{ .Get 0 }}
の部分は、指定された最初の(この場合は唯一の)引数であるCodepenIDを取得するためのものです。 Hugoは、HTML属性のように提供される名前付き引数もサポートしています。 -
.
構文は現在のコンテキストを参照します。 したがって、.Get 0
は、「現在のショートコードに指定された最初の引数を取得する」ことを意味します。
いずれにせよ、ショートブレッド以来、ショートコードが最良だと思います。カスタムショートコードを作成するためのHugoの実装は印象的です。 私の研究から、Jekyllインクルードを使用して同様の効果を得ることが可能であることに注意する必要がありますが、柔軟性と強力性が劣っています。
サードパーティのないコードデモ
私はCodepen(および利用可能な他のコードプレイグラウンド)に多くの時間を費やしていますが、そのようなコンテンツをパターンライブラリに含めることには固有の問題があります。
- APIを使用しているため、オフラインで簡単または効率的に動作させることはできません。
- パターンやコンポーネントを表すだけではありません。 それは、独自のブランドに包まれた独自の複雑なインターフェースです。 これにより、コンポーネントに焦点を合わせる必要があるときに、不要なノイズと注意散漫が発生します。
しばらくの間、私は自分のiframeを使用してコンポーネントのデモを埋め込もうとしました。 デモを独自のWebページとして含むローカルファイルをiframeにポイントします。 iframeを使用することで、サードパーティに依存することなくスタイルと動作をカプセル化することができました。
残念ながら、iframeは扱いにくく、動的にサイズを変更するのが困難です。 オーサリングの複雑さという点では、個別のファイルを維持し、それらにリンクする必要もあります。 コンポーネントを機能させるために必要なコードだけを含めて、コンポーネントを適切に記述したいと思います。 ドキュメントを作成しながら、デモを作成できるようにしたいと考えています。
demo
ショートコード
幸い、Hugoでは、ショートコードタグの開始と終了の間にコンテンツを含むショートコードを作成できます。 コンテンツは、 {{ .Inner }}
を使用してショートコードファイルで利用できます。 したがって、次のようなdemo
ショートコードを使用するとします。
{{<demo>}} This is the content! {{</demo>}}
「これが内容です!」 それを解析するdemo.html
テンプレートでは{{ .Inner }}
として利用できます。 これは、インラインコードのデモをサポートするための良い出発点ですが、カプセル化に取り組む必要があります。
スタイルのカプセル化
スタイルのカプセル化に関しては、次の3つの点について心配する必要があります。
- 親ページからコンポーネントに継承されるスタイル、
- コンポーネントからスタイルを継承する親ページ、
- コンポーネント間で意図せずに共有されているスタイル。
1つの解決策は、CSSセレクターを注意深く管理して、コンポーネント間およびコンポーネントとページ間で重複がないようにすることです。 これは、コンポーネントごとに難解なセレクターを使用することを意味し、簡潔で読みやすいコードをいつ作成できるかを検討する必要はありません。 iframeの利点の1つは、スタイルがデフォルトでカプセル化されていることです。そのため、 button { background: blue }
を記述して、iframe内にのみ適用されると確信できます。
コンポーネントがページからスタイルを継承しないようにするためのそれほど集中的でない方法は、選択された親要素のinitial
値でall
プロパティを使用することです。 この要素はdemo.html
ファイルで設定できます。
<div class="demo"> {{ .Inner }} </div>
次に、 all: initial
をこの要素のインスタンスに適用する必要があります。これは、各インスタンスの子に伝播します。
.demo { all: initial }
initial
の振る舞いはかなり…特異です。 実際には、影響を受けるすべての要素は、ユーザーエージェントスタイル( display: block
for <h2>
要素など)のみを採用することに戻ります。 ただし、それが適用される要素( class=“demo”
、特定のユーザーエージェントスタイルを明示的に復元する必要があります。 この場合、 class=“demo”
は<div>
であるため、これは単なるdisplay: block
です。
.demo { all: initial; display: block; }
注:これまでのところ、Microsoft Edgeではall
がサポートされていませんが、検討中です。 それ以外の場合、サポートは安心して幅広く提供されます。 私たちの目的では、 revert
値はより堅牢で信頼性が高くなりますが、まだどこでもサポートされていません。
Shadow DOM'ing The Shortcode
all: initial
を使用しても、インラインコンポーネントが外部の影響から完全に免れるわけではありませんが(特異性は引き続き適用されます)、予約済みのdemo
クラス名を処理しているため、スタイルが設定されていないことを確信できます。 ほとんどの場合、 html
やbody
などの特異性の低いセレクターから継承されたスタイルは削除されます。
それにもかかわらず、これは親からコンポーネントに入るスタイルのみを扱います。 コンポーネント用に作成されたスタイルがページの他の部分に影響を与えないようにするには、シャドウDOMを使用してカプセル化されたサブツリーを作成する必要があります。

スタイル付きの<button>
要素を文書化したいとします。 button
要素セレクターがパターンライブラリ自体または同じライブラリページの他のコンポーネントの<button>
要素に適用されることを恐れずに、次のようなものを簡単に記述できるようにしたいと思います。
{{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}
秘訣は、ショートコードテンプレートの{{ .Inner }}
部分を取得し、それを新しいShadowRoot
のinnerHTML
として含めることです。 私はこれを次のように実装するかもしれません:
{{ $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
は、コンポーネントコンテナを識別するための変数として設定されます。 いくつかのGoテンプレート関数をパイプ処理して、一意の文字列を作成します…うまくいけば(!)—これは防弾メソッドではありません。 説明のためだけです。 -
root.attachShadow
は、コンポーネントコンテナをシャドウDOMホストにします。 -
{{ .Inner }}
を使用してShadowRoot
のinnerHTML
にデータを入力します。これには、カプセル化されたCSSが含まれています。
JavaScriptの動作を許可する
また、コンポーネントにJavaScriptの動作を含めたいと思います。 最初は、これは簡単だと思いました。 残念ながら、 innerHTML
を介して挿入されたJavaScriptは解析または実行されません。 これは、 <template>
要素のコンテンツからインポートすることで解決できます。 それに応じて実装を修正しました。
{{ $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>
これで、たとえば、機能するトグルボタンのインラインデモを含めることができます。
{{<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>}}
注:インクルーシブコンポーネントのトグルボタンとアクセシビリティについて詳しく説明しました。
JavaScriptカプセル化
驚いたことに、JavaScriptは、CSSがShadowDOMにあるように自動的にカプセル化されません。 つまり、このコンポーネントの例の前に親ページに別の[aria-pressed]
ボタンがあった場合、 document.querySelector
は代わりにそれをターゲットにします。
必要なのは、デモのサブツリーだけのdocument
に相当します。 これは非常に冗長ですが、定義可能です。
document.getElementById('demo-{{ $uniq }}').shadowRoot;
デモコンテナ内の要素をターゲットにする必要があるときはいつでも、この式を記述したくありませんでした。 そこで、私はハックを思いつきました。それによって、式をローカルdemo
変数に割り当て、この割り当てでショートコードを介して提供されるプレフィックス付きスクリプトを作成しました。
if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));
これが適切な場所にあると、 demo
は任意のコンポーネントサブツリーのdocument
と同等になり、 demo.querySelector
を使用してトグルボタンを簡単にターゲットにできます。
var toggle = demo.querySelector('[aria-pressed]');
デモのスクリプトの内容を即時呼び出し関数式(IIFE)で囲んだため、 demo
変数(およびコンポーネントに使用されるすべての先行変数)はグローバルスコープに含まれないことに注意してください。 このように、 demo
は任意のショートコードのスクリプトで使用できますが、手元にあるショートコードのみを参照します。
ECMAScript6が利用可能な場合、 let
またはconst
ステートメントを中括弧で囲むだけで、「ブロックスコープ」を使用してローカリゼーションを実現できます。 ただし、ブロック内の他のすべての定義では、 let
またはconst
( var
を回避)も使用する必要があります。
{ let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }
ShadowDOMのサポート
もちろん、上記のすべては、ShadowDOMバージョン1がサポートされている場合にのみ可能です。 Chrome、Safari、Opera、Androidはすべてかなり見栄えがしますが、FirefoxとMicrosoftのブラウザには問題があります。 attachShadow
が利用できない場合、サポートを機能検出してエラーメッセージを表示することができます。
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'; }
または、ShadyDOMとShadyCSS拡張機能を含めることができます。これは、多少大きな依存関係(60 KB以上)と異なるAPIを意味します。 Rob Dodsonは親切にも基本的なデモを提供してくれました。これを共有して、開始に役立てることができます。
コンポーネントのキャプション
基本的なインラインデモ機能が整っているので、ドキュメントに沿って動作するデモをすばやく作成するのは非常に簡単です。 これにより、「デモにラベルを付けるためのキャプションを提供したい場合はどうすればよいですか?」などの質問をすることができるという贅沢が得られます。 前述のように、Markdownは生の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>
ただし、この修正された構造の唯一の新しい部分は、キャプション自体の文言です。 それを出力に提供するためのシンプルなインターフェイスを提供し、将来の自分自身、およびショートコードを使用する他の人を時間と労力を節約し、タイプミスをコーディングするリスクを減らす方がよいでしょう。 これは、名前付きパラメーターをショートコードに指定することで可能になります。この場合は、単に名前付きのcaption
です。
{{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}
名前付きパラメータは、 {{ .Get "caption" }}
のようにテンプレートでアクセスできますが、これは非常に簡単です。 キャプションが必要なので、周囲の<figure>
と<figcaption>
はオプションにします。 if
句を使用すると、ショートコードがキャプション引数を提供する場合にのみ、関連するコンテンツを提供できます。
{{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}
完全なdemo.html
テンプレートがどのように表示されるかを次に示します(確かに、少し混乱していますが、うまくいきます)。
{{ $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>
最後の注意:キャプション値でマークダウン構文をサポートしたい場合は、Hugoのmarkdownify
関数を介してパイプすることができます。 このようにして、作成者はマークダウン(およびHTML)を提供できますが、強制されることもありません。
{{ .Get "caption" | markdownify }}
結論
そのパフォーマンスと多くの優れた機能のために、Hugoは現在、静的サイトの生成に関して私に快適にフィットしています。 しかし、ショートコードを含めることが最も魅力的だと思います。 この場合、私はしばらくの間解決しようとしていたドキュメントの問題のための簡単なインターフェースを作成することができました。
Webコンポーネントの場合と同様に、マークアップの複雑さの多く(アクセシビリティを調整することで悪化する場合があります)は、ショートコードの背後に隠れている可能性があります。 この場合、私はrole="group"
とaria-labelledby
関係を含めることを指します。これは、 <figure>
により適切にサポートされる「グループラベル」を提供します。特に、誰もがコーディングを2回以上楽しむものではありません。ここで、各インスタンスで一意の属性値を考慮する必要があります。
ショートコードは、HTMLと機能に対するWebコンポーネントとは何かをマークダウンしてコンテンツ化することであると私は信じています。これは、作成者をより簡単に、より信頼性が高く、より一貫性のあるものにする方法です。 この好奇心旺盛なウェブの小さな分野でのさらなる進化を楽しみにしています。
資力
- Hugoドキュメント
- 「パッケージテンプレート」、Goプログラミング言語
- 「ショートコード」Hugo
- 「すべて」(CSS省略形プロパティ)、Mozilla Developer Network
- 「初期(CSSキーワード)、Mozilla Developer Network
- 「ShadowDOMv1:自己完結型のWebコンポーネント」、Eric Bidelman、Web Fundamentals、Google Developers
- 「テンプレート要素の概要」、北村英治、WebComponents.org
- 「含む」ジキル