diff options
Diffstat (limited to 'guides/rails_guides')
-rw-r--r-- | guides/rails_guides/generator.rb | 73 | ||||
-rw-r--r-- | guides/rails_guides/markdown.rb | 161 | ||||
-rw-r--r-- | guides/rails_guides/markdown/renderer.rb | 82 | ||||
-rw-r--r-- | guides/rails_guides/textile_extensions.rb | 67 |
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 |