fix(json): preserve large integer precision in beautify
This commit is contained in:
parent
167cc398ce
commit
56a8e6b982
@ -10,6 +10,10 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import Utils from "../Utils.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
|
* JSON Beautify operation
|
||||||
*/
|
*/
|
||||||
@ -58,14 +62,14 @@ class JSONBeautify extends Operation {
|
|||||||
let json = null;
|
let json = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json = JSON5.parse(input);
|
json = JSON5.parse(protectBigIntLiterals(input));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError("Unable to parse input as JSON.\n" + err);
|
throw new OperationError("Unable to parse input as JSON.\n" + err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBool) json = sortKeys(json);
|
if (sortBool) json = sortKeys(json);
|
||||||
|
|
||||||
return JSON.stringify(json, null, indentStr);
|
return restoreBigIntLiterals(JSON.stringify(json, null, indentStr));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,7 +83,7 @@ class JSONBeautify extends Operation {
|
|||||||
const formatted = args[2];
|
const formatted = args[2];
|
||||||
if (!formatted) return Utils.escapeHtml(data);
|
if (!formatted) return Utils.escapeHtml(data);
|
||||||
|
|
||||||
const json = JSON5.parse(data);
|
const json = JSON5.parse(protectBigIntLiterals(data));
|
||||||
const options = {
|
const options = {
|
||||||
withLinks: true,
|
withLinks: true,
|
||||||
bigNumbers: true
|
bigNumbers: true
|
||||||
@ -156,6 +160,13 @@ function isUrl(string) {
|
|||||||
function json2html(json, options) {
|
function json2html(json, options) {
|
||||||
let html = "";
|
let html = "";
|
||||||
if (typeof json === "string") {
|
if (typeof json === "string") {
|
||||||
|
const bigintLiteral = unwrapBigIntSentinel(json);
|
||||||
|
|
||||||
|
if (bigintLiteral !== null) {
|
||||||
|
html += `<span class="json-literal">${bigintLiteral}</span>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
// Escape tags and quotes
|
// Escape tags and quotes
|
||||||
json = Utils.escapeHtml(json);
|
json = Utils.escapeHtml(json);
|
||||||
|
|
||||||
@ -241,4 +252,151 @@ function json2html(json, options) {
|
|||||||
return html;
|
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;
|
export default JSONBeautify;
|
||||||
|
|||||||
@ -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…
x
Reference in New Issue
Block a user