docurium 0.6.0 → 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 = {}
28
+ @head_data = nil
26
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,18 +119,41 @@ 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
- def generate_docs(options)
149
+ def generate_docs
123
150
  output_index = Rugged::Index.new
124
151
  write_site(output_index)
125
152
  @tf = File.expand_path(File.join(File.dirname(__FILE__), 'docurium', 'layout.mustache'))
126
153
  versions = get_versions
127
154
  versions << 'HEAD'
128
155
  # If the user specified versions, validate them and overwrite
129
- if !(vers = options[:for]).empty?
156
+ if !(vers = (@cli_options[:for] || [])).empty?
130
157
  vers.each do |v|
131
158
  next if versions.include?(v)
132
159
  puts "Unknown version #{v}"
@@ -135,73 +162,35 @@ class Docurium
135
162
  versions = vers
136
163
  end
137
164
 
138
- nversions = versions.size
139
- output = Queue.new
140
- pipes = {}
141
- versions.each do |version|
142
- # We don't need to worry about joining since this process is
143
- # going to die immediately
144
- read, write = IO.pipe
145
- pid = Process.fork do
146
- read.close
147
-
148
- data = generate_doc_for(version)
149
- examples = format_examples!(data, version)
150
-
151
- Marshal.dump([version, data, examples], write)
152
- write.close
153
- end
154
-
155
- pipes[pid] = read
156
- write.close
157
- end
158
-
159
- print "Generating documentation [0/#{nversions}]\r"
160
- head_data = nil
161
-
162
- # This may seem odd, but we need to keep reading from the pipe or
163
- # the buffer will fill and they'll block and never exit. Therefore
164
- # we can't rely on Process.wait to tell us when the work is
165
- # done. Instead read from all the pipes concurrently and send the
166
- # ruby objects through the queue.
167
- Thread.abort_on_exception = true
168
- pipes.each do |pid, read|
169
- Thread.new do
170
- result = read.read
171
- output << Marshal.load(result)
172
- 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)
173
168
  end
174
169
 
175
- for i in 1..nversions
176
- version, data, examples = output.pop
177
-
178
- # There's still some work we need to do serially
179
- tally_sigs!(version, data)
180
- force_utf8(data)
170
+ process_project(versions) do |i, version, result|
171
+ data, examples = result
181
172
  sha = @repo.write(data.to_json, :blob)
182
173
 
183
- print "Generating documentation [#{i}/#{nversions}]\r"
184
-
185
- # Store it so we can show it at the end
186
- if version == 'HEAD'
187
- head_data = data
188
- end
174
+ print "Generating documentation [#{i}/#{versions.count}]\r"
189
175
 
190
- output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644)
191
- examples.each do |path, id|
192
- output_index.add(:path => path, :oid => id, :mode => 0100644)
193
- end
194
-
195
- if head_data
196
- puts ''
197
- show_warnings(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
198
181
  end
182
+ end
199
183
 
184
+ if head_data
185
+ puts ''
186
+ show_warnings(head_data)
200
187
  end
201
188
 
202
- # We tally the sigantures in the order they finished, which is
189
+ return if dry_run?
190
+
191
+ # We tally the signatures in the order they finished, which is
203
192
  # arbitrary due to the concurrency, so we need to sort them once
204
- # they've finsihed.
193
+ # they've finished.
205
194
  sort_sigs!
206
195
 
207
196
  project = {
@@ -251,29 +240,122 @@ class Docurium
251
240
  end
252
241
  end
253
242
 
254
- def show_warnings(data)
255
- out '* checking your api'
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'))
256
303
 
257
304
  # check for unmatched paramaters
258
- unmatched = []
259
305
  data[:functions].each do |f, fdata|
260
- unmatched << f if fdata[:comments] =~ /@param/
261
- end
262
- if unmatched.size > 0
263
- out ' - unmatched params in'
264
- unmatched.sort.each { |p| out ("\t" + p) }
306
+ warnings << Warning::UnmatchedParameter.new(f, type: fdata, input_dir: input_dir) if fdata[:comments] =~ /@param/
265
307
  end
266
308
 
267
309
  # check for changed signatures
268
310
  sigchanges = []
269
- @sigs.each do |fun, data|
270
- if data[:changes]['HEAD']
271
- 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
272
332
  end
273
333
  end
274
- if sigchanges.size > 0
275
- out ' - signature changes in'
276
- 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
277
359
  end
278
360
  end
279
361
 
@@ -292,10 +374,11 @@ class Docurium
292
374
  end
293
375
 
294
376
  data = init_data(version)
295
- parser = DocParser.new
296
- headers.each do |header|
297
- records = parser.parse_file(header, files)
298
- 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
299
382
  end
300
383
 
301
384
  data[:groups] = group_functions!(data)
@@ -368,43 +451,57 @@ class Docurium
368
451
  def group_functions!(data)
369
452
  func = {}
370
453
  data[:functions].each_pair do |key, value|
454
+ debug_set interesting?(:function, key)
455
+ debug "grouping #{key}: #{value}"
371
456
  if @options['prefix']
372
457
  k = key.gsub(@options['prefix'], '')
373
458
  else
374
459
  k = key
375
460
  end
376
461
  group, rest = k.split('_', 2)
462
+ debug "grouped: k: #{k}, group: #{group}, rest: #{rest}"
377
463
  if group.empty?
378
464
  puts "empty group for function #{key}"
379
465
  next
380
466
  end
467
+ debug "grouped: k: #{k}, group: #{group}, rest: #{rest}"
381
468
  data[:functions][key][:group] = group
382
469
  func[group] ||= []
383
470
  func[group] << key
384
471
  func[group].sort!
385
472
  end
386
- misc = []
387
473
  func.to_a.sort
388
474
  end
389
475
 
390
476
  def find_type_usage!(data)
391
- # go through all the functions and callbacks and see where other types are used and returned
477
+ # go through all functions, callbacks, and structs
478
+ # see which other types are used and returned
392
479
  # store them in the types data
393
480
  h = {}
394
481
  h.merge!(data[:functions])
395
482
  h.merge!(data[:callbacks])
396
- h.each do |func, fdata|
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|
397
489
  data[:types].each_with_index do |tdata, i|
398
490
  type, typeData = tdata
399
- data[:types][i][1][:used] ||= {:returns => [], :needs => []}
400
- if fdata[:return][:type].index(/#{type}[ ;\)\*]?/)
401
- 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
402
495
  data[:types][i][1][:used][:returns].sort!
403
496
  end
404
- if fdata[:argline].index(/#{type}[ ;\)\*]?/)
405
- 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
406
499
  data[:types][i][1][:used][:needs].sort!
407
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
408
505
  end
409
506
  end
410
507
  end
@@ -421,9 +518,28 @@ class Docurium
421
518
 
422
519
  file_map = {}
423
520
 
424
- md = Redcarpet::Markdown.new Redcarpet::Render::HTML, :no_intra_emphasis => true
521
+ md = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new({}), :no_intra_emphasis => true)
425
522
  recs.each do |r|
426
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
+
427
543
  # initialize filemap for this file
428
544
  file_map[r[:file]] ||= {
429
545
  :file => r[:file], :functions => [], :meta => {}, :lines => 0
@@ -435,7 +551,7 @@ class Docurium
435
551
  # process this type of record
436
552
  case r[:type]
437
553
  when :function, :callback
438
- t = r[:type] == :function ? :functions : :callbacks
554
+ t = r[:type] == :function ? :functions : :callbacks
439
555
  data[t][r[:name]] ||= {}
440
556
  wanted[:functions].each do |k|
441
557
  next unless r.has_key? k
@@ -503,13 +619,24 @@ class Docurium
503
619
 
504
620
  when :struct, :fnptr
505
621
  data[:types][r[:name]] ||= {}
622
+ known = data[:types][r[:name]]
506
623
  r[:value] ||= r[:name]
507
- wanted[:types].each do |k|
508
- next unless r.has_key? k
509
- if k == :comments
510
- data[:types][r[:name]][k] = md.render r[k]
511
- else
512
- 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]
513
640
  end
514
641
  end
515
642
  if r[:type] == :fnptr
@@ -520,6 +647,10 @@ class Docurium
520
647
  # Anything else we want to record?
521
648
  end
522
649
 
650
+ debug "processed record: #{r}"
651
+ debug
652
+
653
+ debug_restore
523
654
  end
524
655
 
525
656
  data[:files] << file_map.values[0]
@@ -550,4 +681,12 @@ class Docurium
550
681
  def out(text)
551
682
  puts text
552
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
553
692
  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
- }