/* * Author: Alex Kocharin <alex@kocharin.ru> * GIT: https://github.com/rlidwka/jju * License: WTFPL, grab your copy here: http://www.wtfpl.net/txt/copying/ */ // RTFM: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf var Uni = require('./unicode') function isHexDigit(x) { return (x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f') } function isOctDigit(x) { return x >= '0' && x <= '7' } function isDecDigit(x) { return x >= '0' && x <= '9' } var unescapeMap = { '\'': '\'', '"' : '"', '\\': '\\', 'b' : '\b', 'f' : '\f', 'n' : '\n', 'r' : '\r', 't' : '\t', 'v' : '\v', '/' : '/', } function formatError(input, msg, position, lineno, column, json5) { var result = msg + ' at ' + (lineno + 1) + ':' + (column + 1) , tmppos = position - column - 1 , srcline = '' , underline = '' var isLineTerminator = json5 ? Uni.isLineTerminator : Uni.isLineTerminatorJSON // output no more than 70 characters before the wrong ones if (tmppos < position - 70) { tmppos = position - 70 } while (1) { var chr = input[++tmppos] if (isLineTerminator(chr) || tmppos === input.length) { if (position >= tmppos) { // ending line error, so show it after the last char underline += '^' } break } srcline += chr if (position === tmppos) { underline += '^' } else if (position > tmppos) { underline += input[tmppos] === '\t' ? '\t' : ' ' } // output no more than 78 characters on the string if (srcline.length > 78) break } return result + '\n' + srcline + '\n' + underline } function parse(input, options) { // parse as a standard JSON mode var json5 = !(options.mode === 'json' || options.legacy) var isLineTerminator = json5 ? Uni.isLineTerminator : Uni.isLineTerminatorJSON var isWhiteSpace = json5 ? Uni.isWhiteSpace : Uni.isWhiteSpaceJSON var length = input.length , lineno = 0 , linestart = 0 , position = 0 , stack = [] var tokenStart = function() {} var tokenEnd = function(v) {return v} /* tokenize({ raw: '...', type: 'whitespace'|'comment'|'key'|'literal'|'separator'|'newline', value: 'number'|'string'|'whatever', path: [...], }) */ if (options._tokenize) { ;(function() { var start = null tokenStart = function() { if (start !== null) throw Error('internal error, token overlap') start = position } tokenEnd = function(v, type) { if (start != position) { var hash = { raw: input.substr(start, position-start), type: type, stack: stack.slice(0), } if (v !== undefined) hash.value = v options._tokenize.call(null, hash) } start = null return v } })() } function fail(msg) { var column = position - linestart if (!msg) { if (position < length) { var token = '\'' + JSON .stringify(input[position]) .replace(/^"|"$/g, '') .replace(/'/g, "\\'") .replace(/\\"/g, '"') + '\'' if (!msg) msg = 'Unexpected token ' + token } else { if (!msg) msg = 'Unexpected end of input' } } var error = SyntaxError(formatError(input, msg, position, lineno, column, json5)) error.row = lineno + 1 error.column = column + 1 throw error } function newline(chr) { // account for <cr><lf> if (chr === '\r' && input[position] === '\n') position++ linestart = position lineno++ } function parseGeneric() { var result while (position < length) { tokenStart() var chr = input[position++] if (chr === '"' || (chr === '\'' && json5)) { return tokenEnd(parseString(chr), 'literal') } else if (chr === '{') { tokenEnd(undefined, 'separator') return parseObject() } else if (chr === '[') { tokenEnd(undefined, 'separator') return parseArray() } else if (chr === '-' || chr === '.' || isDecDigit(chr) // + number Infinity NaN || (json5 && (chr === '+' || chr === 'I' || chr === 'N')) ) { return tokenEnd(parseNumber(), 'literal') } else if (chr === 'n') { parseKeyword('null') return tokenEnd(null, 'literal') } else if (chr === 't') { parseKeyword('true') return tokenEnd(true, 'literal') } else if (chr === 'f') { parseKeyword('false') return tokenEnd(false, 'literal') } else { position-- return tokenEnd(undefined) } } } function parseKey() { var result while (position < length) { tokenStart() var chr = input[position++] if (chr === '"' || (chr === '\'' && json5)) { return tokenEnd(parseString(chr), 'key') } else if (chr === '{') { tokenEnd(undefined, 'separator') return parseObject() } else if (chr === '[') { tokenEnd(undefined, 'separator') return parseArray() } else if (chr === '.' || isDecDigit(chr) ) { return tokenEnd(parseNumber(true), 'key') } else if (json5 && Uni.isIdentifierStart(chr) || (chr === '\\' && input[position] === 'u')) { // unicode char or a unicode sequence var rollback = position - 1 var result = parseIdentifier() if (result === undefined) { position = rollback return tokenEnd(undefined) } else { return tokenEnd(result, 'key') } } else { position-- return tokenEnd(undefined) } } } function skipWhiteSpace() { tokenStart() while (position < length) { var chr = input[position++] if (isLineTerminator(chr)) { position-- tokenEnd(undefined, 'whitespace') tokenStart() position++ newline(chr) tokenEnd(undefined, 'newline') tokenStart() } else if (isWhiteSpace(chr)) { // nothing } else if (chr === '/' && json5 && (input[position] === '/' || input[position] === '*') ) { position-- tokenEnd(undefined, 'whitespace') tokenStart() position++ skipComment(input[position++] === '*') tokenEnd(undefined, 'comment') tokenStart() } else { position-- break } } return tokenEnd(undefined, 'whitespace') } function skipComment(multi) { while (position < length) { var chr = input[position++] if (isLineTerminator(chr)) { // LineTerminator is an end of singleline comment if (!multi) { // let parent function deal with newline position-- return } newline(chr) } else if (chr === '*' && multi) { // end of multiline comment if (input[position] === '/') { position++ return } } else { // nothing } } if (multi) { fail('Unclosed multiline comment') } } function parseKeyword(keyword) { // keyword[0] is not checked because it should've checked earlier var _pos = position var len = keyword.length for (var i=1; i<len; i++) { if (position >= length || keyword[i] != input[position]) { position = _pos-1 fail() } position++ } } function parseObject() { var result = options.null_prototype ? Object.create(null) : {} , empty_object = {} , is_non_empty = false while (position < length) { skipWhiteSpace() var item1 = parseKey() skipWhiteSpace() tokenStart() var chr = input[position++] tokenEnd(undefined, 'separator') if (chr === '}' && item1 === undefined) { if (!json5 && is_non_empty) { position-- fail('Trailing comma in object') } return result } else if (chr === ':' && item1 !== undefined) { skipWhiteSpace() stack.push(item1) var item2 = parseGeneric() stack.pop() if (item2 === undefined) fail('No value found for key ' + item1) if (typeof(item1) !== 'string') { if (!json5 || typeof(item1) !== 'number') { fail('Wrong key type: ' + item1) } } if ((item1 in empty_object || empty_object[item1] != null) && options.reserved_keys !== 'replace') { if (options.reserved_keys === 'throw') { fail('Reserved key: ' + item1) } else { // silently ignore it } } else { if (typeof(options.reviver) === 'function') { item2 = options.reviver.call(null, item1, item2) } if (item2 !== undefined) { is_non_empty = true Object.defineProperty(result, item1, { value: item2, enumerable: true, configurable: true, writable: true, }) } } skipWhiteSpace() tokenStart() var chr = input[position++] tokenEnd(undefined, 'separator') if (chr === ',') { continue } else if (chr === '}') { return result } else { fail() } } else { position-- fail() } } fail() } function parseArray() { var result = [] while (position < length) { skipWhiteSpace() stack.push(result.length) var item = parseGeneric() stack.pop() skipWhiteSpace() tokenStart() var chr = input[position++] tokenEnd(undefined, 'separator') if (item !== undefined) { if (typeof(options.reviver) === 'function') { item = options.reviver.call(null, String(result.length), item) } if (item === undefined) { result.length++ item = true // hack for check below, not included into result } else { result.push(item) } } if (chr === ',') { if (item === undefined) { fail('Elisions are not supported') } } else if (chr === ']') { if (!json5 && item === undefined && result.length) { position-- fail('Trailing comma in array') } return result } else { position-- fail() } } } function parseNumber() { // rewind because we don't know first char position-- var start = position , chr = input[position++] , t var to_num = function(is_octal) { var str = input.substr(start, position - start) if (is_octal) { var result = parseInt(str.replace(/^0o?/, ''), 8) } else { var result = Number(str) } if (Number.isNaN(result)) { position-- fail('Bad numeric literal - "' + input.substr(start, position - start + 1) + '"') } else if (!json5 && !str.match(/^-?(0|[1-9][0-9]*)(\.[0-9]+)?(e[+-]?[0-9]+)?$/i)) { // additional restrictions imposed by json position-- fail('Non-json numeric literal - "' + input.substr(start, position - start + 1) + '"') } else { return result } } // ex: -5982475.249875e+29384 // ^ skipping this if (chr === '-' || (chr === '+' && json5)) chr = input[position++] if (chr === 'N' && json5) { parseKeyword('NaN') return NaN } if (chr === 'I' && json5) { parseKeyword('Infinity') // returning +inf or -inf return to_num() } if (chr >= '1' && chr <= '9') { // ex: -5982475.249875e+29384 // ^^^ skipping these while (position < length && isDecDigit(input[position])) position++ chr = input[position++] } // special case for leading zero: 0.123456 if (chr === '0') { chr = input[position++] // new syntax, "0o777" old syntax, "0777" var is_octal = chr === 'o' || chr === 'O' || isOctDigit(chr) var is_hex = chr === 'x' || chr === 'X' if (json5 && (is_octal || is_hex)) { while (position < length && (is_hex ? isHexDigit : isOctDigit)( input[position] ) ) position++ var sign = 1 if (input[start] === '-') { sign = -1 start++ } else if (input[start] === '+') { start++ } return sign * to_num(is_octal) } } if (chr === '.') { // ex: -5982475.249875e+29384 // ^^^ skipping these while (position < length && isDecDigit(input[position])) position++ chr = input[position++] } if (chr === 'e' || chr === 'E') { chr = input[position++] if (chr === '-' || chr === '+') position++ // ex: -5982475.249875e+29384 // ^^^ skipping these while (position < length && isDecDigit(input[position])) position++ chr = input[position++] } // we have char in the buffer, so count for it position-- return to_num() } function parseIdentifier() { // rewind because we don't know first char position-- var result = '' while (position < length) { var chr = input[position++] if (chr === '\\' && input[position] === 'u' && isHexDigit(input[position+1]) && isHexDigit(input[position+2]) && isHexDigit(input[position+3]) && isHexDigit(input[position+4]) ) { // UnicodeEscapeSequence chr = String.fromCharCode(parseInt(input.substr(position+1, 4), 16)) position += 5 } if (result.length) { // identifier started if (Uni.isIdentifierPart(chr)) { result += chr } else { position-- return result } } else { if (Uni.isIdentifierStart(chr)) { result += chr } else { return undefined } } } fail() } function parseString(endChar) { // 7.8.4 of ES262 spec var result = '' while (position < length) { var chr = input[position++] if (chr === endChar) { return result } else if (chr === '\\') { if (position >= length) fail() chr = input[position++] if (unescapeMap[chr] && (json5 || (chr != 'v' && chr != "'"))) { result += unescapeMap[chr] } else if (json5 && isLineTerminator(chr)) { // line continuation newline(chr) } else if (chr === 'u' || (chr === 'x' && json5)) { // unicode/character escape sequence var off = chr === 'u' ? 4 : 2 // validation for \uXXXX for (var i=0; i<off; i++) { if (position >= length) fail() if (!isHexDigit(input[position])) fail('Bad escape sequence') position++ } result += String.fromCharCode(parseInt(input.substr(position-off, off), 16)) } else if (json5 && isOctDigit(chr)) { if (chr < '4' && isOctDigit(input[position]) && isOctDigit(input[position+1])) { // three-digit octal var digits = 3 } else if (isOctDigit(input[position])) { // two-digit octal var digits = 2 } else { var digits = 1 } position += digits - 1 result += String.fromCharCode(parseInt(input.substr(position-digits, digits), 8)) /*if (!isOctDigit(input[position])) { // \0 is allowed still result += '\0' } else { fail('Octal literals are not supported') }*/ } else if (json5) { // \X -> x result += chr } else { position-- fail() } } else if (isLineTerminator(chr)) { fail() } else { if (!json5 && chr.charCodeAt(0) < 32) { position-- fail('Unexpected control character') } // SourceCharacter but not one of " or \ or LineTerminator result += chr } } fail() } skipWhiteSpace() var return_value = parseGeneric() if (return_value !== undefined || position < length) { skipWhiteSpace() if (position >= length) { if (typeof(options.reviver) === 'function') { return_value = options.reviver.call(null, '', return_value) } return return_value } else { fail() } } else { if (position) { fail('No data, only a whitespace') } else { fail('No data, empty input') } } } /* * parse(text, options) * or * parse(text, reviver) * * where: * text - string * options - object * reviver - function */ module.exports.parse = function parseJSON(input, options) { // support legacy functions if (typeof(options) === 'function') { options = { reviver: options } } if (input === undefined) { // parse(stringify(x)) should be equal x // with JSON functions it is not 'cause of undefined // so we're fixing it return undefined } // JSON.parse compat if (typeof(input) !== 'string') input = String(input) if (options == null) options = {} if (options.reserved_keys == null) options.reserved_keys = 'ignore' if (options.reserved_keys === 'throw' || options.reserved_keys === 'ignore') { if (options.null_prototype == null) { options.null_prototype = true } } try { return parse(input, options) } catch(err) { // jju is a recursive parser, so JSON.parse("{{{{{{{") could blow up the stack // // this catch is used to skip all those internal calls if (err instanceof SyntaxError && err.row != null && err.column != null) { var old_err = err err = SyntaxError(old_err.message) err.column = old_err.column err.row = old_err.row } throw err } } module.exports.tokenize = function tokenizeJSON(input, options) { if (options == null) options = {} options._tokenize = function(smth) { if (options._addstack) smth.stack.unshift.apply(smth.stack, options._addstack) tokens.push(smth) } var tokens = [] tokens.data = module.exports.parse(input, options) return tokens }