実験的コンポーネント `<wa-markdown>` の追加

shoelace-style/webawesome

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.0dependencies へ移動しています。

技術的な変更

新コンポーネントは packages/webawesome/src/components/markdown/ 以下に markdown.tsmarkdown.styles.tsmarkdown.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)では :hostdisplay: 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の制約を明示した上で実験的機能として公開することで、実際のユースケースをコミュニティからフィードバックとして収集する意図が読み取れます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
2e5fbe38

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術詳細・設計判断(各論)、まとめ(結論)という「総論→各論→結論」の構成が明確で、読者が変更の全体像を体系的に理解しやすい構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのコードブロック(css, typescript)やPRへのリンク記法が正しく使用されており、技術記事としての体裁が整っています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

WebコンポーネントやDOMに関する知識を持つエンジニアを対象としており、冗長な説明がなく、専門性の高い内容が簡潔にまとめられています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが「総論→各論」の構造で書かれ、トピックセンテンスが明確なため、非常に読みやすい文章になっています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードスニペットは、提供されたDiffの内容と正確に一致しています。コードの省略も要点を伝える上で適切です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

ライトDOM、シャドウDOM、パーサーインスタンス、XSSなど、関連する技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

コンポーネントの動作原理や設計上の選択(例:`<script type="text/markdown">` を使う理由やライトDOMへの出力)に関する説明が、PR情報と照合して技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(実験的な位置づけ、Marked採用の理由など)は、PRのDescriptionやDiff内のドキュメントで裏付けられており、ハルシネーションは認められません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2170)や`tabSize`のデフォルト値(4)、`marked`のバージョンなど、記事に含まれる数値や固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「実験的コンポーネント `<wa-markdown>` の追加」は、PRのタイトル「Add the experimental `<wa-markdown>` component」と内容を的確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PRで提供された情報のみに基づいて記事が構成されており、サポート状況やリリース予定といった外部知識の不適切な追加はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「追加されました」といった時間表現がPRの内容(新機能の追加)と一致しており、事実を正確に伝えています。