about summary refs log tree commit diff
path: root/app/lib/toc_generator.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/lib/toc_generator.rb')
-rw-r--r--app/lib/toc_generator.rb69
1 files changed, 69 insertions, 0 deletions
diff --git a/app/lib/toc_generator.rb b/app/lib/toc_generator.rb
new file mode 100644
index 000000000..0c8f766ca
--- /dev/null
+++ b/app/lib/toc_generator.rb
@@ -0,0 +1,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