radar/twist-v2

View on GitHub
backend/lib/twist/processors/asciidoc/chapter_processor.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Twist
  module Processors
    module Asciidoc
      class ChapterProcessor
        include Sidekiq::Worker

        sidekiq_options retry: 0

        include Import[
          "repositories.image_repo",
          "repositories.element_repo",
          "repositories.commit_repo",
          "repositories.footnote_repo",
          "repositories.chapter_repo",
          "repositories.book_repo"
        ]

        def perform(book_permalink, chapter_id, chapter)
          fragment = Nokogiri::HTML::DocumentFragment.parse(chapter)
          @book = book_repo.find_by_permalink(book_permalink)
          @chapter = chapter_repo.by_id(chapter_id)
          @commit = commit_repo.by_id(@chapter.commit_id)
          element_repo.delete_all_chapter_elements(chapter_id)

          content = fragment.children.first.children

          content.css("sup.footnote a").each do |footnote|
            process_footnote(footnote)
          end

          process_content(content)
        end

        private

        attr_reader :book, :chapter, :commit, :chapter_element

        def process_content(elements)
          elements.each do |element|
            m = "process_#{element.name}"
            if respond_to?(m, true)
              send(m, element)
            else
              puts element.to_html
              raise "I don't know how to process a #{element.name}!"
            end
          end
        end

        def process_text(element)
          # TODO: Is there any blank text we wish to include?
          element.text
        end

        # Real HTML elements that aren't divs

        # H2s are chapter titles, but Twist would rather they be h1s.
        # H1s in Asciidoc-land are book titles
        def process_h2(element)
          # Chapter titles are already handled, so no need to create one.
          # chapter.elements.create!(
          #   tag: "h1",
          #   content: element.to_html
          # )
        end

        def process_h3(element)
          create_header("h2", element)
        end

        def process_h4(element)
          create_header("h3", element)
        end

        def process_h5(element)
          create_header("h4", element)
        end

        def create_header(tag, element)
          element.name = tag
          create_element(
            tag: tag,
            identifier: element.text.gsub(/^[\d|.]+/, "").to_slug.normalize.to_s,
            content: element.to_html,
          )
        end

        def process_table(element)
          create_element(
            tag: "table",
            content: element.to_html,
          )
        end

        def process_hr(_element)
          create_element(
            tag: "hr",
          )
        end

        # Div-ified elements after this point.

        def process_div(element)
          return unless element["class"]

          classes = element["class"].split(" ")

          processed = false

          classes.each do |klass|
            if respond_to?("process_#{klass}", true)
              send("process_#{klass}", element)
              processed = true
            end
          end

          return if processed

          case element["class"]
          when "sectionbody", "sect2", "sect3", "sect4"
            process_content(element.children)
          else
            raise "Unknown div #{element["class"]}!"
          end
        end

        def process_paragraph(element)
          p_tag = element.css("p")

          return if p_tag.text.empty?

          create_element(
            tag: "p",
            content: element.css("p").to_html,
          )
        end

        def process_footnote(footnote)
          identifier = footnote["href"][1..-1]
          footnote_repo.link_to_commit_chapter(identifier, commit.id, chapter.id)
        end

        def process_literalblock(element)
          create_element(
            tag: "div",
            content: element.to_html,
          )
        end

        def process_listingblock(element)
          if element.css("pre code").any?
            return process_language_block(element)
          end

          element['class'] += " lang-plaintext"
          create_element(
            tag: "div",
            content: element.to_html,
          )
        end

        def process_language_block(element)
          code = element.css("pre code").first
          lang = case code['data-lang']
                when 'yml' then 'yaml'
                else code['data-lang']
                end

          highlighted_code = Pygments.highlight(code.text, lexer: lang)
          title = element.css(".title").text
          html = %{<div class="listingblock highlighted lang-#{lang}">}
          html << %{<div class="title">#{title}</div>} unless title.empty?
          html << %{<div class="content">#{highlighted_code}</div>}
          html << %{</div>}

          create_element(
            tag: "div",
            content: html,
          )
        end

        def process_imageblock(element)
          src = element.css("img").first["src"]
          candidates = Dir[book.path + "**/#{src}"]
          if candidates.any?
            # TODO: what if more than one image matches the path?
            image_path = candidates.first
            if File.exist?(image_path)
              caption = element.css("div.title").text.strip
            end
          else
            image_path = Twist::Container.root + "public/images/image_missing.png"
            caption = "Image missing: #{src}"
          end

          image = image_repo.find_or_create_image(chapter.id, File.basename(image_path), image_path, caption)

          create_element(
            tag: "img",
            content: src,
            extra: { image_id: image.id },
          )
        end

        def process_ulist(element)
          create_element(
            tag: "ul",
            content: element.css("ul").to_html,
          )
        end

        def process_olist(element)
          create_element(
            tag: "ol",
            content: element.css("ol").to_html
          )
        end

        def process_quoteblock(element)
          create_element(
            tag: "div",
            content: element.to_html
          )
        end

        def process_sidebarblock(element)
          create_element(
            tag: "div",
            content: element,
          )
        end

        def process_admonitionblock(element)
          create_element(
            tag: "div",
            content: <<-HTML.strip
              <div class="#{element["class"]}">
                #{element.css(".content").first.inner_html.strip}
              </div>
            HTML
          )
        end

        def create_element(tag:, content: "", identifier: nil, extra: {})
          raise "Stringify content before passing it to this method!" unless content.is_a?(String)

          element_repo.create({
            chapter_id: chapter.id,
            tag: tag,
            identifier: identifier,
            content: content,
          }.merge(extra))
        end
      end
    end
  end
end