import { XMLParser } from "fast-xml-parser"; import { createWriteStream, existsSync, mkdirSync, statSync } from "node:fs"; import { createRequire } from "node:module"; import { join } from "node:path"; const { upstream } = createRequire(import.meta.url)("../package.json"); const source = "https://forgejo.nishi.boats/api/v1/repos/pyrite-dev/milsko/tags"; // Out files const structsOut = createWriteStream("lib/structs.ts"); const enumsOut = createWriteStream("lib/enums.ts"); const constantsOut = createWriteStream("lib/constants.ts"); const widgetOut = createWriteStream("lib/widget.ts"); const widgetsIndexOut = createWriteStream("lib/widgets/index.ts"); function toCamelCase(input) { const split = input.split("_"); for (let i = 0; i < split.length; i++) { const firstChar = i <= 0 ? split[i].charAt(0).toLowerCase() : split[i].charAt(0).toUpperCase(); split[i] = firstChar + split[i].slice(1); } return split.join(""); } function typeToTypescript(type) { switch (type) { case "integer": return "number"; case "short": return "number"; case "string": return "string"; case "pixmap": return "Pixmap"; default: "any"; } } function scanStructs(root) { const list = root["structs"]["struct"]; for (const struct of list) { const structName = struct["@_name"]; structsOut.write("export interface " + structName + " {\n"); for (const [type, children] of Object.entries(struct)) { // attributes start with @_ if (!type.startsWith("@_") && Array.isArray(children)) { for (const child of children) { const attributeName = toCamelCase(child["@_name"]); let attributeType = typeToTypescript(type); if (type == "struct") { attributeType = child["@_defname"]; } structsOut.write( attributeName + ": " + attributeType + ";\n" ); } } } structsOut.write("}\n\n"); } structsOut.end(); } function scanEnumerations(root) { const list = root["enumerations"]["enumeration"]; for (const enumeration of list) { const enumName = enumeration["@_name"]; enumsOut.write("export enum " + enumName + " {\n"); for (const [type, children] of Object.entries(enumeration)) { // attributes start with @_ if (!type.startsWith("@_")) { for (const child of children) { const attributeName = child["@_name"]; const attributeValue = child["#text"]; enumsOut.write(attributeName); if (attributeValue) enumsOut.write(" = " + attributeValue); enumsOut.write(",\n"); } } } enumsOut.write("}\n\n"); } enumsOut.end(); } function scanConstants(root) { const list = root["constants"]; for (const [type, children] of Object.entries(list)) { for (const child of children) { const constName = child["@_name"]; const constValue = child["#text"]; constantsOut.write( "export const " + constName + ": " + typeToTypescript(type) + " = " + constValue + ";\n" ); } } constantsOut.end(); } function getPropertyInfo(root, query) { const properties = root["properties"]; for (const [type, children] of Object.entries(properties)) { const match = children.find((k) => k["@_name"] == query); if (match) { return { boolean: match["@_boolean"] == "yes", defName: match["@_defname"], type, }; } } } function getPropertySignature(inputName, inputType) { let prefix = null, setter = null, getter = null; switch (inputType) { case "string": prefix = "S"; setter = "setString"; getter = "getString"; break; case "integer": prefix = "I"; setter = "setInteger"; getter = "getInteger"; break; case "handler": prefix = "C"; setter = "addUserHandler"; break; default: prefix = "V"; setter = "setVoid"; getter = "getVoid"; break; } return { prefix, setter, getter }; } function createPropertyMethods(name, type, isBoolean, defName = "unknown") { const output = []; // fluent setter const { prefix, setter, getter } = getPropertySignature(name, type); const methodName = toCamelCase( "set" + name.charAt(0).toUpperCase() + name.slice(1)); const propertyId = prefix + name; let finalType = isBoolean ? "boolean" : typeToTypescript(type); const finalValue = isBoolean ? "+value" : "value"; if (type == "struct") finalType = defName; output.push( // signature "public " + methodName + "(value: " + finalType + "): this {\n" + // call internal method "return this." + setter + "(\"" + propertyId + "\"," + finalValue + ");\n}\n\n" ); // property setter and getter const getterName = toCamelCase(name); let cast = type == "pixmap" ? " as Pixmap" : ""; if (isBoolean) cast = " != 0" else if (type == "struct") cast = "as " + defName output.push( // getter "get " + getterName + "(): " + finalType + " {\n" + "return this." + getter + "(\"" + propertyId + "\")" + cast + ";\n}\n\n" + // setter "set " + getterName + "(value: " + finalType + ") {\n" + "this." + setter + "(\"" + propertyId + "\", " + finalValue + ");\n}\n\n" ); return output.join(""); } function scanProperties(root) { const properties = root["properties"]; widgetOut.write( "import { BaseWidget } from \"./milsko\"\n" + "import { Pixmap } from \"./types\"\n\n" + "export class Widget extends BaseWidget {\n" ); for (const [type, children] of Object.entries(properties)) { for (const prop of children) { if (prop["@_common"] != "yes") continue; const name = prop["@_name"]; const isBoolean = prop["@_boolean"] == "yes"; widgetOut.write(createPropertyMethods(name, type, isBoolean)); } } widgetOut.write("\n}"); widgetOut.close(); } function scanWidgets(root) { const widgets = root["widgets"]["widget"]; const unsupported = ["OpenGL", "Vulkan"]; // first loop through widgets to detect for imports and print lines for (const widget of widgets) { const widgetName = widget["@_name"]; if (unsupported.includes(widgetName)) continue; const typeImports = ["WidgetOptions"]; const structImports = []; const lines = []; const widgetFile = createWriteStream( join("lib/widgets/", widgetName.toLowerCase() + ".ts") ); // scan properties if (widget["properties"]) { const widgetProperties = widget["properties"]["property"]; for (const prop of widgetProperties) { const name = prop["@_name"]; let { boolean, defName, type } = getPropertyInfo(root, name); if (type == "struct") { if (!structImports.includes(defName)) structImports.push(defName); } if (type == "pixmap") { if (!typeImports.includes("Pixmap")) typeImports.push("Pixmap"); } // create methods lines.push(createPropertyMethods(name, type, boolean, defName)) } } lines.push("}\n"); // after looping write the contents in the correct order widgetFile.write( "import { NativeClasses } from \"../milsko\"\n" + "import { Widget } from \"../widget\"\n" ); if (structImports.length) { widgetFile.write( "import { " + structImports.join(", ") + " } from \"../structs\"\n" ); } widgetFile.write( "import { " + typeImports.join(", ") + " } from \"../types\"\n" ); // init class widgetFile.write("\nexport class " + widgetName + " extends Widget {\n"); // constructor widgetFile.write( "constructor(options: WidgetOptions) {\n" + "super(NativeClasses." + widgetName + ", options);" + "}\n\n" ); for (const line of lines) { widgetFile.write(line); } widgetsIndexOut.write( "export { " + widgetName + " } from \"./" + widgetName.toLowerCase() + "\"\n" ); widgetFile.close(); } widgetsIndexOut.close(); } function isArray(tagName) { if (tagName == "property" || tagName == "struct") return true; } async function bindgen() { const parser = new XMLParser({ attributeNamePrefix: "@_", ignoreAttributes: false, parseAttributeValue: true, isArray }); const response = await fetch(source); if (!response.ok) { throw new Error( "Failed to fetch the Milsko repository tags, try running npm run install or npm run update again (" + response.status + ")" ); } const tags = await response.json(); const match = tags.find((k) => k.name == upstream); if (!match) { console.warn( "Could not find a tag that matches the upstream version " + upstream + ", using as commit hash instead." ); } const commitHash = match ? match["id"] : upstream; const specURL = "https://forgejo.nishi.boats/pyrite-dev/milsko/raw/commit/" + commitHash + "/milsko.xml"; const spac = await fetch(specURL); if (!spac.ok) { throw new Error( "Failed to fetch the XML API specification with status code " + spac.status ); } const raw = await spac.text(); const root = parser.parse(raw)["milsko"]; scanStructs(root); scanEnumerations(root); scanConstants(root); scanProperties(root); scanWidgets(root); } bindgen();