comment-parser 0.2.3 → 0.3.2

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.
package/parser.js ADDED
@@ -0,0 +1,350 @@
1
+
2
+ var RE_COMMENT_START = /^\s*\/\*\*\s*$/m;
3
+ var RE_COMMENT_LINE = /^\s*\*(?:\s(\s*)|$)/m;
4
+ var RE_COMMENT_END = /^\s*\*\/\s*$/m;
5
+ var RE_COMMENT_1LINE = /^\s*\/\*\*\s*(.*)\s*\*\/\s*$/;
6
+
7
+ /* ------- util functions ------- */
8
+
9
+ function merge(/* ...objects */) {
10
+ var k, obj, res = {}, objs = Array.prototype.slice.call(arguments);
11
+ while (objs.length) {
12
+ obj = objs.shift();
13
+ for (k in obj) { if (obj.hasOwnProperty(k)) {
14
+ res[k] = obj[k];
15
+ }}
16
+ }
17
+ return res;
18
+ }
19
+
20
+ function find(list, filter) {
21
+ var k, i = list.length, matchs = true;
22
+ while (i--) {
23
+ for (k in filter) { if (filter.hasOwnProperty(k)) {
24
+ matchs = (filter[k] === list[i][k]) && matchs;
25
+ }}
26
+ if (matchs) { return list[i]; }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function skipws(str) {
32
+ var i = 0;
33
+ do {
34
+ if (str[i] !== ' ') { return i; }
35
+ } while (++i < str.length);
36
+ return i;
37
+ }
38
+
39
+ /* ------- default parsers ------- */
40
+
41
+ var PARSERS = {};
42
+
43
+ PARSERS.parse_tag = function parse_tag(str) {
44
+ var result = str.match(/^\s*@(\S+)/);
45
+
46
+ if (!result) { throw new Error('Invalid `@tag`, missing @ symbol'); }
47
+
48
+ return {
49
+ source : result[0],
50
+ data : {tag: result[1]}
51
+ };
52
+ };
53
+
54
+ PARSERS.parse_type = function parse_type(str, data) {
55
+ if (data.errors && data.errors.length) { return null; }
56
+
57
+ var pos = skipws(str);
58
+ var res = '';
59
+ var curlies = 0;
60
+
61
+ if (str[pos] !== '{') { return null; }
62
+
63
+ while (pos < str.length) {
64
+ curlies += (str[pos] === '{' ? 1 : (str[pos] === '}' ? -1 : 0));
65
+ res += str[pos];
66
+ pos ++;
67
+ if (curlies === 0) { break; }
68
+ }
69
+
70
+ if (curlies !== 0) { throw new Error('Invalid `{type}`, unpaired curlies'); }
71
+
72
+ return {
73
+ source : str.slice(0, pos),
74
+ data : {type: res.slice(1, -1)}
75
+ };
76
+ };
77
+
78
+ PARSERS.parse_name = function parse_name(str, data) {
79
+ if (data.errors && data.errors.length) { return null; }
80
+
81
+ var pos = skipws(str);
82
+ var name = '';
83
+ var brackets = 0;
84
+
85
+ while (pos < str.length) {
86
+ brackets += (str[pos] === '[' ? 1 : (str[pos] === ']' ? -1 : 0));
87
+ name += str[pos];
88
+ pos ++;
89
+ if (brackets === 0 && /\s/.test(str[pos])) { break; }
90
+ }
91
+
92
+ if (brackets !== 0) { throw new Error('Invalid `name`, unpaired brackets'); }
93
+
94
+ var res = {name: name, optional: false};
95
+
96
+ if (name[0] === '[' && name[name.length - 1] === ']') {
97
+ res.optional = true;
98
+ name = name.slice(1, -1);
99
+
100
+ if (name.indexOf('=') !== -1) {
101
+ var parts = name.split('=');
102
+ name = parts[0];
103
+ res.default = parts[1].replace(/^(["'])(.+)(\1)$/, '$2');
104
+ }
105
+ }
106
+
107
+ res.name = name;
108
+
109
+ return {
110
+ source : str.slice(0, pos),
111
+ data : res
112
+ };
113
+ };
114
+
115
+ PARSERS.parse_description = function parse_description(str, data) {
116
+ if (data.errors && data.errors.length) { return null; }
117
+
118
+ var result = str.match(/^\s+((.|\s)+)?/);
119
+
120
+ if (result) {
121
+ return {
122
+ source : result[0],
123
+ data : {description: result[1] === undefined ? '' : result[1]}
124
+ };
125
+ }
126
+
127
+ return null;
128
+ };
129
+
130
+ /* ------- parsing ------- */
131
+
132
+ /**
133
+ * Parses "@tag {type} name description"
134
+ * @param {string} str Raw doc string
135
+ * @param {Array[function]} parsers Array of parsers to be applied to the source
136
+ * @returns {object} parsed tag node
137
+ */
138
+ function parse_tag(str, parsers) {
139
+ if (typeof str !== 'string' || str[0] !== '@') { return null; }
140
+
141
+ var data = parsers.reduce(function(state, parser) {
142
+ var result;
143
+
144
+ try {
145
+ result = parser(state.source, merge({}, state.data));
146
+ } catch (err) {
147
+ state.data.errors = (state.data.errors || [])
148
+ .concat(parser.name + ': ' + err.message);
149
+ }
150
+
151
+ if (result) {
152
+ state.source = state.source.slice(result.source.length);
153
+ state.data = merge(state.data, result.data);
154
+ }
155
+
156
+ return state;
157
+ }, {
158
+ source : str,
159
+ data : {}
160
+ }).data;
161
+
162
+ data.optional = !!data.optional;
163
+ data.type = data.type === undefined ? '' : data.type;
164
+ data.name = data.name === undefined ? '' : data.name;
165
+ data.description = data.description === undefined ? '' : data.description;
166
+
167
+ return data;
168
+ }
169
+
170
+ /**
171
+ * Parses comment block (array of String lines)
172
+ */
173
+ function parse_block(source, opts) {
174
+
175
+ function trim(s) {
176
+ return opts.trim ? s.trim() : s;
177
+ }
178
+
179
+ var source_str = source
180
+ .map(function(line) { return trim(line.source); })
181
+ .join('\n');
182
+
183
+ source_str = trim(source_str);
184
+
185
+ var start = source[0].number;
186
+
187
+ // merge source lines into tags
188
+ // we assume tag starts with "@"
189
+ source = source
190
+ .reduce(function(tags, line) {
191
+ line.source = trim(line.source);
192
+
193
+ if (line.source.match(/^\s*@(\w+)/)) {
194
+ tags.push({source: [line.source], line: line.number});
195
+ } else {
196
+ var tag = tags[tags.length - 1];
197
+ tag.source.push(line.source);
198
+ }
199
+
200
+ return tags;
201
+ }, [{source: []}])
202
+ .map(function(tag) {
203
+ tag.source = trim(tag.source.join('\n'));
204
+ return tag;
205
+ });
206
+
207
+ // Block description
208
+ var description = source.shift();
209
+
210
+ // skip if no descriptions and no tags
211
+ if (description.source === '' && source.length === 0) {
212
+ return null;
213
+ }
214
+
215
+ var tags = source.reduce(function(tags, tag) {
216
+ var tag_node = parse_tag(tag.source, opts.parsers);
217
+
218
+ if (!tag_node) { return tags; }
219
+
220
+ tag_node.line = tag.line;
221
+ tag_node.source = tag.source;
222
+
223
+ if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
224
+ var parent_name;
225
+ var parent_tag;
226
+ var parent_tags = tags;
227
+ var parts = tag_node.name.split('.');
228
+
229
+ while (parts.length > 1) {
230
+ parent_name = parts.shift();
231
+ parent_tag = find(parent_tags, {
232
+ tag : tag_node.tag,
233
+ name : parent_name
234
+ });
235
+
236
+ if (!parent_tag) {
237
+ parent_tag = {
238
+ tag : tag_node.tag,
239
+ line : Number(tag_node.line),
240
+ name : parent_name,
241
+ type : '',
242
+ description : ''
243
+ };
244
+ parent_tags.push(parent_tag);
245
+ }
246
+
247
+ parent_tag.tags = parent_tag.tags || [];
248
+ parent_tags = parent_tag.tags;
249
+ }
250
+
251
+ tag_node.name = parts[0];
252
+ parent_tags.push(tag_node);
253
+ return tags;
254
+ }
255
+
256
+ return tags.concat(tag_node);
257
+ }, []);
258
+
259
+ return {
260
+ tags : tags,
261
+ line : start,
262
+ description : description.source,
263
+ source : source_str
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Produces `extract` function with internal state initialized
269
+ */
270
+ function mkextract(opts) {
271
+ var chunk = null;
272
+ var number = 0;
273
+
274
+ opts = merge({}, {
275
+ trim : true,
276
+ dotted_names : false,
277
+ parsers : [
278
+ PARSERS.parse_tag,
279
+ PARSERS.parse_type,
280
+ PARSERS.parse_name,
281
+ PARSERS.parse_description
282
+ ]
283
+ }, opts || {});
284
+
285
+ /**
286
+ * Cumulatively reading lines until they make one comment block
287
+ * Returns block object or null.
288
+ */
289
+ return function extract(line) {
290
+
291
+ // if oneliner
292
+ // then parse it immediately
293
+ if (line.match(RE_COMMENT_1LINE)) {
294
+ return parse_block([{
295
+ source: line.replace(RE_COMMENT_1LINE, '$1'),
296
+ number: number}], opts);
297
+ }
298
+
299
+ number += 1;
300
+
301
+ // if start of comment
302
+ // then init the chunk
303
+ if (line.match(RE_COMMENT_START)) {
304
+ chunk = [{source: line.replace(RE_COMMENT_START, ''), number: number - 1}];
305
+ return null;
306
+ }
307
+
308
+ // if comment line and chunk started
309
+ // then append
310
+ if (chunk && line.match(RE_COMMENT_LINE)) {
311
+ chunk.push({
312
+ number: number - 1,
313
+ source: line.replace(RE_COMMENT_LINE, opts.trim ? '' : '$1')
314
+ });
315
+ return null;
316
+ }
317
+
318
+ // if comment end and chunk started
319
+ // then parse the chunk and push
320
+ if (chunk && line.match(RE_COMMENT_END)) {
321
+ chunk.push({source: line.replace(RE_COMMENT_END, ''), number: number - 1});
322
+ return parse_block(chunk, opts);
323
+ }
324
+
325
+ // if non-comment line
326
+ // then reset the chunk
327
+ chunk = null;
328
+ };
329
+ }
330
+
331
+ /* ------- Public API ------- */
332
+
333
+ module.exports = function parse(source, opts) {
334
+ var block;
335
+ var blocks = [];
336
+ var extract = mkextract(opts);
337
+ var lines = source.split(/\n/);
338
+
339
+ while (lines.length) {
340
+ block = extract(lines.shift());
341
+ if (block) {
342
+ blocks.push(block);
343
+ }
344
+ }
345
+
346
+ return blocks;
347
+ };
348
+
349
+ module.exports.PARSERS = PARSERS;
350
+ module.exports.mkextract = mkextract;
@@ -0,0 +1,139 @@
1
+ var fs = require('fs');
2
+ var expect = require('chai').expect;
3
+ var parse = require('../index');
4
+
5
+ describe('parse() with custom tag parsers', function() {
6
+
7
+ function parsed(func, opts) {
8
+ var str = func.toString();
9
+ return parse(str.slice(
10
+ str.indexOf('{') + 1,
11
+ str.lastIndexOf('}')
12
+ ).trim(), opts);
13
+ }
14
+
15
+ function sample() {
16
+ /**
17
+ * @tag {type} name description
18
+ */
19
+ var a;
20
+ }
21
+
22
+ it('should use `opts.parsers`', function() {
23
+ var parsers = [
24
+ function everything(str) {
25
+ return {
26
+ source : str,
27
+ data : {
28
+ tag : 'tag',
29
+ type : 'type',
30
+ name : 'name',
31
+ optional : false,
32
+ description : 'description'
33
+ }
34
+ };
35
+ }
36
+ ];
37
+
38
+ expect(parsed(sample, {parsers: parsers})[0])
39
+ .to.eql({
40
+ line : 0,
41
+ description : '',
42
+ source : '@tag {type} name description',
43
+ tags: [{
44
+ tag : 'tag',
45
+ type : 'type',
46
+ name : 'name',
47
+ description : 'description',
48
+ optional : false,
49
+ source : '@tag {type} name description',
50
+ line : 1
51
+ }]
52
+ });
53
+ });
54
+
55
+ it('should merge parsers result', function() {
56
+ var parsers = [
57
+ function parser1(str) {
58
+ return {
59
+ source : '',
60
+ data : {tag: 'tag'},
61
+ };
62
+ },
63
+ function parser2(str) {
64
+ return {
65
+ source : '',
66
+ data : {type: 'type'},
67
+ };
68
+ },
69
+ function parser3(str) {
70
+ return {
71
+ source : '',
72
+ data : {
73
+ name : 'name',
74
+ description : 'description'
75
+ },
76
+ };
77
+ }
78
+ ];
79
+
80
+ expect(parsed(sample, {parsers: parsers})[0])
81
+ .to.eql({
82
+ line : 0,
83
+ description : '',
84
+ source : '@tag {type} name description',
85
+ tags: [{
86
+ tag : 'tag',
87
+ type : 'type',
88
+ name : 'name',
89
+ description : 'description',
90
+ optional : false,
91
+ source : '@tag {type} name description',
92
+ line : 1
93
+ }]
94
+ });
95
+ });
96
+
97
+ it('should catch parser exceptions and populate `errors` field', function() {
98
+ var parsers = [
99
+ function parser1(str) {
100
+ return {
101
+ source : '',
102
+ data : {tag: 'tag'}
103
+ };
104
+ },
105
+ function parser2(str) {
106
+ throw new Error('error 1');
107
+ },
108
+ function parser3(str) {
109
+ throw new Error('error 2');
110
+ },
111
+ function parser4(str) {
112
+ return {
113
+ source : '',
114
+ data : {name: 'name'}
115
+ };
116
+ },
117
+ ];
118
+
119
+ expect(parsed(sample, {parsers: parsers})[0])
120
+ .to.eql({
121
+ line : 0,
122
+ description : '',
123
+ source : '@tag {type} name description',
124
+ tags: [{
125
+ tag : 'tag',
126
+ type : '',
127
+ name : 'name',
128
+ description : '',
129
+ optional : false,
130
+ source : '@tag {type} name description',
131
+ errors : [
132
+ 'parser2: error 1',
133
+ 'parser3: error 2'
134
+ ],
135
+ line : 1
136
+ }]
137
+ });
138
+ });
139
+ });
@@ -1,6 +1,6 @@
1
1
 
2
2
  var fs = require('fs');
3
- var stream = require('stream');
3
+ var stream = require('readable-stream');
4
4
  var expect = require('chai').expect
5
5
  var parse = require('../index');
6
6