/** * 简单 json5 解析 (支持 // 单行注释, /* 多行注释 *\/, 字符串, 转义) * 不支持: 数字下划线, 十六进制等扩展 */ 'use strict'; function parseJson5(text) { let i = 0; const len = text.length; function peek() { return text[i]; } function next() { return text[i++]; } function skipWsAndComments() { while (i < len) { const c = text[i]; if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; } if (c === '/' && text[i + 1] === '/') { while (i < len && text[i] !== '\n') i++; continue; } if (c === '/' && text[i + 1] === '*') { i += 2; while (i < len && !(text[i] === '*' && text[i + 1] === '/')) i++; i += 2; continue; } break; } } function readString() { const quote = next(); // " or ' let out = ''; while (i < len) { const c = next(); if (c === '\\') { const esc = next(); switch (esc) { case 'n': out += '\n'; break; case 't': out += '\t'; break; case 'r': out += '\r'; break; case '\\': out += '\\'; break; case '"': out += '"'; break; case "'": out += "'"; break; case '/': out += '/'; break; case 'b': out += '\b'; break; case 'f': out += '\f'; break; case 'u': { const hex = text.substr(i, 4); i += 4; out += String.fromCharCode(parseInt(hex, 16)); break; } default: out += esc; } continue; } if (c === quote) return out; out += c; } throw new Error('unterminated string'); } function readValue() { skipWsAndComments(); const c = peek(); if (c === '"' || c === "'") return readString(); if (c === '{') return readObject(); if (c === '[') return readArray(); if (c === 't' || c === 'f') return readBoolOrNull(); if (c === 'n') return readBoolOrNull(); if (c === '-' || (c >= '0' && c <= '9')) return readNumber(); throw new Error(`unexpected char '${c}' at ${i}`); } function readObject() { next(); // { const obj = {}; while (true) { skipWsAndComments(); if (peek() === '}') { next(); return obj; } // key let key; if (peek() === '"' || peek() === "'") key = readString(); else { // bareword (json5 allows unquoted keys) let s = ''; while (i < len && /[A-Za-z0-9_$-]/.test(text[i])) s += text[i++]; key = s; } skipWsAndComments(); if (next() !== ':') throw new Error(`expected ':' at ${i}`); obj[key] = readValue(); skipWsAndComments(); const nc = next(); if (nc === ',') continue; if (nc === '}') return obj; throw new Error(`expected , or } at ${i}`); } } function readArray() { next(); // [ const arr = []; while (true) { skipWsAndComments(); if (peek() === ']') { next(); return arr; } arr.push(readValue()); skipWsAndComments(); const nc = next(); if (nc === ',') continue; if (nc === ']') return arr; throw new Error(`expected , or ] at ${i}`); } } function readBoolOrNull() { if (text.substr(i, 4) === 'true') { i += 4; return true; } if (text.substr(i, 5) === 'false') { i += 5; return false; } if (text.substr(i, 4) === 'null') { i += 4; return null; } throw new Error(`bad literal at ${i}`); } function readNumber() { let s = ''; if (peek() === '-') s += next(); while (i < len && /[0-9.eE+-]/.test(text[i])) s += next(); const n = parseFloat(s); if (isNaN(n)) throw new Error(`bad number '${s}'`); return n; } skipWsAndComments(); const result = readValue(); skipWsAndComments(); if (i < len) throw new Error(`trailing chars at ${i}: ${text.substr(i, 30)}`); return result; } module.exports = parseJson5;