Skip to content

Commit

Permalink
revises guides generation
Browse files Browse the repository at this point in the history
  • Loading branch information
fxn committed Aug 20, 2010
1 parent d35a67b commit add3ccb
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 100 deletions.
37 changes: 13 additions & 24 deletions railties/guides/rails_guides.rb
@@ -1,42 +1,31 @@
pwd = File.dirname(__FILE__) pwd = File.dirname(__FILE__)
$: << pwd $:.unshift pwd

# Loading Action Pack requires rack and erubis.
require 'rubygems'


begin begin
# Guides generation in the Rails repo.
as_lib = File.join(pwd, "../../activesupport/lib") as_lib = File.join(pwd, "../../activesupport/lib")
ap_lib = File.join(pwd, "../../actionpack/lib") ap_lib = File.join(pwd, "../../actionpack/lib")


$: << as_lib if File.directory?(as_lib) $:.unshift as_lib if File.directory?(as_lib)
$: << ap_lib if File.directory?(ap_lib) $:.unshift ap_lib if File.directory?(ap_lib)

require "action_controller"
require "action_view"
rescue LoadError rescue LoadError
require 'rubygems' # Guides generation from gems.
gem "actionpack", '>= 2.3' gem "actionpack", '>= 2.3'

require "action_controller"
require "action_view"
end end


begin begin
require 'rubygems'
gem 'RedCloth', '>= 4.1.1' gem 'RedCloth', '>= 4.1.1'
require 'redcloth'
rescue Gem::LoadError rescue Gem::LoadError
$stderr.puts %(Generating Guides requires RedCloth 4.1.1+) $stderr.puts('Generating guides requires RedCloth 4.1.1+.')
exit 1 exit 1
end end


require 'redcloth' require "rails_guides/textile_extensions"

module RailsGuides
autoload :Generator, "rails_guides/generator"
autoload :Indexer, "rails_guides/indexer"
autoload :Helpers, "rails_guides/helpers"
autoload :TextileExtensions, "rails_guides/textile_extensions"
end

RedCloth.send(:include, RailsGuides::TextileExtensions) RedCloth.send(:include, RailsGuides::TextileExtensions)


if $0 == __FILE__ require "rails_guides/generator"
RailsGuides::Generator.new.generate RailsGuides::Generator.new.generate
end
218 changes: 169 additions & 49 deletions railties/guides/rails_guides/generator.rb
@@ -1,63 +1,149 @@
# ---------------------------------------------------------------------------
#
# This script generates the guides. It can be invoked either directly or via the
# generate_guides rake task within the railties directory.
#
# Guides are taken from the source directory, and the resulting HTML goes into the
# output directory. Assets are stored under files, and copied to output/files as
# part of the generation process.
#
# Some arguments may be passed via environment variables:
#
# WARNINGS
# If you are writing a guide, please work always with WARNINGS=1. Users can
# generate the guides, and thus this flag is off by default.
#
# 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.
#
# 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.
#
# ALL
# Set to "1" to force the generation of all guides.
#
# ONLY
# Use ONLY if you want to generate only one or a set of guides. Prefixes are
# enough:
#
# # generates only association_basics.html
# ONLY=assoc ruby rails_guides.rb
#
# Separate many using commas:
#
# # generates only
# ONLY=assoc,migrations ruby rails_guides.rb
#
# Note that if you are working on a guide generation will by default process
# only that one, so ONLY is rarely used nowadays.
#
# EDGE
# Set to "1" to indicate generated guides should be marked as edge. This
# inserts a badge and changes the preamble of the home page.
#
# ---------------------------------------------------------------------------

require 'set' require 'set'
require 'fileutils'

require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/object/blank'
require 'action_controller'
require 'action_view'

require 'rails_guides/indexer'
require 'rails_guides/helpers'
require 'rails_guides/levenshtein'


module RailsGuides module RailsGuides
class Generator class Generator
attr_reader :output, :view_path, :view, :guides_dir attr_reader :guides_dir, :source_dir, :output_dir, :edge, :warnings, :all


def initialize(output = nil) GUIDES_RE = /\.(?:textile|html\.erb)$/
@guides_dir = File.join(File.dirname(__FILE__), '..')


@output = output || File.join(@guides_dir, "output") def initialize(output=nil)
initialize_dirs(output)
create_output_dir_if_needed
set_flags_from_environment
end


unless ENV["ONLY"] def generate
FileUtils.rm_r(@output) if File.directory?(@output) generate_guides
FileUtils.mkdir(@output) copy_assets
end end

private
def initialize_dirs(output)
@guides_dir = File.join(File.dirname(__FILE__), '..')
@source_dir = File.join(@guides_dir, "source")
@output_dir = output || File.join(@guides_dir, "output")
end


@view_path = File.join(@guides_dir, "source") def create_output_dir_if_needed
FileUtils.mkdir_p(output_dir)
end end


def generate def set_flags_from_environment
guides = Dir.entries(view_path).find_all {|g| g =~ /textile$/ } @edge = ENV['EDGE'] == '1'
@warnings = ENV['WARNINGS'] == '1'
@all = ENV['ALL'] == '1'
end


if ENV["ONLY"] def generate_guides
only = ENV["ONLY"].split(",").map{|x| x.strip }.map {|o| "#{o}.textile" } guides_to_generate.each do |guide|
guides = guides.find_all {|g| only.include?(g) } output_file = output_file_for(guide)
puts "GENERATING ONLY #{guides.inspect}" generate_guide(guide, output_file) if generate?(guide, output_file)
end end
end


guides.each do |guide| def guides_to_generate
generate_guide(guide) guides = Dir.entries(source_dir).grep(GUIDES_RE)
ENV.key?('ONLY') ? select_only(guides) : guides
end

def select_only(guides)
prefixes = ENV['ONLY'].split(",").map(&:strip)
guides.select do |guide|
prefixes.any? {|p| guide.start_with?(p)}
end end
end


# Copy images and css files to html directory def copy_assets
FileUtils.cp_r File.join(guides_dir, 'images'), File.join(output, 'images') FileUtils.cp_r(Dir.glob("#{guides_dir}/images"), output_dir)
FileUtils.cp_r File.join(guides_dir, 'files'), File.join(output, 'files') FileUtils.cp_r(Dir.glob("#{guides_dir}/files"), output_dir)
end end


def generate_guide(guide) def output_file_for(guide)
guide =~ /(.*?)(\.erb)?\.textile/ guide.sub(GUIDES_RE, '.html')
name = $1 end


puts "Generating #{name}" def generate?(source_file, output_file)
fin = File.join(source_dir, source_file)
fout = File.join(output_dir, output_file)
all || !File.exists?(fout) || File.mtime(fout) < File.mtime(fin)
end


file = File.join(output, "#{name}.html") def generate_guide(guide, output_file)
File.open(file, 'w') do |f| puts "Generating #{output_file}"
@view = ActionView::Base.new(view_path) File.open(File.join(output_dir, output_file), 'w') do |f|
@view.extend(Helpers) view = ActionView::Base.new(source_dir, :edge => edge)
view.extend(Helpers)


if guide =~ /\.erb\.textile/ if guide =~ /\.html\.erb$/
# Generate the erb pages with textile formatting - e.g. index/authors # Generate the special pages like the home.
result = view.render(:layout => 'layout', :file => name) result = view.render(:layout => 'layout', :file => guide)
f.write textile(result)
else else
body = File.read(File.join(view_path, guide)) body = File.read(File.join(source_dir, guide))
body = set_header_section(body, @view) body = set_header_section(body, view)
body = set_index(body, @view) body = set_index(body, view)


result = view.render(:layout => 'layout', :text => textile(body)) result = view.render(:layout => 'layout', :text => textile(body))
f.write result
warn_about_broken_links(result) if @warnings
end end

f.write result
end end
end end


Expand All @@ -66,12 +152,12 @@ def set_header_section(body, view)
header = $1 header = $1


header =~ /h2\.(.*)/ header =~ /h2\.(.*)/
page_title = $1.strip page_title = "Ruby on Rails Guides: #{$1.strip}"


header = textile(header) header = textile(header)


view.content_for(:page_title) { page_title } view.content_for(:page_title) { page_title.html_safe }
view.content_for(:header_section) { header } view.content_for(:header_section) { header.html_safe }
new_body new_body
end end


Expand All @@ -82,36 +168,37 @@ def set_index(body, view)
<ol class="chapters"> <ol class="chapters">
INDEX INDEX


i = Indexer.new(body) i = Indexer.new(body, warnings)
i.index i.index


# Set index for 2 levels # Set index for 2 levels
i.level_hash.each do |key, value| i.level_hash.each do |key, value|
link = view.content_tag(:a, :href => key[:id]) { textile(key[:title]) } link = view.content_tag(:a, :href => key[:id]) { textile(key[:title], true).html_safe }


children = value.keys.map do |k| children = value.keys.map do |k|
l = view.content_tag(:a, :href => k[:id]) { textile(k[:title]) } view.content_tag(:li,
view.content_tag(:li, l) view.content_tag(:a, :href => k[:id]) { textile(k[:title], true).html_safe })
end end


children_ul = view.content_tag(:ul, children) children_ul = children.empty? ? "" : view.content_tag(:ul, children.join(" ").html_safe)


index << view.content_tag(:li, link + children_ul) index << view.content_tag(:li, link.html_safe + children_ul.html_safe)
end end


index << '</ol>' index << '</ol>'
index << '</div>' index << '</div>'


view.content_for(:index_section) { index } view.content_for(:index_section) { index.html_safe }


i.result i.result
end end


def textile(body) def textile(body, lite_mode=false)
# If the issue with notextile is fixed just remove the wrapper. # If the issue with notextile is fixed just remove the wrapper.
with_workaround_for_notextile(body) do |body| with_workaround_for_notextile(body) do |body|
t = RedCloth.new(body) t = RedCloth.new(body)
t.hard_breaks = false t.hard_breaks = false
t.lite_mode = lite_mode
t.to_html(:notestuff, :plusplus, :code, :tip) t.to_html(:notestuff, :plusplus, :code, :tip)
end end
end end
Expand All @@ -127,12 +214,45 @@ def with_workaround_for_notextile(body)
code_blocks << %{<div class="code_container"><code class="#{css_class}">#{es}</code></div>} code_blocks << %{<div class="code_container"><code class="#{css_class}">#{es}</code></div>}
"\ndirty_workaround_for_notextile_#{code_blocks.size - 1}\n" "\ndirty_workaround_for_notextile_#{code_blocks.size - 1}\n"
end end

body = yield body body = yield body

body.gsub(%r{<p>dirty_workaround_for_notextile_(\d+)</p>}) do |_| body.gsub(%r{<p>dirty_workaround_for_notextile_(\d+)</p>}) do |_|
code_blocks[$1.to_i] code_blocks[$1.to_i]
end end
end 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.
anchors = Set.new
html.scan(/<h\d\s+id="([^"]+)/).flatten.each do |anchor|
if anchors.member?(anchor)
puts "*** DUPLICATE ID: #{anchor}, please put and explicit ID, e.g. h4(#explicit-id), or consider rewording"
else
anchors << anchor
end
end

# Also, footnotes are rendered as paragraphs this way.
anchors += Set.new(html.scan(/<p\s+class="footnote"\s+id="([^"]+)/).flatten)
return anchors
end

def check_fragment_identifiers(html, anchors)
html.scan(/<a\s+href="#([^"]+)/).flatten.each do |fragment_identifier|
next if fragment_identifier == 'mainCol' # in layout, jumps to some DIV
unless anchors.member?(fragment_identifier)
guess = anchors.min { |a, b|
Levenshtein.distance(fragment_identifier, a) <=> Levenshtein.distance(fragment_identifier, b)
}
puts "*** BROKEN LINK: ##{fragment_identifier}, perhaps you meant ##{guess}."
end
end
end
end end
end end

0 comments on commit add3ccb

Please sign in to comment.