UNPKG

multiconf

Version:

Work with JSON configs

150 lines (129 loc) 4.1 kB
/** * JSONC Parser class for handling JSON with comments */ class JSONCParser { /** * Parse JSONC string to object. * @param {string} jsoncString JSONC string to parse. * @returns {object} Parsed object. * @throws {SyntaxError} If the JSONC string is invalid. */ static parse(jsoncString) { if (typeof jsoncString !== 'string') { throw new TypeError('Input must be a string'); } // Remove comments and trailing commas. const jsonString = this._removeComments(jsoncString); return JSON.parse(jsonString); } /** * Remove comments and trailing commas from JSONC string. * @param {string} jsoncString JSONC string to clean. * @returns {string} Clean JSON string. * @private */ static _removeComments(jsoncString) { let result = ''; let inString = false; let inSingleLineComment = false; let inMultiLineComment = false; let lastChar = ''; let commentDepth = 0; let escapeNext = false; for (let i = 0; i < jsoncString.length; i++) { const char = jsoncString[i]; const nextChar = jsoncString[i + 1]; // Handle escape sequences. if (char === '\\' && !escapeNext) { escapeNext = true; result += char; continue; } // Handle string literals. if (char === '"' && !escapeNext) { inString = !inString; } // Handle single-line comments. if (!inString && char === '/' && nextChar === '/') { inSingleLineComment = true; i++; // Skip next character. continue; } // Handle multi-line comments. if (!inString && char === '/' && nextChar === '*') { inMultiLineComment = true; commentDepth++; i++; // Skip next character. continue; } // End single-line comment. if (inSingleLineComment && (char === '\n' || char === '\r')) { inSingleLineComment = false; } // Handle nested multi-line comments. if (inMultiLineComment) { if (char === '*' && nextChar === '/') { commentDepth--; if (commentDepth === 0) { inMultiLineComment = false; } i++; // Skip next character. continue; } if (char === '/' && nextChar === '*') { commentDepth++; i++; // Skip next character. continue; } continue; } // Only add characters that are not in comments. if (!inSingleLineComment && !inMultiLineComment) { // Handle trailing commas. if (char === ',' && !inString) { // Look ahead to find the next non-whitespace character. let j = i + 1; let foundNonWhitespace = false; while (j < jsoncString.length) { const nextChar = jsoncString[j]; // Skip over whitespace. if (nextChar === ' ' || nextChar === '\t' || nextChar === '\n' || nextChar === '\r') { j++; continue; } // Skip multiple commas (keep only one). if (nextChar === ',') { j++; continue; } // If the next non-whitespace character is a closing bracket/brace, skip the comma. if (nextChar === '}' || nextChar === ']') { foundNonWhitespace = true; break; } // Otherwise, this is a valid comma. foundNonWhitespace = true; result += char; break; } // If we reached the end of the string or only found closing brackets, don't add the comma. if (!foundNonWhitespace) { // Do nothing - skip the comma. } } else { result += char; } } lastChar = char; escapeNext = false; } // Validate that all comments are properly closed. if (inMultiLineComment) { throw new SyntaxError('Unclosed multi-line comment'); } return result; } } // Export. module.exports = JSONCParser;