UNPKG

polite-json

Version:

JSON.parse and stringify with better errors that respects formatting

162 lines 6.52 kB
/** * Copyright 2017 Kat Marchán * Copyright npm, Inc. * Copyright 2023 Isaac Z. Schlueter * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * 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. * * --- * * 'polite-json' is a fork of 'json-parse-even-better-errors', * extended and distributed under the terms of the MIT license * above. * * 'json-parse-even-better-errors' is a fork of * 'json-parse-better-errors' by Kat Marchán, extended and * distributed under the terms of the MIT license above. */ // version specific /* c8 ignore start */ const hexify = (s) => Array.from(s) .map(c => '0x' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')) .join(''); const parseError = (e, txt, context) => { if (!txt) { return { message: e.message + ' while parsing empty string', position: 0, }; } const badToken = e.message.match(/^Unexpected (?:token (.*?))?/i); const atPos = e.message.match(/at positions? (\d+)/); // version specific /* c8 ignore start */ const errIdx = /^Unexpected end of JSON|Unterminated string in JSON/i.test(e.message) ? txt.length - 1 : atPos && atPos[1] ? +atPos[1] : /is not valid JSON$/.test(e.message) ? 0 : null; const msg = badToken && badToken[1] ? e.message.replace(/^Unexpected token ./, `Unexpected token ${JSON.stringify(badToken[1])} (${hexify(badToken[1])})`) : e.message; /* c8 ignore stop */ if (errIdx !== null && errIdx !== undefined) { const start = errIdx <= context ? 0 : errIdx - context; const end = errIdx + context >= txt.length ? txt.length : errIdx + context; const slice = (start === 0 ? '' : '...') + txt.slice(start, end) + (end === txt.length ? '' : '...'); const near = txt === slice ? '' : 'near '; return { message: msg + ` while parsing ${near}${JSON.stringify(slice)}`, position: errIdx, }; } else { return { message: msg + ` while parsing '${txt.slice(0, context * 2)}'`, position: 0, }; } }; export class JSONParseError extends SyntaxError { code; cause; position; constructor(er, txt, context = 20, caller) { const { message, position } = parseError(er, txt, context); super(message); this.cause = er; this.position = position; this.code = 'EJSONPARSE'; Error.captureStackTrace(this, caller || this.constructor); } get name() { return this.constructor.name; } set name(_) { } get [Symbol.toStringTag]() { return this.constructor.name; } } export const kIndent = Symbol.for('indent'); export const kNewline = Symbol.for('newline'); // only respect indentation if we got a line break, otherwise squash it // things other than objects and arrays aren't indented, so ignore those // Important: in both of these regexps, the $1 capture group is the newline // or undefined, and the $2 capture group is the indent, or undefined. const formatRE = /^\s*[{\[]((?:\r?\n)+)([\s\t]*)/; const emptyRE = /^(?:\{\}|\[\])((?:\r?\n)+)?$/; export const parse = (txt, reviver, context) => { const parseText = stripBOM(String(txt)); if (!reviver) reviver = undefined; context = context || 20; try { // get the indentation so that we can save it back nicely // if the file starts with {" then we have an indent of '', ie, none // otherwise, pick the indentation of the next line after the first \n // If the pattern doesn't match, then it means no indentation. // JSON.stringify ignores symbols, so this is reasonably safe. // if the string is '{}' or '[]', then use the default 2-space indent. const [, newline = '\n', indent = ' '] = parseText.match(emptyRE) || parseText.match(formatRE) || [, '', '']; const result = JSON.parse(parseText, reviver); if (result && typeof result === 'object') { result[kNewline] = newline; result[kIndent] = indent; } return result; } catch (e) { if (typeof txt !== 'string' && !Buffer.isBuffer(txt)) { const isEmptyArray = Array.isArray(txt) && txt.length === 0; throw Object.assign(new TypeError(`Cannot parse ${isEmptyArray ? 'an empty array' : String(txt)}`), { code: 'EJSONPARSE', systemError: e, }); } throw new JSONParseError(e, parseText, context, parse); } }; export const parseNoExceptions = (txt, reviver) => { try { return JSON.parse(stripBOM(String(txt)), reviver); } catch (e) { } }; // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) // because the buffer-to-string conversion in `fs.readFileSync()` // translates it to FEFF, the UTF-16 BOM. const stripBOM = (txt) => String(txt).replace(/^\uFEFF/, ''); export const stringify = (obj, replacer, indent) => { const space = indent === undefined ? obj[kIndent] : indent; // TS is so weird with parameter overloads const res = /* c8 ignore start */ typeof replacer === 'function' ? JSON.stringify(obj, replacer, space) : JSON.stringify(obj, replacer, space); /* c8 ignore stop */ const nl = obj[kNewline] || '\n'; return space ? (nl === '\n' ? res : res.split('\n').join(nl)) + nl : res; }; //# sourceMappingURL=index.js.map