Featured image of post 第3回-フラット配列で目次を表示しよう - PerlとMooで学ぶComposite

第3回-フラット配列で目次を表示しよう - PerlとMooで学ぶComposite

PerlとMooでMarkdown目次生成器を作る第3回。抽出した見出しをインデント付きで表示。まだ階層構造は扱わず、単純な配列処理で目次をレンダリングします。

前回の振り返り

前回は、HeadingExtractorクラスを作成し、正規表現でMarkdownの見出しを抽出しました。見出しはレベル(1〜6)とテキストを持つハッシュの配列として取得できるようになりました。

今回は、この配列を使って目次を表示します。

今回のゴール

抽出した見出しの配列を、レベルに応じてインデントを付けて表示するFlatTOCRendererクラスを作成します。

この段階では、まだ「ツリー構造」は扱いません。単純にレベル値に応じてスペースを追加するだけの処理です。

FlatTOCRendererクラスの実装

 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
package FlatTOCRenderer;
use v5.36;
use Moo;

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

has 'indent_size' => (
    is      => 'ro',
    default => 2,
);

sub render ($self) {
    my @lines;
    
    for my $h ($self->headings->@*) {
        my $indent = ' ' x (($h->{level} - 1) * $self->indent_size);
        my $marker = '- ';
        push @lines, $indent . $marker . $h->{text};
    }
    
    return join("\n", @lines);
}

1;

コードの解説

indent_size属性

1
2
3
4
has 'indent_size' => (
    is      => 'ro',
    default => 2,
);

インデントのスペース数を設定できるようにしています。デフォルトは2スペースです。

renderメソッド

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sub render ($self) {
    my @lines;
    
    for my $h ($self->headings->@*) {
        my $indent = ' ' x (($h->{level} - 1) * $self->indent_size);
        my $marker = '- ';
        push @lines, $indent . $marker . $h->{text};
    }
    
    return join("\n", @lines);
}

繰り返し演算子 x

1
my $indent = ' ' x (($h->{level} - 1) * $self->indent_size);

'文字列' x 回数で、文字列を指定回数繰り返した新しい文字列を作成します。

  • レベル1: (1 - 1) * 2 = 0スペース
  • レベル2: (2 - 1) * 2 = 2スペース
  • レベル3: (3 - 1) * 2 = 4スペース

リストマーカー

1
my $marker = '- ';

Markdown形式のリストマーカーを追加します。

結合

1
return join("\n", @lines);

配列の要素を改行で結合して、1つの文字列として返します。

使用例

これまでに作成したクラスを組み合わせて使ってみましょう。

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

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

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

my $renderer = FlatTOCRenderer->new(
    headings => $extractor->headings,
);

say "目次:";
say $renderer->render;

実行結果

1
2
3
4
5
目次:
- はじめに
  - 第1章
    - セクション1.1
  - 第2章

レベルに応じてインデントが付いた目次が表示されました。

インデントサイズのカスタマイズ

1
2
3
4
my $renderer = FlatTOCRenderer->new(
    headings    => $extractor->headings,
    indent_size => 4,  # 4スペース
);

実行結果:

1
2
3
4
5
目次:
- はじめに
    - 第1章
        - セクション1.1
    - 第2章

この方法の限界

現時点では、見出しは「フラットな配列」として扱われています。各見出しは独立した要素であり、親子関係を持っていません。

これで十分に見えるかもしれませんが、次のようなケースを考えてみてください:

問題1:親子関係の追跡

「第1章」の下にある「セクション1.1」と「セクション1.2」をまとめて取得したい場合、現在の実装では簡単にはできません。

問題2:HTML形式での出力

入れ子になった<ul><li>構造を生成するには、「どの見出しがどの見出しの下にあるか」を知る必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<ul>
  <li>はじめに
    <ul>
      <li>第1章
        <ul>
          <li>セクション1.1</li>
        </ul>
      </li>
      <li>第2章</li>
    </ul>
  </li>
</ul>

現在の配列構造では、この入れ子構造を正しく生成するのは難しいです。

次回は、この階層構造を表現しようとして壁にぶつかる体験をします。

完成コード

今回作成したFlatTOCRendererクラスの完成コードです。

 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
package FlatTOCRenderer;
use v5.36;
use Moo;

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

has 'indent_size' => (
    is      => 'ro',
    default => 2,
);

sub render ($self) {
    my @lines;
    
    for my $h ($self->headings->@*) {
        my $indent = ' ' x (($h->{level} - 1) * $self->indent_size);
        my $marker = '- ';
        push @lines, $indent . $marker . $h->{text};
    }
    
    return join("\n", @lines);
}

1;

まとめ

今回は、抽出した見出しをインデント付きで表示するFlatTOCRendererクラスを作成しました。

学んだこと:

  • 繰り返し演算子xによる文字列の繰り返し
  • joinによる配列の文字列化
  • インデント計算によるシンプルな目次表示

現時点では、見出しは「フラットな配列」として扱われています。次回は、親子関係を表現しようとして、なぜそれが難しいのかを体験します。

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