Switched to JavaFX and added builtin Support

This commit is contained in:
Finn
2026-01-19 22:23:49 +01:00
parent 627e0c86e4
commit 8a20970e12
13 changed files with 1019 additions and 263 deletions

View File

@@ -1,232 +0,0 @@
package org.openautonomousconnection.luascript.dom.jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openautonomousconnection.luascript.hosts.DomHost;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* DomHost implementation backed by a jsoup Document.
*
* <p>Element identity is the HTML attribute {@code id}. This implementation auto-assigns ids to all
* elements that do not have one, making them addressable.</p>
*/
public final class JsoupDomHost implements DomHost {
private final Document document;
private final AtomicLong autoIdSeq = new AtomicLong(1);
public JsoupDomHost(Document document) {
this.document = Objects.requireNonNull(document, "document");
ensureAllElementsHaveId();
}
public Document document() {
return document;
}
public void ensureAllElementsHaveId() {
Elements all = document.getAllElements();
for (Element el : all) {
if (el == document) continue;
if (!hasUsableId(el)) {
el.attr("id", generateUniqueId());
}
}
}
@Override
public List<String> getAllElementIds() {
Elements all = document.getAllElements();
List<String> ids = new ArrayList<>(all.size());
for (Element el : all) {
if (el == document) continue;
String id = el.id();
if (id != null && !id.isBlank()) ids.add(id);
}
return ids;
}
@Override
public Map<String, String> getAttributes(String elementId) {
Element el = byId(elementId);
Map<String, String> out = new LinkedHashMap<>();
el.attributes().forEach(a -> out.put(a.getKey(), a.getValue()));
return out;
}
@Override
public String getTagName(String elementId) {
return byId(elementId).tagName().toLowerCase(Locale.ROOT);
}
@Override
public String getTextContent(String elementId) {
return byId(elementId).text();
}
@Override
public void setTextContent(String elementId, String text) {
byId(elementId).text(text == null ? "" : text);
}
@Override
public String getAttribute(String elementId, String name) {
Element el = byId(elementId);
String n = normalizeAttr(name);
if (!el.hasAttr(n)) return null;
return el.attr(n);
}
@Override
public void setAttribute(String elementId, String name, String value) {
Element el = byId(elementId);
el.attr(normalizeAttr(name), value == null ? "" : value);
}
@Override
public void removeAttribute(String elementId, String name) {
byId(elementId).removeAttr(normalizeAttr(name));
}
@Override
public String getParentId(String elementId) {
Element el = byId(elementId);
Element p = el.parent();
if (p == null || p == document) return null;
if (!hasUsableId(p)) p.attr("id", generateUniqueId());
return p.id();
}
@Override
public List<String> getChildrenIds(String elementId) {
Element el = byId(elementId);
List<String> out = new ArrayList<>();
for (Element child : el.children()) {
if (!hasUsableId(child)) child.attr("id", generateUniqueId());
out.add(child.id());
}
return out;
}
@Override
public String createElement(String tagName, String requestedId) {
String tag = normalizeTag(tagName);
String id = normalizeOrGenerateId(requestedId);
if (exists(id)) throw new IllegalArgumentException("Element id already exists: " + id);
Element el = document.createElement(tag);
el.attr("id", id);
return id;
}
@Override
public void removeElement(String elementId) {
byId(elementId).remove();
}
@Override
public void appendChild(String parentId, String childId) {
Element parent = byId(parentId);
Element child = byId(childId);
child.remove();
parent.appendChild(child);
}
@Override
public void insertBefore(String parentId, String childId, String beforeChildId) {
Element parent = byId(parentId);
Element child = byId(childId);
Element before = byId(beforeChildId);
if (before.parent() == null || !Objects.equals(before.parent(), parent)) {
throw new IllegalArgumentException("beforeChildId is not a direct child of parentId: " + beforeChildId);
}
child.remove();
before.before(child);
}
@Override
public boolean exists(String id) {
if (id == null || id.isBlank()) return false;
return document.getElementById(id) != null;
}
@Override
public List<String> queryByTag(String tagName) {
String tag = normalizeTag(tagName);
Elements els = document.getElementsByTag(tag);
List<String> out = new ArrayList<>(els.size());
for (Element el : els) {
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
out.add(el.id());
}
return out;
}
@Override
public List<String> queryByClass(String className) {
String cls = normalizeCssIdent(className);
Elements els = document.getElementsByClass(cls);
List<String> out = new ArrayList<>(els.size());
for (Element el : els) {
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
out.add(el.id());
}
return out;
}
private Element byId(String id) {
if (id == null || id.isBlank()) throw new IllegalArgumentException("elementId is blank");
Element el = document.getElementById(id);
if (el == null) throw new IllegalArgumentException("Unknown element id: " + id);
return el;
}
private boolean hasUsableId(Element el) {
String id = el.id();
return id != null && !id.isBlank();
}
private String generateUniqueId() {
while (true) {
String id = "__auto_" + autoIdSeq.getAndIncrement();
if (!exists(id)) return id;
}
}
private String normalizeTag(String tagName) {
if (tagName == null) throw new IllegalArgumentException("tagName is null");
String t = tagName.trim().toLowerCase(Locale.ROOT);
if (t.isEmpty()) throw new IllegalArgumentException("tagName is empty");
return t;
}
private String normalizeAttr(String name) {
if (name == null) throw new IllegalArgumentException("attribute name is null");
String n = name.trim();
if (n.isEmpty()) throw new IllegalArgumentException("attribute name is empty");
return n;
}
private String normalizeCssIdent(String s) {
if (s == null) throw new IllegalArgumentException("identifier is null");
String v = s.trim();
if (v.isEmpty()) throw new IllegalArgumentException("identifier is empty");
return v;
}
private String normalizeOrGenerateId(String requestedId) {
if (requestedId != null) {
String id = requestedId.trim();
if (!id.isEmpty()) return id;
}
return generateUniqueId();
}
}

View File

@@ -0,0 +1,357 @@
package org.openautonomousconnection.luascript.fx;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.w3c.dom.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* DomHost implementation backed by JavaFX WebView's W3C DOM (WebEngine#getDocument()).
*
* <p>No jsoup and no JavaScript. All operations are performed via W3C DOM APIs.</p>
*
* <p>Element identity is the {@code id} attribute. This host auto-assigns stable ids to elements that
* do not have one, ensuring addressability for Lua bindings and event routing.</p>
*/
public final class FxDomHost implements DomHost {
private final WebEngine engine;
private final AtomicLong autoIdSeq = new AtomicLong(1);
/**
* Creates a new FxDomHost.
*
* @param engine JavaFX WebEngine
*/
public FxDomHost(WebEngine engine) {
this.engine = Objects.requireNonNull(engine, "engine");
}
/**
* Ensures every element has a stable id.
*/
public void ensureAllElementsHaveId() {
FxThreadBridge.runAndWait(() -> {
Document doc = requireDocument();
NodeList all = doc.getElementsByTagName("*");
for (int i = 0; i < all.getLength(); i++) {
Node n = all.item(i);
if (n instanceof Element el) {
if (!hasUsableId(el)) {
el.setAttribute("id", generateUniqueId(doc));
}
}
}
});
}
@Override
public List<String> getAllElementIds() {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
NodeList all = doc.getElementsByTagName("*");
List<String> ids = new ArrayList<>(all.getLength());
for (int i = 0; i < all.getLength(); i++) {
Node n = all.item(i);
if (n instanceof Element el) {
String id = el.getAttribute("id");
if (id != null && !id.isBlank()) ids.add(id);
}
}
return ids;
});
}
@Override
public Map<String, String> getAttributes(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
NamedNodeMap nnm = el.getAttributes();
Map<String, String> out = new LinkedHashMap<>();
for (int i = 0; i < nnm.getLength(); i++) {
Node a = nnm.item(i);
if (a == null) continue;
out.put(a.getNodeName(), a.getNodeValue());
}
return out;
});
}
@Override
public String getTagName(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
String tag = el.getTagName();
return tag == null ? "" : tag.trim().toLowerCase(Locale.ROOT);
});
}
@Override
public String getTextContent(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
String t = el.getTextContent();
return t == null ? "" : t;
});
}
@Override
public void setTextContent(String elementId, String text) {
FxThreadBridge.runAndWait(() -> byId(elementId).setTextContent(text == null ? "" : text));
}
@Override
public String getAttribute(String elementId, String name) {
return FxThreadBridge.callAndWait(() -> {
Element el = byId(elementId);
String n = normalizeAttr(name);
if (!el.hasAttribute(n)) return null;
String v = el.getAttribute(n);
return v == null ? "" : v;
});
}
@Override
public void setAttribute(String elementId, String name, String value) {
FxThreadBridge.runAndWait(() -> byId(elementId).setAttribute(normalizeAttr(name), value == null ? "" : value));
}
@Override
public void removeAttribute(String elementId, String name) {
FxThreadBridge.runAndWait(() -> byId(elementId).removeAttribute(normalizeAttr(name)));
}
@Override
public String getParentId(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
Element el = byId(elementId);
Node p = el.getParentNode();
if (!(p instanceof Element pe)) return null;
if (!hasUsableId(pe)) pe.setAttribute("id", generateUniqueId(doc));
String id = pe.getAttribute("id");
return (id == null || id.isBlank()) ? null : id;
});
}
@Override
public List<String> getChildrenIds(String elementId) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
Element el = byId(elementId);
NodeList children = el.getChildNodes();
List<String> out = new ArrayList<>();
for (int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if (n instanceof Element ce) {
if (!hasUsableId(ce)) ce.setAttribute("id", generateUniqueId(doc));
out.add(ce.getAttribute("id"));
}
}
return out;
});
}
@Override
public String createElement(String tagName, String requestedId) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
String tag = normalizeTag(tagName);
String id = normalizeOrGenerateId(requestedId, doc);
if (doc.getElementById(id) != null) throw new IllegalArgumentException("Element id already exists: " + id);
Element el = doc.createElement(tag);
el.setAttribute("id", id);
// Make it immediately addressable by putting it into a hidden staging container.
ensureStagingContainer(doc).appendChild(el);
return id;
});
}
@Override
public void removeElement(String elementId) {
FxThreadBridge.runAndWait(() -> {
Element el = byId(elementId);
Node p = el.getParentNode();
if (p != null) p.removeChild(el);
});
}
@Override
public void appendChild(String parentId, String childId) {
FxThreadBridge.runAndWait(() -> {
Element parent = byId(parentId);
Element child = byId(childId);
Node old = child.getParentNode();
if (old != null) old.removeChild(child);
parent.appendChild(child);
});
}
@Override
public void insertBefore(String parentId, String childId, String beforeChildId) {
FxThreadBridge.runAndWait(() -> {
Element parent = byId(parentId);
Element child = byId(childId);
Element before = byId(beforeChildId);
Node beforeParent = before.getParentNode();
if (beforeParent == null || !beforeParent.isSameNode(parent)) {
throw new IllegalArgumentException("beforeChildId is not a direct child of parentId: " + beforeChildId);
}
Node old = child.getParentNode();
if (old != null) old.removeChild(child);
parent.insertBefore(child, before);
});
}
@Override
public boolean exists(String id) {
if (id == null || id.isBlank()) return false;
return FxThreadBridge.callAndWait(() -> {
Document doc = engine.getDocument();
return doc != null && doc.getElementById(id) != null;
});
}
@Override
public List<String> queryByTag(String tagName) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
String tag = normalizeTag(tagName);
NodeList els = doc.getElementsByTagName(tag);
List<String> out = new ArrayList<>(els.getLength());
for (int i = 0; i < els.getLength(); i++) {
Node n = els.item(i);
if (n instanceof Element el) {
if (!hasUsableId(el)) el.setAttribute("id", generateUniqueId(doc));
out.add(el.getAttribute("id"));
}
}
return out;
});
}
@Override
public List<String> queryByClass(String className) {
return FxThreadBridge.callAndWait(() -> {
Document doc = requireDocument();
String cls = normalizeCssIdent(className);
NodeList all = doc.getElementsByTagName("*");
List<String> out = new ArrayList<>();
for (int i = 0; i < all.getLength(); i++) {
Node n = all.item(i);
if (n instanceof Element el) {
String c = el.getAttribute("class");
if (c == null || c.isBlank()) continue;
if (hasClassToken(c, cls)) {
if (!hasUsableId(el)) el.setAttribute("id", generateUniqueId(doc));
out.add(el.getAttribute("id"));
}
}
}
return out;
});
}
/**
* Exposes the current document (FX thread access required by callers).
*
* @return document
*/
public Document requireDocument() {
Document doc = engine.getDocument();
if (doc == null) throw new IllegalStateException("WebEngine document is not available yet (page not loaded?)");
return doc;
}
/**
* Exposes element lookup for FX event host / ui host (FX thread access required by callers).
*
* @param id element id
* @return element
*/
public Element byId(String id) {
if (id == null || id.isBlank()) throw new IllegalArgumentException("elementId is blank");
Document doc = requireDocument();
Element el = doc.getElementById(id);
if (el == null) throw new IllegalArgumentException("Unknown element id: " + id);
return el;
}
private static boolean hasUsableId(Element el) {
String id = el.getAttribute("id");
return id != null && !id.isBlank();
}
private String generateUniqueId(Document doc) {
while (true) {
String id = "__auto_" + autoIdSeq.getAndIncrement();
if (doc.getElementById(id) == null) return id;
}
}
private static Element ensureStagingContainer(Document doc) {
Element body = (Element) doc.getElementsByTagName("body").item(0);
if (body == null) throw new IllegalStateException("No <body> element available");
Element staging = doc.getElementById("__oac_staging");
if (staging != null) return staging;
staging = doc.createElement("div");
staging.setAttribute("id", "__oac_staging");
staging.setAttribute("style", "display:none !important;");
body.appendChild(staging);
return staging;
}
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 normalizeTag(String tagName) {
if (tagName == null) throw new IllegalArgumentException("tagName is null");
String t = tagName.trim().toLowerCase(Locale.ROOT);
if (t.isEmpty()) throw new IllegalArgumentException("tagName is empty");
return t;
}
private static String normalizeAttr(String name) {
if (name == null) throw new IllegalArgumentException("attribute name is null");
String n = name.trim();
if (n.isEmpty()) throw new IllegalArgumentException("attribute name is empty");
return n;
}
private static String normalizeCssIdent(String s) {
if (s == null) throw new IllegalArgumentException("identifier is null");
String v = s.trim();
if (v.isEmpty()) throw new IllegalArgumentException("identifier is empty");
return v;
}
private String normalizeOrGenerateId(String requestedId, Document doc) {
if (requestedId != null) {
String id = requestedId.trim();
if (!id.isEmpty()) return id;
}
return generateUniqueId(doc);
}
}

View File

@@ -0,0 +1,118 @@
package org.openautonomousconnection.luascript.fx;
import org.openautonomousconnection.luascript.fx.FxDomHost;
import org.openautonomousconnection.luascript.events.UiEventRegistry;
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
import org.openautonomousconnection.luascript.hosts.EventHost;
import org.openautonomousconnection.luascript.runtime.LuaEventRouter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* EventHost implementation for JavaFX WebView using W3C DOM EventTarget listeners.
*
* <p>No JavaScript required. Events are forwarded to Lua via {@link LuaEventRouter}.</p>
*/
public final class FxEventHost implements EventHost {
private static final String GLOBAL_TARGET_ID = "__global__";
private final FxDomHost dom;
private final LuaEventRouter router;
private final ConcurrentHashMap<String, EventListener> elementListeners = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, EventListener> globalListeners = new ConcurrentHashMap<>();
/**
* Creates a new FxEventHost.
*
* @param dom fx dom host
* @param router lua router
*/
public FxEventHost(FxDomHost dom, LuaEventRouter router) {
this.dom = Objects.requireNonNull(dom, "dom");
this.router = Objects.requireNonNull(router, "router");
}
@Override
public void addListener(String elementId, String eventName) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
String key = elementId + "\n" + evt;
elementListeners.computeIfAbsent(key, k -> {
EventListener listener = ev -> {
Map<String, Object> payload = FxEventPayloadExtractor.extract(ev);
router.emit(elementId, evt, payload);
};
FxThreadBridge.runAndWait(() -> {
Element el = dom.byId(elementId);
((EventTarget) el).addEventListener(evt, listener, false);
});
return listener;
});
}
@Override
public void removeListener(String elementId, String eventName) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
String key = elementId + "\n" + evt;
EventListener listener = elementListeners.remove(key);
if (listener == null) return;
FxThreadBridge.runAndWait(() -> {
Element el = dom.byId(elementId);
((EventTarget) el).removeEventListener(evt, listener, false);
});
}
@Override
public void addGlobalListener(String eventName) {
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
globalListeners.computeIfAbsent(evt, k -> {
EventListener listener = ev -> {
Map<String, Object> payload = FxEventPayloadExtractor.extract(ev);
router.emit(GLOBAL_TARGET_ID, evt, payload);
};
FxThreadBridge.runAndWait(() -> {
Document doc = dom.requireDocument();
((EventTarget) doc).addEventListener(evt, listener, false);
});
return listener;
});
}
@Override
public void removeGlobalListener(String eventName) {
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
EventListener listener = globalListeners.remove(evt);
if (listener == null) return;
FxThreadBridge.runAndWait(() -> {
Document doc = dom.requireDocument();
((EventTarget) doc).removeEventListener(evt, listener, false);
});
}
}

View File

@@ -0,0 +1,56 @@
package org.openautonomousconnection.luascript.fx;
import com.sun.webkit.dom.KeyboardEventImpl;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.MouseEvent;
import org.w3c.dom.events.UIEvent;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Extracts common DOM event fields into a plain map for Lua.
*/
public final class FxEventPayloadExtractor {
private FxEventPayloadExtractor() {
}
/**
* Extracts an event payload.
*
* @param ev W3C DOM event
* @return map payload (never null)
*/
public static Map<String, Object> extract(Event ev) {
Map<String, Object> data = new LinkedHashMap<>();
if (ev == null) return data;
data.put("type", ev.getType());
if (ev instanceof UIEvent uiev) {
data.put("detail", uiev.getDetail());
}
if (ev instanceof MouseEvent mev) {
data.put("clientX", (int) mev.getClientX());
data.put("clientY", (int) mev.getClientY());
data.put("button", (int) mev.getButton());
data.put("altKey", mev.getAltKey());
data.put("ctrlKey", mev.getCtrlKey());
data.put("shiftKey", mev.getShiftKey());
data.put("metaKey", mev.getMetaKey());
}
if (ev instanceof KeyboardEventImpl kev) {
data.put("key", kev.getKeyIdentifier());
data.put("keyCode", (int) kev.getKeyCode());
data.put("altKey", kev.getAltKey());
data.put("ctrlKey", kev.getCtrlKey());
data.put("shiftKey", kev.getShiftKey());
data.put("metaKey", kev.getMetaKey());
}
return data;
}
}

View File

@@ -0,0 +1,66 @@
package org.openautonomousconnection.luascript.fx;
import javafx.application.Platform;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Executes work on the JavaFX Application Thread and waits synchronously for completion.
*/
public final class FxThreadBridge {
private FxThreadBridge() {
}
/**
* Runs the given task on the FX thread and blocks until completion.
*
* @param task task to execute
* @param <T> result type
* @return task result
*/
public static <T> T callAndWait(Callable<T> task) {
Objects.requireNonNull(task, "task");
if (Platform.isFxApplicationThread()) {
try {
return task.call();
} catch (Exception e) {
throw rethrow(e);
}
}
FutureTask<T> 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);
}
}

View File

@@ -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).
*
* <p>Operations are implemented via W3C DOM attributes/text, and best-effort behavior for
* value/style/class using standard attributes.</p>
*/
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;
}
}

View File

@@ -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.
*
* <p>Relative URLs are resolved against the current WebEngine location.</p>
*/
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();
}
}
}

View File

@@ -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).
*
* <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>
*
* <p>Note: You should call {@link FxDomHost#ensureAllElementsHaveId()} after the WebEngine finished loading.</p>
*
* @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 {
}
}
}

View File

@@ -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: &lt;script&gt; ... or &lt;script src="..."&gt;
* 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<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);
throw new IllegalStateException("Failed to load script src='" + path + "': " + ex.getMessage(), ex);
}
} else {
String inline = dom.getTextContent(elementId);

View File

@@ -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.