@bscotch/gml-parser
Version:
A parser for GML (GameMaker Language) files for programmatic manipulation and analysis of GameMaker projects.
168 lines • 5.8 kB
JavaScript
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