UNPKG

aws-delivlib

Version:

A fabulous library for defining continuous pipelines for building, testing and releasing code libraries.

202 lines (166 loc) • 5.63 kB
const EOL = require('os').EOL const lineReader = require('line-reader') const removeMarkdown = require('remove-markdown') // patterns const semver = /\[?v?([\w\d.-]+\.[\w\d.-]+[a-zA-Z0-9])\]?/ const date = /.*[ ](\d\d?\d?\d?[-/.]\d\d?[-/.]\d\d?\d?\d?).*/ const subhead = /^###/ const listitem = /^[*-]/ const defaultOptions = { removeMarkdown: true } /** * Changelog parser. * * @param {string|object} options - changelog file string or options object containing file string * @param {string} [options.filePath] - path to changelog file * @param {string} [options.text] - changelog text (filePath alternative) * @param {boolean} [options.removeMarkdown=true] - changelog file string to parse * @param {function} [callback] - optional callback * @returns {Promise<object>} - parsed changelog object */ function parseChangelog (options, callback) { if (typeof options === 'undefined') throw new Error('missing options argument') if (typeof options === 'string') options = { filePath: options } if (typeof options === 'object') { const hasFilePath = typeof options.filePath !== 'undefined' const hasText = typeof options.text !== 'undefined' const invalidFilePath = typeof options.filePath !== 'string' const invalidText = typeof options.text !== 'string' if (!hasFilePath && !hasText) { throw new Error('must provide filePath or text') } if (hasFilePath && invalidFilePath) { throw new Error('invalid filePath, expected string') } if (hasText && invalidText) { throw new Error('invalid text, expected string') } } const opts = Object.assign({}, defaultOptions, options) const changelog = parse(opts) if (typeof callback === 'function') { changelog .then(function (log) { callback(null, log) }) .catch(function (err) { callback(err) }) } // otherwise, invoke callback return changelog } /** * Internal parsing logic. * * @param {options} options - options object * @param {string} [options.filePath] - path to changelog file * @param {string} [options.text] - changelog text (filePath alternative) * @param {boolean} [options.removeMarkdown] - remove markdown * @returns {Promise<object>} - parsed changelog object */ function parse (options) { const filePath = options.filePath const text = options.text const data = { log: { versions: [] }, current: null } // allow `handleLine` to mutate log/current data as `this`. const cb = handleLine.bind(data, options) return new Promise(function (resolve, reject) { function done () { // push last version into log if (data.current) { pushCurrent(data) } // clean up description data.log.description = clean(data.log.description) if (data.log.description === '') delete data.log.description resolve(data.log) } if (text) { text.split(/\r\n?|\n/mg).forEach(cb) done() } else { lineReader.eachLine(filePath, cb, EOL).then(done) } }) } /** * Handles each line and mutates data object (bound to `this`) as needed. * * @param {object} options - options object * @param {boolean} options.removeMarkdown - whether or not to remove markdown * @param {string} line - line from changelog file */ function handleLine (options, line) { // skip line if it's a link label if (line.match(/^\[[^[\]]*\] *?:/)) return // set title if it's there if (!this.log.title && line.match(/^# ?[^#]/)) { this.log.title = line.substring(1).trim() return } // new version found! if (line.match(/^##? ?[^#]/)) { if (this.current && this.current.title) pushCurrent(this) this.current = versionFactory() if (semver.exec(line)) this.current.version = semver.exec(line)[1] this.current.title = line.substring(2).trim() if (this.current.title && date.exec(this.current.title)) this.current.date = date.exec(this.current.title)[1] return } // deal with body or description content if (this.current) { this.current.body += line + EOL // handle case where current line is a 'subhead': // - 'handleize' subhead. // - add subhead to 'parsed' data if not already present. if (subhead.exec(line)) { const key = line.replace('###', '').trim() if (!this.current.parsed[key]) { this.current.parsed[key] = [] this.current._private.activeSubhead = key } } // handle case where current line is a 'list item': if (listitem.exec(line)) { const log = options.removeMarkdown ? removeMarkdown(line) : line // add line to 'catch all' array this.current.parsed._.push(log) // add line to 'active subhead' if applicable (eg. 'Added', 'Changed', etc.) if (this.current._private.activeSubhead) { this.current.parsed[this.current._private.activeSubhead].push(log) } } } else { this.log.description = (this.log.description || '') + line + EOL } } function versionFactory () { return { version: null, title: null, date: null, body: '', parsed: { _: [] }, _private: { activeSubhead: null } } } function pushCurrent (data) { // remove private properties delete data.current._private data.current.body = clean(data.current.body) data.log.versions.push(data.current) } function clean (str) { if (!str) return '' // trim str = str.trim() // remove leading newlines str = str.replace(new RegExp('[' + EOL + ']*'), '') // remove trailing newlines str = str.replace(new RegExp('[' + EOL + ']*$'), '') return str } module.exports = parseChangelog