jsduck 3.6.1 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -127,14 +127,10 @@ def compress
127
127
  system "rm -rf #{dir}/resources/codemirror"
128
128
  system "rm -rf #{dir}/resources/.sass-cache"
129
129
 
130
- # Empty the extjs dir, leave only the main JS files, CSS and images
130
+ # Empty the extjs dir, leave only the main JS file and images
131
131
  system "rm -rf #{dir}/extjs"
132
132
  system "mkdir #{dir}/extjs"
133
- system "cp #{EXT_DIR}/ext-all.js #{dir}/extjs"
134
- system "cp #{EXT_DIR}/ext-all-debug.js #{dir}/extjs"
135
- system "cp #{EXT_DIR}/bootstrap.js #{dir}/extjs"
136
- system "mkdir -p #{dir}/extjs/resources/css"
137
- system "cp #{EXT_DIR}/resources/css/ext-all.css #{dir}/extjs/resources/css"
133
+ system "cp template/extjs/ext-all.js #{dir}/extjs"
138
134
  system "mkdir -p #{dir}/extjs/resources/themes/images"
139
135
  system "cp -r #{EXT_DIR}/resources/themes/images/default #{dir}/extjs/resources/themes/images"
140
136
  end
@@ -158,27 +154,28 @@ class JsDuckRunner
158
154
  # This excludes Opera and IE < 8.
159
155
  # We check explicitly for IE version to make sure the code works the
160
156
  # same way in both real IE7 and IE7-mode of IE8/9.
161
- def add_comments(db_name)
157
+ def add_comments(db_name, version)
162
158
  comments_base_url = "http://projects.sencha.com/auth"
163
159
  @options += ["--head-html", <<-EOHTML]
164
160
  <script type="text/javascript">
165
161
  Docs.enableComments = ("withCredentials" in new XMLHttpRequest()) || (Ext.ieVersion >= 8);
166
162
  Docs.baseUrl = "#{comments_base_url}";
167
163
  Docs.commentsDb = "#{db_name}";
164
+ Docs.commentsVersion = "#{version}";
168
165
  </script>
169
166
  EOHTML
170
167
  end
171
168
 
172
169
  # For export of ExtJS, reference extjs from the parent dir
173
- def make_paths_relative
174
- relative_sdk_path = "../"
175
- ["#{@out_dir}/eg-iframe.html", "#{@out_dir}/index.html"].each do |file|
170
+ def make_extjs_path_relative
171
+ ["#{@out_dir}/index.html"].each do |file|
176
172
  out = []
177
173
  IO.read(file).each_line do |line|
178
- out << line.sub(/((src|href)="extjs\/)/, '\2="' + relative_sdk_path)
174
+ out << line.sub(/(src|href)="extjs\//, '\1="../')
179
175
  end
180
176
  File.open(file, 'w') {|f| f.write(out) }
181
177
  end
178
+ system "rm -rf #{@out_dir}/extjs"
182
179
  end
183
180
 
184
181
  def add_ext4
@@ -194,54 +191,12 @@ class JsDuckRunner
194
191
  ]
195
192
  end
196
193
 
197
- def add_touch_export
198
- @options += [
199
- "--json",
200
- "--output", "#{@out_dir}/../export/touch1",
201
- "--external=google.maps.Map,google.maps.LatLng",
202
- "#{@sdk_dir}/touch/sencha-touch.jsb3",
203
- ]
204
- end
205
-
206
- def add_touch2_export
207
- @options += [
208
- "--export", "full",
209
- "--output", "#{@out_dir}/../export/touch2",
210
- "--external=google.maps.Map,google.maps.LatLng",
211
- "#{@sdk_dir}/touch/touch.jsb3",
212
- ]
213
- end
214
-
215
- def set_touch2_src
216
- relative_touch_path = "../"
217
- system("cp", "-r", "#{@sdk_dir}/touch/docs/build-welcome.html", "template-min/welcome.html")
218
- system("cp", "-r", "#{@sdk_dir}/touch/docs/eg-iframe.html", "template-min/eg-iframe.html")
219
-
220
- ["template-min/eg-iframe.html", "template-min/welcome.html"].each do |file|
221
- html = IO.read(file);
222
-
223
- touch_src_re = /((src|href)="touch)/m
224
- out = []
225
-
226
- html.each_line do |line|
227
- out << line.sub(/((src|href)="touch\/)/, '\2="' + relative_touch_path)
228
- end
229
-
230
- File.open(file, 'w') {|f| f.write(out) }
231
- end
232
-
233
- head_html = <<-EOHTML
194
+ def add_phone_redirect
195
+ @options += ["--body-html", <<-EOHTML]
234
196
  <script type="text/javascript">
235
- if (Ext.is.Phone) { window.location = "#{relative_touch_path}examples/"; }
197
+ if (Ext.is.Phone) { window.location = "../examples/"; }
236
198
  </script>
237
199
  EOHTML
238
-
239
- @options += [
240
- "--body-html", head_html,
241
- "--welcome", "template-min/welcome.html",
242
- "--eg-iframe", "template-min/eg-iframe.html",
243
- "--examples-base-url", "#{relative_touch_path}examples/",
244
- ]
245
200
  end
246
201
 
247
202
  def add_debug
@@ -346,13 +301,8 @@ class JsDuckRunner
346
301
 
347
302
  end
348
303
 
349
- # Copy over SDK examples
350
- def copy_sdk_examples
351
- system "mkdir #{@out_dir}/extjs/builds"
352
- system "cp #{@ext_dir}/builds/ext-core.js #{@out_dir}/extjs/builds/ext-core.js"
353
- system "cp #{@ext_dir}/release-notes.html #{@out_dir}/extjs"
354
- system "cp -r #{@ext_dir}/examples #{@out_dir}/extjs"
355
- system "cp -r #{@ext_dir}/welcome #{@out_dir}/extjs"
304
+ def copy_extjs_build
305
+ system "cp -r #{@ext_dir} #{@out_dir}/extjs-build"
356
306
  end
357
307
 
358
308
  # Copy over Sencha Touch
@@ -386,11 +336,20 @@ task :sdk, [:mode] => :sass do |t, args|
386
336
  runner.add_seo if mode == "debug" || mode == "live"
387
337
  runner.add_export_notice("ext-js/4-0") if mode == "export"
388
338
  runner.add_google_analytics if mode == "live"
389
- runner.add_comments('comments-ext-js-4') if mode == "debug" || mode == "live"
339
+ runner.add_comments('ext-js', '4') if mode == "debug" || mode == "live"
340
+ if mode == "export"
341
+ runner.add_options ["--eg-iframe", "#{SDK_DIR}/extjs/docs/eg-iframe-build.html"]
342
+ runner.add_options ["--examples-base-url", "../examples/"]
343
+ else
344
+ runner.add_options ["--examples-base-url", "extjs-build/examples/"]
345
+ end
390
346
  runner.run
391
347
 
392
- runner.copy_sdk_examples if mode == "export" || mode == "live"
393
- runner.make_paths_relative if mode == "export"
348
+ if mode == "export"
349
+ runner.make_extjs_path_relative
350
+ else
351
+ runner.copy_extjs_build
352
+ end
394
353
  end
395
354
 
396
355
  desc "Run JSDuck on Docs app itself"
@@ -464,17 +423,26 @@ task :touch2, [:mode] => :sass do |t, args|
464
423
  compress if mode == "live" || mode == "export"
465
424
 
466
425
  runner = JsDuckRunner.new
467
- runner.add_options ["--output", OUT_DIR, "--config", "#{SDK_DIR}/touch/docs/config.json"]
468
- runner.add_debug if mode == "debug"
469
- runner.add_export_notice("touch/2-0") if mode == "export"
426
+ runner.add_options [
427
+ "--output", OUT_DIR,
428
+ "--config", "#{SDK_DIR}/touch/docs/config.json"
429
+ ]
430
+
470
431
  if mode == "export"
471
- runner.set_touch2_src
472
- else
473
- runner.add_options ["--examples-base-url", "touch/examples/"]
432
+ runner.add_export_notice("touch/2-0")
433
+ runner.add_phone_redirect
434
+ # override settings in config.json
435
+ runner.add_options [
436
+ "--welcome", "#{SDK_DIR}/touch/docs/build-welcome.html",
437
+ "--eg-iframe", "#{SDK_DIR}/touch/docs/build-eg-iframe.html",
438
+ "--examples-base-url", "../examples/",
439
+ ]
474
440
  end
441
+
442
+ runner.add_debug if mode == "debug"
475
443
  runner.add_seo if mode == "debug" || mode == "live"
476
444
  runner.add_google_analytics if mode == "live"
477
- runner.add_comments('comments-touch-2') if mode == "debug" || mode == "live"
445
+ runner.add_comments('touch', '2') if mode == "debug" || mode == "live"
478
446
  runner.run
479
447
 
480
448
  runner.copy_touch2_build if mode != "export"
@@ -531,19 +499,6 @@ task :animator, [:mode] => :sass do |t, args|
531
499
  runner.run
532
500
  end
533
501
 
534
- desc "Run JSDuck JSON Export (for internal use at Sencha)\n" +
535
- "export[touch] - creates export for Touch 1\n" +
536
- "export[touch2] - creates export for Touch 2"
537
- task :export, [:mode] do |t, args|
538
- mode = args[:mode]
539
- throw "Unknown mode #{mode}" unless ["touch", "touch2"].include?(mode)
540
-
541
- runner = JsDuckRunner.new
542
- runner.add_touch_export if mode == "touch"
543
- runner.add_touch2_export if mode == "touch2"
544
- runner.run
545
- end
546
-
547
502
  desc "Build JSDuck gem"
548
503
  task :gem => :sass do
549
504
  compress
data/jsduck.gemspec CHANGED
@@ -2,8 +2,8 @@ Gem::Specification.new do |s|
2
2
  s.required_rubygems_version = ">= 1.3.5"
3
3
 
4
4
  s.name = 'jsduck'
5
- s.version = '3.6.1'
6
- s.date = '2012-02-21'
5
+ s.version = '3.7.0'
6
+ s.date = '2012-02-29'
7
7
  s.summary = "Simple JavaScript Duckumentation generator"
8
8
  s.description = "Documentation generator for Sencha JS frameworks"
9
9
  s.homepage = "https://github.com/senchalabs/jsduck"
data/lib/jsduck/class.rb CHANGED
@@ -33,10 +33,16 @@ module JsDuck
33
33
  @doc = doc
34
34
  end
35
35
 
36
+ # Accessor to internal hash
36
37
  def [](key)
37
38
  @doc[key]
38
39
  end
39
40
 
41
+ # Assignment to internal hash keys
42
+ def []=(key, value)
43
+ @doc[key] = value
44
+ end
45
+
40
46
  # Returns instance of parent class, or nil if there is none
41
47
  def parent
42
48
  @doc[:extends] ? lookup(@doc[:extends]) : nil
@@ -124,7 +124,7 @@ module JsDuck
124
124
  at_member
125
125
  elsif look(/@inherit[dD]oc\b/)
126
126
  at_inheritdoc
127
- elsif look(/@alias\s+[\w.]+#\w+/)
127
+ elsif look(/@alias\s+([\w.]+)?#\w+/)
128
128
  # For backwards compatibility.
129
129
  # @alias tag was used as @inheritdoc before
130
130
  at_inheritdoc
@@ -325,21 +325,24 @@ module JsDuck
325
325
 
326
326
  add_tag(:inheritdoc)
327
327
  skip_horiz_white
328
+
328
329
  if look(@ident_chain_pattern)
329
330
  @current_tag[:cls] = ident_chain
330
- if look(/#\w/)
331
- @input.scan(/#/)
332
- if look(/static-/)
333
- @current_tag[:static] = true
334
- @input.scan(/static-/)
335
- end
336
- if look(/(cfg|property|method|event|css_var|css_mixin)-/)
337
- @current_tag[:type] = ident.to_sym
338
- @input.scan(/-/)
339
- end
340
- @current_tag[:member] = ident
331
+ end
332
+
333
+ if look(/#\w/)
334
+ @input.scan(/#/)
335
+ if look(/static-/)
336
+ @current_tag[:static] = true
337
+ @input.scan(/static-/)
338
+ end
339
+ if look(/(cfg|property|method|event|css_var|css_mixin)-/)
340
+ @current_tag[:type] = ident.to_sym
341
+ @input.scan(/-/)
341
342
  end
343
+ @current_tag[:member] = ident
342
344
  end
345
+
343
346
  skip_white
344
347
  end
345
348
 
@@ -33,24 +33,29 @@ module JsDuck
33
33
  #
34
34
  def fix_examples_data
35
35
  each_item do |ex|
36
+ ex["name"] = ex["url"] unless ex["name"]
37
+
36
38
  unless ex["url"] =~ /^https?:\/\//
37
39
  ex["url"] = @opts.examples_base_url + ex["url"]
38
- ex["name"] = ex["url"] unless ex["name"]
39
- unless ex["title"]
40
- ex["title"] = ex["text"]
41
- ex.delete("text")
42
- end
43
- unless ex["description"]
44
- ex["description"] = ex["desc"]
45
- ex.delete("desc")
46
- end
40
+ end
41
+ unless ex["icon"] =~ /^https?:\/\//
42
+ ex["icon"] = @opts.examples_base_url + ex["icon"]
43
+ end
44
+
45
+ unless ex["title"]
46
+ ex["title"] = ex["text"]
47
+ ex.delete("text")
48
+ end
49
+ unless ex["description"]
50
+ ex["description"] = ex["desc"]
51
+ ex.delete("desc")
47
52
  end
48
53
  end
49
54
  end
50
55
 
51
56
  # Extracts example icon URL from example hash
52
57
  def icon_url(example)
53
- @opts.examples_base_url + example["icon"]
58
+ example["icon"]
54
59
  end
55
60
 
56
61
  end
@@ -12,6 +12,7 @@ module JsDuck
12
12
  # Performs all inheriting
13
13
  def resolve_all
14
14
  @relations.each do |cls|
15
+ resolve_class(cls) if cls[:inheritdoc]
15
16
  cls.all_local_members.each do |member|
16
17
  if member[:inheritdoc]
17
18
  resolve(member)
@@ -33,57 +34,80 @@ module JsDuck
33
34
  #
34
35
  # If the parent also has @inheritdoc, continues recursively.
35
36
  def find_parent(m)
36
- context = m[:files][0]
37
- inherit = m[:inheritdoc]
37
+ if m[:inheritdoc][:cls]
38
+ # @inheritdoc MyClass#member
39
+ parent_cls = @relations[m[:inheritdoc][:cls]]
40
+ return warn("class not found", m) unless parent_cls
41
+
42
+ parent = lookup_member(parent_cls, m)
43
+ return warn("member not found", m) unless parent
44
+
45
+ elsif m[:inheritdoc][:member]
46
+ # @inheritdoc #member
47
+ parent = lookup_member(@relations[m[:owner]], m)
48
+ return warn("member not found", m) unless parent
38
49
 
39
- if inherit[:cls]
40
- parent_cls = @relations[inherit[:cls]]
41
- unless parent_cls
42
- warn("@inheritdoc #{inherit[:cls]}##{inherit[:member]} - class not found", context)
43
- return m
44
- end
45
- parent = parent_cls.get_members(inherit[:member], inherit[:type] || m[:tagname], inherit[:static] || m[:meta][:static])[0]
46
- unless parent
47
- warn("@inheritdoc #{inherit[:cls]}##{inherit[:member]} - member not found", context)
48
- return m
49
- end
50
50
  else
51
+ # @inheritdoc
51
52
  parent_cls = @relations[m[:owner]].parent
52
53
  mixins = @relations[m[:owner]].mixins
54
+
53
55
  # Warn when no parent or mixins at all
54
- if !parent_cls && mixins.length == 0
55
- warn("@inheritdoc - parent class not found", context)
56
- return m
57
- end
56
+ return warn("parent class not found", m) unless parent_cls || mixins.length > 0
57
+
58
58
  # First check for the member in all mixins, because members
59
59
  # from mixins override those from parent class. Looking first
60
60
  # from mixins is probably a bit slower, but it's the correct
61
61
  # order to do things.
62
62
  if mixins.length > 0
63
- parent = mixins.map do |mix|
64
- mix.get_members(m[:name], m[:tagname], m[:meta][:static])[0]
65
- end.compact.first
63
+ parent = mixins.map {|mix| lookup_member(mix, m) }.compact.first
66
64
  end
65
+
67
66
  # When not found, try looking from parent class
68
67
  if !parent && parent_cls
69
- parent = parent_cls.get_members(m[:name], m[:tagname], m[:meta][:static])[0]
68
+ parent = lookup_member(parent_cls, m)
70
69
  end
70
+
71
71
  # Only when both parent and mixins fail, throw warning
72
- if !parent
73
- warn("@inheritdoc - parent member not found", context)
74
- return m
75
- end
72
+ return warn("parent member not found", m) unless parent
76
73
  end
77
74
 
78
- if parent[:inheritdoc]
79
- find_parent(parent)
75
+ return parent[:inheritdoc] ? find_parent(parent) : parent
76
+ end
77
+
78
+ 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]
81
+ end
82
+
83
+ # Copy over doc from parent class.
84
+ def resolve_class(cls)
85
+ parent = find_class_parent(cls)
86
+ cls[:doc] = (cls[:doc] + "\n\n" + parent[:doc]).strip
87
+ end
88
+
89
+ def find_class_parent(cls)
90
+ if cls[:inheritdoc][:cls]
91
+ # @inheritdoc MyClass
92
+ parent = @relations[cls[:inheritdoc][:cls]]
93
+ return warn("class not found", cls) unless parent
80
94
  else
81
- parent
95
+ # @inheritdoc
96
+ parent = cls.parent
97
+ return warn("parent class not found", cls) unless parent
82
98
  end
99
+
100
+ return parent[:inheritdoc] ? find_class_parent(parent) : parent
83
101
  end
84
102
 
85
- def warn(msg, context)
103
+ def warn(msg, item)
104
+ context = item[:files][0]
105
+ i_member = item[:inheritdoc][:member]
106
+
107
+ msg = "@inheritdoc #{item[:inheritdoc][:cls]}"+ (i_member ? "#" + i_member : "") + " - " + msg
86
108
  Logger.instance.warn(:inheritdoc, msg, context[:filename], context[:linenr])
109
+
110
+ return item
87
111
  end
88
112
 
89
113
  end
data/lib/jsduck/lint.rb CHANGED
@@ -41,7 +41,7 @@ module JsDuck
41
41
  end
42
42
  end
43
43
  each_member do |member|
44
- if member[:doc] == "" && !member[:private]
44
+ if member[:doc] == "" && !member[:private] && !member[:meta][:hide]
45
45
  warn(:no_doc, "No documentation for #{member[:owner]}##{member[:name]}", member)
46
46
  end
47
47
  end
@@ -72,7 +72,7 @@ module JsDuck
72
72
  ]
73
73
  @meta_tag_paths = []
74
74
 
75
- @version = "3.6.1"
75
+ @version = "3.7.0"
76
76
 
77
77
  # Customizing output
78
78
  @title = "Sencha Docs - Ext JS"
@@ -95,7 +95,7 @@ module JsDuck
95
95
  @export = nil
96
96
  @seo = false
97
97
  @eg_iframe = nil
98
- @examples_base_url = "extjs/examples/"
98
+ @examples_base_url = "extjs-build/examples/"
99
99
 
100
100
  # Debugging
101
101
  # Turn multiprocessing off by default in Windows
@@ -0,0 +1,2 @@
1
+ config.js
2
+ node_modules
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Authentication with a vBulletin user database
3
+ */
4
+
5
+ var crypto = require('crypto'),
6
+ _ = require('underscore');
7
+
8
+ var ForumUser = exports.ForumUser = function(client) {
9
+ this.client = client;
10
+ };
11
+
12
+ ForumUser.prototype = {
13
+
14
+ login: function(username, password, callback) {
15
+
16
+ var sql = "SELECT userid, usergroupid, membergroupids, email, username, password, salt FROM user WHERE username = ?",
17
+ self = this;
18
+
19
+ this.client.query(sql, [username],
20
+
21
+ function selectCb(err, results, fields) {
22
+ if (err) {
23
+ callback(err);
24
+ return;
25
+ }
26
+
27
+ if (results.length == 0) {
28
+ callback("No such user");
29
+ return;
30
+ }
31
+
32
+ if (!self.checkPassword(password, results[0].salt, results[0].password)) {
33
+ callback("Invalid password");
34
+ return;
35
+ }
36
+
37
+ var user = self.getUserFromResult(results[0]);
38
+
39
+ callback(null, user);
40
+ }
41
+ );
42
+ },
43
+
44
+ clientUser: function(user) {
45
+
46
+ crypto.createHash('md5').update(user.email).digest("hex");
47
+
48
+ return {
49
+ emailHash: user.email,
50
+ userName: user.username,
51
+ userId: user.userid,
52
+ mod: _.include(user.membergroupids, 7)
53
+ };
54
+ },
55
+
56
+ checkPassword: function(password, salt, saltedPassword) {
57
+
58
+ password = crypto.createHash('md5').update(password).digest("hex") + salt;
59
+ password = crypto.createHash('md5').update(password).digest("hex");
60
+
61
+ return password == saltedPassword;
62
+ },
63
+
64
+ getUserFromResult: function(result) {
65
+
66
+ var ids, id;
67
+
68
+ if (result.membergroupids) {
69
+ ids = result.membergroupids.split(',');
70
+ result.membergroupids = [];
71
+ for (id in ids) {
72
+ result.membergroupids.push(Number(ids[id]));
73
+ }
74
+ }
75
+
76
+ result.moderator = _.include(result.membergroupids, 7);
77
+
78
+ return result;
79
+ }
80
+ };
@@ -0,0 +1,332 @@
1
+
2
+ /**
3
+ * JSDuck authentication / commenting server side element. Requires Node.js + MongoDB.
4
+ *
5
+ * Authentication assumes a vBulletin forum database, but could easily be adapted (see ForumUser.js)
6
+ *
7
+ * Expects a config file, config.js, that looks like this:
8
+ *
9
+ * exports.db = {
10
+ * user: 'forumUsername',
11
+ * password: 'forumPassword',
12
+ * host: 'forumHost',
13
+ * dbName: 'forumDb'
14
+ * };
15
+ *
16
+ * exports.sessionSecret = 'random string for session cookie encryption';
17
+ *
18
+ * exports.mongoDb = 'mongodb://mongoHost:port/comments';
19
+ *
20
+ */
21
+
22
+ config = require('./config');
23
+ mongoose = require('mongoose');
24
+ require('./database');
25
+ require('express-namespace');
26
+
27
+ var mysql = require('mysql'),
28
+ client = mysql.createClient({
29
+ host: config.db.host,
30
+ user: config.db.user,
31
+ password: config.db.password,
32
+ database: config.db.dbName
33
+ }),
34
+ express = require('express'),
35
+ connect = require('connect'),
36
+ MongoStore = require('connect-session-mongo'),
37
+ _ = require('underscore'),
38
+ ForumUser = require('./ForumUser').ForumUser,
39
+ forumUser = new ForumUser(client),
40
+ util = require('./util'),
41
+ crypto = require('crypto');
42
+
43
+
44
+ var app = express.createServer(
45
+
46
+ // Headers for Cross Origin Resource Sharing (CORS)
47
+ function (req, res, next) {
48
+ res.setHeader('Access-Control-Allow-Origin', '*');
49
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
50
+ res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
51
+ next();
52
+ },
53
+
54
+ express.cookieParser(),
55
+
56
+ // Hack to set session cookie if session ID is set as a URL param.
57
+ // This is because not all browsers support sending cookies via CORS
58
+ function(req, res, next) {
59
+ if (req.query.sid) {
60
+ var sid = req.query.sid.replace(/ /g, '+');
61
+ req.cookies = req.cookies || {};
62
+ req.cookies['sencha_docs'] = sid;
63
+ }
64
+ next();
65
+ },
66
+
67
+ // Use MongoDB for session storage
68
+ connect.session({ store: new MongoStore, secret: config.sessionSecret, key: 'sencha_docs' }),
69
+
70
+ function(req, res, next) {
71
+ // IE doesn't get content-type, so default to form encoded.
72
+ if (!req.headers['content-type']) {
73
+ req.headers['content-type'] = 'application/x-www-form-urlencoded';
74
+ }
75
+ next();
76
+ },
77
+ express.bodyParser(),
78
+ express.methodOverride()
79
+ );
80
+
81
+ app.enable('jsonp callback');
82
+
83
+ // All URLs start with /auth
84
+ app.namespace('/auth', function(){
85
+
86
+ /**
87
+ * Authentication
88
+ */
89
+
90
+ app.get('/session', function(req, res) {
91
+ var result = req.session && req.session.user && forumUser.clientUser(req.session.user);
92
+ res.json(result || false);
93
+ });
94
+
95
+ app.post('/login', function(req, res){
96
+
97
+ forumUser.login(req.body.username, req.body.password, function(err, result) {
98
+
99
+ if (err) {
100
+ res.json({ success: false, reason: err });
101
+ return;
102
+ }
103
+
104
+ req.session = req.session || {};
105
+ req.session.user = result;
106
+
107
+ var response = _.extend(forumUser.clientUser(result), {
108
+ sessionID: req.sessionID,
109
+ success: true
110
+ });
111
+
112
+ res.json(response);
113
+ });
114
+ });
115
+
116
+ // Remove session
117
+ app.post('/logout', function(req, res){
118
+ req.session.user = null;
119
+ res.json({ success: true });
120
+ });
121
+
122
+ /**
123
+ * Handles comment unsubscription requests.
124
+ */
125
+ app.get('/unsubscribe/:subscriptionId', function(req, res) {
126
+
127
+ Subscription.findOne({ _id: req.params.subscriptionId }, function(err, subscription) {
128
+ if (err) throw(err);
129
+
130
+ if (subscription) {
131
+ if (req.query.all == 'true') {
132
+ Subscription.remove({ userId: subscription.userId }, function(err) {
133
+ res.send("You have been unsubscribed from all threads.");
134
+ });
135
+ } else {
136
+ Subscription.remove({ _id: req.params.subscriptionId }, function(err) {
137
+ res.send("You have been unsubscribed from that thread.");
138
+ });
139
+ }
140
+ } else {
141
+ res.send("You are already unsubscribed.")
142
+ }
143
+ });
144
+ });
145
+
146
+ });
147
+
148
+
149
+ /**
150
+ * Commenting
151
+ */
152
+ app.namespace('/auth/:sdk/:version', function(){
153
+
154
+ /**
155
+ * Returns a list of comments for a particular target (eg class, guide, video)
156
+ */
157
+ app.get('/comments', function(req, res) {
158
+
159
+ Comment.find({
160
+ target: JSON.parse(req.query.startkey),
161
+ deleted: { '$ne': true },
162
+ sdk: req.params.sdk,
163
+ version: req.params.version
164
+ }).sort('createdAt', 1).run(function(err, comments){
165
+ res.json(util.formatComments(comments, req));
166
+ });
167
+ });
168
+
169
+ /**
170
+ * Returns 100 most recent comments.
171
+ */
172
+ app.get('/comments_recent', function(req, res) {
173
+ Comment.find({
174
+ deleted: { '$ne': true },
175
+ sdk: req.params.sdk,
176
+ version: req.params.version
177
+ }).sort('createdAt', -1).limit(100).run(function(err, comments){
178
+ res.json(util.formatComments(comments, req));
179
+ });
180
+ });
181
+
182
+ /**
183
+ * Returns number of comments for each class / method
184
+ */
185
+ app.get('/comments_meta', util.getCommentsMeta, util.getCommentSubscriptions, function(req, res) {
186
+ res.send({ comments: req.commentsMeta, subscriptions: req.commentSubscriptions || [] });
187
+ });
188
+
189
+ /**
190
+ * Returns an individual comment (used when editing a comment)
191
+ */
192
+ app.get('/comments/:commentId', util.findComment, function(req, res) {
193
+ res.json({ success: true, content: req.comment.content });
194
+ });
195
+
196
+ /**
197
+ * Creates a new comment
198
+ */
199
+ app.post('/comments', util.requireLoggedInUser, function(req, res) {
200
+
201
+ var target = JSON.parse(req.body.target);
202
+
203
+ if (target.length === 2) {
204
+ target.push('');
205
+ }
206
+
207
+ var comment = new Comment({
208
+ author: req.session.user.username,
209
+ userId: req.session.user.userid,
210
+ content: req.body.comment,
211
+ action: req.body.action,
212
+ rating: Number(req.body.rating),
213
+ contentHtml: util.sanitize(req.body.comment),
214
+ downVotes: [],
215
+ upVotes: [],
216
+ createdAt: new Date,
217
+ target: target,
218
+ emaiHash: crypto.createHash('md5').update(req.session.user.email).digest("hex"),
219
+ sdk: req.params.sdk,
220
+ version: req.params.version,
221
+ moderator: req.session.user.moderator,
222
+ title: req.body.title,
223
+ url: req.body.url
224
+ });
225
+
226
+ comment.save(function(err, response) {
227
+ res.json({ success: true, id: response._id, action: req.body.action });
228
+
229
+ util.sendEmailUpdates(comment);
230
+ });
231
+ });
232
+
233
+ /**
234
+ * Updates an existing comment (for voting or updating contents)
235
+ */
236
+ app.post('/comments/:commentId', util.requireLoggedInUser, util.findComment, function(req, res) {
237
+
238
+ var voteDirection,
239
+ comment = req.comment;
240
+
241
+ if (req.body.vote) {
242
+ util.vote(req, res, comment);
243
+ } else {
244
+ var canUpdate = _.include(req.session.user.membergroupids, 7) || req.session.user.username == comment.author;
245
+
246
+ if (!canUpdate) {
247
+ res.json({success: false, reason: 'Forbidden'}, 403);
248
+ return;
249
+ }
250
+
251
+ comment.content = req.body.content;
252
+ comment.contentHtml = util.sanitize(req.body.content);
253
+
254
+ comment.updates = comment.updates || [];
255
+ comment.updates.push({
256
+ updatedAt: String(new Date()),
257
+ author: req.session.user.username
258
+ });
259
+
260
+ comment.save(function(err, response) {
261
+ res.json({ success: true, content: comment.contentHtml });
262
+ });
263
+ }
264
+ });
265
+
266
+ /**
267
+ * Deletes a comment
268
+ */
269
+ app.post('/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, function(req, res) {
270
+
271
+ var canDelete = false,
272
+ comment = req.comment;
273
+
274
+ canDelete = _.include(req.session.user.membergroupids, 7) || req.session.user.username == req.comment.author;
275
+
276
+ if (!canDelete) {
277
+ res.json({ success: false, reason: 'Forbidden' }, 403);
278
+ return;
279
+ }
280
+
281
+ comment.deleted = true;
282
+
283
+ comment.save(function(err, response) {
284
+ res.send({ success: true });
285
+ });
286
+ });
287
+
288
+ /**
289
+ * Get email subscriptions
290
+ */
291
+ app.get('/subscriptions', util.getCommentSubscriptions, function(req, res) {
292
+ res.json({ subscriptions: req.commentSubscriptions });
293
+ });
294
+
295
+ /**
296
+ * Subscibe / unsubscribe to a comment thread
297
+ */
298
+ app.post('/subscribe', util.requireLoggedInUser, function(req, res) {
299
+
300
+ var subscriptionBody = {
301
+ sdk: req.params.sdk,
302
+ version: req.params.version,
303
+ target: JSON.parse(req.body.target),
304
+ userId: req.session.user.userid
305
+ };
306
+
307
+ Subscription.findOne(subscriptionBody, function(err, subscription) {
308
+
309
+ if (subscription && req.body.subscribed == 'false') {
310
+
311
+ subscription.remove(function(err, ok) {
312
+ res.send({ success: true });
313
+ });
314
+
315
+ } else if (!subscription && req.body.subscribed == 'true') {
316
+
317
+ subscription = new Subscription(subscriptionBody);
318
+ subscription.email = req.session.user.email;
319
+
320
+ subscription.save(function(err, ok) {
321
+ res.send({ success: true });
322
+ });
323
+ }
324
+ });
325
+ });
326
+
327
+ });
328
+
329
+
330
+ app.listen(3000);
331
+ console.log("Server started...");
332
+
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Defines comment schema and connects to database
3
+ */
4
+ Comment = mongoose.model('Comment', new mongoose.Schema({
5
+ sdk: String,
6
+ version: String,
7
+
8
+ action: String,
9
+ author: String,
10
+ userId: Number,
11
+ content: String,
12
+ contentHtml: String,
13
+ createdAt: Date,
14
+ downVotes: Array,
15
+ emailHash: String,
16
+ rating: Number,
17
+ target: Array,
18
+ upVotes: Array,
19
+ deleted: Boolean,
20
+ updates: Array,
21
+ mod: Boolean,
22
+ title: String,
23
+ url: String
24
+ }));
25
+
26
+ Subscription = mongoose.model('Subscription', new mongoose.Schema({
27
+ sdk: String,
28
+ version: String,
29
+
30
+ createdAt: Date,
31
+ userId: Number,
32
+ email: String,
33
+ target: Array
34
+ }));
35
+
36
+ mongoose.connect(config.mongoDb);
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "jsduck_comments",
3
+ "version": "0.5.0",
4
+ "description": "Commenting backend for JSDuck Documentation",
5
+ "author": "Nick Poudlen <nick@sencha.com>",
6
+ "dependencies": {
7
+ "connect": "",
8
+ "connect-session-mongo": "",
9
+ "express": "",
10
+ "express-namespace": "",
11
+ "marked": "",
12
+ "mongoose": "",
13
+ "mysql": "",
14
+ "sanitizer": "",
15
+ "step": "",
16
+ "underscore": "",
17
+ "nodemailer": ""
18
+ }
19
+ }
20
+
@@ -0,0 +1,245 @@
1
+
2
+ var marked = require('marked'),
3
+ _ = require('underscore'),
4
+ sanitizer = require('sanitizer'),
5
+ nodemailer = require("nodemailer");
6
+
7
+ exports.sanitize = function(content, opts) {
8
+
9
+ var markdowned, sanitized_output, urlFunc;
10
+
11
+ try {
12
+ markdowned = marked(content);
13
+ } catch(e) {
14
+ markdowned = content;
15
+ }
16
+
17
+ var exp = /(\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/igm;
18
+ markdowned = markdowned.replace(exp, "<a href='$1'>$1</a>");
19
+
20
+ if (opts && opts.stripUrls) {
21
+ urlFunc = function(str) {
22
+ if (str.match(/^(http:\/\/(www\.)?sencha.com|#))/)) {
23
+ return str;
24
+ } else {
25
+ return '';
26
+ }
27
+ };
28
+ }
29
+
30
+ sanitized_output = sanitizer.sanitize(markdowned, urlFunc);
31
+ sanitized_output = sanitized_output.replace(/&apos;/g, '&#39;');
32
+
33
+ return sanitized_output;
34
+ };
35
+
36
+ exports.formatComments = function(comments, req) {
37
+
38
+ return _.map(comments, function(comment) {
39
+
40
+ comment = _.extend(comment._doc, {
41
+ score: comment.upVotes.length - comment.downVotes.length,
42
+ createdAt: String(comment.createdAt)
43
+ });
44
+
45
+ if (req.session.user) {
46
+ comment.upVote = _.contains(comment.upVotes, req.session.user.username);
47
+ comment.downVote = _.contains(comment.downVotes, req.session.user.username);
48
+ }
49
+
50
+ return comment;
51
+ });
52
+ };
53
+
54
+ exports.vote = function(req, res, comment) {
55
+
56
+ var voteDirection;
57
+
58
+ if (req.session.user.username == comment.author) {
59
+
60
+ // Ignore votes from the author
61
+ res.json({success: false, reason: 'You cannot vote on your own content'});
62
+ return;
63
+
64
+ } else if (req.body.vote == 'up' && !_.include(comment.upVotes, req.session.user.username)) {
65
+
66
+ var voted = _.include(comment.downVotes, req.session.user.username);
67
+
68
+ comment.downVotes = _.reject(comment.downVotes, function(v) {
69
+ return v == req.session.user.username;
70
+ });
71
+
72
+ if (!voted) {
73
+ voteDirection = 'up';
74
+ comment.upVotes.push(req.session.user.username);
75
+ }
76
+ } else if (req.body.vote == 'down' && !_.include(comment.downVotes, req.session.user.username)) {
77
+
78
+ var voted = _.include(comment.upVotes, req.session.user.username);
79
+
80
+ comment.upVotes = _.reject(comment.upVotes, function(v) {
81
+ return v == req.session.user.username;
82
+ });
83
+
84
+ if (!voted) {
85
+ voteDirection = 'down';
86
+ comment.downVotes.push(req.session.user.username);
87
+ }
88
+ }
89
+
90
+ comment.save(function(err, response) {
91
+ res.json({
92
+ success: true,
93
+ direction: voteDirection,
94
+ total: (comment.upVotes.length - comment.downVotes.length)
95
+ });
96
+ });
97
+ };
98
+
99
+
100
+ exports.requireLoggedInUser = function(req, res, next) {
101
+
102
+ if (!req.session || !req.session.user) {
103
+ res.json({success: false, reason: 'Forbidden'}, 403);
104
+ } else {
105
+ next();
106
+ }
107
+ };
108
+
109
+ exports.findComment = function(req, res, next) {
110
+
111
+ if (req.params.commentId) {
112
+ Comment.findById(req.params.commentId, function(err, comment) {
113
+ req.comment = comment;
114
+ next();
115
+ });
116
+ } else {
117
+ res.json({success: false, reason: 'No such comment'});
118
+ }
119
+
120
+ };
121
+
122
+ exports.sendEmailUpdates = function(comment) {
123
+
124
+ var mailTransport = nodemailer.createTransport("SMTP",{
125
+ host: 'localhost',
126
+ port: 25
127
+ });
128
+
129
+ var sendSubscriptionEmail = function(emails) {
130
+
131
+ var email = emails.shift();
132
+
133
+ if (email) {
134
+ nodemailer.sendMail(email, function(err){
135
+ if (err){
136
+ console.log(err);
137
+ } else{
138
+ console.log("Sent email to " + email.to);
139
+ sendSubscriptionEmail(emails);
140
+ }
141
+ });
142
+ } else {
143
+ console.log("Finished sending emails");
144
+ mailTransport.close();
145
+ }
146
+ }
147
+
148
+ var subscriptionBody = {
149
+ sdk: comment.sdk,
150
+ version: comment.version,
151
+ target: comment.target
152
+ };
153
+
154
+ var emails = [];
155
+
156
+ Subscription.find(subscriptionBody, function(err, subscriptions) {
157
+
158
+ _.each(subscriptions, function(subscription) {
159
+ var mailOptions = {
160
+ transport: mailTransport,
161
+ from: "Sencha Documentation <no-reply@sencha.com>",
162
+ to: subscription.email,
163
+ subject: "Comment on '" + comment.title + "'",
164
+ text: [
165
+ "A comment by " + comment.author + " on '" + comment.title + "' was posted on the Sencha Documentation:\n",
166
+ comment.content + "\n",
167
+ "--",
168
+ "Original thread: " + comment.url,
169
+ "Unsubscribe from this thread: http://projects.sencha.com/auth/unsubscribe/" + subscription._id,
170
+ "Unsubscribe from all threads: http://projects.sencha.com/auth/unsubscribe/" + subscription._id + '?all=true'
171
+ ].join("\n")
172
+ }
173
+
174
+ if (Number(comment.userId) != Number(subscription.userId)) {
175
+ emails.push(mailOptions);
176
+ }
177
+ });
178
+
179
+ if (emails.length) {
180
+ sendSubscriptionEmail(emails);
181
+ } else {
182
+ console.log("No emails to send");
183
+ }
184
+ });
185
+ }
186
+
187
+
188
+ exports.getCommentsMeta = function(req, res, next) {
189
+
190
+ var map = function() {
191
+ if (this.target) {
192
+ emit(this.target.slice(0,3).join('__'), 1);
193
+ } else {
194
+ return;
195
+ }
196
+ }
197
+
198
+ var reduce = function(key, values) {
199
+ var i = 0, total = 0;
200
+
201
+ for (; i< values.length; i++) {
202
+ total += values[i];
203
+ }
204
+
205
+ return total;
206
+ }
207
+
208
+ mongoose.connection.db.executeDbCommand({
209
+ mapreduce: 'comments',
210
+ map: map.toString(),
211
+ reduce: reduce.toString(),
212
+ out: 'commentCounts',
213
+ query: {
214
+ deleted: { '$ne': true },
215
+ sdk: req.params.sdk,
216
+ version: req.params.version
217
+ }
218
+ }, function(err, dbres) {
219
+ mongoose.connection.db.collection('commentCounts', function(err, collection) {
220
+ collection.find({}).toArray(function(err, comments) {
221
+ req.commentsMeta = comments;
222
+ next()
223
+ });
224
+ });
225
+ });
226
+ }
227
+
228
+ exports.getCommentSubscriptions = function(req, res, next) {
229
+ if (req.session.user) {
230
+ Subscription.find({
231
+ sdk: req.params.sdk,
232
+ version: req.params.version,
233
+ userId: req.session.user.userid
234
+ }, function(err, subscriptions) {
235
+ req.commentSubscriptions = _.map(subscriptions, function(subscription) {
236
+ return subscription.target;
237
+ });
238
+ next();
239
+ })
240
+ } else {
241
+ next();
242
+ }
243
+ }
244
+
245
+
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsduck
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 3
8
- - 6
9
- - 1
10
- version: 3.6.1
8
+ - 7
9
+ - 0
10
+ version: 3.7.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Rene Saarsoo
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2012-02-21 00:00:00 +02:00
19
+ date: 2012-02-29 00:00:00 +02:00
20
20
  default_executable:
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency
@@ -150,6 +150,12 @@ files:
150
150
  - lib/jsduck/type_parser.rb
151
151
  - lib/jsduck/videos.rb
152
152
  - lib/jsduck/welcome.rb
153
+ - opt/comments-server-side/.gitignore
154
+ - opt/comments-server-side/ForumUser.js
155
+ - opt/comments-server-side/app.js
156
+ - opt/comments-server-side/database.js
157
+ - opt/comments-server-side/package.json
158
+ - opt/comments-server-side/util.js
153
159
  - opt/example.js
154
160
  - template-min/README.md
155
161
  - template-min/template.html
@@ -213,8 +219,6 @@ files:
213
219
  - template-min/app.js
214
220
  - template-min/print-template.html
215
221
  - template-min/favicon.ico
216
- - template-min/extjs/bootstrap.js
217
- - template-min/extjs/ext-all-debug.js
218
222
  - template-min/extjs/resources/themes/images/default/tab/tab-default-bottom-sides.gif
219
223
  - template-min/extjs/resources/themes/images/default/tab/tab-default-top-disabled-corners.gif
220
224
  - template-min/extjs/resources/themes/images/default/tab/tab-default-top-active-sides.gif
@@ -476,12 +480,20 @@ files:
476
480
  - template-min/extjs/resources/themes/images/default/form/text-bg.gif
477
481
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-bottom-corners.gif
478
482
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-right-sides.gif
483
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-bottom-corners.gif
484
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-right-sides.gif
479
485
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-top-sides.gif
486
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-left-sides.gif
480
487
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-top-corners.gif
481
488
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-left-corners.gif
489
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-right-corners.gif
490
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-top-corners.gif
482
491
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-left-sides.gif
492
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-bottom-sides.gif
493
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-left-corners.gif
483
494
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-right-corners.gif
484
495
  - template-min/extjs/resources/themes/images/default/window-header/window-header-default-bottom-sides.gif
496
+ - template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-top-sides.gif
485
497
  - template-min/extjs/resources/themes/images/default/tab-bar/scroll-left.gif
486
498
  - template-min/extjs/resources/themes/images/default/tab-bar/tab-bar-default-bg.gif
487
499
  - template-min/extjs/resources/themes/images/default/tab-bar/scroll-right.gif
@@ -577,7 +589,6 @@ files:
577
589
  - template-min/extjs/resources/themes/images/default/slider/slider-v-bg.png
578
590
  - template-min/extjs/resources/themes/images/default/slider/slider-v-bg.gif
579
591
  - template-min/extjs/resources/themes/images/default/slider/slider-thumb.png
580
- - template-min/extjs/resources/css/ext-all.css
581
592
  - template-min/extjs/ext-all.js
582
593
  - template-min/build-js.html
583
594
  - template-min/index.php