UNPKG

@bscotch/gml-parser

Version:

A parser for GML (GameMaker Language) files for programmatic manipulation and analysis of GameMaker projects.

168 lines 5.8 kB
import fs from 'node:fs/promises'; import { logger } from './logger.js'; export class StitchParserError extends Error { constructor(message, assertion) { super(message); this.name = 'StitchParserError'; Error.captureStackTrace(this, assertion || this.constructor); } } export const runningInVscode = !!process.env.VSCODE_IPC_HOOK; export const log = Object.assign((...args) => { if (log.enabled) { logger.log(...args); } }, { enabled: false }); export function throwError(message, asserter) { const err = new StitchParserError(message, asserter || throwError); if (runningInVscode) { // VSCode swallows error messages, so we need to log them logger.error(err); } throw err; } export function assert(condition, message) { if (!condition) { throwError(message, assert); } } /** @alias assert */ export const ok = assert; function jsonReplacer(key, value) { if (value instanceof Map) { const obj = {}; for (const [k, v] of value.entries()) { obj[k] = v; } return jsonReplacer(key, obj); } if (typeof value === 'function' && key === 'toJSON') { return jsonReplacer(key, value()); } return value; } export function stringify(obj) { return JSON.stringify(obj, jsonReplacer, 2); } /** * There are multiple ways that Feather/GameMaker accept types to * be encoded in strings. To simplify parsing on our end, we normalize * them all to a single format. */ export function normalizeTypeString(typeString) { typeString = typeString .replace(/\[/g, '<') .replace(/\]/g, '>') .replace(/,|\s+or\s+/gi, '|'); // Sometimes specific array types are specified with e.g. Array.String // instead of Array<String>. Normalize those. typeString = typeString.replace(/^Array\.([A-Z][A-Z0-9]*)/gi, 'Array<$1>'); return typeString; } export function isInRange(range, offset) { assert(range !== undefined, 'Range must be defined'); if (typeof offset === 'number') { return range.start.offset <= offset && range.end.offset >= offset; } else { const isSingleLineRange = range.start.line === range.end.line; // If we're on the start line, we must be at or after the start column if (offset.line === range.start.line) { const isAfterStartColumn = offset.column >= range.start.column; return (isAfterStartColumn && (!isSingleLineRange || offset.column <= range.end.column)); } // If we're on the end line, we must be at or before the end column if (offset.line === range.end.line) { return offset.column <= range.end.column; } // If we're on a line in between, we're in range if (offset.line > range.start.line && offset.line < range.end.line) { return true; } return false; } } export function isBeforeRange(range, offset) { assert(range !== undefined, 'isBeforeRange: Range must be defined'); if (typeof offset === 'number') { return offset < range.end.offset; } else { // If we're before the start line, definitely before the range if (offset.line < range.start.line) { return true; } // If we're on the start line, we must be before the start column if (offset.line === range.start.line) { return offset.column < range.start.column; } return false; } } export function isArray(value) { return Array.isArray(value); } export function isValidIdentifier(name) { return /^[a-z_][a-z0-9_]*$/i.test(name); } export function assertIsValidIdentifier(name) { assert(isValidIdentifier(name), `Invalid identifier: ${name}`); } /** * Depending on the origin, an asset group path could look like * a regular path (`my/path`) or like the path from a config file * (`folders/my/path.yy`). * * This function returns the POSIX-style path (`my/path`) given * one of those inputs/ */ export function groupPathToPosix(path) { return (path // Normalize slashes .replace(/[\\/]+/g, '/') // Remove leading and trailing slashes .replace(/^\//, '') .replace(/\/$/, '') // Remove `folders/` prefix and `.yy` suffix .replace(/^folders\/(.*)\.yy$/, '$1')); } export function xor(a, b) { return (a || b) && !(a && b); } export function neither(a, b) { return !a && !b; } export async function findYyFile(dir) { const dirName = dir.name; const yyFiles = (await dir.listChildren()).filter((p) => p.hasExtension('yy')); assert(yyFiles.length, `No .yy files found in ${dir}`); if (yyFiles.length === 1) { return yyFiles[0]; } // Else we have multiple for some reason. Try to find one that matches the dir name. Start with an exact match, then fall back to case-insensitive. const match = yyFiles.find((p) => p.name === dirName) || yyFiles.find((p) => p.name.toLowerCase() === dirName.toLowerCase()); assert(match, `Multiple .yy files found in ${dir}`); return match; } /** * Given the path to an image file, read just enough bytes to * ensure it's a PNG and to get its width and height */ export async function getPngSize(path) { const size = { width: 0, height: 0 }; const fd = await fs.open(path.toString(), 'r'); try { const buf = Buffer.alloc(24); await fd.read(buf, 0, 24, 16); size.width = buf.readUInt32BE(0); size.height = buf.readUInt32BE(4); } finally { await fd.close(); } assert(size.width > 0, `Invalid width for ${path}`); assert(size.height > 0, `Invalid height for ${path}`); return size; } //# sourceMappingURL=util.js.map