├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── funding.yml ├── index.js ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v1 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/hydrogen 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts 3 | *.log 4 | coverage/ 5 | node_modules/ 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef Options 3 | * Configuration. 4 | * @property {string} [delimiter=':'] 5 | * Character to use as delimiter between key/value pairs. 6 | * @property {Array|string|false} [comment='%'] 7 | * Character(s) to use for line comments, `false` turns off comments. 8 | * @property {boolean|'fix'} [forgiving] 9 | * How relaxed to be. 10 | * When `true`, doesn’t throw for duplicate keys. 11 | * When `'fix'`, doesn’t throw for key/value pairs and overwrites. 12 | * @property {boolean} [log=true] 13 | * Whether to log when `forgiving` ignores an error. 14 | */ 15 | 16 | /** 17 | * @typedef {Options} ToJsonOptions 18 | * Deprecated: please use `Options`. 19 | */ 20 | 21 | const own = {}.hasOwnProperty 22 | 23 | /** 24 | * Transform basic plain-text lists or objects into arrays and objects. 25 | * 26 | * @param {string} value 27 | * Value to parse. 28 | * @param {Options} [options] 29 | * Configuration (optional). 30 | */ 31 | export function toJson(value, options = {}) { 32 | const log = 33 | options.log === null || options.log === undefined ? true : options.log 34 | const comment = 35 | options.comment === null || options.comment === undefined 36 | ? '%' 37 | : options.comment 38 | const comments = comment ? (Array.isArray(comment) ? comment : [comment]) : [] 39 | const delimiter = options.delimiter || ':' 40 | const forgiving = options.forgiving 41 | /** @type {Record} */ 42 | const propertyOrValues = {} 43 | 44 | const lines = value 45 | .split('\n') 46 | .map((line) => { 47 | let commentIndex = -1 48 | 49 | while (++commentIndex < comments.length) { 50 | const index = line.indexOf(comments[commentIndex]) 51 | if (index !== -1) line = line.slice(0, index) 52 | } 53 | 54 | return line.trim() 55 | }) 56 | .filter(Boolean) 57 | 58 | const pairs = lines.map( 59 | // Transform `value` to a property--value tuple. 60 | function (value) { 61 | const values = value.split(delimiter) 62 | /** @type {[string, undefined|string]} */ 63 | // @ts-expect-error: always one. 64 | const result = [values.shift().trim()] 65 | 66 | if (values.length > 0) { 67 | result.push(values.join(delimiter).trim()) 68 | } 69 | 70 | return result 71 | } 72 | ) 73 | 74 | /** @type {boolean|undefined} */ 75 | let isPropertyValuePair 76 | 77 | for (const [index, line] of pairs.entries()) { 78 | const currentLineIsPropertyValuePair = line.length === 2 79 | 80 | if (index === 0) { 81 | isPropertyValuePair = currentLineIsPropertyValuePair 82 | } else if (currentLineIsPropertyValuePair !== isPropertyValuePair) { 83 | throw new Error( 84 | 'Error at `' + 85 | line + 86 | '`: ' + 87 | 'Both property-value pairs and array values found. ' + 88 | 'Make sure either exists.' 89 | ) 90 | } 91 | 92 | if (own.call(propertyOrValues, line[0])) { 93 | if ( 94 | !forgiving || 95 | (forgiving === true && 96 | currentLineIsPropertyValuePair && 97 | line[1] !== propertyOrValues[line[0]]) 98 | ) { 99 | throw new Error( 100 | 'Error at `' + 101 | line + 102 | '`: ' + 103 | 'Duplicate data found. ' + 104 | 'Make sure, in objects, no duplicate properties exist;' + 105 | 'in arrays, no duplicate values.' 106 | ) 107 | } 108 | 109 | if (log) { 110 | if (forgiving === 'fix' && propertyOrValues[line[0]] !== line[1]) { 111 | console.log( 112 | 'Overwriting `' + 113 | propertyOrValues[line[0]] + 114 | '` ' + 115 | 'to `' + 116 | line[1] + 117 | '` for `' + 118 | line[0] + 119 | '`' 120 | ) 121 | } else { 122 | console.log('Ignoring duplicate key for `' + line[0] + '`') 123 | } 124 | } 125 | } 126 | 127 | propertyOrValues[line[0]] = line[1] 128 | } 129 | 130 | if (isPropertyValuePair) { 131 | pairs.sort(sortOnFirstIndex) 132 | return Object.fromEntries(pairs) 133 | } 134 | 135 | return lines.sort() 136 | } 137 | 138 | /** 139 | * Sort on the first (`0`) index. 140 | * @param {[string, undefined|string]} a 141 | * @param {[string, undefined|string]} b 142 | */ 143 | function sortOnFirstIndex(a, b) { 144 | // @ts-expect-error: never empty 145 | return a[0].codePointAt(0) - b[0].codePointAt(0) 146 | } 147 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plain-text-data-to-json", 3 | "version": "2.0.1", 4 | "description": "Transform a simple plain-text database to JSON", 5 | "license": "MIT", 6 | "keywords": [ 7 | "plain-text", 8 | "database", 9 | "json" 10 | ], 11 | "repository": "wooorm/plain-text-data-to-json", 12 | "bugs": "https://github.com/wooorm/plain-text-data-to-json/issues", 13 | "funding": { 14 | "type": "github", 15 | "url": "https://github.com/sponsors/wooorm" 16 | }, 17 | "author": "Titus Wormer (https://wooorm.com)", 18 | "contributors": [ 19 | "Titus Wormer (https://wooorm.com)" 20 | ], 21 | "sideEffects": false, 22 | "type": "module", 23 | "main": "index.js", 24 | "types": "index.d.ts", 25 | "files": [ 26 | "index.d.ts", 27 | "index.js" 28 | ], 29 | "devDependencies": { 30 | "@types/node": "^18.0.00", 31 | "c8": "^7.0.0", 32 | "cept": "^2.0.0", 33 | "prettier": "^2.0.0", 34 | "remark-cli": "^11.0.0", 35 | "remark-preset-wooorm": "^9.0.0", 36 | "type-coverage": "^2.0.0", 37 | "typescript": "^4.0.0", 38 | "xo": "^0.52.0" 39 | }, 40 | "scripts": { 41 | "prepack": "npm run build && npm run format", 42 | "build": "tsc --build --clean && tsc --build && type-coverage", 43 | "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", 44 | "test-api": "node --conditions development test.js", 45 | "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", 46 | "test": "npm run build && npm run format && npm run test-coverage" 47 | }, 48 | "prettier": { 49 | "tabWidth": 2, 50 | "useTabs": false, 51 | "singleQuote": true, 52 | "bracketSpacing": false, 53 | "semi": false, 54 | "trailingComma": "none" 55 | }, 56 | "xo": { 57 | "prettier": true 58 | }, 59 | "remarkConfig": { 60 | "plugins": [ 61 | "preset-wooorm" 62 | ] 63 | }, 64 | "typeCoverage": { 65 | "atLeast": 100, 66 | "detail": true, 67 | "strict": true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # plain-text-data-to-json 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | 8 | Transform basic plain-text lists or objects into arrays and objects. 9 | 10 | ## Contents 11 | 12 | * [What is this?](#what-is-this) 13 | * [When should I use this?](#when-should-i-use-this) 14 | * [Install](#install) 15 | * [Use](#use) 16 | * [API](#api) 17 | * [`toJson(value[, options])`](#tojsonvalue-options) 18 | * [Data](#data) 19 | * [Comments](#comments) 20 | * [Whitespace](#whitespace) 21 | * [Empty lines](#empty-lines) 22 | * [Key/value pairs](#keyvalue-pairs) 23 | * [Values](#values) 24 | * [Errors](#errors) 25 | * [Types](#types) 26 | * [Compatibility](#compatibility) 27 | * [Contribute](#contribute) 28 | * [Security](#security) 29 | * [License](#license) 30 | 31 | ## What is this? 32 | 33 | This package takes a file (a sort of simple database), parses it, and returns 34 | clean data. 35 | 36 | ## When should I use this? 37 | 38 | I found myself rewriting a simple transformation over and over to handle text 39 | files. 40 | One example is this source file in [`emoji-emotion`][emoji-emotion-example] 41 | This project fixes that for me. 42 | You can use it too if it matches your needs. 43 | 44 | ## Install 45 | 46 | This package is [ESM only][esm]. 47 | In Node.js (version 14.14+, 16.0+), install with [npm][]: 48 | 49 | ```sh 50 | npm install plain-text-data-to-json 51 | ``` 52 | 53 | In Deno with [`esm.sh`][esmsh]: 54 | 55 | ```js 56 | import {toJson} from 'https://esm.sh/plain-text-data-to-json@2' 57 | ``` 58 | 59 | In browsers with [`esm.sh`][esmsh]: 60 | 61 | ```html 62 | 65 | ``` 66 | 67 | ## Use 68 | 69 | If we have the following file `input.txt`: 70 | 71 | ```txt 72 | % A comment 73 | 74 | alpha 75 | bravo 76 | charlie 77 | ``` 78 | 79 | …and our module `example.js` looks as follows: 80 | 81 | ```js 82 | import fs from 'node:fs/promises' 83 | import {toJson} from 'plain-text-data-to-json' 84 | 85 | const document = String(await fs.readFile('input.txt')) 86 | 87 | const data = toJson(document) 88 | 89 | await fs.writeFile('output.json', JSON.stringify(data, null, 2) + '\n') 90 | ``` 91 | 92 | …then running `node example.js` yields in `output.json`: 93 | 94 | ```json 95 | [ 96 | "alpha", 97 | "bravo", 98 | "charlie" 99 | ] 100 | ``` 101 | 102 | ## API 103 | 104 | This package exports the identifier `toJson`. 105 | There is no default export. 106 | 107 | ### `toJson(value[, options])` 108 | 109 | Transform basic plain-text lists or objects into arrays and objects. 110 | 111 | ##### `options` 112 | 113 | Configuration (optional). 114 | 115 | ###### `options.delimiter` 116 | 117 | Character to use as delimiter between key/value pairs (`string`, default: 118 | `':'`). 119 | 120 | ###### `options.comment` 121 | 122 | Character(s) to use for line comments, `false` turns off comments (`string`, 123 | `Array`, or `boolean`, default: `'%'`) 124 | 125 | ###### `options.forgiving` 126 | 127 | How relaxed to be (`'fix'` or `boolean`, default: `false`). 128 | When `true`, doesn’t throw for duplicate keys. 129 | When `'fix'`, doesn’t throw for key/value pairs and overwrites (see 130 | [errors][]). 131 | 132 | ###### `options.log` 133 | 134 | Whether to call `console.log` with info when `forgiving` ignores an error 135 | (`boolean`, default: `true`). 136 | 137 | ## Data 138 | 139 | The term plain text might be confusing. 140 | It’s actually more of some (sparingly specified) standard. 141 | 142 | ### Comments 143 | 144 | Use a percentage sign (by default) to specify a comment. 145 | The comment will last until the end of line. 146 | 147 | ```txt 148 | % This is a completely commented line. 149 | unicorn % This is a partially commented line. 150 | ``` 151 | 152 | Yields: 153 | 154 | ```js 155 | ['unicorn'] 156 | ``` 157 | 158 | ### Whitespace 159 | 160 | Initial or final white space (`\s`) is trimmed from values. 161 | 162 | ```txt 163 | unicorn % some value 164 | ``` 165 | 166 | Yields: 167 | 168 | ```js 169 | ['unicorn'] 170 | ``` 171 | 172 | ### Empty lines 173 | 174 | Empty lines are striped. 175 | This includes whitespace only lines. 176 | 177 | ```txt 178 | %%% this file contains a value. %%% 179 | 180 | unicorn 181 | ``` 182 | 183 | Yields: 184 | 185 | ```js 186 | ['unicorn'] 187 | ``` 188 | 189 | ### Key/value pairs 190 | 191 | If a line includes a colon (by default), the library returns an object. 192 | 193 | ```txt 194 | unicorn : magic creature 195 | ``` 196 | 197 | Yields: 198 | 199 | ```js 200 | {unicorn: 'magic creature'} 201 | ``` 202 | 203 | ### Values 204 | 205 | All other lines are treated as array values. 206 | 207 | ```txt 208 | unicorn 209 | ``` 210 | 211 | Yields: 212 | 213 | ```json 214 | ["unicorn"] 215 | ``` 216 | 217 | ### Errors 218 | 219 | Some errors are thrown when malformed “plain-text” is found, such as: 220 | 221 | * when lines both with and without colons exist 222 | * in arrays, when duplicate values exist (unless `forgiving: true`) 223 | * in objects, when duplicate properties exist (unless `forgiving: true`) 224 | * in objects, when duplicate properties with different values exist (unless 225 | `forgiving: "fix"`) 226 | 227 | ## Types 228 | 229 | This package is fully typed with [TypeScript][]. 230 | It exports the additional type `Options`. 231 | 232 | ## Compatibility 233 | 234 | This package is at least compatible with all maintained versions of Node.js. 235 | As of now, that is Node.js 14.14+ and 16.0+. 236 | It also works in Deno and modern browsers. 237 | 238 | ## Contribute 239 | 240 | Yes please! 241 | See [How to Contribute to Open Source][contribute]. 242 | 243 | ## Security 244 | 245 | This package is safe. 246 | 247 | ## License 248 | 249 | [MIT][license] © [Titus Wormer][author] 250 | 251 | 252 | 253 | [build-badge]: https://github.com/wooorm/plain-text-data-to-json/workflows/main/badge.svg 254 | 255 | [build]: https://github.com/wooorm/plain-text-data-to-json/actions 256 | 257 | [coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/plain-text-data-to-json.svg 258 | 259 | [coverage]: https://codecov.io/github/wooorm/plain-text-data-to-json 260 | 261 | [downloads-badge]: https://img.shields.io/npm/dm/plain-text-data-to-json.svg 262 | 263 | [downloads]: https://www.npmjs.com/package/plain-text-data-to-json 264 | 265 | [size-badge]: https://img.shields.io/bundlephobia/minzip/plain-text-data-to-json.svg 266 | 267 | [size]: https://bundlephobia.com/result?p=plain-text-data-to-json 268 | 269 | [npm]: https://docs.npmjs.com/cli/install 270 | 271 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 272 | 273 | [esmsh]: https://esm.sh 274 | 275 | [typescript]: https://www.typescriptlang.org 276 | 277 | [contribute]: https://opensource.guide/how-to-contribute/ 278 | 279 | [license]: license 280 | 281 | [author]: https://wooorm.com 282 | 283 | [errors]: #errors 284 | 285 | [emoji-emotion-example]: https://github.com/words/emoji-emotion/blob/main/faces.txt 286 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {cept} from 'cept' 4 | import {toJson} from './index.js' 5 | 6 | test('toJson', function () { 7 | assert.equal(typeof toJson, 'function', 'should be a `function`') 8 | }) 9 | 10 | test('Comments', function () { 11 | assert.deepEqual( 12 | toJson(['% This is a completely commented line.', 'unicorn'].join('\n')), 13 | ['unicorn'], 14 | 'should strip line comments' 15 | ) 16 | 17 | assert.deepEqual( 18 | toJson('unicorn % This is a partially commented line.'), 19 | ['unicorn'], 20 | 'should strip partial line comments' 21 | ) 22 | 23 | assert.deepEqual( 24 | toJson('unicorn % This is a partially commented line.', { 25 | comment: false 26 | }), 27 | ['unicorn % This is a partially commented line.'], 28 | 'should honour `comment: false`' 29 | ) 30 | 31 | assert.deepEqual( 32 | toJson(['# This is a completely commented line.', 'unicorn'].join('\n'), { 33 | comment: '#' 34 | }), 35 | ['unicorn'], 36 | 'should strip line comments based on a given token' 37 | ) 38 | 39 | assert.deepEqual( 40 | toJson('unicorn # This is a partially commented line.', { 41 | comment: '#' 42 | }), 43 | ['unicorn'], 44 | 'should strip partial comments based on a given token' 45 | ) 46 | 47 | assert.deepEqual( 48 | toJson('unicorn # 1\n% 2\ndoge', { 49 | comment: ['#', '%'] 50 | }), 51 | ['doge', 'unicorn'], 52 | 'should strip partial comments based on a given token' 53 | ) 54 | }) 55 | 56 | test('White space', function () { 57 | assert.deepEqual( 58 | toJson(' \tunicorn \t'), 59 | ['unicorn'], 60 | 'should trim prefixed and suffixed white space' 61 | ) 62 | }) 63 | 64 | test('Blank lines', function () { 65 | assert.deepEqual( 66 | toJson('\n \t \ndoge\n\nunicorn\r\n'), 67 | ['doge', 'unicorn'], 68 | 'should remove empty / blank lines' 69 | ) 70 | }) 71 | 72 | test('EOF', function () { 73 | assert.deepEqual(toJson('unicorn'), ['unicorn'], 'No EOL') 74 | assert.deepEqual(toJson('unicorn\n'), ['unicorn'], 'LF') 75 | assert.deepEqual(toJson('unicorn\r\n'), ['unicorn'], 'CR+LF') 76 | }) 77 | 78 | test('Property-value pairs', function () { 79 | assert.deepEqual( 80 | toJson('unicorn: magic creature'), 81 | {unicorn: 'magic creature'}, 82 | 'should support pair delimiters' 83 | ) 84 | 85 | assert.deepEqual( 86 | toJson( 87 | [ 88 | 'unicorn : magic creature', 89 | '\trainbow:double\t', 90 | 'doge\t:\tso scare' 91 | ].join('\n') 92 | ), 93 | { 94 | doge: 'so scare', 95 | rainbow: 'double', 96 | unicorn: 'magic creature' 97 | }, 98 | 'white-space around pair delimiters' 99 | ) 100 | 101 | assert.deepEqual( 102 | toJson('unicorn\tmagic creature', {delimiter: '\t'}), 103 | {unicorn: 'magic creature'}, 104 | 'given delimiters' 105 | ) 106 | }) 107 | 108 | test('Values', function () { 109 | assert.deepEqual(toJson('unicorn'), ['unicorn'], 'one value') 110 | 111 | assert.deepEqual( 112 | toJson('unicorn \n doge\n\trainbow'), 113 | ['doge', 'rainbow', 'unicorn'], 114 | 'multiple values' 115 | ) 116 | }) 117 | 118 | test('Mixed values', function () { 119 | assert.throws( 120 | function () { 121 | toJson('unicorn\nrainbow: double') 122 | }, 123 | /^Error: Error at `rainbow,double`/, 124 | 'should throw when both property-value pairs and values are provided' 125 | ) 126 | }) 127 | 128 | test('Invalid lists', async function (t) { 129 | assert.throws( 130 | function () { 131 | toJson('unicorn\nrainbow\nunicorn') 132 | }, 133 | /^Error: Error at `unicorn`: Duplicate data found/, 134 | 'should throw when duplicate values exist' 135 | ) 136 | 137 | assert.deepEqual( 138 | toJson('unicorn\nrainbow\nunicorn', {forgiving: true}), 139 | ['rainbow', 'unicorn', 'unicorn'], 140 | 'should honour forgiving' 141 | ) 142 | 143 | await t.test('should log duplicate values when `forgiving`', function () { 144 | const stop = cept(console, 'log', hoist) 145 | /** @type {Array} */ 146 | let parameters = [] 147 | 148 | toJson('unicorn\nrainbow\nunicorn', {forgiving: true}) 149 | 150 | stop() 151 | 152 | assert.equal(parameters[0], 'Ignoring duplicate key for `unicorn`') 153 | 154 | function hoist() { 155 | parameters = [...arguments] 156 | } 157 | }) 158 | 159 | await t.test('should honour `log: false`', function () { 160 | const stop = cept(console, 'log', hoist) 161 | /** @type {Array|undefined} */ 162 | let parameters 163 | 164 | toJson('unicorn\nrainbow\nunicorn', {forgiving: true, log: false}) 165 | 166 | stop() 167 | 168 | assert.equal(parameters, undefined) 169 | 170 | function hoist() { 171 | parameters = [...arguments] 172 | } 173 | }) 174 | }) 175 | 176 | test('Invalid objects', async function (t) { 177 | assert.throws( 178 | function () { 179 | toJson('doge: so scare\nunicorn: magic\ndoge: double') 180 | }, 181 | /^Error: Error at `doge,double`: Duplicate data found/, 182 | 'should throw when duplicate values exist' 183 | ) 184 | 185 | assert.deepEqual( 186 | toJson('doge: so scare\nunicorn: magic creature\ndoge: so scare\n', { 187 | forgiving: true 188 | }), 189 | {doge: 'so scare', unicorn: 'magic creature'}, 190 | 'should honour forgiving' 191 | ) 192 | 193 | await t.test('should log duplicate values when `forgiving`', function () { 194 | const stop = cept(console, 'log', hoist) 195 | /** @type {Array} */ 196 | let parameters = [] 197 | 198 | toJson('doge: so scare\nunicorn: magic creature\ndoge: so scare\n', { 199 | forgiving: true 200 | }) 201 | 202 | stop() 203 | 204 | assert.equal(parameters[0], 'Ignoring duplicate key for `doge`') 205 | 206 | function hoist() { 207 | parameters = [...arguments] 208 | } 209 | }) 210 | 211 | await t.test('should honour `log: false`', function () { 212 | const stop = cept(console, 'log', hoist) 213 | /** @type {Array|undefined} */ 214 | let parameters 215 | 216 | toJson('doge: so scare\nunicorn: magic creature\ndoge: so scare\n', { 217 | forgiving: true, 218 | log: false 219 | }) 220 | 221 | stop() 222 | 223 | assert.equal(parameters, undefined) 224 | 225 | function hoist() { 226 | parameters = [...arguments] 227 | } 228 | }) 229 | 230 | assert.deepEqual( 231 | toJson('doge: so scare\nunicorn: magic creature\ndoge: so scare\n', { 232 | forgiving: 'fix' 233 | }), 234 | {doge: 'so scare', unicorn: 'magic creature'}, 235 | "should honour `forgiving: 'fix'`" 236 | ) 237 | 238 | assert.deepEqual( 239 | toJson('doge: so scare\nunicorn: magic creature\ndoge: rainbows\n', { 240 | forgiving: 'fix' 241 | }), 242 | {doge: 'rainbows', unicorn: 'magic creature'}, 243 | 'duplicate keys with different values' 244 | ) 245 | 246 | await t.test( 247 | 'should log for duplicate keys when `forgiving` is `"fix"', 248 | function () { 249 | const stop = cept(console, 'log', hoist) 250 | /** @type {Array} */ 251 | let parameters = [] 252 | 253 | toJson('doge: so scare\nunicorn: magic creature\ndoge: so scare\n', { 254 | forgiving: true 255 | }) 256 | 257 | stop() 258 | 259 | assert.equal(parameters[0], 'Ignoring duplicate key for `doge`') 260 | 261 | function hoist() { 262 | parameters = [...arguments] 263 | } 264 | } 265 | ) 266 | }) 267 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/**.js"], 3 | "exclude": ["coverage", "node_modules"], 4 | "compilerOptions": { 5 | "checkJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["es2020"], 11 | "module": "node16", 12 | "newLine": "lf", 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "target": "es2020" 16 | } 17 | } 18 | --------------------------------------------------------------------------------