docurium 0.4.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- }