1e4f12c12c
Improve the visual presentation of the Rails Guides, bringing them more inline with the new marketing rubyonrails.org site, as part of a project with the Rails Foundation. The CSS has been recreated from SCSS files, generated into the final pieces. The stylesrc directory contains those commented source files. The generate task will now process those scss => css files. Co-authored-by: Carlos Antonio da Silva <carlosantoniodasilva@gmail.com>
210 lines
6.1 KiB
Ruby
210 lines
6.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "redcarpet"
|
|
require "nokogiri"
|
|
require "rails_guides/markdown/renderer"
|
|
require "rails_guides/markdown/epub_renderer"
|
|
require "rails-html-sanitizer"
|
|
|
|
module RailsGuides
|
|
class Markdown
|
|
def initialize(view:, layout:, edge:, version:, epub:)
|
|
@view = view
|
|
@layout = layout
|
|
@edge = edge
|
|
@version = version
|
|
@index_counter = Hash.new(0)
|
|
@raw_header = ""
|
|
@node_ids = {}
|
|
@epub = epub
|
|
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 dom_ids by prefixing the parent node dom_id
|
|
if @node_ids[dom_id]
|
|
if @node_ids[dom_id].size > 1
|
|
duplicate_nodes = @node_ids.delete(dom_id)
|
|
new_node_id = dom_id_with_parent_node(dom_id, duplicate_nodes[-2])
|
|
duplicate_nodes.last[:id] = new_node_id
|
|
@node_ids[new_node_id] = duplicate_nodes
|
|
end
|
|
dom_id = dom_id_with_parent_node(dom_id, nodes[-2])
|
|
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 dom_id_with_parent_node(dom_id, parent_node)
|
|
if parent_node
|
|
[parent_node[:id], dom_id].join("-")
|
|
else
|
|
dom_id
|
|
end
|
|
end
|
|
|
|
def engine
|
|
renderer = @epub ? EpubRenderer : Renderer
|
|
@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?
|
|
document = html_fragment(@body).tap do |doc|
|
|
hierarchy = []
|
|
|
|
doc.children.each do |node|
|
|
if /^h[2-5]$/.match?(node.name)
|
|
case node.name
|
|
when "h2"
|
|
hierarchy = [node]
|
|
@headings_for_index << [1, node, node.inner_html]
|
|
when "h3"
|
|
hierarchy = hierarchy[0, 1] + [node]
|
|
@headings_for_index << [2, node, node.inner_html]
|
|
when "h4"
|
|
hierarchy = hierarchy[0, 2] + [node]
|
|
when "h5"
|
|
hierarchy = hierarchy[0, 3] + [node]
|
|
end
|
|
|
|
node[:id] = dom_id(hierarchy) unless node[:id]
|
|
node.inner_html = "<span>#{node_index(hierarchy)}</span> #{node.inner_html}"
|
|
end
|
|
end
|
|
|
|
doc.css("h2, h3, h4, h5").each do |node|
|
|
node.inner_html = "<a class='anchorlink' href='##{node[:id]}'>#{node.inner_html}</a>"
|
|
end
|
|
end
|
|
@body = @epub ? document.to_xhtml : document.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 = html_fragment(engine.render(raw_index)).tap do |doc|
|
|
doc.at("ol")[:class] = "chapters"
|
|
end.to_html
|
|
|
|
@index = <<-INDEX.html_safe
|
|
<nav id="subCol">
|
|
<h3 class="chapter">
|
|
<picture>
|
|
<!-- Using the `source` HTML tag to set the dark theme image -->
|
|
<source
|
|
srcset="images/icon_book-close-bookmark-1-wht.svg"
|
|
media="(prefers-color-scheme: dark)"
|
|
/>
|
|
<img src="images/icon_book-close-bookmark-1.svg" alt="Chapter Icon" />
|
|
</picture>
|
|
Chapters
|
|
</h3>
|
|
#{@index}
|
|
</nav>
|
|
INDEX
|
|
end
|
|
end
|
|
|
|
def generate_title
|
|
if heading = html_fragment(@header).at(:h1)
|
|
@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
|
|
|
|
def html_fragment(html)
|
|
if defined?(Nokogiri::HTML5)
|
|
Nokogiri::HTML5.fragment(html)
|
|
else
|
|
Nokogiri::HTML4.fragment(html)
|
|
end
|
|
end
|
|
end
|
|
end
|