aboutsummaryrefslogtreecommitdiffstats
path: root/guides/rails_guides/generator.rb
diff options
context:
space:
mode:
Diffstat (limited to 'guides/rails_guides/generator.rb')
-rw-r--r--guides/rails_guides/generator.rb208
1 files changed, 208 insertions, 0 deletions
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
new file mode 100644
index 0000000000..7205f37be7
--- /dev/null
+++ b/guides/rails_guides/generator.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+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/markdown"
+require "rails_guides/indexer"
+require "rails_guides/helpers"
+require "rails_guides/levenshtein"
+
+module RailsGuides
+ class Generator
+ GUIDES_RE = /\.(?:erb|md)\z/
+
+ def initialize(edge:, version:, all:, only:, kindle:, language:)
+ @edge = edge
+ @version = version
+ @all = all
+ @only = only
+ @kindle = kindle
+ @language = language
+
+ if @kindle
+ check_for_kindlegen
+ register_kindle_mime_types
+ end
+
+ initialize_dirs
+ create_output_dir_if_needed
+ initialize_markdown_renderer
+ end
+
+ def generate
+ generate_guides
+ copy_assets
+ generate_mobi if @kindle
+ end
+
+ private
+
+ def register_kindle_mime_types
+ Mime::Type.register_alias("application/xml", :opf, %w(opf))
+ Mime::Type.register_alias("application/xml", :ncx, %w(ncx))
+ end
+
+ def check_for_kindlegen
+ if `which kindlegen`.blank?
+ raise "Can't create a kindle version without `kindlegen`."
+ end
+ end
+
+ def generate_mobi
+ require "rails_guides/kindle"
+ out = "#{@output_dir}/kindlegen.out"
+ Kindle.generate(@output_dir, mobi, out)
+ puts "(kindlegen log at #{out})."
+ end
+
+ def mobi
+ mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}"
+ mobi += ".#{@language}" if @language
+ mobi += ".mobi"
+ end
+
+ def initialize_dirs
+ @guides_dir = File.expand_path("..", __dir__)
+
+ @source_dir = "#{@guides_dir}/source"
+ @source_dir += "/#{@language}" if @language
+
+ @output_dir = "#{@guides_dir}/output"
+ @output_dir += "/kindle" if @kindle
+ @output_dir += "/#{@language}" if @language
+ end
+
+ def create_output_dir_if_needed
+ FileUtils.mkdir_p(@output_dir)
+ end
+
+ def initialize_markdown_renderer
+ Markdown::Renderer.edge = @edge
+ Markdown::Renderer.version = @version
+ end
+
+ def generate_guides
+ guides_to_generate.each do |guide|
+ output_file = output_file_for(guide)
+ generate_guide(guide, output_file) if generate?(guide, output_file)
+ end
+ end
+
+ def guides_to_generate
+ guides = Dir.entries(@source_dir).grep(GUIDES_RE)
+
+ if @kindle
+ Dir.entries("#{@source_dir}/kindle").grep(GUIDES_RE).map do |entry|
+ next if entry == "KINDLE.md"
+ guides << "kindle/#{entry}"
+ end
+ end
+
+ @only ? select_only(guides) : guides
+ end
+
+ def select_only(guides)
+ prefixes = @only.split(",").map(&:strip)
+ guides.select do |guide|
+ guide.start_with?("kindle", *prefixes)
+ end
+ end
+
+ def copy_assets
+ FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir)
+ end
+
+ def output_file_for(guide)
+ if guide.end_with?(".md")
+ guide.sub(/md\z/, "html")
+ else
+ guide.sub(/\.erb\z/, "")
+ end
+ end
+
+ def output_path_for(output_file)
+ File.join(@output_dir, File.basename(output_file))
+ end
+
+ def generate?(source_file, output_file)
+ fin = File.join(@source_dir, source_file)
+ fout = output_path_for(output_file)
+ @all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
+ end
+
+ def generate_guide(guide, output_file)
+ output_path = output_path_for(output_file)
+ puts "Generating #{guide} as #{output_file}"
+ layout = @kindle ? "kindle/layout" : "layout"
+
+ File.open(output_path, "w") do |f|
+ view = ActionView::Base.new(
+ @source_dir,
+ edge: @edge,
+ version: @version,
+ mobi: "kindle/#{mobi}",
+ language: @language
+ )
+ view.extend(Helpers)
+
+ if guide =~ /\.(\w+)\.erb$/
+ # Generate the special pages like the home.
+ # Passing a template handler in the template name is deprecated. So pass the file name without the extension.
+ result = view.render(layout: layout, formats: [$1], file: $`)
+ else
+ body = File.read("#{@source_dir}/#{guide}")
+ result = RailsGuides::Markdown.new(
+ view: view,
+ layout: layout,
+ edge: @edge,
+ version: @version
+ ).render(body)
+
+ warn_about_broken_links(result)
+ end
+
+ f.write(result)
+ end
+ end
+
+ def warn_about_broken_links(html)
+ anchors = extract_anchors(html)
+ check_fragment_identifiers(html, anchors)
+ end
+
+ def extract_anchors(html)
+ # 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 make sure that there're no headings with the same name at the same level."
+ else
+ anchors << anchor
+ end
+ end
+
+ # Footnotes.
+ anchors += Set.new(html.scan(/<p\s+class="footnote"\s+id="([^"]+)/).flatten)
+ anchors += Set.new(html.scan(/<sup\s+class="footnote"\s+id="([^"]+)/).flatten)
+ 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