diff --git a/src/configparser.js b/src/configparser.js index fdd25a2..fce8275 100644 --- a/src/configparser.js +++ b/src/configparser.js @@ -10,14 +10,19 @@ const interpolation = require('./interpolation'); * @type {RegExp} * @private */ -const SECTION = new RegExp(/^\s*\[([^\]]+)]$/); +const SECTION = new RegExp(/^(?\s*)\[(?[^\]]+)]$/); /** * Regular expression to match key, value pairs. * @type {RegExp} * @private */ -const KEY = new RegExp(/^\s*(.*?)\s*[=:]\s*(.*)$/); +const KEY = new RegExp(/^(?\s*)(?.*?)\s*[=:]\s*(?.*)$/); + +/** + * Regular expression to match second+ lines in a multiline value. + */ +const MULTILINE_VALUE = new RegExp(/^(?\s*)(?.*?)$/); /** * Regular expression to match comments. Either starting with a @@ -34,7 +39,6 @@ const LINE_BOUNDARY = new RegExp(/\r\n|[\n\r\u0085\u2028\u2029]/g); const readFileAsync = util.promisify(fs.readFile); const writeFileAsync = util.promisify(fs.writeFile); -const statAsync = util.promisify(fs.stat); const mkdirAsync = util.promisify(mkdirp); /** @@ -247,23 +251,37 @@ ConfigParser.prototype.writeAsync = async function(file, createMissingDirs = fal function parseLines(file, lines) { let curSec = null; + let curIndent = null; + let curKey = null; lines.forEach((line, lineNumber) => { if(!line || line.match(COMMENT)) return; let res = SECTION.exec(line); if(res){ - const header = res[1]; + const header = res.groups.sectionName; + curIndent = res.groups.indent; curSec = {}; this._sections[header] = curSec; } else if(!curSec) { throw new errors.MissingSectionHeaderError(file, lineNumber, line); } else { res = KEY.exec(line); + const multilineRes = MULTILINE_VALUE.exec(line); if(res){ - const key = res[1]; - curSec[key] = res[2]; - } else { - throw new errors.ParseError(file, lineNumber, line); + const key = res.groups.key; + curSec[key] = res.groups.value; + curKey = key; + return; + } else if (multilineRes) { + const indent = multilineRes.groups.indent; + // If current line is more indented than the section header, + // we know it's a multiline string, in which case we append + // the line to the last key's value. + if (indent.indexOf(curIndent) !== -1 && indent.length > curIndent.length) { + curSec[curKey] += '\n' + multilineRes.groups.value; + return; + } } + throw new errors.ParseError(file, lineNumber, line); } }); } @@ -279,6 +297,9 @@ function getSectionsAsString() { for(key in keys){ if(!keys.hasOwnProperty(key)) continue; let value = keys[key]; + // For multiline values, add a tab indent for subsequent lines, + // and normalize the line boundary with *nix style line endings. + value = value.replace(LINE_BOUNDARY, '\n\t'); out += (key + '=' + value + '\n'); } out += '\n'; diff --git a/test/configparser.js b/test/configparser.js index 7cba242..b6981ac 100644 --- a/test/configparser.js +++ b/test/configparser.js @@ -1,3 +1,7 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + const expect = require('chai').expect; const ConfigParser = require('../src/configparser'); @@ -14,7 +18,10 @@ describe('ConfigParser object', function(){ 'interpolation', 'permissive_section:headers%!?', 'more_complex_interpolation', - 'sec' + 'sec', + 'multiline_value', + 'multiline_value_with_empty_first_line', + 'indented_section_with_multiline_value' ]); }); @@ -39,11 +46,11 @@ describe('ConfigParser object', function(){ }); it('should indicate if a section has a key', function(){ - expect(config.hasKey('section1', 'idontknow')).to.equal(true); - expect(config.hasKey('section1', 'fakekey')).to.equal(false); - expect(config.hasKey('fake section', 'fakekey')).to.equal(false); - expect(config.hasKey('permissive_section:headers%!?', 'hello')).to.equal(true); - expect(config.hasKey('sec', 'key')).to.equal(true); + expect(config.hasKey('section1', 'idontknow')).to.equal(true); + expect(config.hasKey('section1', 'fakekey')).to.equal(false); + expect(config.hasKey('fake section', 'fakekey')).to.equal(false); + expect(config.hasKey('permissive_section:headers%!?', 'hello')).to.equal(true); + expect(config.hasKey('sec', 'key')).to.equal(true); }); it('should get the value for a key in the named section', function(){ @@ -90,4 +97,57 @@ describe('ConfigParser object', function(){ expect(config.removeSection('new-section')).to.equal(true); expect(config.hasSection('new-section')).to.equal(false); }); -}); \ No newline at end of file + + it('should handle multiline strings correctly', function(){ + expect(config.get('multiline_value', 'key')).to.equal('this is a\nstring that spans\nmultiple lines'); + expect(config.get('multiline_value_with_empty_first_line', 'key')).to.equal('\nthis is a\nstring that spans\nmultiple lines'); + expect(config.get('indented_section_with_multiline_value', 'key')).to.equal('this is yet another\nstring that spans\nmultiple lines'); + }); + +}); + +describe('ConfigParser write', function() { + let tempFilePath; + let tempDirPath; + + before(function() { + // Create temporary directory and file for testing purpose + tempDirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-')); + tempFilePath = path.join(tempDirPath, 'temp.ini'); + }); + + after(function() { + // Delete the temporary file + fs.unlinkSync(tempFilePath); + }); + + it('should write to a file', function() { + const config = new ConfigParser(); + + // Modify the configparser + config.addSection('section1'); + config.set('section1', 'key', 'new value'); + config.set('section1', 'another_key', 'new value'); + + // Write the configparser to the file + config.write(tempFilePath); + + // Read the file and verify the content + const fileContent = fs.readFileSync(tempFilePath, 'utf8'); + expect(fileContent).to.equal('[section1]\nkey=new value\nanother_key=new value\n\n'); + }); + + it('should handle multiline values', function() { + const config = new ConfigParser(); + config.addSection('section1'); + // Modify the configparser with multiline values + config.set('section1', 'multiline_key', 'this is a\nmultiline value\nwith three lines'); + + // Write the configparser to the file + config.write(tempFilePath); + + // Read the file and verify the content + const fileContent = fs.readFileSync(tempFilePath, 'utf8'); + expect(fileContent).to.equal('[section1]\nmultiline_key=this is a\n\tmultiline value\n\twith three lines\n\n'); + }); +}); diff --git a/test/data/file.ini b/test/data/file.ini index a99330d..49e2089 100644 --- a/test/data/file.ini +++ b/test/data/file.ini @@ -40,4 +40,20 @@ file=mytextfile.txt [sec] key = [value] test = [something 1234 hello] -[wow] = [45] \ No newline at end of file +[wow] = [45] + +[multiline_value] +key=this is a + string that spans + multiple lines + +[multiline_value_with_empty_first_line] +key= + this is a + string that spans + multiple lines + + [indented_section_with_multiline_value] + key=this is yet another + string that spans + multiple lines