実験的コンポーネント `<wa-markdown>` の追加
WebAwesomeに、MarkdownをHTML内で宣言的にレンダリングするユーティリティコンポーネント <wa-markdown> が実験的機能として追加されました。Marked ライブラリを採用し、インデント正規化やパーサー拡張を考慮した設計になっています。
背景
既存のHTMLページにMarkdownコンテンツを組み込むには、JavaScriptでパーサーを呼び出すコードを自前で書く必要がありました。<wa-markdown> は、この処理をWebコンポーネントとして宣言的に扱えるようにすることを目的としています。PR本文では「実験的なユーティリティ」と明記されており、SEOやSSRには適さない点も含め、実際のユースケースを探るための機能として位置づけられています。
Markdownパーサーの選定にあたっては、バンドルサイズとプラグインエコシステムのトレードオフが考慮されました。Marked はその両面でバランスが取れていると判断され採用されています。package.json および package-lock.json の変更では、これまで devDependencies として管理されていた marked@^11.2.0 が dependencies へ移動しています。
技術的な変更
新コンポーネントは packages/webawesome/src/components/markdown/ 以下に markdown.ts・markdown.styles.ts・markdown.test.ts の3ファイルで構成されます。
コンテンツの受け渡しと <script type="text/markdown">
Markdownのソースは <script type="text/markdown"> 要素の内容として渡します。ブラウザがMarkdown内のHTMLタグや記号をDOMとしてパースしてしまうことを防ぐための設計です。コンポーネントはこの <script> 要素のテキストコンテンツを読み取り、レンダリングを行います。
<wa-markdown>
<script type="text/markdown">
## Getting Started
Here's a quick overview with **bold**, *italic*, and `inline code`.
- Install the package
- Import the component
- Start writing markdown
</script>
</wa-markdown>
インデント正規化と tabSize
markdown.ts は共通の先頭ホワイトスペース(共通インデント)を自動的に除去します。これにより、HTMLの構造に合わせてMarkdownを自由にインデントしても、パース結果に余分な空白が混入しません。タブ幅は tab-size 属性(tabSize プロパティ)で制御でき、デフォルトは 4 です。
ライトDOMへの出力
レンダリングされたHTMLはシャドウDOMではなくライトDOMに注入されます。これにより、ページ既存のスタイルシートがそのまま適用されます。スタイルファイル(markdown.styles.ts)では :host に display: contents を指定しており、コンポーネントのホスト要素自体はレイアウト上の存在感を持ちません。
:host {
display: contents;
}
共有Markedインスタンスとパーサー拡張
モジュールスコープで単一の sharedMarked インスタンスが生成され、全 <wa-markdown> 要素で共有されます。インスタンスメソッド marked プロパティおよび静的メソッド WaMarkdown.getMarked() で参照でき、marked.use() でカスタムレンダラーやサードパーティプラグインをグローバルに登録できます。
const sharedMarked = new Marked();
const connectedInstances = new Set<WaMarkdown>();
export default class WaMarkdown extends WebAwesomeElement {
/** Returns the shared Marked instance used by all <wa-markdown> components. */
static getMarked(): Marked {
return sharedMarked;
}
/** Re-renders all connected <wa-markdown> instances. */
static updateAll(): void {
for (const instance of connectedInstances) {
instance.renderMarkdown();
}
}
get marked(): Marked {
return sharedMarked;
}
}
Markedの設定を変更した後は WaMarkdown.updateAll() を呼ぶことで、接続中の全インスタンスを再レンダリングできます。接続中のインスタンスは connectedInstances という Set で管理され、connectedCallback / disconnectedCallback で追加・削除されます。
テスト
markdown.test.ts では @open-wc/testing を使用したテストが追加されています。Litの html テンプレートタグは <script> 要素を内包するテンプレートを扱えないため、テストヘルパー createMarkdownElement() を使って <wa-markdown> と <script type="text/markdown"> を命令的に生成しています。
設計判断
シャドウDOMを使わずライトDOMにレンダリングする選択が、この設計の根幹にあります。シャドウDOMを採用すると、ページのスタイルシートがレンダリング結果に届かなくなります。Markdownから生成される <h2> や <p> などの要素にページ既存のCSSを適用するためには、ライトDOMへの注入が必然的な選択です。
パーサーインスタンスの共有も注目すべき判断です。インスタンスごとに独立したMarkedを持つ設計ではなく、モジュールスコープの単一インスタンスを全コンポーネントで共有しています。これにより、marked.use() による拡張が全インスタンスに一括適用される一方、設定は必然的にグローバルな状態になります。PRではこのトレードオフをドキュメントにも明記しており、意図的な設計判断として位置づけられています。
XSSリスクについてはドキュメントで明示的に警告されています(「Do not use this component with unsanitized user input」)。サニタイズ処理をコンポーネント内に含めないことで責務を明確に分離し、ユーザーが適切なサニタイザーを選択できる余地を残しています。
まとめ
<wa-markdown> は、<script type="text/markdown"> によるブラウザのHTMLパース回避、ライトDOMへの出力によるスタイル継承、共有Markedインスタンスによる統一された拡張ポイントという3つの設計判断を組み合わせた実験的コンポーネントです。SEOやSSRの制約を明示した上で実験的機能として公開することで、実際のユースケースをコミュニティからフィードバックとして収集する意図が読み取れます。