tomparse 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tomparse.yml ADDED
@@ -0,0 +1,68 @@
1
+ ---
2
+ revision: 2013
3
+ type: ruby
4
+ sources:
5
+ - var
6
+ - VERSION
7
+ authors:
8
+ - name: trans
9
+ email: transfire@gmail.com
10
+ organizations:
11
+ - name: Rubyworks
12
+ website: http://rubyworks.github.com
13
+ requirements:
14
+ - groups:
15
+ - test
16
+ development: true
17
+ name: citron
18
+ - groups:
19
+ - test
20
+ development: true
21
+ name: ae
22
+ - groups:
23
+ - build
24
+ development: true
25
+ name: detroit
26
+ conflicts: []
27
+ alternatives: []
28
+ resources:
29
+ - type: home
30
+ uri: http://rubyworks.github.com/tomparse
31
+ label: Homepage
32
+ - type: code
33
+ uri: http://github.com/rubyworks/tomparse
34
+ label: Source Code
35
+ - type: api
36
+ uri: http://rubydoc.info/gems/tomparse/frames
37
+ label: API Guide
38
+ - type: mail
39
+ uri: http://groups.google.com/group/rubyworks-mailinglist
40
+ label: Mailing List
41
+ - type: chat
42
+ uri: http://chat.us.freenode.net/rubyworks
43
+ label: IRC Channel
44
+ repositories:
45
+ - name: upstream
46
+ scm: git
47
+ uri: http://github.com/rubyworks/tomparse/tomparse.git
48
+ categories:
49
+ - documentation
50
+ copyrights:
51
+ - holder: Rubyworks
52
+ year: '2012'
53
+ license: BSD-2-Clause
54
+ customs: []
55
+ paths:
56
+ lib:
57
+ - lib
58
+ created: '2012-03-04'
59
+ summary: TomDoc parser for Ruby
60
+ title: TomParse
61
+ name: tomparse
62
+ description: ! 'TomParse provides no other functionality than to take a code comment
63
+
64
+ and parse it in to a convenient object-oriented structure in accordance
65
+
66
+ with TomDoc standard.'
67
+ version: 0.4.0
68
+ date: '2013-02-11'
@@ -0,0 +1,69 @@
1
+ module TomParse
2
+
3
+ # Encapsulate a method argument.
4
+ #
5
+ class Argument
6
+
7
+ attr_accessor :name
8
+
9
+ attr_accessor :description
10
+
11
+ attr_accessor :options
12
+
13
+ # Create new Argument object.
14
+ #
15
+ # name - name of argument
16
+ # description - argument description
17
+ #
18
+ def initialize(name, description = '')
19
+ @name = name.to_s.intern
20
+ parse(description)
21
+ end
22
+
23
+ # Is this an optional argument?
24
+ #
25
+ # Returns Boolean.
26
+ def optional?
27
+ @description.downcase.include? 'optional'
28
+ end
29
+
30
+ # Parse arguments section. Arguments occur subsequent to
31
+ # the description.
32
+ #
33
+ # section - String containing argument definitions.
34
+ #
35
+ # Returns nothing.
36
+ def parse(description)
37
+ desc = []
38
+ opts = []
39
+
40
+ lines = description.lines.to_a
41
+
42
+ until lines.empty? or /^\s+\:(\w+)\s+-\s+(.*?)$/ =~ lines.first
43
+ desc << lines.shift.chomp.squeeze(" ")
44
+ end
45
+
46
+ opts = []
47
+ last_indent = nil
48
+
49
+ lines.each do |line|
50
+ next if line.strip.empty?
51
+ indent = line.scan(/^\s*/)[0].to_s.size
52
+
53
+ if last_indent && indent > last_indent
54
+ opts.last.description << line.squeeze(" ")
55
+ else
56
+ param, d = line.split(" - ")
57
+ opts << Option.new(param.strip, d.strip) if param && d
58
+ end
59
+
60
+ last_indent = indent
61
+ end
62
+
63
+ @description = desc.join
64
+ @options = opts
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,30 @@
1
+ module TomParse
2
+
3
+ # Encapsulate a named parameter.
4
+ #
5
+ class Option
6
+
7
+ attr_accessor :name
8
+
9
+ attr_accessor :description
10
+
11
+ # Create new Argument object.
12
+ #
13
+ # name - name of option
14
+ # description - option description
15
+ #
16
+ def initialize(name, description = '')
17
+ @name = name.to_s.intern
18
+ @description = description
19
+ end
20
+
21
+ # Is this a required option?
22
+ #
23
+ # Returns Boolean.
24
+ def required?
25
+ @description.downcase.include? 'required'
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,30 @@
1
+ module TomParse
2
+
3
+ # Raised when comment can't be parsed, which means it's most
4
+ # likely not valid TomDoc.
5
+ #
6
+ class ParseError < RuntimeError
7
+ # Create new ParseError object.
8
+ #
9
+ # doc - document string
10
+ #
11
+ def initialize(doc)
12
+ @doc = doc
13
+ end
14
+
15
+ # Provide access to document string.
16
+ #
17
+ # Returns String.
18
+ def message
19
+ @doc
20
+ end
21
+
22
+ # Provide access to document string.
23
+ #
24
+ # Returns String.
25
+ def to_s
26
+ @doc
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,699 @@
1
+ module TomParse
2
+
3
+ # Encapsulate parsed tomdoc documentation.
4
+ #
5
+ # TODO: Currently uses lazy evaluation, eventually this should
6
+ # be removed and simply parsed all at once.
7
+ #
8
+ class Parser
9
+
10
+ #
11
+ attr_accessor :raw
12
+
13
+ # Public: Initialize a TomDoc object.
14
+ #
15
+ # text - The raw text of a method or class/module comment.
16
+ #
17
+ # Returns new TomDoc instance.
18
+ def initialize(text, parse_options={})
19
+ @raw = text.to_s.strip
20
+
21
+ @arguments = []
22
+ @options = []
23
+ @examples = []
24
+ @returns = []
25
+ @raises = []
26
+ @signatures = []
27
+ @signature_fields = []
28
+ @tags = []
29
+
30
+ #parse unless @raw.empty?
31
+ end
32
+
33
+ # Raw documentation text.
34
+ #
35
+ # Returns String of raw documentation text.
36
+ def to_s
37
+ @raw
38
+ end
39
+
40
+ # Validate given comment text.
41
+ #
42
+ # Returns true if comment is valid, otherwise false.
43
+ def self.valid?(text)
44
+ new(text).valid?
45
+ end
46
+
47
+ # Validate raw comment.
48
+ #
49
+ # TODO: This needs improvement.
50
+ #
51
+ # Returns true if comment is valid, otherwise false.
52
+ def valid?
53
+ begin
54
+ new(text).validate
55
+ true
56
+ rescue ParseError
57
+ false
58
+ end
59
+ end
60
+
61
+ # Validate raw comment.
62
+ #
63
+ # Returns true if comment is valid.
64
+ # Raises ParseError if comment is not valid.
65
+ def validate
66
+ if !raw.include?('Returns')
67
+ raise ParseError.new("No `Returns' statement.")
68
+ end
69
+
70
+ if sections.size < 2
71
+ raise ParseError.new("No description section found.")
72
+ end
73
+
74
+ true
75
+ end
76
+
77
+ # The raw comment text cleaned-up and ready for section parsing.
78
+ #
79
+ # Returns cleaned-up comment String.
80
+ def tomdoc
81
+ lines = raw.split("\n")
82
+
83
+ # remove remark symbol
84
+ if lines.all?{ |line| /^\s*#/ =~ line }
85
+ lines = lines.map do |line|
86
+ line =~ /^(\s*#)/ ? line.sub($1, '') : nil
87
+ end
88
+ end
89
+
90
+ # for some reason the first line is coming in without indention
91
+ # regardless, so we temporary remove it
92
+ first = lines.shift
93
+
94
+ # remove indention
95
+ spaces = lines.map do |line|
96
+ next if line.strip.empty?
97
+ md = /^(\s*)/.match(line)
98
+ md ? md[1].size : nil
99
+ end.compact
100
+
101
+ space = spaces.min || 0
102
+ lines = lines.map do |line|
103
+ if line.strip.empty?
104
+ line.strip
105
+ else
106
+ line[space..-1]
107
+ end
108
+ end
109
+
110
+ # put first line back
111
+ lines.unshift(first.sub(/^\s*/,'')) if first
112
+
113
+ lines.compact.join("\n")
114
+ end
115
+
116
+ # List of comment sections. These are divided simply on "\n\n".
117
+ #
118
+ # Returns Array of comment sections.
119
+ def sections
120
+ parsed {
121
+ @sections
122
+ }
123
+ end
124
+
125
+ # Description of method or class/module.
126
+ #
127
+ # Returns description String.
128
+ def description
129
+ parsed {
130
+ @description
131
+ }
132
+ end
133
+
134
+ # Arguments list.
135
+ #
136
+ # Returns list of arguments.
137
+ def arguments
138
+ parsed {
139
+ @arguments
140
+ }
141
+ end
142
+ alias args arguments
143
+
144
+ # Keyword arguments, aka Options.
145
+ #
146
+ # Returns list of options.
147
+ def options
148
+ parsed {
149
+ @options
150
+ }
151
+ end
152
+ alias keyword_arguments options
153
+
154
+ # List of use examples of a method or class/module.
155
+ #
156
+ # Returns String of examples.
157
+ def examples
158
+ parsed {
159
+ @examples
160
+ }
161
+ end
162
+
163
+ # Description of a methods yield procedure.
164
+ #
165
+ # Returns String decription of yield procedure.
166
+ def yields
167
+ parsed {
168
+ @yields
169
+ }
170
+ end
171
+
172
+ # The list of retrun values a method can return.
173
+ #
174
+ # Returns Array of method return descriptions.
175
+ def returns
176
+ parsed {
177
+ @returns
178
+ }
179
+ end
180
+
181
+ # A list of errors a method might raise.
182
+ #
183
+ # Returns Array of method raises descriptions.
184
+ def raises
185
+ parsed {
186
+ @raises
187
+ }
188
+ end
189
+
190
+ # A list of alternate method signatures.
191
+ #
192
+ # Returns Array of signatures.
193
+ def signatures
194
+ parsed {
195
+ @signatures
196
+ }
197
+ end
198
+
199
+ # A list of signature fields.
200
+ #
201
+ # Returns Array of field definitions.
202
+ def signature_fields
203
+ parsed {
204
+ @signature_fields
205
+ }
206
+ end
207
+
208
+ # List of tags.
209
+ #
210
+ # Returns an associatve array of tags. [Array<Array<String>>]
211
+ def tags
212
+ parsed {
213
+ @tags
214
+ }
215
+ end
216
+
217
+ # Method status, can be `Public`, `Internal` or `Deprecated`.
218
+ #
219
+ # Returns [String]
220
+ def status
221
+ parsed {
222
+ @status
223
+ }
224
+ end
225
+
226
+ # Check if method is public.
227
+ #
228
+ # Returns true if method is public.
229
+ def public?
230
+ parsed {
231
+ @status == 'Public'
232
+ }
233
+ end
234
+
235
+ # Check if method is internal.
236
+ #
237
+ # Returns true if method is internal.
238
+ def internal?
239
+ parsed {
240
+ @status == 'Internal'
241
+ }
242
+ end
243
+
244
+ # Check if method is deprecated.
245
+ #
246
+ # Returns true if method is deprecated.
247
+ def deprecated?
248
+ parsed {
249
+ @status == 'Deprecated'
250
+ }
251
+ end
252
+
253
+ =begin
254
+ # Internal: Parse the Tomdoc formatted comment.
255
+ #
256
+ # Returns true if there was a comment to parse.
257
+ def parse
258
+ @parsed = true
259
+
260
+ sections = tomdoc.split("\n\n")
261
+
262
+ return false if sections.empty?
263
+
264
+ # The description is always the first section, but it may have
265
+ # multiple paragraphs. This routine collects those together.
266
+ desc = [sections.shift]
267
+ loop do
268
+ s = sections.first
269
+ break if s.nil? # got nothing
270
+ break if s =~ /^\w+\s+\-/m # argument line
271
+ break if section_type(s) != nil # another section type
272
+ desc << sections.shift
273
+ end
274
+ sections = [desc.join("\n\n")] + sections
275
+
276
+ @sections = sections.dup
277
+
278
+ parse_description(sections.shift)
279
+
280
+ if sections.first && sections.first =~ /^\w+\s+\-/m
281
+ parse_arguments(sections.shift)
282
+ end
283
+
284
+ current = sections.shift
285
+ while current
286
+ case type = section_type(current)
287
+ #when :arguments
288
+ # parse_arguments(current)
289
+ #when :options
290
+ # parse_options(current)
291
+ when :examples
292
+ parse_examples(current, sections)
293
+ when :yields
294
+ parse_yields(current)
295
+ when :returns
296
+ parse_returns(current) # also does raises
297
+ when :raises
298
+ parse_returns(current) # also does returns
299
+ when :signature
300
+ parse_signature(current, sections)
301
+ when Symbol
302
+ parse_tag(current)
303
+ end
304
+ current = sections.shift
305
+ end
306
+
307
+ return @parsed
308
+ end
309
+ =end
310
+
311
+ # Internal: Parse the Tomdoc formatted comment.
312
+ #
313
+ # Returns true if there was a comment to parse.
314
+ def parse
315
+ @parsed = true
316
+
317
+ sections = smart_split(tomdoc)
318
+
319
+ return false if sections.empty?
320
+
321
+ # We are assuming that the first section is always description.
322
+ # And it should be, but people aren't always proper, so perhaps
323
+ # this can be made a little smarter in the future.
324
+ parse_description(sections.shift)
325
+
326
+ # The second section may be arguments.
327
+ if sections.first && sections.first =~ /^\w+\s+\-/m
328
+ parse_arguments(sections.shift)
329
+ end
330
+
331
+ current = sections.shift
332
+ while current
333
+ case type = section_type(current)
334
+ when :arguments
335
+ parse_arguments(current)
336
+ when :options
337
+ parse_options(current)
338
+ when :example
339
+ parse_example(current)
340
+ when :examples
341
+ parse_examples(current)
342
+ when :yields
343
+ parse_yields(current)
344
+ when :returns
345
+ parse_returns(current)
346
+ when :raises
347
+ parse_raises(current)
348
+ when :signature
349
+ parse_signature(current)
350
+ when Symbol
351
+ parse_tag(current)
352
+ end
353
+ current = sections.shift
354
+ end
355
+
356
+ return @parsed
357
+ end
358
+
359
+ private
360
+
361
+ # Has the comment been parsed yet?
362
+ def parsed(&block)
363
+ parse unless @parsed
364
+ block.call
365
+ end
366
+
367
+ # Split the documentation up into proper sections.
368
+ #
369
+ # Returns an array section strings. [Array<String>]
370
+ def smart_split(doc)
371
+ splits = []
372
+ index = -1
373
+
374
+ lines = doc.lines.to_a
375
+
376
+ # Remove any blank lines off the top.
377
+ lines.shift while lines.first && lines.first.strip.empty?
378
+
379
+ # Keep a copy of the lines for later use.
380
+ doc_lines = lines.dup
381
+
382
+ # The first line may have a `Public`/`Private`/`Deprecated` marker.
383
+ # We need to remove that for the moment to properly check for
384
+ # subsequent labeled sections.
385
+ #first = lines.first.dup
386
+ #lines.first.sub(/^[A-Z]\w+\:\s*/, '')
387
+
388
+ # The description is always the first section, but it may have
389
+ # multiple paragraphs. And the second section may be an arguments
390
+ # list without a header. This loop handles that.
391
+ while line = lines.shift
392
+ index += 1
393
+ if argument_line?(line)
394
+ splits << index
395
+ break
396
+ elsif section_type(line)
397
+ splits << index
398
+ break
399
+ end
400
+ end
401
+
402
+ # The rest of the the document should have identifiable section markers.
403
+ while line = lines.shift
404
+ index += 1
405
+ if section_type(line)
406
+ splits << index
407
+ end
408
+ end
409
+
410
+ # Now we split the documentation up into sections using
411
+ # the line indexes we collected above.
412
+ sections = []
413
+ b = 0
414
+ splits.shift if splits.first == 0
415
+ splits.each do |i|
416
+ sections << doc_lines[b...i].join
417
+ b = i
418
+ end
419
+ sections << doc_lines[b..-1].join
420
+
421
+ return sections
422
+ end
423
+
424
+ # Check if a line of text could be an argument definition.
425
+ # I.e. it has a word followed by a dash.
426
+ #
427
+ # Return [Boolean]
428
+ def argument_line?(line)
429
+ /^\w+\s+\-/m =~ line.strip
430
+ end
431
+
432
+ # Determine section type.
433
+ def section_type(section)
434
+ case section
435
+ when /\AArguments\s*$/
436
+ :arguments
437
+ when /\AOptions\s*$/
438
+ :options
439
+ when /\AExamples\s*$/
440
+ :examples
441
+ when /\AExample\s*$/
442
+ :example
443
+ when /\ASignature(s)?\s*$/
444
+ :signature
445
+ when /^Yield(s)?/
446
+ :yields
447
+ when /^Return(s)?/
448
+ :returns
449
+ when /^Raise(s)?/
450
+ :raises
451
+ when /\A([A-Z]\w+)\:\ /
452
+ $1.to_sym
453
+ else
454
+ nil
455
+ end
456
+ end
457
+
458
+ # Recognized description status.
459
+ TOMDOC_STATUS = ['Internal', 'Public', 'Deprecated']
460
+
461
+ # Parse description.
462
+ #
463
+ # section - String containig description.
464
+ #
465
+ # Returns nothing.
466
+ def parse_description(section)
467
+ if md = /^([A-Z]\w+\:)/.match(section)
468
+ @status = md[1].chomp(':')
469
+ if TOMDOC_STATUS.include?(@status)
470
+ @description = md.post_match.strip
471
+ else
472
+ @description = section.strip
473
+ end
474
+ else
475
+ @description = section.strip
476
+ end
477
+ end
478
+
479
+ # Parse arguments section. Arguments occur subsequent to
480
+ # the description.
481
+ #
482
+ # section - String containing argument definitions.
483
+ #
484
+ # Returns nothing.
485
+ def parse_arguments(section)
486
+ args = []
487
+ last_indent = nil
488
+
489
+ section.lines.each do |line|
490
+ next if /^Arguments\s*$/i =~ line # optional header
491
+ next if line.strip.empty?
492
+ indent = line.scan(/^\s*/)[0].to_s.size
493
+
494
+ if last_indent && indent >= last_indent
495
+ args.last.description << "\r\n" + line
496
+ else
497
+ param, desc = line.split(" - ")
498
+ args << Argument.new(param.strip, desc.to_s.strip) if param #&& desc
499
+ last_indent = indent + 1
500
+ end
501
+ end
502
+
503
+ args.each do |arg|
504
+ arg.parse(arg.description)
505
+ end
506
+
507
+ @arguments = args
508
+ end
509
+
510
+ # the description.
511
+ #
512
+ # section - String containing argument definitions.
513
+ #
514
+ # Returns nothing.
515
+ def parse_options(section)
516
+ opts = []
517
+ last_indent = nil
518
+
519
+ section.lines.each do |line|
520
+ next if /^\s*Options\s*$/i =~ line # optional header
521
+ next if line.strip.empty?
522
+ indent = line.scan(/^\s*/)[0].to_s.size
523
+
524
+ if last_indent && indent > 0 && indent >= last_indent
525
+ opts.last.description << "\r\n" + line
526
+ else
527
+ param, desc = line.split(" - ")
528
+ opts << Option.new(param.strip, desc.strip) if param && desc
529
+ end
530
+
531
+ last_indent = indent
532
+ end
533
+
534
+ #opts.each do |opt|
535
+ # opt.parse(arg.description)
536
+ #end
537
+
538
+ @options = opts
539
+ end
540
+
541
+ # Parse example.
542
+ #
543
+ # section - String starting with `Examples`.
544
+ # sections - All sections subsequent to section.
545
+ #
546
+ # Returns nothing.
547
+ def parse_example(section)
548
+ examples = []
549
+
550
+ # TODO: make the unidention smarter (could be more than 2 spaces)
551
+ section = section.sub('Example', '').gsub(/^\s{2}/,'')
552
+
553
+ @examples << section unless section.strip.empty?
554
+ end
555
+
556
+ # Parse examples.
557
+ #
558
+ # section - String starting with `Examples`.
559
+ # sections - All sections subsequent to section.
560
+ #
561
+ # Returns nothing.
562
+ def parse_examples(section)
563
+ #examples = []
564
+
565
+ # TODO: make the unidention smarter (could be more than 2 spaces)
566
+ section = section.sub('Examples', '')
567
+
568
+ section.split("\n\n").each do |ex|
569
+ @examples << ex.gsub(/^\s{2}/,'') unless ex.strip.empty?
570
+ end
571
+ end
572
+
573
+ # Parse yields section.
574
+ #
575
+ # section - String contaning Yields line.
576
+ #
577
+ # Returns nothing.
578
+ def parse_yields(section)
579
+ @yields = section.strip
580
+ end
581
+
582
+ # Parse returns section.
583
+ #
584
+ # section - String contaning Returns and/or Raises lines.
585
+ #
586
+ # Returns nothing.
587
+ def parse_returns(section)
588
+ text = section.gsub(/\s+/, ' ').strip
589
+ @returns << text
590
+
591
+ #returns, raises, current = [], [], []
592
+ #
593
+ #lines = section.split("\n")
594
+ #lines.each do |line|
595
+ # case line
596
+ # when /^Returns/
597
+ # returns << line
598
+ # current = returns
599
+ # when /^Raises/
600
+ # raises << line
601
+ # current = raises
602
+ # when /^\s+/
603
+ # current.last << line.squeeze(' ')
604
+ # else
605
+ # current << line # TODO: What to do with non-compliant line?
606
+ # end
607
+ #end
608
+ #
609
+ #@returns.concat(returns)
610
+ #@raises.concat(raises)
611
+ end
612
+
613
+ # Parse raises section.
614
+ #
615
+ # section - String contaning Raises text.
616
+ #
617
+ # Returns nothing.
618
+ def parse_raises(section)
619
+ text = section.gsub(/\s+/, ' ').strip
620
+ @raises << text.strip
621
+ end
622
+
623
+ # Parse signature section.
624
+ #
625
+ # IMPORTANT! This is not mojombo TomDoc! Rather signatures are simply
626
+ # a list of alternate ways to call a method, e.g. when *args is used but
627
+ # only specific argument patterns are possible.
628
+ #
629
+ # section - String starting with `Signature`.
630
+ #
631
+ # Returns nothing.
632
+ def parse_signature(section)
633
+ signatures = []
634
+
635
+ section = section.sub(/^\s*Signature(s)?/, '').strip
636
+
637
+ lines = section.lines.to_a
638
+
639
+ lines.each do |line|
640
+ next if line.strip.empty?
641
+ signatures << line.strip
642
+ end
643
+
644
+ @signatures = signatures
645
+
646
+ #if line =~ /^\w+\s*\-/m
647
+ # parse_signature_fields(sections.shift)
648
+ #end
649
+ end
650
+
651
+ =begin
652
+ # Subsequent to Signature section there can be field
653
+ # definitions.
654
+ #
655
+ # section - String subsequent to signatures.
656
+ #
657
+ # Returns nothing.
658
+ def parse_signature_fields(section)
659
+ args = []
660
+ last_indent = nil
661
+
662
+ section.split("\n").each do |line|
663
+ next if line.strip.empty?
664
+ indent = line.scan(/^\s*/)[0].to_s.size
665
+
666
+ if last_indent && indent > last_indent
667
+ args.last.description << line.squeeze(" ")
668
+ else
669
+ param, desc = line.split(" - ")
670
+ args << Argument.new(param.strip, desc.strip) if param && desc
671
+ end
672
+
673
+ last_indent = indent
674
+ end
675
+
676
+ @signature_fields = args
677
+ end
678
+ =end
679
+
680
+ # Tags are arbitrary sections designated by a capitalized label and a colon.
681
+ #
682
+ # label - String name of the tag.
683
+ # section - String of the tag section.
684
+ #
685
+ # Returns nothing.
686
+ def parse_tag(section)
687
+ md = /^([A-Z]\w+)\:\ /m.match(section)
688
+
689
+ label = md[1]
690
+ desc = md.post_match
691
+
692
+ warn "No label?" unless label
693
+
694
+ @tags << [label, desc.strip] if label
695
+ end
696
+
697
+ end
698
+
699
+ end