require 'rexml/text'
require 'pathname'
require 'erb'
module DNote
# = Developer Notes
#
# This class goes through you source files and compiles
# an list of any labeled comments. Labels are single word
# prefixes to a comment ending in a colon.
#
# By default the labels supported are TODO, FIXME, OPTIMIZE and DEPRECATE.
#
# Output is a set of files in XML and RDoc's simple
# markup format.
#
# TODO: Add ability to read header notes. They oftern
# have a outline format, rather then the single line.
#
# TODO: Need good CSS file.
#
# TODO: Need XSL?
#
class Notes
# Default note labels to look for in source code.
DEFAULT_LABELS = ['TODO', 'FIXME', 'OPTIMIZE', 'DEPRECATE']
#
attr_accessor :title
# Paths to search.
attr_accessor :paths
# Labels to document. Defaults are: TODO, FIXME, OPTIMIZE and DEPRECATE.
attr_accessor :labels
#
def initialize(paths, options={})
initialize_defaults
options.each do |k, v|
__send__("#{k}=", v)
end
#@paths = ['**/*.rb'] if @paths.empty?
parse
end
#
def initialize_defaults
@labels = DEFAULT_LABELS
@paths = ["**/*.rb"]
@title = "Developer's Notes"
@format = "rdoc"
end
#
def notes
@notes
end
#
def counts
@counts
end
# Scans source code for developer notes and writes them to
# well organized files.
#
def display(format)
#paths = self.paths
#output = self.output
#parse
#paths = paths.to_list
#labels = labels.split(',') if String === labels
#labels = [labels].flatten.compact
#records, counts = extract(labels, loadpath)
#records = organize(records)
#case format.to_s
#when 'rdoc', 'txt', 'text'
# text = format_rd(records)
#else
# text = format_xml(records)
#end
if notes.empty?
$stderr << "No #{labels.join(', ')} notes.\n"
else
#temp = templates.find{ |f| /#{format}$/ =~ f }
#erb = ERB.new(File.read(temp))
#text = erb.result(binding)
text = __send__("to_#{format}")
#if output
# #templates.each do |template|
# #text = format_notes(notes, format)
# file = write(txt, format)
# #file = file #Pathname.new(file).relative_path_from(Pathname.pwd) #project.root
# puts "Updated #{file}"
# #end
#else
puts text
#end
$stderr << "\n(" + counts.map{|l,n| "#{n} #{l}s"}.join(', ') + ")\n"
end
end
#
def labels=(labels)
@labels = (
case labels
when String
labels.split(/[:;,]/)
else
labels = [labels].flatten.compact.uniq
end
)
end
# Gather and count notes. This returns two elements,
# a hash in the form of label=>notes and a counts hash.
def parse
records, counts = [], Hash.new(0)
files.each do |fname|
next unless File.file?(fname)
#next unless fname =~ /\.rb$/ # TODO: should this be done?
File.open(fname) do |f|
lineno, save, text = 0, nil, nil
while line = f.gets
lineno += 1
save = match_common(line, lineno, fname) || match_arbitrary(line, lineno, fname)
if save
#file = fname
text = save['note']
#save = {'label'=>label,'file'=>file,'line'=>line_no,'note'=>text}
records << save
counts[save['label']] += 1
else
if text
if line =~ /^\s*[#]{0,1}\s*$/ or line !~ /^\s*#/ or line =~ /^\s*#[+][+]/
text.strip!
text = nil
else
text << ' ' << line.gsub(/^\s*#\s*/,'')
end
end
end
end
end
end
# organize the notes
notes = organize(records)
#
@notes, @counts = notes, counts
end
#
def files
@files ||= (
self.paths.map do |path|
if File.directory?(path)
Dir.glob(File.join(path, '**/*'))
else
Dir.glob(path)
end
end.flatten.uniq
)
end
#
def match_common(line, lineno, file)
rec = nil
labels.each do |label|
if md = /\#\s*#{Regexp.escape(label)}[:]?\s*(.*?)$/.match(line)
text = md[1]
rec = {'label'=>label,'file'=>file,'line'=>lineno,'note'=>text}
end
end
return rec
end
#
def match_arbitrary(line, lineno, file)
rec = nil
labels.each do |label|
if md = /\#\s*([A-Z]+)[:]\s*(.*?)$/.match(line)
label, text = md[1], md[2]
rec = {'label'=>label,'file'=>file,'line'=>lineno,'note'=>text}
end
end
return rec
end
# Organize records in heirarchical form.
#
def organize(records)
orecs = {}
records.each do |record|
label = record['label']
file = record['file']
line = record['line']
note = record['note'].rstrip
orecs[label] ||= {}
orecs[label][file] ||= []
orecs[label][file] << [line, note]
end
orecs
end
#
def to(format)
__send__("to_#{format}")
end
# Format notes in RDoc format.
#
def to_rdoc
out = []
out << "= Development Notes"
notes.each do |label, per_file|
out << %[\n== #{label}]
per_file.each do |file, line_notes|
out << %[\n=== file://#{file}\n]
line_notes.sort!{ |a,b| a[0] <=> b[0] }
line_notes.each do |line, note|
out << %[* #{note} (#{line})]
end
end
end
return out.join("\n")
end
# Format notes in RDoc format.
#
def to_markdown
out = []
out << "# Development Notes"
notes.each do |label, per_file|
out << %[\n## #{label}]
per_file.each do |file, line_notes|
out << %[\n### file://#{file}\n]
line_notes.sort!{ |a,b| a[0] <=> b[0] }
line_notes.each do |line, note|
out << %[* #{note} (#{line})]
end
end
end
return out.join("\n")
end
# Format notes in XML format.
#
def to_xml
xml = []
xml << "