docurium 0.6.0 → 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 = {}
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
- }