From 20df39ee4e17a4b32711fe65c84fd12d58f3269b Mon Sep 17 00:00:00 2001 From: PwLDev <64767383+PwLDev@users.noreply.github.com> Date: Mon, 4 May 2026 11:38:01 -0700 Subject: [PATCH] new bindgen --- scripts/bindgen.js | 228 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 219 insertions(+), 9 deletions(-) diff --git a/scripts/bindgen.js b/scripts/bindgen.js index b45f444..ca901b5 100644 --- a/scripts/bindgen.js +++ b/scripts/bindgen.js @@ -1,6 +1,7 @@ import { XMLParser } from "fast-xml-parser"; -import { createWriteStream } from "node:fs"; +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 = @@ -10,6 +11,7 @@ const source = const structsOut = createWriteStream("lib/structs.ts"); const enumsOut = createWriteStream("lib/enums.ts"); const constantsOut = createWriteStream("lib/constants.ts"); +const widgetOut = createWriteStream("lib/widget.ts"); function toCamelCase(input) { const split = input.split("_"); @@ -33,6 +35,8 @@ function typeToTypescript(type) { return "number"; case "string": return "string"; + case "pixmap": + return "Pixmap"; default: "any"; } @@ -42,7 +46,6 @@ function scanStructs(root) { const list = root["structs"]["struct"]; for (const struct of list) { - console.log(struct); const structName = struct["@_name"]; structsOut.write("export interface " + structName + " {\n"); @@ -51,8 +54,14 @@ function scanStructs(root) { 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 + ": " + typeToTypescript(type) + ";\n" + attributeName + ": " + attributeType + ";\n" ); } } @@ -115,11 +124,207 @@ function scanConstants(root) { 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/", toCamelCase(widgetName) + ".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); + } + + widgetFile.close(); + } +} + +function isArray(tagName) { + if (tagName == "property" || tagName == "struct") + return true; +} + async function bindgen() { const parser = new XMLParser({ attributeNamePrefix: "@_", ignoreAttributes: false, - parseAttributeValue: true + parseAttributeValue: true, + isArray }); const response = await fetch(source); @@ -135,14 +340,17 @@ async function bindgen() { const match = tags.find((k) => k.name == upstream); if (!match) { - throw new Error( - "Could not find a tag that matches the upstream version " + upstream + console.warn( + "Could not find a tag that matches the upstream version " + upstream + + ", using as commit hash instead." ); } - const commitHash = match["id"] - const specURL = "https://forgejo.nishi.boats/pyrite-dev/milsko/raw/commit/" + - commitHash + "/milsko.xml"; + 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) { @@ -158,6 +366,8 @@ async function bindgen() { scanStructs(root); scanEnumerations(root); scanConstants(root); + scanProperties(root); + scanWidgets(root); } bindgen();