前回の振り返り
前回は、同じツリー構造からMarkdown、HTML、JSON形式で出力する機能を実装しました。Compositeパターンの「統一インターフェース」により、再帰的に複数フォーマットでレンダリングできるようになりました。
今回は最終回として、アンカーID自動生成機能を追加し、Markdown目次ジェネレーターを完成させます。
今回のゴール
見出しテキストからアンカーID(#introduction、#chapter-1など)を自動生成し、リンク付きの目次を出力する機能を実装します。
アンカーIDの生成ルール
多くのMarkdownパーサー(GitHub Flavored Markdownなど)は、以下のルールでアンカーIDを生成します:
- 小文字に変換
- 空白をハイフン
-に変換 - 英数字とハイフン以外を削除
- 連続するハイフンを1つに
例:
| 見出しテキスト | アンカーID |
|---|---|
| はじめに | #はじめに |
| 第1章 概要 | #第1章-概要 |
| Hello World! | #hello-world |
AnchoredRendererクラスの実装
既存のクラスを変更せず、新しいレンダラークラスを追加します。
| |
コードの解説
_text_to_anchorメソッド
| |
正規表現のポイント:
\p{Han}: 漢字\p{Hiragana}: ひらがな\p{Katakana}: カタカナ
日本語の見出しもそのままアンカーIDとして使えるようにしています。
動的メソッド呼び出し
| |
canメソッドで、$nodeがchildrenメソッドを持っているかチェックします。これにより、LeafHeading(子を持たない)とSectionHeading(子を持つ)の両方に対応できます。
開放閉鎖原則(OCP)の実践
今回、既存のクラス(SectionHeading、LeafHeading、TOCParser)を一切変更せずに、新しい出力形式を追加しました。
これが開放閉鎖原則(OCP: Open/Closed Principle)です:
ソフトウェアの構成要素は、拡張に対して開いており、修正に対して閉じているべきである。
- 拡張に対して開いている: 新しいレンダラーを追加できる
- 修正に対して閉じている: 既存コードを変更しない
使用例
| |
実行結果
| |
クリック可能なリンク付きの目次が生成されました!
OCP(開放閉鎖原則)を学べる他のシリーズ
開放閉鎖原則は、SOLID原則の中核です。他のパターンでのOCP実践例:
- Decoratorパターン(ログ解析)第8回 - 設定でパイプライン構築
- Stateパターン(自動販売機)第10回 - 新しい状態の追加
シリーズ完成コード一覧
これで、Markdown目次ジェネレーターが完成しました。作成したクラスを一覧化します:
1. MarkdownReader(第1回)
ファイルを読み込み、行の配列を提供
2. HeadingExtractor(第2回)
正規表現で見出しを抽出し、レベルとテキストのハッシュ配列を提供
3. FlatTOCRenderer(第3回)
フラットな配列をインデント付きで表示(シンプル版)
4. Heading Role(第5回)
共通インターフェースを定義
5. LeafHeading(第5回)
子を持たない末端見出し
6. SectionHeading(第5回・第7回)
子を持つセクション見出し。複数フォーマット出力対応
7. TOCParser(第6回)
スタックベースでツリー構造を自動構築
8. AnchoredRenderer(第8回)
アンカーID付きのリンク目次を生成
拡張のアイデア
完成したツールをさらに拡張するアイデア:
- 重複アンカーの処理: 同じテキストの見出しに連番を付ける(
#概要、#概要-1) - Hugo/Jekyll対応: 各ツールのアンカー生成ルールに合わせる
- カスタムフォーマッター: CSSクラス付きHTMLなど
- ファイル出力: 生成した目次をファイルに書き出す
まとめ
8回にわたるシリーズで、Markdown目次ジェネレーターを完成させました。
シリーズ全体で学んだこと
- 第1-3回: ファイルI/O、正規表現、シンプルなレンダリング
- 第4回: 条件分岐爆発の問題体験
- 第5回: Compositeパターンの導入(Role、Leaf、Composite)
- 第6回: スタックベースのパーサーでツリー自動構築
- 第7回: 複数フォーマット出力(Markdown、HTML、JSON)
- 第8回: アンカー生成とOCP(開放閉鎖原則)の実践
Compositeパターンのポイント
- 部分-全体階層の統一的扱い: 葉要素も複合要素も同じインターフェース
- 再帰的処理:
renderメソッドが子要素のrenderを呼び出す - 拡張性: 新しいレンダラーを追加しても既存コード変更不要
お疲れさまでした!完成したツールをぜひ実際のプロジェクトで活用してください。
