jsduck 3.7.0 → 3.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +3 -1
- data/Rakefile +1 -1
- data/jsduck.gemspec +2 -2
- data/lib/jsduck/app.rb +2 -1
- data/lib/jsduck/app_data.rb +1 -1
- data/lib/jsduck/class.rb +13 -2
- data/lib/jsduck/doc_formatter.rb +30 -43
- data/lib/jsduck/doc_parser.rb +26 -5
- data/lib/jsduck/guides.rb +2 -1
- data/lib/jsduck/index_html.rb +2 -1
- data/lib/jsduck/inline_img.rb +53 -0
- data/lib/jsduck/inline_video.rb +58 -0
- data/lib/jsduck/io.rb +30 -0
- data/lib/jsduck/json_duck.rb +2 -1
- data/lib/jsduck/options.rb +6 -8
- data/lib/jsduck/search_data.rb +61 -25
- data/lib/jsduck/tag/aside.rb +0 -3
- data/lib/jsduck/type_parser.rb +305 -30
- data/lib/jsduck/welcome.rb +2 -1
- data/opt/comments-server-side/app.js +60 -45
- data/opt/comments-server-side/package.json +1 -1
- data/opt/comments-server-side/util.js +129 -54
- metadata +398 -404
data/lib/jsduck/welcome.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'jsduck/null_object'
|
2
|
+
require 'jsduck/io'
|
2
3
|
|
3
4
|
module JsDuck
|
4
5
|
|
@@ -14,7 +15,7 @@ module JsDuck
|
|
14
15
|
|
15
16
|
# Parses welcome HTML file with content for welcome page.
|
16
17
|
def initialize(filename)
|
17
|
-
@html = IO.read(filename)
|
18
|
+
@html = JsDuck::IO.read(filename)
|
18
19
|
end
|
19
20
|
|
20
21
|
# Returns the HTML
|
@@ -138,7 +138,7 @@ app.namespace('/auth', function(){
|
|
138
138
|
});
|
139
139
|
}
|
140
140
|
} else {
|
141
|
-
res.send("You are already unsubscribed.")
|
141
|
+
res.send("You are already unsubscribed.");
|
142
142
|
}
|
143
143
|
});
|
144
144
|
});
|
@@ -156,34 +156,56 @@ app.namespace('/auth/:sdk/:version', function(){
|
|
156
156
|
*/
|
157
157
|
app.get('/comments', function(req, res) {
|
158
158
|
|
159
|
+
if (!req.query.startkey) {
|
160
|
+
res.json({error: 'Invalid request'});
|
161
|
+
return;
|
162
|
+
}
|
163
|
+
|
159
164
|
Comment.find({
|
160
165
|
target: JSON.parse(req.query.startkey),
|
161
166
|
deleted: { '$ne': true },
|
162
167
|
sdk: req.params.sdk,
|
163
168
|
version: req.params.version
|
164
169
|
}).sort('createdAt', 1).run(function(err, comments){
|
165
|
-
res.json(util.
|
170
|
+
res.json(util.scoreComments(comments, req));
|
166
171
|
});
|
167
172
|
});
|
168
173
|
|
169
174
|
/**
|
170
|
-
* Returns
|
175
|
+
* Returns n most recent comments.
|
176
|
+
* Takes two parameters: offset and limit.
|
177
|
+
*
|
178
|
+
* The last comment object returned will contain `total_rows`,
|
179
|
+
* `offset` and `limit` fields. I'd say it's a hack, but at least
|
180
|
+
* it works for now.
|
171
181
|
*/
|
172
182
|
app.get('/comments_recent', function(req, res) {
|
173
|
-
|
183
|
+
var offset = parseInt(req.query.offset, 10) || 0;
|
184
|
+
var limit = parseInt(req.query.limit, 10) || 100;
|
185
|
+
var filter = {
|
174
186
|
deleted: { '$ne': true },
|
175
187
|
sdk: req.params.sdk,
|
176
188
|
version: req.params.version
|
177
|
-
}
|
178
|
-
|
189
|
+
};
|
190
|
+
Comment.find(filter).sort('createdAt', -1).skip(offset).limit(limit).run(function(err, comments) {
|
191
|
+
comments = util.scoreComments(comments, req);
|
192
|
+
// Count all comments, store count to last comment
|
193
|
+
Comment.count(filter).run(function(err, count) {
|
194
|
+
var last = comments[comments.length-1];
|
195
|
+
last.total_rows = count;
|
196
|
+
last.offset = offset;
|
197
|
+
last.limit = limit;
|
198
|
+
res.json(comments);
|
199
|
+
});
|
179
200
|
});
|
180
201
|
});
|
181
202
|
|
182
203
|
/**
|
183
|
-
* Returns number of comments for each class
|
204
|
+
* Returns number of comments for each class/member,
|
205
|
+
* and a list of classes/members into which the user has subscribed.
|
184
206
|
*/
|
185
|
-
app.get('/comments_meta', util.
|
186
|
-
res.send({ comments: req.
|
207
|
+
app.get('/comments_meta', util.getCommentCounts, util.getCommentSubscriptions, function(req, res) {
|
208
|
+
res.send({ comments: req.commentCounts, subscriptions: req.commentSubscriptions || [] });
|
187
209
|
});
|
188
210
|
|
189
211
|
/**
|
@@ -210,7 +232,7 @@ app.namespace('/auth/:sdk/:version', function(){
|
|
210
232
|
content: req.body.comment,
|
211
233
|
action: req.body.action,
|
212
234
|
rating: Number(req.body.rating),
|
213
|
-
contentHtml: util.
|
235
|
+
contentHtml: util.markdown(req.body.comment),
|
214
236
|
downVotes: [],
|
215
237
|
upVotes: [],
|
216
238
|
createdAt: new Date,
|
@@ -241,24 +263,19 @@ app.namespace('/auth/:sdk/:version', function(){
|
|
241
263
|
if (req.body.vote) {
|
242
264
|
util.vote(req, res, comment);
|
243
265
|
} else {
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
comment.updates = comment.updates || [];
|
255
|
-
comment.updates.push({
|
256
|
-
updatedAt: String(new Date()),
|
257
|
-
author: req.session.user.username
|
258
|
-
});
|
266
|
+
util.requireOwner(req, res, function() {
|
267
|
+
comment.content = req.body.content;
|
268
|
+
comment.contentHtml = util.markdown(req.body.content);
|
269
|
+
|
270
|
+
comment.updates = comment.updates || [];
|
271
|
+
comment.updates.push({
|
272
|
+
updatedAt: String(new Date()),
|
273
|
+
author: req.session.user.username
|
274
|
+
});
|
259
275
|
|
260
|
-
|
261
|
-
|
276
|
+
comment.save(function(err, response) {
|
277
|
+
res.json({ success: true, content: comment.contentHtml });
|
278
|
+
});
|
262
279
|
});
|
263
280
|
}
|
264
281
|
});
|
@@ -266,25 +283,23 @@ app.namespace('/auth/:sdk/:version', function(){
|
|
266
283
|
/**
|
267
284
|
* Deletes a comment
|
268
285
|
*/
|
269
|
-
app.post('/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, function(req, res) {
|
270
|
-
|
271
|
-
|
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) {
|
286
|
+
app.post('/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, util.requireOwner, function(req, res) {
|
287
|
+
req.comment.deleted = true;
|
288
|
+
req.comment.save(function(err, response) {
|
284
289
|
res.send({ success: true });
|
285
290
|
});
|
286
291
|
});
|
287
292
|
|
293
|
+
/**
|
294
|
+
* Restores deleted comment
|
295
|
+
*/
|
296
|
+
app.post('/comments/:commentId/undo_delete', util.requireLoggedInUser, util.findComment, util.requireOwner, function(req, res) {
|
297
|
+
req.comment.deleted = false;
|
298
|
+
req.comment.save(function(err, response) {
|
299
|
+
res.send({ success: true, comment: util.scoreComments([req.comment], req)[0] });
|
300
|
+
});
|
301
|
+
});
|
302
|
+
|
288
303
|
/**
|
289
304
|
* Get email subscriptions
|
290
305
|
*/
|
@@ -326,7 +341,7 @@ app.namespace('/auth/:sdk/:version', function(){
|
|
326
341
|
|
327
342
|
});
|
328
343
|
|
329
|
-
|
330
|
-
app.listen(
|
331
|
-
console.log("Server started...");
|
344
|
+
var port = 3000;
|
345
|
+
app.listen(port);
|
346
|
+
console.log("Server started at port "+port+"...");
|
332
347
|
|
@@ -4,39 +4,41 @@ var marked = require('marked'),
|
|
4
4
|
sanitizer = require('sanitizer'),
|
5
5
|
nodemailer = require("nodemailer");
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
/**
|
8
|
+
* Converts Markdown-formatted comment text into HTML.
|
9
|
+
*
|
10
|
+
* @param {String} content Markdown-formatted text
|
11
|
+
* @return {String} HTML
|
12
|
+
*/
|
13
|
+
exports.markdown = function(content) {
|
14
|
+
var markdowned;
|
11
15
|
try {
|
12
16
|
markdowned = marked(content);
|
13
17
|
} catch(e) {
|
14
18
|
markdowned = content;
|
15
19
|
}
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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(/'/g, ''');
|
21
|
+
// Strip dangerous markup, but allow links to all URL-s
|
22
|
+
var sanitized_output = sanitizer.sanitize(markdowned, function(str) {
|
23
|
+
return str;
|
24
|
+
});
|
32
25
|
|
33
|
-
|
26
|
+
// IE does not support '
|
27
|
+
return sanitized_output.replace(/'/g, ''');
|
34
28
|
};
|
35
29
|
|
36
|
-
|
37
|
-
|
30
|
+
/**
|
31
|
+
* Calculates up/down scores for each comment.
|
32
|
+
*
|
33
|
+
* Marks if the current user has already voted on the comment.
|
34
|
+
* Ensures createdAt timestamp is a string.
|
35
|
+
*
|
36
|
+
* @param {Object[]} comments
|
37
|
+
* @param {Object} req Containing username data
|
38
|
+
* @return {Object[]}
|
39
|
+
*/
|
40
|
+
exports.scoreComments = function(comments, req) {
|
38
41
|
return _.map(comments, function(comment) {
|
39
|
-
|
40
42
|
comment = _.extend(comment._doc, {
|
41
43
|
score: comment.upVotes.length - comment.downVotes.length,
|
42
44
|
createdAt: String(comment.createdAt)
|
@@ -51,39 +53,46 @@ exports.formatComments = function(comments, req) {
|
|
51
53
|
});
|
52
54
|
};
|
53
55
|
|
56
|
+
/**
|
57
|
+
* Performs voting on comment.
|
58
|
+
*
|
59
|
+
* @param {Object} req The request object.
|
60
|
+
* @param {Object} res The response object where voting result is written.
|
61
|
+
* @param {Comment} comment The comment to vote on.
|
62
|
+
*/
|
54
63
|
exports.vote = function(req, res, comment) {
|
55
|
-
|
56
64
|
var voteDirection;
|
65
|
+
var username = req.session.user.username;
|
57
66
|
|
58
|
-
if (
|
67
|
+
if (username == comment.author) {
|
59
68
|
|
60
69
|
// Ignore votes from the author
|
61
70
|
res.json({success: false, reason: 'You cannot vote on your own content'});
|
62
71
|
return;
|
63
72
|
|
64
|
-
} else if (req.body.vote == 'up' && !_.include(comment.upVotes,
|
73
|
+
} else if (req.body.vote == 'up' && !_.include(comment.upVotes, username)) {
|
65
74
|
|
66
|
-
var voted = _.include(comment.downVotes,
|
75
|
+
var voted = _.include(comment.downVotes, username);
|
67
76
|
|
68
77
|
comment.downVotes = _.reject(comment.downVotes, function(v) {
|
69
|
-
return v ==
|
78
|
+
return v == username;
|
70
79
|
});
|
71
80
|
|
72
81
|
if (!voted) {
|
73
82
|
voteDirection = 'up';
|
74
|
-
comment.upVotes.push(
|
83
|
+
comment.upVotes.push(username);
|
75
84
|
}
|
76
|
-
} else if (req.body.vote == 'down' && !_.include(comment.downVotes,
|
85
|
+
} else if (req.body.vote == 'down' && !_.include(comment.downVotes, username)) {
|
77
86
|
|
78
|
-
var voted = _.include(comment.upVotes,
|
87
|
+
var voted = _.include(comment.upVotes, username);
|
79
88
|
|
80
89
|
comment.upVotes = _.reject(comment.upVotes, function(v) {
|
81
|
-
return v ==
|
90
|
+
return v == username;
|
82
91
|
});
|
83
92
|
|
84
93
|
if (!voted) {
|
85
94
|
voteDirection = 'down';
|
86
|
-
comment.downVotes.push(
|
95
|
+
comment.downVotes.push(username);
|
87
96
|
}
|
88
97
|
}
|
89
98
|
|
@@ -96,9 +105,14 @@ exports.vote = function(req, res, comment) {
|
|
96
105
|
});
|
97
106
|
};
|
98
107
|
|
99
|
-
|
108
|
+
/**
|
109
|
+
* Ensures that user is logged in.
|
110
|
+
*
|
111
|
+
* @param {Object} req
|
112
|
+
* @param {Object} res
|
113
|
+
* @param {Function} next
|
114
|
+
*/
|
100
115
|
exports.requireLoggedInUser = function(req, res, next) {
|
101
|
-
|
102
116
|
if (!req.session || !req.session.user) {
|
103
117
|
res.json({success: false, reason: 'Forbidden'}, 403);
|
104
118
|
} else {
|
@@ -106,8 +120,16 @@ exports.requireLoggedInUser = function(req, res, next) {
|
|
106
120
|
}
|
107
121
|
};
|
108
122
|
|
123
|
+
/**
|
124
|
+
* Looks up comment by ID.
|
125
|
+
*
|
126
|
+
* Stores it into `req.comment`.
|
127
|
+
*
|
128
|
+
* @param {Object} req
|
129
|
+
* @param {Object} res
|
130
|
+
* @param {Function} next
|
131
|
+
*/
|
109
132
|
exports.findComment = function(req, res, next) {
|
110
|
-
|
111
133
|
if (req.params.commentId) {
|
112
134
|
Comment.findById(req.params.commentId, function(err, comment) {
|
113
135
|
req.comment = comment;
|
@@ -116,18 +138,41 @@ exports.findComment = function(req, res, next) {
|
|
116
138
|
} else {
|
117
139
|
res.json({success: false, reason: 'No such comment'});
|
118
140
|
}
|
141
|
+
};
|
119
142
|
|
143
|
+
/**
|
144
|
+
* Ensures that user is allowed to modify/delete the comment,
|
145
|
+
* that is, he is the owner of the comment or a moderator.
|
146
|
+
*
|
147
|
+
* @param {Object} req
|
148
|
+
* @param {Object} res
|
149
|
+
* @param {Function} next
|
150
|
+
*/
|
151
|
+
exports.requireOwner = function(req, res, next) {
|
152
|
+
var isModerator = _.include(req.session.user.membergroupids, 7);
|
153
|
+
var isAuthor = req.session.user.username == req.comment.author;
|
154
|
+
|
155
|
+
if (isModerator || isAuthor) {
|
156
|
+
next();
|
157
|
+
}
|
158
|
+
else {
|
159
|
+
res.json({ success: false, reason: 'Forbidden' }, 403);
|
160
|
+
}
|
120
161
|
};
|
121
162
|
|
163
|
+
/**
|
164
|
+
* Sends e-mail updates when comment is posted to a thread that has
|
165
|
+
* subscribers.
|
166
|
+
*
|
167
|
+
* @param {Comment} comment
|
168
|
+
*/
|
122
169
|
exports.sendEmailUpdates = function(comment) {
|
123
|
-
|
124
170
|
var mailTransport = nodemailer.createTransport("SMTP",{
|
125
171
|
host: 'localhost',
|
126
172
|
port: 25
|
127
173
|
});
|
128
174
|
|
129
175
|
var sendSubscriptionEmail = function(emails) {
|
130
|
-
|
131
176
|
var email = emails.shift();
|
132
177
|
|
133
178
|
if (email) {
|
@@ -143,7 +188,7 @@ exports.sendEmailUpdates = function(comment) {
|
|
143
188
|
console.log("Finished sending emails");
|
144
189
|
mailTransport.close();
|
145
190
|
}
|
146
|
-
}
|
191
|
+
};
|
147
192
|
|
148
193
|
var subscriptionBody = {
|
149
194
|
sdk: comment.sdk,
|
@@ -154,7 +199,6 @@ exports.sendEmailUpdates = function(comment) {
|
|
154
199
|
var emails = [];
|
155
200
|
|
156
201
|
Subscription.find(subscriptionBody, function(err, subscriptions) {
|
157
|
-
|
158
202
|
_.each(subscriptions, function(subscription) {
|
159
203
|
var mailOptions = {
|
160
204
|
transport: mailTransport,
|
@@ -169,7 +213,7 @@ exports.sendEmailUpdates = function(comment) {
|
|
169
213
|
"Unsubscribe from this thread: http://projects.sencha.com/auth/unsubscribe/" + subscription._id,
|
170
214
|
"Unsubscribe from all threads: http://projects.sencha.com/auth/unsubscribe/" + subscription._id + '?all=true'
|
171
215
|
].join("\n")
|
172
|
-
}
|
216
|
+
};
|
173
217
|
|
174
218
|
if (Number(comment.userId) != Number(subscription.userId)) {
|
175
219
|
emails.push(mailOptions);
|
@@ -182,28 +226,43 @@ exports.sendEmailUpdates = function(comment) {
|
|
182
226
|
console.log("No emails to send");
|
183
227
|
}
|
184
228
|
});
|
185
|
-
}
|
186
|
-
|
187
|
-
|
188
|
-
exports.getCommentsMeta = function(req, res, next) {
|
229
|
+
};
|
189
230
|
|
231
|
+
/**
|
232
|
+
* Retrieves comment counts for each target.
|
233
|
+
*
|
234
|
+
* Stores into `req.commentCounts` field an array like this:
|
235
|
+
*
|
236
|
+
* [
|
237
|
+
* {"_id": "class__Ext__", "value": 3},
|
238
|
+
* {"_id": "class__Ext__method-define", "value": 1},
|
239
|
+
* {"_id": "class__Ext.Panel__cfg-title", "value": 8}
|
240
|
+
* ]
|
241
|
+
*
|
242
|
+
* @param {Object} req
|
243
|
+
* @param {Object} res
|
244
|
+
* @param {Function} next
|
245
|
+
*/
|
246
|
+
exports.getCommentCounts = function(req, res, next) {
|
247
|
+
// Map each comment into: ("type__Class__member", 1)
|
190
248
|
var map = function() {
|
191
249
|
if (this.target) {
|
192
250
|
emit(this.target.slice(0,3).join('__'), 1);
|
193
251
|
} else {
|
194
252
|
return;
|
195
253
|
}
|
196
|
-
}
|
254
|
+
};
|
197
255
|
|
256
|
+
// Sum comment counts for each target
|
198
257
|
var reduce = function(key, values) {
|
199
|
-
var
|
258
|
+
var total = 0;
|
200
259
|
|
201
|
-
for (; i< values.length; i++) {
|
260
|
+
for (var i = 0; i < values.length; i++) {
|
202
261
|
total += values[i];
|
203
262
|
}
|
204
263
|
|
205
264
|
return total;
|
206
|
-
}
|
265
|
+
};
|
207
266
|
|
208
267
|
mongoose.connection.db.executeDbCommand({
|
209
268
|
mapreduce: 'comments',
|
@@ -218,13 +277,29 @@ exports.getCommentsMeta = function(req, res, next) {
|
|
218
277
|
}, function(err, dbres) {
|
219
278
|
mongoose.connection.db.collection('commentCounts', function(err, collection) {
|
220
279
|
collection.find({}).toArray(function(err, comments) {
|
221
|
-
req.
|
222
|
-
next()
|
280
|
+
req.commentCounts = comments;
|
281
|
+
next();
|
223
282
|
});
|
224
283
|
});
|
225
284
|
});
|
226
|
-
}
|
285
|
+
};
|
227
286
|
|
287
|
+
/**
|
288
|
+
* Retrieves list of commenting targets into which the current user
|
289
|
+
* has subscribed for e-mail updates.
|
290
|
+
*
|
291
|
+
* Stores them into `req.commentSubscriptions` field as array:
|
292
|
+
*
|
293
|
+
* [
|
294
|
+
* ["class", "Ext", ""],
|
295
|
+
* ["class", "Ext", "method-define"],
|
296
|
+
* ["class", "Ext.Panel", "cfg-title"]
|
297
|
+
* ]
|
298
|
+
*
|
299
|
+
* @param {Object} req
|
300
|
+
* @param {Object} res
|
301
|
+
* @param {Function} next
|
302
|
+
*/
|
228
303
|
exports.getCommentSubscriptions = function(req, res, next) {
|
229
304
|
if (req.session.user) {
|
230
305
|
Subscription.find({
|
@@ -236,10 +311,10 @@ exports.getCommentSubscriptions = function(req, res, next) {
|
|
236
311
|
return subscription.target;
|
237
312
|
});
|
238
313
|
next();
|
239
|
-
})
|
314
|
+
});
|
240
315
|
} else {
|
241
316
|
next();
|
242
317
|
}
|
243
|
-
}
|
318
|
+
};
|
244
319
|
|
245
320
|
|