comment-parser 0.2.4 → 0.3.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.
package/.jshintrc ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "node": true,
3
+ "strict": true,
4
+ "maxlen": 100,
5
+ "undef": true,
6
+ "unused": true,
7
+ "onecase": true,
8
+ "lastsemic": true,
9
+ "latedef" : true,
10
+ "indent": 2
11
+ }
package/README.md CHANGED
@@ -25,45 +25,104 @@ this would be parsed into following
25
25
 
26
26
  ```javascript
27
27
  [{
28
- tags: [{
29
- tag: "some-tag",
30
- type: "Type",
31
- name: "name",
32
- line: 15,
33
- description: "Singleline or multiline description text",
34
- tags: [{
35
- tag: "some-tag",
36
- type: "Type",
37
- name: "subname",
38
- line: 16,
39
- description: "Singleline or multiline description text",
40
- tags: [{
41
- tag: "some-tag",
42
- type: "Type",
43
- name: "subsubname",
44
- line: 17,
45
- description: "Singleline or\nmultiline description text"
46
- }]
47
- }]
48
- }, {
49
- tag: "another-tag",
50
- type: "",
51
- name: "",
52
- line: 18,
53
- description: ""
54
- }],
55
- description: "Singleline or multiline description text. Line breaks are preserved."
28
+ "tags": [{
29
+ "tag": "some-tag",
30
+ "type": "Type",
31
+ "name": "name",
32
+ "optional": false,
33
+ "description": "Singleline or multiline description text",
34
+ "line": 3,
35
+ "source": "@some-tag {Type} name Singleline or multiline description text"
36
+ }, {
37
+ "tag": "some-tag",
38
+ "type": "Type",
39
+ "name": "name.subname",
40
+ "optional": false,
41
+ "description": "Singleline or multiline description text",
42
+ "line": 4,
43
+ "source": "@some-tag {Type} name.subname Singleline or multiline description text"
44
+ }, {
45
+ "tag": "some-tag",
46
+ "type": "Type",
47
+ "name": "name.subname.subsubname",
48
+ "optional": false,
49
+ "description": "Singleline or\nmultiline description text",
50
+ "line": 5,
51
+ "source": "@some-tag {Type} name.subname.subsubname Singleline or\nmultiline description text"
52
+ }, {
53
+ "tag": "another-tag",
54
+ "name": "",
55
+ "optional": false,
56
+ "type": "",
57
+ "description": "",
58
+ "line": 7,
59
+ "source": "@another-tag"
60
+ }],
61
+ "line": 0,
62
+ "description": "Singleline or multiline description text. Line breaks are preserved.",
63
+ "source": "Singleline or multiline description text. Line breaks are preserved.\n\n@some-tag {Type} name Singleline or multiline description text\n@some-tag {Type} name.subname Singleline or multiline description text\n@some-tag {Type} name.subname.subsubname Singleline or\nmultiline description text\n@another-tag"
56
64
  }]
57
65
  ```
58
66
 
59
67
  By default dotted names like `name.subname.subsubname` will be expanded into nested sections, this can be prevented by passing `opts.dotted_names = false`.
60
68
 
61
- You can also make raw line available in parsed results by passing `opts.raw_value = true`.
62
-
63
69
  Invalid comment blocks are skipped. Comments starting with `/*` and `/***` are considered not valid.
64
70
 
65
71
  Also you can parse entire file with `parse.file('path/to/file', callback)` or acquire an instance of [Transform](http://nodejs.org/api/stream.html#stream_class_stream_transform) stream with `parse.stream()`.
66
72
 
73
+ Custom parsers
74
+ ==============
75
+
76
+ In case you need to parse tags in different way you can pass `opts.parsers = [parser1, ..., parserN]`, where each parser is `function name(str:String, data:Object):{source:String, data:Object}`.
77
+
78
+ Each parser function takes string left after previous parsers applied and data produced by them. And returns `null` or `{source: '', data:{}}` where `source` is consumed substring and `data` is a payload with tag node fields.
79
+
80
+ Tag node data is build by merging result bits from all parsers. Here is some example that is not doing actual parsing but is demonstrating the flow:
81
+
82
+ ```
83
+ /**
84
+ * Source to be parsed below
85
+ * @tag {type} name Description
86
+ */
87
+ parse(source, {parsers: [
88
+ // takes entire string
89
+ function parse_tag(str, data) {
90
+ return {source: ' @tag', data: {tag: 'tag'}};
91
+ },
92
+ // parser throwing exception
93
+ function check_tag(str, data) {
94
+ if (allowed_tags.indexOf(data.tag) === -1) {
95
+ throw new Error('Unrecognized tag "' + data.tag + '"');
96
+ }
97
+ // takes the rest of the string after ' @tag''
98
+ function parse_name1(str, data) {
99
+ return {source: ' name', data: {name: 'name'}};
100
+ },
101
+ // alternative name parser
102
+ function parse_name2(str, data) {
103
+ return {source: ' name', data: {name: 'name'}};
104
+ }
105
+ ```
106
+
107
+ This would produce following:
108
+
109
+ ```
110
+ [{
111
+ "tags": [{
112
+ "tag": "tag",
113
+ "type": "type",
114
+ "name": "name",
115
+ "optional": false,
116
+ "description": "Description",
117
+ "line": 2,
118
+ "source": "@tag {type} name Description"
119
+ }],
120
+ "line": 0,
121
+ "description": "Source to be parsed below",
122
+ "source": "Source to be parsed below\n@tag {type} name Description"
123
+ }]
124
+ ```
125
+
67
126
  Happy coding :)
68
127
 
69
128
 
package/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ 'use strict';
3
+
2
4
  var fs = require('fs');
3
5
  var stream = require('stream');
4
6
  var util = require('util');
@@ -7,154 +9,235 @@ var RE_COMMENT_START = /^\s*\/\*\*\s*$/m;
7
9
  var RE_COMMENT_LINE = /^\s*\*(?:\s|$)/m;
8
10
  var RE_COMMENT_END = /^\s*\*\/\s*$/m;
9
11
  var RE_COMMENT_1LINE = /^\s*\/\*\*\s*(.*)\s*\*\/\s*$/;
10
- var RE_SPACE = /\s/;
11
12
 
12
- /**
13
- * analogue of str.match(/@(\S+)(?:\s+\{([^\}]+)\})?(?:\s+(\S+))?(?:\s+([^$]+))?/);
14
- * @param {string} str raw jsdoc string
15
- * @returns {object} parsed tag node
16
- */
17
- function parse_tag_line(str) {
18
- if (typeof str !== 'string') { return false; }
19
-
20
- if (str[0] !== '@') { return false; }
21
-
22
- var pos = 1;
23
- var l = str.length;
24
- var error = null;
25
- var new_line = false;
26
- var res = {
27
- tag : _tag(),
28
- type : !new_line && _type() || '',
29
- name : !new_line && _name() || '',
30
- description : _rest() || ''
31
- };
13
+ /* ------- util functions ------- */
32
14
 
33
- if (error) {
34
- res.error = error;
15
+ function merge(/* ...objects */) {
16
+ var k, obj, res = {}, objs = Array.prototype.slice.call(arguments);
17
+ while (objs.length) {
18
+ obj = objs.shift();
19
+ for (k in obj) { if (obj.hasOwnProperty(k)) {
20
+ res[k] = obj[k];
21
+ }}
35
22
  }
36
-
37
23
  return res;
24
+ }
38
25
 
39
- function _skipws() {
40
- var prev_pos = pos;
41
- while (pos < l && RE_SPACE.test(str[pos])) {
42
- new_line = new_line || str[pos] === '\n';
43
- pos++;
44
- }
26
+ function find(list, filter) {
27
+ var k, i = list.length, matchs = true;
28
+ while (i--) {
29
+ for (k in filter) { if (filter.hasOwnProperty(k)) {
30
+ matchs = (filter[k] === list[i][k]) && matchs;
31
+ }}
32
+ if (matchs) { return list[i]; }
45
33
  }
46
- function _tag() { // @(\S+)
47
- var sp = str.search(RE_SPACE, pos);
48
- sp = sp < 0 ? l : sp;
49
- var res = str.substr(pos, sp - pos);
50
- pos = sp;
51
- return res;
34
+ return null;
35
+ }
36
+
37
+ function skipws(str) {
38
+ var i = 0;
39
+ do {
40
+ if (str[i] !== ' ') { return i; }
41
+ } while (++i < str.length);
42
+ return i;
43
+ }
44
+
45
+ /* ------- default parsers ------- */
46
+
47
+ var PARSERS = {};
48
+
49
+ PARSERS.parse_tag = function parse_tag(str) {
50
+ var result = str.match(/^\s*@(\S+)/);
51
+
52
+ if (!result) { throw new Error('Invalid `@tag`, missing @ symbol'); }
53
+
54
+ return {
55
+ source : result[0],
56
+ data : {tag: result[1]}
57
+ };
58
+ };
59
+
60
+ PARSERS.parse_type = function parse_type(str, data) {
61
+ if (data.errors && data.errors.length) { return null; }
62
+
63
+ var pos = skipws(str);
64
+ var res = '';
65
+ var curlies = 0;
66
+
67
+ if (str[pos] !== '{') { return null; }
68
+
69
+ while (pos < str.length) {
70
+ curlies += (str[pos] === '{' ? 1 : (str[pos] === '}' ? -1 : 0));
71
+ res += str[pos];
72
+ pos ++;
73
+ if (curlies === 0) { break; }
52
74
  }
53
- function _type() { // (?:\s+\{([^\}]+)\})?
54
- _skipws();
55
- if (str[pos] !== '{') { return ''; }
56
- var ch;
57
- var res = '';
58
- var curlies = 0;
59
- while (pos < l) {
60
- ch = str[pos];
61
- curlies += ch === '{' ? 1 : ch === '}' ? -1 : 0;
62
- res += ch;
63
- pos ++;
64
- if (!curlies) {
65
- break;
66
- }
67
- }
68
- if (curlies !== 0) {
69
- // throw new Error('Unpaired curly in type doc');
70
- error = 'Unpaired curly in type doc';
71
- pos -= res.length;
72
- return '';
73
- }
74
- return res.substr(1, res.length - 2);
75
+
76
+ if (curlies !== 0) { throw new Error('Invalid `{type}`, unpaired curlies'); }
77
+
78
+ return {
79
+ source : str.slice(0, pos),
80
+ data : {type: res.slice(1, -1)}
81
+ };
82
+ };
83
+
84
+ PARSERS.parse_name = function parse_name(str, data) {
85
+ if (data.errors && data.errors.length) { return null; }
86
+
87
+ var pos = skipws(str);
88
+ var name = '';
89
+ var brackets = 0;
90
+
91
+ while (pos < str.length) {
92
+ brackets += (str[pos] === '[' ? 1 : (str[pos] === ']' ? -1 : 0));
93
+ name += str[pos];
94
+ pos ++;
95
+ if (brackets === 0 && /\s/.test(str[pos])) { break; }
75
96
  }
76
- function _name() { // (?:\s+(\S+))?
77
- if (error) { return ''; }
78
- _skipws();
79
- var ch;
80
- var res = '';
81
- var brackets = 0;
82
- var re = /\s/;
83
- while (pos < l) {
84
- ch = str[pos];
85
- brackets += ch === '[' ? 1 : ch === ']' ? -1 : 0;
86
- res += ch;
87
- pos ++;
88
- if (brackets === 0 && re.test(str[pos])) {
89
- break;
90
- }
91
- }
92
- if (brackets) {
93
- // throw new Error('Unpaired curly in type doc');
94
- error = 'Unpaired brackets in type doc';
95
- pos -= res.length;
96
- return '';
97
+
98
+ if (brackets !== 0) { throw new Error('Invalid `name`, unpaired brackets'); }
99
+
100
+ var res = {name: name, optional: false};
101
+
102
+ if (name[0] === '[' && name[name.length - 1] === ']') {
103
+ res.optional = true;
104
+ name = name.slice(1, -1);
105
+
106
+ if (name.indexOf('=') !== -1) {
107
+ var parts = name.split('=');
108
+ name = parts[0];
109
+ res.default = parts[1].replace(/^(["'])(.+)(\1)$/, '$2');
97
110
  }
98
- return res;
99
111
  }
100
- function _rest() { // (?:\s+([^$]+))?
101
- _skipws();
102
- return str.substr(pos);
112
+
113
+ res.name = name;
114
+
115
+ return {
116
+ source : str.slice(0, pos),
117
+ data : res
118
+ };
119
+ };
120
+
121
+ PARSERS.parse_description = function parse_description(str, data) {
122
+ if (data.errors && data.errors.length) { return null; }
123
+
124
+ var result = str.match(/^\s+([^$]+)?/);
125
+
126
+ if (result) {
127
+ return {
128
+ source : result[0],
129
+ data : {description: result[1] === undefined ? '' : result[1]}
130
+ };
103
131
  }
132
+
133
+ return null;
134
+ };
135
+
136
+ /* ------- parsing ------- */
137
+
138
+ /**
139
+ * Parses "@tag {type} name description"
140
+ * @param {string} str Raw doc string
141
+ * @param {Array[function]} parsers Array of parsers to be applied to the source
142
+ * @returns {object} parsed tag node
143
+ */
144
+ function parse_tag(str, parsers) {
145
+ if (typeof str !== 'string' || str[0] !== '@') { return null; }
146
+
147
+ var data = parsers.reduce(function(state, parser) {
148
+ var result;
149
+
150
+ try {
151
+ result = parser(state.source, merge({}, state.data));
152
+ // console.log('----------------');
153
+ // console.log(parser.name, ':', result);
154
+ } catch (err) {
155
+ // console.warn('Parser "%s" failed: %s', parser.name, err.message);
156
+ state.data.errors = (state.data.errors || [])
157
+ .concat(parser.name + ': ' + err.message);
158
+ }
159
+
160
+ if (result) {
161
+ state.source = state.source.slice(result.source.length);
162
+ state.data = merge(state.data, result.data);
163
+ }
164
+
165
+ return state;
166
+ }, {
167
+ source : str,
168
+ data : {}
169
+ }).data;
170
+
171
+ data.optional = !!data.optional;
172
+ data.type = data.type === undefined ? '' : data.type;
173
+ data.name = data.name === undefined ? '' : data.name;
174
+ data.description = data.description === undefined ? '' : data.description;
175
+
176
+ return data;
104
177
  }
105
178
 
106
- function parse_chunk(source, base_line_number, opts) {
179
+ /**
180
+ * Parses comment block (array of String lines)
181
+ */
182
+ function parse_block(source, opts) {
183
+
184
+ var source_str = source
185
+ .map(function(line) { return line.source; })
186
+ .join('\n')
187
+ .trim();
188
+
189
+ var start = source[0].number;
190
+
191
+ // merge source lines into tags
192
+ // we assume tag starts with "@"
107
193
  source = source
108
- .reduce(function(sections, line) {
109
- if (line.value === '' && line.line === base_line_number) return sections;
110
- if (line.value.match(/^@(\w+)/)) { sections.push([]); }
111
- var section = sections[sections.length - 1];
112
- section.line = 'line' in section ? section.line : line.line;
113
- section.push(line.value);
114
- return sections;
115
- }, [[]])
116
- .map(function(section) {
117
- return {value: section.length ? section.join('\n').trim() : null, line: section.line};
194
+ .reduce(function(tags, line) {
195
+ line.source = line.source.trim();
196
+
197
+ if (line.source.match(/^@(\w+)/)) {
198
+ tags.push({source: [line.source], line: line.number});
199
+ } else {
200
+ var tag = tags[tags.length - 1];
201
+ tag.source.push(line.source);
202
+ }
203
+
204
+ return tags;
205
+ }, [{source: []}])
206
+ .map(function(tag) {
207
+ tag.source = tag.source.join('\n').trim();
208
+ return tag;
118
209
  });
119
210
 
211
+ // Block description
120
212
  var description = source.shift();
121
213
 
122
- var tags = source.reduce(function(tags, tag) {
123
- var tag_node = parse_tag_line(tag.value);
124
- if (!tag_node) { return tags; }
125
-
126
- tag_node.line = Number(tag.line);
127
- if (opts.raw_value) {
128
- tag_node.value = tag.value;
129
- }
214
+ // skip if no descriptions and no tags
215
+ if (description.source === '' && source.length === 0) {
216
+ return null;
217
+ }
130
218
 
131
- // used for split results below
132
- var parts;
219
+ var tags = source.reduce(function(tags, tag) {
220
+ var tag_node = parse_tag(tag.source, opts.parsers || [
221
+ PARSERS.parse_tag,
222
+ PARSERS.parse_type,
223
+ PARSERS.parse_name,
224
+ PARSERS.parse_description
225
+ ]);
133
226
 
134
- // parsing optional and default value if exists
135
- // probably if should be hidden with option or moved out to some jsdoc standard
136
- if (tag_node.name[0] === '[' && tag_node.name[tag_node.name.length - 1] === ']') {
137
- tag_node.optional = true;
138
- tag_node.name = tag_node.name.substr(1, tag_node.name.length - 2);
227
+ if (!tag_node) { return tags; }
139
228
 
140
- // default value here
141
- if (tag_node.name.indexOf('=') !== -1) {
142
- parts = tag_node.name.split('=');
143
- tag_node.name = parts[0];
144
- tag_node.default = parts[1].replace(/^(["'])(.+)(\1)$/, '$2');
145
- }
146
- }
229
+ tag_node.line = tag.line;
230
+ tag_node.source = tag.source;
147
231
 
148
- // hidden with `dotted_names` parsing of `obj.value` naming standard
149
232
  if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
150
233
  var parent_name;
151
234
  var parent_tag;
152
235
  var parent_tags = tags;
153
- parts = tag_node.name.split('.');
236
+ var parts = tag_node.name.split('.');
154
237
 
155
238
  while (parts.length > 1) {
156
239
  parent_name = parts.shift();
157
- parent_tag = _find(parent_tags, {
240
+ parent_tag = find(parent_tags, {
158
241
  tag : tag_node.tag,
159
242
  name : parent_name
160
243
  });
@@ -181,37 +264,50 @@ function parse_chunk(source, base_line_number, opts) {
181
264
 
182
265
  return tags.concat(tag_node);
183
266
  }, []);
184
-
267
+
268
+ // console.log('-----------');
269
+ // console.log(description, tags);
270
+
185
271
  return {
186
272
  tags : tags,
187
- line : Number(description.line || 0),
188
- description : description.value || ''
273
+ line : start,
274
+ description : description.source,
275
+ source : source_str
189
276
  };
190
277
  }
191
278
 
279
+ /**
280
+ * Produces `extract` function with internal state initialized
281
+ */
192
282
  function mkextract(opts) {
283
+
193
284
  var chunk = null;
194
- var line_number = 0;
195
- var base_line_number = 0;
285
+ var number = 0;
196
286
 
287
+ /**
288
+ * Cumulatively reading lines until they make one comment block
289
+ * Returns block object or null.
290
+ */
197
291
  return function extract(line) {
198
- line_number += 1;
199
292
 
200
293
  // if oneliner
201
294
  // then parse it immediately
202
- if (!chunk && line.match(RE_COMMENT_1LINE)) {
203
- // console.log('line (1)', line, line_number);
295
+ if (line.match(RE_COMMENT_1LINE)) {
296
+ // console.log('line (1)', line);
204
297
  // console.log(' clean:', line.replace(RE_COMMENT_1LINE, '$1'));
205
- return parse_chunk([{value: line.replace(RE_COMMENT_1LINE, '$1'), line: line_number - 1}], line_number - 1, opts);
298
+ return parse_block([{
299
+ source: line.replace(RE_COMMENT_1LINE, '$1'),
300
+ number: number}], opts);
206
301
  }
207
302
 
303
+ number += 1;
304
+
208
305
  // if start of comment
209
306
  // then init the chunk
210
307
  if (line.match(RE_COMMENT_START)) {
211
308
  // console.log('line (1)', line);
212
309
  // console.log(' clean:', line.replace(RE_COMMENT_START, ''));
213
- base_line_number = line_number - 1;
214
- chunk = [{value: line.replace(RE_COMMENT_START, ''), line: line_number - 1}];
310
+ chunk = [{source: line.replace(RE_COMMENT_START, ''), number: number - 1}];
215
311
  return null;
216
312
  }
217
313
 
@@ -220,7 +316,7 @@ function mkextract(opts) {
220
316
  if (chunk && line.match(RE_COMMENT_LINE)) {
221
317
  // console.log('line (2)', line);
222
318
  // console.log(' clean:', line.replace(RE_COMMENT_LINE, ''));
223
- chunk.push({value: line.replace(RE_COMMENT_LINE, ''), line: line_number - 1});
319
+ chunk.push({source: line.replace(RE_COMMENT_LINE, ''), number: number - 1});
224
320
  return null;
225
321
  }
226
322
 
@@ -229,8 +325,8 @@ function mkextract(opts) {
229
325
  if (chunk && line.match(RE_COMMENT_END)) {
230
326
  // console.log('line (3)', line);
231
327
  // console.log(' clean:', line.replace(RE_COMMENT_END, ''));
232
- chunk.push({value: line.replace(RE_COMMENT_END, ''), line: line_number - 1});
233
- return parse_chunk(chunk, base_line_number, opts);
328
+ chunk.push({source: line.replace(RE_COMMENT_END, ''), number: number - 1});
329
+ return parse_block(chunk, opts);
234
330
  }
235
331
 
236
332
  // if non-comment line
@@ -239,7 +335,7 @@ function mkextract(opts) {
239
335
  };
240
336
  }
241
337
 
242
- /* ------- Transform strean ------- */
338
+ /* ------- Transform stream ------- */
243
339
 
244
340
  function Parser(opts) {
245
341
  opts = opts || {};
@@ -284,6 +380,8 @@ module.exports = function parse(source, opts) {
284
380
  return blocks;
285
381
  };
286
382
 
383
+ module.exports.PARSERS = PARSERS;
384
+
287
385
  module.exports.file = function file(file_path, done) {
288
386
 
289
387
  var collected = [];
@@ -304,19 +402,3 @@ module.exports.file = function file(file_path, done) {
304
402
  module.exports.stream = function stream(opts) {
305
403
  return new Parser(opts);
306
404
  };
307
-
308
- function _find(list, filter) {
309
- var i, l, k, yes, item;
310
- for (i = 0, l = list.length; i < l; i++) {
311
- item = list[i];
312
- yes = true;
313
- for (k in filter) {
314
- if (filter.hasOwnProperty(k)) {
315
- yes = yes && filter[k] === list[i][k];
316
- }
317
- }
318
- if (yes) {
319
- return item;
320
- }
321
- }
322
- }
package/package.json CHANGED
@@ -1,19 +1,22 @@
1
1
  {
2
2
  "name": "comment-parser",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Generic JSDoc-like comment parser. ",
5
5
  "main": "index.js",
6
6
  "directories": {
7
7
  "test": "tests"
8
8
  },
9
- "dependencies": {
10
- },
9
+ "dependencies": {},
11
10
  "devDependencies": {
11
+ "chai": "~1.9.0",
12
+ "jshint": "^2.5.10",
13
+ "jshint-stylish": "^1.0.0",
12
14
  "mocha": "~1.17.1",
13
- "chai": "~1.9.0"
15
+ "nodemon": "^1.2.1"
14
16
  },
15
17
  "scripts": {
16
- "test": "mocha tests/*"
18
+ "test": "jshint --reporter node_modules/jshint-stylish/stylish.js index.js && mocha tests/*",
19
+ "watch": "nodemon -q -w index.js -w tests/ -x npm test"
17
20
  },
18
21
  "repository": {
19
22
  "type": "git",