Class: JsDuck::DocParser

Inherits:
Object
  • Object
show all
Defined in:
lib/jsduck/doc_parser.rb

Overview

Parses doc-comment into array of @tags

For each @tag it produces Hash like the following:

{
  :tagname => :cfg/:property/:type/:extends/...,
  :doc => "Some documentation for this tag",
  ...@tag specific stuff like :name, :type, and so on...
}

When doc-comment begins with comment, not preceded by @tag, then the comment will be placed into Hash with :tagname => :default.

Unrecognized @tags are left as is into documentation as if they were normal text.

See Also:

  • {@link} are parsed separately in JsDuck::DocFormatter.

Instance Method Summary collapse

Constructor Details

#initializeDocParser

Returns a new instance of DocParser.



27
28
29
30
31
# File 'lib/jsduck/doc_parser.rb', line 27

def initialize
  @ident_pattern = /[$\w-]+/
  @ident_chain_pattern = /[$\w-]+(\.[$\w-]+)*/
  @meta_tags = MetaTagRegistry.instance
end

Instance Method Details

#add_tag(tag) ⇒ Object



80
81
82
# File 'lib/jsduck/doc_parser.rb', line 80

def add_tag(tag)
  @tags << @current_tag = {:tagname => tag, :doc => ""}
end

#at_aliasObject

matches @alias <ident-chain>



326
327
328
329
330
331
332
# File 'lib/jsduck/doc_parser.rb', line 326

def at_alias
  match(/@alias/)
  add_tag(:alias)
  skip_horiz_white
  @current_tag[:name] = ident_chain
  skip_white
end

#at_cfgObject

matches @cfg type name …



249
250
251
252
253
254
255
256
# File 'lib/jsduck/doc_parser.rb', line 249

def at_cfg
  match(/@cfg/)
  add_tag(:cfg)
  maybe_type
  maybe_name_with_default
  maybe_required
  skip_white
end

#at_classObject

matches @class name …



184
185
186
187
188
189
# File 'lib/jsduck/doc_parser.rb', line 184

def at_class
  match(/@class/)
  add_tag(:class)
  maybe_ident_chain(:name)
  skip_white
end

#at_eventObject

matches @event name …



209
210
211
212
213
214
# File 'lib/jsduck/doc_parser.rb', line 209

def at_event
  match(/@event/)
  add_tag(:event)
  maybe_name
  skip_white
end

#at_extendsObject

matches @extends name …



192
193
194
195
196
197
# File 'lib/jsduck/doc_parser.rb', line 192

def at_extends
  match(/@extends?/)
  add_tag(:extends)
  maybe_ident_chain(:extends)
  skip_white
end

#at_inheritdocObject

matches @inheritdoc class.name#static-type-member



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/jsduck/doc_parser.rb', line 335

def at_inheritdoc
  match(/@inherit[dD]oc|@alias/)

  add_tag(:inheritdoc)
  skip_horiz_white

  if look(@ident_chain_pattern)
    @current_tag[:cls] = ident_chain
  end

  if look(/#\w/)
    @input.scan(/#/)
    if look(/static-/)
      @current_tag[:static] = true
      @input.scan(/static-/)
    end
    if look(/(cfg|property|method|event|css_var|css_mixin)-/)
      @current_tag[:type] = ident.to_sym
      @input.scan(/-/)
    end
    @current_tag[:member] = ident
  end

  skip_white
end

#at_memberObject

matches @member name …



309
310
311
312
313
314
# File 'lib/jsduck/doc_parser.rb', line 309

def at_member
  match(/@member/)
  add_tag(:member)
  maybe_ident_chain(:member)
  skip_white
end

#at_methodObject

matches @method name …



217
218
219
220
221
222
# File 'lib/jsduck/doc_parser.rb', line 217

def at_method
  match(/@method/)
  add_tag(:method)
  maybe_name
  skip_white
end

#at_paramObject

matches @param type [name] (optional) …



225
226
227
228
229
230
231
232
# File 'lib/jsduck/doc_parser.rb', line 225

def at_param
  match(/@param/)
  add_tag(:param)
  maybe_type
  maybe_name_with_default
  maybe_optional
  skip_white
end

#at_propertyObject

matches @property type name …

ext-doc doesn’t support type and name for @property - name is inferred from source and @type is required to specify type, jsdoc-toolkit on the other hand follows the sensible route, and so do we.



264
265
266
267
268
269
270
# File 'lib/jsduck/doc_parser.rb', line 264

def at_property
  match(/@property/)
  add_tag(:property)
  maybe_type
  maybe_name_with_default
  skip_white
end

#at_returnObject

matches @return type [ return.name ] …



235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/jsduck/doc_parser.rb', line 235

def at_return
  match(/@returns?/)
  add_tag(:return)
  maybe_type
  skip_white
  if look(/return\.\w/)
    @current_tag[:name] = ident_chain
  else
    @current_tag[:name] = "return"
  end
  skip_white
end

#at_throwsObject

matches @throws type …



282
283
284
285
286
287
# File 'lib/jsduck/doc_parser.rb', line 282

def at_throws
  match(/@throws/)
  add_tag(:throws)
  maybe_type
  skip_white
end

#at_typeObject

matches @type type or @type type

The presence of @type implies that we are dealing with property. ext-doc allows type name to be either inside curly braces or without them at all.



294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/jsduck/doc_parser.rb', line 294

def at_type
  match(/@type/)
  add_tag(:type)
  skip_horiz_white
  if look(/\{/)
    tdf = typedef
    @current_tag[:type] = tdf[:type]
    @current_tag[:optional] = true if tdf[:optional]
  elsif look(/\S/)
    @current_tag[:type] = @input.scan(/\S+/)
  end
  skip_white
end

#at_varObject

matches @var type $name …



273
274
275
276
277
278
279
# File 'lib/jsduck/doc_parser.rb', line 273

def at_var
  match(/@var/)
  add_tag(:css_var)
  maybe_type
  maybe_name_with_default
  skip_white
end

#at_xtype(tag, namespace) ⇒ Object

matches @xtype/ptype/ftype/… name



317
318
319
320
321
322
323
# File 'lib/jsduck/doc_parser.rb', line 317

def at_xtype(tag, namespace)
  match(tag)
  add_tag(:alias)
  skip_horiz_white
  @current_tag[:name] = namespace + "." + (ident_chain || "")
  skip_white
end

#boolean_at_tag(regex, propname) ⇒ Object

Used to match @private, @ignore, @hide, …



362
363
364
365
366
# File 'lib/jsduck/doc_parser.rb', line 362

def boolean_at_tag(regex, propname)
  match(regex)
  add_tag(propname)
  skip_white
end

#class_listObject

matches <ident_chain> <ident_chain> … until line end



474
475
476
477
478
479
480
481
482
# File 'lib/jsduck/doc_parser.rb', line 474

def class_list
  skip_horiz_white
  classes = []
  while look(@ident_chain_pattern)
    classes << ident_chain
    skip_horiz_white
  end
  classes
end

#class_list_at_tag(regex, tagname) ⇒ Object

matches @<tagname> classname1 classname2 …



200
201
202
203
204
205
206
# File 'lib/jsduck/doc_parser.rb', line 200

def class_list_at_tag(regex, tagname)
  match(regex)
  add_tag(tagname)
  skip_horiz_white
  @current_tag[tagname] = class_list
  skip_white
end

#default_valueObject

attempts to match javascript literal, when it fails grabs anything up to closing “]”



435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/jsduck/doc_parser.rb', line 435

def default_value
  start_pos = @input.pos
  lit = JsLiteralParser.new(@input).literal
  if lit && look(/ *\]/)
    # When lital matched and there's nothing after it up to the closing "]"
    JsLiteralBuilder.new.to_s(lit)
  else
    # Otherwise reset parsing position to where we started
    # and rescan up to "]" using simple regex.
    @input.pos = start_pos
    match(/[^\]]*/)
  end
end

#identObject

matches identifier and returns its name



490
491
492
# File 'lib/jsduck/doc_parser.rb', line 490

def ident
  @input.scan(/\w+/)
end

#ident_chainObject

matches chained.identifier.name and returns it



485
486
487
# File 'lib/jsduck/doc_parser.rb', line 485

def ident_chain
  @input.scan(@ident_chain_pattern)
end

#look(re) ⇒ Object



494
495
496
# File 'lib/jsduck/doc_parser.rb', line 494

def look(re)
  @input.check(re)
end

#match(re) ⇒ Object



498
499
500
# File 'lib/jsduck/doc_parser.rb', line 498

def match(re)
  @input.scan(re)
end

#maybe_ident_chain(propname) ⇒ Object

matches ident.chain if possible and sets it on @current_tag



426
427
428
429
430
431
# File 'lib/jsduck/doc_parser.rb', line 426

def maybe_ident_chain(propname)
  skip_horiz_white
  if look(@ident_chain_pattern)
    @current_tag[propname] = ident_chain
  end
end

#maybe_nameObject

matches identifier name if possible and sets it on @current_tag



418
419
420
421
422
423
# File 'lib/jsduck/doc_parser.rb', line 418

def maybe_name
  skip_horiz_white
  if look(@ident_pattern)
    @current_tag[:name] = @input.scan(@ident_pattern)
  end
end

#maybe_name_with_defaultObject

matches: <ident-chain> | “[” <ident-chain> [ “=” <default-value> ] “]”



380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/jsduck/doc_parser.rb', line 380

def maybe_name_with_default
  skip_horiz_white
  if look(/\[/)
    match(/\[/)
    maybe_ident_chain(:name)
    skip_horiz_white
    if look(/=/)
      match(/=/)
      skip_horiz_white
      @current_tag[:default] = default_value
    end
    skip_horiz_white
    match(/\]/)
    @current_tag[:optional] = true
  else
    maybe_ident_chain(:name)
  end
end

#maybe_optionalObject

matches: “(optional)”



400
401
402
403
404
405
406
# File 'lib/jsduck/doc_parser.rb', line 400

def maybe_optional
  skip_horiz_white
  if look(/\(optional\)/i)
    match(/\(optional\)/i)
    @current_tag[:optional] = true
  end
end

#maybe_requiredObject

matches: “(required)”



409
410
411
412
413
414
415
# File 'lib/jsduck/doc_parser.rb', line 409

def maybe_required
  skip_horiz_white
  if look(/\(required\)/i)
    match(/\(required\)/i)
    @current_tag[:optional] = false
  end
end

#maybe_typeObject

matches type if possible and sets it on @current_tag Also checks for optionality= in type definition.



370
371
372
373
374
375
376
377
# File 'lib/jsduck/doc_parser.rb', line 370

def maybe_type
  skip_horiz_white
  if look(/\{/)
    tdf = typedef
    @current_tag[:type] = tdf[:type]
    @current_tag[:optional] = true if tdf[:optional]
  end
end

#meta_at_tag(tag) ⇒ Object

Matches the given meta-tag



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/jsduck/doc_parser.rb', line 158

def meta_at_tag(tag)
  prev_tag = @current_tag

  add_tag(:meta)
  @current_tag[:name] = tag.key
  match(/\w+/)
  skip_horiz_white

  if tag.boolean
    # For boolean tags, only scan the tag name and switch context
    # back to previous tag.
    skip_white
    @current_tag = prev_tag
  elsif tag.multiline
    # For multiline tags we leave the tag open for :doc addition
    # just like with built-in multiline tags.
  else
    # Fors singleline tags, scan to the end of line and finish the
    # tag.
    @current_tag[:doc] = @input.scan(/.*$/).strip
    skip_white
    @current_tag = prev_tag
  end
end

#parse(input) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/jsduck/doc_parser.rb', line 33

def parse(input)
  @tags = []
  @input = StringScanner.new(purify(input))
  parse_loop
  # The parsing process can leave whitespace at the ends of
  # doc-strings, here we get rid of it.  Additionally null all empty docs
  @tags.each do |tag|
    tag[:doc].strip!
    tag[:doc] = nil if tag[:doc] == ""
  end
  # Get rid of empty default tag
  if @tags.first && @tags.first[:tagname] == :default && !@tags.first[:doc]
    @tags.shift
  end
  @tags
end

#parse_loopObject



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/jsduck/doc_parser.rb', line 84

def parse_loop
  add_tag(:default)
  while !@input.eos? do
    if look(/@class\b/)
      at_class
    elsif look(/@extends?\b/)
      at_extends
    elsif look(/@mixins?\b/)
      class_list_at_tag(/@mixins?/, :mixins)
    elsif look(/@alternateClassNames?\b/)
      class_list_at_tag(/@alternateClassNames?/, :alternateClassNames)
    elsif look(/@uses\b/)
      class_list_at_tag(/@uses/, :uses)
    elsif look(/@requires\b/)
      class_list_at_tag(/@requires/, :requires)
    elsif look(/@singleton\b/)
      boolean_at_tag(/@singleton/, :singleton)
    elsif look(/@event\b/)
      at_event
    elsif look(/@method\b/)
      at_method
    elsif look(/@constructor\b/)
      boolean_at_tag(/@constructor/, :constructor)
    elsif look(/@param\b/)
      at_param
    elsif look(/@returns?\b/)
      at_return
    elsif look(/@cfg\b/)
      at_cfg
    elsif look(/@property\b/)
      at_property
    elsif look(/@type\b/)
      at_type
    elsif look(/@xtype\b/)
      at_xtype(/@xtype/, "widget")
    elsif look(/@ftype\b/)
      at_xtype(/@ftype/, "feature")
    elsif look(/@ptype\b/)
      at_xtype(/@ptype/, "plugin")
    elsif look(/@member\b/)
      at_member
    elsif look(/@inherit[dD]oc\b/)
      at_inheritdoc
    elsif look(/@alias\s+([\w.]+)?#\w+/)
      # For backwards compatibility.
      # @alias tag was used as @inheritdoc before
      at_inheritdoc
    elsif look(/@alias/)
      at_alias
    elsif look(/@var\b/)
      at_var
    elsif look(/@throws\b/)
      at_throws
    elsif look(/@inheritable\b/)
      boolean_at_tag(/@inheritable/, :inheritable)
    elsif look(/@accessor\b/)
      boolean_at_tag(/@accessor/, :accessor)
    elsif look(/@evented\b/)
      boolean_at_tag(/@evented/, :evented)
    elsif look(/@/)
      @input.scan(/@/)
      tag = @meta_tags[look(/\w+/)]
      if tag
        meta_at_tag(tag)
      else
        @current_tag[:doc] += "@"
      end
    elsif look(/[^@]/)
      @current_tag[:doc] += @input.scan(/[^@]+/)
    end
  end
end

#purify(input) ⇒ Object

Extracts content inside /** … */



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/jsduck/doc_parser.rb', line 51

def purify(input)
  result = []
  # Remove the beginning /** and end */
  input = input.sub(/\A\/\*\* ?/, "").sub(/ ?\*\/\Z/, "")
  # Now we are left with only two types of lines:
  # - those beginning with *
  # - and those without it
  indent = nil
  input.each_line do |line|
    line.chomp!
    if line =~ /\A\s*\*\s?(.*)\Z/
      # When comment contains *-lines, switch indent-trimming off
      indent = 0
      result << $1
    elsif line =~ /\A\s*\Z/
      # pass-through empty lines
      result << line
    elsif indent == nil && line =~ /\A(\s*)(.*?\Z)/
      # When indent not measured, measure it and remember
      indent = $1.length
      result << $2
    else
      # Trim away indent if available
      result << line.sub(/\A\s{0,#{indent||0}}/, "")
    end
  end
  return result.join("\n")
end

#skip_horiz_whiteObject

skips horizontal whitespace (tabs and spaces)



507
508
509
# File 'lib/jsduck/doc_parser.rb', line 507

def skip_horiz_white
  @input.scan(/[ \t]+/)
end

#skip_whiteObject



502
503
504
# File 'lib/jsduck/doc_parser.rb', line 502

def skip_white
  @input.scan(/\s+/)
end

#typedefObject

matches …= and returns text inside brackets



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/jsduck/doc_parser.rb', line 450

def typedef
  match(/\{/)
  name = @input.scan(/[^{}]*/)

  # Type definition can contain nested braces: {{foo:Number}}
  # In such case we parse the definition so that the braces are balanced.
  while @input.check(/[{]/)
    name += "{" + typedef[:type] +"}"
    name += @input.scan(/[^{}]*/)
  end

  if name =~ /=$/
    name = name.chop
    optional = true
  else
    optional = nil
  end

  match(/\}/)

  return {:type => name, :optional => optional}
end