first commit

This commit is contained in:
Finn
2026-01-16 21:47:04 +01:00
commit 02d39e2303
40 changed files with 2658 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
package org.openautonomousconnection.luascript.runtime;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.events.UiEventRegistry;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.openautonomousconnection.luascript.hosts.EventHost;
import java.util.Map;
import java.util.Objects;
/**
* Scans the DOM for attributes in variant B form: on:EVENT="path.to.handler"
* and binds them automatically via LuaEventDispatcher + EventHost subscriptions.
*/
public final class LuaDomBinder {
private static final String ATTR_PREFIX = "on:";
private final DomHost dom;
private final EventHost eventHost;
private final LuaEventDispatcher dispatcher;
public LuaDomBinder(DomHost dom, EventHost eventHost, LuaEventDispatcher dispatcher) {
this.dom = Objects.requireNonNull(dom, "dom");
this.eventHost = Objects.requireNonNull(eventHost, "eventHost");
this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher");
}
public void bindAll() {
for (String elementId : dom.getAllElementIds()) {
Map<String, String> attrs = dom.getAttributes(elementId);
if (attrs == null || attrs.isEmpty()) continue;
for (Map.Entry<String, String> e : attrs.entrySet()) {
String attr = e.getKey();
if (attr == null) continue;
String a = attr.trim().toLowerCase();
if (!a.startsWith(ATTR_PREFIX)) continue;
String eventName = UiEventRegistry.normalize(a.substring(ATTR_PREFIX.length()));
String handlerPath = (e.getValue() == null) ? "" : e.getValue().trim();
if (handlerPath.isEmpty()) {
throw new IllegalStateException("Empty handler for attribute '" + attr + "' on element '" + elementId + "'");
}
dispatcher.bind(elementId, eventName, handlerPath);
eventHost.addListener(elementId, eventName);
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.openautonomousconnection.luascript.runtime;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.events.UiEvent;
import org.openautonomousconnection.luascript.events.UiEventRegistry;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
/**
* Single entry point for the host to forward events into Lua.
*/
public final class LuaEventRouter {
private final LuaEventDispatcher dispatcher;
public LuaEventRouter(LuaEventDispatcher dispatcher) {
this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher");
}
public boolean emit(String elementId, String eventName, Map<String, Object> data) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
Map<String, Object> payload = (data == null) ? Collections.emptyMap() : data;
return dispatcher.dispatch(new UiEvent(elementId, evt, payload));
}
}

View File

@@ -0,0 +1,64 @@
package org.openautonomousconnection.luascript.runtime;
import org.luaj.vm2.Globals;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.luascript.security.LuaSecurityManager;
import org.openautonomousconnection.luascript.tables.console.ConsoleTable;
import org.openautonomousconnection.luascript.tables.DomTable;
import org.openautonomousconnection.luascript.tables.EventsTable;
import org.openautonomousconnection.luascript.tables.UiTable;
import java.util.Objects;
/**
* High-level entry point for wiring Lua tables + bootstrap + event routing.
*/
public final class LuaRuntime implements AutoCloseable {
private final Globals globals;
private final HostServices services;
private final LuaSecurityManager securityManager;
private final LuaEventDispatcher dispatcher;
private final LuaEventRouter eventRouter;
public LuaRuntime(Globals globals, HostServices services) {
this(globals, services, LuaExecutionPolicy.uiDefault());
}
public LuaRuntime(Globals globals, HostServices services, LuaExecutionPolicy policy) {
this.globals = Objects.requireNonNull(globals, "globals");
this.services = Objects.requireNonNull(services, "services");
Objects.requireNonNull(policy, "policy");
this.securityManager = new LuaSecurityManager();
this.dispatcher = new LuaEventDispatcher(globals, securityManager, policy);
this.eventRouter = new LuaEventRouter(dispatcher);
}
public Globals globals() {
return globals;
}
public LuaEventRouter eventRouter() {
return eventRouter;
}
public void installStdTables(boolean overwrite) {
new UiTable().inject(globals, services, overwrite);
new ConsoleTable().inject(globals, services, overwrite);
new EventsTable(dispatcher).inject(globals, services, overwrite);
new DomTable().inject(globals, services, overwrite);
}
public void bootstrapFromDom() {
LuaScriptBootstrap.bootstrap(globals, services, dispatcher);
}
@Override
public void close() {
securityManager.close();
}
}

View File

@@ -0,0 +1,69 @@
package org.openautonomousconnection.luascript.runtime;
import org.luaj.vm2.Globals;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.openautonomousconnection.luascript.hosts.EventHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.ResourceHost;
import java.util.Map;
import java.util.Objects;
/**
* Bootstrap that:
* 1) Executes all Lua scripts found in DOM (<script type="lua"> and <script type="lua" src="...">)
* 2) Scans DOM for on:* handlers and binds them automatically.
*/
public final class LuaScriptBootstrap {
private LuaScriptBootstrap() { }
public static void bootstrap(Globals globals, HostServices services, LuaEventDispatcher dispatcher) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(services, "services");
Objects.requireNonNull(dispatcher, "dispatcher");
DomHost dom = services.dom().orElseThrow(() -> new IllegalStateException("DomHost not provided"));
EventHost eventHost = services.events().orElseThrow(() -> new IllegalStateException("EventHost not provided"));
ResourceHost resources = services.resources().orElseThrow(() -> new IllegalStateException("ResourceHost not provided"));
executeAllScripts(globals, dom, resources);
new LuaDomBinder(dom, eventHost, dispatcher).bindAll();
}
private static void executeAllScripts(Globals globals, DomHost dom, ResourceHost resources) {
for (String elementId : dom.getAllElementIds()) {
String tag = safeLower(dom.getTagName(elementId));
if (!"script".equals(tag)) continue;
Map<String, String> attrs = dom.getAttributes(elementId);
if (attrs == null) continue;
String type = safeLower(attrs.getOrDefault("type", ""));
String src = attrs.get("src");
boolean isLuaByType = "lua".equals(type) || "text/lua".equals(type) || "application/lua".equals(type);
if (!isLuaByType) continue; // IMPORTANT: only run scripts explicitly marked as Lua
boolean hasSrc = src != null && !src.trim().isEmpty();
if (hasSrc) {
String path = src.trim();
try {
String code = resources.readText(path);
LuaScriptExecutor.execute(globals, code, path);
} catch (Exception ex) {
throw new IllegalStateException("Failed to load lua script src='" + path + "': " + ex.getMessage(), ex);
}
} else {
String inline = dom.getTextContent(elementId);
if (inline == null) inline = "";
LuaScriptExecutor.execute(globals, inline, "inline:" + elementId);
}
}
}
private static String safeLower(String s) {
return s == null ? "" : s.trim().toLowerCase();
}
}

View File

@@ -0,0 +1,28 @@
package org.openautonomousconnection.luascript.runtime;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaValue;
import java.util.Objects;
/**
* Loads and executes Lua source code in a given Globals environment.
*/
public final class LuaScriptExecutor {
private LuaScriptExecutor() { }
public static void execute(Globals globals, String source, String chunkName) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(source, "source");
Objects.requireNonNull(chunkName, "chunkName");
try {
LuaValue chunk = globals.load(source, chunkName);
chunk.call();
} catch (LuaError e) {
throw new LuaError("Lua error in '" + chunkName + "': " + e.getMessage());
}
}
}