# frozen_string_literal: true module Epub class Navigation Node = Struct.new(:level, :entry, :parent, keyword_init: true) Entry = Struct.new(:title, :link, :navigation, keyword_init: true) SELECTOR = "h1, h2[id], h3[id], h4[id], h5[id], h6[id]" # Go through each files, sequentially and extract the table of contents # hierarchy, so you don't have to do it yourself. # # Notice that only `h2-h6` headings with an `id` attribute will be added to # the list. `h1` headings will always be added; if they don't have an id, # then they'll be linked to the file itself. # # The output structure doesn't look like the one you're expecting, make sure # your headings have the `id` attribute. # def self.extract_html(files, root_dir:) navigation = extract(files, root_dir:) html = renderer(navigation) <<~HTML HTML end def self.renderer(navigation) return "" if navigation.empty? html = [] html << "
    " navigation.each do |item| title = CGI.escape_html(item.title) html << "
  1. \n" html << %[#{title}] html << renderer(item.navigation) html << "\n
  2. " end html << "
" html.join end def self.extract(files, root_dir:) root = Node.new(level: 0, entry: Entry.new(navigation: [])) current = root single_file = files.size == 1 sections = files.map do |file| { html: Nokogiri::HTML(File.read(file)), path: Pathname.new(file).relative_path_from(root_dir).to_s } end sections.each do |section| section[:html].css(SELECTOR).each do |node| title = node.text.strip level = node.name[1].to_i link = if single_file "##{node.attributes['id']}" else "#{section[:path]}##{node.attributes['id']}" end entry = Entry.new(title:, link:, navigation: []) if level > current.level current = Node.new(level:, entry:, parent: current) elsif level == current.level current = Node.new(level:, entry:, parent: current.parent) else while current.parent && current.parent.level >= level current = current.parent end current = Node.new(level:, entry:, parent: current.parent) end current.parent.entry[:navigation] << entry end end root.entry[:navigation] end end end