UNPKG

@bscotch/yy

Version:

Stringify, parse, read, and write GameMaker yy and yyp files.

249 lines 7.63 kB
/** * @file Modified from public domain code: https://github.com/sidorares/json-bigint/blob/master/lib/parse.js */ const suspectProtoRx = /(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])/; const suspectConstructorRx = /(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)/; const escapee = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', }; export function parseYy(source, schema) { // Clear trailing commas source = source.replace(/,(\s*[}\]])/g, '$1'); /** Index of the current character */ let at = 0; /** The current character */ let ch = ' '; const text = source; function error(m) { throw { name: 'SyntaxError', message: m, at: at, text: text, }; } function next(c) { // If a c parameter is provided, verify that it matches the current character. if (c && c !== ch) { error("Expected '" + c + "' instead of '" + ch + "'"); } // Get the next character. When there are no more characters, // return the empty string. ch = text.charAt(at); at += 1; return ch; } function number() { // Parse a number value. let str = ''; if (ch === '-') { str = '-'; next('-'); } while (ch >= '0' && ch <= '9') { str += ch; next(); } if (ch === '.') { str += '.'; while (next() && ch >= '0' && ch <= '9') { str += ch; } } if (ch === 'e' || ch === 'E') { str += ch; next(); if (ch === '-' || ch === '+') { str += ch; next(); } while (ch >= '0' && ch <= '9') { str += ch; next(); } } // Store as a BigInt if // 1. it's an integer, and // 2. it's too big to store as a vanilla number. // (BigInts can only be parsed from purely-numeric strings) const num = +str; if (!str.match(/\.|E-/i)) { // Then it's not a float or a scientific notation number that will // turn into one. (e.g. not `1.0` or `1E-10`) const asBigInt = BigInt(str); if (asBigInt > Number.MAX_SAFE_INTEGER) { return asBigInt; } } return num; } function string() { // Parse a string value. let hex, i, string = '', uffff; // When parsing for string values, we must look for " and \ characters. if (ch === '"') { let startAt = at; while (next()) { if (ch === '"') { if (at - 1 > startAt) string += text.substring(startAt, at - 1); next(); return string; } if (ch === '\\') { if (at - 1 > startAt) string += text.substring(startAt, at - 1); next(); if (ch === 'u') { uffff = 0; for (i = 0; i < 4; i += 1) { hex = parseInt(next(), 16); if (!isFinite(hex)) { break; } uffff = uffff * 16 + hex; } string += String.fromCharCode(uffff); } else if (typeof escapee[ch] === 'string') { string += escapee[ch]; } else { break; } startAt = at; } } } error('Bad string'); } function white() { // Skip whitespace. while (ch && ch <= ' ') { next(); } } function word() { // true, false, or null. switch (ch) { case 't': next('t'); next('r'); next('u'); next('e'); return true; case 'f': next('f'); next('a'); next('l'); next('s'); next('e'); return false; case 'n': next('n'); next('u'); next('l'); next('l'); return null; } error("Unexpected '" + ch + "'"); } function value() { // Parse a JSON value. It could be an object, an array, a string, a number, // or a word. white(); switch (ch) { case '{': return object(); case '[': return array(); case '"': return string(); case '-': return number(); default: return ch >= '0' && ch <= '9' ? number() : word(); } } function array() { // Parse an array value. const array = []; if (ch === '[') { next('['); white(); if (ch === ']') { next(']'); return array; // empty array } while (ch) { array.push(value()); white(); if (ch === ']') { next(']'); return array; } next(','); white(); } } error('Bad array'); } function object() { // Parse an object value. let key; const obj = {}; /** * In 2024, GameMaker started using a new format, * where `resourceType` is replaced with a key * `${ResourceType}` (whose value is the version for that type). * To simplify downstream use, we'll normalize by adding * a common resourcetype key via the `yyResourceTypeSymbol`, * and note the format while we're at it using the * `yyFormatSymbol` key. */ if (ch === '{') { next('{'); white(); if (ch === '}') { next('}'); return obj; // empty object } while (ch) { key = string(); white(); next(':'); if (suspectProtoRx.test(key) === true) { error('Object contains forbidden prototype property'); } else if (suspectConstructorRx.test(key) === true) { error('Object contains forbidden constructor property'); } else { obj[key] = value(); } white(); if (ch === '}') { next('}'); return obj; } next(','); white(); } } error('Bad object'); } const result = value(); white(); if (ch) { error('Syntax error'); } return schema ? schema.parse(result) : result; } //# sourceMappingURL=Yy.parse.js.map