diff --git a/CHANGELOG.md b/CHANGELOG.md index e397b62..ed42c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## \[1.1.0] - 2018-11-21 +## \[1.1.0] - 2018-11-22 ### Added - Option `escapeQuotes` to escape quotes in the output of strings (not wrapped by JSON output). - TSLint instead of ESLint, for compatibility with CI services. -- Codacy quality and coverage services. +- [Codacy](https://api.codacy.com) quality and coverage services. ### Changed - Convert `export.default` to `module.exports` in internal modules. Since it is a node.js library, it looks right and produces a cleaner code. - The output of chained properties stops with a primitive value, to avoid some compile-time errors. +- Updated Readme, add "vulnerabilities" badge from [snyk.io](https://snyk.io). +- Regression of the replacement of `NaN` with `null` since the later alters the behavior of the Date ctor. +- Simplify the `parseChunk` function, logic moved to the `parseHelper` class. ### Removed diff --git a/README.md b/README.md index 438fb89..784320c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # jscc -[![npm][npm-image]][npm-url] -[![License][license-image]][license-url] -[![AppVeyor][appveypr-image]][appveypr-url] -[![Build Status][travis-image]][travis-url] -[![Codebeat][codebeat-image]][codebeat-url] -[![Coverage][codecov-image]][codecov-url] +[![jscc on npm][npm-badge]][npm-url] +[![Windows Build][appveypr-badge]][appveypr-url] +[![Linux Build][travis-badge]][travis-url] +[![Codebeat][codebeat-badge]][codebeat-url] +[![Coverage][codecov-badge]][codecov-url] +[![Vulnerabilities][snyk-badge]][snyk-url] +[![License MIT][license-badge]][license-url] Featuring some of the C preprocessor characteristics through special, configurable comments, jscc can be used in any type of files to build multiple versions of your software from the same code base. @@ -68,6 +69,8 @@ The result is a plain JS object with a property `code`, a string with the proces If a callback is provided, jscc will operate asynchronously and call the callback with an error object, if any, or `null` in the first parameter and the result in the second. +Please see the Wiki to know the supported [options](https://github.com/aMarCruz/jscc/wiki/Options). + ## Directives jscc works with _directives_ inserted in the text files and prefixed with configurable character sequences, that defaults to `'/*'`, `'//'` and `' -[npm-image]: https://img.shields.io/npm/v/jscc.svg +[npm-badge]: https://img.shields.io/npm/v/jscc.svg [npm-url]: https://www.npmjs.com/package/jscc -[license-image]: https://img.shields.io/github/license/mashape/apistatus.svg +[license-badge]: https://img.shields.io/github/license/mashape/apistatus.svg [license-url]: https://github.com/aMarCruz/jscc/blob/master/LICENSE -[appveypr-image]: https://ci.appveyor.com/api/projects/status/hdsef0p6q0oqr127?svg=true +[appveypr-badge]: https://ci.appveyor.com/api/projects/status/hdsef0p6q0oqr127?svg=true [appveypr-url]: https://ci.appveyor.com/project/aMarCruz/jscc -[travis-image]: https://img.shields.io/travis/aMarCruz/jscc.svg +[travis-badge]: https://img.shields.io/travis/aMarCruz/jscc.svg [travis-url]: https://travis-ci.org/aMarCruz/jscc +[snyk-badge]: https://snyk.io/test/github/aMarCruz/jscc/badge.svg?targetFile=package.json +[snyk-url]: https://snyk.io/test/github/aMarCruz/jscc?targetFile=package.json [codacy-badge]: https://api.codacy.com/project/badge/Grade/30e8679fcd614227837ad250dd6c4030 [codacy-url]: https://www.codacy.com/app/aMarCruz/jscc?utm_source=github.com&utm_medium=referral&utm_content=aMarCruz/jscc&utm_campaign=Badge_Grade [codacyc-badge]: https://api.codacy.com/project/badge/Coverage/30e8679fcd614227837ad250dd6c4030 [codacyc-url]: https://www.codacy.com/app/aMarCruz/jscc?utm_source=github.com&utm_medium=referral&utm_content=aMarCruz/jscc&utm_campaign=Badge_Coverage -[codebeat-image]: https://codebeat.co/badges/7e15dc9d-42a8-4ea2-8bae-a21c09490fbe +[codebeat-badge]: https://codebeat.co/badges/7e15dc9d-42a8-4ea2-8bae-a21c09490fbe [codebeat-url]: https://codebeat.co/projects/github-com-amarcruz-jscc-dev -[codecov-image]: https://codecov.io/gh/aMarCruz/jscc/branch/dev/graph/badge.svg +[codecov-badge]: https://codecov.io/gh/aMarCruz/jscc/branch/dev/graph/badge.svg [codecov-url]: https://codecov.io/gh/aMarCruz/jscc -[climate-image]: https://codeclimate.com/github/aMarCruz/jscc/badges/gpa.svg +[climate-badge]: https://codeclimate.com/github/aMarCruz/jscc/badges/gpa.svg [climate-url]: https://codeclimate.com/github/aMarCruz/jscc -[issues-image]: https://codeclimate.com/github/aMarCruz/jscc/badges/issue_count.svg +[issues-badge]: https://codeclimate.com/github/aMarCruz/jscc/badges/issue_count.svg [issues-url]: https://codeclimate.com/github/aMarCruz/jscc -[climatec-image]: https://api.codeclimate.com/v1/badges/50d60a10ec7c9156b429/test_coverage -[climatec-url]: https://codeclimate.com/github/aMarCruz/jscc/test_coverage -[bmc-image]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png -[bmc-url]: https://www.buymeacoffee.com/aMarCruz +[climatec-badge]: https://api.codeclimate.com/v1/badges/50d60a10ec7c9156b429/test_coverage +[climatec-url]: https://codeclimate.com/github/aMarCruz/jscc/test_coverage +[kofi-url]: https://ko-fi.com/C0C7LF7I diff --git a/src/parse-chunks.ts b/src/parse-chunks.ts index ef20ce3..74d8dfd 100644 --- a/src/parse-chunks.ts +++ b/src/parse-chunks.ts @@ -3,7 +3,14 @@ import ParseHelper = require('./parse-helper') import Parser = require('./parser') /** - * Workaround for possible error with a BOM mark in the source. + * Handles possible error with a BOM mark in the source by replacing it with + * an EOL (it allows the parser regex identify the start of the first line). + * + * The mark is preserved because, if there are replacements, the edited buffer + * will be obtained from magicStr, which will not touch the mark, and if there + * are not, the caller will use the original source. + * + * @param source The original source */ const withoutBOMmark = (source: string) => { return source.charCodeAt(0) === 0xFEFF ? '\n' + source.slice(1) : source @@ -14,58 +21,47 @@ const withoutBOMmark = (source: string) => { * For each match found, calls the parser with the result of the regex and * the parser returns the next position from which to continue the search. * - * @param parser Parser instance to use + * @param parser jscc parser instance * @param source The original source * @param helper Functions to flush and remove chuncks */ const parseChunks = function _parseChunks (parser: Parser, source: string, helper: ParseHelper) { - let hideStart = 0 // keep the start position of the block to hide - let lastIndex = 0 // keep the position of the next chunk to parse - let output = true - - const re = parser.getRegex() // $1:keyword, $2:expression + // Get a regex from the jscc parser to match line containing directives. + // This regex depends on the prefixes in use and its match is handled by + // the jscc parser, here we only care about the position of the matched line. + const re = parser.getRegex() let match = re.exec(withoutBOMmark(source)) - const changes = !!match // avoid send sourceMap if there's no changes - while (match) { - const index = match.index + // With `re`, there's no way for a line other than a directive to be + // matched, so we can set a flag here to avoid a non-necessary sourcemap. + const changes = !!match - // Replace varnames in the current chunk and flush it, if necessary. - helper.commit(lastIndex, index, output) - - if (output !== parser.parse(match)) { - // Output state changed - - if (output) { - // The output ends, save the position where this new - // hidden block begins. - hideStart = index - } - - // Else, the output begins and the hidden block will be removed. - // (hasOutput is initialized with `true`, so a hidden block exists) - output = !output + while (match) { - } else if (output) { - // The output state has not changed and the output is enabled, - // will remove the line of the processed directive. - hideStart = index + // The parser could change the jscc varnames, so it's necessary + // to replace any pending chunks before parsing the line. + helper.flushPrev(match.index) - // Otherwise, it will be removed together with the current hidden - // block when this ends. - } + // Parse the line and update buffers and searching position. + // `parser.parse` returns the new output state. + re.lastIndex = helper.flushLine( + match.index, + re.lastIndex, + parser.parse(match) + ) - lastIndex = re.lastIndex = helper.remove(hideStart, re.lastIndex, output) + // With lastIndex already updated, search the next directive. match = re.exec(source) } // This will throw if the buffer has unbalanced blocks parser.close() - // This final flush is necessary, don't delete it - return helper.commit(lastIndex, source.length, true) || changes + // This final flush is necessary because the source can have replacements + // even if it does not contain directives. + return helper.flush() || changes } export = parseChunks diff --git a/src/parse-helper.ts b/src/parse-helper.ts index da361fe..051601a 100644 --- a/src/parse-helper.ts +++ b/src/parse-helper.ts @@ -10,6 +10,9 @@ const EOLS = /[^\r\n]+/g */ class ParseHelper { + private lastPos = 0 // keep the next offset to flush + private output = true // output state, starts "active" + /** * @param source Original source * @param props jscc properties @@ -17,22 +20,74 @@ class ParseHelper { constructor (private source: string, private props: JsccProps) { } + /** + * Final flush. The final output is always in "active" state. + */ + public flush () { + return this.commit(this.lastPos, this.source.length) + } + + /** + * Write pending changes. + * + * _IMPORTANT:_ `Parser.parse` can change the current jscc values, + * so this function _MUST BE_ called before the parsing to make any + * replacements with the current values. + * + * @param offset Starting position of the matched line + */ + public flushPrev (offset: number) { + if (this.output) { + this.commit(this.lastPos, offset) + } + } + + /** + * A line was processed, flush buffers as necessary. + * + * @param start Starting position of the chunk into the original buffer + * @param end Position of the character following the chunk + * @param output The updated output state + * @returns The updated position where to continue the search. + */ + public flushLine (start: number, end: number, output: boolean) { + + // Find the start of the next line in the buffer. + if (end < this.source.length) { + end += this.source.substr(end, 2) === '\r\n' ? 2 : 1 + } + + if (output !== this.output) { + this.output = output + this.flushit(start, end) + + } else if (output) { + // flushPrev was already called, so no need to commit + this.remove(start, end) + } + + return end + } + /** * If the parsed `chunk` seems to contain varnames to replace, call the * remapVars function which will make the replacement and store the chunk * into the MagicString intance. Otherwise, do nothing. * + * _NOTE:_ This function updates `this.lastPos` + * * @param start Starting position of the chunk into the original buffer - * @param end Ending position (the character followng the chunk) - * @param output Must replace the block now? + * @param end Position of the character following the chunk * @returns `true` if the chunk was changed */ - public commit (start: number, end: number, output: boolean) { + private commit (start: number, end: number) { - if (!output || start >= end) { + if (start >= end) { return false } + this.lastPos = end + // Get the fragment of source where to search varnames to replace const chunk = this.source.slice(start, end) @@ -41,29 +96,39 @@ class ParseHelper { } /** - * Removes the block from the `start` to the `end` position, inclusive, plus - * the following line-ending (one char for mac/unix, two for windows type). + * Removes the block from the `start` to the `end` position, inclusive. + * + * _NOTE:_ This function updates `this.lastPos` * * @param start Starting position of the chunk into the original buffer - * @param end Ending position (the character followng the chunk) - * @param output Must remove the block now? + * @param end Position of the character following the chunk * @returns Position of the character following the removed block. */ - public remove (start: number, end: number, output: boolean) { + private remove (start: number, end: number) { - if (end < this.source.length) { - end += this.source[end] === '\r' && this.source[end + 1] === '\n' ? 2 : 1 - } + this.lastPos = end - // Only do the replacement if the output is enabled - if (output) { - const block = this.props.keepLines - ? this.source.slice(start, end).replace(EOLS, '') : '' + const block = this.props.keepLines + ? this.source.slice(start, end).replace(EOLS, '') : '' - this.props.magicStr.overwrite(start, end, block) - } + this.props.magicStr.overwrite(start, end, block) + } - return end + /** + * The output state changed, flush the buffer. + * + * @param start Start of current line + * @param end End of current line + */ + private flushit (start: number, end: number) { + if (this.output) { + // Output begins, remove previous hidden block. + this.remove(this.lastPos, end) + + } else { + // Output ends, flush the already processed block. + this.commit(this.lastPos, start) + } } } diff --git a/src/remap-vars.ts b/src/remap-vars.ts index 4140d57..a6e5e58 100644 --- a/src/remap-vars.ts +++ b/src/remap-vars.ts @@ -50,7 +50,7 @@ const stringObject = (obj: object) => { // toISOString throw with NaN dates, toJSON returns `null` if (obj instanceof Date) { - str = isNaN(+obj) ? 'null' : obj.toJSON() + str = isNaN(+obj) ? 'NaN' : obj.toJSON() } else if (obj instanceof RegExp) { str = obj.source @@ -59,7 +59,7 @@ const stringObject = (obj: object) => { str = obj.valueOf() } else if (obj instanceof Number) { - str = isNaN(+obj) ? 'null' : String(obj) + str = String(obj) } else { str = JSON.stringify(obj, stringifyFn) @@ -82,17 +82,17 @@ const stringObject = (obj: object) => { */ const stringValue = (value: any, escapeQuotes: number) => { - // `NaN` returns `null`, for consistency with `JSON.stringify` - // eslint-disable-next-line no-self-compare - if (value !== value) { - return 'null' + // Trap falsy values, including `NaN` and empty strings. + if (!value) { + return String(value) } - // stringObject accepts `NaN` object but no `null`s. - if (value && typeof value === 'object') { + // stringObject accepts `NaN` objects. + if (typeof value === 'object') { return stringObject(value) } + // Other non-falsy primitive values. let str = String(value) if (escapeQuotes & QUOTES.Single) { diff --git a/test/s00-jscc.spec.ts b/test/s00-jscc.spec.ts index f777dea..a3a2845 100644 --- a/test/s00-jscc.spec.ts +++ b/test/s00-jscc.spec.ts @@ -133,6 +133,7 @@ describe('jscc', function () { it('must preserve tuf8 BOM in the source', function () { // Seems nodeJS uses \uFEFF to mark any enconding testFileStr('utf8-bom.txt', /^\ufeffOK$/) + expect(preprocStr('\ufeff')).to.be('\ufeff') }) }) diff --git a/test/s06-replacement.spec.ts b/test/s06-replacement.spec.ts index f1b9c16..65beb0e 100644 --- a/test/s06-replacement.spec.ts +++ b/test/s06-replacement.spec.ts @@ -54,22 +54,31 @@ describe('Code Replacement', function () { }) }) - it('Date objects must output its unquoted JSON value', function () { + it('Valid dates must output its unquoted JSON value, if alone', function () { const D = new Date('2018-10-17T00:00:00.0Z').toJSON() testStr([ - `//#set _V1 new Date("${D}")`, - '//#set _V2 new Date(NaN)', - `//#set _O {v1:new Date("${D}"),v2:new Date(NaN)}`, - '$_V1', - '$_V2', - '$_O.v1,$_O.v2', + `//#set _V new Date("${D}")`, + `//#set _O {v:new Date("${D}")}`, + '$_V', + '$_O.v', + ], `${D}\n${D}`) + }) + + it('Valid dates in JSON objects output has its quoted JSON value', function () { + const D = new Date('2018-10-17T00:00:00.0Z').toJSON() + testStr([ + `//#set _O {v:new Date("${D}")}`, '$_O', - ], [ - D, - 'null', - D + ',null', - `{"v1":"${D}","v2":null}`, - ].join('\n')) + ], `{"v":"${D}"}`) + }) + + it("Invalid dates must outputs 'NaN', if alone", function () { + testStr([ + '//#set _V new Date(NaN)', + `//#set _O {v:new Date(NaN)}`, + '$_V', + '$_O.v', + ], 'NaN\nNaN') }) it('Regex objects must output its unquoted `source` value', function () { @@ -92,7 +101,7 @@ describe('Code Replacement', function () { ].join('\n')) }) - it('Regex must escape quoted `source` only in objects', function () { + it('Regex must escape quoted `source` only in JSON objects', function () { testStr([ `//#set _R1 /"'/`, `//#set _O {r:/"'/}`, @@ -107,7 +116,7 @@ describe('Code Replacement', function () { ].join('\n')) }) - it('Infinity, -Infinity, and NaN numbers has custom output', function () { + it('Infinity and -Infinity has custom output in JSON objects', function () { const v1 = JSON.stringify(Number.MAX_VALUE) const v2 = JSON.stringify(Number.MIN_VALUE) testStr([ @@ -115,7 +124,7 @@ describe('Code Replacement', function () { '//#set _V2 new Number(Infinity)', '//#set _V3 -Infinity', '//#set _V4 new Number(-Infinity)', - '//#set _O {v1:Infinity, v2:-Infinity, v3:new Number(-Infinity), v4:NaN}', + '//#set _O {v1:_V1, v2:_V2, v3:_V3, v4:_V4}', '$_V1', '$_V2', '$_V3', @@ -132,20 +141,27 @@ describe('Code Replacement', function () { '-Infinity', '-Infinity', 'Infinity', + 'Infinity', '-Infinity', '-Infinity', - 'null', - `{"v1":${v1},"v2":${v2},"v3":${v2},"v4":null}`, + `{"v1":${v1},"v2":${v1},"v3":${v2},"v4":${v2}}`, ].join('\n')) }) - it('`NaN` values on object instances must output `null`', function () { + it('Primitive `NaN` or Number(NaN) must output an unquoted `NaN`', function () { testStr([ - '//#set _N new Number(NaN)', - '//#set _D new Date(NaN)', + '//#set _N NaN', + '//#set _D new Number(NaN)', '$_N', '$_D', - ], 'null\nnull') + ], 'NaN\nNaN') + }) + + it('`NaN` numbers (primitive or object) must output `null` in JSON objects', function () { + testStr([ + '//#set _O {v1:NaN, v2:new Number(NaN)}', + '$_O', + ], '{"v1":null,"v2":null}') }) it('Do not confuse `Infinity` with the string "Infinity"', function () { diff --git a/test/s14-non-js.spec.ts b/test/s14-non-js.spec.ts index f35ee9a..caeb51f 100644 --- a/test/s14-non-js.spec.ts +++ b/test/s14-non-js.spec.ts @@ -15,7 +15,7 @@ describe('HTML Processing', function () { }) }) - it('must handle custom prefixes, byt example html short comments "