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/.jshintrc +11 -0
- package/.travis.yml +8 -0
- package/CHANGELOG.md +15 -1
- package/README.md +106 -36
- package/index.js +19 -270
- package/package.json +8 -3
- package/parser.js +350 -0
- package/tests/custom-parsers.spec.js +139 -0
- package/tests/files.spec.js +1 -1
- package/tests/parse.spec.js +306 -96
- package/tests/option-raw.spec.js +0 -154
- package/tests/parse-location.spec.js +0 -169
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
|
+
});
|