jsduck 3.11.2 → 4.0.beta

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.
@@ -1,6 +1,4 @@
1
1
  require 'strscan'
2
- require 'jsduck/js_literal_parser'
3
- require 'jsduck/js_literal_builder'
4
2
  require 'jsduck/meta_tag_registry'
5
3
 
6
4
  module JsDuck
@@ -50,9 +48,7 @@ module JsDuck
50
48
  # Extracts content inside /** ... */
51
49
  def purify(input)
52
50
  result = []
53
- # Remove the beginning /** and end */
54
- input = input.sub(/\A\/\*\* ?/, "").sub(/ ?\*\/\Z/, "")
55
- # Now we are left with only two types of lines:
51
+ # We can have two types of lines:
56
52
  # - those beginning with *
57
53
  # - and those without it
58
54
  indent = nil
@@ -141,7 +137,7 @@ module JsDuck
141
137
  elsif look(/@evented\b/)
142
138
  boolean_at_tag(/@evented/, :evented)
143
139
  elsif look(/@/)
144
- @input.scan(/@/)
140
+ match(/@/)
145
141
  tag = @meta_tags[look(/\w+/)]
146
142
  if tag
147
143
  meta_at_tag(tag)
@@ -149,7 +145,7 @@ module JsDuck
149
145
  @current_tag[:doc] += "@"
150
146
  end
151
147
  elsif look(/[^@]/)
152
- @current_tag[:doc] += @input.scan(/[^@]+/)
148
+ @current_tag[:doc] += match(/[^@]+/)
153
149
  end
154
150
  end
155
151
  end
@@ -174,7 +170,7 @@ module JsDuck
174
170
  else
175
171
  # Fors singleline tags, scan to the end of line and finish the
176
172
  # tag.
177
- @current_tag[:doc] = @input.scan(/.*$/).strip
173
+ @current_tag[:doc] = match(/.*$/).strip
178
174
  skip_white
179
175
  @current_tag = prev_tag
180
176
  end
@@ -300,7 +296,7 @@ module JsDuck
300
296
  @current_tag[:type] = tdf[:type]
301
297
  @current_tag[:optional] = true if tdf[:optional]
302
298
  elsif look(/\S/)
303
- @current_tag[:type] = @input.scan(/\S+/)
299
+ @current_tag[:type] = match(/\S+/)
304
300
  end
305
301
  skip_white
306
302
  end
@@ -343,14 +339,14 @@ module JsDuck
343
339
  end
344
340
 
345
341
  if look(/#\w/)
346
- @input.scan(/#/)
342
+ match(/#/)
347
343
  if look(/static-/)
348
344
  @current_tag[:static] = true
349
- @input.scan(/static-/)
345
+ match(/static-/)
350
346
  end
351
347
  if look(/(cfg|property|method|event|css_var|css_mixin)-/)
352
348
  @current_tag[:type] = ident.to_sym
353
- @input.scan(/-/)
349
+ match(/-/)
354
350
  end
355
351
  @current_tag[:member] = ident
356
352
  end
@@ -418,7 +414,7 @@ module JsDuck
418
414
  def maybe_name
419
415
  skip_horiz_white
420
416
  if look(@ident_pattern)
421
- @current_tag[:name] = @input.scan(@ident_pattern)
417
+ @current_tag[:name] = match(@ident_pattern)
422
418
  end
423
419
  end
424
420
 
@@ -430,17 +426,15 @@ module JsDuck
430
426
  end
431
427
  end
432
428
 
433
- # attempts to match javascript literal,
434
- # when it fails grabs anything up to closing "]"
429
+ # Attempts to allow balanced braces in default value.
430
+ # When the nested parsing doesn't finish at closing "]",
431
+ # roll back to beginning and simply grab anything up to closing "]".
435
432
  def default_value
436
433
  start_pos = @input.pos
437
- lit = JsLiteralParser.new(@input).literal
438
- if lit && look(/ *\]/)
439
- # When lital matched and there's nothing after it up to the closing "]"
440
- JsLiteralBuilder.new.to_s(lit)
434
+ value = parse_balanced(/\[/, /\]/, /[^\[\]]*/)
435
+ if look(/\]/)
436
+ value
441
437
  else
442
- # Otherwise reset parsing position to where we started
443
- # and rescan up to "]" using simple regex.
444
438
  @input.pos = start_pos
445
439
  match(/[^\]]*/)
446
440
  end
@@ -449,14 +443,8 @@ module JsDuck
449
443
  # matches {...=} and returns text inside brackets
450
444
  def typedef
451
445
  match(/\{/)
452
- name = @input.scan(/[^{}]*/)
453
446
 
454
- # Type definition can contain nested braces: {{foo:Number}}
455
- # In such case we parse the definition so that the braces are balanced.
456
- while @input.check(/[{]/)
457
- name += "{" + typedef[:type] +"}"
458
- name += @input.scan(/[^{}]*/)
459
- end
447
+ name = parse_balanced(/\{/, /\}/, /[^{}]*/)
460
448
 
461
449
  if name =~ /=$/
462
450
  name = name.chop
@@ -470,6 +458,23 @@ module JsDuck
470
458
  return {:type => name, :optional => optional}
471
459
  end
472
460
 
461
+ # Helper method to parse a string up to a closing brace,
462
+ # balancing opening-closing braces in between.
463
+ #
464
+ # @param re_open The beginning brace regex
465
+ # @param re_close The closing brace regex
466
+ # @param re_rest Regex to match text without any braces
467
+ def parse_balanced(re_open, re_close, re_rest)
468
+ result = match(re_rest)
469
+ while look(re_open)
470
+ result += match(re_open)
471
+ result += parse_balanced(re_open, re_close, re_rest)
472
+ result += match(re_close)
473
+ result += match(re_rest)
474
+ end
475
+ result
476
+ end
477
+
473
478
  # matches <ident_chain> <ident_chain> ... until line end
474
479
  def class_list
475
480
  skip_horiz_white
@@ -0,0 +1,58 @@
1
+ module JsDuck
2
+
3
+ # Detects the type of documentation object: class, method, cfg, etc
4
+ class DocType
5
+ # Given parsed documentation and code, returns the tagname for
6
+ # documentation item.
7
+ #
8
+ # @param docs Result from DocParser
9
+ # @param code Result from Ast#detect or CssParser#parse
10
+ # @returns One of: :class, :method, :event, :cfg, :property, :css_var, :css_mixin
11
+ #
12
+ def detect(docs, code)
13
+ doc_map = build_doc_map(docs)
14
+
15
+ if doc_map[:class]
16
+ :class
17
+ elsif doc_map[:event]
18
+ :event
19
+ elsif doc_map[:method]
20
+ :method
21
+ elsif doc_map[:property] || doc_map[:type]
22
+ :property
23
+ elsif doc_map[:css_var]
24
+ :css_var
25
+ elsif doc_map[:cfg] && doc_map[:cfg].length == 1
26
+ # When just one @cfg, avoid treating it as @class
27
+ :cfg
28
+ elsif code[:tagname] == :class
29
+ :class
30
+ elsif code[:tagname] == :css_mixin
31
+ :css_mixin
32
+ elsif doc_map[:cfg]
33
+ :cfg
34
+ elsif doc_map[:constructor]
35
+ :method
36
+ else
37
+ code[:tagname]
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Build map of at-tags for quick lookup
44
+ def build_doc_map(docs)
45
+ map = {}
46
+ docs.each do |tag|
47
+ if map[tag[:tagname]]
48
+ map[tag[:tagname]] << tag
49
+ else
50
+ map[tag[:tagname]] = [tag]
51
+ end
52
+ end
53
+ map
54
+ end
55
+ end
56
+
57
+ end
58
+
@@ -0,0 +1,32 @@
1
+ require 'execjs'
2
+ require 'json'
3
+ require 'singleton'
4
+
5
+ module JsDuck
6
+
7
+ # Runs Esprima.js through execjs (this will select any available
8
+ # JavaScript runtime - preferably therubyracer on MRI and JScript
9
+ # on Windows).
10
+ #
11
+ # Initialized as singleton to avoid loading the esprima.js more
12
+ # than once - otherwise performace will severely suffer.
13
+ class Esprima
14
+ include Singleton
15
+
16
+ def initialize
17
+ esprima_path = File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__))))+"/esprima/esprima.js";
18
+ esprima = IO.read(esprima_path)
19
+ helper = "function runEsprima(js) { return JSON.stringify(esprima.parse(js, {comment: true, range: true, raw: true})); }"
20
+ @context = ExecJS.compile(esprima + "\n\n" + helper)
21
+ end
22
+
23
+ # Parses JavaScript source code using Esprima.js
24
+ #
25
+ # Returns the resulting AST
26
+ def parse(input)
27
+ json = @context.call("runEsprima", input)
28
+ return JSON.parse(json, :max_nesting => false)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,69 @@
1
+ module JsDuck
2
+
3
+ # Evaluates Esprima AST node into Ruby object
4
+ class Evaluator
5
+
6
+ # Converts AST node into a value.
7
+ #
8
+ # - String literals become Ruby strings
9
+ # - Number literals become Ruby numbers
10
+ # - Regex literals become :regexp symbols
11
+ # - Array expressions become Ruby arrays
12
+ # - etc
13
+ #
14
+ # For anything it doesn't know how to evaluate (like a function
15
+ # expression) it throws exception.
16
+ #
17
+ def to_value(ast)
18
+ case ast["type"]
19
+ when "ArrayExpression"
20
+ ast["elements"].map {|e| to_value(e) }
21
+ when "ObjectExpression"
22
+ h = {}
23
+ ast["properties"].each do |p|
24
+ key = key_value(p["key"])
25
+ value = to_value(p["value"])
26
+ h[key] = value
27
+ end
28
+ h
29
+ when "BinaryExpression"
30
+ if ast["operator"] == "+"
31
+ to_value(ast["left"]) + to_value(ast["right"])
32
+ else
33
+ throw "Unable to handle operator: " + ast["operator"]
34
+ end
35
+ when "MemberExpression"
36
+ if base_css_prefix?(ast)
37
+ "x-"
38
+ else
39
+ throw "Unable to handle this MemberExpression"
40
+ end
41
+ when "Literal"
42
+ if ast["raw"] =~ /\A\//
43
+ :regexp
44
+ else
45
+ ast["value"]
46
+ end
47
+ else
48
+ throw "Unknown node type: " + ast["type"]
49
+ end
50
+ end
51
+
52
+ # Turns object property key into string value
53
+ def key_value(key)
54
+ key["type"] == "Identifier" ? key["name"] : key["value"]
55
+ end
56
+
57
+ # True when MemberExpression == Ext.baseCSSPrefix
58
+ def base_css_prefix?(ast)
59
+ ast["computed"] == false &&
60
+ ast["object"]["type"] == "Identifier" &&
61
+ ast["object"]["name"] == "Ext" &&
62
+ ast["property"]["type"] == "Identifier" &&
63
+ ast["property"]["name"] == "baseCSSPrefix"
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
data/lib/jsduck/guides.rb CHANGED
@@ -36,13 +36,15 @@ module JsDuck
36
36
  end
37
37
 
38
38
  # Modified each_item that also loads HTML for each guide
39
- def each_item
40
- super do |guide|
41
- # Load the guide if not loaded
42
- guide[:html] = load_guide(guide) if guide[:html] == nil
43
- # Pass guide to block if it was successfully loaded.
44
- yield guide if guide[:html]
39
+ def each_item(&block)
40
+ unless @loaded
41
+ super do |guide|
42
+ guide[:html] = load_guide(guide)
43
+ end
44
+ @loaded = true
45
45
  end
46
+
47
+ super(&block)
46
48
  end
47
49
 
48
50
  # Modified to_array that excludes the :html from guide nodes
@@ -58,13 +60,12 @@ module JsDuck
58
60
  def load_guide(guide)
59
61
  in_dir = @path + "/guides/" + guide["name"]
60
62
 
61
- return Logger.instance.warn(:guide, "Guide #{in_dir} not found") unless File.exists?(in_dir)
62
-
63
- guide_file = in_dir + "/README.md"
63
+ begin
64
+ return Logger.instance.warn(:guide, "Guide #{in_dir} not found") unless File.exists?(in_dir)
64
65
 
65
- return Logger.instance.warn(:guide, "README.md not found in #{in_dir}") unless File.exists?(guide_file)
66
+ guide_file = in_dir + "/README.md"
67
+ return Logger.instance.warn(:guide, "README.md not found in #{in_dir}") unless File.exists?(guide_file)
66
68
 
67
- begin
68
69
  @formatter.doc_context = {:filename => guide_file, :linenr => 0}
69
70
  name = File.basename(in_dir)
70
71
  @formatter.img_path = "guides/#{name}"
@@ -13,20 +13,54 @@ module JsDuck
13
13
  def resolve_all
14
14
  @relations.each do |cls|
15
15
  resolve_class(cls) if cls[:inheritdoc]
16
+
17
+ new_cfgs = []
16
18
  cls.all_local_members.each do |member|
17
- if member[:inheritdoc]
18
- resolve(member)
19
+ if member[:inheritdoc] || member[:autodetected]
20
+ resolve(member, new_cfgs)
19
21
  end
20
22
  end
23
+ move_cfgs(cls, new_cfgs)
21
24
  end
22
25
  end
23
26
 
27
+ private
28
+
24
29
  # Copy over doc/params/return from parent member.
25
- def resolve(m)
30
+ def resolve(m, new_cfgs)
26
31
  parent = find_parent(m)
27
- m[:doc] = (m[:doc] + "\n\n" + parent[:doc]).strip
28
- m[:params] = parent[:params] if parent[:params]
29
- m[:return] = parent[:return] if parent[:return]
32
+
33
+ if m[:inheritdoc] && parent
34
+ m[:doc] = (m[:doc] + "\n\n" + parent[:doc]).strip
35
+ m[:params] = parent[:params] if parent[:params]
36
+ m[:return] = parent[:return] if parent[:return]
37
+
38
+ # remember properties that have changed to configs
39
+ if m[:tagname] != parent[:tagname]
40
+ new_cfgs << m
41
+ end
42
+ end
43
+
44
+ resolve_visibility(m, parent)
45
+ end
46
+
47
+ # Changes given properties into configs within class
48
+ def move_cfgs(cls, members)
49
+ members.each do |m|
50
+ m[:tagname] = :cfg
51
+ cls[:members][:property].delete(m)
52
+ cls[:members][:cfg] << m
53
+ end
54
+ end
55
+
56
+ # For auto-detected members/classes (which have @private == :inherit)
57
+ # Use the visibility from parent class (defaulting to private when no parent).
58
+ def resolve_visibility(m, parent)
59
+ if m[:autodetected]
60
+ if !parent || parent[:private]
61
+ m[:meta][:private] = m[:private] = true
62
+ end
63
+ end
30
64
  end
31
65
 
32
66
  # Finds parent member of the given member. When @inheritdoc names
@@ -34,7 +68,8 @@ module JsDuck
34
68
  #
35
69
  # If the parent also has @inheritdoc, continues recursively.
36
70
  def find_parent(m)
37
- if m[:inheritdoc][:cls]
71
+ inherit = m[:inheritdoc] || {}
72
+ if inherit[:cls]
38
73
  # @inheritdoc MyClass#member
39
74
  parent_cls = @relations[m[:inheritdoc][:cls]]
40
75
  return warn("class not found", m) unless parent_cls
@@ -42,7 +77,7 @@ module JsDuck
42
77
  parent = lookup_member(parent_cls, m)
43
78
  return warn("member not found", m) unless parent
44
79
 
45
- elsif m[:inheritdoc][:member]
80
+ elsif inherit[:member]
46
81
  # @inheritdoc #member
47
82
  parent = lookup_member(@relations[m[:owner]], m)
48
83
  return warn("member not found", m) unless parent
@@ -53,7 +88,10 @@ module JsDuck
53
88
  mixins = @relations[m[:owner]].mixins
54
89
 
55
90
  # Warn when no parent or mixins at all
56
- return warn("parent class not found", m) unless parent_cls || mixins.length > 0
91
+ if !parent_cls && mixins.length == 0
92
+ warn("parent class not found", m) unless m[:autodetected]
93
+ return nil
94
+ end
57
95
 
58
96
  # First check for the member in all mixins, because members
59
97
  # from mixins override those from parent class. Looking first
@@ -69,21 +107,49 @@ module JsDuck
69
107
  end
70
108
 
71
109
  # Only when both parent and mixins fail, throw warning
72
- return warn("parent member not found", m) unless parent
110
+ if !parent
111
+ warn("parent member not found", m) unless m[:autodetected]
112
+ return nil
113
+ end
73
114
  end
74
115
 
75
116
  return parent[:inheritdoc] ? find_parent(parent) : parent
76
117
  end
77
118
 
78
119
  def lookup_member(cls, m)
79
- inherit = m[:inheritdoc]
80
- cls.get_members(inherit[:member] || m[:name], inherit[:type] || m[:tagname], inherit[:static] || m[:meta][:static])[0]
120
+ inherit = m[:inheritdoc] || {}
121
+ name = inherit[:member] || m[:name]
122
+ tagname = inherit[:type] || m[:tagname]
123
+ static = inherit[:static] || m[:meta][:static]
124
+
125
+ # Auto-detected properties can override either a property or a
126
+ # config. So look for both types.
127
+ if m[:autodetected] && tagname == :property
128
+ cfg = cls.get_members(name, :cfg, static)[0]
129
+ prop = cls.get_members(name, :property, static)[0]
130
+
131
+ if cfg && prop
132
+ prop
133
+ elsif cfg
134
+ cfg
135
+ elsif prop
136
+ prop
137
+ else
138
+ nil
139
+ end
140
+
141
+ else
142
+ cls.get_members(name, tagname, static)[0]
143
+ end
81
144
  end
82
145
 
83
146
  # Copy over doc from parent class.
84
147
  def resolve_class(cls)
85
148
  parent = find_class_parent(cls)
86
- cls[:doc] = (cls[:doc] + "\n\n" + parent[:doc]).strip
149
+
150
+ if parent
151
+ cls[:doc] = (cls[:doc] + "\n\n" + parent[:doc]).strip
152
+ end
87
153
  end
88
154
 
89
155
  def find_class_parent(cls)
@@ -107,7 +173,7 @@ module JsDuck
107
173
  msg = "@inheritdoc #{item[:inheritdoc][:cls]}"+ (i_member ? "#" + i_member : "") + " - " + msg
108
174
  Logger.instance.warn(:inheritdoc, msg, context[:filename], context[:linenr])
109
175
 
110
- return item
176
+ return nil
111
177
  end
112
178
 
113
179
  end