fast-json-format
Version:
Fast JSON formatting library
200 lines (176 loc) • 4.89 kB
JavaScript
/**
* Pretty-prints a JSON-like string without parsing.
* Fast path: chunked copying, fast string scan, lookahead for empty {} / [].
*
* @param {string} input
* @param {string} indent
* @returns {string}
*/
function fastJsonFormat(input, indent = ' ') {
if (input === undefined) return '';
// For non-string input, fall back to JSON.stringify behavior.
if (typeof input !== 'string') {
try {
return JSON.stringify(input, null, indent);
} catch {
return '';
}
}
const s = String(input);
const n = s.length;
// Fast minify-like path when indent is empty.
const useIndent = typeof indent === 'string' ? indent : ' ';
const pretty = useIndent.length > 0;
// Output as array of chunks (strings). Much faster than char-by-char.
const out = [];
let level = 0;
// Cached indents.
const indents = [''];
const getIndent = (k) => {
if (!pretty) return ''; // minify fast-path
if (indents[k] !== undefined) return indents[k];
let cur = indents[indents.length - 1];
for (let j = indents.length; j <= k; j++) {
cur += useIndent;
indents[j] = cur;
}
return indents[k];
};
// Character codes
const QUOTE = 34; // "
const BACKSLASH = 92; // \
const OPEN_BRACE = 123; // {
const CLOSE_BRACE = 125; // }
const OPEN_BRACKET = 91; // [
const CLOSE_BRACKET = 93;// ]
const COMMA = 44; // ,
const COLON = 58; // :
const SPACE = 32; // ' '
const TAB = 9; // '\t'
const NEWLINE = 10; // '\n'
const CR = 13; // '\r'
const isSpaceCode = (c) =>
c === SPACE || c === TAB || c === NEWLINE || c === CR;
// Skip whitespace starting at idx; return first non-space index (<= n)
const skipWS = (idx) => {
while (idx < n && isSpaceCode(s.charCodeAt(idx))) idx++;
return idx;
};
// Scan a JSON string starting at index of opening quote `i` (s[i] === '"').
// Returns index just after the closing quote and pushes the entire slice.
const scanString = (i) => {
let j = i + 1;
while (j < n) {
const c = s.charCodeAt(j);
if (c === QUOTE) { // end of string
j++;
out.push(s.slice(i, j));
return j;
}
if (c === BACKSLASH) {
// Handle escape: \" \\ \/ \b \f \n \r \t or \uXXXX
j++;
if (j < n && s.charCodeAt(j) === 117 /* 'u' */) {
// Skip 'u' + 4 hex digits if present
// (Keep it forgiving; don't validate hex strictly)
j += 5; // 'u' + 4 chars
} else {
j++; // skip the escaped char
}
continue;
}
j++;
}
// Unterminated: copy to end (forgiving)
out.push(s.slice(i, n));
return n;
};
// Copy a run of non-structural, non-space characters starting at i.
// Stops at space or one of the structural chars ,:{}[]"
const scanAtom = (i) => {
let j = i;
scan: while (j < n) {
const c = s.charCodeAt(j);
switch (c) {
case SPACE:
case TAB:
case NEWLINE:
case CR:
case QUOTE:
case OPEN_BRACE:
case CLOSE_BRACE:
case OPEN_BRACKET:
case CLOSE_BRACKET:
case COMMA:
case COLON:
break scan;
}
j++;
}
if (j > i) out.push(s.slice(i, j));
return j;
};
let i = 0;
while (i < n) {
i = skipWS(i);
if (i >= n) break;
const c = s.charCodeAt(i);
// Strings
if (c === QUOTE) {
i = scanString(i);
continue;
}
// Structural tokens
if (c === OPEN_BRACE || c === OPEN_BRACKET) {
const openCh = s[i];
const isBrace = c === OPEN_BRACE;
const closeCh = isBrace ? '}' : ']';
// Lookahead for empty {} or []: skip spaces to next significant char
let k = skipWS(i + 1);
if (k < n && s[k] === closeCh) {
// Emit {} / [] (no newline/indent)
out.push(openCh, closeCh);
i = k + 1;
continue;
}
// Non-empty: normal pretty formatting
out.push(openCh);
if (pretty) {
out.push('\n', getIndent(level + 1));
}
level++;
i++;
continue;
}
if (c === CLOSE_BRACE || c === CLOSE_BRACKET) {
level = level > 0 ? level - 1 : 0;
if (pretty) {
out.push('\n', getIndent(level));
}
out.push(s[i]);
i++;
continue;
}
if (c === COMMA) {
out.push(',');
if (pretty) {
out.push('\n', getIndent(level));
}
i++;
continue;
}
if (c === COLON) {
if (pretty) {
out.push(':', ' ');
} else {
out.push(':');
}
i++;
continue;
}
// Outside strings & not structural: copy a whole run (numbers, literals, bigint suffix, identifiers)
i = scanAtom(i);
}
return out.join('');
}
module.exports = fastJsonFormat;