import { Logger } from './vendor/logger.mjs';
import { Icon } from './icon.mjs';
import { Frame } from './frame.mjs';
import pako from './vendor/pako.esm.mjs';
/**
* @public
*/
class VYI {
/**
* The version of the module.
*/
static version = "VERSION_REPLACE_ME";
/** The logger module this module uses to log errors / logs.
* @private
* @type {Object}
*/
static logger = new Logger();
/**
* A map of icons that belong to this VYI
* @private
* @type {Map}
*/
icons = new Map();
/**
* The name of this vyi.
* @private
* @type {string}
*/
name = 'failed-to-find-vyi-name';
/**
* The version of the VYI.
* @private
* @type {number}
*/
formatVersion = 1;
/**
* Initializes this module with the information from the VYI passed.
* @param {Object} [pVyiData] - A JSON / Javascript object containing the vyi information.this.ogger
*/
constructor(pVyiData) {
VYI.logger.registerType('Vyi-module', '#ff6600');
this.parse(pVyiData);
}
/**
* Parses the provided VYI data (either a URL, a JSON object, or binary data) and processes it.
* If the data is a URL (string), it fetches/reads the data and parses it as JSON.
* If the data is binary, it attempts to inflate it using pako or decode it as plain text.
*
* @param {string|Object|Uint8Array|ArrayBuffer} pVyiData - The VYI data to parse. Or a path to a VYI file.
* @returns {Promise<void>} - A promise that resolves when the parsing is completed.
* @throws {Error} - Throws an error if fetching, inflating, or decoding fails.
*/
async parse(pVyiData) {
if (!pVyiData) return;
try {
let vyi;
if (typeof pVyiData === 'string') {
try {
vyi = JSON.parse(pVyiData);
} catch(pError) {
// Binary string
if (!pVyiData.includes('.vyr') && !pVyiData.includes('.vyi')) {
vyi = pVyiData;
if (!vyi) {
throw new Error('Non vyi data found from binary string');
}
} else {
const isNodeEnv = typeof window === 'undefined';
vyi = isNodeEnv
? await this.readFileAndGetVYI(pVyiData)
: await this.fetchAndParseJSON(pVyiData);
}
}
} else if (pVyiData instanceof VYI) {
vyi = pVyiData.export();
} else if (pVyiData instanceof Object && !Array.isArray(pVyiData) && !(pVyiData instanceof ArrayBuffer || pVyiData instanceof Uint8Array)) {
vyi = pVyiData;
} else if (pVyiData instanceof ArrayBuffer || pVyiData instanceof Uint8Array) {
vyi = await this.handleBinaryData(pVyiData);
} else {
throw new Error('Error processing: Invalid input type provided.');
}
if (!vyi || (!vyi.v || !vyi.i)) {
throw new Error('Non vyi data found.');
}
this.processVyiData(vyi);
} catch (pError) {
VYI.logger.prefix('Vyi-module').error(`${pError.message}`);
}
}
/**
* Read the file from a binary string.
* @param {string} pVyiData - The compressed binary string representing the vyi data.
* @returns {Promise<VYI>} - An vyi that was compressed in the file.
*/
readFile(pVyiData) {
return new Promise((pResolve, pReject) => {
const isNodeEnv = typeof window === 'undefined';
if (isNodeEnv) {
const data = pako.inflate(pVyiData, { to: 'string' });
const vyi = JSON.parse(data);
pResolve(vyi);
} else {
const file = new File(decompressed, 'temp');
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = (pEvent) => {
pResolve(pEvent.result);
}
fileReader.onerror = (pEvent) => {
pReject(new Error('Error reading file'));
}
}
});
}
/**
* Reads a file and returns the vyi from it.
* @private
* @param {string} - The URL to read the data from.
* @returns {Promise<Object>} - The vyi from the file.
*/
async readFileAndGetVYI(pURL) {
const fs = (await import('fs')).promises;
const data = await fs.readFile(pURL);
return this.handleBinaryData(data);
}
/**
* Fetches data from a URL and parses it as JSON.
* @private
* @param {string} pURL - The URL to fetch the data from.
* @returns {Promise<Object>} - A promise that resolves to the parsed JSON data.
*/
async fetchAndParseJSON(pURL) {
try {
const response = await fetch(pURL);
const jsonData = await response.json();
return jsonData;
} catch (pError) {
throw new Error(`Failed to fetch or parse JSON from URL: ${pError}`);
}
}
/**
* Handles binary data (ArrayBuffer or Uint8Array). Attempts to inflate or decode it.
* @private
* @param {ArrayBuffer|Uint8Array} pBinaryData - The binary data to process.
* @returns {Object|string|null} - The parsed JSON object or decoded string, or null if it fails.
*/
handleBinaryData(pBinaryData) {
const byteArray = pBinaryData instanceof ArrayBuffer ? new Uint8Array(pBinaryData) : pBinaryData;
try {
// Attempt to decompress the binary data
const decompressed = pako.inflate(byteArray, { to: 'string' });
if (!decompressed) return null;
return JSON.parse(decompressed);
} catch (pError) {
// Fallback attempt to decode directly as UTF-8 string
try {
const decodedText = new TextDecoder().decode(byteArray);
if (!decodedText) return null;
return JSON.parse(decodedText);
} catch (pDecodeError) {
console.error('Decoding failed:', pDecodeError);
return null; // Return null if both decompression and decoding fail
}
}
}
/**
* Processes the parsed VYI data and adds icons to the VYI module instance.
* @private
* @param {Object} pVyi - The parsed VYI data.
*/
processVyiData(pVyi) {
const icons = pVyi.i;
this.formatVersion = pVyi.v || 1;
if (Array.isArray(icons)) {
icons.forEach((pIconData) => {
this.addIcon(pIconData);
});
} else {
VYI.logger.prefix('Vyi-module').error('Invalid .vyi file! Cannot parse icons.');
}
}
/**
* Adds an icon to this VYI.
* @param {Icon|Array} pIconData - The icon data to use.
* @returns {Icon|undefined} - The Icon added or undefined.
*/
addIcon(pIconData) {
if (!pIconData) {
VYI.logger.prefix('Vyi-module').error('No icon data passed!');
return;
}
if (!(pIconData instanceof Icon) && !Array.isArray(pIconData)) {
VYI.logger.prefix('Vyi-module').error('Invalid icon data type passed!');
return;
}
const icon = pIconData instanceof Icon
? pIconData
: new Icon(pIconData);
this.icons.set(icon.id, icon);
icon.setVyi(this);
return icon;
}
/**
* Removes the icon passed.
* @param {Icon} pIcon - The icon to remove from this vyi.
*/
removeIcon(pIcon) {
if (!pIcon) return;
if (pIcon instanceof Icon) {
if (this.icons.delete(pIcon.id)) {;
pIcon.removeVyi();
}
}
}
/**
* Removes the icon via it's name. The LAST defined icon that has the passed name will be removed. As names are not unique.
* @param {string} pName - The name to use to find the icon.
*/
removeIconByName(pName) {
const icon = this.getIcon(pName);
this.removeIcon(icon);
}
/**
* Removes the icon via it's id.
* @param {string} pName - The id to use to find the icon.
*/
removeIconById(pId) {
const icon = this.getIconById(pId);
this.removeIcon(icon);
}
/**
* Returns all the icon names in this vyi.
* @returns {Array} An array of icon names in this vyi.
*/
getIconNames() {
const iconNames = this.getIcons().map((pIcon) => pIcon.name);
return iconNames;
}
/**
* Gets the icon that has the name pName. The LAST defined icon that has the passed name will be returned.
* @param {string} pName - The name of the icon to get.
* @returns {Icon|undefined} The icon that has the name pName or undefined.
*/
getIcon(pName) {
if (typeof pName === 'string') {
const icons = this.getIcons();
for (let i = icons.length - 1; i >= 0; i--) {
const icon = icons[i];
// If the icon has the same name, return that icon
if (icon.getName() === pName) {
return icon;
}
}
} else {
VYI.logger.prefix('Vyi-module').error('Invalid name type used!');
}
}
/**
* Gets the number of icons this vyi has.
* @returns {number} The amount of icons this vyi has.
*/
getIconCount() {
return this.icons.size;
}
/**
* Gets an icon by the id provided.
* @param {string} pId - The id of the icon.
* @returns {Icon} The icon that has the id that was passed.
*/
getIconById(pId) {
if (!pId) return;
// We recursively check the states of the parent to get the icon if its not found on the vyi.
const icon = this.icons.get(pId) || this.getIcons().find((pIcon) => pIcon.states.has(pId))?.getStateById(pId);
return icon;
}
/**
* Gets all the icons in this vyi.
* @returns {Array<Icon>}
*/
getIcons() {
return Array.from(this.icons.values());
}
/**
* Renames the vyi.
* @param {string} pName - The name to give this vyi.
*/
rename(pName) {
if (typeof pName === 'string') {
this.name = pName;
} else {
VYI.logger.prefix('Vyi-module').error('Invalid name type used!');
}
}
/**
* Gets the name of the vyi.
* @returns {string} The name of the vyi.
*/
getName() {
return this.name;
}
/**
* Exports this VYI into VYI format.
* @param {boolean} [pCompressed] - Whether the data will be compressed.
* @returns {Object|string} Returns the vyi data in either a binary string or an object.
*/
export(pCompressed) {
const vyi = {};
vyi.v = this.formatVersion;
vyi.i = this.getIcons().map((pIcon) => pIcon.export());
if (pCompressed) {
const binaryData = new TextEncoder().encode(JSON.stringify(vyi));
const compressed = pako.deflate(binaryData);
return compressed;
}
return vyi;
}
}
export { VYI, Icon, Frame };