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>



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

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 …



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

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

#at_classObject

matches @class name …



182
183
184
185
186
187
# File 'lib/jsduck/doc_parser.rb', line 182

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

#at_eventObject

matches @event name …



207
208
209
210
211
212
# File 'lib/jsduck/doc_parser.rb', line 207

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

#at_extendsObject

matches @extends name …



190
191
192
193
194
195
# File 'lib/jsduck/doc_parser.rb', line 190

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

#at_inheritdocObject

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



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/jsduck/doc_parser.rb', line 325

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 …



299
300
301
302
303
304
# File 'lib/jsduck/doc_parser.rb', line 299

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

#at_methodObject

matches @method name …



215
216
217
218
219
220
# File 'lib/jsduck/doc_parser.rb', line 215

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

#at_paramObject

matches @param type [name] (optional) …



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

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.



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

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

#at_returnObject

matches @return type [ return.name ] …



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

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_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.



284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/jsduck/doc_parser.rb', line 284

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 …



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

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

#at_xtype(tag, namespace) ⇒ Object

matches @xtype/ptype/ftype/… name



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

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, …



352
353
354
355
356
# File 'lib/jsduck/doc_parser.rb', line 352

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

#class_listObject

matches <ident_chain> <ident_chain> … until line end



464
465
466
467
468
469
470
471
472
# File 'lib/jsduck/doc_parser.rb', line 464

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 …



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

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 “]”



425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/jsduck/doc_parser.rb', line 425

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



480
481
482
# File 'lib/jsduck/doc_parser.rb', line 480

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

#ident_chainObject

matches chained.identifier.name and returns it



475
476
477
# File 'lib/jsduck/doc_parser.rb', line 475

def ident_chain
  @input.scan(@ident_chain_pattern)
end

#look(re) ⇒ Object



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

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

#match(re) ⇒ Object



488
489
490
# File 'lib/jsduck/doc_parser.rb', line 488

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

#maybe_ident_chain(propname) ⇒ Object

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



416
417
418
419
420
421
# File 'lib/jsduck/doc_parser.rb', line 416

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



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

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> ] “]”



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/jsduck/doc_parser.rb', line 370

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)”



390
391
392
393
394
395
396
# File 'lib/jsduck/doc_parser.rb', line 390

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

#maybe_requiredObject

matches: “(required)”



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

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.



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

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



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

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
# 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(/@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)



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

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

#skip_whiteObject



492
493
494
# File 'lib/jsduck/doc_parser.rb', line 492

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

#typedefObject

matches …= and returns text inside brackets



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/jsduck/doc_parser.rb', line 440

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