This commit is contained in:
UnlegitDqrk
2026-02-10 19:24:38 +01:00
parent 93fff3ffb3
commit 86800cb166
9 changed files with 480 additions and 193 deletions

View File

@@ -6,7 +6,7 @@
<groupId>org.openautonomousconnection</groupId>
<artifactId>LuaScript</artifactId>
<version>1.0.0-BETA.1.4</version>
<version>1.0.0-BETA.1.5</version>
<organization>
<name>Open Autonomous Connection</name>

View File

@@ -1,30 +1,41 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Host capability for console logging.
* Abstraction for script console output.
*/
public interface ConsoleHost {
/**
* Info log.
*
* @param message message
*/
void info(String message);
/**
* Standard log.
*
* @param message message
*/
void log(String message);
/**
* Warning log.
*
* @param message message
*/
void warn(String message);
/**
* Error-like stacktrace print.
*
* @param message message
*/
void error(String message);
/**
* Exception-like print.
*
* @param message message
*/
void exception(String message);

View File

@@ -4,79 +4,79 @@ import java.util.List;
import java.util.Map;
/**
* Host capability that exposes a DOM-like API.
* Abstraction over DOM access for scripting.
*
* <p>Element identity is the (stable) element id.</p>
* <p>All element references are by stable element {@code id}.</p>
*/
public interface DomHost {
/**
* Returns all element ids known to the renderer (must be stable).
* Returns a list of all element ids in the document.
*
* @return list of element ids
* @return list of ids (never null)
*/
List<String> getAllElementIds();
/**
* Returns all attributes for the given element id.
* Returns all attributes of the element.
*
* @param elementId element id
* @return attributes map (attributeName -> attributeValue)
* @return attributes map (never null)
*/
Map<String, String> getAttributes(String elementId);
/**
* Returns the tag name of the element (lowercase recommended), e.g. "script", "button".
* Returns the element tag name, lowercase if possible.
*
* @param elementId element id
* @return tag name
* @return tag name (never null)
*/
String getTagName(String elementId);
/**
* Returns the text content of an element
* Returns the element text content.
*
* @param elementId element id
* @return text content (never null)
* @return text (never null)
*/
String getTextContent(String elementId);
/**
* Sets the text content of an element.
* Sets the element text content.
*
* @param elementId element id
* @param text text
* @param text new text
*/
void setTextContent(String elementId, String text);
/**
* Gets a single attribute or null if missing.
* Returns an attribute value or null if missing.
*
* @param elementId element id
* @param name attribute name
* @return value or null
* @param name attribute name
* @return value or null if missing
*/
String getAttribute(String elementId, String name);
/**
* Sets an attribute (creates it if missing).
* Sets an attribute value (empty string allowed).
*
* @param elementId element id
* @param name attribute name
* @param value attribute value
* @param name attribute name
* @param value attribute value
*/
void setAttribute(String elementId, String name, String value);
/**
* Removes an attribute.
* Removes an attribute from the element.
*
* @param elementId element id
* @param name attribute name
* @param name attribute name
*/
void removeAttribute(String elementId, String name);
/**
* Returns parent id or null.
* Returns the parent element id or null if none.
*
* @param elementId element id
* @return parent id or null
@@ -84,48 +84,48 @@ public interface DomHost {
String getParentId(String elementId);
/**
* Returns direct children ids.
* Returns children element ids.
*
* @param elementId element id
* @return children ids
* @return list of children ids (never null)
*/
List<String> getChildrenIds(String elementId);
/**
* Creates a new element (detached) and returns its id.
* Creates an element and makes it addressable immediately.
*
* @param tagName tag name
* @param requestedId optional requested id, may be null/blank for auto id
* @param tagName tag name
* @param requestedId requested id or null
* @return created element id
*/
String createElement(String tagName, String requestedId);
/**
* Removes an element from the DOM.
* Removes an element from the document.
*
* @param elementId element id
*/
void removeElement(String elementId);
/**
* Moves/appends child under parent.
* Appends a child element to a parent.
*
* @param parentId parent id
* @param childId child id
* @param childId child id
*/
void appendChild(String parentId, String childId);
/**
* Inserts child before an existing direct child.
* Inserts {@code childId} before {@code beforeChildId} within {@code parentId}.
*
* @param parentId parent id
* @param childId child id
* @param parentId parent id
* @param childId child id
* @param beforeChildId existing child id
*/
void insertBefore(String parentId, String childId, String beforeChildId);
/**
* Checks if an element id exists.
* Checks if an element with the given id exists.
*
* @param id element id
* @return true if exists
@@ -133,18 +133,18 @@ public interface DomHost {
boolean exists(String id);
/**
* Returns element ids by tag.
* Queries elements by tag name and returns their ids.
*
* @param tagName tag
* @return ids
* @param tagName tag name
* @return list of ids (never null)
*/
List<String> queryByTag(String tagName);
/**
* Returns element ids by class.
* Queries elements by class token and returns their ids.
*
* @param className class
* @return ids
* @param className class name token
* @return list of ids (never null)
*/
List<String> queryByClass(String className);
}

View File

@@ -1,37 +1,41 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.Map;
/**
* Event subscription abstraction (implemented by the client UI layer).
* Abstraction over DOM event subscription for scripting.
*
* <p>Implementations forward DOM events into a {@code LuaEventRouter}.</p>
*/
public interface EventHost {
/**
* Subscribes to an element event.
* Adds a listener for an element's event.
*
* @param elementId element id
* @param eventName event name (e.g. click)
* @param eventName normalized event name
*/
void addListener(String elementId, String eventName);
/**
* Unsubscribes from an element event.
* Removes a listener for an element's event.
*
* @param elementId element id
* @param eventName event name
* @param eventName normalized event name
*/
void removeListener(String elementId, String eventName);
/**
* Subscribes to a global event (app/window scope).
* Adds a global (document-level) listener.
*
* @param eventName event name
* @param eventName normalized event name
*/
void addGlobalListener(String eventName);
/**
* Unsubscribes from a global event.
* Removes a global (document-level) listener.
*
* @param eventName event name
* @param eventName normalized event name
*/
void removeGlobalListener(String eventName);
}

View File

@@ -1,165 +1,145 @@
package org.openautonomousconnection.luascript.hosts;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.fx.FxDomHost;
import org.openautonomousconnection.luascript.fx.FxEventHost;
import org.openautonomousconnection.luascript.fx.FxUiHost;
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
import org.openautonomousconnection.luascript.runtime.LuaRuntime;
import java.util.Objects;
import java.util.Optional;
/**
* Service container holding optional host capabilities.
* Container for host-side services exposed to Lua.
*
* <p>This avoids one huge "bridge" interface.</p>
* <p>All services are optional to allow different embedding scenarios.</p>
*/
public final class HostServices {
private final ConsoleHost console;
private final DomHost dom;
private final EventHost events;
private final ResourceHost resources;
private final UiHost ui;
private HostServices(Builder b) {
this.console = b.console;
this.ui = b.ui;
this.dom = b.dom;
this.events = b.events;
this.resources = b.resources;
}
public interface HostServices {
/**
* Builds a JavaFX WebView preset of HostServices (DOM + events + resources + UI).
* Returns an optional UI host.
*
* <p>Important: This method creates the {@link EventHost} using the provided {@link LuaRuntime}'s
* {@link org.openautonomousconnection.luascript.runtime.LuaEventRouter}. Therefore, the runtime must already
* be constructed before calling this method.</p>
* @return ui host
*/
Optional<UiHost> ui();
/**
* Returns an optional DOM host.
*
* <p>Note: You should call {@link FxDomHost#ensureAllElementsHaveId()} after the WebEngine finished loading.</p>
* @return dom host
*/
Optional<DomHost> dom();
/**
* Returns an optional event host.
*
* @param engine JavaFX WebEngine
* @param runtime Lua runtime used for event routing
* @param console optional console host (may be null)
* @return HostServices preset for JavaFX WebView
* @return event host
*/
public static HostServices fxPreset(WebEngine engine, LuaRuntime runtime, ConsoleHost console) {
Objects.requireNonNull(engine, "engine");
Objects.requireNonNull(runtime, "runtime");
FxDomHost dom = new FxDomHost(engine);
FxWebViewResourceHost resources = new FxWebViewResourceHost(engine);
FxUiHost ui = new FxUiHost(engine, dom);
FxEventHost events = new FxEventHost(dom, runtime.eventRouter());
Builder b = builder().dom(dom).events(events).resources(resources).ui(ui);
if (console != null) {
b.console(console);
}
return b.build();
}
Optional<EventHost> events();
/**
* @return builder
* Returns an optional resource host.
*
* @return resource host
*/
public static Builder builder() {
return new Builder();
}
Optional<ResourceHost> resources();
/**
* @return optional DomHost capability
* Returns an optional console host.
*
* @return console host
*/
public Optional<DomHost> dom() {
return Optional.ofNullable(dom);
}
Optional<ConsoleHost> console();
/**
* @return optional EventHost capability
* Simple immutable implementation.
*/
public Optional<EventHost> events() {
return Optional.ofNullable(events);
}
/**
* @return optional ResourceHost capability
*/
public Optional<ResourceHost> resources() {
return Optional.ofNullable(resources);
}
/**
* @return optional console host
*/
public Optional<ConsoleHost> console() {
return Optional.ofNullable(console);
}
/**
* @return optional ui host
*/
public Optional<UiHost> ui() {
return Optional.ofNullable(ui);
}
/**
* Builder for HostServices.
*/
public static final class Builder {
private ConsoleHost console;
private DomHost dom;
private EventHost events;
private ResourceHost resources;
private UiHost ui;
public Builder console(ConsoleHost console) {
this.console = Objects.requireNonNull(console, "console");
return this;
}
public Builder ui(UiHost ui) {
this.ui = Objects.requireNonNull(ui, "ui");
return this;
}
final class Default implements HostServices {
private final UiHost ui;
private final DomHost dom;
private final EventHost events;
private final ResourceHost resources;
private final ConsoleHost console;
/**
* Provides dom capability.
* Creates a HostServices container.
*
* @param ui ui host
* @param dom dom host
* @return this
*/
public Builder dom(DomHost dom) {
this.dom = Objects.requireNonNull(dom, "dom");
return this;
}
/**
* Provides event subscription capability.
*
* @param events event host
* @return this
* @param resources resource host
* @param console console host
*/
public Builder events(EventHost events) {
this.events = Objects.requireNonNull(events, "events");
return this;
public Default(UiHost ui, DomHost dom, EventHost events, ResourceHost resources, ConsoleHost console) {
this.ui = ui;
this.dom = dom;
this.events = events;
this.resources = resources;
this.console = console;
}
@Override
public Optional<UiHost> ui() {
return Optional.ofNullable(ui);
}
@Override
public Optional<DomHost> dom() {
return Optional.ofNullable(dom);
}
@Override
public Optional<EventHost> events() {
return Optional.ofNullable(events);
}
@Override
public Optional<ResourceHost> resources() {
return Optional.ofNullable(resources);
}
@Override
public Optional<ConsoleHost> console() {
return Optional.ofNullable(console);
}
}
/**
* Stdout-based console host.
*/
final class StdoutConsole implements ConsoleHost {
private final String prefix;
/**
* Provides resource loading capability.
* Creates a new stdout console with a prefix.
*
* @param resources resource host
* @return this
* @param prefix prefix (may be empty)
*/
public Builder resources(ResourceHost resources) {
this.resources = Objects.requireNonNull(resources, "resources");
return this;
public StdoutConsole(String prefix) {
this.prefix = Objects.requireNonNull(prefix, "prefix");
}
public HostServices build() {
return new HostServices(this);
@Override
public void info(String message) {
System.out.println(prefix + "[info] " + safe(message));
}
@Override
public void log(String message) {
System.out.println(prefix + "[log] " + safe(message));
}
@Override
public void warn(String message) {
System.out.println(prefix + "[warn] " + safe(message));
}
@Override
public void error(String message) {
System.err.println(prefix + "[error] " + safe(message));
}
@Override
public void exception(String message) {
System.err.println(prefix + "[exception] " + safe(message));
}
private static String safe(String s) {
return s == null ? "" : s;
}
}
}

View File

@@ -1,16 +1,16 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Resource loading abstraction for LuaScript (e.g. script src).
* Abstraction for loading external resources (e.g. {@code <script src="...">}).
*/
public interface ResourceHost {
/**
* Reads text from a script source (file/url/virtual path).
* Reads a UTF-8 text resource.
*
* @param src source identifier
* @param src resource location (absolute or relative)
* @return text content
* @throws Exception on load failures
* @throws Exception on IO or resolution error
*/
String readText(String src) throws Exception;
}

View File

@@ -1,59 +1,215 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Host capability for UI operations.
* Abstraction for UI operations exposed to scripts.
*/
public interface UiHost {
/**
* Displays an alert-like message.
*
* @param message message
*/
void alert(String message);
/**
* Displays a confirm-like prompt.
*
* @param message message
* @return true if accepted
*/
boolean confirm(String message);
/**
* Displays a prompt-like query.
*
* @param message message
* @param defaultValue default value
* @return user response or default
*/
String prompt(String message, String defaultValue);
/**
* Sets element text.
*
* @param elementId element id
* @param text text
*/
void setText(String elementId, String text);
/**
* Gets element text.
*
* @param elementId element id
* @return text (never null)
*/
String getText(String elementId);
/**
* Sets element HTML (best-effort for non-JS hosts).
*
* @param elementId element id
* @param html html
*/
void setHtml(String elementId, String html);
/**
* Gets element HTML (best-effort for non-JS hosts).
*
* @param elementId element id
* @return html (never null)
*/
String getHtml(String elementId);
/**
* Sets a form-like value.
*
* @param elementId element id
* @param value value
*/
void setValue(String elementId, String value);
/**
* Gets a form-like value.
*
* @param elementId element id
* @return value (never null)
*/
String getValue(String elementId);
/**
* Enables/disables an element.
*
* @param elementId element id
* @param enabled enabled
*/
void setEnabled(String elementId, boolean enabled);
/**
* Shows/hides an element.
*
* @param elementId element id
* @param visible visible
*/
void setVisible(String elementId, boolean visible);
/**
* Adds a class token.
*
* @param elementId element id
* @param className class token
*/
void addClass(String elementId, String className);
/**
* Removes a class token.
*
* @param elementId element id
* @param className class token
*/
void removeClass(String elementId, String className);
/**
* Toggles a class token.
*
* @param elementId element id
* @param className class token
* @return true if class is present after toggle
*/
boolean toggleClass(String elementId, String className);
/**
* Checks whether a class token is present.
*
* @param elementId element id
* @param className class token
* @return true if present
*/
boolean hasClass(String elementId, String className);
/**
* Sets a CSS property via style attribute (best-effort).
*
* @param elementId element id
* @param property css property
* @param value css value
*/
void setStyle(String elementId, String property, String value);
/**
* Gets a CSS property via style attribute (best-effort).
*
* @param elementId element id
* @param property css property
* @return css value or empty string
*/
String getStyle(String elementId, String property);
/**
* Sets an attribute.
*
* @param elementId element id
* @param name attribute name
* @param value value
*/
void setAttribute(String elementId, String name, String value);
/**
* Gets an attribute value or null if missing.
*
* @param elementId element id
* @param name attribute name
* @return value or null
*/
String getAttribute(String elementId, String name);
/**
* Removes an attribute.
*
* @param elementId element id
* @param name attribute name
*/
void removeAttribute(String elementId, String name);
/**
* Focuses an element (best-effort).
*
* @param elementId element id
*/
void focus(String elementId);
/**
* Blurs an element (best-effort).
*
* @param elementId element id
*/
void blur(String elementId);
/**
* Scrolls an element into view (best-effort).
*
* @param elementId element id
*/
void scrollIntoView(String elementId);
/**
* Returns viewport width or -1 if unknown.
*
* @return width
*/
int viewportWidth();
/**
* Returns viewport height or -1 if unknown.
*
* @return height
*/
int viewportHeight();
/**
* Returns current host time in milliseconds.
*
* @return millis
*/
long nowMillis();
}

View File

@@ -0,0 +1,134 @@
package org.openautonomousconnection.luascript.runtime;
import javafx.concurrent.Worker;
import javafx.scene.web.WebEngine;
import org.luaj.vm2.Globals;
import org.openautonomousconnection.luascript.fx.FxDomHost;
import org.openautonomousconnection.luascript.fx.FxEventHost;
import org.openautonomousconnection.luascript.fx.FxUiHost;
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.luascript.utils.LuaGlobalsFactory;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* JavaFX WebView integration entry point for LuaScript (no JavaScript).
*
* <p>Hard rule: every HTML script tag is treated as Lua.</p>
*/
public final class FxLuaScriptEngine implements AutoCloseable {
private final WebEngine engine;
private final LuaExecutionPolicy policy;
private final AtomicBoolean bootstrapped = new AtomicBoolean(false);
private LuaRuntime runtime;
/**
* Creates an integration engine with default UI execution policy.
*
* @param engine web engine
*/
public FxLuaScriptEngine(WebEngine engine) {
this(engine, LuaExecutionPolicy.uiDefault());
}
/**
* Creates an integration engine with a custom execution policy.
*
* @param engine web engine
* @param policy execution policy
*/
public FxLuaScriptEngine(WebEngine engine, LuaExecutionPolicy policy) {
this.engine = Objects.requireNonNull(engine, "engine");
this.policy = Objects.requireNonNull(policy, "policy");
}
/**
* Installs a load hook that bootstraps Lua when a page finished loading.
*/
public void install() {
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
if (newState == Worker.State.SUCCEEDED) {
bootstrapped.set(false);
bootstrap();
} else if (newState == Worker.State.CANCELLED || newState == Worker.State.FAILED) {
bootstrapped.set(false);
closeRuntimeQuietly();
}
});
}
/**
* Bootstraps Lua for the currently loaded document.
*/
public void bootstrap() {
if (!bootstrapped.compareAndSet(false, true)) return;
closeRuntimeQuietly();
// DOM host must exist before event/UI tables, and must ensure stable ids.
FxDomHost dom = new FxDomHost(engine);
dom.ensureAllElementsHaveId();
// Create per-page globals; harden sandbox in production.
Globals globals = LuaGlobalsFactory.create(
new LuaGlobalsFactory.Options()
.enableDebug(false)
.sandbox(true)
);
// Create runtime first (router lives inside it).
HostServices.StdoutConsole console = new HostServices.StdoutConsole("[lua] ");
FxUiHost uiHost = new FxUiHost(engine, dom);
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
// runtime depends on services; events depends on runtime router.
// We'll create eventHost after runtime, then build HostServices with it.
LuaRuntime rt = new LuaRuntime(globals, new HostServices.Default(uiHost, dom, null, resourceHost, console), policy);
FxEventHost eventHost = new FxEventHost(dom, rt.eventRouter());
// Rebuild services including eventHost and reinstall tables.
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console);
// Replace runtime with correct services (clean and deterministic).
rt.close();
rt = new LuaRuntime(globals, services, policy);
rt.installStdTables(true);
rt.bootstrapFromDom();
this.runtime = rt;
}
/**
* Returns active runtime or null if not bootstrapped.
*
* @return runtime or null
*/
public LuaRuntime runtimeOrNull() {
return runtime;
}
@Override
public void close() {
closeRuntimeQuietly();
}
private void closeRuntimeQuietly() {
LuaRuntime rt = this.runtime;
this.runtime = null;
if (rt != null) {
try {
rt.close();
} catch (Exception ignored) {
// Best-effort shutdown.
}
}
}
}

View File

@@ -12,8 +12,10 @@ import java.util.Objects;
/**
* Bootstrap that:
* 1) Executes ALL scripts found in DOM as Lua: &lt;script&gt; ... or &lt;script src="..."&gt;
* 1) Executes ALL scripts found in DOM as Lua: <script> ... or <script src="...">
* 2) Scans DOM for on:* handlers and binds them automatically.
*
* <p>Hard client rule: JavaScript is not executed. Every HTML script tag is treated as Lua.</p>
*/
public final class LuaScriptBootstrap {
@@ -36,11 +38,11 @@ public final class LuaScriptBootstrap {
EventHost eventHost = services.events().orElseThrow(() -> new IllegalStateException("EventHost not provided"));
ResourceHost resources = services.resources().orElseThrow(() -> new IllegalStateException("ResourceHost not provided"));
executeAllScripts(globals, dom, resources);
executeAllScriptsAsLua(globals, dom, resources);
new LuaDomBinder(dom, eventHost, dispatcher).bindAll();
}
private static void executeAllScripts(Globals globals, DomHost dom, ResourceHost resources) {
private static void executeAllScriptsAsLua(Globals globals, DomHost dom, ResourceHost resources) {
for (String elementId : dom.getAllElementIds()) {
String tag = safeLower(dom.getTagName(elementId));
if (!"script".equals(tag)) continue;