comment-parser 0.2.1 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # v0.2.3
2
+
3
+ - `bugfix` Accept `/** one line */` comments
4
+ - `refactor` Get rid of `lodash` to avoid unnecessary extra size when bundled
5
+
6
+ # v0.2.2
7
+
8
+ - `feature` allow spaces in default values `@my-tag {my.type} [name=John Doe]`
9
+
1
10
  # v0.2.1
2
11
 
3
12
  - `refactor` make line pasing mechanism more tolerable
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,135 +1,243 @@
1
1
 
2
+ 'use strict';
3
+
2
4
  var fs = require('fs');
3
5
  var stream = require('stream');
4
6
  var util = require('util');
5
7
 
6
- var _ = require('lodash');
7
-
8
8
  var RE_COMMENT_START = /^\s*\/\*\*\s*$/m;
9
9
  var RE_COMMENT_LINE = /^\s*\*(?:\s|$)/m;
10
10
  var RE_COMMENT_END = /^\s*\*\/\s*$/m;
11
+ var RE_COMMENT_1LINE = /^\s*\/\*\*\s*(.*)\s*\*\/\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 res = {
26
- tag : _tag(),
27
- type : _type() || '',
28
- name : _name() || '',
29
- description : _rest() || ''
30
- };
13
+ /* ------- util functions ------- */
31
14
 
32
- if (error) {
33
- 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
+ }}
34
22
  }
35
-
36
23
  return res;
24
+ }
37
25
 
38
- function _skipws() {
39
- while (str[pos] === ' ' && pos < l) { pos ++; }
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]; }
40
33
  }
41
- function _tag() { // @(\S+)
42
- var sp = str.indexOf(' ', pos);
43
- sp = sp < 0 ? l : sp;
44
- var res = str.substr(pos, sp - pos);
45
- pos = sp;
46
- 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; }
47
74
  }
48
- function _type() { // (?:\s+\{([^\}]+)\})?
49
- _skipws();
50
- if (str[pos] !== '{') { return ''; }
51
- var ch;
52
- var res = '';
53
- var curlies = 0;
54
- while (pos < l) {
55
- ch = str[pos];
56
- curlies += ch === '{' ? 1 : ch === '}' ? -1 : 0;
57
- res += ch;
58
- pos ++;
59
- if (!curlies) {
60
- break;
61
- }
62
- }
63
- if (curlies) {
64
- // throw new Error('Unpaired curly in type doc');
65
- error = 'Unpaired curly in type doc';
66
- pos -= res.length;
67
- return '';
68
- }
69
- 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; }
70
96
  }
71
- function _name() { // (?:\s+(\S+))?
72
- if (error) { return ''; }
73
- _skipws();
74
- return _tag();
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');
110
+ }
75
111
  }
76
- function _rest() { // (?:\s+([^$]+))?
77
- _skipws();
78
- 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
+ };
79
131
  }
80
- }
81
132
 
82
- function parse_chunk(source, opts) {
83
- source = source
84
- .reduce(function(sections, line) {
85
- if (line.value.match(/^@(\w+)/)) { sections.push([]); }
86
- var section = sections[sections.length - 1];
87
- section.line = section.line || line.line;
88
- section.push(line.value);
89
- return sections;
90
- }, [[]])
91
- .map(function(section) {
92
- return {value: section.join('\n').trim(), line: section.line};
93
- });
133
+ return null;
134
+ };
94
135
 
95
- var description = source[0].value.match(/^@(\S+)/) ? {value: '', line: 0} : source.shift();
136
+ /* ------- parsing ------- */
96
137
 
97
- var tags = source.reduce(function(tags, tag) {
98
- var tag_node = parse_tag_line(tag.value);
99
- if (!tag_node) { return tags; }
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
+ }
100
159
 
101
- tag_node.line = Number(tag.line);
102
- if (opts.raw_value) {
103
- tag_node.value = tag.value;
160
+ if (result) {
161
+ state.source = state.source.slice(result.source.length);
162
+ state.data = merge(state.data, result.data);
104
163
  }
105
164
 
106
- // used for split results below
107
- var parts;
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;
177
+ }
178
+
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();
108
188
 
109
- // parsing optional and default value if exists
110
- // probably if should be hidden with option or moved out to some jsdoc standard
111
- if (tag_node.name.match(/^\[(\S+)\]$/)) {
112
- tag_node.optional = true;
113
- tag_node.name = RegExp.$1;
189
+ var start = source[0].number;
114
190
 
115
- // default value here
116
- if (tag_node.name.indexOf('=') !== -1) {
117
- parts = tag_node.name.split('=');
118
- tag_node.name = parts[0];
119
- tag_node.default = parts[1];
191
+ // merge source lines into tags
192
+ // we assume tag starts with "@"
193
+ source = source
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);
120
202
  }
121
- }
122
203
 
123
- // hidden with `dotted_names` parsing of `obj.value` naming standard
204
+ return tags;
205
+ }, [{source: []}])
206
+ .map(function(tag) {
207
+ tag.source = tag.source.join('\n').trim();
208
+ return tag;
209
+ });
210
+
211
+ // Block description
212
+ var description = source.shift();
213
+
214
+ // skip if no descriptions and no tags
215
+ if (description.source === '' && source.length === 0) {
216
+ return null;
217
+ }
218
+
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
+ ]);
226
+
227
+ if (!tag_node) { return tags; }
228
+
229
+ tag_node.line = tag.line;
230
+ tag_node.source = tag.source;
231
+
124
232
  if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
125
233
  var parent_name;
126
234
  var parent_tag;
127
235
  var parent_tags = tags;
128
- parts = tag_node.name.split('.');
236
+ var parts = tag_node.name.split('.');
129
237
 
130
238
  while (parts.length > 1) {
131
239
  parent_name = parts.shift();
132
- parent_tag = _.findWhere(parent_tags, {
240
+ parent_tag = find(parent_tags, {
133
241
  tag : tag_node.tag,
134
242
  name : parent_name
135
243
  });
@@ -156,28 +264,50 @@ function parse_chunk(source, opts) {
156
264
 
157
265
  return tags.concat(tag_node);
158
266
  }, []);
159
-
267
+
268
+ // console.log('-----------');
269
+ // console.log(description, tags);
270
+
160
271
  return {
161
272
  tags : tags,
162
- line : Number(description.line),
163
- description : description.value
273
+ line : start,
274
+ description : description.source,
275
+ source : source_str
164
276
  };
165
277
  }
166
278
 
279
+ /**
280
+ * Produces `extract` function with internal state initialized
281
+ */
167
282
  function mkextract(opts) {
168
283
 
169
284
  var chunk = null;
170
- var line_number = 0;
285
+ var number = 0;
171
286
 
287
+ /**
288
+ * Cumulatively reading lines until they make one comment block
289
+ * Returns block object or null.
290
+ */
172
291
  return function extract(line) {
173
- line_number += 1;
292
+
293
+ // if oneliner
294
+ // then parse it immediately
295
+ if (line.match(RE_COMMENT_1LINE)) {
296
+ // console.log('line (1)', line);
297
+ // console.log(' clean:', line.replace(RE_COMMENT_1LINE, '$1'));
298
+ return parse_block([{
299
+ source: line.replace(RE_COMMENT_1LINE, '$1'),
300
+ number: number}], opts);
301
+ }
302
+
303
+ number += 1;
174
304
 
175
305
  // if start of comment
176
306
  // then init the chunk
177
307
  if (line.match(RE_COMMENT_START)) {
178
308
  // console.log('line (1)', line);
179
309
  // console.log(' clean:', line.replace(RE_COMMENT_START, ''));
180
- chunk = [{value: line.replace(RE_COMMENT_START, ''), line: line_number - 1}];
310
+ chunk = [{source: line.replace(RE_COMMENT_START, ''), number: number - 1}];
181
311
  return null;
182
312
  }
183
313
 
@@ -186,7 +316,7 @@ function mkextract(opts) {
186
316
  if (chunk && line.match(RE_COMMENT_LINE)) {
187
317
  // console.log('line (2)', line);
188
318
  // console.log(' clean:', line.replace(RE_COMMENT_LINE, ''));
189
- chunk.push({value: line.replace(RE_COMMENT_LINE, ''), line: line_number - 1});
319
+ chunk.push({source: line.replace(RE_COMMENT_LINE, ''), number: number - 1});
190
320
  return null;
191
321
  }
192
322
 
@@ -195,18 +325,17 @@ function mkextract(opts) {
195
325
  if (chunk && line.match(RE_COMMENT_END)) {
196
326
  // console.log('line (3)', line);
197
327
  // console.log(' clean:', line.replace(RE_COMMENT_END, ''));
198
- chunk.push({value: line.replace(RE_COMMENT_END, ''), line: line_number - 1});
199
- return parse_chunk(chunk, opts);
328
+ chunk.push({source: line.replace(RE_COMMENT_END, ''), number: number - 1});
329
+ return parse_block(chunk, opts);
200
330
  }
201
331
 
202
332
  // if non-comment line
203
333
  // then reset the chunk
204
334
  chunk = null;
205
- line_number = 0;
206
335
  };
207
336
  }
208
337
 
209
- /* ------- Transform strean ------- */
338
+ /* ------- Transform stream ------- */
210
339
 
211
340
  function Parser(opts) {
212
341
  opts = opts || {};
@@ -251,6 +380,8 @@ module.exports = function parse(source, opts) {
251
380
  return blocks;
252
381
  };
253
382
 
383
+ module.exports.PARSERS = PARSERS;
384
+
254
385
  module.exports.file = function file(file_path, done) {
255
386
 
256
387
  var collected = [];
package/package.json CHANGED
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "comment-parser",
3
- "version": "0.2.1",
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
- "lodash": "~2.4.1"
11
- },
9
+ "dependencies": {},
12
10
  "devDependencies": {
11
+ "chai": "~1.9.0",
12
+ "jshint": "^2.5.10",
13
+ "jshint-stylish": "^1.0.0",
13
14
  "mocha": "~1.17.1",
14
- "chai": "~1.9.0"
15
+ "nodemon": "^1.2.1"
15
16
  },
16
17
  "scripts": {
17
- "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"
18
20
  },
19
21
  "repository": {
20
22
  "type": "git",