jsduck 3.7.0 → 3.8.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/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
|
|