docurium 0.4.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/docurium.rb CHANGED
@@ -4,12 +4,14 @@ require 'version_sorter'
4
4
  require 'rocco'
5
5
  require 'docurium/version'
6
6
  require 'docurium/layout'
7
+ require 'docurium/debug'
7
8
  require 'libdetect'
8
9
  require 'docurium/docparser'
9
10
  require 'pp'
10
11
  require 'rugged'
11
12
  require 'redcarpet'
12
13
  require 'redcarpet/compat'
14
+ require 'parallel'
13
15
  require 'thread'
14
16
 
15
17
  # Markdown expects the old redcarpet compat API, so let's tell it what
@@ -17,13 +19,15 @@ require 'thread'
17
19
  Rocco::Markdown = RedcarpetCompat
18
20
 
19
21
  class Docurium
20
- attr_accessor :branch, :output_dir, :data
22
+ attr_accessor :branch, :output_dir, :data, :head_data
21
23
 
22
- def initialize(config_file, repo = nil)
24
+ def initialize(config_file, cli_options = {}, repo = nil)
23
25
  raise "You need to specify a config file" if !config_file
24
26
  raise "You need to specify a valid config file" if !valid_config(config_file)
25
27
  @sigs = {}
26
- @repo = repo || Rugged::Repository.discover('.')
28
+ @head_data = nil
29
+ @repo = repo || Rugged::Repository.discover(config_file)
30
+ @cli_options = cli_options
27
31
  end
28
32
 
29
33
  def init_data(version = 'HEAD')
@@ -115,8 +119,31 @@ class Docurium
115
119
  def generate_doc_for(version)
116
120
  index = Rugged::Index.new
117
121
  read_subtree(index, version, option_version(version, 'input', ''))
122
+
118
123
  data = parse_headers(index, version)
119
- data
124
+ examples = format_examples!(data, version)
125
+ [data, examples]
126
+ end
127
+
128
+ def process_project(versions)
129
+ nversions = versions.count
130
+ Parallel.each_with_index(versions, finish: -> (version, index, result) do
131
+ data, examples = result
132
+ # There's still some work we need to do serially
133
+ tally_sigs!(version, data)
134
+ force_utf8(data)
135
+
136
+ puts "Adding documentation for #{version} [#{index}/#{nversions}]"
137
+
138
+ # Store it so we can show it at the end
139
+ @head_data = data if version == 'HEAD'
140
+
141
+ yield index, version, result if block_given?
142
+
143
+ end) do |version, index|
144
+ puts "Generating documentation for #{version} [#{index}/#{nversions}]"
145
+ generate_doc_for(version)
146
+ end
120
147
  end
121
148
 
122
149
  def generate_docs
@@ -125,68 +152,46 @@ class Docurium
125
152
  @tf = File.expand_path(File.join(File.dirname(__FILE__), 'docurium', 'layout.mustache'))
126
153
  versions = get_versions
127
154
  versions << 'HEAD'
128
- nversions = versions.size
129
- output = Queue.new
130
- pipes = {}
131
- versions.each do |version|
132
- # We don't need to worry about joining since this process is
133
- # going to die immediately
134
- read, write = IO.pipe
135
- pid = Process.fork do
136
- read.close
137
-
138
- data = generate_doc_for(version)
139
- examples = format_examples!(data, version)
140
-
141
- Marshal.dump([version, data, examples], write)
142
- write.close
155
+ # If the user specified versions, validate them and overwrite
156
+ if !(vers = (@cli_options[:for] || [])).empty?
157
+ vers.each do |v|
158
+ next if versions.include?(v)
159
+ puts "Unknown version #{v}"
160
+ exit(false)
143
161
  end
144
-
145
- pipes[pid] = read
146
- write.close
162
+ versions = vers
147
163
  end
148
164
 
149
- print "Generating documentation [0/#{nversions}]\r"
150
- head_data = nil
151
-
152
- # This may seem odd, but we need to keep reading from the pipe or
153
- # the buffer will fill and they'll block and never exit. Therefore
154
- # we can't rely on Process.wait to tell us when the work is
155
- # done. Instead read from all the pipes concurrently and send the
156
- # ruby objects through the queue.
157
- Thread.abort_on_exception = true
158
- pipes.each do |pid, read|
159
- Thread.new do
160
- result = read.read
161
- output << Marshal.load(result)
162
- end
165
+ if (@repo.config['user.name'].nil? || @repo.config['user.email'].nil?)
166
+ puts "ERROR: 'user.name' or 'user.email' is not configured. Docurium will not be able to commit the documentation"
167
+ exit(false)
163
168
  end
164
169
 
165
- for i in 1..nversions
166
- version, data, examples = output.pop
167
-
168
- # There's still some work we need to do serially
169
- tally_sigs!(version, data)
170
+ process_project(versions) do |i, version, result|
171
+ data, examples = result
170
172
  sha = @repo.write(data.to_json, :blob)
171
173
 
172
- print "Generating documentation [#{i}/#{nversions}]\r"
174
+ print "Generating documentation [#{i}/#{versions.count}]\r"
173
175
 
174
- # Store it so we can show it at the end
175
- if version == 'HEAD'
176
- head_data = data
176
+ unless dry_run?
177
+ output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644)
178
+ examples.each do |path, id|
179
+ output_index.add(:path => path, :oid => id, :mode => 0100644)
180
+ end
177
181
  end
182
+ end
178
183
 
179
- output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644)
180
- examples.each do |path, id|
181
- output_index.add(:path => path, :oid => id, :mode => 0100644)
182
- end
184
+ if head_data
185
+ puts ''
186
+ show_warnings(head_data)
187
+ end
183
188
 
184
- if head_data
185
- puts ''
186
- show_warnings(data)
187
- end
189
+ return if dry_run?
188
190
 
189
- end
191
+ # We tally the signatures in the order they finished, which is
192
+ # arbitrary due to the concurrency, so we need to sort them once
193
+ # they've finished.
194
+ sort_sigs!
190
195
 
191
196
  project = {
192
197
  :versions => versions.reverse,
@@ -220,34 +225,145 @@ class Docurium
220
225
  puts "\tupdated #{br}"
221
226
  end
222
227
 
223
- def show_warnings(data)
224
- out '* checking your api'
228
+ def force_utf8(data)
229
+ # Walk the data to force strings encoding to UTF-8.
230
+ if data.instance_of? Hash
231
+ data.each do |key, value|
232
+ if [:comment, :comments, :description].include?(key)
233
+ data[key] = value.force_encoding('UTF-8') unless value.nil?
234
+ else
235
+ force_utf8(value)
236
+ end
237
+ end
238
+ elsif data.respond_to?(:each)
239
+ data.each { |x| force_utf8(x) }
240
+ end
241
+ end
242
+
243
+ class Warning
244
+ class UnmatchedParameter < Warning
245
+ def initialize(function, opts = {})
246
+ super :unmatched_param, :function, function, opts
247
+ end
248
+
249
+ def _message; "unmatched param"; end
250
+ end
251
+
252
+ class SignatureChanged < Warning
253
+ def initialize(function, opts = {})
254
+ super :signature_changed, :function, function, opts
255
+ end
256
+
257
+ def _message; "signature changed"; end
258
+ end
259
+
260
+ class MissingDocumentation < Warning
261
+ def initialize(type, identifier, opts = {})
262
+ super :missing_documentation, type, identifier, opts
263
+ end
264
+
265
+ def _message
266
+ ["%s %s is missing documentation", :type, :identifier]
267
+ end
268
+ end
269
+
270
+ WARNINGS = [
271
+ :unmatched_param,
272
+ :signature_changed,
273
+ :missing_documentation,
274
+ ]
275
+
276
+ attr_reader :warning, :type, :identifier, :file, :line, :column
277
+
278
+ def initialize(warning, type, identifier, opts = {})
279
+ raise ArgumentError.new("invalid warning class") unless WARNINGS.include?(warning)
280
+ @warning = warning
281
+ @type = type
282
+ @identifier = identifier
283
+ if type = opts.delete(:type)
284
+ @file = type[:file]
285
+ if input_dir = opts.delete(:input_dir)
286
+ File.expand_path(File.join(input_dir, @file))
287
+ end
288
+ @file ||= "<missing>"
289
+ @line = type[:line] || 1
290
+ @column = type[:column] || 1
291
+ end
292
+ end
293
+
294
+ def message
295
+ msg = self._message
296
+ msg.kind_of?(Array) ? msg.shift % msg.map {|a| self.send(a).to_s } : msg
297
+ end
298
+ end
299
+
300
+ def collect_warnings(data)
301
+ warnings = []
302
+ input_dir = File.join(@project_dir, option_version("HEAD", 'input'))
225
303
 
226
304
  # check for unmatched paramaters
227
- unmatched = []
228
305
  data[:functions].each do |f, fdata|
229
- unmatched << f if fdata[:comments] =~ /@param/
230
- end
231
- if unmatched.size > 0
232
- out ' - unmatched params in'
233
- unmatched.sort.each { |p| out ("\t" + p) }
306
+ warnings << Warning::UnmatchedParameter.new(f, type: fdata, input_dir: input_dir) if fdata[:comments] =~ /@param/
234
307
  end
235
308
 
236
309
  # check for changed signatures
237
310
  sigchanges = []
238
- @sigs.each do |fun, data|
239
- if data[:changes]['HEAD']
240
- sigchanges << fun
311
+ @sigs.each do |fun, sig_data|
312
+ warnings << Warning::SignatureChanged.new(fun) if sig_data[:changes]['HEAD']
313
+ end
314
+
315
+ # check for undocumented things
316
+ types = [:functions, :callbacks, :globals, :types]
317
+ types.each do |type_id|
318
+ under_type = type_id.tap {|t| break t.to_s[0..-2].to_sym }
319
+ data[type_id].each do |ident, type|
320
+ under_type = type[:type] if type_id == :types
321
+
322
+ warnings << Warning::MissingDocumentation.new(under_type, ident, type: type, input_dir: input_dir) if type[:description].empty?
323
+
324
+ case type[:type]
325
+ when :struct
326
+ if type[:fields]
327
+ type[:fields].each do |field|
328
+ warnings << Warning::MissingDocumentation.new(:field, "#{ident}.#{field[:name]}", type: type, input_dir: input_dir) if field[:comments].empty?
329
+ end
330
+ end
331
+ end
241
332
  end
242
333
  end
243
- if sigchanges.size > 0
244
- out ' - signature changes in'
245
- sigchanges.sort.each { |p| out ("\t" + p) }
334
+ warnings
335
+ end
336
+
337
+ def check_warnings(options)
338
+ versions = []
339
+ versions << get_versions.pop
340
+ versions << 'HEAD'
341
+
342
+ process_project(versions)
343
+
344
+ collect_warnings(head_data).each do |warning|
345
+ puts "#{warning.file}:#{warning.line}:#{warning.column}: #{warning.message}"
346
+ end
347
+ end
348
+
349
+ def show_warnings(data)
350
+ out '* checking your api'
351
+
352
+ collect_warnings(data).group_by {|w| w.warning }.each do |klass, klass_warnings|
353
+ klass_warnings.group_by {|w| w.type }.each do |type, type_warnings|
354
+ out " - " + type_warnings[0].message
355
+ type_warnings.sort_by {|w| w.identifier }.each do |warning|
356
+ out "\t" + warning.identifier
357
+ end
358
+ end
246
359
  end
247
360
  end
248
361
 
249
362
  def get_versions
250
- VersionSorter.sort(@repo.tags.map { |tag| tag.name.gsub(%r(^refs/tags/), '') })
363
+ releases = @repo.tags
364
+ .map { |tag| tag.name.gsub(%r(^refs/tags/), '') }
365
+ .delete_if { |tagname| tagname.match(%r(-rc\d*$)) }
366
+ VersionSorter.sort(releases)
251
367
  end
252
368
 
253
369
  def parse_headers(index, version)
@@ -258,10 +374,11 @@ class Docurium
258
374
  end
259
375
 
260
376
  data = init_data(version)
261
- parser = DocParser.new
262
- headers.each do |header|
263
- records = parser.parse_file(header, files)
264
- update_globals!(data, records)
377
+ DocParser.with_files(files, :prefix => version) do |parser|
378
+ headers.each do |header|
379
+ records = parser.parse_file(header, debug: interesting?(:file, header))
380
+ update_globals!(data, records)
381
+ end
265
382
  end
266
383
 
267
384
  data[:groups] = group_functions!(data)
@@ -288,6 +405,14 @@ class Docurium
288
405
  end
289
406
  end
290
407
 
408
+ def sort_sigs!
409
+ @sigs.keys.each do |fn|
410
+ VersionSorter.sort!(@sigs[fn][:exists])
411
+ # Put HEAD at the back
412
+ @sigs[fn][:exists] << @sigs[fn][:exists].shift
413
+ end
414
+ end
415
+
291
416
  def find_subtree(version, path)
292
417
  tree = nil
293
418
  if version == 'HEAD'
@@ -326,40 +451,57 @@ class Docurium
326
451
  def group_functions!(data)
327
452
  func = {}
328
453
  data[:functions].each_pair do |key, value|
454
+ debug_set interesting?(:function, key)
455
+ debug "grouping #{key}: #{value}"
329
456
  if @options['prefix']
330
457
  k = key.gsub(@options['prefix'], '')
331
458
  else
332
459
  k = key
333
460
  end
334
461
  group, rest = k.split('_', 2)
335
- next if group.empty?
336
- if !rest
337
- group = value[:file].gsub('.h', '').gsub('/', '_')
462
+ debug "grouped: k: #{k}, group: #{group}, rest: #{rest}"
463
+ if group.empty?
464
+ puts "empty group for function #{key}"
465
+ next
338
466
  end
467
+ debug "grouped: k: #{k}, group: #{group}, rest: #{rest}"
339
468
  data[:functions][key][:group] = group
340
469
  func[group] ||= []
341
470
  func[group] << key
342
471
  func[group].sort!
343
472
  end
344
- misc = []
345
473
  func.to_a.sort
346
474
  end
347
475
 
348
476
  def find_type_usage!(data)
349
- # go through all the functions and see where types are used and returned
477
+ # go through all functions, callbacks, and structs
478
+ # see which other types are used and returned
350
479
  # store them in the types data
351
- data[:functions].each do |func, fdata|
480
+ h = {}
481
+ h.merge!(data[:functions])
482
+ h.merge!(data[:callbacks])
483
+
484
+ structs = data[:types].find_all {|t, tdata| (tdata[:type] == :struct and tdata[:fields] and not tdata[:fields].empty?) }
485
+ structs = Hash[structs.map {|t, tdata| [t, tdata] }]
486
+ h.merge!(structs)
487
+
488
+ h.each do |use, use_data|
352
489
  data[:types].each_with_index do |tdata, i|
353
490
  type, typeData = tdata
354
- data[:types][i][1][:used] ||= {:returns => [], :needs => []}
355
- if fdata[:return][:type].index(/#{type}[ ;\)\*]/)
356
- data[:types][i][1][:used][:returns] << func
491
+
492
+ data[:types][i][1][:used] ||= {:returns => [], :needs => [], :fields => []}
493
+ if use_data[:return] && use_data[:return][:type].index(/#{type}[ ;\)\*]?/)
494
+ data[:types][i][1][:used][:returns] << use
357
495
  data[:types][i][1][:used][:returns].sort!
358
496
  end
359
- if fdata[:argline].index(/#{type}[ ;\)\*]/)
360
- data[:types][i][1][:used][:needs] << func
497
+ if use_data[:argline] && use_data[:argline].index(/#{type}[ ;\)\*]?/)
498
+ data[:types][i][1][:used][:needs] << use
361
499
  data[:types][i][1][:used][:needs].sort!
362
500
  end
501
+ if use_data[:fields] and use_data[:fields].find {|f| f[:type] == type }
502
+ data[:types][i][1][:used][:fields] << use
503
+ data[:types][i][1][:used][:fields].sort!
504
+ end
363
505
  end
364
506
  end
365
507
  end
@@ -376,9 +518,28 @@ class Docurium
376
518
 
377
519
  file_map = {}
378
520
 
379
- md = Redcarpet::Markdown.new Redcarpet::Render::HTML, :no_intra_emphasis => true
521
+ md = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new({}), :no_intra_emphasis => true)
380
522
  recs.each do |r|
381
523
 
524
+ types = %w(function file type).map(&:to_sym)
525
+ dbg = false
526
+ types.each do |t|
527
+ dbg ||= if r[:type] == t and interesting?(t, r[:name])
528
+ true
529
+ elsif t == :file and interesting?(:file, r[:file])
530
+ true
531
+ elsif [:struct, :enum].include?(r[:type]) and interesting?(:type, r[:name])
532
+ true
533
+ else
534
+ false
535
+ end
536
+ end
537
+
538
+ debug_set dbg
539
+
540
+ debug "processing record: #{r}"
541
+ debug
542
+
382
543
  # initialize filemap for this file
383
544
  file_map[r[:file]] ||= {
384
545
  :file => r[:file], :functions => [], :meta => {}, :lines => 0
@@ -390,7 +551,7 @@ class Docurium
390
551
  # process this type of record
391
552
  case r[:type]
392
553
  when :function, :callback
393
- t = r[:type] == :function ? :functions : :callbacks
554
+ t = r[:type] == :function ? :functions : :callbacks
394
555
  data[t][r[:name]] ||= {}
395
556
  wanted[:functions].each do |k|
396
557
  next unless r.has_key? k
@@ -458,13 +619,24 @@ class Docurium
458
619
 
459
620
  when :struct, :fnptr
460
621
  data[:types][r[:name]] ||= {}
622
+ known = data[:types][r[:name]]
461
623
  r[:value] ||= r[:name]
462
- wanted[:types].each do |k|
463
- next unless r.has_key? k
464
- if k == :comments
465
- data[:types][r[:name]][k] = md.render r[k]
466
- else
467
- data[:types][r[:name]][k] = r[k]
624
+ # we don't want to override "opaque" structs with typedefs or
625
+ # "public" documentation
626
+ unless r[:tdef].nil? and known[:fields] and known[:comments] and known[:description]
627
+ wanted[:types].each do |k|
628
+ next unless r.has_key? k
629
+ if k == :comments
630
+ data[:types][r[:name]][k] = md.render r[k]
631
+ else
632
+ data[:types][r[:name]][k] = r[k]
633
+ end
634
+ end
635
+ else
636
+ # We're about to skip that type. Just make sure we preserve the
637
+ # :fields comment
638
+ if r[:fields] and known[:fields].empty?
639
+ data[:types][r[:name]][:fields] = r[:fields]
468
640
  end
469
641
  end
470
642
  if r[:type] == :fnptr
@@ -475,6 +647,10 @@ class Docurium
475
647
  # Anything else we want to record?
476
648
  end
477
649
 
650
+ debug "processed record: #{r}"
651
+ debug
652
+
653
+ debug_restore
478
654
  end
479
655
 
480
656
  data[:files] << file_map.values[0]
@@ -505,4 +681,12 @@ class Docurium
505
681
  def out(text)
506
682
  puts text
507
683
  end
684
+
685
+ def dry_run?
686
+ @cli_options[:dry_run]
687
+ end
688
+
689
+ def interesting?(type, what)
690
+ @cli_options['debug'] || (@cli_options["debug-#{type}"] || []).include?(what)
691
+ end
508
692
  end
data/lib/libdetect.rb CHANGED
@@ -15,7 +15,7 @@ module LibDetect
15
15
  ENV['LIBCLANG'] = DARWIN_LIBCLANG
16
16
  when /linux/
17
17
  prog = 'llvm-config'
18
- if find_executable(prog)
18
+ if find_executable(prog) and not ENV.has_key?('LLVM_CONFIG')
19
19
  ENV['LLVM_CONFIG'] = prog
20
20
  end
21
21
  end
data/site/css/style.css CHANGED
@@ -95,12 +95,12 @@ input.search {
95
95
  background: url(../images/search_icon.png) 5px 50% no-repeat white;
96
96
  }
97
97
 
98
- a small {
98
+ a small {
99
99
  font-size: 0.8em;
100
100
  color: #aaa;
101
101
  }
102
102
 
103
- h2 small {
103
+ h2 small {
104
104
  font-size: 0.8em;
105
105
  font-weight: normal;
106
106
  color: #666;
@@ -117,18 +117,25 @@ table.methods tr td.methodName a {
117
117
  font-weight: bold;
118
118
  }
119
119
 
120
- table.funcTable tr td {
120
+ table.funcTable tr td,
121
+ table.structTable tr td {
121
122
  padding: 5px 10px;
122
123
  border-bottom: 1px solid #eee;
123
124
  }
124
- table.funcTable tr td.comment {
125
- color: #999;
126
- }
127
- table.funcTable tr td.var {
125
+
126
+ .enumTable .var,
127
+ .funcTable .var,
128
+ .structTable .var {
128
129
  font-weight: bold;
129
130
  color: #833;
130
131
  }
131
132
 
133
+ .enumTable .type,
134
+ .funcTable .type,
135
+ .structTable .type {
136
+ text-align: right;
137
+ }
138
+
132
139
  code.params {
133
140
  white-space: pre-wrap; /* css-3 */
134
141
  white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
@@ -152,7 +159,9 @@ code.params {
152
159
 
153
160
  .returns { margin-bottom: 15px; }
154
161
 
155
- h1.funcTitle {
162
+ h1.funcTitle,
163
+ h1.enumTitle,
164
+ h1.structTitle {
156
165
  font-size: 1.6em;
157
166
  }
158
167
  h3.funcDesc {
@@ -255,6 +264,3 @@ p.functionList a.introd {
255
264
  color: #933;
256
265
  }
257
266
 
258
- .type-comment {
259
- padding-left: 3em;
260
- }