docurium 0.0.5 → 0.1.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/bin/cm +2 -2
- data/docurium.gemspec +4 -1
- data/lib/docurium.rb +211 -344
- data/lib/docurium/cparser.rb +427 -0
- data/lib/docurium/layout.mustache +5 -1
- data/lib/docurium/version.rb +3 -0
- data/site/js/docurium.js +7 -4
- metadata +119 -108
@@ -0,0 +1,427 @@
|
|
1
|
+
class Docurium
|
2
|
+
class CParser
|
3
|
+
|
4
|
+
# Remove common prefix of all lines in comment.
|
5
|
+
# Otherwise tries to preserve formatting in case it is relevant.
|
6
|
+
def cleanup_comment(comment)
|
7
|
+
return "" unless comment
|
8
|
+
|
9
|
+
lines = 0
|
10
|
+
prefixed = 0
|
11
|
+
shortest = nil
|
12
|
+
|
13
|
+
compacted = comment.sub(/^\n+/,"").sub(/\n\s*$/, "\n")
|
14
|
+
|
15
|
+
compacted.split(/\n/).each do |line|
|
16
|
+
lines += 1
|
17
|
+
if line =~ /^\s*\*\s*$/ || line =~ /^\s*$/
|
18
|
+
# don't count length of blank lines or lines with just a " * " on
|
19
|
+
# them towards shortest common prefix
|
20
|
+
prefixed += 1
|
21
|
+
shortest = line if shortest.nil?
|
22
|
+
elsif line =~ /^(\s*\*\s*)/
|
23
|
+
prefixed += 1
|
24
|
+
shortest = $1 if shortest.nil? || shortest.length > $1.length
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if shortest =~ /\s$/
|
29
|
+
shortest = Regexp.quote(shortest.chop) + "[ \t]"
|
30
|
+
elsif shortest
|
31
|
+
shortest = Regexp.quote(shortest)
|
32
|
+
end
|
33
|
+
|
34
|
+
if lines == prefixed && !shortest.nil? && shortest.length > 0
|
35
|
+
if shortest =~ /\*/
|
36
|
+
return comment.gsub(/^#{shortest}/, "").gsub(/^\s*\*\s*\n/, "\n")
|
37
|
+
else
|
38
|
+
return comment.gsub(/^#{shortest}/, "")
|
39
|
+
end
|
40
|
+
else
|
41
|
+
return comment
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Find the shortest common prefix of an array of strings
|
46
|
+
def shortest_common_prefix(arr)
|
47
|
+
arr.inject do |pfx,str|
|
48
|
+
pfx = pfx.chop while pfx != str[0...pfx.length]; pfx
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Match #define A(B) or #define A
|
53
|
+
# and convert a series of simple #defines into an enum
|
54
|
+
def detect_define(d)
|
55
|
+
if d[:body] =~ /\A\#\s*define\s+((\w+)\([^\)]+\))/
|
56
|
+
d[:type] = :macro
|
57
|
+
d[:decl] = $1.strip
|
58
|
+
d[:name] = $2
|
59
|
+
d[:tdef] = nil
|
60
|
+
elsif d[:body] =~ /\A\#\s*define\s+(\w+)/
|
61
|
+
names = []
|
62
|
+
d[:body].scan(/\#\s*define\s+(\w+)/) { |m| names << m[0].to_s }
|
63
|
+
d[:tdef] = nil
|
64
|
+
names.uniq!
|
65
|
+
if names.length == 1
|
66
|
+
d[:type] = :define
|
67
|
+
d[:decl] = names[0]
|
68
|
+
d[:name] = names[0]
|
69
|
+
elsif names.length > 1
|
70
|
+
d[:type] = :enum
|
71
|
+
d[:decl] = names
|
72
|
+
d[:name] = shortest_common_prefix(names)
|
73
|
+
d[:name].sub!(/_*$/, '')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Take a multi-line #define and join into a simple definition
|
79
|
+
def join_define(text)
|
80
|
+
text = text.split("\n\n", 2).first || ""
|
81
|
+
|
82
|
+
# Ruby 1.8 does not support negative lookbehind regex so let's
|
83
|
+
# get the joined macro definition a slightly more awkward way
|
84
|
+
text.split(/\s*\n\s*/).inject("\\") do |val, line|
|
85
|
+
(val[-1] == ?\\) ? val = val.chop.strip + " " + line : val
|
86
|
+
end.strip.gsub(/^\s*\\*\s*/, '')
|
87
|
+
end
|
88
|
+
|
89
|
+
# Process #define A(B) macros
|
90
|
+
def parse_macro(d)
|
91
|
+
if d[:body] =~ /define\s+#{Regexp.quote(d[:name])}\([^\)]*\)[ \t]*(.*)/m
|
92
|
+
d[:value] = join_define($1)
|
93
|
+
end
|
94
|
+
d[:comments] = d[:rawComments].strip
|
95
|
+
end
|
96
|
+
|
97
|
+
# Process #define A ... macros
|
98
|
+
def parse_define(d)
|
99
|
+
if d[:body] =~ /define\s+#{Regexp.quote(d[:name])}[ \t]*(.*)/
|
100
|
+
d[:value] = join_define($1)
|
101
|
+
end
|
102
|
+
d[:comments] = d[:rawComments].strip
|
103
|
+
end
|
104
|
+
|
105
|
+
# Match enum {} (and possibly a typedef thereof)
|
106
|
+
def detect_enum(d)
|
107
|
+
if d[:body] =~ /\A(typedef)?\s*enum\s*\{([^\}]+)\}\s*([^;]+)?;/i
|
108
|
+
typedef, values, name = $1, $2, $3
|
109
|
+
d[:type] = :enum
|
110
|
+
d[:decl] = values.strip.split(/\s*,\s*/).map do |v|
|
111
|
+
v.split(/\s*=\s*/)[0].strip
|
112
|
+
end
|
113
|
+
if typedef.nil?
|
114
|
+
d[:name] = shortest_common_prefix(d[:decl])
|
115
|
+
d[:name].sub!(/_*$/, '')
|
116
|
+
# Using the common prefix for grouping enum values is a little
|
117
|
+
# overly aggressive in some cases. If we ended up with too short
|
118
|
+
# a prefix or a prefix which is too generic, then skip it.
|
119
|
+
d[:name] = nil unless d[:name].scan('_').length > 1
|
120
|
+
else
|
121
|
+
d[:name] = name
|
122
|
+
end
|
123
|
+
d[:tdef] = typedef
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Process enum definitions
|
128
|
+
def parse_enum(d)
|
129
|
+
if d[:decl].respond_to? :map
|
130
|
+
d[:block] = d[:decl].map { |v| v.strip }.join("\n")
|
131
|
+
else
|
132
|
+
d[:block] = d[:decl]
|
133
|
+
end
|
134
|
+
d[:comments] = d[:rawComments].strip
|
135
|
+
end
|
136
|
+
|
137
|
+
# Match struct {} (and typedef thereof) or opaque struct typedef
|
138
|
+
def detect_struct(d)
|
139
|
+
if d[:body] =~ /\A(typedef)?\s*struct\s*(\w+)?\s*\{([^\}]+)\}\s*([^;]+)?/i
|
140
|
+
typedef, name1, fields, name2 = $1, $2, $3, $4
|
141
|
+
d[:type] = :struct
|
142
|
+
d[:name] = typedef.nil? ? name1 : name2;
|
143
|
+
d[:tdef] = typedef
|
144
|
+
d[:decl] = fields.strip.split(/\s*\;\s*/).map do |x|
|
145
|
+
x.strip.gsub(/\s+/, " ").gsub(/\(\s+/,"(")
|
146
|
+
end
|
147
|
+
elsif d[:body] =~ /\A(typedef)\s+struct\s+\w+\s+(\w+)/
|
148
|
+
d[:type] = :struct
|
149
|
+
d[:decl] = ""
|
150
|
+
d[:name] = $2
|
151
|
+
d[:tdef] = $1
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Process struct definition
|
156
|
+
def parse_struct(d)
|
157
|
+
if d[:decl].respond_to? :map
|
158
|
+
d[:block] = d[:decl].map { |v| v.strip }.join("\n")
|
159
|
+
else
|
160
|
+
d[:block] = d[:decl]
|
161
|
+
end
|
162
|
+
d[:comments] = d[:rawComments].strip
|
163
|
+
end
|
164
|
+
|
165
|
+
# Match other typedefs, checking explicitly for function pointers
|
166
|
+
# but otherwise just trying to extract a name as simply as possible.
|
167
|
+
def detect_typedef(d)
|
168
|
+
if d[:body] =~ /\Atypedef\s+([^;]+);/
|
169
|
+
d[:decl] = $1.strip
|
170
|
+
if d[:decl] =~ /\S+\s+\(\*([^\)]+)\)\(/
|
171
|
+
d[:type] = :fnptr
|
172
|
+
d[:name] = $1
|
173
|
+
else
|
174
|
+
d[:type] = :typedef
|
175
|
+
d[:name] = d[:decl].split(/\s+/).last
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Process typedef definition
|
181
|
+
def parse_typedef(d)
|
182
|
+
d[:comments] = d[:rawComments].strip
|
183
|
+
end
|
184
|
+
|
185
|
+
# Process function pointer typedef definition
|
186
|
+
def parse_fnptr(d)
|
187
|
+
d[:comments] = d[:rawComments].strip
|
188
|
+
end
|
189
|
+
|
190
|
+
# Match function prototypes or inline function declarations
|
191
|
+
def detect_function(d)
|
192
|
+
if d[:body] =~ /[;\{]/
|
193
|
+
d[:type] = :file
|
194
|
+
d[:decl] = ""
|
195
|
+
|
196
|
+
proto = d[:body].split(/[;\{]/, 2).first.strip
|
197
|
+
if proto[-1] == ?)
|
198
|
+
(proto.length - 1).downto(0) do |p|
|
199
|
+
tail = proto[p .. -1]
|
200
|
+
if tail.count(")") == tail.count("(")
|
201
|
+
if proto[0..p] =~ /(\w+)\(\z/
|
202
|
+
d[:name] = $1
|
203
|
+
d[:type] = :function
|
204
|
+
d[:decl] = proto
|
205
|
+
end
|
206
|
+
break
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Process function prototype and comments
|
214
|
+
def parse_function(d)
|
215
|
+
d[:args] = []
|
216
|
+
|
217
|
+
rval, argline = d[:decl].split(/\s*#{Regexp.quote(d[:name])}\s*/, 2)
|
218
|
+
|
219
|
+
# clean up rval if it is like "extern static int" or "GIT_EXTERN(int)"
|
220
|
+
while rval =~ /[A-Za-z0-9_]+\(([^\)]+)\)$/i
|
221
|
+
rval = $1
|
222
|
+
end
|
223
|
+
rval.gsub!(/extern|static/, '')
|
224
|
+
rval.strip!
|
225
|
+
d[:return] = { :type => rval }
|
226
|
+
|
227
|
+
# clean up argline
|
228
|
+
argline = argline.slice(1..-2) while argline[0] == ?( && argline[-1] == ?)
|
229
|
+
d[:argline] = argline.strip
|
230
|
+
d[:args] = []
|
231
|
+
left = 0
|
232
|
+
|
233
|
+
# parse argline
|
234
|
+
(0 .. argline.length).each do |i|
|
235
|
+
next unless argline[i] == ?, || argline.length == i
|
236
|
+
|
237
|
+
s = argline.slice(left .. i)
|
238
|
+
next unless s.count("(") == s.count(")")
|
239
|
+
|
240
|
+
s.chop! if argline[i] == ?,
|
241
|
+
s.strip!
|
242
|
+
|
243
|
+
if s =~ /\(\s*\*\s*(\w+)\s*\)\s*\(/
|
244
|
+
argname = $1
|
245
|
+
d[:args] << {
|
246
|
+
:name => argname,
|
247
|
+
:type => s.sub(/\s*#{Regexp.quote(argname)}\s*/, '').strip
|
248
|
+
}
|
249
|
+
elsif s =~ /\W(\w+)$/
|
250
|
+
argname = $1
|
251
|
+
d[:args] << {
|
252
|
+
:name => argname,
|
253
|
+
:type => s[0 ... - argname.length].strip,
|
254
|
+
}
|
255
|
+
else
|
256
|
+
# argline is probably something like "(void)"
|
257
|
+
end
|
258
|
+
|
259
|
+
left = i + 1
|
260
|
+
end
|
261
|
+
|
262
|
+
# parse comments
|
263
|
+
if d[:rawComments] =~ /\@(param|return)/i
|
264
|
+
d[:args].each do |arg|
|
265
|
+
param_comment = /\@param\s+#{Regexp.quote(arg[:name])}/.match(d[:rawComments])
|
266
|
+
if param_comment
|
267
|
+
after = param_comment.post_match
|
268
|
+
end_comment = after.index(/(?:@param|@return|\Z)/)
|
269
|
+
arg[:comment] = after[0 ... end_comment].strip.gsub(/\s+/, ' ')
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
return_comment = /\@return\s+/.match(d[:rawComments])
|
274
|
+
if return_comment
|
275
|
+
after = return_comment.post_match
|
276
|
+
d[:return][:comment] = after[0 ... after.index(/(?:@param|\Z)/)].strip.gsub(/\s+/, ' ')
|
277
|
+
end
|
278
|
+
else
|
279
|
+
# support for TomDoc params
|
280
|
+
end
|
281
|
+
|
282
|
+
# add in inline parameter comments
|
283
|
+
if d[:inlines] # array of [param line]/[comment] pairs
|
284
|
+
d[:inlines].each do |inl|
|
285
|
+
d[:args].find do |arg|
|
286
|
+
if inl[0] =~ /\b#{Regexp.quote(arg[:name])}$/
|
287
|
+
arg[:comment] += "\n#{inl[1]}"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# generate function signature
|
294
|
+
d[:sig] = d[:args].map { |a| a[:type].to_s }.join('::')
|
295
|
+
|
296
|
+
# pull off function description
|
297
|
+
if d[:rawComments] =~ /^\s*(public|internal|deprecated):/i
|
298
|
+
# support for TomDoc description
|
299
|
+
else
|
300
|
+
desc, comments = d[:rawComments].split("\n\n", 2)
|
301
|
+
d[:description] = desc.strip
|
302
|
+
d[:comments] = comments || ""
|
303
|
+
params_start = d[:comments].index(/\s?\@(?:param|return)/)
|
304
|
+
d[:comments] = d[:comments].slice(0, params_start) if params_start
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Match otherwise unrecognized commented blocks
|
309
|
+
def detect_catchall(d)
|
310
|
+
d[:type] = :file
|
311
|
+
d[:decl] = ""
|
312
|
+
end
|
313
|
+
|
314
|
+
# Process comment blocks that are only associated with the whole file.
|
315
|
+
def parse_file(d)
|
316
|
+
m = []
|
317
|
+
d[:brief] = m[1] if m = /@brief (.*?)$/.match(d[:rawComments])
|
318
|
+
d[:defgroup] = m[1] if m = /@defgroup (.*?)$/.match(d[:rawComments])
|
319
|
+
d[:ingroup] = m[1] if m = /@ingroup (.*?)$/.match(d[:rawComments])
|
320
|
+
comments = d[:rawComments].gsub(/^@.*$/, '').strip + "\n"
|
321
|
+
if d[:comments]
|
322
|
+
d[:comments] = d[:comments].strip + "\n\n" + comments
|
323
|
+
else
|
324
|
+
d[:comments] = comments
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Array of detectors to execute in order
|
329
|
+
DETECTORS = %w(define enum struct typedef function catchall)
|
330
|
+
|
331
|
+
# Given a commented chunk of file, try to parse it.
|
332
|
+
def parse_declaration_block(d)
|
333
|
+
# skip uncommented declarations
|
334
|
+
return unless d[:rawComments].length > 0
|
335
|
+
|
336
|
+
# remove inline comments in declaration
|
337
|
+
while comment = d[:body].index("/*") do
|
338
|
+
end_comment = d[:body].index("*/", comment)
|
339
|
+
d[:body].slice!(comment, end_comment - comment + 2)
|
340
|
+
end
|
341
|
+
|
342
|
+
# if there are multiple #ifdef'ed declarations, we'll just
|
343
|
+
# strip out the #if/#ifdef and use the first one
|
344
|
+
d[:body].sub!(/[^\n]+\n/, '') if d[:body] =~ /\A\#\s*if/
|
345
|
+
|
346
|
+
# try detectors until one assigns a :type to the declaration
|
347
|
+
# it's going to be one of:
|
348
|
+
# - :define -> #defines + convert a series of simple ones to :enum
|
349
|
+
# - :enum -> (typedef)? enum { ... };
|
350
|
+
# - :struct -> (typedef)? struct { ... };
|
351
|
+
# - :fnptr -> typedef x (*fn)(...);
|
352
|
+
# - :typedef -> typedef x y; (not enum, struct, fnptr)
|
353
|
+
# - :function -> rval something(like this);
|
354
|
+
# - :file -> everything else goes to "file" scope
|
355
|
+
DETECTORS.find { |p| method("detect_#{p}").call(d); d.has_key?(:type) }
|
356
|
+
|
357
|
+
# if we detected something, call a parser for that type of thing
|
358
|
+
method("parse_#{d[:type]}").call(d) if d[:type]
|
359
|
+
end
|
360
|
+
|
361
|
+
# Parse a chunk of text as a header file
|
362
|
+
def parse_text(filename, content)
|
363
|
+
# break into comments and non-comments with line numbers
|
364
|
+
content = "/** */" + content if content[0..2] != "/**"
|
365
|
+
recs = []
|
366
|
+
lineno = 1
|
367
|
+
openblock = false
|
368
|
+
|
369
|
+
content.split(/\/\*\*/).each do |chunk|
|
370
|
+
c, b = chunk.split(/[ \t]*\*\//, 2)
|
371
|
+
next unless c || b
|
372
|
+
|
373
|
+
lineno += c.scan("\n").length if c
|
374
|
+
|
375
|
+
# special handling for /**< ... */ inline comments or
|
376
|
+
# for /** ... */ inside an open block
|
377
|
+
if openblock || c[0] == ?<
|
378
|
+
c = c.sub(/^</, '').strip
|
379
|
+
|
380
|
+
so_far = recs[-1][:body]
|
381
|
+
last_line = so_far[ so_far.rindex("\n")+1 .. -1 ].strip.chomp(",").chomp(";")
|
382
|
+
if last_line.empty? && b =~ /^([^;]+)\;/ # apply to this line instead
|
383
|
+
last_line = $1.strip.chomp(",").chomp(";")
|
384
|
+
end
|
385
|
+
|
386
|
+
if !last_line.empty?
|
387
|
+
recs[-1][:inlines] ||= []
|
388
|
+
recs[-1][:inlines] << [ last_line, c ]
|
389
|
+
if b
|
390
|
+
recs[-1][:body] += b
|
391
|
+
lineno += b.scan("\n").length
|
392
|
+
openblock = false if b =~ /\}/
|
393
|
+
end
|
394
|
+
next
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# make comment have a uniform " *" prefix if needed
|
399
|
+
if c !~ /\A[ \t]*\n/ && c =~ /^(\s*\*)/
|
400
|
+
c = $1 + c
|
401
|
+
end
|
402
|
+
|
403
|
+
# check for unterminated { brace (to handle inline comments later)
|
404
|
+
openblock = true if b =~ /\{[^\}]+\Z/
|
405
|
+
|
406
|
+
recs << {
|
407
|
+
:file => filename,
|
408
|
+
:line => lineno + (b.start_with?("\n") ? 1 : 0),
|
409
|
+
:body => b,
|
410
|
+
:rawComments => cleanup_comment(c),
|
411
|
+
}
|
412
|
+
|
413
|
+
lineno += b.scan("\n").length if b
|
414
|
+
end
|
415
|
+
|
416
|
+
# try parsers on each chunk of commented header
|
417
|
+
recs.each do |r|
|
418
|
+
r[:body].strip!
|
419
|
+
r[:rawComments].strip!
|
420
|
+
r[:lineto] = r[:line] + r[:body].scan("\n").length
|
421
|
+
parse_declaration_block(r)
|
422
|
+
end
|
423
|
+
|
424
|
+
recs
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
@@ -3,7 +3,11 @@
|
|
3
3
|
<head>
|
4
4
|
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
5
5
|
<title>{{ title }}</title>
|
6
|
-
<link rel="stylesheet" href="
|
6
|
+
<link rel="stylesheet" href="https://raw.github.com/jashkenas/docco/c640fda9b0b1d09d2b7236f655b3663dc06c63d6/resources/docco.css">
|
7
|
+
<style type="text/css">
|
8
|
+
a.fnlink {text-decoration: none}
|
9
|
+
a.fnlink:hover {text-decoration: underline}
|
10
|
+
</style>
|
7
11
|
</head>
|
8
12
|
<body>
|
9
13
|
<div id='container'>
|
data/site/js/docurium.js
CHANGED
@@ -154,9 +154,8 @@ $(function() {
|
|
154
154
|
content.append(retdiv)
|
155
155
|
|
156
156
|
// Show Non-Parsed Function Comments
|
157
|
-
if (fdata[fname]['comments'])
|
158
|
-
content.append($('<
|
159
|
-
}
|
157
|
+
if (fdata[fname]['comments'])
|
158
|
+
content.append($('<div>').append(fdata[fname]['comments']))
|
160
159
|
|
161
160
|
// Show Function Signature
|
162
161
|
ex = $('<code>').addClass('params')
|
@@ -382,7 +381,11 @@ $(function() {
|
|
382
381
|
argsText = '( ' + fdata[f]['argline'] + ' )'
|
383
382
|
link = $('<a>').attr('href', '#' + groupLink(gname, f)).append(f)
|
384
383
|
$('.content').append($('<h2>').append(link).append($('<small>').append(argsText)))
|
385
|
-
|
384
|
+
description = fdata[f]['description']
|
385
|
+
if(fdata[f]['comments'])
|
386
|
+
description += "\n\n" + fdata[f]['comments']
|
387
|
+
|
388
|
+
$('.content').append($('<div>').addClass('description').append(description))
|
386
389
|
}
|
387
390
|
return false
|
388
391
|
},
|