2017-08-13 09:02:48 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-08-06 13:21:59 -04:00
|
|
|
require "redcarpet"
|
|
|
|
require "nokogiri"
|
|
|
|
require "rails_guides/markdown/renderer"
|
2019-01-04 02:37:24 -05:00
|
|
|
require "rails-html-sanitizer"
|
2012-08-31 12:01:06 -04:00
|
|
|
|
|
|
|
module RailsGuides
|
|
|
|
class Markdown
|
2017-02-12 04:21:20 -05:00
|
|
|
def initialize(view:, layout:, edge:, version:)
|
|
|
|
@view = view
|
|
|
|
@layout = layout
|
|
|
|
@edge = edge
|
|
|
|
@version = version
|
2012-08-31 17:49:17 -04:00
|
|
|
@index_counter = Hash.new(0)
|
2017-02-12 04:21:20 -05:00
|
|
|
@raw_header = ""
|
|
|
|
@node_ids = {}
|
2012-08-31 12:01:06 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def render(body)
|
2012-09-01 16:39:47 -04:00
|
|
|
@raw_body = body
|
|
|
|
extract_raw_header_and_body
|
2012-08-31 17:49:17 -04:00
|
|
|
generate_header
|
2019-01-04 02:37:24 -05:00
|
|
|
generate_description
|
2012-08-31 17:49:17 -04:00
|
|
|
generate_title
|
|
|
|
generate_body
|
|
|
|
generate_structure
|
|
|
|
generate_index
|
|
|
|
render_page
|
2012-08-31 12:01:06 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
2012-08-31 17:49:17 -04:00
|
|
|
def dom_id(nodes)
|
2012-09-16 23:12:45 -04:00
|
|
|
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)
|
2014-08-23 23:58:01 -04:00
|
|
|
escaped_chars = Regexp.escape('\\/`*_{}[]()#+-.!:,;|&<>^~=\'"')
|
|
|
|
|
2016-08-06 13:21:59 -04:00
|
|
|
text.downcase.gsub(/\?/, "-questionmark")
|
|
|
|
.gsub(/!/, "-bang")
|
|
|
|
.gsub(/[#{escaped_chars}]+/, " ").strip
|
|
|
|
.gsub(/\s+/, "-")
|
2012-08-31 17:49:17 -04:00
|
|
|
end
|
|
|
|
|
2012-08-31 12:01:06 -04:00
|
|
|
def engine
|
2016-08-09 17:36:39 -04:00
|
|
|
@engine ||= Redcarpet::Markdown.new(Renderer,
|
|
|
|
no_intra_emphasis: true,
|
2012-08-31 12:01:06 -04:00
|
|
|
fenced_code_blocks: true,
|
|
|
|
autolink: true,
|
|
|
|
strikethrough: true,
|
2012-09-01 21:37:50 -04:00
|
|
|
superscript: true,
|
2017-02-12 04:21:20 -05:00
|
|
|
tables: true
|
|
|
|
)
|
2012-08-31 12:01:06 -04:00
|
|
|
end
|
|
|
|
|
2012-09-01 16:39:47 -04:00
|
|
|
def extract_raw_header_and_body
|
2018-07-28 17:37:17 -04:00
|
|
|
if /^\-{40,}$/.match?(@raw_body)
|
2012-09-01 16:39:47 -04:00
|
|
|
@raw_header, _, @raw_body = @raw_body.partition(/^\-{40,}$/).map(&:strip)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-08-31 17:49:17 -04:00
|
|
|
def generate_body
|
|
|
|
@body = engine.render(@raw_body)
|
|
|
|
end
|
|
|
|
|
|
|
|
def generate_header
|
|
|
|
@header = engine.render(@raw_header).html_safe
|
|
|
|
end
|
|
|
|
|
2019-01-04 02:37:24 -05:00
|
|
|
def generate_description
|
|
|
|
sanitizer = Rails::Html::FullSanitizer.new
|
|
|
|
@description = sanitizer.sanitize(@header).squish
|
|
|
|
end
|
|
|
|
|
2012-08-31 17:49:17 -04:00
|
|
|
def generate_structure
|
2012-09-16 23:12:45 -04:00
|
|
|
@headings_for_index = []
|
2012-09-01 16:39:47 -04:00
|
|
|
if @body.present?
|
2013-10-27 15:14:41 -04:00
|
|
|
@body = Nokogiri::HTML.fragment(@body).tap do |doc|
|
2012-09-01 16:39:47 -04:00
|
|
|
hierarchy = []
|
|
|
|
|
2013-10-27 15:14:41 -04:00
|
|
|
doc.children.each do |node|
|
2018-07-28 17:37:17 -04:00
|
|
|
if /^h[3-6]$/.match?(node.name)
|
2012-09-01 16:39:47 -04:00
|
|
|
case node.name
|
2016-08-06 13:21:59 -04:00
|
|
|
when "h3"
|
2012-09-01 16:39:47 -04:00
|
|
|
hierarchy = [node]
|
2012-09-16 23:12:45 -04:00
|
|
|
@headings_for_index << [1, node, node.inner_html]
|
2016-08-06 13:21:59 -04:00
|
|
|
when "h4"
|
2012-09-01 16:39:47 -04:00
|
|
|
hierarchy = hierarchy[0, 1] + [node]
|
2012-09-16 23:12:45 -04:00
|
|
|
@headings_for_index << [2, node, node.inner_html]
|
2016-08-06 13:21:59 -04:00
|
|
|
when "h5"
|
2012-09-01 16:39:47 -04:00
|
|
|
hierarchy = hierarchy[0, 2] + [node]
|
2016-08-06 13:21:59 -04:00
|
|
|
when "h6"
|
2012-09-01 16:39:47 -04:00
|
|
|
hierarchy = hierarchy[0, 3] + [node]
|
|
|
|
end
|
2012-08-31 17:49:17 -04:00
|
|
|
|
2018-09-23 06:57:36 -04:00
|
|
|
node[:id] = dom_id(hierarchy) unless node[:id]
|
2012-09-03 21:21:04 -04:00
|
|
|
node.inner_html = "#{node_index(hierarchy)} #{node.inner_html}"
|
2012-09-01 16:39:47 -04:00
|
|
|
end
|
2012-08-31 17:49:17 -04:00
|
|
|
end
|
2017-04-02 00:35:20 -04:00
|
|
|
|
2017-04-19 20:02:03 -04:00
|
|
|
doc.css("h3, h4, h5, h6").each do |node|
|
2017-04-02 00:35:20 -04:00
|
|
|
node.inner_html = "<a class='anchorlink' href='##{node[:id]}'>#{node.inner_html}</a>"
|
|
|
|
end
|
2012-09-01 16:39:47 -04:00
|
|
|
end.to_html
|
|
|
|
end
|
2012-08-31 17:49:17 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def generate_index
|
2012-09-16 23:12:45 -04:00
|
|
|
if @headings_for_index.present?
|
2016-08-06 13:21:59 -04:00
|
|
|
raw_index = ""
|
2012-09-16 23:12:45 -04:00
|
|
|
@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
|
|
|
|
|
2013-10-27 15:14:41 -04:00
|
|
|
@index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc|
|
2016-08-06 13:21:59 -04:00
|
|
|
doc.at("ol")[:class] = "chapters"
|
2012-09-01 16:39:47 -04:00
|
|
|
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
|
2012-08-31 17:49:17 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def generate_title
|
2013-10-27 15:14:41 -04:00
|
|
|
if heading = Nokogiri::HTML.fragment(@header).at(:h2)
|
|
|
|
@title = "#{heading.text} — Ruby on Rails Guides"
|
2012-09-01 16:39:47 -04:00
|
|
|
else
|
|
|
|
@title = "Ruby on Rails Guides"
|
|
|
|
end
|
2012-08-31 17:49:17 -04:00
|
|
|
end
|
2012-08-31 12:01:06 -04:00
|
|
|
|
2012-08-31 17:49:17 -04:00
|
|
|
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}"
|
2012-08-31 12:01:06 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-08-31 17:49:17 -04:00
|
|
|
def render_page
|
|
|
|
@view.content_for(:header_section) { @header }
|
2019-01-04 02:37:24 -05:00
|
|
|
@view.content_for(:description) { @description }
|
2012-08-31 17:49:17 -04:00
|
|
|
@view.content_for(:page_title) { @title }
|
2012-09-01 16:39:47 -04:00
|
|
|
@view.content_for(:index_section) { @index }
|
2016-10-25 09:18:34 -04:00
|
|
|
@view.render(layout: @layout, html: @body.html_safe)
|
2012-08-31 12:01:06 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|