123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- var PARSERS = require('./parsers')
- var MARKER_START = '/**'
- var MARKER_START_SKIP = '/***'
- var MARKER_END = '*/'
- /* ------- util functions ------- */
- function merge (/* ...objects */) {
- var k, obj
- var res = {}
- var objs = Array.prototype.slice.call(arguments)
- for (var i = 0, l = objs.length; i < l; i++) {
- obj = objs[i]
- for (k in obj) {
- if (obj.hasOwnProperty(k)) {
- res[k] = obj[k]
- }
- }
- }
- return res
- }
- function find (list, filter) {
- var k
- var i = list.length
- var matchs = true
- while (i--) {
- for (k in filter) {
- if (filter.hasOwnProperty(k)) {
- matchs = (filter[k] === list[i][k]) && matchs
- }
- }
- if (matchs) { return list[i] }
- }
- return null
- }
- /* ------- parsing ------- */
- /**
- * Parses "@tag {type} name description"
- * @param {string} str Raw doc string
- * @param {Array[function]} parsers Array of parsers to be applied to the source
- * @returns {object} parsed tag node
- */
- function parse_tag (str, parsers) {
- if (typeof str !== 'string' || str[0] !== '@') { return null }
- var data = parsers.reduce(function (state, parser) {
- var result
- try {
- result = parser(state.source, merge({}, state.data))
- } catch (err) {
- state.data.errors = (state.data.errors || [])
- .concat(parser.name + ': ' + err.message)
- }
- if (result) {
- state.source = state.source.slice(result.source.length)
- state.data = merge(state.data, result.data)
- }
- return state
- }, {
- source: str,
- data: {}
- }).data
- data.optional = !!data.optional
- data.type = data.type === undefined ? '' : data.type
- data.name = data.name === undefined ? '' : data.name
- data.description = data.description === undefined ? '' : data.description
- return data
- }
- /**
- * Parses comment block (array of String lines)
- */
- function parse_block (source, opts) {
- var trim = opts.trim
- ? function trim (s) { return s.trim() }
- : function trim (s) { return s }
- var source_str = source
- .map(function (line) { return trim(line.source) })
- .join('\n')
- source_str = trim(source_str)
- var start = source[0].number
- // merge source lines into tags
- // we assume tag starts with "@"
- source = source
- .reduce(function (tags, line) {
- line.source = trim(line.source)
- if (line.source.match(/^\s*@(\S+)/)) {
- tags.push({source: [line.source], line: line.number})
- } else {
- var tag = tags[tags.length - 1]
- if (opts.join !== undefined && opts.join !== false && opts.join !== 0 &&
- !line.startWithStar && tag.source.length > 0) {
- var source
- if (typeof opts.join === 'string') {
- source = opts.join + line.source.replace(/^\s+/, '')
- } else if (typeof opts.join === 'number') {
- source = line.source
- } else {
- source = ' ' + line.source.replace(/^\s+/, '')
- }
- tag.source[tag.source.length - 1] += source
- } else {
- tag.source.push(line.source)
- }
- }
- return tags
- }, [{source: []}])
- .map(function (tag) {
- tag.source = trim(tag.source.join('\n'))
- return tag
- })
- // Block description
- var description = source.shift()
- // skip if no descriptions and no tags
- if (description.source === '' && source.length === 0) {
- return null
- }
- var tags = source.reduce(function (tags, tag) {
- var tag_node = parse_tag(tag.source, opts.parsers)
- if (!tag_node) { return tags }
- tag_node.line = tag.line
- tag_node.source = tag.source
- if (opts.dotted_names && tag_node.name.indexOf('.') !== -1) {
- var parent_name
- var parent_tag
- var parent_tags = tags
- var parts = tag_node.name.split('.')
- while (parts.length > 1) {
- parent_name = parts.shift()
- parent_tag = find(parent_tags, {
- tag: tag_node.tag,
- name: parent_name
- })
- if (!parent_tag) {
- parent_tag = {
- tag: tag_node.tag,
- line: Number(tag_node.line),
- name: parent_name,
- type: '',
- description: ''
- }
- parent_tags.push(parent_tag)
- }
- parent_tag.tags = parent_tag.tags || []
- parent_tags = parent_tag.tags
- }
- tag_node.name = parts[0]
- parent_tags.push(tag_node)
- return tags
- }
- return tags.concat(tag_node)
- }, [])
- return {
- tags: tags,
- line: start,
- description: description.source,
- source: source_str
- }
- }
- /**
- * Produces `extract` function with internal state initialized
- */
- function mkextract (opts) {
- var chunk = null
- var indent = 0
- var number = 0
- opts = merge({}, {
- trim: true,
- dotted_names: false,
- parsers: [
- PARSERS.parse_tag,
- PARSERS.parse_type,
- PARSERS.parse_name,
- PARSERS.parse_description
- ]
- }, opts || {})
- /**
- * Read lines until they make a block
- * Return parsed block once fullfilled or null otherwise
- */
- return function extract (line) {
- var result = null
- var startPos = line.indexOf(MARKER_START)
- var endPos = line.indexOf(MARKER_END)
- // if open marker detected and it's not skip one
- if (startPos !== -1 && line.indexOf(MARKER_START_SKIP) !== startPos) {
- chunk = []
- indent = startPos + MARKER_START.length
- }
- // if we are on middle of comment block
- if (chunk) {
- var lineStart = indent
- var startWithStar = false
- // figure out if we slice from opening marker pos
- // or line start is shifted to the left
- var nonSpaceChar = line.match(/\S/)
- // skip for the first line starting with /** (fresh chunk)
- // it always has the right indentation
- if (chunk.length > 0 && nonSpaceChar) {
- if (nonSpaceChar[0] === '*') {
- lineStart = nonSpaceChar.index + 2
- startWithStar = true
- } else if (nonSpaceChar.index < indent) {
- lineStart = nonSpaceChar.index
- }
- }
- // slice the line until end or until closing marker start
- chunk.push({
- number: number,
- startWithStar: startWithStar,
- source: line.slice(lineStart, endPos === -1 ? line.length : endPos)
- })
- // finalize block if end marker detected
- if (endPos !== -1) {
- result = parse_block(chunk, opts)
- chunk = null
- indent = 0
- }
- }
- number += 1
- return result
- }
- }
- /* ------- Public API ------- */
- module.exports = function parse (source, opts) {
- var block
- var blocks = []
- var extract = mkextract(opts)
- var lines = source.split(/\n/)
- for (var i = 0, l = lines.length; i < l; i++) {
- block = extract(lines.shift())
- if (block) {
- blocks.push(block)
- }
- }
- return blocks
- }
- module.exports.PARSERS = PARSERS
- module.exports.mkextract = mkextract
|