rails 目次の自動生成機能

今回は業務の中で目次の自動生成メソッドを開発する機会があったのでその実装を記録として残しておきます。

## 背景
今回の実装に至った背景として、columnページのデザイン改修がありました。その過程で、記事に対して目次を表示し、クリックで該当箇所へジャンプする機能を追加する必要が生じました。この機能は、Qiitaのような目次機能を参考にしています。また、記事のHTMLはデータベースのカラムに直接埋め込まれている仕様となっているため、この仕様に沿った形で目次機能を実装することが求められました。

## 実装
今回はメソッド化して使いまわせて、かつスタイルも自由に変更できるようにする設定を心がけました。

色々調べるとgemを組み合わせて使用する方法もありましたが、今回はどのサービスでも決められた仕様の場合には使い回しができるよう設定をしました。

まずカラムの中身は下記のようになっています。
```
<div><div class="ballonn-wrap customer"><div class="avater-box customer"></div><div class="bubble customer"><p>◯◯◯◯◯◯◯◯◯◯◯</p></div></div></div><div class="ballonn-wrap"><div class="avater-box expart"></div><div class="bubble expart"><p>◯◯◯◯◯◯◯◯◯◯◯</p></div></div><p>◯◯◯◯◯◯◯◯◯◯◯</p><p><br></p><p>◯◯◯◯◯◯◯◯◯◯◯</p><p><br></p><p>◯◯◯◯◯◯◯◯◯◯◯</p><p><br></p><p>◯◯◯◯◯◯◯◯◯◯◯</p><p><br></p><div><div class="expartIntroduction__box"><div class="expartIntroduction__img"></div><div class="expartIntroduction__txt-box"><p class="expartIntroduction__name">◯◯◯◯◯◯◯◯◯◯◯</p>
```

このデータを処理の前に「.html_safe」を行う必要があります。

### html_safeとは？
タグを含む文字列を「安全なHTML」としてRailsに認識させ、エスケープせずにそのまま出力できるようにします。これにより、解析したタグや変更されたHTMLコードが意図通りにブラウザで表示されるようになります。タグの解析や書き換え後、結果をそのままHTMLとして出力するために必要なメソッドです。

この処理を通した後にその値を下記メソットへ渡します。

```
  def generate_toc(html_content)
    @toc = "<ul class='toc__list'>"
    h2_counter = 0
    h3_counter = 0
    h2_id = nil
  
    # 正規表現で <h2> と <h3> をキャプチャし、中のテキストも取得
    html_content.scan(/(<h[2-3][^>]*>)(.*?)(<\/h[2-3]>)/m).each_with_index do |(opening_tag, content, closing_tag), index|
      # <h2> の場合
      if opening_tag.start_with?('<h2')
        h2_counter += 1
        h3_counter = 0
        h2_id = "section-#{h2_counter}"
  
        # タグ内のテキストを取得（ネストされた <span> や <strong> を除去）
        text = content.gsub(/<[^>]+>/, '').strip
        
        # 目次に <h2> の内容を追加
        @toc += "</ul></li>" if index.positive?
        @toc += "<li class='toc__item'><a href='##{h2_id}' class='toc__link'><span class='toc__number'>#{h2_counter}.</span> #{text}</a><ul class='toc__sub-list'>"
  
      # <h3> の場合
      elsif opening_tag.start_with?('<h3')
        if h2_id
          h3_counter += 1
          h3_id = "#{h2_id}-sub-#{h3_counter}"
  
          # タグ内のテキストを取得
          text = content.gsub(/<[^>]+>/, '').strip
  
          # 目次に <h3> の内容を追加
          @toc += "<li class='toc__sub-item'><a href='##{h3_id}' class='toc__sub-link'>#{text}</a></li>"
        end
      end
    end
  
    @toc += "</ul></li></ul>" if h2_id
  
    # HTMLの見出しタグにIDを追加
    modified_html = html_content.dup
    h2_counter = 0
    h3_counter = 0
    h2_id = nil
  
    # <h2>と<h3>タグにIDを追加する処理
    modified_html.scan(/(<h[2-3][^>]*>)(.*?)(<\/h[2-3]>)/m).each do |opening_tag, content, closing_tag|
      if opening_tag.start_with?('<h2')
        h2_counter += 1
        h3_counter = 0
        h2_id = "section-#{h2_counter}"
        
        # <h2> タグにIDを追加
        new_opening_tag = opening_tag.sub('<h2', "<h2 id='#{h2_id}'")
        new_headline = "#{new_opening_tag}#{content}#{closing_tag}"
        modified_html.sub!(opening_tag + content + closing_tag, new_headline)
  
      elsif opening_tag.start_with?('<h3')
        if h2_id
          h3_counter += 1
          h3_id = "#{h2_id}-sub-#{h3_counter}"
  
          # <h3> タグにIDを追加
          new_opening_tag = opening_tag.sub('<h3', "<h3 id='#{h3_id}'")
          new_headline = "#{new_opening_tag}#{content}#{closing_tag}"
          modified_html.sub!(opening_tag + content + closing_tag, new_headline)
        end
      end
    end
  
    @html_content = modified_html
    @toc
  end
```

このコードは、HTMLコンテンツから見出しタグ（h2とh3）を抽出し、それらを基に目次を生成します。各見出しにユニークなIDを付与し、そのIDに基づいて目次から対応する見出しへリンクできるようにします。生成された目次はulリストとして構築され、見出しタグは目次に対応するIDを含むように書き換えられます。

これにより、クリック可能な目次が作成できます。

あとは使用したいページに合わせてスタイルを設定することで適切に目次を生成できるようになります！

出力時には、下記のように出力できます。
```
.toc
  p.toc__title このページの内容
  = @toc.html_safe
```