aboutsummaryrefslogtreecommitdiffstats
path: root/guides/rails_guides
diff options
context:
space:
mode:
Diffstat (limited to 'guides/rails_guides')
-rw-r--r--guides/rails_guides/generator.rb73
-rw-r--r--guides/rails_guides/markdown.rb161
-rw-r--r--guides/rails_guides/markdown/renderer.rb82
-rw-r--r--guides/rails_guides/textile_extensions.rb67
4 files changed, 252 insertions, 131 deletions
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index 230bebf3bb..3b124ef236 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -15,7 +15,7 @@
#
# Internal links (anchors) are checked. If a reference is broken levenshtein
# distance is used to suggest an existing one. This is useful since IDs are
-# generated by Textile from headers and thus edits alter them.
+# generated by Markdown from headers and thus edits alter them.
#
# Also detects duplicated IDs. They happen if there are headers with the same
# text. Please do resolve them, if any, so guides are valid XHTML.
@@ -65,7 +65,7 @@ module RailsGuides
class Generator
attr_reader :guides_dir, :source_dir, :output_dir, :edge, :warnings, :all
- GUIDES_RE = /\.(?:textile|erb)$/
+ GUIDES_RE = /\.(?:erb|md)\z/
def initialize(output=nil)
set_flags_from_environment
@@ -152,6 +152,7 @@ module RailsGuides
if kindle?
Dir.entries("#{source_dir}/kindle").grep(GUIDES_RE).map do |entry|
+ next if entry == 'KINDLE.md'
guides << "kindle/#{entry}"
end
end
@@ -171,10 +172,10 @@ module RailsGuides
end
def output_file_for(guide)
- if guide =~/\.textile$/
- guide.sub(/\.textile$/, '.html')
+ if guide.end_with?('.md')
+ guide.sub(/md\z/, 'html')
else
- guide.sub(/\.erb$/, '')
+ guide.sub(/\.erb\z/, '')
end
end
@@ -203,10 +204,7 @@ module RailsGuides
result = view.render(:layout => layout, :formats => [$1], :file => $`)
else
body = File.read(File.join(source_dir, guide))
- body = set_header_section(body, view)
- body = set_index(body, view)
-
- result = view.render(:layout => layout, :text => textile(body))
+ result = RailsGuides::Markdown.new(view, layout).render(body)
warn_about_broken_links(result) if @warnings
end
@@ -215,70 +213,17 @@ module RailsGuides
end
end
- def set_header_section(body, view)
- new_body = body.gsub(/(.*?)endprologue\./m, '').strip
- header = $1
-
- header =~ /h2\.(.*)/
- page_title = "Ruby on Rails Guides: #{$1.strip}"
-
- header = textile(header)
-
- view.content_for(:page_title) { page_title.html_safe }
- view.content_for(:header_section) { header.html_safe }
- new_body
- end
-
- def set_index(body, view)
- index = <<-INDEX
- <div id="subCol">
- <h3 class="chapter"><img src="images/chapters_icon.gif" alt="" />Chapters</h3>
- <ol class="chapters">
- INDEX
-
- i = Indexer.new(body, warnings)
- i.index
-
- # Set index for 2 levels
- i.level_hash.each do |key, value|
- link = view.content_tag(:a, :href => key[:id]) { textile(key[:title], true).html_safe }
-
- children = value.keys.map do |k|
- view.content_tag(:li,
- view.content_tag(:a, :href => k[:id]) { textile(k[:title], true).html_safe })
- end
-
- children_ul = children.empty? ? "" : view.content_tag(:ul, children.join(" ").html_safe)
-
- index << view.content_tag(:li, link.html_safe + children_ul.html_safe)
- end
-
- index << '</ol>'
- index << '</div>'
-
- view.content_for(:index_section) { index.html_safe }
-
- i.result
- end
-
- def textile(body, lite_mode=false)
- t = RedCloth.new(body)
- t.hard_breaks = false
- t.lite_mode = lite_mode
- t.to_html(:notestuff, :plusplus, :code)
- end
-
def warn_about_broken_links(html)
anchors = extract_anchors(html)
check_fragment_identifiers(html, anchors)
end
def extract_anchors(html)
- # Textile generates headers with IDs computed from titles.
+ # Markdown generates headers with IDs computed from titles.
anchors = Set.new
html.scan(/<h\d\s+id="([^"]+)/).flatten.each do |anchor|
if anchors.member?(anchor)
- puts "*** DUPLICATE ID: #{anchor}, please use an explicit ID, e.g. h4(#explicit-id), or consider rewording"
+ puts "*** DUPLICATE ID: #{anchor}, please make sure that there're no headings with the same name at the same level."
else
anchors << anchor
end
diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb
new file mode 100644
index 0000000000..650489e6cb
--- /dev/null
+++ b/guides/rails_guides/markdown.rb
@@ -0,0 +1,161 @@
+require 'redcarpet'
+require 'nokogiri'
+require 'rails_guides/markdown/renderer'
+
+module RailsGuides
+ class Markdown
+ def initialize(view, layout)
+ @view = view
+ @layout = layout
+ @index_counter = Hash.new(0)
+ @raw_header = ''
+ @node_ids = {}
+ end
+
+ def render(body)
+ @raw_body = body
+ extract_raw_header_and_body
+ generate_header
+ 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)
+ text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/[^a-z0-9]+/, ' ')
+ .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 @raw_body =~ /^\-{40,}$/
+ @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_structure
+ @headings_for_index = []
+ if @body.present?
+ @body = Nokogiri::HTML(@body).tap do |doc|
+ hierarchy = []
+
+ doc.at('body').children.each do |node|
+ if node.name =~ /^h[3-6]$/
+ 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)
+ node.inner_html = "#{node_index(hierarchy)} #{node.inner_html}"
+ end
+ 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(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(@header).at(:h2)
+ @title = "Ruby on Rails Guides: #{heading.text}".html_safe
+ 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(:page_title) { @title }
+ @view.content_for(:index_section) { @index }
+ @view.render(:layout => @layout, :text => @body)
+ end
+ end
+end
diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb
new file mode 100644
index 0000000000..2f36af1fb3
--- /dev/null
+++ b/guides/rails_guides/markdown/renderer.rb
@@ -0,0 +1,82 @@
+module RailsGuides
+ class Markdown
+ class Renderer < Redcarpet::Render::HTML
+ def initialize(options={})
+ super
+ end
+
+ def block_code(code, language)
+ <<-HTML
+<div class="code_container">
+<pre class="brush: #{brush_for(language)}; gutter: false; toolbar: false">
+#{ERB::Util.h(code).strip}
+</pre>
+</div>
+HTML
+ end
+
+ def header(text, header_level)
+ # Always increase the heading level by, so we can use h1, h2 heading in the document
+ header_level += 1
+
+ %(<h#{header_level}>#{text}</h#{header_level}>)
+ end
+
+ def paragraph(text)
+ if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)/
+ convert_notes(text)
+ elsif text =~ /^\[<sup>(\d+)\]:<\/sup> (.+)$/
+ linkback = %(<a href="#footnote-#{$1}-ref"><sup>#{$1}</sup></a>)
+ %(<p class="footnote" id="footnote-#{$1}">#{linkback} #{$2}</p>)
+ else
+ text = convert_footnotes(text)
+ "<p>#{text}</p>"
+ end
+ end
+
+ private
+
+ def convert_footnotes(text)
+ text.gsub(/\[<sup>(\d+)\]<\/sup>/i) do
+ %(<sup class="footnote" id="footnote-#{$1}-ref">) +
+ %(<a href="#footnote-#{$1}">#{$1}</a></sup>)
+ end
+ end
+
+ def brush_for(code_type)
+ case code_type
+ when 'ruby', 'sql', 'plain'
+ code_type
+ when 'erb'
+ 'ruby; html-script: true'
+ when 'html'
+ 'xml' # html is understood, but there are .xml rules in the CSS
+ else
+ 'plain'
+ end
+ end
+
+ def convert_notes(body)
+ # The following regexp detects special labels followed by a
+ # paragraph, perhaps at the end of the document.
+ #
+ # It is important that we do not eat more than one newline
+ # because formatting may be wrong otherwise. For example,
+ # if a bulleted list follows the first item is not rendered
+ # as a list item, but as a paragraph starting with a plain
+ # asterisk.
+ body.gsub(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)(\n(?=\n)|\Z)/m) do |m|
+ css_class = case $1
+ when 'CAUTION', 'IMPORTANT'
+ 'warning'
+ when 'TIP'
+ 'info'
+ else
+ $1.downcase
+ end
+ %(<div class="#{css_class}"><p>#{$2.strip}</p></div>)
+ end
+ end
+ end
+ end
+end
diff --git a/guides/rails_guides/textile_extensions.rb b/guides/rails_guides/textile_extensions.rb
deleted file mode 100644
index 1faddd4ca0..0000000000
--- a/guides/rails_guides/textile_extensions.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-module RedCloth::Formatters::HTML
- def emdash(opts)
- "--"
- end
-end
-
-module RailsGuides
- module TextileExtensions
- def notestuff(body)
- # The following regexp detects special labels followed by a
- # paragraph, perhaps at the end of the document.
- #
- # It is important that we do not eat more than one newline
- # because formatting may be wrong otherwise. For example,
- # if a bulleted list follows the first item is not rendered
- # as a list item, but as a paragraph starting with a plain
- # asterisk.
- body.gsub!(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO)[.:](.*?)(\n(?=\n)|\Z)/m) do |m|
- css_class = case $1
- when 'CAUTION', 'IMPORTANT'
- 'warning'
- when 'TIP'
- 'info'
- else
- $1.downcase
- end
- %Q(<div class="#{css_class}"><p>#{$2.strip}</p></div>)
- end
- end
-
- def plusplus(body)
- body.gsub!(/\+(.*?)\+/) do |m|
- "<notextile><tt>#{$1}</tt></notextile>"
- end
-
- # The real plus sign
- body.gsub!('<plus>', '+')
- end
-
- def brush_for(code_type)
- case code_type
- when 'ruby', 'sql', 'plain'
- code_type
- when 'erb'
- 'ruby; html-script: true'
- when 'html'
- 'xml' # html is understood, but there are .xml rules in the CSS
- else
- 'plain'
- end
- end
-
- def code(body)
- body.gsub!(%r{<(yaml|shell|ruby|erb|html|sql|plain)>(.*?)</\1>}m) do |m|
- <<HTML
-<notextile>
-<div class="code_container">
-<pre class="brush: #{brush_for($1)}; gutter: false; toolbar: false">
-#{ERB::Util.h($2).strip}
-</pre>
-</div>
-</notextile>
-HTML
- end
- end
- end
-end