gyp-parser
Version:
GYP file format parser in JS
261 lines (239 loc) • 7.59 kB
JavaScript
'use strict';
class ParseError extends Error {
constructor(input, at, message) {
const before = input.slice(Math.max(at - 6, 0), Math.min(input.length, at));
const exact = input[at];
const after = input.slice(Math.min(input.length, at + 1), Math.min(input.length, at + 6));
const source = `${before}<${exact}>${after}`;
super(`${message} around position ${at} (${source})`);
}
}
function parseGYP(input) {
const [ err, value ] = parseElement(input, 0);
if (err) throw err;
return value;
}
function discardWhitespace(input, at) {
while (at < input.length && input[at].trim() === '') at++;
if (input[at] === '#') {
while (at < input.length && input[at] !== '\n') at++;
return discardWhitespace(input, at);
}
return at;
}
function parseValue(input, at) {
at = discardWhitespace(input, at);
let attempt = [];
switch (input[at]) {
case '{': return parseObject(input, at);
case '[': return parseArray(input, at);
case '"':
case "'": return parseString(input, at);
case 't': return parseTrue(input, at);
case 'f': return parseFalse(input, at);
case 'n': return parseNull(input, at);
case '-':
case '+': return parseNumber(input, at);
default:
if (input.codePointAt(at) >= 0x30 && input.codePointAt(at) <= 0x39)
return parseNumber(input, at);
}
return [ new ParseError(input, at, 'Unexpected token') ];
}
function parseTrue(input, at) {
if (input.slice(at, at + 4) === 'true')
return [ null, true, at + 4 ];
return [ new ParseError(input, at, 'Expected "true"') ];
}
function parseFalse(input, at) {
if (input.slice(at, at + 5) === 'false')
return [ null, false, at + 5 ];
return [ new ParseError(input, at, 'Expected "false"') ];
}
function parseNull(input, at) {
if (input.slice(at, at + 4) === 'null')
return [ null, null, at + 4 ];
return [ new ParseError(input, at, 'Expected "null"') ];
}
function parseObject(input, at) {
if (input[at] !== '{')
return [ new ParseError(input, at, 'Expected "{"') ];
at++;
at = discardWhitespace(input, at);
if (input[at] === '}')
return [ null, {}, at + 1 ];
const [ err, members, newAt ] = parseMembers(input, at);
if (err) return [ err ];
if (input[newAt] !== '}')
return [ new ParseError(input, at, 'Expected "}"') ];
return [ err, ObjectFromEntries(members), newAt + 1 ];
}
function ObjectFromEntries(entries) {
const ret = {};
for (const [ key, value ] of entries)
ret[key] = value;
return ret;
}
function parseMembers(input, at) {
if (input[at] === ',')
return [ new ParseError(input, at, 'Unexpected ","') ];
const members = [];
while (input[at] !== '}') {
let [ err, member, newAt ] = parseMember(input, at);
if (err) return [ err ];
at = newAt;
members.push(member);
if (at >= input.length)
return [ new ParseError(input, at, 'Unexpected end of input') ];
at = discardWhitespace(input, at);
if (input[at] === '}') break;
if (input[at] !== ',')
return [ new ParseError(input, at, 'Expected "," or "}"') ];
at++;
at = discardWhitespace(input, at);
}
return [ null, members, at ];
}
function parseMember(input, at) {
at = discardWhitespace(input, at);
const member = [ null, null ];
{
const [ err, key, newAt ] = parseString(input, at);
if (err) return [ err ];
at = newAt;
member[0] = key;
}
at = discardWhitespace(input, at);
if (input[at] !== ':')
return [ new ParseError(input, at, 'Expected ":"') ];
at++;
{
const [ err, element, newAt ] = parseElement(input, at);
if (err) return [ err ];
at = newAt;
member[1] = element;
}
return [ null, member, at ];
}
function parseArray(input, at) {
if (input[at] !== '[')
return [ new ParseError(input, at, 'Expected "["') ];
at++;
at = discardWhitespace(input, at);
if (input[at] === ']')
return [ null, [], at + 1 ];
const [ err, elements, newAt ] = parseElements(input, at);
if (err) return [ err ];
if (input[newAt] !== ']')
return [ new ParseError(input, at, 'Expected "]"') ];
return [ err, elements, newAt + 1 ];
}
function parseElements(input, at) {
if (input[at] === ',')
return [ new ParseError(input, at, 'Unexpected ","') ];
const elements = [];
while (input[at] !== ']') {
let [ err, element, newAt ] = parseElement(input, at);
if (err) return [ err ];
at = newAt;
elements.push(element);
if (at >= input.length)
return [ new ParseError(input, at, 'Unexpected end of input') ];
at = discardWhitespace(input, at);
if (input[at] === ']') break;
if (input[at] !== ',')
return [ new ParseError(input, at, 'Expected "," or "]"') ];
at++;
at = discardWhitespace(input, at);
}
return [ null, elements, at ];
}
function parseElement(input, at) {
at = discardWhitespace(input, at);
const [ err, value, newAt ] = parseValue(input, at);
at = newAt;
at = discardWhitespace(input, at);
return [ err, value, at ];
}
function parseString(input, at) {
if (input[at] !== '"' && input[at] !== "'")
return [ new ParseError(input, at, 'Expected \' or "') ];
const type = input[at++];
let value = '';
while (input[at] !== type) {
if (input[at] === '\\') {
switch (input[++at]) {
case '"':
case "'":
case '\\':
case '/':
value += input[at++];
break;
case 'b': value += '\b'; at++; break;
case 'f': value += '\f'; at++; break;
case 'n': value += '\n'; at++; break;
case 'r': value += '\r'; at++; break;
case 't': value += '\t'; at++; break;
case 'u': {
at++;
const hexString = input.slice(at, at + 4);
if (!/^[0-9A-Fa-f]+$/.test(hexString))
return [ new ParseError(input, at, 'Invalid hex escape') ];
value += String.fromCodePoint(parseInt(hexString, 16));
at += 4;
break;
}
case 'x': {
at++;
const hexString = input.slice(at, at + 2);
if (!/^[0-9A-Fa-f]+$/.test(hexString))
return [ new ParseError(input, at, 'Invalid hex escape') ];
value += String.fromCodePoint(parseInt(hexString, 16));
at += 2;
break;
}
case '\r':
if (input[at + 1] === '\n') {
at += 2;
} else {
at++;
}
break;
case '\n':
at++;
break;
default:
return [ new ParseError(input, at, 'Unknown escape character') ];
}
} else {
value += input[at++];
}
if (at >= input.length)
return [ new ParseError(input, at, 'Unexpected end of input') ];
}
at++;
at = discardWhitespace(input, at);
if (input[at] === '"' || input[at] === "'") {
const [ err, more, newAt ] = parseString(input, at);
if (err) return [ err ];
return [ null, value + more, newAt ];
}
return [ null, value, at ];
}
function parseNumber(input, at) {
if (input[at] === '+') at++;
if (input[at] === '0' && input[at + 1] === 'x') {
at += 2;
const m = input.slice(at).match(/^([0-9a-fA-F]+)/);
if (!m) {
return [ new ParseError(input, at, 'Expected hexadecimal number') ];
}
return [ null, parseInt(m[0], 16), at + m[0].length ];
}
const m = input.slice(at).match(/^[-]?([1-9][0-9]+|[0-9])(\.[0-9]*)?([eE][+-][0-9]+)?/);
if (!m) {
return [ new ParseError(input, at, 'Expected number') ];
}
return [ null, JSON.parse(m[0]), at + m[0].length ];
}
exports.parse = parseGYP;