about summary refs log tree commit diff
path: root/app/lib/toc_generator.rb
blob: 0c8f766ca42fdde089afdac7795c3ab5cbf81963 (plain) (blame)
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# frozen_string_literal: true

class TOCGenerator
  TARGET_ELEMENTS = %w(h1 h2 h3 h4 h5 h6).freeze
  LISTED_ELEMENTS = %w(h2 h3).freeze

  class Section
    attr_accessor :depth, :title, :children, :anchor

    def initialize(depth, title, anchor)
      @depth    = depth
      @title    = title
      @children = []
      @anchor   = anchor
    end

    delegate :<<, to: :children
  end

  def initialize(source_html)
    @source_html = source_html
    @processed   = false
    @target_html = ''
    @headers     = []
    @slugs       = Hash.new { |h, k| h[k] = 0 }
  end

  def html
    parse_and_transform unless @processed
    @target_html
  end

  def toc
    parse_and_transform unless @processed
    @headers
  end

  private

  def parse_and_transform
    return if @source_html.blank?

    parsed_html = Nokogiri::HTML.fragment(@source_html)

    parsed_html.traverse do |node|
      next unless TARGET_ELEMENTS.include?(node.name)

      anchor = node['id'] || node.text.parameterize.presence || 'sec'
      @slugs[anchor] += 1
      anchor = "#{anchor}-#{@slugs[anchor]}" if @slugs[anchor] > 1

      node['id'] = anchor

      next unless LISTED_ELEMENTS.include?(node.name)

      depth          = node.name[1..-1]
      latest_section = @headers.last

      if latest_section.nil? || latest_section.depth >= depth
        @headers << Section.new(depth, node.text, anchor)
      else
        latest_section << Section.new(depth, node.text, anchor)
      end
    end

    @target_html = parsed_html.to_s
    @processed   = true
  end
end