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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +42 -0
- data/Gemfile +4 -2
- data/bin/cm +14 -0
- data/docurium.gemspec +3 -2
- data/lib/docurium/cli.rb +6 -1
- data/lib/docurium/debug.rb +41 -0
- data/lib/docurium/docparser.rb +94 -26
- data/lib/docurium/layout.rb +0 -3
- data/lib/docurium/version.rb +1 -1
- data/lib/docurium.rb +235 -96
- data/site/css/style.css +17 -11
- data/site/index.html +123 -114
- data/site/js/backbone.js +1 -1
- data/site/js/docurium.js +151 -157
- data/site/js/underscore.js +5 -5
- data/test/docurium_test.rb +52 -15
- data/test/fixtures/git2/cherrypick.h +2 -0
- data/test/fixtures/git2/common.h +1 -6
- data/test/fixtures/git2/repository.h +39 -0
- data/test/fixtures/git2/types.h +2 -0
- data/test/parser_test.rb +8 -4
- metadata +26 -12
- data/.travis.yml +0 -14
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
|
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 =
|
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
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
176
|
-
|
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}/#{
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
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
|
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
|
-
|
255
|
-
|
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
|
-
|
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,
|
270
|
-
if
|
271
|
-
|
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
|
-
|
275
|
-
|
276
|
-
|
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
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
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
|
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
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
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
|
405
|
-
data[:types][i][1][:used][:needs] <<
|
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
|
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
|
-
|
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
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
}
|