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.



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

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

Instance Method Details

#add_tag(tag) ⇒ Object



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

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

#at_aliasObject

matches @alias <ident-chain>



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

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 …



294
295
296
297
298
299
300
301
# File 'lib/jsduck/doc_parser.rb', line 294

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

#at_classObject

matches @class name …



229
230
231
232
233
234
# File 'lib/jsduck/doc_parser.rb', line 229

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

#at_enumObject

matches @enum type name …



335
336
337
338
339
340
341
342
# File 'lib/jsduck/doc_parser.rb', line 335

def at_enum
  match(/@enum/)
  add_tag(:class)
  @current_tag[:enum] = true
  maybe_type
  maybe_name_with_default
  skip_white
end

#at_eventObject

matches @event name …



254
255
256
257
258
259
# File 'lib/jsduck/doc_parser.rb', line 254

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

#at_extendsObject

matches @extends name …



237
238
239
240
241
242
# File 'lib/jsduck/doc_parser.rb', line 237

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

#at_inheritdocObject

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



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/jsduck/doc_parser.rb', line 406

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/)
    match(/#/)
    if look(/static-/)
      @current_tag[:static] = true
      match(/static-/)
    end
    if look(/(cfg|property|method|event|css_var|css_mixin)-/)
      @current_tag[:type] = ident.to_sym
      match(/-/)
    end
    @current_tag[:member] = ident
  end

  skip_white
end

#at_memberObject

matches @member name …



380
381
382
383
384
385
# File 'lib/jsduck/doc_parser.rb', line 380

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

#at_methodObject

matches @method name …



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

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

#at_overrideObject

matches @override name …



345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/jsduck/doc_parser.rb', line 345

def at_override
  match(/@override/)
  add_tag(:override)
  maybe_ident_chain(:class)
  skip_white

  # When @override not followed by class name, ignore the tag.
  # That's because the current ext codebase has some methods
  # tagged with @override to denote they override something.
  # But that's not what @override is meant for in JSDuck.
  unless @current_tag[:class]
    remove_last_tag
  end
end

#at_paramObject

matches @param type [name] (optional) …



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

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.



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

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

#at_returnObject

matches @return type [ return.name ] …



280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/jsduck/doc_parser.rb', line 280

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 …



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

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.



365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/jsduck/doc_parser.rb', line 365

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] = match(/\S+/)
  end
  skip_white
end

#at_varObject

matches @var type $name …



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

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



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

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



433
434
435
436
437
# File 'lib/jsduck/doc_parser.rb', line 433

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

#class_listObject

matches <ident_chain> <ident_chain> … until line end



573
574
575
576
577
578
579
580
581
# File 'lib/jsduck/doc_parser.rb', line 573

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 …



245
246
247
248
249
250
251
# File 'lib/jsduck/doc_parser.rb', line 245

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 allow balanced braces in default value. When the nested parsing doesn’t finish at closing “]”, roll back to beginning and simply grab anything up to closing “]”.



507
508
509
510
511
512
513
514
515
516
# File 'lib/jsduck/doc_parser.rb', line 507

def default_value
  start_pos = @input.pos
  value = parse_balanced(/\[/, /\]/, /[^\[\]'"]*/)
  if look(/\]/)
    value
  else
    @input.pos = start_pos
    match(/[^\]]*/)
  end
end

#identObject

matches identifier and returns its name



589
590
591
# File 'lib/jsduck/doc_parser.rb', line 589

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

#ident_chainObject

matches chained.identifier.name and returns it



584
585
586
# File 'lib/jsduck/doc_parser.rb', line 584

def ident_chain
  @input.scan(@ident_chain_pattern)
end

#indented_as_code?Boolean

Returns:

  • (Boolean)


178
179
180
# File 'lib/jsduck/doc_parser.rb', line 178

def indented_as_code?
  @current_tag[:doc] =~ /^ {4,}[^\n]*\z/
end

#look(re) ⇒ Object



593
594
595
# File 'lib/jsduck/doc_parser.rb', line 593

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

#match(re) ⇒ Object



597
598
599
# File 'lib/jsduck/doc_parser.rb', line 597

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

#maybe_ident_chain(propname) ⇒ Object

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



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

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



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

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

#maybe_name_with_defaultObject

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



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

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



471
472
473
474
475
476
477
# File 'lib/jsduck/doc_parser.rb', line 471

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

#maybe_requiredObject

matches: “(required)”



480
481
482
483
484
485
486
# File 'lib/jsduck/doc_parser.rb', line 480

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.



441
442
443
444
445
446
447
448
# File 'lib/jsduck/doc_parser.rb', line 441

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



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/jsduck/doc_parser.rb', line 203

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] = match(/.*$/).strip
    skip_white
    @current_tag = prev_tag
  end
end

#other_at_tagObject

Processes anything else beginning with @-sign.

  • When @ is not followed by any word chards, do nothing.

  • When it’s one of the meta-tags, process it as such.

  • When it’s something else, print a warning.



188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/jsduck/doc_parser.rb', line 188

def other_at_tag
  match(/@/)

  name = look(/\w+/)
  tag = @meta_tags[name]

  if tag
    meta_at_tag(tag)
  elsif name
    Logger.warn(:tag, "Unsupported tag: @#{name}", @filename, @linenr)
    @current_tag[:doc] += "@"
  end
end

#parse(input, filename = "", linenr = 0) ⇒ Object



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

def parse(input, filename="", linenr=0)
  @filename = filename
  @linenr = linenr
  @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_balanced(re_open, re_close, re_rest) ⇒ Object

Helper method to parse a string up to a closing brace, balancing opening-closing braces in between.

Parameters:

  • re_open

    The beginning brace regex

  • re_close

    The closing brace regex

  • re_rest

    Regex to match text without any braces and strings



542
543
544
545
546
547
548
549
550
551
# File 'lib/jsduck/doc_parser.rb', line 542

def parse_balanced(re_open, re_close, re_rest)
  result = parse_with_strings(re_rest)
  while look(re_open)
    result += match(re_open)
    result += parse_balanced(re_open, re_close, re_rest)
    result += match(re_close)
    result += parse_with_strings(re_rest)
  end
  result
end

#parse_loopObject



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
156
157
# File 'lib/jsduck/doc_parser.rb', line 88

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(/@enum\b/)
      at_enum
    elsif look(/@override\b/)
      at_override
    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(/@/)
      other_at_tag
    elsif look(/[^@]/)
      skip_to_next_at_tag
    end
  end
end

#parse_string(quote) ⇒ Object

Parses “…” or ‘…’ including the escape sequence ' or ‘"



566
567
568
569
570
# File 'lib/jsduck/doc_parser.rb', line 566

def parse_string(quote)
  re_quote = Regexp.new(quote)
  re_rest = Regexp.new("(?:[^"+quote+"\\\\]|\\\\.)*")
  match(re_quote) + match(re_rest) + (match(re_quote) || "")
end

#parse_with_strings(re_rest) ⇒ Object

Helper for parse_balanced to parse rest of the text between braces, taking account the strings which might occur there.



555
556
557
558
559
560
561
562
563
# File 'lib/jsduck/doc_parser.rb', line 555

def parse_with_strings(re_rest)
  result = match(re_rest)
  while look(/['"]/)
    result += parse_string('"') if look(/"/)
    result += parse_string("'") if look(/'/)
    result += match(re_rest)
  end
  result
end

#prev_char_is_whitespace?Boolean

Returns:

  • (Boolean)


174
175
176
# File 'lib/jsduck/doc_parser.rb', line 174

def prev_char_is_whitespace?
  @current_tag[:doc][-1,1] =~ /\s/
end

#purify(input) ⇒ Object

Extracts content inside /** … */



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
# File 'lib/jsduck/doc_parser.rb', line 52

def purify(input)
  result = []
  # We can have 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

#remove_last_tagObject



83
84
85
86
# File 'lib/jsduck/doc_parser.rb', line 83

def remove_last_tag
  @tags.pop
  @current_tag = @tags.last
end

#skip_horiz_whiteObject

skips horizontal whitespace (tabs and spaces)



606
607
608
# File 'lib/jsduck/doc_parser.rb', line 606

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

#skip_to_next_at_tagObject

Skips until the beginning of next @tag.

There must be space before the next @tag - this ensures that we don’t detect tags inside “[email protected]” or “@link”.

Also check that the @tag is not part of an indented code block - in which case we also ignore the tag.



166
167
168
169
170
171
172
# File 'lib/jsduck/doc_parser.rb', line 166

def skip_to_next_at_tag
  @current_tag[:doc] += match(/[^@]+/)

  while look(/@/) && (!prev_char_is_whitespace? || indented_as_code?)
    @current_tag[:doc] += match(/@+[^@]+/)
  end
end

#skip_whiteObject



601
602
603
# File 'lib/jsduck/doc_parser.rb', line 601

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

#typedefObject

matches …= and returns text inside brackets



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/jsduck/doc_parser.rb', line 519

def typedef
  match(/\{/)

  name = parse_balanced(/\{/, /\}/, /[^{}'"]*/)

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

  match(/\}/)

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