parser.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. var PARSERS = require('./parsers')
  2. var MARKER_START = '/**'
  3. var MARKER_START_SKIP = '/***'
  4. var MARKER_END = '*/'
  5. /* ------- util functions ------- */
  6. function merge (/* ...objects */) {
  7. var k, obj
  8. var res = {}
  9. var objs = Array.prototype.slice.call(arguments)
  10. for (var i = 0, l = objs.length; i < l; i++) {
  11. obj = objs[i]
  12. for (k in obj) {
  13. if (obj.hasOwnProperty(k)) {
  14. res[k] = obj[k]
  15. }
  16. }
  17. }
  18. return res
  19. }
  20. function find (list, filter) {
  21. var k
  22. var i = list.length
  23. var matchs = true
  24. while (i--) {
  25. for (k in filter) {
  26. if (filter.hasOwnProperty(k)) {
  27. matchs = (filter[k] === list[i][k]) && matchs
  28. }
  29. }
  30. if (matchs) { return list[i] }
  31. }
  32. return null
  33. }
  34. /* ------- parsing ------- */
  35. /**
  36. * Parses "@tag {type} name description"
  37. * @param {string} str Raw doc string
  38. * @param {Array[function]} parsers Array of parsers to be applied to the source
  39. * @returns {object} parsed tag node
  40. */
  41. function parse_tag (str, parsers) {
  42. if (typeof str !== 'string' || str[0] !== '@') { return null }
  43. var data = parsers.reduce(function (state, parser) {
  44. var result
  45. try {
  46. result = parser(state.source, merge({}, state.data))
  47. } catch (err) {
  48. state.data.errors = (state.data.errors || [])
  49. .concat(parser.name + ': ' + err.message)
  50. }
  51. if (result) {
  52. state.source = state.source.slice(result.source.length)
  53. state.data = merge(state.data, result.data)
  54. }
  55. return state
  56. }, {
  57. source: str,
  58. data: {}
  59. }).data
  60. data.optional = !!data.optional
  61. data.type = data.type === undefined ? '' : data.type
  62. data.name = data.name === undefined ? '' : data.name
  63. data.description = data.description === undefined ? '' : data.description
  64. return data
  65. }
  66. /**
  67. * Parses comment block (array of String lines)
  68. */
  69. function parse_block (source, opts) {
  70. var trim = opts.trim
  71. ? function trim (s) { return s.trim() }
  72. : function trim (s) { return s }
  73. var source_str = source
  74. .map(function (line) { return trim(line.source) })
  75. .join('\n')
  76. source_str = trim(source_str)
  77. var start = source[0].number
  78. // merge source lines into tags
  79. // we assume tag starts with "@"
  80. source = source
  81. .reduce(function (tags, line) {
  82. line.source = trim(line.source)
  83. if (line.source.match(/^\s*@(\S+)/)) {
  84. tags.push({source: [line.source], line: line.number})
  85. } else {
  86. var tag = tags[tags.length - 1]
  87. if (opts.join !== undefined && opts.join !== false && opts.join !== 0 &&
  88. !line.startWithStar && tag.source.length > 0) {
  89. var source
  90. if (typeof opts.join === 'string') {
  91. source = opts.join + line.source.replace(/^\s+/, '')
  92. } else if (typeof opts.join === 'number') {
  93. source = line.source
  94. } else {
  95. source = ' ' + line.source.replace(/^\s+/, '')
  96. }
  97. tag.source[tag.source.length - 1] += source
  98. } else {
  99. tag.source.push(line.source)
  100. }
  101. }
  102. return tags
  103. }, [{source: []}])
  104. .map(function (tag) {
  105. tag.source = trim(tag.source.join('\n'))
  106. return tag
  107. })
  108. // Block description
  109. var description = source.shift()
  110. // skip if no descriptions and no tags
  111. if (description.source === '' && source.length === 0) {
  112. return null
  113. }
  114. var tags = source.reduce(function (tags, tag) {
  115. var tag_node = parse_tag(tag.source, opts.parsers)
  116. if (!tag_node) { return tags }
  117. tag_node.line = tag.line
  118. tag_node.source = tag.source
  119. if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
  120. var parent_name
  121. var parent_tag
  122. var parent_tags = tags
  123. var parts = tag_node.name.split('.')
  124. while (parts.length > 1) {
  125. parent_name = parts.shift()
  126. parent_tag = find(parent_tags, {
  127. tag: tag_node.tag,
  128. name: parent_name
  129. })
  130. if (!parent_tag) {
  131. parent_tag = {
  132. tag: tag_node.tag,
  133. line: Number(tag_node.line),
  134. name: parent_name,
  135. type: '',
  136. description: ''
  137. }
  138. parent_tags.push(parent_tag)
  139. }
  140. parent_tag.tags = parent_tag.tags || []
  141. parent_tags = parent_tag.tags
  142. }
  143. tag_node.name = parts[0]
  144. parent_tags.push(tag_node)
  145. return tags
  146. }
  147. return tags.concat(tag_node)
  148. }, [])
  149. return {
  150. tags: tags,
  151. line: start,
  152. description: description.source,
  153. source: source_str
  154. }
  155. }
  156. /**
  157. * Produces `extract` function with internal state initialized
  158. */
  159. function mkextract (opts) {
  160. var chunk = null
  161. var indent = 0
  162. var number = 0
  163. opts = merge({}, {
  164. trim: true,
  165. dotted_names: false,
  166. parsers: [
  167. PARSERS.parse_tag,
  168. PARSERS.parse_type,
  169. PARSERS.parse_name,
  170. PARSERS.parse_description
  171. ]
  172. }, opts || {})
  173. /**
  174. * Read lines until they make a block
  175. * Return parsed block once fullfilled or null otherwise
  176. */
  177. return function extract (line) {
  178. var result = null
  179. var startPos = line.indexOf(MARKER_START)
  180. var endPos = line.indexOf(MARKER_END)
  181. // if open marker detected and it's not skip one
  182. if (startPos !== -1 && line.indexOf(MARKER_START_SKIP) !== startPos) {
  183. chunk = []
  184. indent = startPos + MARKER_START.length
  185. }
  186. // if we are on middle of comment block
  187. if (chunk) {
  188. var lineStart = indent
  189. var startWithStar = false
  190. // figure out if we slice from opening marker pos
  191. // or line start is shifted to the left
  192. var nonSpaceChar = line.match(/\S/)
  193. // skip for the first line starting with /** (fresh chunk)
  194. // it always has the right indentation
  195. if (chunk.length > 0 && nonSpaceChar) {
  196. if (nonSpaceChar[0] === '*') {
  197. lineStart = nonSpaceChar.index + 2
  198. startWithStar = true
  199. } else if (nonSpaceChar.index < indent) {
  200. lineStart = nonSpaceChar.index
  201. }
  202. }
  203. // slice the line until end or until closing marker start
  204. chunk.push({
  205. number: number,
  206. startWithStar: startWithStar,
  207. source: line.slice(lineStart, endPos === -1 ? line.length : endPos)
  208. })
  209. // finalize block if end marker detected
  210. if (endPos !== -1) {
  211. result = parse_block(chunk, opts)
  212. chunk = null
  213. indent = 0
  214. }
  215. }
  216. number += 1
  217. return result
  218. }
  219. }
  220. /* ------- Public API ------- */
  221. module.exports = function parse (source, opts) {
  222. var block
  223. var blocks = []
  224. var extract = mkextract(opts)
  225. var lines = source.split(/\n/)
  226. for (var i = 0, l = lines.length; i < l; i++) {
  227. block = extract(lines.shift())
  228. if (block) {
  229. blocks.push(block)
  230. }
  231. }
  232. return blocks
  233. }
  234. module.exports.PARSERS = PARSERS
  235. module.exports.mkextract = mkextract