verbose(true) class DocsetGenerator require 'rubygems' require 'nokogiri' require 'fileutils' def parse_html_docref(node) code = '' code << node.xpath(".//p[@class='abstract']").text code << "\n" code << node.xpath(".//div[@class='api discussion']").text.sub(/^Discussion/, '') code.strip! code.gsub!(/^/m, ' # ') code << "\n" return code end def parse_type(type) type = type.to_s type.strip! star = type.sub!(/\s*\*$/, '') # Remove pointer star. case type when /\*$/ # A double pointer, in MacRuby this becomes a Pointer. 'Pointer' when 'id' 'Object' when 'void' 'nil' when 'SEL' 'Symbol' when 'bool', 'BOOL' 'Boolean' when 'float', 'double', 'CGFloat' 'Float' when 'int', 'short', 'long', 'long long', 'NSInteger', 'NSUInteger' 'Integer' when 'NSString', 'NSMutableString' 'String' when 'NSArray', 'NSMutableArray' 'Array' when 'NSDictionary', 'NSMutableDictionary' 'Hash' else type end end def parse_html_class(name, doc) # Find superclass (mandatory). sclass = nil doc.xpath("//table[@class='specbox']/tr").each do |node| if md = node.text.match(/Inherits from([^ ]+)/) sclass = md[1] break end end return nil unless sclass code = doc.xpath(".//p[@class='abstract']")[0].text code.gsub!(/^/m, '# ') code << "\nclass #{name} < #{sclass}\n\n" # Properties. doc.xpath("//div[@class='api propertyObjC']").each do |node| decl = node.xpath(".//div[@class='declaration']/div[@class='declaration']").text readonly = decl.include?('readonly') decl.sub!(/@property\s*(\([^\)]+\))?/, '') md = decl.match(/(\w+)$/) next unless md title = md[1] type = md.pre_match code << parse_html_docref(node) code << " # @return [#{parse_type(type)}]\n" code << ' ' << (readonly ? "attr_reader" : "attr_accessor") << " :#{title}\n\n" end # Methods. methods = [] methods.concat(doc.xpath("//div[@class='api classMethod']")) methods.concat(doc.xpath("//div[@class='api instanceMethod']")) methods.each do |node| decl = node.xpath(".//div[@class='declaration']").text types = decl.scan(/\(([^)]+)\)/) ret_type = types.shift # Docref. code << parse_html_docref(node) # Parameters and return value. arg_names = node.xpath(".//div[@class='api parameters']//dt") arg_docs = node.xpath(".//div[@class='api parameters']//dd") if arg_names.size == arg_docs.size has_types = types.size == arg_names.size arg_names.each_with_index do |arg_name, i| arg_doc = arg_docs[i] code << " # @param " code << "[#{parse_type(types[i])}] " if has_types code << "#{arg_name.text} #{arg_doc.text}\n" end end retdoc = node.xpath(".//div[@class='return_value']/p").text.strip code << " # @return " code << "[#{parse_type(ret_type)}] " if ret_type code << "#{retdoc}" unless retdoc.empty? code << "\n" is_class_method = decl.match(/^\s*\+/) != nil decl.sub!(/^\s*[\+\-]/, '') # Remove method qualifier. sel_parts = decl.gsub(/\([^)]+\)/, '').split.map { |x| x.split(':') } head = sel_parts.shift code << " def #{is_class_method ? 'self.' : ''}#{head[0]}(" code << "#{head[1]}" if head.size > 1 unless sel_parts.empty? code << ', ' code << sel_parts.map { |part| "#{part[0]}:#{part[1]}" }.join(', ') end code << "); end\n\n" end code << "end" return code end def parse_html_data(data) doc = Nokogiri::HTML(data) title = doc.xpath('/html/head/title') if title and md = title.text.match(/^(.+)Class Reference$/) parse_html_class(md[1].strip, doc) else nil end end def initialize(outpath, paths) @input_paths = [] paths.each do |path| if File.directory?(path) @input_paths.concat(Dir.glob(path + '/**/*.html')) else @input_paths << path end end @outpath = outpath end def run rb_files_dir = '/tmp/rb_docset' FileUtils.rm_rf(rb_files_dir) FileUtils.mkdir_p(rb_files_dir) @input_paths.map { |path| parse_html_data(File.read(path)) }.compact.each_with_index do |code, n| File.open(File.join(rb_files_dir, "t#{n}.rb"), 'w') do |io| io.write(code) end end sh "yard doc #{rb_files_dir}" sh "mv doc \"#{@outpath}\"" end end require 'redcloth' class GuideGenerator < RedCloth def initialize(src, dest, no_header_footer) base = File.basename(src, '.textile') @title = base.gsub(/([a-z])([A-Z])/, '\1 \2') @dest = dest @no_header_footer = no_header_footer data = File.read(src) super(fix_anchors(data)) end def run File.open(@dest, 'w') do |io| unless @no_header_footer io.puts html_header #io.puts toc io.puts '
' end io.puts to_html(:inline_plus_annotations, :textile) unless @no_header_footer io.puts '
' io.puts html_footer end end end def toc toggle_func = <

Table of Contents (hide)

" current_level = 1 text << "
" self.to_s.scan(/^h([2-3])\((#[^)]+)\)\.([^\n]+)\n/).each do |level, anchor, title| level = level.to_i if level > current_level text << '
    ' current_level = level elsif level < current_level text << '
' current_level = level end text << "
  • #{title}
  • " end while current_level > 1 text << '' current_level -= 1 end text << '
    ' return text end # Transform +foo+ into foo. def inline_plus_annotations(text) text.gsub!(/\+(.*?)\+/m) { |m| rip_offtags("#{$~[1]}") } end # Auto-define anchors for h[1-4] elements. def fix_anchors(text) text.gsub(/^(h[1-4])\.([^\n]+)/m) do |m| "#{$1}(##{anchor_name($2)}). #{$2}" end end def anchor_name(title) title.strip.gsub(/\s/, '-').downcase end def css < #{@title} EOS end def html_footer < EOS end end task :default => :all #task :all => [:guides, :docset] task :all => [:guides] # no docset in the main distribution... task :guides do devcenter = ENV['devcenter_destdir'] Dir.glob('*.textile').each do |src| dest = (File.basename(src, '.textile') + '.html') dest = dest.downcase.gsub(/ /, '-') if devcenter dest = File.join(devcenter, dest) if devcenter if devcenter or !File.exist?(dest) or File.mtime(src) > File.mtime(dest) or File.mtime(__FILE__) > File.mtime(dest) GuideGenerator.new(src, dest, devcenter != nil).run end end end DOCSET_PATHS = %w{ /Library/Developer/Documentation/DocSets/com.apple.adc.documentation.AppleiOS4_3.iOSLibrary.docset/Contents/Resources/Documents/documentation/UIKit/Reference /Library/Developer/Documentation/DocSets/com.apple.adc.documentation.AppleiOS4_3.iOSLibrary.docset/Contents/Resources/Documents/documentation/Cocoa/Reference } task :docset do if !File.exist?('docset') DocsetGenerator.new('docset', DOCSET_PATHS).run end end task :clean do rm_rf 'docset' Dir.glob('*.html').each { |x| rm_rf x } end