Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

548 lines (493 sloc) 15.519 kB
require 'json'
require 'tempfile'
require 'version_sorter'
require 'rocco'
require 'docurium/layout'
require 'pp'
class Docurium
Version = VERSION = '0.0.3'
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 = {}
clear_data
end
def clear_data(version = 'HEAD')
@data = {:files => [], :functions => {}, :globals => {}, :types => {}, :prefix => ''}
@data[:prefix] = option_version(version, 'input', '')
end
def option_version(version, option, default = nil)
if @options['legacy']
if valhash = @options['legacy'][option]
valhash.each do |value, versions|
return value if versions.include?(version)
end
end
end
opt = @options[option]
opt = default if !opt
opt
end
def generate_docs
out "* generating docs"
outdir = mkdir_temp
copy_site(outdir)
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
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`
files = []
Dir.glob("**/*") do |file|
next if !File.file?(file)
files << file
end
files.each do |file|
out " # #{file}"
# 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
rf_path = File.basename(file).split('.')[0..-2].join('.') + '.html'
rel_path = "ex/#{version}/#{rf_path}"
rf_path = File.join(outdir, rel_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}\" href=\"../../##{version}/group/#{fdata[:group]}/#{f}\">#{f}</a>#{extra}"
end
end
# 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
end
end
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
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
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
end
def show_warnings
out '* checking your api'
# check for unmatched paramaters
unmatched = []
@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) }
end
# check for changed signatures
sigchanges = []
@sigs.each do |fun, data|
if data[:changes]['HEAD']
sigchanges << fun
end
end
if sigchanges.size > 0
out ' - signature changes in'
sigchanges.sort.each { |p| out ("\t" + p) }
end
end
def get_versions
VersionSorter.sort(git('tag').split("\n"))
end
def parse_headers
headers.each do |header|
parse_header(header)
end
@data[:groups] = group_functions
@data[:types] = @data[:types].sort # make it an assoc array
find_type_usage
end
private
def tally_sigs(version)
@lastsigs ||= {}
@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
end
end
@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}`
end
out.strip
end
def checkout(version, workdir)
with_git_env(workdir) do
`git read-tree #{version}:#{@data[:prefix]}`
`git checkout-index -a`
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')
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
end
def group_functions
func = {}
@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
func[group] ||= []
func[group] << key
func[group].sort!
end
misc = []
func.to_a.sort
end
def headers
h = []
Dir.glob(File.join('**/*.h')).each do |header|
next if !File.file?(header)
h << header
end
h
end
def find_type_usage
# 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|
type, typeData = tdata
@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!
end
if fdata[:argline].index(/#{type}[ ;\)\*]/)
@data[:types][i][1][:used][:needs] << func
@data[:types][i][1][:used][:needs].sort!
end
end
end
end
def header_content(header_path)
File.readlines(header_path)
end
def parse_header(filepath)
lineno = 0
content = header_content(filepath)
# look for structs and enums
in_block = false
block = ''
linestart = 0
tdef, type, name = nil
content.each do |line|
lineno += 1
line = line.strip
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
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}
end
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
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
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]
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}"
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 += '*'
''
end
desc = ''
comments = comments.gsub(/\@param #{Regexp.escape(var)} ([^@]*)/m) do |m|
desc = $1.gsub("\n", ' ').gsub("\t", ' ').strip
''
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
''
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
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
end
end
funcs
end
# TODO: rolled this back, want to strip the first few spaces, not everything
def strip_block(block)
block.strip
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', '.'), '.')
end
end
def write_dir
out "Writing to directory #{output_dir}"
out "Done!"
end
def out(text)
puts text
end
end
Jump to Line
Something went wrong with that request. Please try again.