UNPKG

@bscotch/yy

Version:

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

154 lines 5.2 kB
import { z } from 'zod'; export const nameField = '%Name'; export function randomString(length = 32) { let a = ''; for (let i = 0; i < length; i++) { a += '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'[(Math.random() * 60) | 0]; } return a; } export class FixedNumber extends Number { digits; constructor(value, digits = 1) { super(value.valueOf()); this.digits = digits; } [Symbol.toPrimitive](hint) { return hint === 'string' ? this.toString() : this.valueOf(); } toString() { return this.toFixed(this.digits); } toJSON() { return this.valueOf(); } } /** * A wrapper that transforms the wrapped value to `undefined` and marks it as optional. * Useful for fields you want to provide for extra * information when transforming parents, since the * `z.input<>` inferred type will show this field, * but that don't normally exist in the source data. */ export function hint(schema) { return schema.optional().transform(() => undefined); } export const fixed0 = new FixedNumber(0); export const fixed1 = new FixedNumber(1); export function fixedNumber(schema = z.number(), digits = 1) { const coercedToNumber = z.preprocess((arg) => arg instanceof FixedNumber || typeof arg === 'number' ? +arg : arg, schema); return coercedToNumber.transform((value) => new FixedNumber(value, digits)); } /** * Schema for a number or bigint cast to a bigint */ export function bigNumber() { return z .union([z.number(), z.bigint()]) .transform((value) => (typeof value === 'bigint' ? value : BigInt(value))); } /** * Ensure that an array is initialized to an array with * at least one element, allowing for defaults to be * populated in each element. */ export function ensureObjects(obj, minItems = 1) { return z.preprocess((arg) => { arg = typeof arg === 'undefined' ? [] : arg; if (Array.isArray(arg) && arg.length < minItems) { const newItems = [...Array(Math.max(minItems - arg.length, 0))].map(() => ({})); arg.push(...newItems); } return arg; }, z.array(obj)); } /** * Shorthand for a `ZodObject` instance that doesn't strip * out any unknown keys, and that logs unexpected keys to * the console. */ export function unstable(shape) { return z.looseObject(shape); // return z.object(shape).catchall( // z.unknown().superRefine((_arg, ctx) => { // // The new format for name/resourcetype keys should be ignore, since those are handled in other ways. // ctx.issues.every((iss) => { // const isNewKey = `${String(iss.path?.at(-1))}`.match(/^[$%]/); // if (!isNewKey) { // console.log(`WARNING: Unexpected Key "${iss.path?.join('/')}"`); // } // }); // }), // ); } export function getYyResourceId(yyType, name) { return { name, path: `${yyType}/${name}/${name}.yy`, }; } export function yyResourceIdSchemaGenerator(yyType) { const pathFromName = (name) => `${yyType}/${name}/${name}.yy`; return z.preprocess((arg) => { if (arg === null || !['undefined', 'object'].includes(typeof arg)) { return arg; } const objectId = arg === undefined ? {} : arg; if (objectId.name && !objectId.path) { objectId.path = pathFromName(objectId.name); } return objectId; }, z .object({ /** Object name */ name: z.string(), /** Object resource path, e.g. "objects/{name}/{name}.yy" */ path: z.string(), }) .refine((arg) => arg.path === pathFromName(arg.name))); } export function yyIsNewFormat(yyData) { if (!yyData || typeof yyData !== 'object') return false; if (nameField in yyData && yyData[nameField] !== undefined) return true; if ('resourceType' in yyData && yyData.resourceType === '2.0') return true; return false; } export function isObjectWithField(obj, field) { return (obj !== null && typeof obj === 'object' && field in obj && // @ts-expect-error obj[field] !== undefined); } export function assert(claim, message) { if (!claim) throw new Error(message); } export function toPosixPath(path) { return path.replace(/\\/g, '/'); } export function parsePath(path) { path = toPosixPath(path); const parts = path.match(/^(?<parent>.*\/)?(?<filename>[^/]+?(?<ext>\.[^.]*)?)$/); assert(parts, `Could not identify path parts of "${path}"`); return { ...parts.groups, fullpath: path }; } /** * Join path parts, ensuring exactly one POSIX separator is between * each part. Maintains initial and final seps. */ export function joinPaths(...parts) { return parts .map((part, i) => { if (i === 0) return part.replace(/\/+$/, ''); // First part: trim end only if (i === parts.length - 1) return part.replace(/^\/+/, ''); // Last part: trim start only return part.replace(/^\/+|\/+$/g, ''); // Middle parts: trim both ends }) .join('/'); } //# sourceMappingURL=utility.js.map