diff --git a/lib/parser.js b/lib/parser.js index a61e3e9d..3a99a9bf 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -2,8 +2,7 @@ var extended = require("./extended"), isUndefinedOrNull = extended.isUndefinedOrNull, trim = extended.trim, trimLeft = extended.trimLeft, - trimRight = extended.trimRight, - LINE_BREAK = extended.LINE_BREAK; + trimRight = extended.trimRight; function createParser(options) { options = options || {}; @@ -13,9 +12,10 @@ function createParser(options) { doTrim = options.trim || false, ESCAPE = options.quote || '"', VALUE_REGEXP = new RegExp("([^" + delimiter + "'\"\\s\\\\]*(?:\\s+[^" + delimiter + "'\"\\s\\\\]+)*)"), - SEARCH_REGEXP = new RegExp("(?:\\n|" + delimiter + ")"), + SEARCH_REGEXP = new RegExp("(?:\\n|\\r|" + delimiter + ")"), ESCAPE_CHAR = options.escape || '"', - NEXT_TOKEN_REGEXP = new RegExp("([^\\s]|\\\n|" + delimiter + ")"); + NEXT_TOKEN_REGEXP = new RegExp("([^\\s]|\\n|\\r|" + delimiter + ")"), + LINE_BREAK = /[\r\n]/; function formatItem(item) { if (doTrim) { @@ -67,11 +67,11 @@ function createParser(options) { if (hasMoreData) { cursor = null; } else { - throw new Error("Parse Error: expected: '" + ESCAPE + "' got: '" + nextToken + "'. at '" + str.substr(cursor).replace(/\n/g, "\\n" + "'")); + throw new Error("Parse Error: expected: '" + ESCAPE + "' got: '" + nextToken + "'. at '" + str.substr(cursor).replace(/[r\n]/g, "\\n" + "'")); } } else if ((!depth && nextToken && nextToken.search(SEARCH_REGEXP) === -1)) { - throw new Error("Parse Error: expected: '" + ESCAPE + "' got: '" + nextToken + "'. at '" + str.substr(cursor, 10).replace(/\n/g, "\\n" + "'")); - } else if (hasMoreData && (!nextToken || nextToken.search(LINE_BREAK) === -1)) { + throw new Error("Parse Error: expected: '" + ESCAPE + "' got: '" + nextToken + "'. at '" + str.substr(cursor, 10).replace(/[\r\n]/g, "\\n" + "'")); + } else if (hasMoreData && (!nextToken || !LINE_BREAK.test(nextToken))) { cursor = null; } if (cursor !== null) { @@ -98,7 +98,7 @@ function createParser(options) { items.push(formatItem(searchStr.substr(0, nextIndex))); cursor += nextIndex + 1; } - } else if (nextChar.search(LINE_BREAK) !== -1) { + } else if (LINE_BREAK.test(nextChar)) { items.push(formatItem(searchStr.substr(0, nextIndex))); cursor += nextIndex; } else if (!hasMoreData) { @@ -127,7 +127,7 @@ function createParser(options) { if (isUndefinedOrNull(token)) { i = lastLineI; break; - } else if (token === LINE_BREAK) { + } else if (LINE_BREAK.test(token)) { i = nextToken.cursor + 1; if (i < l) { rows.push(items); diff --git a/lib/parser_stream.js b/lib/parser_stream.js index a903fa12..20760673 100644 --- a/lib/parser_stream.js +++ b/lib/parser_stream.js @@ -61,7 +61,7 @@ extended(ParserStream).extend({ __handleLine: function __parseLineData(line, index, ignore) { var ignoreEmpty = this._ignoreEmpty; - if (extended.isBoolean(ignoreEmpty) && ignoreEmpty && EMPTY.test(line.join(""))) { + if (extended.isBoolean(ignoreEmpty) && ignoreEmpty && (!line || EMPTY.test(line.join("")))) { return null; } if (!ignore) { diff --git a/package.json b/package.json index ee5ca21a..678e7918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fast-csv", - "version": "0.2.0", + "version": "0.2.1", "description": "CSV parser and writer", "main": "index.js", "scripts": { diff --git a/test/parser.test.js b/test/parser.test.js index e70f8a85..00461587 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -4,199 +4,402 @@ var it = require("it"), it.describe("fast-csv parser", function (it) { - it.describe("unescaped data", function (it) { - - it.should("parse a block of CSV text", function () { - var data = "first_name,last_name,email_address\nFirst1,Last1,email1@email.com"; - var myParser = parser({delimiter: ","}); - assert.deepEqual(myParser(data, false), {"line": "", "rows": [ - ["first_name", "last_name", "email_address"], - ["First1", "Last1", "email1@email.com"] - ]}); - }); + it.describe("with \\n", function (it) { + + it.describe("unescaped data", function (it) { - it.should("return the rest of the line if there is more data", function () { - var data = "first_name,last_name,email_address\nFirst1,Last1,email1@email.com"; - var myParser = parser({delimiter: ","}); - assert.deepEqual(myParser(data, true), { - "line": "First1,Last1,email1@email.com", - "rows": [ - ["first_name", "last_name", "email_address"] - ] + it.should("parse a block of CSV text", function () { + var data = "first_name,last_name,email_address\nFirst1,Last1,email1@email.com"; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First1", "Last1", "email1@email.com"] + ]}); }); - }); - it.should("accept new data and return the result", function () { - var data = "first_name,last_name,email_address\nFirst1,Last1,email1@email.com,"; - var myParser = parser({delimiter: ","}); - var parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": "First1,Last1,email1@email.com,", - "rows": [ - ["first_name", "last_name", "email_address"] - ] - }); - assert.deepEqual(myParser(parsedData.line + "\nFirst2,Last2,email2@email.com", false), {"line": "", "rows": [ - ["First1", "Last1", "email1@email.com"], - ["First2", "Last2", "email2@email.com"] - ]}); - }); + it.should("return the rest of the line if there is more data", function () { + var data = "first_name,last_name,email_address\nFirst1,Last1,email1@email.com"; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, true), { + "line": "First1,Last1,email1@email.com", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + }); - it.should("not parse a row if a new line is not found and there is more data", function () { - var data = "first_name,last_name,email_address"; - var myParser = parser({delimiter: ","}); - var parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": "first_name,last_name,email_address", - "rows": [] + it.should("accept new data and return the result", function () { + var data = "first_name,last_name,email_address\nFirst1,Last1,email1@email.com,"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "First1,Last1,email1@email.com,", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + assert.deepEqual(myParser(parsedData.line + "\nFirst2,Last2,email2@email.com", false), {"line": "", "rows": [ + ["First1", "Last1", "email1@email.com"], + ["First2", "Last2", "email2@email.com"] + ]}); + }); + + it.should("not parse a row if a new line is not found and there is more data", function () { + var data = "first_name,last_name,email_address"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "first_name,last_name,email_address", + "rows": [] + }); }); - }); - it.should("not parse a row if there is a trailing delimiter and there is more data", function () { - var data = "first_name,last_name,email_address,"; - var myParser = parser({delimiter: ","}); - var parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": "first_name,last_name,email_address,", - "rows": [] + it.should("not parse a row if there is a trailing delimiter and there is more data", function () { + var data = "first_name,last_name,email_address,"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "first_name,last_name,email_address,", + "rows": [] + }); }); + + it.should("parse a row if a new line is found and there is more data", function () { + var data = "first_name,last_name,email_address\n"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + }); + }); - it.should("parse a row if a new line is found and there is more data", function () { - var data = "first_name,last_name,email_address\n"; - var myParser = parser({delimiter: ","}); - var parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": "", - "rows": [ - ["first_name", "last_name", "email_address"] - ] + it.describe("escaped values", function (it) { + + it.should("parse a block of CSV text", function () { + var data = 'first_name,last_name,email_address\n"First,1","Last,1","email1@email.com"'; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First,1", "Last,1", "email1@email.com"] + ]}); + }); + + it.should("parse a block of CSV text with escaped escaped char", function () { + var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com"'; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First,\"1\"", "Last,\"1\"", "email1@email.com"] + ]}); + }); + + it.should("parse a block of CSV text with alternate escape char", function () { + var data = 'first_name,last_name,email_address\n"First,\\"1\\"","Last,\\"1\\"","email1@email.com"'; + var myParser = parser({delimiter: ",", escape: "\\"}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First,\"1\"", "Last,\"1\"", "email1@email.com"] + ]}); + }); + + it.should("return the rest of the line if a complete value is not found", function () { + var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com'; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, true), { + "line": '"First,""1""","Last,""1""","email1@email.com', + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + }); + + it.should("accept more data appended to the returned line with escaped values", function () { + var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com'; + var myParser = parser({delimiter: ","}), + parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"First,""1""","Last,""1""","email1@email.com', + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + assert.deepEqual(myParser(parsedData.line + '"\n"First,""2""","Last,""2""","email2@email.com"', false), { + line: "", + rows: [ + ["First,\"1\"", "Last,\"1\"", "email1@email.com"], + ["First,\"2\"", "Last,\"2\"", "email2@email.com"] + ] + }); + }); + + it.should("throw an error if there is not more data and there is an invalid escape sequence", function () { + var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com'; + var myParser = parser({delimiter: ","}), + parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"First,""1""","Last,""1""","email1@email.com', + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + assert.throws(function () { + assert.deepEqual(myParser(parsedData.line + '\n"First,"",2""","Last""2""","email2@email.com"', false), { + line: "", + rows: [ + ["First,\"1\"", "Last,\"1\"", "email1@email.com"], + ["First,\"2\"", "Last,\"2\"", "email2@email.com"] + ] + }); + }, Error, ' Parse Error: expected: \'"\' got: \'F\'. at \'First,""2""","Last""2""","email2@email.com"'); + }); + + it.should("handle empty values properly", function () { + var data = '"","",""\n,Last4,email4@email.com'; + var myParser = parser({delimiter: ","}), + parsedData = myParser(data, false); + assert.deepEqual(parsedData, {"line": "", "rows": [ + ["", "", ""], + ["", "Last4", "email4@email.com"] + ]}); + }); + + it.should("not parse a row if a new line is not found and there is more data", function () { + var data = '"first_name","last_name","email_address"'; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"first_name","last_name","email_address"', + "rows": [] + }); + }); + + it.should("not parse a row if there is a trailing delimiter and there is more data", function () { + var data = '"first_name","last_name","email_address",'; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"first_name","last_name","email_address",', + "rows": [] + }); + }); + + it.should("parse a row if a new line is found and there is more data", function () { + var data = '"first_name","last_name","email_address"\n'; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); }); }); }); - it.describe("escaped values", function (it) { + it.describe("with \\r", function (it) { - it.should("parse a block of CSV text", function () { - var data = 'first_name,last_name,email_address\n"First,1","Last,1","email1@email.com"'; - var myParser = parser({delimiter: ","}); - assert.deepEqual(myParser(data, false), {"line": "", "rows": [ - ["first_name", "last_name", "email_address"], - ["First,1", "Last,1", "email1@email.com"] - ]}); - }); + it.describe("unescaped data", function (it) { - it.should("parse a block of CSV text with escaped escaped char", function () { - var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com"'; - var myParser = parser({delimiter: ","}); - assert.deepEqual(myParser(data, false), {"line": "", "rows": [ - ["first_name", "last_name", "email_address"], - ["First,\"1\"", "Last,\"1\"", "email1@email.com"] - ]}); - }); + it.should("parse a block of CSV text", function () { + var data = "first_name,last_name,email_address\rFirst1,Last1,email1@email.com"; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First1", "Last1", "email1@email.com"] + ]}); + }); - it.should("parse a block of CSV text with alternate escape char", function () { - var data = 'first_name,last_name,email_address\n"First,\\"1\\"","Last,\\"1\\"","email1@email.com"'; - var myParser = parser({delimiter: ",", escape: "\\"}); - assert.deepEqual(myParser(data, false), {"line": "", "rows": [ - ["first_name", "last_name", "email_address"], - ["First,\"1\"", "Last,\"1\"", "email1@email.com"] - ]}); - }); + it.should("return the rest of the line if there is more data", function () { + var data = "first_name,last_name,email_address\rFirst1,Last1,email1@email.com"; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, true), { + "line": "First1,Last1,email1@email.com", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + }); - it.should("return the rest of the line if a complete value is not found", function () { - var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com'; - var myParser = parser({delimiter: ","}); - assert.deepEqual(myParser(data, true), { - "line": '"First,""1""","Last,""1""","email1@email.com', - "rows": [ - ["first_name", "last_name", "email_address"] - ] + it.should("accept new data and return the result", function () { + var data = "first_name,last_name,email_address\rFirst1,Last1,email1@email.com,"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "First1,Last1,email1@email.com,", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + assert.deepEqual(myParser(parsedData.line + "\rFirst2,Last2,email2@email.com", false), {"line": "", "rows": [ + ["First1", "Last1", "email1@email.com"], + ["First2", "Last2", "email2@email.com"] + ]}); }); - }); - it.should("accept more data appended to the returned line with escaped values", function () { - var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com'; - var myParser = parser({delimiter: ","}), - parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": '"First,""1""","Last,""1""","email1@email.com', - "rows": [ - ["first_name", "last_name", "email_address"] - ] - }); - assert.deepEqual(myParser(parsedData.line + '"\n"First,""2""","Last,""2""","email2@email.com"', false), { - line: "", - rows: [ - ["First,\"1\"", "Last,\"1\"", "email1@email.com"], - ["First,\"2\"", "Last,\"2\"", "email2@email.com"] - ] + it.should("not parse a row if a new line is not found and there is more data", function () { + var data = "first_name,last_name,email_address"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "first_name,last_name,email_address", + "rows": [] + }); + }); + + it.should("not parse a row if there is a trailing delimiter and there is more data", function () { + var data = "first_name,last_name,email_address,"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "first_name,last_name,email_address,", + "rows": [] + }); + }); + + it.should("parse a row if a new line is found and there is more data", function () { + var data = "first_name,last_name,email_address\r"; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); }); + }); - it.should("throw an error if there is not more data and there is an invalid escape sequence", function () { - var data = 'first_name,last_name,email_address\n"First,""1""","Last,""1""","email1@email.com'; - var myParser = parser({delimiter: ","}), - parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": '"First,""1""","Last,""1""","email1@email.com', - "rows": [ - ["first_name", "last_name", "email_address"] - ] - }); - assert.throws(function () { - assert.deepEqual(myParser(parsedData.line + '\n"First,"",2""","Last""2""","email2@email.com"', false), { + it.describe("escaped values", function (it) { + + it.should("parse a block of CSV text", function () { + var data = 'first_name,last_name,email_address\r"First,1","Last,1","email1@email.com"'; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First,1", "Last,1", "email1@email.com"] + ]}); + }); + + it.should("parse a block of CSV text with escaped escaped char", function () { + var data = 'first_name,last_name,email_address\r"First,""1""","Last,""1""","email1@email.com"'; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First,\"1\"", "Last,\"1\"", "email1@email.com"] + ]}); + }); + + it.should("parse a block of CSV text with alternate escape char", function () { + var data = 'first_name,last_name,email_address\r"First,\\"1\\"","Last,\\"1\\"","email1@email.com"'; + var myParser = parser({delimiter: ",", escape: "\\"}); + assert.deepEqual(myParser(data, false), {"line": "", "rows": [ + ["first_name", "last_name", "email_address"], + ["First,\"1\"", "Last,\"1\"", "email1@email.com"] + ]}); + }); + + it.should("return the rest of the line if a complete value is not found", function () { + var data = 'first_name,last_name,email_address\r"First,""1""","Last,""1""","email1@email.com'; + var myParser = parser({delimiter: ","}); + assert.deepEqual(myParser(data, true), { + "line": '"First,""1""","Last,""1""","email1@email.com', + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + }); + + it.should("accept more data appended to the returned line with escaped values", function () { + var data = 'first_name,last_name,email_address\r"First,""1""","Last,""1""","email1@email.com'; + var myParser = parser({delimiter: ","}), + parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"First,""1""","Last,""1""","email1@email.com', + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + assert.deepEqual(myParser(parsedData.line + '"\r"First,""2""","Last,""2""","email2@email.com"', false), { line: "", rows: [ ["First,\"1\"", "Last,\"1\"", "email1@email.com"], ["First,\"2\"", "Last,\"2\"", "email2@email.com"] ] }); - }, Error, ' Parse Error: expected: \'"\' got: \'F\'. at \'First,""2""","Last""2""","email2@email.com"'); - }); + }); - it.should("handle empty values properly", function () { - var data = '"","",""\n,Last4,email4@email.com'; - var myParser = parser({delimiter: ","}), - parsedData = myParser(data, false); - assert.deepEqual(parsedData, {"line": "", "rows": [ - ["", "", ""], - ["", "Last4", "email4@email.com"] - ]}); - }); + it.should("throw an error if there is not more data and there is an invalid escape sequence", function () { + var data = 'first_name,last_name,email_address\r"First,""1""","Last,""1""","email1@email.com'; + var myParser = parser({delimiter: ","}), + parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"First,""1""","Last,""1""","email1@email.com', + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); + assert.throws(function () { + assert.deepEqual(myParser(parsedData.line + '\r"First,"",2""","Last""2""","email2@email.com"', false), { + line: "", + rows: [ + ["First,\"1\"", "Last,\"1\"", "email1@email.com"], + ["First,\"2\"", "Last,\"2\"", "email2@email.com"] + ] + }); + }, Error, ' Parse Error: expected: \'"\' got: \'F\'. at \'First,""2""","Last""2""","email2@email.com"'); + }); - it.should("not parse a row if a new line is not found and there is more data", function () { - var data = '"first_name","last_name","email_address"'; - var myParser = parser({delimiter: ","}); - var parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": '"first_name","last_name","email_address"', - "rows": [] + it.should("handle empty values properly", function () { + var data = '"","",""\r,Last4,email4@email.com'; + var myParser = parser({delimiter: ","}), + parsedData = myParser(data, false); + assert.deepEqual(parsedData, {"line": "", "rows": [ + ["", "", ""], + ["", "Last4", "email4@email.com"] + ]}); }); - }); - it.should("not parse a row if there is a trailing delimiter and there is more data", function () { - var data = '"first_name","last_name","email_address",'; - var myParser = parser({delimiter: ","}); - var parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": '"first_name","last_name","email_address",', - "rows": [] + it.should("not parse a row if a new line is not found and there is more data", function () { + var data = '"first_name","last_name","email_address"'; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"first_name","last_name","email_address"', + "rows": [] + }); }); - }); - it.should("parse a row if a new line is found and there is more data", function () { - var data = '"first_name","last_name","email_address"\n'; - var myParser = parser({delimiter: ","}); - var parsedData = myParser(data, true); - assert.deepEqual(parsedData, { - "line": "", - "rows": [ - ["first_name", "last_name", "email_address"] - ] + it.should("not parse a row if there is a trailing delimiter and there is more data", function () { + var data = '"first_name","last_name","email_address",'; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": '"first_name","last_name","email_address",', + "rows": [] + }); + }); + + it.should("parse a row if a new line is found and there is more data", function () { + var data = '"first_name","last_name","email_address"\r'; + var myParser = parser({delimiter: ","}); + var parsedData = myParser(data, true); + assert.deepEqual(parsedData, { + "line": "", + "rows": [ + ["first_name", "last_name", "email_address"] + ] + }); }); }); + }); }); \ No newline at end of file