'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * @fileoverview JSON syntax helpers * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Predefined Tokens //----------------------------------------------------------------------------- const LBRACKET = "["; const RBRACKET = "]"; const LBRACE = "{"; const RBRACE = "}"; const COLON = ":"; const COMMA = ","; const TRUE = "true"; const FALSE = "false"; const NULL = "null"; const QUOTE = "\""; const expectedKeywords = new Map([ ["t", TRUE], ["f", FALSE], ["n", NULL] ]); const escapeToChar = new Map([ [QUOTE, QUOTE], ["\\", "\\"], ["/", "/"], ["b", "\b"], ["n", "\n"], ["f", "\f"], ["r", "\r"], ["t", "\t"] ]); const knownTokenTypes = new Map([ [LBRACKET, "Punctuator"], [RBRACKET, "Punctuator"], [LBRACE, "Punctuator"], [RBRACE, "Punctuator"], [COLON, "Punctuator"], [COMMA, "Punctuator"], [TRUE, "Boolean"], [FALSE, "Boolean"], [NULL, "Null"] ]); /** * @fileoverview JSON tokenization/parsing errors * @author Nicholas C. Zakas */ /** * Base class that attaches location to an error. */ class ErrorWithLocation extends Error { /** * * @param {string} message The error message to report. * @param {int} loc.line The line on which the error occurred. * @param {int} loc.column The column in the line where the error occurrred. * @param {int} loc.index The index in the string where the error occurred. */ constructor(message, { line, column, index }) { super(`${ message } (${ line }:${ column})`); /** * The line on which the error occurred. * @type int * @property line */ this.line = line; /** * The column on which the error occurred. * @type int * @property column */ this.column = column; /** * The index into the string where the error occurred. * @type int * @property index */ this.index = index; } } /** * Error thrown when an unexpected character is found during tokenizing. */ class UnexpectedChar extends ErrorWithLocation { /** * Creates a new instance. * @param {string} unexpected The character that was found. * @param {Object} loc The location information for the found character. */ constructor(unexpected, loc) { super(`Unexpected character ${ unexpected } found.`, loc); } } /** * Error thrown when an unexpected token is found during parsing. */ class UnexpectedToken extends ErrorWithLocation { /** * Creates a new instance. * @param {string} expected The character that was expected. * @param {string} unexpected The character that was found. * @param {Object} loc The location information for the found character. */ constructor(token) { super(`Unexpected token ${ token.type }(${ token.value }) found.`, token.loc.start); } } /** * Error thrown when the end of input is found where it isn't expected. */ class UnexpectedEOF extends ErrorWithLocation { /** * Creates a new instance. * @param {Object} loc The location information for the found character. */ constructor(loc) { super("Unexpected end of input found.", loc); } } /** * @fileoverview JSON tokenizer * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- const QUOTE$1 = "\""; const SLASH = "/"; const STAR = "*"; const DEFAULT_OPTIONS = { comments: false, ranges: false }; function isWhitespace(c) { return /[\s\n]/.test(c); } function isDigit(c) { return c >= "0" && c <= "9"; } function isHexDigit(c) { return isDigit(c) || /[a-f]/i.test(c); } function isPositiveDigit(c) { return c >= "1" && c <= "9"; } function isKeywordStart(c) { return /[tfn]/.test(c); } function isNumberStart(c) { return isDigit(c) || c === "." || c === "-"; } //----------------------------------------------------------------------------- // Main //----------------------------------------------------------------------------- /** * Creates an iterator over the tokens representing the source text. * @param {string} text The source text to tokenize. * @returns {Iterator} An iterator over the tokens. */ function tokenize(text, options) { options = Object.freeze({ ...DEFAULT_OPTIONS, ...options }); let offset = -1; let line = 1; let column = 0; let newLine = false; const tokens = []; function createToken(tokenType, value, startLoc, endLoc) { const endOffset = startLoc.offset + value.length; let range = options.ranges ? { range: [startLoc.offset, endOffset] } : undefined; return { type: tokenType, value, loc: { start: startLoc, end: endLoc || { line: startLoc.line, column: startLoc.column + value.length, offset: endOffset } }, ...range }; } function next() { let c = text.charAt(++offset); if (newLine) { line++; column = 1; newLine = false; } else { column++; } if (c === "\r") { newLine = true; // if we already see a \r, just ignore upcoming \n if (text.charAt(offset + 1) === "\n") { offset++; } } else if (c === "\n") { newLine = true; } return c; } function locate() { return { line, column, offset }; } function readKeyword(c) { // get the expected keyword let value = expectedKeywords.get(c); // check to see if it actually exists if (text.slice(offset, offset + value.length) === value) { offset += value.length - 1; column += value.length - 1; return { value, c: next() }; } // find the first unexpected character for (let j = 1; j < value.length; j++) { if (value[j] !== text.charAt(offset + j)) { unexpected(next()); } } } function readString(c) { let value = c; c = next(); while (c && c !== QUOTE$1) { // escapes if (c === "\\") { value += c; c = next(); if (escapeToChar.has(c)) { value += c; } else if (c === "u") { value += c; for (let i = 0; i < 4; i++) { c = next(); if (isHexDigit(c)) { value += c; } else { unexpected(c); } } } else { unexpected(c); } } else { value += c; } c = next(); } if (!c) { unexpectedEOF(); } value += c; return { value, c: next() }; } function readNumber(c) { let value = ""; // Number may start with a minus but not a plus if (c === "-") { value += c; c = next(); // Next digit cannot be zero if (!isDigit(c)) { unexpected(c); } } // Zero must be followed by a decimal point or nothing if (c === "0") { value += c; c = next(); if (isDigit(c)) { unexpected(c); } } else { if (!isPositiveDigit(c)) { unexpected(c); } do { value += c; c = next(); } while (isDigit(c)); } // Decimal point may be followed by any number of digits if (c === ".") { do { value += c; c = next(); } while (isDigit(c)); } // Exponent is always last if (c === "e" || c === "E") { value += c; c = next(); if (c === "+" || c === "-") { value += c; c = next(); } /* * Must always have a digit in this position to avoid: * 5e * 12E+ * 42e- */ if (!isDigit(c)) { unexpected(c); } while (isDigit(c)) { value += c; c = next(); } } return { value, c }; } /** * Reads in either a single-line or multi-line comment. * @param {string} c The first character of the comment. * @returns {string} The comment string. * @throws {UnexpectedChar} when the comment cannot be read. * @throws {UnexpectedEOF} when EOF is reached before the comment is * finalized. */ function readComment(c) { let value = c; // next character determines single- or multi-line c = next(); // single-line comments if (c === "/") { do { value += c; c = next(); } while (c && c !== "\r" && c !== "\n"); return { value, c }; } // multi-line comments if (c === STAR) { while (c) { value += c; c = next(); // check for end of comment if (c === STAR) { value += c; c = next(); //end of comment if (c === SLASH) { value += c; /* * The single-line comment functionality cues up the * next character, so we do the same here to avoid * splitting logic later. */ c = next(); return { value, c }; } } } unexpectedEOF(); } // if we've made it here, there's an invalid character unexpected(c); } /** * Convenience function for throwing unexpected character errors. * @param {string} c The unexpected character. * @returns {void} * @throws {UnexpectedChar} always. */ function unexpected(c) { throw new UnexpectedChar(c, locate()); } /** * Convenience function for throwing unexpected EOF errors. * @returns {void} * @throws {UnexpectedEOF} always. */ function unexpectedEOF() { throw new UnexpectedEOF(locate()); } let c = next(); while (offset < text.length) { while (isWhitespace(c)) { c = next(); } if (!c) { break; } const start = locate(); // check for easy case if (knownTokenTypes.has(c)) { tokens.push(createToken(knownTokenTypes.get(c), c, start)); c = next(); } else if (isKeywordStart(c)) { const result = readKeyword(c); let value = result.value; c = result.c; tokens.push(createToken(knownTokenTypes.get(value), value, start)); } else if (isNumberStart(c)) { const result = readNumber(c); let value = result.value; c = result.c; tokens.push(createToken("Number", value, start)); } else if (c === QUOTE$1) { const result = readString(c); let value = result.value; c = result.c; tokens.push(createToken("String", value, start)); } else if (c === SLASH && options.comments) { const result = readComment(c); let value = result.value; c = result.c; tokens.push(createToken(value.startsWith("//") ? "LineComment" : "BlockComment", value, start, locate())); } else { unexpected(c); } } return tokens; } /** * @fileoverview Momoa JSON AST types * @author Nicholas C. Zakas */ const types = { document(body, parts = {}) { return { type: "Document", body, ...parts }; }, string(value, parts = {}) { return { type: "String", value, ...parts }; }, number(value, parts = {}) { return { type: "Number", value, ...parts }; }, boolean(value, parts = {}) { return { type: "Boolean", value, ...parts }; }, null(parts = {}) { return { type: "Null", value: "null", ...parts }; }, array(elements, parts = {}) { return { type: "Array", elements, ...parts }; }, object(members, parts = {}) { return { type: "Object", members, ...parts }; }, member(name, value, parts = {}) { return { type: "Member", name, value, ...parts }; }, }; /** * @fileoverview JSON parser * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- const DEFAULT_OPTIONS$1 = { tokens: false, comments: false, ranges: false }; /** * Converts a JSON-encoded string into a JavaScript string, interpreting each * escape sequence. * @param {Token} token The string token to convert into a JavaScript string. * @returns {string} A JavaScript string. */ function getStringValue(token) { // slice off the quotation marks let value = token.value.slice(1, -1); let result = ""; let escapeIndex = value.indexOf("\\"); let lastIndex = 0; // While there are escapes, interpret them to build up the result while (escapeIndex >= 0) { // append the text that happened before the escape result += value.slice(lastIndex, escapeIndex); // get the character immediately after the \ const escapeChar = value.charAt(escapeIndex + 1); // check for the non-Unicode escape sequences first if (escapeToChar.has(escapeChar)) { result += escapeToChar.get(escapeChar); lastIndex = escapeIndex + 2; } else if (escapeChar === "u") { const hexCode = value.slice(escapeIndex + 2, escapeIndex + 6); if (hexCode.length < 4 || /[^0-9a-f]/i.test(hexCode)) { throw new ErrorWithLocation( `Invalid unicode escape \\u${ hexCode}.`, { line: token.loc.start.line, column: token.loc.start.column + escapeIndex, offset: token.loc.start.offset + escapeIndex } ); } result += String.fromCharCode(parseInt(hexCode, 16)); lastIndex = escapeIndex + 6; } else { throw new ErrorWithLocation( `Invalid escape \\${ escapeChar }.`, { line: token.loc.start.line, column: token.loc.start.column + escapeIndex, offset: token.loc.start.offset + escapeIndex } ); } // find the next escape sequence escapeIndex = value.indexOf("\\", lastIndex); } // get the last segment of the string value result += value.slice(lastIndex); return result; } /** * Gets the JavaScript value represented by a JSON token. * @param {Token} token The JSON token to get a value for. * @returns {*} A number, string, boolean, or `null`. */ function getLiteralValue(token) { switch (token.type) { case "Boolean": return token.value === "true"; case "Number": return Number(token.value); case "Null": return null; case "String": return getStringValue(token); } } //----------------------------------------------------------------------------- // Main Function //----------------------------------------------------------------------------- /** * * @param {string} text The text to parse. * @param {boolean} [options.tokens=false] Determines if tokens are returned in * the AST. * @param {boolean} [options.comments=false] Determines if comments are allowed * in the JSON. * @param {boolean} [options.ranges=false] Determines if ranges will be returned * in addition to `loc` properties. * @returns {Object} The AST representing the parsed JSON. * @throws {Error} When there is a parsing error. */ function parse(text, options) { options = Object.freeze({ ...DEFAULT_OPTIONS$1, ...options }); const tokens = tokenize(text, { comments: !!options.comments, ranges: !!options.ranges }); let tokenIndex = 0; function nextNoComments() { return tokens[tokenIndex++]; } function nextSkipComments() { const nextToken = tokens[tokenIndex++]; if (nextToken && nextToken.type.endsWith("Comment")) { return nextSkipComments(); } return nextToken; } // determine correct way to evaluate tokens based on presence of comments const next = options.comments ? nextSkipComments : nextNoComments; function assertTokenValue(token, value) { if (!token || token.value !== value) { throw new UnexpectedToken(token); } } function assertTokenType(token, type) { if (!token || token.type !== type) { throw new UnexpectedToken(token); } } function createRange(start, end) { return options.ranges ? { range: [start.offset, end.offset] } : undefined; } function createLiteralNode(token) { const range = createRange(token.loc.start, token.loc.end); return { type: token.type, value: getLiteralValue(token), loc: { start: { ...token.loc.start }, end: { ...token.loc.end } }, ...range }; } function parseProperty(token) { assertTokenType(token, "String"); const name = createLiteralNode(token); token = next(); assertTokenValue(token, ":"); const value = parseValue(); const range = createRange(name.loc.start, value.loc.end); return types.member(name, value, { loc: { start: { ...name.loc.start }, end: { ...value.loc.end } }, ...range }); } function parseObject(firstToken) { // The first token must be a { or else it's an error assertTokenValue(firstToken, "{"); const members = []; let token = next(); if (token && token.value !== "}") { do { // add the value into the array members.push(parseProperty(token)); token = next(); if (token.value === ",") { token = next(); } else { break; } } while (token); } assertTokenValue(token, "}"); const range = createRange(firstToken.loc.start, token.loc.end); return types.object(members, { loc: { start: { ...firstToken.loc.start }, end: { ...token.loc.end } }, ...range }); } function parseArray(firstToken) { // The first token must be a [ or else it's an error assertTokenValue(firstToken, "["); const elements = []; let token = next(); if (token && token.value !== "]") { do { // add the value into the array elements.push(parseValue(token)); token = next(); if (token.value === ",") { token = next(); } else { break; } } while (token); } assertTokenValue(token, "]"); const range = createRange(firstToken.loc.start, token.loc.end); return types.array(elements, { type: "Array", elements, loc: { start: { ...firstToken.loc.start }, end: { ...token.loc.end } }, ...range }); } function parseValue(token) { token = token || next(); switch (token.type) { case "String": case "Boolean": case "Number": case "Null": return createLiteralNode(token); case "Punctuator": if (token.value === "{") { return parseObject(token); } else if (token.value === "[") { return parseArray(token); } /*falls through*/ default: throw new UnexpectedToken(token); } } const docBody = parseValue(); const unexpectedToken = next(); if (unexpectedToken) { throw new UnexpectedToken(unexpectedToken); } const docParts = { loc: { start: { line: 1, column: 1, offset: 0 }, end: { ...docBody.loc.end } } }; if (options.tokens) { docParts.tokens = tokens; } if (options.ranges) { docParts.range = createRange(docParts.loc.start, docParts.loc.end); } return types.document(docBody, docParts); } /** * @fileoverview Traversal approaches for Momoa JSON AST. * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Data //----------------------------------------------------------------------------- const childKeys = new Map([ ["Document", ["body"]], ["Object", ["members"]], ["Member", ["name", "value"]], ["Array", ["elements"]], ["String", []], ["Number", []], ["Boolean", []], ["Null", []] ]); //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /** * Determines if a given value is an object. * @param {*} value The value to check. * @returns {boolean} True if the value is an object, false if not. */ function isObject(value) { return value && (typeof value === "object"); } /** * Determines if a given value is an AST node. * @param {*} value The value to check. * @returns {boolean} True if the value is a node, false if not. */ function isNode(value) { return isObject(value) && (typeof value.type === "string"); } //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Traverses an AST from the given node. * @param {Node} root The node to traverse from * @param {Object} visitor An object with an `enter` and `exit` method. */ function traverse(root, visitor) { /** * Recursively visits a node. * @param {Node} node The node to visit. * @param {Node} parent The parent of the node to visit. * @returns {void} */ function visitNode(node, parent) { if (typeof visitor.enter === "function") { visitor.enter(node, parent); } for (const key of childKeys.get(node.type)) { const value = node[key]; if (isObject(value)) { if (Array.isArray(value)) { value.forEach(child => visitNode(child, node)); } else if (isNode(value)) { visitNode(value, node); } } } if (typeof visitor.exit === "function") { visitor.exit(node, parent); } } visitNode(root); } /** * Creates an iterator over the given AST. * @param {Node} root The root AST node to traverse. * @param {Function} [filter] A filter function to determine which steps to * return; * @returns {Iterator} An iterator over the AST. */ function iterator(root, filter = () => true) { const traversal = []; traverse(root, { enter(node, parent) { traversal.push({ node, parent, phase: "enter" }); }, exit(node, parent) { traversal.push({ node, parent, phase: "exit" }); } }); return traversal.filter(filter).values(); } /** * @fileoverview Evaluator for Momoa AST. * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Evaluates a Momoa AST node into a JavaScript value. * @param {Node} node The node to interpet. * @returns {*} The JavaScript value for the node. */ function evaluate(node) { switch (node.type) { case "String": case "Number": case "Boolean": return node.value; case "Null": return null; case "Array": return node.elements.map(evaluate); case "Object": { const object = {}; node.members.forEach(member => { object[evaluate(member.name)] = evaluate(member.value); }); return object; } case "Document": return evaluate(node.body); case "Property": throw new Error("Cannot evaluate object property outside of an object."); default: throw new Error(`Unknown node type ${ node.type }.`); } } /** * @fileoverview Printer for Momoa AST. * @author Nicholas C. Zakas */ //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Converts a Momoa AST back into a JSON string. * @param {Node} node The node to print. * @param {int} [options.indent=0] The number of spaces to indent each line. If * greater than 0, then newlines and indents will be added to output. * @returns {string} The JSON representation of the AST. */ function print(node, { indent = 0 } = {}) { const value = evaluate(node); return JSON.stringify(value, null, indent); } /** * @fileoverview File defining the interface of the package. * @author Nicholas C. Zakas */ exports.evaluate = evaluate; exports.iterator = iterator; exports.parse = parse; exports.print = print; exports.tokenize = tokenize; exports.traverse = traverse; exports.types = types;