aboutsummaryrefslogblamecommitdiffstats
path: root/guides/rails_guides/markdown.rb
blob: 018f49ffd008df07ef5ac9d8fd04c87ec5296451 (plain) (tree)
1
2
3
4
5
6
7
8
9

                             


                                        
                              


                  




                                                   
                                  

                         


                    

                                 
                     
                          




                        


           

                       


















                                                                                    

                                                                       



                                                            

         
                

                                                     


                                   
                            

                      

         
                                     
                                        



                                                                                   







                                                      




                                                         
                            
                                
                         
                                                             

                          
                                       
                                             
                              
                         
                                    
                                                                   
                         
                                                      
                                                                   
                         
                                                      
                         
                                                      
                   
 
                                                              
                                                                               
                 
               
 
                                                    

                                                                                                   

                     


                        
                                       
                        







                                                              
                                                                                 
                                             








                                                                                          


                        

                                                             


                                         
         
 












                                                                                                    


           

                                                      
                                                        
                                                 
                                                    
                                                            


         
# frozen_string_literal: true

require "redcarpet"
require "nokogiri"
require "rails_guides/markdown/renderer"
require "rails-html-sanitizer"

module RailsGuides
  class Markdown
    def initialize(view:, layout:, edge:, version:)
      @view          = view
      @layout        = layout
      @edge          = edge
      @version       = version
      @index_counter = Hash.new(0)
      @raw_header    = ""
      @node_ids      = {}
    end

    def render(body)
      @raw_body = body
      extract_raw_header_and_body
      generate_header
      generate_description
      generate_title
      generate_body
      generate_structure
      generate_index
      render_page
    end

    private

      def dom_id(nodes)
        dom_id = dom_id_text(nodes.last.text)

        # Fix duplicate node by prefix with its parent node
        if @node_ids[dom_id]
          if @node_ids[dom_id].size > 1
            duplicate_nodes = @node_ids.delete(dom_id)
            new_node_id = "#{duplicate_nodes[-2][:id]}-#{duplicate_nodes.last[:id]}"
            duplicate_nodes.last[:id] = new_node_id
            @node_ids[new_node_id] = duplicate_nodes
          end

          dom_id = "#{nodes[-2][:id]}-#{dom_id}"
        end

        @node_ids[dom_id] = nodes
        dom_id
      end

      def dom_id_text(text)
        escaped_chars = Regexp.escape('\\/`*_{}[]()#+-.!:,;|&<>^~=\'"')

        text.downcase.gsub(/\?/, "-questionmark")
                     .gsub(/!/, "-bang")
                     .gsub(/[#{escaped_chars}]+/, " ").strip
                     .gsub(/\s+/, "-")
      end

      def engine
        @engine ||= Redcarpet::Markdown.new(Renderer,
          no_intra_emphasis: true,
          fenced_code_blocks: true,
          autolink: true,
          strikethrough: true,
          superscript: true,
          tables: true
        )
      end

      def extract_raw_header_and_body
        if /^\-{40,}$/.match?(@raw_body)
          @raw_header, _, @raw_body = @raw_body.partition(/^\-{40,}$/).map(&:strip)
        end
      end

      def generate_body
        @body = engine.render(@raw_body)
      end

      def generate_header
        @header = engine.render(@raw_header).html_safe
      end

      def generate_description
        sanitizer = Rails::Html::FullSanitizer.new
        @description = sanitizer.sanitize(@header).squish
      end

      def generate_structure
        @headings_for_index = []
        if @body.present?
          @body = Nokogiri::HTML.fragment(@body).tap do |doc|
            hierarchy = []

            doc.children.each do |node|
              if /^h[3-6]$/.match?(node.name)
                case node.name
                when "h3"
                  hierarchy = [node]
                  @headings_for_index << [1, node, node.inner_html]
                when "h4"
                  hierarchy = hierarchy[0, 1] + [node]
                  @headings_for_index << [2, node, node.inner_html]
                when "h5"
                  hierarchy = hierarchy[0, 2] + [node]
                when "h6"
                  hierarchy = hierarchy[0, 3] + [node]
                end

                node[:id] = dom_id(hierarchy) unless node[:id]
                node.inner_html = "#{node_index(hierarchy)} #{node.inner_html}"
              end
            end

            doc.css("h3, h4, h5, h6").each do |node|
              node.inner_html = "<a class='anchorlink' href='##{node[:id]}'>#{node.inner_html}</a>"
            end
          end.to_html
        end
      end

      def generate_index
        if @headings_for_index.present?
          raw_index = ""
          @headings_for_index.each do |level, node, label|
            if level == 1
              raw_index += "1. [#{label}](##{node[:id]})\n"
            elsif level == 2
              raw_index += "    * [#{label}](##{node[:id]})\n"
            end
          end

          @index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc|
            doc.at("ol")[:class] = "chapters"
          end.to_html

          @index = <<-INDEX.html_safe
          <div id="subCol">
            <h3 class="chapter"><img src="images/chapters_icon.gif" alt="" />Chapters</h3>
            #{@index}
          </div>
          INDEX
        end
      end

      def generate_title
        if heading = Nokogiri::HTML.fragment(@header).at(:h2)
          @title = "#{heading.text} — Ruby on Rails Guides"
        else
          @title = "Ruby on Rails Guides"
        end
      end

      def node_index(hierarchy)
        case hierarchy.size
        when 1
          @index_counter[2] = @index_counter[3] = @index_counter[4] = 0
          "#{@index_counter[1] += 1}"
        when 2
          @index_counter[3] = @index_counter[4] = 0
          "#{@index_counter[1]}.#{@index_counter[2] += 1}"
        when 3
          @index_counter[4] = 0
          "#{@index_counter[1]}.#{@index_counter[2]}.#{@index_counter[3] += 1}"
        when 4
          "#{@index_counter[1]}.#{@index_counter[2]}.#{@index_counter[3]}.#{@index_counter[4] += 1}"
        end
      end

      def render_page
        @view.content_for(:header_section) { @header }
        @view.content_for(:description) { @description }
        @view.content_for(:page_title) { @title }
        @view.content_for(:index_section) { @index }
        @view.render(layout: @layout, html: @body.html_safe)
      end
  end
end