lib/docurium.rb in docurium-0.0.5 vs lib/docurium.rb in docurium-0.1.0

- old
+ new

@@ -1,22 +1,26 @@ require 'json' require 'tempfile' require 'version_sorter' require 'rocco' +require 'docurium/version' require 'docurium/layout' +require 'docurium/cparser' require 'pp' +require 'rugged' +require 'redcarpet' class Docurium - Version = VERSION = '0.0.5' - attr_accessor :branch, :output_dir, :data def initialize(config_file) 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 = {} + repo_path = Rugged::Repository.discover('.') + @repo = Rugged::Repository.new(repo_path) clear_data end def clear_data(version = 'HEAD') @data = {:files => [], :functions => {}, :globals => {}, :types => {}, :prefix => ''} @@ -36,128 +40,111 @@ opt end def generate_docs out "* generating docs" - outdir = mkdir_temp - copy_site(outdir) + output_index = Rugged::Index.new + write_site(output_index) versions = get_versions versions << 'HEAD' versions.each do |version| out " - processing version #{version}" - workdir = mkdir_temp - Dir.chdir(workdir) do - clear_data(version) - checkout(version, workdir) - parse_headers - tally_sigs(version) - end + index = @repo.index + index.clear + clear_data(version) + read_subtree(index, version, @data[:prefix]) + parse_headers(index) + tally_sigs(version) tf = File.expand_path(File.join(File.dirname(__FILE__), 'docurium', 'layout.mustache')) if ex = option_version(version, 'examples') - workdir = mkdir_temp - Dir.chdir(workdir) do - with_git_env(workdir) do - `git rev-parse #{version}:#{ex} 2>&1` # check that it exists - if $?.exitstatus == 0 - out " - processing examples for #{version}" - `git read-tree #{version}:#{ex}` - `git checkout-index -a` + if subtree = find_subtree(version, ex) # check that it exists + index.read_tree(subtree) + out " - processing examples for #{version}" - files = [] - Dir.glob("**/*.c") do |file| - next if !File.file?(file) - files << file - end - files.each do |file| - out " # #{file}" + files = [] + index.each do |entry| + next unless entry[:path].match(/\.c$/) + files << entry[:path] + end - # highlight, roccoize and link - rocco = Rocco.new(file, files, {:language => 'c'}) - rocco_layout = Rocco::Layout.new(rocco, tf) - rocco_layout.version = version - rf = rocco_layout.render + files.each do |file| + out " # #{file}" - rf_path = File.basename(file).split('.')[0..-2].join('.') + '.html' - rel_path = "ex/#{version}/#{rf_path}" - rf_path = File.join(outdir, rel_path) + # 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 - # 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}\" href=\"../../##{version}/group/#{fdata[:group]}/#{f}\">#{f}</a>#{extra}" - end - end + rf_path = File.basename(file).split('.')[0..-2].join('.') + '.html' + rel_path = "ex/#{version}/#{rf_path}" - # write example to docs directory - FileUtils.mkdir_p(File.dirname(rf_path)) - File.open(rf_path, 'w+') do |f| - @data[:examples] ||= [] - @data[:examples] << [file, rel_path] - f.write(rf) - 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 + + # write example to the repo + sha = @repo.write(rf, :blob) + output_index.add(:path => rel_path, :oid => sha, :mode => 0100644) + + @data[:examples] ||= [] + @data[:examples] << [file, rel_path] end end if version == 'HEAD' show_warnings end end - File.open(File.join(outdir, "#{version}.json"), 'w+') do |f| - f.write(@data.to_json) - end + sha = @repo.write(@data.to_json, :blob) + output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644) end - Dir.chdir(outdir) do - project = { - :versions => versions.reverse, - :github => @options['github'], - :name => @options['name'], - :signatures => @sigs, - :groups => @groups - } - File.open("project.json", 'w+') do |f| - f.write(project.to_json) - 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) - if br = @options['branch'] - out "* writing to branch #{br}" - ref = "refs/heads/#{br}" - with_git_env(outdir) do - psha = `git rev-parse #{ref}`.chomp - `git add -A` - tsha = `git write-tree`.chomp - puts "\twrote tree #{tsha}" - if(psha == ref) - csha = `echo 'generated docs' | git commit-tree #{tsha}`.chomp - else - csha = `echo 'generated docs' | git commit-tree #{tsha} -p #{psha}`.chomp - end - puts "\twrote commit #{csha}" - `git update-ref -m 'generated docs' #{ref} #{csha}` - puts "\tupdated #{br}" - end - else - final_dir = File.join(@project_dir, @options['output'] || 'docs') - out "* output html in #{final_dir}" - FileUtils.mkdir_p(final_dir) - Dir.chdir(final_dir) do - FileUtils.cp_r(File.join(outdir, '.'), '.') - end - end + 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) + user = { :name => @repo.config['user.name'], :email => @repo.config['user.email'], :time => Time.now } + options = {} + options[:tree] = tsha + options[:author] = user + options[:committer] = user + options[:message] = 'generated docs' + options[:parents] = ref ? [ref.target] : [] + options[:update_ref] = refname + csha = Rugged::Commit.create(@repo, options) + puts "\twrote commit #{csha}" + puts "\tupdated #{br}" end def show_warnings out '* checking your api' @@ -183,17 +170,21 @@ sigchanges.sort.each { |p| out ("\t" + p) } end end def get_versions - VersionSorter.sort(git('tag').split("\n")) + tags = [] + @repo.tags.each { |tag| tags << tag.gsub(%r(^refs/tags/), '') } + VersionSorter.sort(tags) end - def parse_headers - headers.each do |header| - parse_header(header) + def parse_headers(index) + headers(index).each do |header| + records = parse_header(index, header) + update_globals(records) end + @data[:groups] = group_functions @data[:types] = @data[:types].sort # make it an assoc array find_type_usage end @@ -212,42 +203,43 @@ @sigs[fun_name][:exists] << version @lastsigs[fun_name] = fun_data[:sig] end end - def git(command) - out = '' - Dir.chdir(@project_dir) do - out = `git #{command}` + def find_subtree(version, path) + tree = nil + if version == 'HEAD' + tree = @repo.lookup(@repo.head.target).tree + else + trg = @repo.lookup(Rugged::Reference.lookup(@repo, "refs/tags/#{version}").target) + if(trg.class == Rugged::Tag) + trg = trg.target + end + + tree = trg.tree end - out.strip - end - def checkout(version, workdir) - with_git_env(workdir) do - `git read-tree #{version}:#{@data[:prefix]}` - `git checkout-index -a` + begin + tree_entry = tree.path(path) + @repo.lookup(tree_entry[:oid]) + rescue Rugged::TreeError + nil end end - def with_git_env(workdir) - ENV['GIT_INDEX_FILE'] = mkfile_temp - ENV['GIT_WORK_TREE'] = workdir - ENV['GIT_DIR'] = File.join(@project_dir, '.git') - yield - ENV.delete('GIT_INDEX_FILE') - ENV.delete('GIT_WORK_TREE') - ENV.delete('GIT_DIR') + def read_subtree(index, version, path) + tree = find_subtree(version, path) + index.read_tree(tree) end def valid_config(file) return false if !File.file?(file) fpath = File.expand_path(file) @project_dir = File.dirname(fpath) @config_file = File.basename(fpath) @options = JSON.parse(File.read(fpath)) - true + !!@options['branch'] end def group_functions func = {} @data[:functions].each_pair do |key, value| @@ -269,15 +261,15 @@ end misc = [] func.to_a.sort end - def headers + def headers(index = nil) h = [] - Dir.glob(File.join('**/*.h')).each do |header| - next if !File.file?(header) - h << header + index.each do |entry| + next unless entry[:path].match(/\.h$/) + h << entry[:path] end h end def find_type_usage @@ -297,251 +289,126 @@ end end end end - def header_content(header_path) - File.readlines(header_path) + 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 parse_header(filepath) - lineno = 0 - content = header_content(filepath) + 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), + :globals => %W/value file line comments/.map(&:to_sym), + :meta => %W/brief defgroup ingroup comments/.map(&:to_sym), + } - # look for structs and enums - in_block = false - block = '' - linestart = 0 - tdef, type, name = nil - content.each do |line| - lineno += 1 - line = line.strip + file_map = {} - if line[0, 1] == '#' #preprocessor - if m = /\#define (.*?) (.*)/.match(line) - @data[:globals][m[1]] = {:value => m[2].strip, :file => filepath, :line => lineno} - else - next - end + md = Redcarpet::Markdown.new Redcarpet::Render::HTML, :no_intra_emphasis => true + recs.each do |r| + + # initialize filemap for this file + file_map[r[:file]] ||= { + :file => r[:file], :functions => [], :meta => {}, :lines => 0 + } + if file_map[r[:file]][:lines] < r[:lineto] + file_map[r[:file]][:lines] = r[:lineto] end - if m = /^(typedef )*(struct|enum) (.*?)(\{|(\w*?);)/.match(line) - tdef = m[1] # typdef or nil - type = m[2] # struct or enum - name = m[3] # name or nil - linestart = lineno - name.strip! if name - tdef.strip! if tdef - if m[4] == '{' - # struct or enum - in_block = true - else - # single line, probably typedef - val = m[4].gsub(';', '').strip - if !name.empty? - name = name.gsub('*', '').strip - @data[:types][name] = {:tdef => tdef, :type => type, :value => val, :file => filepath, :line => lineno} + # process this type of record + case r[:type] + when :function + @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 end - elsif m = /\}(.*?);/.match(line) - if !m[1].strip.empty? - name = m[1].strip - end - name = name.gsub('*', '').strip - @data[:types][name] = {:block => block, :tdef => tdef, :type => type, :value => val, :file => filepath, :line => linestart, :lineto => lineno} - in_block = false - block = '' - elsif in_block - block += line + "\n" - end - end - - in_comment = false - in_block = false - current = -1 - data = [] - lineno = 0 - # look for functions - content.each do |line| - lineno += 1 - line = line.strip - next if line.size == 0 - next if line[0, 1] == '#' - in_block = true if line =~ /\{/ - if m = /(.*?)\/\*(.*?)\*\//.match(line) - code = m[1] - comment = m[2] - current += 1 - data[current] ||= {:comments => clean_comment(comment), :code => [code], :line => lineno} - elsif m = /(.*?)\/\/(.*?)/.match(line) - code = m[1] - comment = m[2] - current += 1 - data[current] ||= {:comments => clean_comment(comment), :code => [code], :line => lineno} - else - if line =~ /\/\*/ - in_comment = true - current += 1 - elsif current == -1 - current += 1 - end - data[current] ||= {:comments => '', :code => [], :line => lineno} - data[current][:lineto] = lineno - if in_comment - data[current][:comments] += clean_comment(line) + "\n" - else - data[current][:code] << line - end - if (m = /(.*?);$/.match(line)) && (data[current][:code].size > 0) && !in_block - current += 1 - end - in_comment = false if line =~ /\*\// - in_block = false if line =~ /\}/ - end - end - data.compact! - meta = extract_meta(data) - funcs = extract_functions(filepath, data) - @data[:files] << {:file => filepath, :meta => meta, :functions => funcs, :lines => lineno} - end + file_map[r[:file]][:functions] << r[:name] - def clean_comment(comment) - comment = comment.gsub(/^\/\//, '') - comment = comment.gsub(/^\/\**/, '') - comment = comment.gsub(/^\**/, '') - comment = comment.gsub(/^[\w\*]*\//, '') - comment - end - - # go through all the comment blocks and extract: - # @file, @brief, @defgroup and @ingroup - def extract_meta(data) - file, brief, defgroup, ingroup = nil - data.each do |block| - block[:comments].each do |comment| - m = [] - file = m[1] if m = /@file (.*?)$/.match(comment) - brief = m[1] if m = /@brief (.*?)$/.match(comment) - defgroup = m[1] if m = /@defgroup (.*?)$/.match(comment) - ingroup = m[1] if m = /@ingroup (.*?)$/.match(comment) - end - end - {:file => file, :brief => brief, :defgroup => defgroup, :ingroup => ingroup} - end - - def extract_functions(file, data) - @data[:functions] - funcs = [] - data.each do |block| - ignore = false - code = block[:code].join(" ") - code = code.gsub(/\{(.*)\}/, '') # strip inline code - rawComments = block[:comments] - comments = block[:comments] - - if m = /^(.*?) ([a-z_]+)\((.*)\)/.match(code) - ret = m[1].strip - if r = /\((.*)\)/.match(ret) # strip macro - ret = r[1] + when :define, :macro + @data[:globals][r[:decl]] ||= {} + wanted[:globals].each do |k| + @data[:globals][r[:decl]][k] = r[k] if r.has_key?(k) end - fun = m[2].strip - origArgs = m[3].strip - # replace ridiculous syntax - args = origArgs.gsub(/(\w+) \(\*(.*?)\)\(([^\)]*)\)/) do |m| - type, name = $1, $2 - cast = $3.gsub(',', '###') - "#{type}(*)(#{cast}) #{name}" + when :file + wanted[:meta].each do |k| + file_map[r[:file]][:meta][k] = r[k] if r.has_key?(k) end - args = args.split(',').map do |arg| - argarry = arg.split(' ') - var = argarry.pop - type = argarry.join(' ').gsub('###', ',') + ' ' - - ## split pointers off end of type or beg of name - var.gsub!('*') do |m| - type += '*' - '' + when :enum + if !r[:name] + # Explode unnamed enum into multiple global defines + r[:decl].each do |n| + @data[:globals][n] ||= { + :file => r[:file], :line => r[:line], + :value => "", :comments => r[:comments], + } + m = /#{Regexp.quote(n)}/.match(r[:body]) + if m + @data[:globals][n][:line] += m.pre_match.scan("\n").length + if m.post_match =~ /\s*=\s*([^,\}]+)/ + @data[:globals][n][:value] = $1 + end + end end - desc = '' - comments = comments.gsub(/\@param #{Regexp.escape(var)} ([^@]*)/m) do |m| - desc = $1.gsub("\n", ' ').gsub("\t", ' ').strip - '' + else + @data[:types][r[:name]] ||= {} + wanted[:types].each do |k| + @data[:types][r[:name]][k] = r[k] if r.has_key?(k) end - ## TODO: parse comments to extract data about args - {:type => type.strip, :name => var, :comment => desc} end - sig = args.map do |arg| - arg[:type].to_s - end.join('::') - - return_comment = '' - comments.gsub!(/\@return ([^@]*)/m) do |m| - return_comment = $1.gsub("\n", ' ').gsub("\t", ' ').strip - '' + when :struct, :fnptr + @data[:types][r[:name]] ||= {} + r[:value] ||= r[:name] + wanted[:types].each do |k| + @data[:types][r[:name]][k] = r[k] if r.has_key?(k) end - - comments = strip_block(comments) - comment_lines = comments.split("\n\n") - - desc = '' - if comments.size > 0 - desc = comment_lines.shift.split("\n").map { |e| e.strip }.join(' ') - comments = comment_lines.join("\n\n").strip + if r[:type] == :fnptr + @data[:types][r[:name]][:type] = "function pointer" end - next if fun == 'defined' - @data[:functions][fun] = { - :description => desc, - :return => {:type => ret, :comment => return_comment}, - :args => args, - :argline => origArgs, - :file => file, - :line => block[:line], - :lineto => block[:lineto], - :comments => comments, - :sig => sig, - :rawComments => rawComments - } - funcs << fun + else + # Anything else we want to record? end + end - funcs - end - # TODO: rolled this back, want to strip the first few spaces, not everything - def strip_block(block) - block.strip + @data[:files] << file_map.values[0] end - def mkdir_temp - tf = Tempfile.new('docurium') - tpath = tf.path - tf.unlink - FileUtils.mkdir_p(tpath) - tpath - end - - def mkfile_temp - tf = Tempfile.new('docurium-index') - tpath = tf.path - tf.unlink - tpath - end - - def copy_site(outdir) - here = File.expand_path(File.dirname(__FILE__)) - FileUtils.mkdir_p(outdir) - Dir.chdir(outdir) do - FileUtils.cp_r(File.join(here, '..', 'site', '.'), '.') + def add_dir_to_index(index, prefix, dir) + Dir.new(dir).each do |filename| + next if [".", ".."].include? filename + name = File.join(dir, filename) + if File.directory? name + add_dir_to_index(index, prefix, name) + else + rel_path = name.gsub(prefix, '') + content = File.read(name) + sha = @repo.write(content, :blob) + index.add(:path => rel_path, :oid => sha, :mode => 0100644) + end end end - def write_dir - out "Writing to directory #{output_dir}" - out "Done!" + def write_site(index) + here = File.expand_path(File.dirname(__FILE__)) + dirname = File.join(here, '..', 'site') + dirname = File.realpath(dirname) + add_dir_to_index(index, dirname + '/', dirname) end def out(text) puts text end