jsduck 3.6.1 → 3.7.0

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