From a0c24ae03a6234195a47809d0889956240e292cf Mon Sep 17 00:00:00 2001 From: Vaibhav Acharya Date: Mon, 17 Nov 2025 00:12:10 +0530 Subject: [PATCH] refactor: simplify and optimize --- .github/workflows/test.yml | 8 +- .prettierrc | 1 + benchmark.js | 261 ++++++++++++++------- license.md | 2 +- package-lock.json | 21 +- package.json | 3 +- readme.md | 21 +- src/constants/index.js | 41 ---- src/index.js | 341 ++++++++++++++++------------ src/utils/decode-escaped-unicode.js | 47 ---- src/utils/ensure-string.js | 12 - tests/basic.spec.js | 30 +-- tests/custom-indentation.spec.js | 30 ++- tests/edge-cases.spec.js | 38 ++-- tests/empty-literals.spec.js | 26 +-- tests/escaped.spec.js | 41 ++-- tests/invalid-json.spec.js | 14 +- tests/large.spec.js | 53 +++-- tests/nested.spec.js | 19 +- tests/non-string-fallback.spec.js | 16 +- tests/real-world.spec.js | 18 +- tests/unicode.spec.js | 36 +-- tests/utils.js | 10 +- tests/whitespaces.spec.js | 12 +- 24 files changed, 589 insertions(+), 512 deletions(-) create mode 100644 .prettierrc delete mode 100644 src/constants/index.js delete mode 100644 src/utils/decode-escaped-unicode.js delete mode 100644 src/utils/ensure-string.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24abf5a..d73f98d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,11 +16,11 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version-file: '.nvmrc' - cache: 'npm' - cache-dependency-path: './package-lock.json' + node-version-file: ".nvmrc" + cache: "npm" + cache-dependency-path: "./package-lock.json" - name: Install dependencies run: npm i - name: Test - run: npm run test \ No newline at end of file + run: npm run test diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/benchmark.js b/benchmark.js index 724551e..faba99b 100644 --- a/benchmark.js +++ b/benchmark.js @@ -1,10 +1,10 @@ -const Benchmark = require('benchmark'); -const { faker } = require('@faker-js/faker'); -const chalk = require('chalk'); -const fastJsonFormat = require('./src/index.js'); -const JSONbig = require('json-bigint'); -const LosslessJSON = require('lossless-json'); -const jsoncParser = require('jsonc-parser'); +const Benchmark = require("benchmark"); +const { faker } = require("@faker-js/faker"); +const chalk = require("chalk"); +const fastJsonFormat = require("./src/index.js"); +const JSONbig = require("json-bigint"); +const LosslessJSON = require("lossless-json"); +const jsoncParser = require("jsonc-parser"); /** * Generates a nested JSON string of approximately the target size @@ -23,170 +23,259 @@ function generateNestedJSON(targetSizeBytes) { city: faker.location.city(), state: faker.location.state(), country: faker.location.country(), - zipCode: faker.location.zipCode() + zipCode: faker.location.zipCode(), }, company: { name: faker.company.name(), catchPhrase: faker.company.catchPhrase(), - bs: faker.company.buzzPhrase() + bs: faker.company.buzzPhrase(), }, phone: faker.phone.number(), website: faker.internet.url(), - createdAt: faker.date.past().toISOString() + createdAt: faker.date.past().toISOString(), }); // Cache some users for performance const CACHED_USERS_COUNT = 100; const cachedUsers = Array.from({ length: CACHED_USERS_COUNT }, generateUser); - + // Calculate average size of one user const sampleUserJson = JSON.stringify(cachedUsers[0]); const AVERAGE_USER_SIZE = Buffer.byteLength(sampleUserJson); - + // Estimate number of users needed const estimatedUsersNeeded = Math.ceil(targetSizeBytes / AVERAGE_USER_SIZE); - + const users = []; let currentSize = 12; // Size of '{"users":[]}' let userIndex = 0; - + // Build up users array until we reach target size - while (currentSize < targetSizeBytes && userIndex < estimatedUsersNeeded * 1.2) { + while ( + currentSize < targetSizeBytes && + userIndex < estimatedUsersNeeded * 1.2 + ) { // Use cached user but vary the id for uniqueness const user = { ...cachedUsers[userIndex % CACHED_USERS_COUNT], - id: faker.string.uuid() + id: faker.string.uuid(), }; - + users.push(user); - + // Estimate current size (with JSON overhead) const userJson = JSON.stringify(user); currentSize += Buffer.byteLength(userJson) + (userIndex > 0 ? 1 : 0); // Add 1 for comma userIndex++; } - + return JSON.stringify({ users }); } const testSizes = [ - { name: '100 KB', bytes: 100 * 1024 }, - { name: '1 MB', bytes: 1024 * 1024 }, - { name: '5 MB', bytes: 5 * 1024 * 1024 }, - { name: '10 MB', bytes: 10 * 1024 * 1024 } + { name: "100 KB", bytes: 100 * 1024 }, + { name: "1 MB", bytes: 1024 * 1024 }, + { name: "5 MB", bytes: 5 * 1024 * 1024 }, + { name: "10 MB", bytes: 10 * 1024 * 1024 }, ]; -console.log('\n' + chalk.bold.cyan('πŸš€ Fast JSON Format Benchmark') + '\n'); -console.log(chalk.gray('⚑ Comparing ') + chalk.yellow('fast-json-format') + chalk.gray(' vs ') + chalk.yellow('jsonc-parser') + chalk.gray(' vs ') + chalk.yellow('json-bigint') + chalk.gray(' vs ') + chalk.yellow('lossless-json') + chalk.gray(' vs ') + chalk.yellow('JSON.stringify(JSON.parse())') + '\n'); -console.log(chalk.bold.blue('πŸ“Š Generating test data...') + '\n'); +console.log("\n" + chalk.bold.cyan("πŸš€ Fast JSON Format Benchmark") + "\n"); +console.log( + chalk.gray("⚑ Comparing ") + + chalk.yellow("fast-json-format") + + chalk.gray(" vs ") + + chalk.yellow("jsonc-parser") + + chalk.gray(" vs ") + + chalk.yellow("json-bigint") + + chalk.gray(" vs ") + + chalk.yellow("lossless-json") + + chalk.gray(" vs ") + + chalk.yellow("JSON.stringify(JSON.parse())") + + "\n", +); +console.log(chalk.bold.blue("πŸ“Š Generating test data...") + "\n"); -const testCases = testSizes.map(size => { - console.log(chalk.gray(' ⏳ Generating ') + chalk.cyan(size.name) + chalk.gray(' JSON...')); +const testCases = testSizes.map((size) => { + console.log( + chalk.gray(" ⏳ Generating ") + + chalk.cyan(size.name) + + chalk.gray(" JSON..."), + ); const data = generateNestedJSON(size.bytes); const actualSize = data.length; - console.log(chalk.gray(' βœ… Generated: ') + chalk.green(`${(actualSize / 1024).toFixed(1)} KB`) + chalk.gray(` (${((actualSize / size.bytes) * 100).toFixed(1)}% of target)`)); + console.log( + chalk.gray(" βœ… Generated: ") + + chalk.green(`${(actualSize / 1024).toFixed(1)} KB`) + + chalk.gray( + ` (${((actualSize / size.bytes) * 100).toFixed(1)}% of target)`, + ), + ); return { name: size.name, data: data, - actualSize: actualSize + actualSize: actualSize, }; }); -console.log('\n' + chalk.bold.magenta('🏁 Running benchmarks...') + '\n'); +console.log("\n" + chalk.bold.magenta("🏁 Running benchmarks...") + "\n"); // Store all results for summary table const allResults = []; -testCases.forEach(testCase => { - console.log('\n' + chalk.bold.yellow(`⚑ ${testCase.name}`) + ' ' + chalk.gray('━'.repeat(50))); - console.log(chalk.gray(' Size: ') + chalk.cyan(`${(testCase.actualSize / 1024).toFixed(1)} KB`)); - +testCases.forEach((testCase) => { + console.log( + "\n" + + chalk.bold.yellow(`⚑ ${testCase.name}`) + + " " + + chalk.gray("━".repeat(50)), + ); + console.log( + chalk.gray(" Size: ") + + chalk.cyan(`${(testCase.actualSize / 1024).toFixed(1)} KB`), + ); + const suite = new Benchmark.Suite(); - + const results = []; - + suite - .add('fast-json-format', function() { - fastJsonFormat(testCase.data, ' '); + .add("fast-json-format", function () { + fastJsonFormat(testCase.data, " "); }) - .add('jsonc-parser', function() { + .add("jsonc-parser", function () { JSON.stringify(jsoncParser.parse(testCase.data), null, 2); }) - .add('json-bigint', function() { + .add("json-bigint", function () { JSONbig.stringify(JSONbig.parse(testCase.data), null, 2); }) - .add('lossless-json', function() { + .add("lossless-json", function () { LosslessJSON.stringify(LosslessJSON.parse(testCase.data), null, 2); }) - .add('JSON.stringify', function() { + .add("JSON.stringify", function () { JSON.stringify(JSON.parse(testCase.data), null, 2); }) - .on('cycle', function(event) { + .on("cycle", function (event) { const name = event.target.name; - const ops = event.target.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }); + const ops = event.target.hz.toLocaleString("en-US", { + maximumFractionDigits: 0, + }); const margin = event.target.stats.rme.toFixed(2); - + results.push({ name, hz: event.target.hz }); - - const symbol = results.length === 1 ? 'β”œβ”€' : results.length === 2 ? 'β”œβ”€' : results.length === 3 ? 'β”œβ”€' : results.length === 4 ? 'β”œβ”€' : '└─'; - const color = name === 'fast-json-format' ? chalk.green : name === 'JSON.stringify' ? chalk.blue : name === 'json-bigint' ? chalk.magenta : name === 'jsonc-parser' ? chalk.cyan : chalk.yellow; - - console.log(chalk.gray(` ${symbol} `) + color(name) + chalk.gray(': ') + chalk.bold.white(ops) + chalk.gray(' ops/sec Β±' + margin + '%')); + + const symbol = + results.length === 1 + ? "β”œβ”€" + : results.length === 2 + ? "β”œβ”€" + : results.length === 3 + ? "β”œβ”€" + : results.length === 4 + ? "β”œβ”€" + : "└─"; + const color = + name === "fast-json-format" + ? chalk.green + : name === "JSON.stringify" + ? chalk.blue + : name === "json-bigint" + ? chalk.magenta + : name === "jsonc-parser" + ? chalk.cyan + : chalk.yellow; + + console.log( + chalk.gray(` ${symbol} `) + + color(name) + + chalk.gray(": ") + + chalk.bold.white(ops) + + chalk.gray(" ops/sec Β±" + margin + "%"), + ); }) - .on('complete', function() { - const fastest = this.filter('fastest')[0]; - const slowest = this.filter('slowest')[0]; + .on("complete", function () { + const fastest = this.filter("fastest")[0]; + const slowest = this.filter("slowest")[0]; const speedup = fastest.hz / slowest.hz; - + // Store results for summary allResults.push({ size: testCase.name, - results: results + results: results, }); }) - .run({ 'async': false }); + .run({ async: false }); }); // Display summary table -console.log('\n\n' + chalk.bold.cyan('πŸ“Š Summary Table') + '\n'); +console.log("\n\n" + chalk.bold.cyan("πŸ“Š Summary Table") + "\n"); // Build table header -const libs = ['fast-json-format', 'jsonc-parser', 'json-bigint', 'lossless-json', 'JSON.stringify']; +const libs = [ + "fast-json-format", + "jsonc-parser", + "json-bigint", + "lossless-json", + "JSON.stringify", +]; const colWidths = { size: 12, lib: 20 }; // Header console.log( - chalk.bold.white('Size'.padEnd(colWidths.size)) + ' β”‚ ' + - chalk.bold.green('fast-json-format'.padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.bold.cyan('jsonc-parser'.padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.bold.magenta('json-bigint'.padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.bold.yellow('lossless-json'.padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.bold.blue('JSON.stringify'.padEnd(colWidths.lib)) + chalk.bold.white("Size".padEnd(colWidths.size)) + + " β”‚ " + + chalk.bold.green("fast-json-format".padEnd(colWidths.lib)) + + " β”‚ " + + chalk.bold.cyan("jsonc-parser".padEnd(colWidths.lib)) + + " β”‚ " + + chalk.bold.magenta("json-bigint".padEnd(colWidths.lib)) + + " β”‚ " + + chalk.bold.yellow("lossless-json".padEnd(colWidths.lib)) + + " β”‚ " + + chalk.bold.blue("JSON.stringify".padEnd(colWidths.lib)), +); +console.log( + "─".repeat(colWidths.size) + + "─┼─" + + "─".repeat(colWidths.lib) + + "─┼─" + + "─".repeat(colWidths.lib) + + "─┼─" + + "─".repeat(colWidths.lib) + + "─┼─" + + "─".repeat(colWidths.lib) + + "─┼─" + + "─".repeat(colWidths.lib), ); -console.log('─'.repeat(colWidths.size) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib)); // Rows -allResults.forEach(result => { - const fastJson = result.results.find(r => r.name === 'fast-json-format'); - const jsonBigint = result.results.find(r => r.name === 'json-bigint'); - const losslessJson = result.results.find(r => r.name === 'lossless-json'); - const jsoncParser = result.results.find(r => r.name === 'jsonc-parser'); - const jsonStringify = result.results.find(r => r.name === 'JSON.stringify'); - - const fastJsonOps = fastJson ? fastJson.hz.toFixed(0) : 'N/A'; - const jsonBigintOps = jsonBigint ? jsonBigint.hz.toFixed(0) : 'N/A'; - const losslessJsonOps = losslessJson ? losslessJson.hz.toFixed(0) : 'N/A'; - const jsoncParserOps = jsoncParser ? jsoncParser.hz.toFixed(0) : 'N/A'; - const jsonStringifyOps = jsonStringify ? jsonStringify.hz.toFixed(0) : 'N/A'; - +allResults.forEach((result) => { + const fastJson = result.results.find((r) => r.name === "fast-json-format"); + const jsonBigint = result.results.find((r) => r.name === "json-bigint"); + const losslessJson = result.results.find((r) => r.name === "lossless-json"); + const jsoncParser = result.results.find((r) => r.name === "jsonc-parser"); + const jsonStringify = result.results.find((r) => r.name === "JSON.stringify"); + + const fastJsonOps = fastJson ? fastJson.hz.toFixed(0) : "N/A"; + const jsonBigintOps = jsonBigint ? jsonBigint.hz.toFixed(0) : "N/A"; + const losslessJsonOps = losslessJson ? losslessJson.hz.toFixed(0) : "N/A"; + const jsoncParserOps = jsoncParser ? jsoncParser.hz.toFixed(0) : "N/A"; + const jsonStringifyOps = jsonStringify ? jsonStringify.hz.toFixed(0) : "N/A"; + console.log( - chalk.cyan(result.size.padEnd(colWidths.size)) + ' β”‚ ' + - chalk.white((fastJsonOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.white((jsoncParserOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.white((jsonBigintOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.white((losslessJsonOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' + - chalk.white((jsonStringifyOps + ' ops/sec').padEnd(colWidths.lib)) + chalk.cyan(result.size.padEnd(colWidths.size)) + + " β”‚ " + + chalk.white((fastJsonOps + " ops/sec").padEnd(colWidths.lib)) + + " β”‚ " + + chalk.white((jsoncParserOps + " ops/sec").padEnd(colWidths.lib)) + + " β”‚ " + + chalk.white((jsonBigintOps + " ops/sec").padEnd(colWidths.lib)) + + " β”‚ " + + chalk.white((losslessJsonOps + " ops/sec").padEnd(colWidths.lib)) + + " β”‚ " + + chalk.white((jsonStringifyOps + " ops/sec").padEnd(colWidths.lib)), ); }); -console.log('\n' + chalk.gray('Note: Higher ops/sec = better performance') + '\n'); \ No newline at end of file +console.log( + "\n" + chalk.gray("Note: Higher ops/sec = better performance") + "\n", +); diff --git a/license.md b/license.md index aa7e426..86eee58 100644 --- a/license.md +++ b/license.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/package-lock.json b/package-lock.json index 834ba14..a041428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "jest": "^30.2.0", "json-bigint": "^1.0.0", "jsonc-parser": "^3.3.1", - "lossless-json": "^4.3.0" + "lossless-json": "^4.3.0", + "prettier": "^3.6.2" } }, "node_modules/@babel/code-frame": { @@ -49,6 +50,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1708,6 +1710,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3657,6 +3660,22 @@ "dev": true, "license": "MIT" }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", diff --git a/package.json b/package.json index a450fd4..59543a3 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "jest": "^30.2.0", "json-bigint": "^1.0.0", "jsonc-parser": "^3.3.1", - "lossless-json": "^4.3.0" + "lossless-json": "^4.3.0", + "prettier": "^3.6.2" }, "license": "MIT" } diff --git a/readme.md b/readme.md index dda080f..73a9da3 100644 --- a/readme.md +++ b/readme.md @@ -6,8 +6,8 @@ A blazing fast JSON formatting library that pretty-prints JSON like strings `JSON.stringify(JSON.parse(str), null, 2)` is fast β€” but it’s also **lossy** and **strict**: -- **❌ Breaks on BigInt:** `12345678901234567890n`, precision is lost. -- **βš™οΈ Loses numeric precision:** `1.2300` becomes `1.23`, zeroes are dropped. +- **❌ Breaks on BigInt:** `12345678901234567890n`, precision is lost. +- **βš™οΈ Loses numeric precision:** `1.2300` becomes `1.23`, zeroes are dropped. - **🚫 Fails on imperfect JSON:** Minor syntax issues in β€œJSON-like” strings can crash it. `fast-json-format` aims to pretty-print **without losing data or precision**, while staying lightweight and forgiving. @@ -32,7 +32,7 @@ npm install fast-json-format ### Basic Usage ```javascript -const fastJsonFormat = require('fast-json-format'); +const fastJsonFormat = require("fast-json-format"); const minified = '{"name":"John","age":30,"city":"New York"}'; const formatted = fastJsonFormat(minified); @@ -49,7 +49,7 @@ console.log(formatted); ```javascript // Use 4 spaces -const formatted = fastJsonFormat(jsonString, ' '); +const formatted = fastJsonFormat(jsonString, " "); ``` ## Performance @@ -63,13 +63,15 @@ npm run benchmark JSON.stringify is inherently faster (as it’s native and C++-optimized) Performance improvements are welcome :) -```text +```bash Size β”‚ fast-json-format β”‚ jsonc-parser β”‚ json-bigint β”‚ lossless-json β”‚ JSON.stringify ─────────────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┼───────────────────── -100 KB β”‚ 1839 ops/sec β”‚ 1265 ops/sec β”‚ 1053 ops/sec β”‚ 886 ops/sec β”‚ 3025 ops/sec -1 MB β”‚ 178 ops/sec β”‚ 125 ops/sec β”‚ 98 ops/sec β”‚ 61 ops/sec β”‚ 296 ops/sec -5 MB β”‚ 28 ops/sec β”‚ 21 ops/sec β”‚ 18 ops/sec β”‚ 9 ops/sec β”‚ 58 ops/sec -10 MB β”‚ 15 ops/sec β”‚ 11 ops/sec β”‚ 9 ops/sec β”‚ 4 ops/sec β”‚ 30 ops/sec +100 KB β”‚ 3040 ops/sec β”‚ 1498 ops/sec β”‚ 1152 ops/sec β”‚ 995 ops/sec β”‚ 3441 ops/sec +1 MB β”‚ 271 ops/sec β”‚ 146 ops/sec β”‚ 112 ops/sec β”‚ 86 ops/sec β”‚ 344 ops/sec +5 MB β”‚ 53 ops/sec β”‚ 29 ops/sec β”‚ 21 ops/sec β”‚ 13 ops/sec β”‚ 69 ops/sec +10 MB β”‚ 12 ops/sec β”‚ 14 ops/sec β”‚ 10 ops/sec β”‚ 6 ops/sec β”‚ 34 ops/sec + +Note: Higher ops/sec = better performance ``` ## Testing @@ -85,4 +87,3 @@ MIT License - Copyright (c) Bruno Software Inc. ## Contributing Issues and pull requests are welcome on the project repository. - diff --git a/src/constants/index.js b/src/constants/index.js deleted file mode 100644 index 187faaa..0000000 --- a/src/constants/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Lookup table for structural characters in JSON such as {}[],:" - * @type {Uint8Array} - */ -const STRUCTURAL_CHARS = new Uint8Array(128); - -/** - * Lookup table for whitespace characters (tab, newline, carriage return, space) - * @type {Uint8Array} - */ -const WHITESPACE_CHARS = new Uint8Array(128); - -/** - * Common JSON structural character codes. - * @readonly - * @enum {number} - */ -const CHAR_CODE = { - QUOTE: 34, // " - BACKSLASH: 92, // \ - SLASH: 47, // / - OPEN_BRACE: 123, // { - CLOSE_BRACE: 125, // } - OPEN_BRACKET: 91, // [ - CLOSE_BRACKET: 93, // ] - COMMA: 44, // , - COLON: 58, // : -}; - -// Initialize lookup tables -(() => { - /** @type {number[]} JSON structural characters: " , : [ ] { } */ - const structuralCodes = [34, 44, 58, 91, 93, 123, 125]; - structuralCodes.forEach((code) => (STRUCTURAL_CHARS[code] = 1)); - - /** @type {number[]} Whitespace characters: \t \n \r space */ - const whitespaceCodes = [9, 10, 13, 32]; - whitespaceCodes.forEach((code) => (WHITESPACE_CHARS[code] = 1)); -})(); - -module.exports = { STRUCTURAL_CHARS, WHITESPACE_CHARS, CHAR_CODE }; diff --git a/src/index.js b/src/index.js index 1695805..e15184f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,212 +1,267 @@ -const { - CHAR_CODE, - STRUCTURAL_CHARS, - WHITESPACE_CHARS, -} = require("./constants/index"); -const { decodeEscapedUnicode } = require("./utils/decode-escaped-unicode"); -const { ensureString } = require("./utils/ensure-string"); +/** + * JSON syntax characters to their ASCII codes mapping + */ +const ASCII_CHARS = { + QUOTE: 34, + BACKSLASH: 92, + SLASH: 47, + OPEN_BRACE: 123, + CLOSE_BRACE: 125, + OPEN_BRACKET: 91, + CLOSE_BRACKET: 93, + COMMA: 44, + COLON: 58, + SPACE: 32, + TAB: 9, + LF: 10, + CR: 13, + u: 117, +}; /** - * Fast JSON pretty printer with streaming-style buffering. + * Pretty print JSON * - * @param {string | object} inputRaw - Input JSON string or object - * @param {string} [indent=" "] - Indentation characters, e.g. two spaces or "\t" - * @returns {string} Pretty-printed JSON + * @param {string | object} inputRaw JSON string or object to format + * @param {string} [indentStr=" "] Indentation string (default: two spaces) + * @returns {string} Formatted JSON string */ -function fastJsonFormat(inputRaw, indentString = " ") { - /** @type {string | object} */ - const input = ensureString(inputRaw); - if (input === undefined) return ""; +function fastJsonFormat(inputRaw, indentStr = " ") { + if (inputRaw === undefined) return ""; + + let input = inputRaw; + if ( + typeof inputRaw === "object" && + inputRaw !== null && + inputRaw.constructor === String + ) { + input = inputRaw.valueOf(); + } - // Handle non-string input by delegating to JSON.stringify if (typeof input !== "string") { try { - return JSON.stringify(input, null, indentString); + return JSON.stringify(input, null, indentStr); } catch { return ""; } } - /** @type {string} */ const json = input; const jsonLength = json.length; - const shouldPrettyPrint = - typeof indentString === "string" && indentString.length > 0; - - /** @type {number} */ - const CHUNK_SIZE = Math.min(1 << 16, Math.max(1 << 12, input.length / 8)); // 64 KB - - /** @type {string} */ - let textBuffer = ""; - - /** @type {TextEncoder} */ - const encoder = new TextEncoder(); - - /** @type {Uint8Array} */ - let outputArray = new Uint8Array((jsonLength * 3) << 1); - - /** @type {number} */ - let offset = 0; - - /** - * Flush buffered text into outputArray. - * @param {boolean} [isFinal=false] - Whether this is the final flush - * @returns {void} - */ - const flushBuffer = (exit) => { - if (!textBuffer) return; - const encoded = encoder.encode(textBuffer); - const needed = offset + encoded.length; - - if (needed > outputArray.length) { - const newLength = Math.max(needed, outputArray.length << 1); - const newArray = new Uint8Array(newLength); - newArray.set(outputArray.subarray(0, offset)); - outputArray = newArray; - } - outputArray.set(encoded, offset); - offset = needed; - - if (!exit) textBuffer = ""; - }; - - /** - * Append text to the buffer, flushing automatically if necessary. - * @param {string} text - * @returns {void} - */ - const append = (content) => { - textBuffer += content; - if (textBuffer.length > CHUNK_SIZE) flushBuffer(); - }; - - /** - * Generate an indentation string for a given depth level. - * @param {number} level - * @returns {string} - */ - const makeIndent = (level) => indentString.repeat(level); - - /** @type {number} */ - let index = 0; + const indent = + typeof indentStr === "string" && indentStr.length > 0 ? indentStr : " "; + const shouldIndent = typeof indentStr === "string" && indentStr.length > 0; + + const indentCache = new Array(101); + indentCache[0] = ""; + for (let d = 1; d <= 100; d++) { + indentCache[d] = indentCache[d - 1] + indent; + } - /** @type {number} */ + let output = ""; + let index = 0; let depth = 0; - // === Main scanning loop === while (index < jsonLength) { - // Skip whitespace - for ( - ; - index < jsonLength && WHITESPACE_CHARS[json.charCodeAt(index)]; - index++ - ); + while (index < jsonLength) { + const charCode = json.charCodeAt(index); + + if ( + charCode !== ASCII_CHARS.SPACE && + charCode !== ASCII_CHARS.TAB && + charCode !== ASCII_CHARS.LF && + charCode !== ASCII_CHARS.CR + ) + break; + + index++; + } + if (index >= jsonLength) break; - const currentCharCode = json.charCodeAt(index); + const ch = json.charCodeAt(index); + + if (ch === ASCII_CHARS.QUOTE) { + const strStart = index++; - // String literals - if (currentCharCode === CHAR_CODE.QUOTE) { - const stringStart = index++; while (index < jsonLength) { - const nextChar = json.charCodeAt(index); - if (nextChar === CHAR_CODE.QUOTE) { + const c = json.charCodeAt(index); + + if (c === ASCII_CHARS.QUOTE) { index++; break; } - if (nextChar === CHAR_CODE.BACKSLASH) { + + if (c === ASCII_CHARS.BACKSLASH) { index += 2; } else { index++; } } - const innerContent = json.slice(stringStart + 1, index - 1); - const decodedString = decodeEscapedUnicode(innerContent); + const rawContent = json.substring(strStart + 1, index - 1); + + if ( + rawContent.indexOf("\\u") === -1 && + rawContent.indexOf("\\/") === -1 + ) { + output += '"' + rawContent + '"'; + + continue; + } + + let decoded = ""; + let i = 0; + const len = rawContent.length; + + while (i < len) { + const cc = rawContent.charCodeAt(i); + + if ( + cc === ASCII_CHARS.BACKSLASH && + i + 5 < len && + rawContent.charCodeAt(i + 1) === ASCII_CHARS.u + ) { + const hex = rawContent.substring(i + 2, i + 6); + const code = parseInt(hex, 16); + + if (!isNaN(code)) { + decoded += String.fromCharCode(code); + i += 6; + + continue; + } + } + + if ( + cc === ASCII_CHARS.BACKSLASH && + i + 1 < len && + rawContent.charCodeAt(i + 1) === ASCII_CHARS.SLASH + ) { + decoded += "/"; + i += 2; + + continue; + } + + decoded += rawContent[i]; + i++; + } + + output += '"' + decoded + '"'; - append(`"${decodedString}"`); continue; } - // Opening braces/brackets - if ( - currentCharCode === CHAR_CODE.OPEN_BRACE || - currentCharCode === CHAR_CODE.OPEN_BRACKET - ) { + if (ch === ASCII_CHARS.OPEN_BRACE || ch === ASCII_CHARS.OPEN_BRACKET) { const openChar = json[index]; - const closeChar = currentCharCode === CHAR_CODE.OPEN_BRACE ? "}" : "]"; + const closeCode = + ch === ASCII_CHARS.OPEN_BRACE + ? ASCII_CHARS.CLOSE_BRACE + : ASCII_CHARS.CLOSE_BRACKET; + + let j = index + 1; + while (j < jsonLength) { + const c = json.charCodeAt(j); + if ( + c !== ASCII_CHARS.SPACE && + c !== ASCII_CHARS.TAB && + c !== ASCII_CHARS.LF && + c !== ASCII_CHARS.CR + ) { + break; + } - let lookahead = index + 1; - while ( - lookahead < jsonLength && - WHITESPACE_CHARS[json.charCodeAt(lookahead)] - ) - lookahead++; + j++; + } + + if (j < jsonLength && json.charCodeAt(j) === closeCode) { + output += openChar + json[j]; + index = j + 1; - // Empty object/array - if (lookahead < jsonLength && json[lookahead] === closeChar) { - append(openChar + closeChar); - index = lookahead + 1; continue; } - append(openChar); - if (shouldPrettyPrint) { - append(`\n${makeIndent(depth + 1)}`); + output += openChar; + + if (shouldIndent) { + depth++; + + const indentStr = + depth <= 100 ? indentCache[depth] : indent.repeat(depth); + + output += "\n" + indentStr; } - depth++; + index++; + continue; } - // Closing braces/brackets - if ( - currentCharCode === CHAR_CODE.CLOSE_BRACE || - currentCharCode === CHAR_CODE.CLOSE_BRACKET - ) { + if (ch === ASCII_CHARS.CLOSE_BRACE || ch === ASCII_CHARS.CLOSE_BRACKET) { depth = Math.max(0, depth - 1); - if (shouldPrettyPrint) { - append(`\n${makeIndent(depth)}`); + + if (shouldIndent) { + const indentStr = + depth <= 100 ? indentCache[depth] : indent.repeat(depth); + + output += "\n" + indentStr; } - append(json[index++]); + + output += json[index]; + index++; + continue; } - // Comma - if (currentCharCode === CHAR_CODE.COMMA) { - append(","); - if (shouldPrettyPrint) { - append(`\n${makeIndent(depth)}`); + if (ch === ASCII_CHARS.COMMA) { + output += ","; + + if (shouldIndent) { + const indentStr = + depth <= 100 ? indentCache[depth] : indent.repeat(depth); + + output += "\n" + indentStr; } + index++; + continue; } - // Colon - if (currentCharCode === CHAR_CODE.COLON) { - if (shouldPrettyPrint) append(": "); - else append(":"); + if (ch === ASCII_CHARS.COLON) { + output += shouldIndent ? ": " : ":"; index++; + continue; } - // Regular values (numbers, literals, etc.) const tokenStart = index; - while ( - index < jsonLength && - !STRUCTURAL_CHARS[json.charCodeAt(index)] && - !WHITESPACE_CHARS[json.charCodeAt(index)] - ) { + while (index < jsonLength) { + const c = json.charCodeAt(index); + + if ( + c === ASCII_CHARS.SPACE || + c === ASCII_CHARS.TAB || + c === ASCII_CHARS.LF || + c === ASCII_CHARS.CR || + c === ASCII_CHARS.COMMA || + c === ASCII_CHARS.COLON || + c === ASCII_CHARS.OPEN_BRACE || + c === ASCII_CHARS.CLOSE_BRACE || + c === ASCII_CHARS.OPEN_BRACKET || + c === ASCII_CHARS.CLOSE_BRACKET + ) { + break; + } + index++; } - append(json.slice(tokenStart, index)); - } - // Flush any remaining buffer - if (textBuffer.length) flushBuffer(1); + output += json.substring(tokenStart, index); + } - return new TextDecoder().decode(outputArray.subarray(0, offset)); + return output; } module.exports = fastJsonFormat; diff --git a/src/utils/decode-escaped-unicode.js b/src/utils/decode-escaped-unicode.js deleted file mode 100644 index 5121bf9..0000000 --- a/src/utils/decode-escaped-unicode.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Decodes escaped Unicode sequences like "\u0041" β†’ "A" - * Also converts escaped forward slashes "\/" β†’ "/" - * - * @param {string} str - Input string possibly containing escape sequences - * @returns {string} Decoded string - */ -function decodeEscapedUnicode(input) { - if (input.indexOf("\\u") === -1 && input.indexOf("\\/") === -1) { - return input; - } - - /** @type {string[]} */ - let output = []; - let i = 0; - const len = input.length; - - while (i < len) { - const ch = input.charCodeAt(i); - - // Handle \uXXXX - if (ch === 92 && i + 5 < len && input.charCodeAt(i + 1) === 117) { - const hex = input.substr(i + 2, 4); - const code = parseInt(hex, 16); - if (!isNaN(code)) { - output.push(String.fromCharCode(code)); - i += 6; - continue; - } - } - - // Handle "\/" - if (ch === 92 && i + 1 < len && input.charCodeAt(i + 1) === 47) { - output.push("/"); - i += 2; - continue; - } - - // Normal character - output.push(input[i]); - i++; - } - - return output.join(""); -} - -module.exports = { decodeEscapedUnicode }; diff --git a/src/utils/ensure-string.js b/src/utils/ensure-string.js deleted file mode 100644 index 260a665..0000000 --- a/src/utils/ensure-string.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Safely convert a String object to a primitive string. - * - * @template T - * @param {T} value - Any input value - * @returns {string | T} String value if applicable, otherwise unchanged - */ -function ensureString(input) { - return input instanceof String ? input.toString() : input; -} - -module.exports = { ensureString }; diff --git a/tests/basic.spec.js b/tests/basic.spec.js index 0c44a51..51896d6 100644 --- a/tests/basic.spec.js +++ b/tests/basic.spec.js @@ -1,7 +1,7 @@ -const { assertEqual } = require('./utils'); +const { assertEqual } = require("./utils"); -describe('basic functionality', () => { - it('should format a simple object', () => { +describe("basic functionality", () => { + it("should format a simple object", () => { const input = '{"name":"John","age":30}'; const expected = `{ "name": "John", @@ -11,8 +11,8 @@ describe('basic functionality', () => { assertEqual(input, expected); }); - it('should format a simple array', () => { - const input = '[1,2,3,4,5]'; + it("should format a simple array", () => { + const input = "[1,2,3,4,5]"; const expected = `[ 1, 2, @@ -24,7 +24,7 @@ describe('basic functionality', () => { assertEqual(input, expected); }); - it('should preserve whitespace inside strings', () => { + it("should preserve whitespace inside strings", () => { const input = '{"text":"Hello World"}'; const expected = `{ "text": "Hello World" @@ -33,8 +33,8 @@ describe('basic functionality', () => { }); }); -describe('boolean and null values', () => { - it('should format boolean values', () => { +describe("boolean and null values", () => { + it("should format boolean values", () => { const input = '{"active":true,"deleted":false}'; const expected = `{ "active": true, @@ -43,7 +43,7 @@ describe('boolean and null values', () => { assertEqual(input, expected); }); - it('should format null values', () => { + it("should format null values", () => { const input = '{"value":null}'; const expected = `{ "value": null @@ -51,7 +51,7 @@ describe('boolean and null values', () => { assertEqual(input, expected); }); - it('should format mixed value types', () => { + it("should format mixed value types", () => { const input = '{"string":"text","number":42,"boolean":true,"null":null}'; const expected = `{ "string": "text", @@ -63,8 +63,8 @@ describe('boolean and null values', () => { }); }); -describe('numeric values', () => { - it('should format integer values', () => { +describe("numeric values", () => { + it("should format integer values", () => { const input = '{"count":100}'; const expected = `{ "count": 100 @@ -72,7 +72,7 @@ describe('numeric values', () => { assertEqual(input, expected); }); - it('should format floating point values', () => { + it("should format floating point values", () => { const input = '{"price":19.99,"tax":2.5}'; const expected = `{ "price": 19.99, @@ -81,7 +81,7 @@ describe('numeric values', () => { assertEqual(input, expected); }); - it('should format negative numbers', () => { + it("should format negative numbers", () => { const input = '{"temperature":-10,"balance":-500.25}'; const expected = `{ "temperature": -10, @@ -89,4 +89,4 @@ describe('numeric values', () => { }`; assertEqual(input, expected); }); -}); \ No newline at end of file +}); diff --git a/tests/custom-indentation.spec.js b/tests/custom-indentation.spec.js index 6254b8e..97e6ba5 100644 --- a/tests/custom-indentation.spec.js +++ b/tests/custom-indentation.spec.js @@ -1,33 +1,39 @@ -const fastJsonFormat = require('../src/index'); +const fastJsonFormat = require("../src/index"); -describe('custom indentation', () => { - it('should use custom indent with 4 spaces', () => { +describe("custom indentation", () => { + it("should use custom indent with 4 spaces", () => { const input = '{"name":"John","age":30}'; const expected = `{ "name": "John", "age": 30 }`; - expect(fastJsonFormat(input, ' ')).toBe(expected); - expect(fastJsonFormat(input, ' ')).toBe(JSON.stringify(JSON.parse(input), null, 4)); + expect(fastJsonFormat(input, " ")).toBe(expected); + expect(fastJsonFormat(input, " ")).toBe( + JSON.stringify(JSON.parse(input), null, 4), + ); }); - it('should use single space indentation', () => { + it("should use single space indentation", () => { const input = '{"name":"John"}'; const expected = `{ "name": "John" }`; - expect(fastJsonFormat(input, ' ')).toBe(expected); - expect(fastJsonFormat(input, ' ')).toBe(JSON.stringify(JSON.parse(input), null, 1)); + expect(fastJsonFormat(input, " ")).toBe(expected); + expect(fastJsonFormat(input, " ")).toBe( + JSON.stringify(JSON.parse(input), null, 1), + ); }); - it('should handle custom indent with nested structures', () => { + it("should handle custom indent with nested structures", () => { const input = '{"user":{"name":"John"}}'; const expected = `{ "user": { "name": "John" } }`; - expect(fastJsonFormat(input, ' ')).toBe(expected); - expect(fastJsonFormat(input, ' ')).toBe(JSON.stringify(JSON.parse(input), null, 4)); + expect(fastJsonFormat(input, " ")).toBe(expected); + expect(fastJsonFormat(input, " ")).toBe( + JSON.stringify(JSON.parse(input), null, 4), + ); }); -}); \ No newline at end of file +}); diff --git a/tests/edge-cases.spec.js b/tests/edge-cases.spec.js index 158e4ea..24afe96 100644 --- a/tests/edge-cases.spec.js +++ b/tests/edge-cases.spec.js @@ -1,38 +1,38 @@ -const fastJsonFormat = require('../src/index'); +const fastJsonFormat = require("../src/index"); -describe('edge cases', () => { - it('should return empty string for non-string input (undefined)', () => { - expect(fastJsonFormat(undefined)).toBe(''); +describe("edge cases", () => { + it("should return empty string for non-string input (undefined)", () => { + expect(fastJsonFormat(undefined)).toBe(""); }); - it('should return empty string for non-string input (null)', () => { - expect(fastJsonFormat(null)).toBe('null'); - expect(fastJsonFormat(null)).toBe(JSON.stringify(JSON.parse('null'))); + it("should return empty string for non-string input (null)", () => { + expect(fastJsonFormat(null)).toBe("null"); + expect(fastJsonFormat(null)).toBe(JSON.stringify(JSON.parse("null"))); }); - it('should return empty string for non-string input (number)', () => { - expect(fastJsonFormat(123)).toBe('123'); - expect(fastJsonFormat(123)).toBe(JSON.stringify(JSON.parse('123'))); + it("should return empty string for non-string input (number)", () => { + expect(fastJsonFormat(123)).toBe("123"); + expect(fastJsonFormat(123)).toBe(JSON.stringify(JSON.parse("123"))); }); // This would throw an error if one tried JSON.parse() // The lib is expected to handle this gracefully and return the string as-is - it('should return string as-is for string input', () => { - expect(fastJsonFormat('hello')).toBe('hello'); + it("should return string as-is for string input", () => { + expect(fastJsonFormat("hello")).toBe("hello"); }); - it('should handle string with only whitespace', () => { - expect(fastJsonFormat(' \n\t ')).toBe(''); + it("should handle string with only whitespace", () => { + expect(fastJsonFormat(" \n\t ")).toBe(""); }); - it('should handle unmatched brackets gracefully', () => { + it("should handle unmatched brackets gracefully", () => { const input = '{"name":"John"'; const expected = `{ "name": "John"`; expect(fastJsonFormat(input)).toBe(expected); }); - it('should handle extra closing brackets', () => { + it("should handle extra closing brackets", () => { const input = '{"name":"John"}}'; const expected = `{ "name": "John" @@ -41,7 +41,7 @@ describe('edge cases', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should handle strings with brackets', () => { + it("should handle strings with brackets", () => { const input = '{"regex":"[a-z]+"}'; const expected = `{ "regex": "[a-z]+" @@ -49,7 +49,7 @@ describe('edge cases', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should handle strings with colons and commas', () => { + it("should handle strings with colons and commas", () => { const input = '{"time":"12:30:45","list":"a,b,c"}'; const expected = `{ "time": "12:30:45", @@ -57,4 +57,4 @@ describe('edge cases', () => { }`; expect(fastJsonFormat(input)).toBe(expected); }); -}); \ No newline at end of file +}); diff --git a/tests/empty-literals.spec.js b/tests/empty-literals.spec.js index 437f2d1..90e9e13 100644 --- a/tests/empty-literals.spec.js +++ b/tests/empty-literals.spec.js @@ -1,22 +1,22 @@ -const { assertEqual } = require('./utils'); +const { assertEqual } = require("./utils"); -describe('empty literals', () => { - it('should format an empty object', () => { - const input = '{}'; - const expected = '{}'; +describe("empty literals", () => { + it("should format an empty object", () => { + const input = "{}"; + const expected = "{}"; assertEqual(input, expected); }); - it('should format an empty object with whitespace', () => { + it("should format an empty object with whitespace", () => { const input = ` { } `; - const expected = '{}'; + const expected = "{}"; assertEqual(input, expected); }); - it('should format nested empty object with whitespace', () => { + it("should format nested empty object with whitespace", () => { const input = ` { "a": { @@ -29,13 +29,13 @@ describe('empty literals', () => { assertEqual(input, expected); }); - it('should format an empty array', () => { - const input = '[]'; - const expected = '[]'; + it("should format an empty array", () => { + const input = "[]"; + const expected = "[]"; assertEqual(input, expected); }); - it('should format nested empty array with whitespace', () => { + it("should format nested empty array with whitespace", () => { const input = ` [ [ @@ -47,4 +47,4 @@ describe('empty literals', () => { ]`; assertEqual(input, expected); }); -}); \ No newline at end of file +}); diff --git a/tests/escaped.spec.js b/tests/escaped.spec.js index a97b77c..1d7d3d6 100644 --- a/tests/escaped.spec.js +++ b/tests/escaped.spec.js @@ -1,7 +1,7 @@ -const { assertEqual } = require('./utils'); +const { assertEqual } = require("./utils"); -describe('string handling', () => { - it('should handle escaped quotes correctly', () => { +describe("string handling", () => { + it("should handle escaped quotes correctly", () => { const input = '{"quote":"She said \\"Hello\\""}'; const expected = `{ "quote": "She said \\"Hello\\"" @@ -9,7 +9,7 @@ describe('string handling', () => { assertEqual(input, expected); }); - it('should handle multiple escaped quotes', () => { + it("should handle multiple escaped quotes", () => { const input = '{"text":"\\"start\\" middle \\"end\\""}'; const expected = `{ "text": "\\"start\\" middle \\"end\\"" @@ -17,7 +17,7 @@ describe('string handling', () => { assertEqual(input, expected); }); - it('should handle backslashes correctly', () => { + it("should handle backslashes correctly", () => { const input = '{"path":"C:\\\\Users\\\\file.txt"}'; const expected = `{ "path": "C:\\\\Users\\\\file.txt" @@ -25,7 +25,7 @@ describe('string handling', () => { assertEqual(input, expected); }); - it('should handle strings with special characters', () => { + it("should handle strings with special characters", () => { const input = '{"special":"{}[],:"}'; const expected = `{ "special": "{}[],:" @@ -33,7 +33,7 @@ describe('string handling', () => { assertEqual(input, expected); }); - it('should handle strings with newlines and special chars', () => { + it("should handle strings with newlines and special chars", () => { const input = '{"multiline":"line1\\nline2\\nline3"}'; const expected = `{ "multiline": "line1\\nline2\\nline3" @@ -42,8 +42,8 @@ describe('string handling', () => { }); }); -describe('escaped characters', () => { - it('should handle double backslash before quote', () => { +describe("escaped characters", () => { + it("should handle double backslash before quote", () => { const input = '{"path":"C:\\\\Program Files\\\\"}'; const expected = `{ "path": "C:\\\\Program Files\\\\" @@ -51,7 +51,7 @@ describe('escaped characters', () => { assertEqual(input, expected); }); - it('should handle odd number of backslashes before quote', () => { + it("should handle odd number of backslashes before quote", () => { const input = '{"text":"before\\\\\\\\"}'; const expected = `{ "text": "before\\\\\\\\" @@ -59,7 +59,7 @@ describe('escaped characters', () => { assertEqual(input, expected); }); - it('should handle escaped backslash followed by escaped quote', () => { + it("should handle escaped backslash followed by escaped quote", () => { const input = '{"mixed":"test\\\\\\"end"}'; const expected = `{ "mixed": "test\\\\\\"end" @@ -68,8 +68,8 @@ describe('escaped characters', () => { }); }); -describe('forward slash escape sequences', () => { - it('should decode \\/ escape sequences to forward slashes', () => { +describe("forward slash escape sequences", () => { + it("should decode \\/ escape sequences to forward slashes", () => { const input = '{"url":"https:\\/\\/example.com\\/api\\/v1"}'; const expected = `{ "url": "https://example.com/api/v1" @@ -77,7 +77,7 @@ describe('forward slash escape sequences', () => { assertEqual(input, expected); }); - it('should handle unescaped forward slashes correctly', () => { + it("should handle unescaped forward slashes correctly", () => { const input = '{"url":"https://example.com/api/v1"}'; const expected = `{ "url": "https://example.com/api/v1" @@ -85,8 +85,9 @@ describe('forward slash escape sequences', () => { assertEqual(input, expected); }); - it('should handle forward slashes mixed with other escape sequences', () => { - const input = '{"text":"line1\\npath\\/to\\/file\\ttab","unicode":"\\u4e16\\u754c\\/path"}'; + it("should handle forward slashes mixed with other escape sequences", () => { + const input = + '{"text":"line1\\npath\\/to\\/file\\ttab","unicode":"\\u4e16\\u754c\\/path"}'; const expected = `{ "text": "line1\\npath/to/file\\ttab", "unicode": "δΈ–η•Œ/path" @@ -94,7 +95,7 @@ describe('forward slash escape sequences', () => { assertEqual(input, expected); }); - it('should handle a single escaped forward slash', () => { + it("should handle a single escaped forward slash", () => { const input = '{"slash":"\\/"}'; const expected = `{ "slash": "/" @@ -102,7 +103,7 @@ describe('forward slash escape sequences', () => { assertEqual(input, expected); }); - it('should handle multiple consecutive escaped forward slashes', () => { + it("should handle multiple consecutive escaped forward slashes", () => { const input = '{"path":"\\/\\/network\\/share"}'; const expected = `{ "path": "//network/share" @@ -110,11 +111,11 @@ describe('forward slash escape sequences', () => { assertEqual(input, expected); }); - it('should handle escaped forward slash at end of string', () => { + it("should handle escaped forward slash at end of string", () => { const input = '{"url":"https://example.com\\/"}'; const expected = `{ "url": "https://example.com/" }`; assertEqual(input, expected); }); -}); \ No newline at end of file +}); diff --git a/tests/invalid-json.spec.js b/tests/invalid-json.spec.js index 4f92ce3..5362881 100644 --- a/tests/invalid-json.spec.js +++ b/tests/invalid-json.spec.js @@ -1,7 +1,7 @@ -const fastJsonFormat = require('../src/index'); +const fastJsonFormat = require("../src/index"); -describe('invalid JSON handling', () => { - it('should format JSON with unquoted keys', () => { +describe("invalid JSON handling", () => { + it("should format JSON with unquoted keys", () => { const input = '{name:"John",age:30}'; const expected = `{ name: "John", @@ -10,7 +10,7 @@ describe('invalid JSON handling', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should handle BigInt literals', () => { + it("should handle BigInt literals", () => { const input = '{"bigNumber":9007199254740991n}'; const expected = `{ "bigNumber": 9007199254740991n @@ -21,7 +21,7 @@ describe('invalid JSON handling', () => { // Ideally, we should not have newline after the last comma // but for now we will keep it as it is and not introduce a performance penalty // to handle this case. - it('should handle trailing commas', () => { + it("should handle trailing commas", () => { const input = '{"name":"John","age":30,}'; const expected = `{ "name": "John", @@ -31,11 +31,11 @@ describe('invalid JSON handling', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should handle single quotes (treating as regular characters)', () => { + it("should handle single quotes (treating as regular characters)", () => { const input = "{'name':'John'}"; const expected = `{ 'name': 'John' }`; expect(fastJsonFormat(input)).toBe(expected); }); -}); \ No newline at end of file +}); diff --git a/tests/large.spec.js b/tests/large.spec.js index 63f138d..bdcbf85 100644 --- a/tests/large.spec.js +++ b/tests/large.spec.js @@ -1,56 +1,55 @@ -const { assertEqual } = require('./utils'); +const { assertEqual } = require("./utils"); -describe('large nested structures', () => { - it('should handle large nested structures', () => { +describe("large nested structures", () => { + it("should handle large nested structures", () => { const depth = 20; - let input = ''; - let expected = ''; - + let input = ""; + let expected = ""; + for (let i = 0; i < depth; i++) { input += '{"level":'; - expected += '{\n' + ' '.repeat(i + 1) + '"level": '; + expected += "{\n" + " ".repeat(i + 1) + '"level": '; } - - input += '42'; - expected += '42'; - + + input += "42"; + expected += "42"; + for (let i = depth - 1; i >= 0; i--) { - input += '}'; - expected += '\n' + ' '.repeat(i) + '}'; + input += "}"; + expected += "\n" + " ".repeat(i) + "}"; } - + assertEqual(input, expected); }); - it('should handle long arrays', () => { + it("should handle long arrays", () => { const items = Array.from({ length: 100 }, (_, i) => i); const input = JSON.stringify(items); - + // Build expected output manually - let expected = '[\n'; + let expected = "[\n"; for (let i = 0; i < items.length; i++) { - expected += ' ' + items[i]; + expected += " " + items[i]; if (i < items.length - 1) { - expected += ',\n'; + expected += ",\n"; } else { - expected += '\n'; + expected += "\n"; } } - expected += ']'; - + expected += "]"; + assertEqual(input, expected); }); - it('should handle very long strings', () => { - const longString = 'a'.repeat(10000); + it("should handle very long strings", () => { + const longString = "a".repeat(10000); const input = `{"text":"${longString}"}`; - + // Build expected output manually const expected = `{ "text": "${longString}" }`; - + assertEqual(input, expected); }); }); - diff --git a/tests/nested.spec.js b/tests/nested.spec.js index b37b882..2da5870 100644 --- a/tests/nested.spec.js +++ b/tests/nested.spec.js @@ -1,8 +1,9 @@ -const { assertEqual } = require('./utils'); +const { assertEqual } = require("./utils"); -describe('nested structures', () => { - it('should format nested objects', () => { - const input = '{"user":{"name":"John","address":{"city":"NYC","zip":"10001"}}}'; +describe("nested structures", () => { + it("should format nested objects", () => { + const input = + '{"user":{"name":"John","address":{"city":"NYC","zip":"10001"}}}'; const expected = `{ "user": { "name": "John", @@ -15,8 +16,8 @@ describe('nested structures', () => { assertEqual(input, expected); }); - it('should format nested arrays', () => { - const input = '[[1,2],[3,4],[5,6]]'; + it("should format nested arrays", () => { + const input = "[[1,2],[3,4],[5,6]]"; const expected = `[ [ 1, @@ -34,7 +35,7 @@ describe('nested structures', () => { assertEqual(input, expected); }); - it('should format mixed nested structures', () => { + it("should format mixed nested structures", () => { const input = '{"items":[{"id":1,"name":"Item1"},{"id":2,"name":"Item2"}]}'; const expected = `{ "items": [ @@ -51,7 +52,7 @@ describe('nested structures', () => { assertEqual(input, expected); }); - it('should handle deeply nested structures', () => { + it("should handle deeply nested structures", () => { const input = '{"a":{"b":{"c":{"d":{"e":"value"}}}}}'; const expected = `{ "a": { @@ -66,4 +67,4 @@ describe('nested structures', () => { }`; assertEqual(input, expected); }); -}); \ No newline at end of file +}); diff --git a/tests/non-string-fallback.spec.js b/tests/non-string-fallback.spec.js index 5a06431..1a09524 100644 --- a/tests/non-string-fallback.spec.js +++ b/tests/non-string-fallback.spec.js @@ -1,9 +1,9 @@ -const fastJsonFormat = require('../src/index'); +const fastJsonFormat = require("../src/index"); // For non-string input, we should default to JSON.stringify behavior -describe('non-string fallback', () => { - it('should default to JSON.stringify behavior object input', () => { +describe("non-string fallback", () => { + it("should default to JSON.stringify behavior object input", () => { const input = { active: true, deleted: false }; const expected = `{ "active": true, @@ -12,7 +12,7 @@ describe('non-string fallback', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should default to JSON.stringify behavior array input', () => { + it("should default to JSON.stringify behavior array input", () => { const input = [1, 2, 3]; const expected = `[ 1, @@ -22,21 +22,21 @@ describe('non-string fallback', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should default to JSON.stringify behavior number input', () => { + it("should default to JSON.stringify behavior number input", () => { const input = 123; const expected = `123`; expect(fastJsonFormat(input)).toBe(expected); }); - it('should default to JSON.stringify behavior boolean input', () => { + it("should default to JSON.stringify behavior boolean input", () => { const input = true; const expected = `true`; expect(fastJsonFormat(input)).toBe(expected); }); - it('should default to JSON.stringify behavior null input', () => { + it("should default to JSON.stringify behavior null input", () => { const input = null; const expected = `null`; expect(fastJsonFormat(input)).toBe(expected); }); -}); \ No newline at end of file +}); diff --git a/tests/real-world.spec.js b/tests/real-world.spec.js index 0b1117f..4b29ba3 100644 --- a/tests/real-world.spec.js +++ b/tests/real-world.spec.js @@ -1,8 +1,9 @@ -const { assertEqual } = require('./utils'); +const { assertEqual } = require("./utils"); -describe('complex real-world scenarios', () => { - it('should format API response-like structure', () => { - const input = '{"status":"success","data":{"users":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}],"total":2},"timestamp":1634567890}'; +describe("complex real-world scenarios", () => { + it("should format API response-like structure", () => { + const input = + '{"status":"success","data":{"users":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}],"total":2},"timestamp":1634567890}'; const expected = `{ "status": "success", "data": { @@ -25,8 +26,9 @@ describe('complex real-world scenarios', () => { assertEqual(input, expected); }); - it('should format configuration-like structure', () => { - const input = '{"server":{"host":"localhost","port":8080,"ssl":false},"database":{"host":"db.example.com","credentials":{"username":"admin","password":"secret123"}},"features":["auth","logging","caching"]}'; + it("should format configuration-like structure", () => { + const input = + '{"server":{"host":"localhost","port":8080,"ssl":false},"database":{"host":"db.example.com","credentials":{"username":"admin","password":"secret123"}},"features":["auth","logging","caching"]}'; const expected = `{ "server": { "host": "localhost", @@ -49,7 +51,7 @@ describe('complex real-world scenarios', () => { assertEqual(input, expected); }); - it('should handle array of mixed types', () => { + it("should handle array of mixed types", () => { const input = '[1,"text",true,null,{"key":"value"},[1,2,3]]'; const expected = `[ 1, @@ -67,4 +69,4 @@ describe('complex real-world scenarios', () => { ]`; assertEqual(input, expected); }); -}); \ No newline at end of file +}); diff --git a/tests/unicode.spec.js b/tests/unicode.spec.js index 74e8396..e2c5445 100644 --- a/tests/unicode.spec.js +++ b/tests/unicode.spec.js @@ -1,8 +1,8 @@ -const { assertEqual } = require('./utils'); -const fastJsonFormat = require('../src/index'); +const { assertEqual } = require("./utils"); +const fastJsonFormat = require("../src/index"); -describe('unicode handling', () => { - it('should decode \\uXXXX escape sequences to unicode characters', () => { +describe("unicode handling", () => { + it("should decode \\uXXXX escape sequences to unicode characters", () => { const input = '{"name":"\\u4e16\\u754c"}'; const expected = `{ "name": "δΈ–η•Œ" @@ -10,7 +10,7 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should handle mixed ASCII and unicode escapes', () => { + it("should handle mixed ASCII and unicode escapes", () => { const input = '{"greeting":"Hello \\u4e16\\u754c"}'; const expected = `{ "greeting": "Hello δΈ–η•Œ" @@ -18,8 +18,9 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should decode multiple unicode strings', () => { - const input = '{"chinese":"\\u4f60\\u597d","japanese":"\\u3053\\u3093\\u306b\\u3061\\u306f"}'; + it("should decode multiple unicode strings", () => { + const input = + '{"chinese":"\\u4f60\\u597d","japanese":"\\u3053\\u3093\\u306b\\u3061\\u306f"}'; const expected = `{ "chinese": "δ½ ε₯½", "japanese": "こんにけは" @@ -27,7 +28,7 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should handle emoji surrogate pairs', () => { + it("should handle emoji surrogate pairs", () => { const input = '{"emoji":"\\ud83d\\ude00"}'; const expected = `{ "emoji": "πŸ˜€" @@ -35,7 +36,7 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should decode unicode in nested objects', () => { + it("should decode unicode in nested objects", () => { const input = '{"outer":{"inner":"\\u4e2d\\u6587"}}'; const expected = `{ "outer": { @@ -45,7 +46,7 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should handle unicode in array values', () => { + it("should handle unicode in array values", () => { const input = '["\\u4e00","\\u4e8c","\\u4e09"]'; const expected = `[ "δΈ€", @@ -55,7 +56,7 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should handle uppercase hex in unicode escapes', () => { + it("should handle uppercase hex in unicode escapes", () => { const input = '{"text":"\\u4E16\\u754C"}'; const expected = `{ "text": "δΈ–η•Œ" @@ -63,7 +64,7 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should preserve invalid unicode escapes as-is', () => { + it("should preserve invalid unicode escapes as-is", () => { // Note: This is invalid JSON that JSON.parse would reject // Testing forgiving behavior only const input = '{"invalid":"\\uXYZ"}'; @@ -73,7 +74,7 @@ describe('unicode handling', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should preserve incomplete unicode escapes as-is', () => { + it("should preserve incomplete unicode escapes as-is", () => { // Note: This is invalid JSON that JSON.parse would reject // Testing forgiving behavior only const input = '{"incomplete":"\\u12"}'; @@ -83,7 +84,7 @@ describe('unicode handling', () => { expect(fastJsonFormat(input)).toBe(expected); }); - it('should handle unicode mixed with other escape sequences', () => { + it("should handle unicode mixed with other escape sequences", () => { const input = '{"text":"\\u4e16\\n\\u754c\\t\\u0021"}'; const expected = `{ "text": "δΈ–\\nη•Œ\\t!" @@ -91,16 +92,15 @@ describe('unicode handling', () => { assertEqual(input, expected); }); - it('should behave like JSON.stringify for unicode content', () => { + it("should behave like JSON.stringify for unicode content", () => { // JSON.stringify preserves actual unicode characters (doesn't escape them) const obj = { name: "δΈ–η•Œ" }; const fromStringify = JSON.stringify(obj, null, 2); - + // fastJsonFormat should decode \uXXXX to match that behavior const input = '{"name":"\\u4e16\\u754c"}'; const fromFormatter = fastJsonFormat(input); - + expect(fromFormatter).toBe(fromStringify); }); }); - diff --git a/tests/utils.js b/tests/utils.js index cc2fba4..043cc9f 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,10 +1,12 @@ -const fastJsonFormat = require('../src/index'); +const fastJsonFormat = require("../src/index"); const assertEqual = (input, expected) => { expect(fastJsonFormat(input)).toBe(expected); - expect(fastJsonFormat(input)).toBe(JSON.stringify(JSON.parse(input), null, 2)); + expect(fastJsonFormat(input)).toBe( + JSON.stringify(JSON.parse(input), null, 2), + ); }; module.exports = { - assertEqual -}; \ No newline at end of file + assertEqual, +}; diff --git a/tests/whitespaces.spec.js b/tests/whitespaces.spec.js index e871e67..8c45c5c 100644 --- a/tests/whitespaces.spec.js +++ b/tests/whitespaces.spec.js @@ -1,7 +1,7 @@ -const { assertEqual } = require('./utils'); +const { assertEqual } = require("./utils"); -describe('whitespace handling', () => { - it('should skip existing whitespace outside strings', () => { +describe("whitespace handling", () => { + it("should skip existing whitespace outside strings", () => { const input = ' { "name" : "John" , "age" : 30 } '; const expected = `{ "name": "John", @@ -10,7 +10,7 @@ describe('whitespace handling', () => { assertEqual(input, expected); }); - it('should handle tabs and newlines in input', () => { + it("should handle tabs and newlines in input", () => { const input = '{\n\t"name":\t"John",\n\t"age":\t30\n}'; const expected = `{ "name": "John", @@ -19,11 +19,11 @@ describe('whitespace handling', () => { assertEqual(input, expected); }); - it('should handle mixed whitespace characters', () => { + it("should handle mixed whitespace characters", () => { const input = '{\r\n "key" : \t"value"\r\n}'; const expected = `{ "key": "value" }`; assertEqual(input, expected); }); -}); \ No newline at end of file +});