ft = new FutureTask<>(task);
+ Platform.runLater(ft);
+
+ try {
+ return ft.get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException("Interrupted while waiting for FX thread", e);
+ } catch (ExecutionException e) {
+ throw rethrow(e.getCause());
+ }
+ }
+
+ /**
+ * Runs the given runnable on the FX thread and blocks until completion.
+ *
+ * @param runnable runnable
+ */
+ public static void runAndWait(Runnable runnable) {
+ callAndWait(() -> {
+ runnable.run();
+ return null;
+ });
+ }
+
+ private static RuntimeException rethrow(Throwable t) {
+ if (t instanceof RuntimeException re) return re;
+ if (t instanceof Error err) throw err;
+ return new RuntimeException(t);
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxUiHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxUiHost.java
new file mode 100644
index 0000000..1accc2f
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxUiHost.java
@@ -0,0 +1,293 @@
+package org.openautonomousconnection.luascript.fx;
+
+import javafx.scene.web.WebEngine;
+import org.openautonomousconnection.luascript.fx.FxDomHost;
+import org.openautonomousconnection.luascript.fx.FxThreadBridge;
+import org.openautonomousconnection.luascript.hosts.UiHost;
+import org.w3c.dom.Element;
+
+import java.util.Objects;
+
+/**
+ * UiHost implementation for JavaFX WebView (no JavaScript).
+ *
+ * Operations are implemented via W3C DOM attributes/text, and best-effort behavior for
+ * value/style/class using standard attributes.
+ */
+public final class FxUiHost implements UiHost {
+
+ private final WebEngine engine;
+ private final FxDomHost dom;
+
+ /**
+ * Creates a new UI host.
+ *
+ * @param engine web engine
+ * @param dom dom host
+ */
+ public FxUiHost(WebEngine engine, FxDomHost dom) {
+ this.engine = Objects.requireNonNull(engine, "engine");
+ this.dom = Objects.requireNonNull(dom, "dom");
+ }
+
+ @Override
+ public void alert(String message) {
+ // No JS: use simple JavaFX dialog-less fallback (log-style). You can replace with real Dialogs later.
+ // Keeping it deterministic and non-blocking for now.
+ System.out.println("[ui.alert] " + (message == null ? "" : message));
+ }
+
+ @Override
+ public boolean confirm(String message) {
+ // No JS: deterministic default (false). Replace with JavaFX dialogs if you want UI interaction.
+ System.out.println("[ui.confirm] " + (message == null ? "" : message));
+ return false;
+ }
+
+ @Override
+ public String prompt(String message, String defaultValue) {
+ // No JS: deterministic default.
+ System.out.println("[ui.prompt] " + (message == null ? "" : message));
+ return defaultValue;
+ }
+
+ @Override
+ public void setText(String elementId, String text) {
+ dom.setTextContent(elementId, text);
+ }
+
+ @Override
+ public String getText(String elementId) {
+ return dom.getTextContent(elementId);
+ }
+
+ @Override
+ public void setHtml(String elementId, String html) {
+ // Without JS, safest is to set textContent (prevents HTML parsing).
+ // If you need real HTML injection, we must extend DomHost with fragment parsing (not in current API).
+ dom.setTextContent(elementId, html);
+ }
+
+ @Override
+ public String getHtml(String elementId) {
+ // Without JS, best-effort: return textContent.
+ return dom.getTextContent(elementId);
+ }
+
+ @Override
+ public void setValue(String elementId, String value) {
+ // Input/textarea value is commonly reflected as attribute "value".
+ dom.setAttribute(elementId, "value", value == null ? "" : value);
+ }
+
+ @Override
+ public String getValue(String elementId) {
+ String v = dom.getAttribute(elementId, "value");
+ return v == null ? "" : v;
+ }
+
+ @Override
+ public void setEnabled(String elementId, boolean enabled) {
+ if (enabled) dom.removeAttribute(elementId, "disabled");
+ else dom.setAttribute(elementId, "disabled", "disabled");
+ }
+
+ @Override
+ public void setVisible(String elementId, boolean visible) {
+ // Best-effort via style attribute
+ String style = dom.getAttribute(elementId, "style");
+ style = style == null ? "" : style;
+
+ style = removeCssProp(style, "display");
+ if (!visible) {
+ style = style.trim();
+ if (!style.isEmpty() && !style.endsWith(";")) style += ";";
+ style += "display:none;";
+ }
+
+ dom.setAttribute(elementId, "style", style);
+ }
+
+ @Override
+ public void addClass(String elementId, String className) {
+ String cls = Objects.requireNonNull(className, "className").trim();
+ if (cls.isEmpty()) return;
+
+ FxThreadBridge.runAndWait(() -> {
+ Element el = dom.byId(elementId);
+ String c = el.getAttribute("class");
+ c = (c == null) ? "" : c.trim();
+
+ if (c.isEmpty()) {
+ el.setAttribute("class", cls);
+ return;
+ }
+
+ if (!hasClassToken(c, cls)) {
+ el.setAttribute("class", c + " " + cls);
+ }
+ });
+ }
+
+ @Override
+ public void removeClass(String elementId, String className) {
+ String cls = Objects.requireNonNull(className, "className").trim();
+ if (cls.isEmpty()) return;
+
+ FxThreadBridge.runAndWait(() -> {
+ Element el = dom.byId(elementId);
+ String c = el.getAttribute("class");
+ c = (c == null) ? "" : c.trim();
+ if (c.isEmpty()) return;
+
+ String[] parts = c.split("\\s+");
+ StringBuilder sb = new StringBuilder();
+ for (String p : parts) {
+ if (p.equals(cls)) continue;
+ if (!sb.isEmpty()) sb.append(' ');
+ sb.append(p);
+ }
+ el.setAttribute("class", sb.toString());
+ });
+ }
+
+ @Override
+ public boolean toggleClass(String elementId, String className) {
+ if (hasClass(elementId, className)) {
+ removeClass(elementId, className);
+ return false;
+ }
+ addClass(elementId, className);
+ return true;
+ }
+
+ @Override
+ public boolean hasClass(String elementId, String className) {
+ String cls = Objects.requireNonNull(className, "className").trim();
+ if (cls.isEmpty()) return false;
+
+ return FxThreadBridge.callAndWait(() -> {
+ Element el = dom.byId(elementId);
+ String c = el.getAttribute("class");
+ c = (c == null) ? "" : c.trim();
+ return !c.isEmpty() && hasClassToken(c, cls);
+ });
+ }
+
+ @Override
+ public void setStyle(String elementId, String property, String value) {
+ String prop = Objects.requireNonNull(property, "property").trim().toLowerCase();
+ if (prop.isEmpty()) return;
+
+ String style = dom.getAttribute(elementId, "style");
+ style = style == null ? "" : style;
+
+ style = removeCssProp(style, prop);
+ String v = value == null ? "" : value.trim();
+
+ if (!v.isEmpty()) {
+ style = style.trim();
+ if (!style.isEmpty() && !style.endsWith(";")) style += ";";
+ style += prop + ":" + v + ";";
+ }
+ dom.setAttribute(elementId, "style", style);
+ }
+
+ @Override
+ public String getStyle(String elementId, String property) {
+ // Best-effort parsing from style attribute.
+ String prop = Objects.requireNonNull(property, "property").trim().toLowerCase();
+ if (prop.isEmpty()) return "";
+
+ String style = dom.getAttribute(elementId, "style");
+ style = style == null ? "" : style;
+
+ for (String part : style.split(";")) {
+ String p = part.trim();
+ if (p.isEmpty()) continue;
+ int idx = p.indexOf(':');
+ if (idx <= 0) continue;
+
+ String k = p.substring(0, idx).trim().toLowerCase();
+ if (k.equals(prop)) return p.substring(idx + 1).trim();
+ }
+ return "";
+ }
+
+ @Override
+ public void setAttribute(String elementId, String name, String value) {
+ dom.setAttribute(elementId, name, value);
+ }
+
+ @Override
+ public String getAttribute(String elementId, String name) {
+ return dom.getAttribute(elementId, name);
+ }
+
+ @Override
+ public void removeAttribute(String elementId, String name) {
+ dom.removeAttribute(elementId, name);
+ }
+
+ @Override
+ public void focus(String elementId) {
+ // Without JS, focus control is limited. Best-effort via attribute.
+ dom.setAttribute(elementId, "autofocus", "autofocus");
+ }
+
+ @Override
+ public void blur(String elementId) {
+ // No-op without JS.
+ }
+
+ @Override
+ public void scrollIntoView(String elementId) {
+ // No JS => no reliable scrollIntoView. No-op.
+ }
+
+ @Override
+ public int viewportWidth() {
+ // Without JS, no real viewport query. Return -1 (unknown).
+ return -1;
+ }
+
+ @Override
+ public int viewportHeight() {
+ return -1;
+ }
+
+ @Override
+ public long nowMillis() {
+ return System.currentTimeMillis();
+ }
+
+ private static boolean hasClassToken(String classAttr, String cls) {
+ String[] parts = classAttr.trim().split("\\s+");
+ for (String p : parts) {
+ if (p.equals(cls)) return true;
+ }
+ return false;
+ }
+
+ private static String removeCssProp(String style, String propLower) {
+ if (style == null || style.isBlank()) return "";
+ StringBuilder sb = new StringBuilder();
+
+ for (String part : style.split(";")) {
+ String p = part.trim();
+ if (p.isEmpty()) continue;
+ int idx = p.indexOf(':');
+ if (idx <= 0) continue;
+
+ String k = p.substring(0, idx).trim().toLowerCase();
+ if (k.equals(propLower)) continue;
+
+ if (!sb.isEmpty()) sb.append(';');
+ sb.append(p);
+ }
+
+ String out = sb.toString().trim();
+ if (!out.isEmpty() && !out.endsWith(";")) out += ";";
+ return out;
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxWebViewResourceHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxWebViewResourceHost.java
new file mode 100644
index 0000000..f935862
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxWebViewResourceHost.java
@@ -0,0 +1,56 @@
+package org.openautonomousconnection.luascript.fx;
+
+import javafx.scene.web.WebEngine;
+import org.openautonomousconnection.luascript.fx.FxThreadBridge;
+import org.openautonomousconnection.luascript.hosts.ResourceHost;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+/**
+ * ResourceHost that loads script src via URLConnection.
+ *
+ * Relative URLs are resolved against the current WebEngine location.
+ */
+public final class FxWebViewResourceHost implements ResourceHost {
+
+ private final WebEngine engine;
+
+ /**
+ * Creates a new resource host.
+ *
+ * @param engine web engine
+ */
+ public FxWebViewResourceHost(WebEngine engine) {
+ this.engine = Objects.requireNonNull(engine, "engine");
+ }
+
+ @Override
+ public String readText(String src) throws Exception {
+ Objects.requireNonNull(src, "src");
+ String trimmed = src.trim();
+ if (trimmed.isEmpty()) throw new IllegalArgumentException("src is empty");
+
+ String base = FxThreadBridge.callAndWait(engine::getLocation);
+ URL url = (base == null || base.isBlank())
+ ? new URL(trimmed)
+ : new URL(new URL(base), trimmed);
+
+ URLConnection con = url.openConnection();
+ con.setUseCaches(false);
+
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {
+ StringBuilder sb = new StringBuilder(4096);
+ char[] buf = new char[8192];
+ int r;
+ while ((r = br.read(buf)) >= 0) {
+ sb.append(buf, 0, r);
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java b/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java
index 364fc1c..da1b43d 100644
--- a/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java
+++ b/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java
@@ -1,5 +1,12 @@
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;
@@ -24,6 +31,38 @@ public final class HostServices {
this.resources = b.resources;
}
+ /**
+ * Builds a JavaFX WebView preset of HostServices (DOM + events + resources + UI).
+ *
+ * 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.
+ *
+ * Note: You should call {@link FxDomHost#ensureAllElementsHaveId()} after the WebEngine finished loading.
+ *
+ * @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
+ */
+ 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();
+ }
+
/**
* @return builder
*/
@@ -124,4 +163,3 @@ public final class HostServices {
}
}
}
-
diff --git a/src/main/java/org/openautonomousconnection/luascript/runtime/LuaScriptBootstrap.java b/src/main/java/org/openautonomousconnection/luascript/runtime/LuaScriptBootstrap.java
index 155e86f..07e7171 100644
--- a/src/main/java/org/openautonomousconnection/luascript/runtime/LuaScriptBootstrap.java
+++ b/src/main/java/org/openautonomousconnection/luascript/runtime/LuaScriptBootstrap.java
@@ -12,14 +12,21 @@ import java.util.Objects;
/**
* Bootstrap that:
- * 1) Executes all Lua scripts found in DOM
- * * 2) Scans DOM for on:* handlers and binds them automatically.
+ * 1) Executes ALL scripts found in DOM as Lua: <script> ... or <script src="...">
+ * 2) Scans DOM for on:* handlers and binds them automatically.
*/
public final class LuaScriptBootstrap {
private LuaScriptBootstrap() {
}
+ /**
+ * Bootstraps Lua from DOM.
+ *
+ * @param globals globals
+ * @param services host services
+ * @param dispatcher event dispatcher
+ */
public static void bootstrap(Globals globals, HostServices services, LuaEventDispatcher dispatcher) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(services, "services");
@@ -41,20 +48,16 @@ public final class LuaScriptBootstrap {
Map 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);
+ throw new IllegalStateException("Failed to load script src='" + path + "': " + ex.getMessage(), ex);
}
} else {
String inline = dom.getTextContent(elementId);
diff --git a/src/main/resources/license/jsoup/jsoup b/src/main/resources/license/jsoup/jsoup
deleted file mode 100644
index 7db9149..0000000
--- a/src/main/resources/license/jsoup/jsoup
+++ /dev/null
@@ -1,13 +0,0 @@
-
-jsoup License
-
-The jsoup code-base (including source and compiled packages) are distributed under the open source MIT license as described below.
-The MIT License
-
-Copyright © 2009 - 2025 Jonathan Hedley (https://jsoup.org/)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.