From 10a90b032a26c32938bb23d9819102d40c83caed Mon Sep 17 00:00:00 2001 From: Ryan Fitzer Date: Tue, 23 Nov 2021 17:17:24 -0800 Subject: [PATCH] Fix: Fixes #2. --- .github/workflows/main.yml | 29 +++ .gitignore | 1 - .npmignore | 7 - .npmrc | 1 + LICENSE | 25 +-- README.md | 208 +++++++++-------- examples/custom-parsers/parsers.js | 8 +- runkit.config.js => examples/runkit.config.js | 2 +- package.json | 29 ++- src/index.js | 210 +++++++++--------- 10 files changed, 281 insertions(+), 239 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .npmignore create mode 100644 .npmrc rename runkit.config.js => examples/runkit.config.js (97%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..2cdeabb --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,29 @@ +name: CI + +on: + - push + - pull_request + +jobs: + test: + name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + node-version: + - 18 + - 16 + - 14 + - 7 + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index c2bace0..6f71afd 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .DS_Store node_modules -.tm_properties package-lock.json diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 801cbfb..0000000 --- a/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -test -examples -node_modules -.travis.yml -.eslintrc.js -.prettierignore -.prettierrc.json \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8fffc26..b101c9e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,9 @@ -Copyright (c) 2015 Ryan Fitzer +MIT License -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: +Copyright (c) Ryan Fitzer (ryanfitzer.com) -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index d06b654..7508e8f 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# Comment Serializer # +# Comment Serializer [![NPM version](https://badge.fury.io/js/comment-serializer.svg)](https://www.npmjs.com/package/comment-serializer) [![Try on RunKit](https://badge.runkitcdn.com/comment-serializer.svg)](https://runkit.com/npm/comment-serializer) -Comment Serializer parses a source string for documentation comment blocks and returns a serialized object. It is language and comment syntax style agnostic. It configured to support most documentation comment block styles. +Comment Serializer parses a source string for documentation comment blocks and returns a serialized object. It is language and comment syntax style agnostic. It configured to support most documentation comment block styles. Try out an [example on RunKit](https://runkit.com/npm/comment-serializer). - -## Usage ## +## Usage **source-code.txt**: + ```txt //! // This is the general description. @@ -22,31 +22,31 @@ Blah, blah, blah... Some code... ``` **my-serializer.js** + ```js -const { readFileSync } = require( 'fs' ); -const serializer = require( 'comment-serializer' ); +const { readFileSync } = require('fs'); +const serializer = require('comment-serializer'); -const src = readFileSync( 'source-code.txt', { encoding: 'utf8' } ); +const src = readFileSync('source-code.txt', { encoding: 'utf8' }); const mySerializer = serializer({ tokens: { commentBegin: '//!', commentLinePrefix: '//', tagPrefix: '^', - commentEnd: '///' - } + commentEnd: '///', + }, }); -const result = mySerializer( src ); +const result = mySerializer(src); ``` +## Options -## Options ## +### `options.parsers` -### `options.parsers` ### - - - Required: No - - Type: `object` +- Required: No +- Type: `object` Custom tag parsers. An object of functions, keyed by the name of the tag to be parsed. The function receives the tag's content and must return the parsed value as a `string`. @@ -54,7 +54,7 @@ When no parsers are provided, tags are serialized into `tag` and `value` pairs, You can find some examples of custom parsers in [`examples/custom-parsers/parsers.js`](examples/custom-parsers/parsers.js) -#### Example #### +#### Example Source to parse: @@ -69,29 +69,32 @@ The custom parser: ```js const mySerializer = serializer({ parsers: { - mySpecialTag: ( value ) => value.toUpperCase() - } + mySpecialTag: (value) => value.toUpperCase(), + }, }); ``` Result: ```js -[{ - line: 1, - source: '/**\n * @mySpecialTag This value is special!\n */', - context: '', - content: '@mySpecialTag This value is special!', - preface: '', - tags: [{ - line: 2, - tag: 'mySpecialTag', - value: 'This value is special!', - valueParsed: 'THIS VALUE IS SPECIAL!', - source: '@mySpecialTag This value is special!' - }] -}] - +[ + { + line: 1, + source: '/**\n * @mySpecialTag This value is special!\n */', + context: '', + content: '@mySpecialTag This value is special!', + preface: '', + tags: [ + { + line: 2, + tag: 'mySpecialTag', + value: 'This value is special!', + valueParsed: 'THIS VALUE IS SPECIAL!', + source: '@mySpecialTag This value is special!', + }, + ], + }, +]; ``` A more advanced example: @@ -108,89 +111,106 @@ A more advanced example: ```js const mySerializer = serializer({ parsers: { - 'mySpecialTag': ( value ) => { - - const match = value.match( /\s*[-*]\s+[\w-_]*/g ); - + mySpecialTag: (value) => { + const match = value.match(/\s*[-*]\s+[\w-_]*/g); + return [ { type: 'items', - value: match.map( item => item.trim().replace( /[-*]\s/, '' ) ) - } + value: match.map((item) => item.trim().replace(/[-*]\s/, '')), + }, ]; - } - } + }, + }, }); ``` Result: ```js -[{ - line: 1, - source: '/**\n * @mySpecialTag\n * - item-1\n * - item-2\n * - item-3\n */', - context: '', - content: '@mySpecialTag\n - item-1\n - item-2\n - item-3', - preface: '', - tags: [{ - line: 2, - tag: 'mySpecialTag', - value: '\n - item-1\n - item-2\n - item-3', - valueParsed: [{ - type: 'items', - value: ['item-1', 'item-2', 'item-3'] - }], - source: '@mySpecialTag\n - item-1\n - item-2\n - item-3' - }] -}] +[ + { + line: 1, + source: + '/**\n * @mySpecialTag\n * - item-1\n * - item-2\n * - item-3\n */', + context: '', + content: '@mySpecialTag\n - item-1\n - item-2\n - item-3', + preface: '', + tags: [ + { + line: 2, + tag: 'mySpecialTag', + value: '\n - item-1\n - item-2\n - item-3', + valueParsed: [ + { + type: 'items', + value: ['item-1', 'item-2', 'item-3'], + }, + ], + source: '@mySpecialTag\n - item-1\n - item-2\n - item-3', + }, + ], + }, +]; ``` -### `options.tokens` ### +##### Tag Parsing Errors - - Required: No - - Type: `object` +Errors that occur while parsing a tag's value are caught. When this happens, tag's the `valueParsed` property will be an empty `array`, and the error object is added to the `error` property. -Customize the comment delimiters. The default tokens use [JSDoc comment block](https://google.github.io/styleguide/jsguide.html#jsdoc) syntax. +Example: + +```js +{ + line: 2, + tag: 'mySpecialTag', + value: '\n - item-1\n - item-2\n - item-3', + valueParsed: [], + source: '@mySpecialTag\n - item-1\n - item-2\n - item-3', + error: { Error } +} +``` +### `options.tokens` -### `options.tokens.commentBegin` ### +- Required: No +- Type: `object` - - Required: No - - Type: `string` - - Default: `'/**'` +Customize the comment delimiters. The default tokens use [JSDoc comment block](https://google.github.io/styleguide/jsguide.html#jsdoc) syntax. -The delimiter marking the beginning of a comment block. +### `options.tokens.commentBegin` +- Required: No +- Type: `string` +- Default: `'/**'` -### `options.tokens.commentLinePrefix` ### +The delimiter marking the beginning of a comment block. - - Required: No - - Type: `string` - - Default: `'*'` +### `options.tokens.commentLinePrefix` -The delimiter marking a new line in the body of a comment block. +- Required: No +- Type: `string` +- Default: `'*'` +The delimiter marking a new line in the body of a comment block. -### `options.tokens.tagPrefix` ### +### `options.tokens.tagPrefix` - - Required: No - - Type: `string` - - Default: `'@'` +- Required: No +- Type: `string` +- Default: `'@'` The delimiter marking the start of a tag in the comment body. +### `options.tokens.commentEnd` -### `options.tokens.commentEnd` ### - - - Required: No - - Type: `string` - - Default: `'*/'` +- Required: No +- Type: `string` +- Default: `'*/'` The delimiter marking the end of a comment block. - - -## Token Syntax Support ## +## Token Syntax Support Comment delimiters: @@ -199,18 +219,18 @@ Comment delimiters: * <- commentLinePrefix * @tag <- tagPrefix (the "@" symbol) */ <- commentEnd - ``` +``` While most documentation comment styles should be supported, there are a few rules around the syntax: - 1. The `commentBegin`, `commentLinePrefix`, `commentEnd` and `tagPrefix` delimiter tokens must be distinct from each other. +1. The `commentBegin`, `commentLinePrefix`, `commentEnd` and `tagPrefix` delimiter tokens must be distinct from each other. + +2. Delimiters should not rely on a whitespace character as a delimiter. For example, the following style would not be supported: - 2. Delimiters should not rely on a whitespace character as a delimiter. For example, the following style would not be supported: +``` +/** + An unsupported style. - ``` - /** - An unsupported style. - - @tag - */ - ``` + @tag + */ +``` diff --git a/examples/custom-parsers/parsers.js b/examples/custom-parsers/parsers.js index 3a07faf..c11dc9a 100644 --- a/examples/custom-parsers/parsers.js +++ b/examples/custom-parsers/parsers.js @@ -5,7 +5,7 @@ module.exports = { * @usage `@state :hover This is a description`. * * @param {string} value The tag's value to parse. - * @returns {array} + * @return {array} */ state: (value) => { const match = value.match(/(^:[-_a-zA-Z]+)(.*)/); @@ -34,7 +34,7 @@ module.exports = { * @usage `@modifier ._some_class This is a description`. * * @param {string} value The tag's value to parse. - * @returns {array} + * @return {array} */ modifier: (value) => { const match = value.match(/^(\.?-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(.*)/); @@ -71,7 +71,7 @@ module.exports = { * ` * * @param {string} value The tag's value to parse. - * @returns {array} + * @return {array} */ example: (value) => { const match = value.match(/([^\n]*)\n((?:.|\n)*)/); @@ -108,7 +108,7 @@ module.exports = { * * tag-3` * * @param {string} value The tag's value to parse. - * @returns {array} + * @return {array} */ tags: (value) => { const match = value.match(/\s*[-*]\s+[\w-_]*/g); diff --git a/runkit.config.js b/examples/runkit.config.js similarity index 97% rename from runkit.config.js rename to examples/runkit.config.js index d95db61..9db4bae 100644 --- a/runkit.config.js +++ b/examples/runkit.config.js @@ -1,4 +1,4 @@ -const createSerializer = require('./src'); +const createSerializer = require('../src'); const src = ` //! diff --git a/package.json b/package.json index 0940c1c..2cfe726 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,31 @@ { "name": "comment-serializer", "version": "2.0.0", - "author": "Ryan Fitzer", "description": "A serializer for syntax-configurable source code documentation block comments.", "license": "MIT", - "main": "src/index.js", - "repository": { - "type": "git", - "url": "https://github.com/ryanfitzer/comment-serializer.git" + "author": { + "name": "Ryan Fitzer", + "email": "ryan@ryanfitzer.com", + "url": "http://ryanfitzer.com" }, + "main": "src/index.js", + "repository": "ryanfitzer/comment-serializer", "engines": { "node": ">=7.0.0" }, "scripts": { "test": "mocha" }, + "files": [ + "src" + ], + "devDependencies": { + "eslint": "^8.3.0", + "eslint-config-prettier": "^8.3.0", + "mocha": "^9.1.3" + }, + "runkitExampleFilename": "examples/runkit.config.js", + "sideEffects": false, "keywords": [ "comment", "comments", @@ -31,11 +42,5 @@ "cssdoc", "htmldoc", "customdoc" - ], - "devDependencies": { - "eslint": "^8.3.0", - "eslint-config-prettier": "^8.3.0", - "mocha": "^9.1.3" - }, - "runkitExampleFilename": "runkit.config.js" + ] } diff --git a/src/index.js b/src/index.js index 5378838..984b462 100644 --- a/src/index.js +++ b/src/index.js @@ -2,28 +2,44 @@ * Creates a configured serializer function. * * @param {object} [options] The configuration object. - * @returns {function} The configured serializer. + * @return {function} The configured serializer. */ -module.exports = ( options = {} ) => { - - const patterns = Object.assign({ - commentBegin: '/**', - commentEnd: '*/', - commentLinePrefix: '*', - tagPrefix: '@' - }, options.tokens || {} ); +module.exports = (options = {}) => { + const patterns = Object.assign( + { + commentBegin: '/**', + commentEnd: '*/', + commentLinePrefix: '*', + tagPrefix: '@', + }, + options.tokens || {} + ); const rCharacterClasses = /([\].|*?+(){}^$\\:=[])/g; // Example: https://github.com/VerbalExpressions/JSVerbalExpressions/blob/master/VerbalExpressions.js#L63 const lastMatch = '\\$&'; // Last match, URL: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastMatch - const safeTagPrefix = patterns.tagPrefix.replace( rCharacterClasses, lastMatch ); - const safeCommentBegin = patterns.commentBegin.replace( rCharacterClasses, lastMatch ); - const safeCommentLinePrefix = patterns.commentLinePrefix.replace( rCharacterClasses, lastMatch ); - const safeCommentEnd = patterns.commentEnd.replace( rCharacterClasses, lastMatch ); + const safeTagPrefix = patterns.tagPrefix.replace( + rCharacterClasses, + lastMatch + ); + const safeCommentBegin = patterns.commentBegin.replace( + rCharacterClasses, + lastMatch + ); + const safeCommentLinePrefix = patterns.commentLinePrefix.replace( + rCharacterClasses, + lastMatch + ); + const safeCommentEnd = patterns.commentEnd.replace( + rCharacterClasses, + lastMatch + ); const rLeadSpaces = /^[^\S\n]*/; - const rComment = new RegExp( `(${safeCommentBegin}\\s*\\n\\s*${safeCommentLinePrefix}(?:.|\\n)*?${safeCommentEnd}\\s*\\n?)` ); - const rCommentLinePrefix = new RegExp( `^(\\s)*${safeCommentLinePrefix}` ); - const rTagName = new RegExp( `^${safeTagPrefix}([\\w-])+` ); + const rComment = new RegExp( + `(${safeCommentBegin}\\s*\\n\\s*${safeCommentLinePrefix}(?:.|\\n)*?${safeCommentEnd}\\s*\\n?)` + ); + const rCommentLinePrefix = new RegExp(`^(\\s)*${safeCommentLinePrefix}`); + const rTagName = new RegExp(`^${safeTagPrefix}([\\w-])+`); const parsers = options.parsers || {}; /** @@ -34,33 +50,28 @@ module.exports = ( options = {} ) => { * - context: source between the `commentEnd` token and next `commentBegin` token (or EOF). * * @param {string} sourceStr The source to parse. - * @returns {Array} + * @return {array} */ - const explodeSections = ( sourceStr ) => { - - const sections = sourceStr.split( rComment ); - let prevSectionLineLength = getLinesLength( sections.shift() ); - - return sections.reduce( function ( accum, section, index ) { + const explodeSections = (sourceStr) => { + const sections = sourceStr.split(rComment); + let prevSectionLineLength = getLinesLength(sections.shift()); + return sections.reduce(function (accum, section, index) { // Group each comment with its context - if ( index % 2 ) { - - accum[ accum.length - 1 ].context = section.trimRight(); - } - else { + if (index % 2) { + accum[accum.length - 1].context = section.trimRight(); + } else { accum.push({ line: prevSectionLineLength + 1, - source: section.trim() + source: section.trim(), }); } - prevSectionLineLength += getLinesLength( section ); + prevSectionLineLength += getLinesLength(section); return accum; - - }, [] ); - } + }, []); + }; /** * Strip and serialize a comment into its various parts. @@ -71,129 +82,126 @@ module.exports = ( options = {} ) => { * * @param {number} lineNumber The comment's starting line number. * @param {string} sourceStr The comment's source. - * @returns {Object} + * @return {object} */ - const stripAndSerializeComment = ( lineNumber, sourceStr ) => { - + const stripAndSerializeComment = (lineNumber, sourceStr) => { // Strip comment delimiter tokens let stripped = sourceStr - .replace( patterns.commentBegin, '' ) - .replace( patterns.commentEnd, '' ) - .split( '\n' ) - .map( line => line.replace( rCommentLinePrefix, '' ) ); + .replace(patterns.commentBegin, '') + .replace(patterns.commentEnd, '') + .split('\n') + .map((line) => line.replace(rCommentLinePrefix, '')); // Determine the number of leading spaces to strip - const prefixSpaces = stripped.reduce( function ( accum, line ) { - - if ( !accum.length && line.match( /\s*\S|\n/ ) ) { - accum = line.match( /\s*/ )[0]; + const prefixSpaces = stripped.reduce(function (accum, line) { + if (!accum.length && line.match(/\s*\S|\n/)) { + accum = line.match(/\s*/)[0]; } return accum; }); // Strip leading spaces - stripped = stripped.map( line => line.replace( prefixSpaces, '' ) ); + stripped = stripped.map((line) => line.replace(prefixSpaces, '')); // Get line number for first tag - const firstTagLineNumber = stripped.reduce( function ( accum, line, index ) { - - if ( isNaN( accum ) && line.match( rTagName ) ) { + const firstTagLineNumber = stripped.reduce(function ( + accum, + line, + index + ) { + if (isNaN(accum) && line.match(rTagName)) { accum = index; } return accum; + }, + undefined); - }, undefined ); - - const comment = stripped.join( '\n' ).trim(); - const tags = stripped.splice( firstTagLineNumber ).join( '\n' ); - const preface = stripped.join( '\n' ).trim(); + const comment = stripped.join('\n').trim(); + const tags = stripped.splice(firstTagLineNumber).join('\n'); + const preface = stripped.join('\n').trim(); return { preface, content: comment, - tags: serializeTags( lineNumber + firstTagLineNumber, tags ) + tags: serializeTags(lineNumber + firstTagLineNumber, tags), }; - } + }; /** * Takes a tags block and serializes it into individual tag objects. * * @param {number} lineNumber The tags block starting line number. * @param {string} tags The tags block. - * @returns {Array} + * @return {array} */ - const serializeTags = ( lineNumber, tags ) => { - - return tags.split( /\n/ ) - .reduce( function ( acc, line, index ) { - - if ( !index || rTagName.test( line ) ) { - acc.push( `${line}\n` ); - } - else { - acc[ acc.length - 1 ] += `${line}\n`; + const serializeTags = (lineNumber, tags) => { + return tags + .split(/\n/) + .reduce(function (acc, line, index) { + if (!index || rTagName.test(line)) { + acc.push(`${line}\n`); + } else { + acc[acc.length - 1] += `${line}\n`; } return acc; - - }, [] ) - .map( function ( block ) { - + }, []) + .map(function (block) { const trimmed = block.trim(); - const tag = block.match( rTagName )[0]; + const tag = block.match(rTagName)[0]; const result = { line: lineNumber, - tag: tag.replace( patterns.tagPrefix, '' ), - value: trimmed.replace( tag, '' ).replace( rLeadSpaces, '' ), + tag: tag.replace(patterns.tagPrefix, ''), + value: trimmed.replace(tag, '').replace(rLeadSpaces, ''), valueParsed: [], - source: trimmed + source: trimmed, }; - lineNumber = lineNumber + getLinesLength( block ); + lineNumber = lineNumber + getLinesLength(block); return result; }) - .map( function ( tag ) { - - const parser = parsers[ tag.tag ]; - - if ( parser ) { - - try { - - tag.valueParsed = parser( tag.value ); - } - catch ( err ) { - - tag.error = err; - } + .map(function (tag) { + const parser = parsers[tag.tag]; + + if (!parser) return tag; + + try { + tag.valueParsed = parser(tag.value); + } catch (e) { + tag.error = new Error( + `The "${tag.tag}" tag encountered an error while parsing the following value: ${tag.value}.`, + { + cause: e, + } + ); } return tag; }); - } + }; /** * Get the number of newlines in a block of text. * * @param {string} text Text to use. - * @returns {number} + * @return {number} */ - const getLinesLength = ( text ) => { - - const matches = text.match( /\n/g ); + const getLinesLength = (text) => { + const matches = text.match(/\n/g); return matches ? matches.length : 0; - } - - return ( src ) => { - - return explodeSections( src ).map( function ( section ) { + }; - const result = stripAndSerializeComment( section.line, section.source ); + return (src) => { + return explodeSections(src).map(function (section) { + const result = stripAndSerializeComment( + section.line, + section.source + ); section.content = result.content; section.preface = result.preface; @@ -202,4 +210,4 @@ module.exports = ( options = {} ) => { return section; }); }; -}; \ No newline at end of file +};