├── .gitignore ├── .prettierignore ├── tests ├── @js.properties │ ├── malformed │ │ ├── unicode-escape-empty.properties │ │ └── unicode-escape-insufficient-digits.properties │ ├── empty-key-element.properties.json │ ├── line-terminator-cr.properties │ ├── final-slash.properties │ ├── line-terminator-cr.properties.json │ ├── line-terminator-crlf.properties.json │ ├── line-terminator-lf.properties.json │ ├── line-terminator-noeol.properties.json │ ├── final-slash.properties.json │ ├── line-terminator-lf.properties │ ├── trailing-whitespaces.properties │ ├── line-terminator-crlf.properties │ ├── line-terminator-noeol.properties │ ├── line-terminator-mixed.properties.json │ ├── duplicates.properties.json │ ├── line-terminator-mixed.properties │ ├── empty-key-element.properties │ ├── trailing-whitespaces.properties.json │ ├── multiline.properties.json │ ├── namespaced.properties │ ├── namespaced.properties.json │ ├── duplicates.properties │ ├── wikipedia-example.LICENSE │ ├── wikipedia-example.properties.json │ ├── multiline.properties │ ├── namespaced.properties.namespaced.json │ ├── escaped.properties.json │ ├── separators.properties.json │ ├── escaped.properties │ ├── separators.properties │ ├── wikipedia-example.properties │ └── README.md ├── node-properties-parser │ ├── README.md │ ├── test.json │ └── test.properties ├── external.tests.js ├── folding.tests.js └── corner-cases.tests.js ├── .eslintrc.yaml ├── lib ├── index.js ├── ast.js ├── index.d.ts ├── stringify.js └── parse.js ├── .github └── workflows │ └── nodejs.yml ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | coverage/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | tests/@js.properties/ 3 | -------------------------------------------------------------------------------- /tests/@js.properties/malformed/unicode-escape-empty.properties: -------------------------------------------------------------------------------- 1 | foo = \u foo 2 | -------------------------------------------------------------------------------- /tests/@js.properties/empty-key-element.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "", 3 | "": "" 4 | } -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-cr.properties: -------------------------------------------------------------------------------- 1 | # CR as the line terminator foo = bar baz = qux -------------------------------------------------------------------------------- /tests/@js.properties/malformed/unicode-escape-insufficient-digits.properties: -------------------------------------------------------------------------------- 1 | foo = \u123z foo 2 | -------------------------------------------------------------------------------- /tests/@js.properties/final-slash.properties: -------------------------------------------------------------------------------- 1 | foo = a\\\ 2 | b 3 | 4 | bar = z\ 5 | 6 | baz = shh\ -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-cr.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "baz": "qux" 4 | } -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-crlf.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "baz": "qux" 4 | } -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-lf.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "baz": "qux" 4 | } -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-noeol.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "baz": "qux" 4 | } -------------------------------------------------------------------------------- /tests/@js.properties/final-slash.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "a\\b", 3 | "bar": "z", 4 | "baz": "shh" 5 | } -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-lf.properties: -------------------------------------------------------------------------------- 1 | # LF as the line terminator 2 | foo = bar 3 | baz = qux 4 | -------------------------------------------------------------------------------- /tests/@js.properties/trailing-whitespaces.properties: -------------------------------------------------------------------------------- 1 | foo = a b c 2 | bar : d e f 3 | baz g h i 4 | -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-crlf.properties: -------------------------------------------------------------------------------- 1 | # CRLF as the line terminator 2 | foo = bar 3 | baz = qux 4 | -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-noeol.properties: -------------------------------------------------------------------------------- 1 | # LF as the line terminator; no final eol 2 | foo = bar 3 | baz = qux -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-mixed.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "baz": "qux", 4 | "quux": "quuz" 5 | } -------------------------------------------------------------------------------- /tests/@js.properties/duplicates.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "baz", 3 | "qux": "quuz", 4 | "quux": "quuz", 5 | "dup": "duplicate" 6 | } -------------------------------------------------------------------------------- /tests/@js.properties/line-terminator-mixed.properties: -------------------------------------------------------------------------------- 1 | # CR or LF or CRLF as the line terminator foo = bar 2 | baz = qux 3 | quux = quuz 4 | -------------------------------------------------------------------------------- /tests/@js.properties/empty-key-element.properties: -------------------------------------------------------------------------------- 1 | # Empty Element 2 | foo 3 | 4 | # Empty Key 5 | =bar 6 | 7 | # Empty Key and Element 8 | : 9 | -------------------------------------------------------------------------------- /tests/@js.properties/trailing-whitespaces.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "a b\tc ", 3 | "bar": "d\te\tf\t \t", 4 | "baz": "g\fh\fi\f\t\f \f" 5 | } -------------------------------------------------------------------------------- /tests/@js.properties/multiline.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "AaBbCc", 3 | "bar001122": "baz", 4 | "qux": "quux", 5 | "multilinename": "multiline value" 6 | } -------------------------------------------------------------------------------- /tests/@js.properties/namespaced.properties: -------------------------------------------------------------------------------- 1 | foo = A.B.C 2 | foo.bar : abc 3 | foo.baz def 4 | qux.quux = quuz 5 | 6 | a.b1 = a.b1 7 | a.b2.c1 = a.b2.c1 8 | a.b2.c2 = a.b2.c2 9 | -------------------------------------------------------------------------------- /tests/@js.properties/namespaced.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "A.B.C", 3 | "foo.bar": "abc", 4 | "foo.baz": "def", 5 | "qux.quux": "quuz", 6 | "a.b1": "a.b1", 7 | "a.b2.c1": "a.b2.c1", 8 | "a.b2.c2": "a.b2.c2" 9 | } -------------------------------------------------------------------------------- /tests/@js.properties/duplicates.properties: -------------------------------------------------------------------------------- 1 | # Duplicate keys 2 | foo = bar 3 | foo = baz 4 | 5 | # Duplicate elements 6 | qux = quuz 7 | quux = quuz 8 | 9 | # Duplicate key and element pairs 10 | dup = duplicate 11 | dup : duplicate 12 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | env: 4 | es6: true 5 | node: true 6 | 7 | extends: 8 | - eslint:recommended 9 | - prettier 10 | 11 | rules: 12 | no-control-regex: 0 13 | 14 | overrides: 15 | - files: 16 | - tests/*.js 17 | env: 18 | jest: true 19 | -------------------------------------------------------------------------------- /tests/@js.properties/wikipedia-example.LICENSE: -------------------------------------------------------------------------------- 1 | File: wikipedia-example.properties 2 | 3 | Source: https://en.wikipedia.org/wiki/.properties 4 | 5 | License: [Creative Commons Attribution-ShareAlike License](https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License) 6 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { Node, Pair, Comment, EmptyLine } = require('./ast') 2 | const { parse, parseLines } = require('./parse') 3 | const { stringify } = require('./stringify') 4 | 5 | module.exports = { 6 | Node, 7 | Pair, 8 | Comment, 9 | EmptyLine, 10 | parse, 11 | parseLines, 12 | stringify 13 | } 14 | -------------------------------------------------------------------------------- /tests/@js.properties/wikipedia-example.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "website": "https://en.wikipedia.org/", 3 | "language": "English", 4 | "message": "Welcome to Wikipedia!", 5 | "key with spaces": "This is the value that could be looked up with the key \"key with spaces\".", 6 | "tab": "\t", 7 | "path": "c:\\wiki\\templates" 8 | } -------------------------------------------------------------------------------- /tests/@js.properties/multiline.properties: -------------------------------------------------------------------------------- 1 | # Multiline element 2 | foo = Aa\ 3 | Bb\ 4 | Cc 5 | 6 | # Multiline key 7 | bar\ 8 | 00\ 9 | 11\ 10 | 22 : baz 11 | 12 | # Multiline separator 13 | qux \ 14 | = \ 15 | quux 16 | 17 | # Multiline key/element/separator 18 | multiline\ 19 | name \ 20 | multiline \ 21 | value 22 | -------------------------------------------------------------------------------- /tests/@js.properties/namespaced.properties.namespaced.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": { 3 | "": "A.B.C", 4 | "bar": "abc", 5 | "baz": "def" 6 | }, 7 | "qux": { 8 | "quux": "quuz" 9 | }, 10 | "a": { 11 | "b1": "a.b1", 12 | "b2": { 13 | "c1": "a.b2.c1", 14 | "c2": "a.b2.c2" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /tests/node-properties-parser/README.md: -------------------------------------------------------------------------------- 1 | The test case in this folder has been extracted from the 2 | **node-properties-parser** project by Xavi Ramirez, at the following URL: 3 | 4 | https://github.com/xavi-/node-properties-parser/tree/fb1b7038380fa295ff80ed0d1a1fad3ad1788738/test 5 | 6 | These files have been available under the MIT license. 7 | -------------------------------------------------------------------------------- /tests/@js.properties/escaped.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "a=b": "c=d=e", 3 | "f:g": "h:i:j", 4 | "k l": "m n o", 5 | "f\to\to": "b\fa\fr", 6 | "f\ro\ro": "b\na\nr", 7 | "aabbccddeef\fgghhiijjkkllmmn\nooppqqr\rsst\tvvwwxxyyzz": "aabbccddeef\fgghhiijjkkllmmn\nooppqqr\rsst\tvvwwxxyyzz", 8 | "uሴu": "u噸u", 9 | "uꯍu": "u췯u", 10 | "UUWXYZU": "UUghijU" 11 | } -------------------------------------------------------------------------------- /tests/@js.properties/separators.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo1": "bar1", 3 | "foo2": "bar2", 4 | "foo3": "bar3", 5 | "foo4": "bar4", 6 | "foo5": "bar5", 7 | "foo6": "bar6", 8 | "foo7": "bar7", 9 | "foo8": "bar8", 10 | "foo9": "bar9", 11 | "foo10": "bar10", 12 | "foo11": "bar11", 13 | "foo12": "", 14 | "foo13": "", 15 | "foo14": "" 16 | } -------------------------------------------------------------------------------- /tests/@js.properties/escaped.properties: -------------------------------------------------------------------------------- 1 | a\=b = c\=d=e 2 | f\:g : h\:i:j 3 | k\ l m\ n o 4 | 5 | f\to\to = b\fa\fr 6 | f\ro\ro = b\na\nr 7 | 8 | a\ab\bc\cd\de\ef\fg\gh\hi\ij\jk\kl\lm\mn\no\op\pq\qr\rs\st\tv\vw\wx\xy\yz\z = a\ab\bc\cd\de\ef\fg\gh\hi\ij\jk\kl\lm\mn\no\op\pq\qr\rs\st\tv\vw\wx\xy\yz\z 9 | u\u1234u = u\u5678u 10 | u\uabcdu = u\ucdefu 11 | 12 | U\UWXYZU = U\UghijU 13 | -------------------------------------------------------------------------------- /tests/@js.properties/separators.properties: -------------------------------------------------------------------------------- 1 | # Separated by equal sign (=) 2 | foo1=bar1 3 | foo2 =bar2 4 | foo3= bar3 5 | foo4 = bar4 6 | 7 | # Separated by colon (:) 8 | foo5:bar5 9 | foo6 :bar6 10 | foo7: bar7 11 | foo8 : bar8 12 | 13 | # Separated by white spaces ([ \t\f]) 14 | foo9 bar9 15 | foo10 bar10 16 | foo11 bar11 17 | 18 | # Separated by new lines ([\r\n]) (empty value) 19 | foo12 20 | foo13 21 | 22 | # Separated by eof (empty value) 23 | foo14 -------------------------------------------------------------------------------- /tests/node-properties-parser/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "lala": "ℊ the foo foo lalala;", 3 | "website": "http://en.wikipedia.org/", 4 | "language": "English", 5 | "message": "Welcome to Wikipedia!", 6 | "key with spaces": "This is the value that could be looked up with the key \"key with spaces\".", 7 | "tab": "\t", 8 | "long-unicode": "\u00000009", 9 | "space separator": "key val \n three", 10 | "another-test": ":: hihi", 11 | "null-prop": "" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [16, 20, latest] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm ci 21 | - run: npm run lint 22 | - run: npx jest --coverage 23 | -------------------------------------------------------------------------------- /tests/node-properties-parser/test.properties: -------------------------------------------------------------------------------- 1 | # You are reading the ".properties" entry. 2 | ! The exclamation mark can also mark text as comments. 3 | lala=\u210A the foo foo \ 4 | lalala; 5 | website = http://en.wikipedia.org/ 6 | language = English 7 | # The backslash below tells the application to continue reading 8 | # the value onto the next line. 9 | message = Welcome to \ 10 | Wikipedia! 11 | # Add spaces to the key 12 | key\ with\ spaces = This is the value that could be looked up with the key "key with spaces". 13 | # Unicode 14 | tab : \u0009 15 | long-unicode : \u00000009 16 | space\ separator key val \n three 17 | another-test ::: hihi 18 | null-prop 19 | -------------------------------------------------------------------------------- /lib/ast.js: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor(type, range) { 3 | this.type = type 4 | if (range) this.range = range 5 | } 6 | } 7 | 8 | class Pair extends Node { 9 | constructor(key, value, range) { 10 | super('PAIR', range) 11 | this.key = key 12 | this.value = value 13 | } 14 | 15 | separator(src) { 16 | if (Array.isArray(this.range) && this.range.length >= 3) { 17 | // eslint-disable-next-line no-unused-vars 18 | const [_, start, end] = this.range 19 | return src.slice(start, end) 20 | } 21 | return null 22 | } 23 | } 24 | 25 | class Comment extends Node { 26 | constructor(comment, range) { 27 | super('COMMENT', range) 28 | this.comment = comment 29 | } 30 | } 31 | 32 | class EmptyLine extends Node { 33 | constructor(range) { 34 | super('EMPTY_LINE', range) 35 | } 36 | } 37 | 38 | module.exports = { Node, Pair, Comment, EmptyLine } 39 | -------------------------------------------------------------------------------- /tests/@js.properties/wikipedia-example.properties: -------------------------------------------------------------------------------- 1 | # You are reading the ".properties" entry. 2 | ! The exclamation mark can also mark text as comments. 3 | # The key characters =, and : should be written with 4 | # a preceding backslash to ensure that they are properly loaded. 5 | # However, there is no need to precede the value characters =, and : by a backslash. 6 | website = https://en.wikipedia.org/ 7 | language = English 8 | # The backslash below tells the application to continue reading 9 | # the value onto the next line. 10 | message = Welcome to \ 11 | Wikipedia! 12 | # Add spaces to the key 13 | key\ with\ spaces = This is the value that could be looked up with the key "key with spaces". 14 | # Unicode 15 | tab : \u0009 16 | # If you want your property to include a backslash, it should be escaped by another backslash 17 | path=c:\\wiki\\templates 18 | # However, some editors will handle this automatically 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-properties", 3 | "version": "1.1.0", 4 | "license": "MIT", 5 | "author": "Eemeli Aro ", 6 | "repository": "github:eemeli/dot-properties", 7 | "description": "Parse & stringify .properties files", 8 | "keywords": [ 9 | "properties", 10 | "java", 11 | "resource", 12 | "bundle", 13 | "i18n" 14 | ], 15 | "types": "./lib/index.d.ts", 16 | "main": "./lib/index.js", 17 | "files": [ 18 | "lib/" 19 | ], 20 | "scripts": { 21 | "lint": "eslint .", 22 | "prettier": "prettier --write .", 23 | "test": "jest" 24 | }, 25 | "jest": { 26 | "collectCoverage": true, 27 | "testMatch": [ 28 | "**/tests/*.js" 29 | ] 30 | }, 31 | "prettier": { 32 | "arrowParens": "avoid", 33 | "semi": false, 34 | "singleQuote": true, 35 | "trailingComma": "none" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^8.9.0", 39 | "eslint-config-prettier": "^9.1.0", 40 | "jest": "^29.7.0", 41 | "prettier": "^3.2.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Eemeli Aro 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/@js.properties/README.md: -------------------------------------------------------------------------------- 1 | The test cases in this folder have been extracted from the 2 | **@js.properties/properties** project, at the following URL: 3 | 4 | https://github.com/jsproperties/properties/tree/9a2d67ab7194d1105b0dc85fbf3ad7b7c3f83212/test/data 5 | 6 | These files have been available under the following license: 7 | 8 | ``` 9 | MIT License 10 | 11 | Copyright (c) 2018 pallxk 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a 14 | copy of this software and associated documentation files (the 15 | "Software"), to deal in the Software without restriction, including 16 | without limitation the rights to use, copy, modify, merge, publish, 17 | distribute, sublicense, and/or sell copies of the Software, and to 18 | permit persons to whom the Software is furnished to do so, subject to 19 | the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included 22 | in all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 25 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 27 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 28 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 29 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 30 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | ``` 32 | -------------------------------------------------------------------------------- /tests/external.tests.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { parse, parseLines, stringify } = require('../lib/index') 4 | 5 | function testCase({ 6 | name, 7 | root, 8 | srcPath, 9 | tgtPath, 10 | options: { parsePath = false } = {} 11 | }) { 12 | test(name, () => { 13 | const src = fs.readFileSync(path.resolve(root, srcPath), 'utf8') 14 | const tgt = fs.readFileSync(path.resolve(root, tgtPath), 'utf8') 15 | const exp = JSON.parse(tgt) 16 | 17 | // parse(string): object 18 | const res = parse(src, parsePath) 19 | expect(res).toMatchObject(exp) 20 | 21 | // stringify(object): string 22 | const src2 = stringify(res) 23 | const res2 = parse(src2, parsePath) 24 | expect(res2).toMatchObject(exp) 25 | 26 | // parseLines(string, true): Node[] 27 | const ast = parseLines(src, true) 28 | const res3 = parse(ast, parsePath) 29 | expect(res3).toMatchObject(exp) 30 | 31 | // stringify(Node[]) 32 | const src3 = stringify(ast) 33 | const res4 = parse(src3, parsePath) 34 | expect(res4).toMatchObject(exp) 35 | }) 36 | } 37 | 38 | describe('@js.properties', () => { 39 | const root = path.resolve(__dirname, '@js.properties') 40 | const tests = fs.readdirSync(root).filter(name => /\.properties$/.test(name)) 41 | tests.forEach(name => { 42 | testCase({ 43 | name, 44 | root, 45 | srcPath: name, 46 | tgtPath: name + '.json' 47 | }) 48 | }) 49 | 50 | testCase({ 51 | name: 'namespaced properties with path', 52 | root, 53 | srcPath: 'namespaced.properties', 54 | tgtPath: 'namespaced.properties.namespaced.json', 55 | options: { parsePath: true } 56 | }) 57 | }) 58 | 59 | describe('node-properties-parser', () => { 60 | testCase({ 61 | name: 'test', 62 | root: path.resolve(__dirname, 'node-properties-parser'), 63 | srcPath: 'test.properties', 64 | tgtPath: 'test.json' 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/folding.tests.js: -------------------------------------------------------------------------------- 1 | const { stringify } = require('../lib/index') 2 | 3 | describe('Folding', () => { 4 | test('long value starts on new line', () => { 5 | const lines = [ 6 | ['k0', '0 val0'], 7 | ['k1', '1 value1'], 8 | ['key', 'v0'], 9 | ['key1', 'value1'], 10 | ['key2', 'value2 continues'] 11 | ] 12 | expect(stringify(lines, { indent: ' ', lineWidth: 8 })).toBe( 13 | `\ 14 | k0 = \\ 15 | 0 val0 16 | k1 = \\ 17 | 1 \\ 18 | value1 19 | key = v0 20 | key1 = \\ 21 | value1 22 | key2 = \\ 23 | value2\\ 24 | \\ \\ 25 | contin\\ 26 | ues` 27 | ) 28 | }) 29 | 30 | test('values after long keys do not start on new line', () => { 31 | const lines = [ 32 | ['longish key', 'v0'], 33 | ['longkeyxx', '42'], 34 | ['somelongkey', 'withlongvalue'] 35 | ] 36 | expect(stringify(lines, { indent: ' ', lineWidth: 8 })).toBe( 37 | `\ 38 | longish\\ \\ 39 | key = \\ 40 | v0 41 | longkeyx\\ 42 | x = 42 43 | somelong\\ 44 | key = \\ 45 | withlo\\ 46 | ngvalu\\ 47 | e` 48 | ) 49 | }) 50 | 51 | describe('foldChars', () => { 52 | const lines = [ 53 | ['key0', '12 345678'], 54 | ['key1', '12.345678'], 55 | ['key2', '12\t345678'], 56 | ['key3', '12\f345678'], 57 | ['key4', ' 12 345678'], 58 | ['key5', '12,345678'] 59 | ] 60 | test('default', () => { 61 | expect(stringify(lines, { indent: ' ', lineWidth: 8 })).toBe( 62 | `\ 63 | key0 = \\ 64 | 12 \\ 65 | 345678 66 | key1 = \\ 67 | 12.\\ 68 | 345678 69 | key2 = \\ 70 | 12\\t\\ 71 | 345678 72 | key3 = \\ 73 | 12\\f\\ 74 | 345678 75 | key4 = \\ 76 | \\ 12 \\ 77 | 345678 78 | key5 = \\ 79 | 12,345\\ 80 | 678` 81 | ) 82 | }) 83 | 84 | test('empty', () => { 85 | expect( 86 | stringify(lines, { indent: ' ', lineWidth: 8, foldChars: '' }) 87 | ).toBe( 88 | `\ 89 | key0 = \\ 90 | 12 345\\ 91 | 678 92 | key1 = \\ 93 | 12.345\\ 94 | 678 95 | key2 = \\ 96 | 12\\t34\\ 97 | 5678 98 | key3 = \\ 99 | 12\\f34\\ 100 | 5678 101 | key4 = \\ 102 | \\ 12 3\\ 103 | 45678 104 | key5 = \\ 105 | 12,345\\ 106 | 678` 107 | ) 108 | }) 109 | 110 | test('custom', () => { 111 | expect( 112 | stringify(lines, { indent: ' ', lineWidth: 8, foldChars: ',' }) 113 | ).toBe( 114 | `\ 115 | key0 = \\ 116 | 12 345\\ 117 | 678 118 | key1 = \\ 119 | 12.345\\ 120 | 678 121 | key2 = \\ 122 | 12\\t34\\ 123 | 5678 124 | key3 = \\ 125 | 12\\f34\\ 126 | 5678 127 | key4 = \\ 128 | \\ 12 3\\ 129 | 45678 130 | key5 = \\ 131 | 12,\\ 132 | 345678` 133 | ) 134 | }) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export class Node { 2 | type: Node.Type 3 | range?: [number, number] | [number, number, number, number] 4 | constructor( 5 | type: Node.Type, 6 | range?: [number, number] | [number, number, number, number] 7 | ) 8 | } 9 | declare namespace Node { 10 | type Type = 'COMMENT' | 'EMPTY_LINE' | 'PAIR' 11 | } 12 | 13 | export class Comment extends Node { 14 | type: 'COMMENT' 15 | comment: string 16 | range?: [number, number] 17 | constructor(comment: string, range?: [number, number]) 18 | } 19 | 20 | export class EmptyLine extends Node { 21 | type: 'EMPTY_LINE' 22 | range?: [number, number] 23 | } 24 | 25 | export class Pair extends Node { 26 | type: 'PAIR' 27 | key: string 28 | value: string 29 | range?: [number, number, number, number] 30 | constructor( 31 | key: string, 32 | value: string, 33 | range?: [number, number, number, number] 34 | ) 35 | separator(src: string): string | null 36 | } 37 | 38 | type Line = string | string[] 39 | interface Tree { 40 | [key: string]: string | Tree 41 | } 42 | 43 | /** 44 | * Splits the input string into an array of logical lines 45 | * 46 | * Key-value pairs are `[key, value]` arrays with string values. Escape 47 | * sequences in keys and values are parsed. Empty lines are included as empty 48 | * strings, and comments as strings that start with `#` or `!` characters. 49 | * Leading whitespace is not included. 50 | */ 51 | export function parseLines(str: string, ast?: false): Line[] 52 | 53 | /** Splits the input string into an array of AST nodes */ 54 | export function parseLines(str: string, ast: true): Required[] 55 | 56 | /** 57 | * Parses an input string read from a .properties file into a JavaScript Object 58 | * 59 | * If the second `path` parameter is true, dots `.` in keys will result in a 60 | * multi-level object (use a string value to customise). If a parent level is 61 | * directly assigned a value while it also has a child with an assigned value, 62 | * the parent value will be assigned to its empty string `''` key. Repeated keys 63 | * will take the last assigned value. Key order is not guaranteed, but is likely 64 | * to match the order of the input lines. 65 | */ 66 | export function parse( 67 | str: string | Line[] | Node[], 68 | path?: boolean | string 69 | ): Tree 70 | 71 | // prettier-ignore 72 | interface StringifyOptions { 73 | commentPrefix?: '# ' | string, // could also use e.g. '!' 74 | defaultKey?: '' | string, // YAML 1.1 used '=' 75 | indent?: ' ' | string, // tabs are also valid 76 | keySep?: ' = ' | string, // should have at most one = or : 77 | latin1?: true | boolean, // default encoding for .properties files 78 | lineWidth?: 80 | number | null, // use null to disable 79 | newline?: '\n' | string, // Windows uses \r\n 80 | pathSep?: '.' | string // if non-default, use the same in parse() 81 | foldChars?: '\f\t .' | string 82 | } 83 | 84 | /** 85 | * Stringifies a hierarchical object or an array of lines to .properties format 86 | * 87 | * If the input is a hierarchical object, keys will consist of the path parts 88 | * joined by `.` characters. With array input, string values represent blank or 89 | * comment lines and string arrays are [key, value] pairs. The characters `\`, 90 | * `\n` and `\r` will be appropriately escaped. If the `latin1` option is not 91 | * set to false, all non-Latin-1 characters will also be `\u` escaped. 92 | * 93 | * Output styling is controlled by the second options parameter; by default a 94 | * spaced `=` separates the key from the value, `\n` is the newline separator, 95 | * lines are folded at 80 characters, with subsequent lines indented by four 96 | * spaces, and comment lines are prefixed with a `#`. `''` as a key value is 97 | * considered the default, and set as the value of a key corresponding to its 98 | * parent object's path. 99 | */ 100 | export function stringify(object: object, options?: StringifyOptions): string 101 | -------------------------------------------------------------------------------- /lib/stringify.js: -------------------------------------------------------------------------------- 1 | const { Pair, Comment, EmptyLine } = require('./ast') 2 | 3 | const escapeNonPrintable = (str, latin1) => 4 | String(str).replace( 5 | latin1 !== false ? /[^\t\n\f\r -~\xa1-\xff]/g : /[\0-\b\v\x0e-\x1f]/g, 6 | ch => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0') 7 | ) 8 | 9 | const escape = str => 10 | String(str) 11 | .replace(/\\/g, '\\\\') 12 | .replace(/\f/g, '\\f') 13 | .replace(/\n/g, '\\n') 14 | .replace(/\r/g, '\\r') 15 | .replace(/\t/g, '\\t') 16 | const escapeKey = key => escape(key).replace(/[ =:]/g, '\\$&') 17 | const escapeValue = value => escape(value).replace(/^ /, '\\ ') 18 | 19 | const prefixComment = (str, prefix) => 20 | str.replace(/^\s*([#!][ \t\f]*)?/g, prefix) 21 | 22 | function fold({ indent, latin1, lineWidth, newline, foldChars }, key, value) { 23 | const printKey = escapeNonPrintable(key, latin1) 24 | const printValue = escapeNonPrintable(value, latin1) 25 | let line = printKey + printValue 26 | if (!lineWidth || lineWidth < 0 || line.length <= lineWidth) return line 27 | let start = 0 28 | let split = undefined 29 | let i = 0 30 | if (key && printKey.length < lineWidth) { 31 | line = printKey + newline + indent + printValue 32 | start = printKey.length + newline.length 33 | i = start + indent.length 34 | } 35 | for (let ch = line[i]; ch; ch = line[++i]) { 36 | let end = i - start >= lineWidth ? split || i : undefined 37 | if (!end) { 38 | switch (ch) { 39 | case '\r': 40 | if (line[i + 1] === '\n') i += 1 41 | // fallthrough 42 | case '\n': 43 | end = i + 1 44 | break 45 | case '\\': 46 | i += 1 47 | switch (line[i]) { 48 | case 'r': 49 | if (line[i + 1] === '\\' && line[i + 2] === 'n') i += 2 50 | // fallthrough 51 | case 'n': 52 | end = i + 1 53 | break 54 | case ' ': 55 | case '=': 56 | case ':': 57 | if (foldChars.includes(line[i])) split = i + 1 58 | break 59 | case 'f': 60 | if (foldChars.includes('\f')) split = i + 1 61 | break 62 | case 't': 63 | if (foldChars.includes('\t')) split = i + 1 64 | break 65 | } 66 | break 67 | default: 68 | if (foldChars.includes(ch)) split = i + 1 69 | } 70 | } 71 | if (end) { 72 | let lineEnd = end 73 | let ch = line[lineEnd - 1] 74 | while (ch === '\n' || ch === '\r') { 75 | lineEnd -= 1 76 | ch = line[lineEnd - 1] 77 | } 78 | const next = line[end] 79 | const atWhitespace = next === '\t' || next === '\f' || next === ' ' 80 | line = 81 | line.slice(0, lineEnd) + 82 | newline + 83 | indent + 84 | (atWhitespace ? '\\' : '') + 85 | line.slice(end) 86 | start = lineEnd + newline.length 87 | split = undefined 88 | i = start + indent.length - 1 89 | } 90 | } 91 | return line 92 | } 93 | 94 | const toLines = (obj, pathSep, defaultKey, prefix = '') => 95 | Object.keys(obj).reduce((lines, key) => { 96 | const value = obj[key] 97 | if (value && typeof value === 'object') { 98 | return lines.concat( 99 | toLines(value, pathSep, defaultKey, prefix + key + pathSep) 100 | ) 101 | } else { 102 | const k = 103 | key === defaultKey ? prefix.slice(0, -pathSep.length) : prefix + key 104 | lines.push([k, value]) 105 | return lines 106 | } 107 | }, []) 108 | 109 | function stringify( 110 | input, 111 | { 112 | commentPrefix = '# ', 113 | defaultKey = '', 114 | indent = ' ', 115 | keySep = ' = ', 116 | latin1 = true, 117 | lineWidth = 80, 118 | newline = '\n', 119 | pathSep = '.', 120 | foldChars = '\f\t .' 121 | } = {} 122 | ) { 123 | if (!input) return '' 124 | if (!Array.isArray(input)) input = toLines(input, pathSep, defaultKey) 125 | const lineOpt = { 126 | indent, 127 | latin1, 128 | lineWidth, 129 | newline: '\\' + newline, 130 | foldChars 131 | } 132 | const commentOpt = { 133 | indent: commentPrefix, 134 | latin1, 135 | lineWidth, 136 | newline, 137 | foldChars 138 | } 139 | return input 140 | .map(line => { 141 | switch (true) { 142 | case !line: 143 | case line instanceof EmptyLine: 144 | return '' 145 | 146 | case Array.isArray(line): 147 | return fold( 148 | lineOpt, 149 | escapeKey(line[0]) + keySep, 150 | escapeValue(line[1]) 151 | ) 152 | case line instanceof Pair: 153 | return fold( 154 | lineOpt, 155 | escapeKey(line.key) + keySep, 156 | escapeValue(line.value) 157 | ) 158 | 159 | case line instanceof Comment: 160 | return fold( 161 | commentOpt, 162 | '', 163 | prefixComment(line.comment, commentPrefix) 164 | ) 165 | default: 166 | return fold( 167 | commentOpt, 168 | '', 169 | prefixComment(String(line), commentPrefix) 170 | ) 171 | } 172 | }) 173 | .join(newline) 174 | } 175 | 176 | module.exports = { stringify } 177 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | const { Pair, Comment, EmptyLine } = require('./ast') 2 | 3 | const atComment = (src, offset) => { 4 | const ch = src[offset] 5 | return ch === '#' || ch === '!' 6 | } 7 | 8 | const atLineEnd = (src, offset) => { 9 | const ch = src[offset] 10 | return !ch || ch === '\r' || ch === '\n' 11 | } 12 | 13 | const endOfIndent = (src, offset) => { 14 | let ch = src[offset] 15 | while (ch === '\t' || ch === '\f' || ch === ' ') { 16 | offset += 1 17 | ch = src[offset] 18 | } 19 | return offset 20 | } 21 | 22 | const endOfComment = (src, offset) => { 23 | let ch = src[offset] 24 | while (ch && ch !== '\r' && ch !== '\n') { 25 | offset += 1 26 | ch = src[offset] 27 | } 28 | return offset 29 | } 30 | 31 | const endOfKey = (src, offset) => { 32 | let ch = src[offset] 33 | while ( 34 | ch && 35 | ch !== '\r' && 36 | ch !== '\n' && 37 | ch !== '\t' && 38 | ch !== '\f' && 39 | ch !== ' ' && 40 | ch !== ':' && 41 | ch !== '=' 42 | ) { 43 | if (ch === '\\') { 44 | if (src[offset + 1] === '\n') { 45 | offset = endOfIndent(src, offset + 2) 46 | } else { 47 | offset += 2 48 | } 49 | } else { 50 | offset += 1 51 | } 52 | ch = src[offset] 53 | } 54 | return offset 55 | } 56 | 57 | const endOfSeparator = (src, offset) => { 58 | let ch = src[offset] 59 | let hasEqSign = false 60 | loop: while ( 61 | ch === '\t' || 62 | ch === '\f' || 63 | ch === ' ' || 64 | ch === '=' || 65 | ch === ':' || 66 | ch === '\\' 67 | ) { 68 | switch (ch) { 69 | case '\\': 70 | if (src[offset + 1] !== '\n') break loop 71 | offset = endOfIndent(src, offset + 2) 72 | break 73 | case '=': 74 | case ':': 75 | if (hasEqSign) break loop 76 | hasEqSign = true 77 | // fallthrough 78 | default: 79 | offset += 1 80 | } 81 | ch = src[offset] 82 | } 83 | return offset 84 | } 85 | 86 | const endOfValue = (src, offset) => { 87 | let ch = src[offset] 88 | while (ch && ch !== '\r' && ch !== '\n') { 89 | offset += ch === '\\' ? 2 : 1 90 | ch = src[offset] 91 | if (ch === '\n' && src[offset - 1] === '\r') { 92 | // escaped CRLF line terminator 93 | offset += 1 94 | ch = src[offset] 95 | } 96 | } 97 | return offset 98 | } 99 | 100 | const unescape = str => 101 | str.replace(/\\(u[0-9a-fA-F]{4}|\r?\n[ \t\f]*|.)?/g, (match, code) => { 102 | switch (code && code[0]) { 103 | case 'f': 104 | return '\f' 105 | case 'n': 106 | return '\n' 107 | case 'r': 108 | return '\r' 109 | case 't': 110 | return '\t' 111 | case 'u': { 112 | const c = parseInt(code.substr(1), 16) 113 | return isNaN(c) ? code : String.fromCharCode(c) 114 | } 115 | case '\r': 116 | case '\n': 117 | case undefined: 118 | return '' 119 | default: 120 | return code 121 | } 122 | }) 123 | 124 | function parseLines(src, ast) { 125 | const lines = [] 126 | for (let i = 0; i < src.length; ++i) { 127 | if (src[i] === '\n' && src[i - 1] === '\r') i += 1 128 | if (!src[i]) break 129 | const keyStart = endOfIndent(src, i) 130 | if (atLineEnd(src, keyStart)) { 131 | lines.push(ast ? new EmptyLine([i, keyStart]) : '') 132 | i = keyStart 133 | continue 134 | } 135 | if (atComment(src, keyStart)) { 136 | const commentEnd = endOfComment(src, keyStart) 137 | const comment = src.slice(keyStart, commentEnd) 138 | lines.push(ast ? new Comment(comment, [keyStart, commentEnd]) : comment) 139 | i = commentEnd 140 | continue 141 | } 142 | const keyEnd = endOfKey(src, keyStart) 143 | const key = unescape(src.slice(keyStart, keyEnd)) 144 | const valueStart = endOfSeparator(src, keyEnd) 145 | if (atLineEnd(src, valueStart)) { 146 | lines.push( 147 | ast 148 | ? new Pair(key, '', [keyStart, keyEnd, valueStart, valueStart]) 149 | : [key, ''] 150 | ) 151 | i = valueStart 152 | continue 153 | } 154 | const valueEnd = endOfValue(src, valueStart) 155 | const value = unescape(src.slice(valueStart, valueEnd)) 156 | lines.push( 157 | ast 158 | ? new Pair(key, value, [keyStart, keyEnd, valueStart, valueEnd]) 159 | : [key, value] 160 | ) 161 | i = valueEnd 162 | } 163 | return lines 164 | } 165 | 166 | function addPair(res, key, value, pathSep) { 167 | if (!pathSep) { 168 | res[key] = value 169 | return 170 | } 171 | 172 | const keyPath = key.split(pathSep) 173 | let parent = res 174 | while (keyPath.length >= 2) { 175 | const p = keyPath.shift() 176 | if (p === '__proto__') return 177 | if (!parent[p]) { 178 | parent[p] = {} 179 | } else if (typeof parent[p] !== 'object') { 180 | parent[p] = { '': parent[p] } 181 | } 182 | parent = parent[p] 183 | } 184 | const leaf = keyPath[0] 185 | if (typeof parent[leaf] === 'object') { 186 | parent[leaf][''] = value 187 | } else { 188 | parent[leaf] = value 189 | } 190 | } 191 | 192 | function parse(src, path) { 193 | const pathSep = path ? (typeof path === 'string' ? path : '.') : null 194 | const lines = Array.isArray(src) ? src : parseLines(src, false) 195 | const res = {} 196 | for (const line of lines) { 197 | if (line instanceof Pair) addPair(res, line.key, line.value, pathSep) 198 | else if (Array.isArray(line)) addPair(res, line[0], line[1], pathSep) 199 | } 200 | return res 201 | } 202 | 203 | module.exports = { parse, parseLines } 204 | -------------------------------------------------------------------------------- /tests/corner-cases.tests.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { Pair, parse, parseLines, stringify } = require('../lib/index') 4 | 5 | describe('lines', () => { 6 | const srcPath = path.resolve( 7 | __dirname, 8 | 'node-properties-parser', 9 | 'test.properties' 10 | ) 11 | const src = fs.readFileSync(srcPath, 'utf8') 12 | const exp = [ 13 | '# You are reading the ".properties" entry.', 14 | '! The exclamation mark can also mark text as comments.', 15 | ['lala', '\u210A the foo foo lalala;'], 16 | ['website', 'http://en.wikipedia.org/'], 17 | ['language', 'English'], 18 | '# The backslash below tells the application to continue reading', 19 | '# the value onto the next line.', 20 | ['message', 'Welcome to Wikipedia!'], 21 | '# Add spaces to the key', 22 | [ 23 | 'key with spaces', 24 | 'This is the value that could be looked up with the key "key with spaces".' 25 | ], 26 | '# Unicode', 27 | ['tab', '\u0009'], 28 | ['long-unicode', '\u00000009'], 29 | ['space separator', 'key val \n three'], 30 | ['another-test', ':: hihi'], 31 | ['null-prop', ''] 32 | ] 33 | 34 | test('read lines', () => { 35 | const lines = parseLines(src + '\n\n') 36 | expect(lines).toMatchObject([...exp, '', '']) 37 | }) 38 | 39 | test('read lines with CRLF endings', () => { 40 | const src = `language = English\r\nmessage = Welcome to \\\r\n Wikipedia!\r\n\r\n` 41 | const lines = parseLines(src) 42 | expect(lines).toMatchObject([ 43 | ['language', 'English'], 44 | ['message', 'Welcome to Wikipedia!'], 45 | '' 46 | ]) 47 | }) 48 | 49 | test('write lines', () => { 50 | const str = stringify(exp) 51 | expect(parseLines(str)).not.toMatchObject(exp) 52 | const fix = str.replace('# The exclamation mark', '! The exclamation mark') 53 | expect(parseLines(fix)).toMatchObject(exp) 54 | }) 55 | }) 56 | 57 | describe('stringify', () => { 58 | test('empty input', () => { 59 | const res = stringify(null) 60 | expect(res).toBe('') 61 | }) 62 | 63 | test('ascii', () => { 64 | const src = 'ipsum áé ĐѺ lore\0' 65 | const exp = 'ipsum áé \\u0110\\u047a lore\\u0000' 66 | const res0 = stringify([['', src]], { keySep: '' }) 67 | const res1 = stringify([['', src]], { latin1: false, keySep: '' }) 68 | expect(res0).toBe(exp) 69 | expect(res1).toBe(src.slice(0, -1) + '\\u0000') 70 | }) 71 | 72 | test('\\\\ overdose', () => { 73 | const slash = '\\'.repeat(200) 74 | expect(() => stringify([[slash + slash]])).not.toThrow() 75 | }) 76 | 77 | test('manual line breaks', () => { 78 | const lorem = `\r 79 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed\r 80 | do eiusmod tempor incididunt ut labore et dolore magna\r 81 | aliqua. Ut enim ad minim veniam, quis nostrud exercitation\r 82 | ullamco laboris nisi ut aliquip ex ea commodo consequat.\r 83 | Duis aute irure dolor in reprehenderit in voluptate velit\r 84 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint\r 85 | occaecat cupidatat non proident, sunt in culpa qui officia\r 86 | deserunt mollit anim id est laborum.` 87 | expect(stringify([lorem])).toBe(lorem.replace(/\r\n/gm, '\n# ').trim()) 88 | expect(stringify([['key', lorem]])).toBe( 89 | 'key = \\\n ' + lorem.replace(/\r\n/g, '\\r\\n\\\n ') 90 | ) 91 | }) 92 | 93 | test('lines with empty strings result in blank lines', () => { 94 | const emptyLine = '' 95 | expect(stringify([emptyLine])).toBe(emptyLine) 96 | const lines = [['key1', 'value1'], '', ['key2', 'value2']] 97 | expect(stringify(lines)).toBe('key1 = value1\n\nkey2 = value2') 98 | }) 99 | 100 | test('empty comments', () => { 101 | const lines = [['key1', 'value1'], '#', '! ', ['key2', 'value2']] 102 | expect(stringify(lines)).toBe('key1 = value1\n# \n# \nkey2 = value2') 103 | }) 104 | 105 | test('Negative linewidth', () => { 106 | const foo = 'foo '.repeat(200) 107 | expect(stringify([foo], { lineWidth: -1 })).toBe(`# ${foo}`) 108 | }) 109 | 110 | test('Whitespace in comment at folding point', () => { 111 | const lines = ['comment\rcomment\ncomment\fcomment\t'] 112 | expect(stringify(lines, { lineWidth: 10 })).toBe( 113 | '# comment\n# comment\n# comment\f\n# comment\t' 114 | ) 115 | }) 116 | }) 117 | 118 | describe('options', () => { 119 | const obj = { 120 | '': 'root', 121 | a: { '': 'A.', a: 'A.A' }, 122 | b: { '': 'B', b: 'B.B' } 123 | } 124 | 125 | test('read default values', () => { 126 | const src = `:root 127 | a:A 128 | a.:A. 129 | a.a:A.A 130 | b.b:B.B 131 | b:B` 132 | expect(parse(src, true)).toMatchObject(obj) 133 | }) 134 | 135 | test('write default values', () => { 136 | const src = `:root 137 | a:A. 138 | a.a:A.A 139 | b:B 140 | b.b:B.B` 141 | expect(stringify(obj, { keySep: ':' })).toBe(src) 142 | }) 143 | 144 | test('custom path separator', () => { 145 | const src = `:root 146 | a:A 147 | a/:A. 148 | a/a:A.A 149 | b/b:B.B 150 | b:B` 151 | expect(parse(src, '/')).toMatchObject(obj) 152 | }) 153 | }) 154 | 155 | describe('bad input', () => { 156 | test('malformed unicode escape', () => { 157 | const src = `foo: \\uabcx` 158 | expect(parse(src, true)).toEqual({ foo: 'uabcx' }) 159 | }) 160 | 161 | test('prototype pollution', () => { 162 | const src = '__proto__.pollutedKey = pollutedValue' 163 | expect(parse(src, true)).toEqual({}) 164 | expect({}.pollutedKey).not.toBeDefined() 165 | }) 166 | }) 167 | 168 | describe('AST', () => { 169 | test('pair separator', () => { 170 | const src = 'key:\nkey2: value2' 171 | const ast = parseLines(src, true) 172 | expect(ast[0].separator(src)).toBe(':') 173 | expect(ast[1].separator(src)).toBe(': ') 174 | }) 175 | 176 | test('add node', () => { 177 | const src = 'key:\nkey2: value2' 178 | const ast = parseLines(src, true) 179 | ast.push(new Pair('key3', 'value3')) 180 | expect(ast[2].separator(src)).toBeNull() 181 | 182 | expect(parse(ast)).toMatchObject({ 183 | key: '', 184 | key2: 'value2', 185 | key3: 'value3' 186 | }) 187 | expect(stringify(ast)).toBe('key = \nkey2 = value2\nkey3 = value3') 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dot-properties 2 | 3 | JavaScript `parse()` and `stringify()` for `.properties` (`text/x-java-properties`) files as defined in [java.util.Properties](https://docs.oracle.com/javase/9/docs/api/java/util/Properties.html#load-java.io.Reader-). 4 | 5 | To install: 6 | 7 | ``` 8 | npm install dot-properties 9 | ``` 10 | 11 | For usage examples, see [below](#example) or take a look through the project's [test suite](tests/). 12 | 13 | ## API 14 | 15 | ### `parse(str: string | Line[] | Node[], path?: boolean | string): object` 16 | 17 | Parses an input string read from a .properties file into a JavaScript Object 18 | 19 | If the second `path` parameter is true, dots `.` in keys will result in a multi-level object (use a string value to customise). If a parent level is directly assigned a value while it also has a child with an assigned value, the parent value will be assigned to its empty string `''` key. Repeated keys will take the last assigned value. Key order is not guaranteed, but is likely to match the order of the input lines. 20 | 21 | ### `parseLines(str: string, ast?: false): Line[]` 22 | 23 | Splits the input string into an array of logical lines; useful if you want to preserve order, comments and/or empty lines while processing. Used internally by `parse()`. 24 | 25 | Key-value pairs are `[key, value]` arrays with string values. Escape sequences in keys and values are parsed. Empty lines are included as empty strings `''`, and comments as strings that start with `#` or `!` characters. Leading whitespace is not included. 26 | 27 | ### `parseLines(str: string, ast: true): Node[]` 28 | 29 | Splits the input string into an array of AST nodes; see [the source](./lib/ast.js) for more details. 30 | 31 | ### `stringify(input: object, options?: StringifyOptions): string` 32 | 33 | Stringifies a hierarchical object or an array of lines or nodes to `.properties` format 34 | 35 | If `input` is a hierarchical object, keys will consist of the path parts joined by `.` characters. With array input, string values represent blank or comment lines and string arrays are `[key, value]` pairs. Control characters and `\` will be appropriately escaped. If the `latin1` option is not set to false, all non-Latin-1 characters will also be `\u` escaped. Non-empty string lines represent comments, and will have any existing `#` or `!` prefix replaced by the `commentPrefix`. 36 | 37 | Output styling is controlled by the second (optional) `options` parameter; by default a spaced `=` separates the key from the value, `\n` is the newline separator, lines are folded at 80 characters (at most, splitting at nice places), with subsequent lines indented by four spaces, and comment lines are prefixed with a `#`. `''` as a key value is considered the default, and set as the value of a key corresponding to its parent object's path: 38 | 39 | 40 | ```js 41 | const defaultOptions = { 42 | commentPrefix: '# ', // could also use e.g. '!' 43 | defaultKey: '', // YAML 1.1 used '=' 44 | indent: ' ', // tabs are also valid 45 | keySep: ' = ', // should have at most one = or : 46 | latin1: true, // default encoding for .properties files 47 | lineWidth: 80, // use null to disable 48 | newline: '\n', // Windows uses \r\n 49 | pathSep: '.', // if non-default, use the same in parse() 50 | foldChars: '\f\t .' // preferred characters for line folding 51 | } 52 | ``` 53 | 54 | ## Example 55 | 56 | ### `example.properties` 57 | 58 | ``` 59 | # You are reading the ".properties" entry. 60 | ! The exclamation mark can also mark text as comments. 61 | # The key characters =, and : should be written with a preceding 62 | # backslash to ensure that they are properly loaded. However, there 63 | # is no need to precede the value characters =, and : by a backslash. 64 | website = https://en.wikipedia.org/ 65 | language = English 66 | 67 | # The backslash below tells the application to continue reading 68 | # the value onto the next line. 69 | message = Welcome to \ 70 | Wikipedia! 71 | 72 | # Add spaces to the key 73 | key\ with\ spaces = This is the value that could be looked up with \ 74 | the key "key with spaces". 75 | 76 | # Unicode 77 | tab : \u0009 78 | 79 | # If you want your property to include a backslash, it should be 80 | # escaped by another backslash 81 | path c:\\wiki\\templates 82 | # However, some editors will handle this automatically 83 | ``` 84 | 85 | _Source: [Wikipedia](https://en.wikipedia.org/wiki/.properties)_ 86 | 87 | ### `example.js` 88 | 89 | ```js 90 | const fs = require('fs') 91 | const { parse, parseLines, stringify } = require('dot-properties') 92 | const src = fs.readFileSync('./example.properties', 'utf8') 93 | 94 | const obj = parse(src) 95 | // { website: 'https://en.wikipedia.org/', 96 | // language: 'English', 97 | // message: 'Welcome to Wikipedia!', 98 | // 'key with spaces': 99 | // 'This is the value that could be looked up with the key "key with spaces".', 100 | // tab: '', 101 | // path: 'c:wiki\templates' } 102 | 103 | const str = stringify(obj, { lineWidth: 60 }) 104 | console.log(str) 105 | // website = https://en.wikipedia.org/ 106 | // language = English 107 | // message = Welcome to Wikipedia! 108 | // key\ with\ spaces = This is the value that could be looked \ 109 | // up with the key "key with spaces". 110 | // tab = \t 111 | // path = c:\\wiki\\templates 112 | 113 | const lines = parseLines(src) 114 | // [ '# You are reading the ".properties" entry.', 115 | // '! The exclamation mark can also mark text as comments.', 116 | // '# The key characters =, and : should be written with a preceding', 117 | // '# backslash to ensure that they are properly loaded. However, there', 118 | // '# is no need to precede the value characters =, and : by a backslash.', 119 | // [ 'website', 'https://en.wikipedia.org/' ], 120 | // [ 'language', 'English' ], 121 | // '', 122 | // '# The backslash below tells the application to continue reading', 123 | // '# the value onto the next line.', 124 | // [ 'message', 'Welcome to Wikipedia!' ], 125 | // '', 126 | // '# Add spaces to the key', 127 | // [ 'key with spaces', 128 | // 'This is the value that could be looked up with the key "key with spaces".' ], 129 | // '', 130 | // '# Unicode', 131 | // [ 'tab', '' ], 132 | // '', 133 | // '# If you want your property to include a backslash, it should be', 134 | // '# escaped by another backslash', 135 | // [ 'path', 'c:wiki\templates' ], 136 | // '# However, some editors will handle this automatically' ] 137 | 138 | const str2 = stringify(lines.slice(8, 14), { lineWidth: 60 }) 139 | console.log(str2) 140 | // # The backslash below tells the application to continue 141 | // # reading 142 | // # the value onto the next line. 143 | // message = Welcome to Wikipedia! 144 | // 145 | // # Add spaces to the key 146 | // key\ with\ spaces = This is the value that could be looked \ 147 | // up with the key "key with spaces". 148 | ``` 149 | --------------------------------------------------------------------------------