milsko-node/scripts/bindgen.js

373 lines
10 KiB
JavaScript
Raw Normal View History

2026-04-28 13:11:03 -07:00
import { XMLParser } from "fast-xml-parser";
2026-05-04 11:38:01 -07:00
import { createWriteStream, existsSync, mkdirSync, statSync } from "node:fs";
2026-04-28 13:11:03 -07:00
import { createRequire } from "node:module";
2026-05-04 11:38:01 -07:00
import { join } from "node:path";
2026-04-28 13:11:03 -07:00
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");
2026-05-04 11:38:01 -07:00
const widgetOut = createWriteStream("lib/widget.ts");
2026-04-28 13:11:03 -07:00
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";
2026-05-04 11:38:01 -07:00
case "pixmap":
return "Pixmap";
2026-04-28 13:11:03 -07:00
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"]);
2026-05-04 11:38:01 -07:00
let attributeType = typeToTypescript(type);
if (type == "struct") {
attributeType = child["@_defname"];
}
2026-04-28 13:11:03 -07:00
structsOut.write(
2026-05-04 11:38:01 -07:00
attributeName + ": " + attributeType + ";\n"
2026-04-28 13:11:03 -07:00
);
}
}
}
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();
}
2026-05-04 11:38:01 -07:00
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;
}
2026-04-28 13:11:03 -07:00
async function bindgen() {
const parser = new XMLParser({
attributeNamePrefix: "@_",
ignoreAttributes: false,
2026-05-04 11:38:01 -07:00
parseAttributeValue: true,
isArray
2026-04-28 13:11:03 -07:00
});
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) {
2026-05-04 11:38:01 -07:00
console.warn(
"Could not find a tag that matches the upstream version " + upstream +
", using as commit hash instead."
2026-04-28 13:11:03 -07:00
);
}
2026-05-04 11:38:01 -07:00
const commitHash = match ? match["id"] : upstream;
const specURL =
"https://forgejo.nishi.boats/pyrite-dev/milsko/raw/commit/" +
commitHash +
"/milsko.xml";
2026-04-28 13:11:03 -07:00
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);
2026-05-04 11:38:01 -07:00
scanProperties(root);
scanWidgets(root);
2026-04-28 13:11:03 -07:00
}
bindgen();