# 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