lib/docurium.rb in docurium-0.3.2 vs lib/docurium.rb in docurium-0.4.0

- old
+ new

@@ -2,14 +2,16 @@ require 'tempfile' require 'version_sorter' require 'rocco' require 'docurium/version' require 'docurium/layout' -require 'docurium/cparser' +require 'libdetect' +require 'docurium/docparser' require 'pp' require 'rugged' require 'redcarpet' +require 'thread' # Markdown expects the old redcarpet compat API, so let's tell it what # to use Rocco::Markdown = RedcarpetCompat @@ -18,23 +20,17 @@ def initialize(config_file, repo = nil) raise "You need to specify a config file" if !config_file raise "You need to specify a valid config file" if !valid_config(config_file) @sigs = {} - @groups = {} - if repo - @repo = repo - else - repo_path = Rugged::Repository.discover('.') - @repo = Rugged::Repository.new(repo_path) - end - clear_data + @repo = repo || Rugged::Repository.discover('.') end - def clear_data(version = 'HEAD') - @data = {:files => [], :functions => {}, :globals => {}, :types => {}, :prefix => ''} - @data[:prefix] = option_version(version, 'input', '') + def init_data(version = 'HEAD') + data = {:files => [], :functions => {}, :globals => {}, :types => {}, :prefix => ''} + data[:prefix] = option_version(version, 'input', '') + data end def option_version(version, option, default = nil) if @options['legacy'] if valhash = @options['legacy'][option] @@ -46,103 +42,157 @@ opt = @options[option] opt = default if !opt opt end + def format_examples!(data, version) + examples = [] + if ex = option_version(version, 'examples') + if subtree = find_subtree(version, ex) # check that it exists + index = Rugged::Index.new + index.read_tree(subtree) + + files = [] + index.each do |entry| + next unless entry[:path].match(/\.c$/) + files << entry[:path] + end + + files.each do |file| + # highlight, roccoize and link + rocco = Rocco.new(file, files, {:language => 'c'}) do + ientry = index[file] + blob = @repo.lookup(ientry[:oid]) + blob.content + end + rocco_layout = Rocco::Layout.new(rocco, @tf) + rocco_layout.version = version + rf = rocco_layout.render + + extlen = -(File.extname(file).length + 1) + rf_path = file[0..extlen] + '.html' + rel_path = "ex/#{version}/#{rf_path}" + + # look for function names in the examples and link + id_num = 0 + data[:functions].each do |f, fdata| + rf.gsub!(/#{f}([^\w])/) do |fmatch| + extra = $1 + id_num += 1 + name = f + '-' + id_num.to_s + # save data for cross-link + data[:functions][f][:examples] ||= {} + data[:functions][f][:examples][file] ||= [] + data[:functions][f][:examples][file] << rel_path + '#' + name + "<a name=\"#{name}\" class=\"fnlink\" href=\"../../##{version}/group/#{fdata[:group]}/#{f}\">#{f}</a>#{extra}" + end + end + + # write example to the repo + sha = @repo.write(rf, :blob) + examples << [rel_path, sha] + + data[:examples] ||= [] + data[:examples] << [file, rel_path] + end + end + end + + examples + end + + def generate_doc_for(version) + index = Rugged::Index.new + read_subtree(index, version, option_version(version, 'input', '')) + data = parse_headers(index, version) + data + end + def generate_docs - out "* generating docs" output_index = Rugged::Index.new write_site(output_index) + @tf = File.expand_path(File.join(File.dirname(__FILE__), 'docurium', 'layout.mustache')) versions = get_versions versions << 'HEAD' + nversions = versions.size + output = Queue.new + pipes = {} versions.each do |version| - out " - processing version #{version}" - index = @repo.index - index.clear - clear_data(version) - read_subtree(index, version, @data[:prefix]) - parse_headers(index) - tally_sigs(version) + # We don't need to worry about joining since this process is + # going to die immediately + read, write = IO.pipe + pid = Process.fork do + read.close - tf = File.expand_path(File.join(File.dirname(__FILE__), 'docurium', 'layout.mustache')) - if ex = option_version(version, 'examples') - if subtree = find_subtree(version, ex) # check that it exists - index.read_tree(subtree) - out " - processing examples for #{version}" + data = generate_doc_for(version) + examples = format_examples!(data, version) - files = [] - index.each do |entry| - next unless entry[:path].match(/\.c$/) - files << entry[:path] - end + Marshal.dump([version, data, examples], write) + write.close + end - files.each do |file| - out " # #{file}" + pipes[pid] = read + write.close + end - # highlight, roccoize and link - rocco = Rocco.new(file, files, {:language => 'c'}) do - ientry = index[file] - blob = @repo.lookup(ientry[:oid]) - blob.content - end - rocco_layout = Rocco::Layout.new(rocco, tf) - rocco_layout.version = version - rf = rocco_layout.render + print "Generating documentation [0/#{nversions}]\r" + head_data = nil - extlen = -(File.extname(file).length + 1) - rf_path = file[0..extlen] + '.html' - rel_path = "ex/#{version}/#{rf_path}" + # This may seem odd, but we need to keep reading from the pipe or + # the buffer will fill and they'll block and never exit. Therefore + # we can't rely on Process.wait to tell us when the work is + # done. Instead read from all the pipes concurrently and send the + # ruby objects through the queue. + Thread.abort_on_exception = true + pipes.each do |pid, read| + Thread.new do + result = read.read + output << Marshal.load(result) + end + end - # look for function names in the examples and link - id_num = 0 - @data[:functions].each do |f, fdata| - rf.gsub!(/#{f}([^\w])/) do |fmatch| - extra = $1 - id_num += 1 - name = f + '-' + id_num.to_s - # save data for cross-link - @data[:functions][f][:examples] ||= {} - @data[:functions][f][:examples][file] ||= [] - @data[:functions][f][:examples][file] << rel_path + '#' + name - "<a name=\"#{name}\" class=\"fnlink\" href=\"../../##{version}/group/#{fdata[:group]}/#{f}\">#{f}</a>#{extra}" - end - end + for i in 1..nversions + version, data, examples = output.pop - # write example to the repo - sha = @repo.write(rf, :blob) - output_index.add(:path => rel_path, :oid => sha, :mode => 0100644) + # There's still some work we need to do serially + tally_sigs!(version, data) + sha = @repo.write(data.to_json, :blob) - @data[:examples] ||= [] - @data[:examples] << [file, rel_path] - end - end + print "Generating documentation [#{i}/#{nversions}]\r" - if version == 'HEAD' - show_warnings - end + # Store it so we can show it at the end + if version == 'HEAD' + head_data = data end - sha = @repo.write(@data.to_json, :blob) output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644) + examples.each do |path, id| + output_index.add(:path => path, :oid => id, :mode => 0100644) + end + + if head_data + puts '' + show_warnings(data) + end + end project = { :versions => versions.reverse, :github => @options['github'], :name => @options['name'], :signatures => @sigs, - :groups => @groups } sha = @repo.write(project.to_json, :blob) output_index.add(:path => "project.json", :oid => sha, :mode => 0100644) br = @options['branch'] out "* writing to branch #{br}" refname = "refs/heads/#{br}" tsha = output_index.write_tree(@repo) puts "\twrote tree #{tsha}" - ref = Rugged::Reference.lookup(@repo, refname) + ref = @repo.references[refname] user = { :name => @repo.config['user.name'], :email => @repo.config['user.email'], :time => Time.now } options = {} options[:tree] = tsha options[:author] = user options[:committer] = user @@ -152,16 +202,16 @@ csha = Rugged::Commit.create(@repo, options) puts "\twrote commit #{csha}" puts "\tupdated #{br}" end - def show_warnings + def show_warnings(data) out '* checking your api' # check for unmatched paramaters unmatched = [] - @data[:functions].each do |f, fdata| + data[:functions].each do |f, fdata| unmatched << f if fdata[:comments] =~ /@param/ end if unmatched.size > 0 out ' - unmatched params in' unmatched.sort.each { |p| out ("\t" + p) } @@ -179,31 +229,39 @@ sigchanges.sort.each { |p| out ("\t" + p) } end end def get_versions - tags = [] - @repo.tags.each { |tag| tags << tag.gsub(%r(^refs/tags/), '') } - VersionSorter.sort(tags) + VersionSorter.sort(@repo.tags.map { |tag| tag.name.gsub(%r(^refs/tags/), '') }) end - def parse_headers(index) - headers(index).each do |header| - records = parse_header(index, header) - update_globals(records) + def parse_headers(index, version) + headers = index.map { |e| e[:path] }.grep(/\.h$/) + + files = headers.map do |file| + [file, @repo.lookup(index[file][:oid]).content] end - @data[:groups] = group_functions - @data[:types] = @data[:types].sort # make it an assoc array - find_type_usage + data = init_data(version) + parser = DocParser.new + headers.each do |header| + records = parser.parse_file(header, files) + update_globals!(data, records) + end + + data[:groups] = group_functions!(data) + data[:types] = data[:types].sort # make it an assoc array + find_type_usage!(data) + + data end private - def tally_sigs(version) + def tally_sigs!(version, data) @lastsigs ||= {} - @data[:functions].each do |fun_name, fun_data| + data[:functions].each do |fun_name, fun_data| if !@sigs[fun_name] @sigs[fun_name] ||= {:exists => [], :changes => {}} else if @lastsigs[fun_name] != fun_data[:sig] @sigs[fun_name][:changes][version] = true @@ -215,14 +273,14 @@ end def find_subtree(version, path) tree = nil if version == 'HEAD' - tree = @repo.lookup(@repo.head.target).tree + tree = @repo.head.target.tree else - trg = @repo.lookup(Rugged::Reference.lookup(@repo, "refs/tags/#{version}").target) - if(trg.class == Rugged::Tag) + trg = @repo.references["refs/tags/#{version}"].target + if(trg.kind_of? Rugged::Tag::Annotation) trg = trg.target end tree = trg.tree end @@ -247,72 +305,57 @@ @config_file = File.basename(fpath) @options = JSON.parse(File.read(fpath)) !!@options['branch'] end - def group_functions + def group_functions!(data) func = {} - @data[:functions].each_pair do |key, value| + data[:functions].each_pair do |key, value| if @options['prefix'] k = key.gsub(@options['prefix'], '') else k = key end group, rest = k.split('_', 2) next if group.empty? if !rest group = value[:file].gsub('.h', '').gsub('/', '_') end - @data[:functions][key][:group] = group - @groups[key] = group + data[:functions][key][:group] = group func[group] ||= [] func[group] << key func[group].sort! end misc = [] func.to_a.sort end - def headers(index = nil) - h = [] - index.each do |entry| - next unless entry[:path].match(/\.h$/) - h << entry[:path] - end - h - end - - def find_type_usage + def find_type_usage!(data) # go through all the functions and see where types are used and returned # store them in the types data - @data[:functions].each do |func, fdata| - @data[:types].each_with_index do |tdata, i| + data[:functions].each do |func, fdata| + data[:types].each_with_index do |tdata, i| type, typeData = tdata - @data[:types][i][1][:used] ||= {:returns => [], :needs => []} + data[:types][i][1][:used] ||= {:returns => [], :needs => []} if fdata[:return][:type].index(/#{type}[ ;\)\*]/) - @data[:types][i][1][:used][:returns] << func - @data[:types][i][1][:used][:returns].sort! + data[:types][i][1][:used][:returns] << func + data[:types][i][1][:used][:returns].sort! end if fdata[:argline].index(/#{type}[ ;\)\*]/) - @data[:types][i][1][:used][:needs] << func - @data[:types][i][1][:used][:needs].sort! + data[:types][i][1][:used][:needs] << func + data[:types][i][1][:used][:needs].sort! end end end end - def parse_header(index, path) - id = index[path][:oid] - blob = @repo.lookup(id) - parser = Docurium::CParser.new - parser.parse_text(path, blob.content) - end + def update_globals!(data, recs) + return if recs.empty? - def update_globals(recs) wanted = { :functions => %W/type value file line lineto args argline sig return group description comments/.map(&:to_sym), - :types => %W/type value file line lineto block tdef comments/.map(&:to_sym), + :types => %W/decl type value file line lineto block tdef description comments fields/.map(&:to_sym), :globals => %W/value file line comments/.map(&:to_sym), :meta => %W/brief defgroup ingroup comments/.map(&:to_sym), } file_map = {} @@ -329,31 +372,31 @@ end # process this type of record case r[:type] when :function - @data[:functions][r[:name]] ||= {} + data[:functions][r[:name]] ||= {} wanted[:functions].each do |k| next unless r.has_key? k conents = nil if k == :description || k == :comments contents = md.render r[k] else contents = r[k] end - @data[:functions][r[:name]][k] = contents + data[:functions][r[:name]][k] = contents end file_map[r[:file]][:functions] << r[:name] when :define, :macro - @data[:globals][r[:decl]] ||= {} + data[:globals][r[:decl]] ||= {} wanted[:globals].each do |k| next unless r.has_key? k if k == :description || k == :comments - @data[:globals][r[:decl]][k] = md.render r[k] + data[:globals][r[:decl]][k] = md.render r[k] else - @data[:globals][r[:decl]][k] = r[k] + data[:globals][r[:decl]][k] = r[k] end end when :file wanted[:meta].each do |k| @@ -362,58 +405,64 @@ when :enum if !r[:name] # Explode unnamed enum into multiple global defines r[:decl].each do |n| - @data[:globals][n] ||= { + data[:globals][n] ||= { :file => r[:file], :line => r[:line], :value => "", :comments => md.render(r[:comments]), } m = /#{Regexp.quote(n)}/.match(r[:body]) if m - @data[:globals][n][:line] += m.pre_match.scan("\n").length + data[:globals][n][:line] += m.pre_match.scan("\n").length if m.post_match =~ /\s*=\s*([^,\}]+)/ - @data[:globals][n][:value] = $1 + data[:globals][n][:value] = $1 end end end else # enum has name - @data[:types][r[:name]] ||= {} + data[:types][r[:name]] ||= {} wanted[:types].each do |k| next unless r.has_key? k contents = r[k] if k == :comments contents = md.render r[k] elsif k == :block - old_block = @data[:types][r[:name]][k] + old_block = data[:types][r[:name]][k] contents = old_block ? [old_block, r[k]].join("\n") : r[k] + elsif k == :fields + type = data[:types][r[:name]] + type[:fields] = [] + r[:fields].each do |f| + f[:comments] = md.render(f[:comments]) + end end - @data[:types][r[:name]][k] = contents + data[:types][r[:name]][k] = contents end end when :struct, :fnptr - @data[:types][r[:name]] ||= {} + data[:types][r[:name]] ||= {} r[:value] ||= r[:name] wanted[:types].each do |k| next unless r.has_key? k if k == :comments - @data[:types][r[:name]][k] = md.render r[k] + data[:types][r[:name]][k] = md.render r[k] else - @data[:types][r[:name]][k] = r[k] + data[:types][r[:name]][k] = r[k] end end if r[:type] == :fnptr - @data[:types][r[:name]][:type] = "function pointer" + data[:types][r[:name]][:type] = "function pointer" end else # Anything else we want to record? end end - @data[:files] << file_map.values[0] + data[:files] << file_map.values[0] end def add_dir_to_index(index, prefix, dir) Dir.new(dir).each do |filename| next if [".", ".."].include? filename