Featured image of post 第8回-リンク自動生成とアンカーID付与(完成) - PerlとMooで学ぶComposite

第8回-リンク自動生成とアンカーID付与(完成) - PerlとMooで学ぶComposite

PerlとMooでCompositeパターン完全マスター!アンカーID自動生成でリンク付き目次を実現し、開放閉鎖原則(OCP)を実践。実務で使えるMarkdown処理ツールを完成させます。

前回の振り返り

前回は、同じツリー構造からMarkdown、HTML、JSON形式で出力する機能を実装しました。Compositeパターンの「統一インターフェース」により、再帰的に複数フォーマットでレンダリングできるようになりました。

今回は最終回として、アンカーID自動生成機能を追加し、Markdown目次ジェネレーターを完成させます。

今回のゴール

見出しテキストからアンカーID(#introduction#chapter-1など)を自動生成し、リンク付きの目次を出力する機能を実装します。

アンカーIDの生成ルール

多くのMarkdownパーサー(GitHub Flavored Markdownなど)は、以下のルールでアンカーIDを生成します:

  1. 小文字に変換
  2. 空白をハイフン-に変換
  3. 英数字とハイフン以外を削除
  4. 連続するハイフンを1つに

例:

見出しテキストアンカーID
はじめに#はじめに
第1章 概要#第1章-概要
Hello World!#hello-world

AnchoredRendererクラスの実装

既存のクラスを変更せず、新しいレンダラークラスを追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package AnchoredRenderer;
use v5.36;
use Moo;

has 'root' => (
    is       => 'ro',
    required => 1,
);

sub render ($self) {
    my @lines;
    for my $child ($self->root->children->@*) {
        push @lines, $self->_render_node($child, 0);
    }
    return join("\n", @lines);
}

sub _render_node ($self, $node, $indent) {
    my $spaces = '  ' x $indent;
    my $anchor = $self->_text_to_anchor($node->text);
    my $link   = "[$node->{text}]($anchor)";
    
    my @lines = ($spaces . '- ' . $link);
    
    if ($node->can('children') && $node->children->@*) {
        for my $child ($node->children->@*) {
            push @lines, $self->_render_node($child, $indent + 1);
        }
    }
    
    return join("\n", @lines);
}

sub _text_to_anchor ($self, $text) {
    my $anchor = lc($text);           # 小文字に変換
    $anchor =~ s/\s+/-/g;             # 空白をハイフンに
    $anchor =~ s/[^\w\-\p{Han}\p{Hiragana}\p{Katakana}]//g;  # 許可文字以外を削除
    $anchor =~ s/-+/-/g;              # 連続ハイフンを1つに
    $anchor =~ s/^-|-$//g;            # 先頭・末尾のハイフンを削除
    return "#$anchor";
}

1;

コードの解説

_text_to_anchorメソッド

1
2
3
4
5
6
7
8
sub _text_to_anchor ($self, $text) {
    my $anchor = lc($text);           # 小文字に変換
    $anchor =~ s/\s+/-/g;             # 空白をハイフンに
    $anchor =~ s/[^\w\-\p{Han}\p{Hiragana}\p{Katakana}]//g;  # 許可文字以外を削除
    $anchor =~ s/-+/-/g;              # 連続ハイフンを1つに
    $anchor =~ s/^-|-$//g;            # 先頭・末尾のハイフンを削除
    return "#$anchor";
}

正規表現のポイント:

  • \p{Han}: 漢字
  • \p{Hiragana}: ひらがな
  • \p{Katakana}: カタカナ

日本語の見出しもそのままアンカーIDとして使えるようにしています。

動的メソッド呼び出し

1
if ($node->can('children') && $node->children->@*) {

canメソッドで、$nodechildrenメソッドを持っているかチェックします。これにより、LeafHeading(子を持たない)とSectionHeading(子を持つ)の両方に対応できます。

開放閉鎖原則(OCP)の実践

今回、既存のクラス(SectionHeading、LeafHeading、TOCParser)を一切変更せずに、新しい出力形式を追加しました。

これが開放閉鎖原則(OCP: Open/Closed Principle)です:

ソフトウェアの構成要素は、拡張に対して開いており、修正に対して閉じているべきである。

  • 拡張に対して開いている: 新しいレンダラーを追加できる
  • 修正に対して閉じている: 既存コードを変更しない

使用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env perl
use v5.36;
use lib '.';
use MarkdownReader;
use HeadingExtractor;
use TOCParser;
use AnchoredRenderer;

my $reader = MarkdownReader->new(
    filepath => 'sample.md',
);

my $extractor = HeadingExtractor->new(
    lines => $reader->lines,
);

my $parser = TOCParser->new(
    headings => $extractor->headings,
);

my $renderer = AnchoredRenderer->new(
    root => $parser->root,
);

say "=== リンク付き目次 ===";
say $renderer->render;

実行結果

1
2
3
4
5
=== リンク付き目次 ===
- [はじめに](#はじめに)
  - [第1章](#第1章)
    - [セクション1.1](#セクション11)
  - [第2章](#第2章)

クリック可能なリンク付きの目次が生成されました!

OCP(開放閉鎖原則)を学べる他のシリーズ

開放閉鎖原則は、SOLID原則の中核です。他のパターンでのOCP実践例:

シリーズ完成コード一覧

これで、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. 重複アンカーの処理: 同じテキストの見出しに連番を付ける(#概要#概要-1
  2. Hugo/Jekyll対応: 各ツールのアンカー生成ルールに合わせる
  3. カスタムフォーマッター: CSSクラス付きHTMLなど
  4. ファイル出力: 生成した目次をファイルに書き出す

まとめ

8回にわたるシリーズで、Markdown目次ジェネレーターを完成させました。

シリーズ全体で学んだこと

  1. 第1-3回: ファイルI/O、正規表現、シンプルなレンダリング
  2. 第4回: 条件分岐爆発の問題体験
  3. 第5回: Compositeパターンの導入(Role、Leaf、Composite)
  4. 第6回: スタックベースのパーサーでツリー自動構築
  5. 第7回: 複数フォーマット出力(Markdown、HTML、JSON)
  6. 第8回: アンカー生成とOCP(開放閉鎖原則)の実践

Compositeパターンのポイント

  • 部分-全体階層の統一的扱い: 葉要素も複合要素も同じインターフェース
  • 再帰的処理: renderメソッドが子要素のrenderを呼び出す
  • 拡張性: 新しいレンダラーを追加しても既存コード変更不要

お疲れさまでした!完成したツールをぜひ実際のプロジェクトで活用してください。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。