tomparse 0.3.0 → 0.4.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/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