multiconf
Version:
Work with JSON configs
150 lines (129 loc) • 4.1 kB
JavaScript
/**
* 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;