Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 161 additions & 3 deletions src/core/operations/JSONBeautify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import OperationError from "../errors/OperationError.mjs";
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";

const BIGINT_SENTINEL_PREFIX = "__CYBERCHEF_BIGINT__";
const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
const MIN_SAFE_INTEGER_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);

/**
* JSON Beautify operation
*/
Expand Down Expand Up @@ -58,14 +62,14 @@ class JSONBeautify extends Operation {
let json = null;

try {
json = JSON5.parse(input);
json = JSON5.parse(protectBigIntLiterals(input));
} catch (err) {
throw new OperationError("Unable to parse input as JSON.\n" + err);
}

if (sortBool) json = sortKeys(json);

return JSON.stringify(json, null, indentStr);
return restoreBigIntLiterals(JSON.stringify(json, null, indentStr));
}

/**
Expand All @@ -79,7 +83,7 @@ class JSONBeautify extends Operation {
const formatted = args[2];
if (!formatted) return Utils.escapeHtml(data);

const json = JSON5.parse(data);
const json = JSON5.parse(protectBigIntLiterals(data));
const options = {
withLinks: true,
bigNumbers: true
Expand Down Expand Up @@ -156,6 +160,13 @@ function isUrl(string) {
function json2html(json, options) {
let html = "";
if (typeof json === "string") {
const bigintLiteral = unwrapBigIntSentinel(json);

if (bigintLiteral !== null) {
html += `<span class="json-literal">${bigintLiteral}</span>`;
return html;
}

// Escape tags and quotes
json = Utils.escapeHtml(json);

Expand Down Expand Up @@ -241,4 +252,151 @@ function json2html(json, options) {
return html;
}

/**
* Protect large integer literals from JSON5 number parsing.
*
* @param {string} input
* @returns {string}
*/
function protectBigIntLiterals(input) {
let output = "";
let token = "";
let inString = false;
let stringQuote = "";
let escapeNext = false;
let inLineComment = false;
let inBlockComment = false;

const flushToken = () => {
if (!token) return;
output += protectNumberToken(token);
token = "";
};

for (let i = 0; i < input.length; i++) {
const char = input[i];
const next = input[i + 1] || "";

if (inLineComment) {
output += char;
if (char === "\n") inLineComment = false;
continue;
}

if (inBlockComment) {
output += char;
if (char === "*" && next === "/") {
output += next;
i++;
inBlockComment = false;
}
continue;
}

if (inString) {
output += char;
if (escapeNext) {
escapeNext = false;
} else if (char === "\\") {
escapeNext = true;
} else if (char === stringQuote) {
inString = false;
stringQuote = "";
}
continue;
}

if (char === "\"" || char === "'") {
flushToken();
inString = true;
stringQuote = char;
output += char;
continue;
}

if (char === "/" && next === "/") {
flushToken();
output += char + next;
i++;
inLineComment = true;
continue;
}

if (char === "/" && next === "*") {
flushToken();
output += char + next;
i++;
inBlockComment = true;
continue;
}

if (/[0-9-]/.test(char) && token.length === 0) {
const prev = output[output.length - 1] || "";
if (prev === "" || /[\s[:,{[]/.test(prev)) {
token = char;
continue;
}
}

if (token) {
if (/[0-9]/.test(char)) {
token += char;
continue;
}

flushToken();
}

output += char;
}

flushToken();

return output;
}

/**
* Replace an unsafe integer token with a quoted sentinel.
*
* @param {string} token
* @returns {string}
*/
function protectNumberToken(token) {
if (!/^-?(0|[1-9]\d+)$/.test(token)) {
return token;
}

const value = BigInt(token);
if (value > MAX_SAFE_INTEGER_BIGINT || value < MIN_SAFE_INTEGER_BIGINT) {
return `"${BIGINT_SENTINEL_PREFIX}${token}"`;
}

return token;
}

/**
* Restore quoted bigint sentinels back into raw integer literals.
*
* @param {string} input
* @returns {string}
*/
function restoreBigIntLiterals(input) {
const bigintLiteralRegex = /"__CYBERCHEF_BIGINT__(-?[0-9]+)"/g;
return input.replace(bigintLiteralRegex, (match, value) => value);
}

/**
* Decode a bigint sentinel value into its original number literal.
*
* @param {string} value
* @returns {string|null}
*/
function unwrapBigIntSentinel(value) {
if (!value.startsWith(BIGINT_SENTINEL_PREFIX)) {
return null;
}

return value.slice(BIGINT_SENTINEL_PREFIX.length);
}

export default JSONBeautify;
15 changes: 15 additions & 0 deletions tests/operations/tests/JSONBeautify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,19 @@ TestRegister.addTests([
}
],
},
{
name: "JSON Beautify: preserve large integer precision",
input: "{\"a\":1234567890123456789}",
expectedOutput: "{\n\t\"a\": 1234567890123456789\n}",
recipeConfig: [
{
op: "JSON Beautify",
args: ["\t", false, false],
},
{
op: "HTML To Text",
args: []
}
],
},
]);
Loading