// JSON formatting const esprima = require('esprima') const { CommentArray, PREFIX_BEFORE, PREFIX_AFTER_PROP, PREFIX_AFTER_COLON, PREFIX_AFTER_VALUE, COLON, UNDEFINED } = require('./array') const tokenize = code => esprima.tokenize(code, { comment: true, loc: true }) const previous_hosts = [] let comments_host = null let unassigned_comments = null const previous_props = [] let last_prop let remove_comments = false let inline = false let tokens = null let last = null let current = null let index let reviver = null const clean = () => { previous_props.length = previous_hosts.length = 0 last = null last_prop = UNDEFINED } const free = () => { clean() tokens.length = 0 unassigned_comments = comments_host = tokens = last = current = reviver = null } const PREFIX_BEFORE_ALL = 'before-all' const PREFIX_AFTER = 'after' const PREFIX_AFTER_ALL = 'after-all' const BRACKET_OPEN = '[' const BRACKET_CLOSE = ']' const CURLY_BRACKET_OPEN = '{' const CURLY_BRACKET_CLOSE = '}' const COMMA = ',' const EMPTY = '' const MINUS = '-' const symbolFor = prefix => Symbol.for( last_prop !== UNDEFINED ? `${prefix}:${last_prop}` : prefix ) const transform = (k, v) => reviver ? reviver(k, v) : v const unexpected = () => { const error = new SyntaxError(`Unexpected token ${current.value.slice(0, 1)}`) Object.assign(error, current.loc.start) throw error } const unexpected_end = () => { const error = new SyntaxError('Unexpected end of JSON input') Object.assign(error, last ? last.loc.end // Empty string : { line: 1, column: 0 }) throw error } // Move the reader to the next const next = () => { const new_token = tokens[++ index] inline = current && new_token && current.loc.end.line === new_token.loc.start.line || false last = current current = new_token } const type = () => { if (!current) { unexpected_end() } return current.type === 'Punctuator' ? current.value : current.type } const is = t => type() === t const expect = a => { if (!is(a)) { unexpected() } } const set_comments_host = new_host => { previous_hosts.push(comments_host) comments_host = new_host } const restore_comments_host = () => { comments_host = previous_hosts.pop() } const assign_comments = prefix => { if (!unassigned_comments) { return } comments_host[symbolFor(prefix)] = unassigned_comments unassigned_comments = null } const parse_comments = prefix => { const comments = [] while ( current && ( is('LineComment') || is('BlockComment') ) ) { const comment = { ...current, inline } // delete comment.loc comments.push(comment) next() } if (remove_comments) { return } if (!comments.length) { return } if (prefix) { comments_host[symbolFor(prefix)] = comments return } unassigned_comments = comments } const set_prop = (prop, push) => { if (push) { previous_props.push(last_prop) } last_prop = prop } const restore_prop = () => { last_prop = previous_props.pop() } const parse_object = () => { const obj = {} set_comments_host(obj) set_prop(UNDEFINED, true) let started = false let name parse_comments() while (!is(CURLY_BRACKET_CLOSE)) { if (started) { // key-value pair delimiter expect(COMMA) next() parse_comments() if (is(CURLY_BRACKET_CLOSE)) { break } } started = true expect('String') name = JSON.parse(current.value) set_prop(name) assign_comments(PREFIX_BEFORE) next() parse_comments(PREFIX_AFTER_PROP) expect(COLON) next() parse_comments(PREFIX_AFTER_COLON) obj[name] = transform(name, walk()) parse_comments(PREFIX_AFTER_VALUE) } // bypass } next() last_prop = undefined // If there is no properties in the object, // try to save unassigned comments assign_comments( started ? PREFIX_AFTER : PREFIX_BEFORE ) restore_comments_host() restore_prop() return obj } const parse_array = () => { const array = new CommentArray() set_comments_host(array) set_prop(UNDEFINED, true) let started = false let i = 0 parse_comments() while (!is(BRACKET_CLOSE)) { if (started) { expect(COMMA) next() parse_comments() if (is(BRACKET_CLOSE)) { break } } started = true set_prop(i) assign_comments(PREFIX_BEFORE) array[i] = transform(i, walk()) parse_comments(PREFIX_AFTER_VALUE) i ++ } next() last_prop = undefined assign_comments( started ? PREFIX_AFTER : PREFIX_BEFORE ) restore_comments_host() restore_prop() return array } function walk () { let tt = type() if (tt === CURLY_BRACKET_OPEN) { next() return parse_object() } if (tt === BRACKET_OPEN) { next() return parse_array() } let negative = EMPTY // -1 if (tt === MINUS) { next() tt = type() negative = MINUS } let v switch (tt) { case 'String': case 'Boolean': case 'Null': case 'Numeric': v = current.value next() return JSON.parse(negative + v) default: } } const isObject = subject => Object(subject) === subject const parse = (code, rev, no_comments) => { // Clean variables in closure clean() tokens = tokenize(code) reviver = rev remove_comments = no_comments if (!tokens.length) { unexpected_end() } index = - 1 next() set_comments_host({}) parse_comments(PREFIX_BEFORE_ALL) let result = walk() parse_comments(PREFIX_AFTER_ALL) if (current) { unexpected() } if (!no_comments && result !== null) { if (!isObject(result)) { // 1 -> new Number(1) // true -> new Boolean(1) // "foo" -> new String("foo") // eslint-disable-next-line no-new-object result = new Object(result) } Object.assign(result, comments_host) } restore_comments_host() // reviver result = transform('', result) free() return result } module.exports = { parse, tokenize, PREFIX_BEFORE, PREFIX_BEFORE_ALL, PREFIX_AFTER_PROP, PREFIX_AFTER_COLON, PREFIX_AFTER_VALUE, PREFIX_AFTER, PREFIX_AFTER_ALL, BRACKET_OPEN, BRACKET_CLOSE, CURLY_BRACKET_OPEN, CURLY_BRACKET_CLOSE, COLON, COMMA, EMPTY, UNDEFINED }