new bindgen

This commit is contained in:
PwLDev 2026-05-04 11:38:01 -07:00
commit 20df39ee4e

View file

@ -1,6 +1,7 @@
import { XMLParser } from "fast-xml-parser"; 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 { createRequire } from "node:module";
import { join } from "node:path";
const { upstream } = createRequire(import.meta.url)("../package.json"); const { upstream } = createRequire(import.meta.url)("../package.json");
const source = const source =
@ -10,6 +11,7 @@ const source =
const structsOut = createWriteStream("lib/structs.ts"); const structsOut = createWriteStream("lib/structs.ts");
const enumsOut = createWriteStream("lib/enums.ts"); const enumsOut = createWriteStream("lib/enums.ts");
const constantsOut = createWriteStream("lib/constants.ts"); const constantsOut = createWriteStream("lib/constants.ts");
const widgetOut = createWriteStream("lib/widget.ts");
function toCamelCase(input) { function toCamelCase(input) {
const split = input.split("_"); const split = input.split("_");
@ -33,6 +35,8 @@ function typeToTypescript(type) {
return "number"; return "number";
case "string": case "string":
return "string"; return "string";
case "pixmap":
return "Pixmap";
default: default:
"any"; "any";
} }
@ -42,7 +46,6 @@ function scanStructs(root) {
const list = root["structs"]["struct"]; const list = root["structs"]["struct"];
for (const struct of list) { for (const struct of list) {
console.log(struct);
const structName = struct["@_name"]; const structName = struct["@_name"];
structsOut.write("export interface " + structName + " {\n"); structsOut.write("export interface " + structName + " {\n");
@ -51,8 +54,14 @@ function scanStructs(root) {
if (!type.startsWith("@_") && Array.isArray(children)) { if (!type.startsWith("@_") && Array.isArray(children)) {
for (const child of children) { for (const child of children) {
const attributeName = toCamelCase(child["@_name"]); const attributeName = toCamelCase(child["@_name"]);
let attributeType = typeToTypescript(type);
if (type == "struct") {
attributeType = child["@_defname"];
}
structsOut.write( structsOut.write(
attributeName + ": " + typeToTypescript(type) + ";\n" attributeName + ": " + attributeType + ";\n"
); );
} }
} }
@ -115,11 +124,207 @@ function scanConstants(root) {
constantsOut.end(); 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() { async function bindgen() {
const parser = new XMLParser({ const parser = new XMLParser({
attributeNamePrefix: "@_", attributeNamePrefix: "@_",
ignoreAttributes: false, ignoreAttributes: false,
parseAttributeValue: true parseAttributeValue: true,
isArray
}); });
const response = await fetch(source); const response = await fetch(source);
@ -135,14 +340,17 @@ async function bindgen() {
const match = tags.find((k) => k.name == upstream); const match = tags.find((k) => k.name == upstream);
if (!match) { if (!match) {
throw new Error( console.warn(
"Could not find a tag that matches the upstream version " + upstream "Could not find a tag that matches the upstream version " + upstream +
", using as commit hash instead."
); );
} }
const commitHash = match["id"] const commitHash = match ? match["id"] : upstream;
const specURL = "https://forgejo.nishi.boats/pyrite-dev/milsko/raw/commit/" + const specURL =
commitHash + "/milsko.xml"; "https://forgejo.nishi.boats/pyrite-dev/milsko/raw/commit/" +
commitHash +
"/milsko.xml";
const spac = await fetch(specURL); const spac = await fetch(specURL);
if (!spac.ok) { if (!spac.ok) {
@ -158,6 +366,8 @@ async function bindgen() {
scanStructs(root); scanStructs(root);
scanEnumerations(root); scanEnumerations(root);
scanConstants(root); scanConstants(root);
scanProperties(root);
scanWidgets(root);
} }
bindgen(); bindgen();