diff --git a/.idea/misc.xml b/.idea/misc.xml index 001e756..4e5bb42 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,6 +8,55 @@ + + + + diff --git a/pom.xml b/pom.xml index 3326057..dea4805 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.openautonomousconnection LuaScript - 0.0.0-STABLE.1.3 + 0.0.0-STABLE.1.4 Open Autonomous Connection diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxClipboardHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxClipboardHost.java new file mode 100644 index 0000000..700885c --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxClipboardHost.java @@ -0,0 +1,35 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.application.Platform; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import org.openautonomousconnection.luascript.hosts.ClipboardHost; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +/** + * JavaFX clipboard host (text only). + */ +public final class FxClipboardHost implements ClipboardHost { + + @Override + public void setText(String text) { + String s = text == null ? "" : text; + Platform.runLater(() -> { + ClipboardContent c = new ClipboardContent(); + c.putString(s); + Clipboard.getSystemClipboard().setContent(c); + }); + } + + @Override + public String getText() { + AtomicReference out = new AtomicReference<>(""); + FxThreadBridge.runAndWait(() -> { + String s = Clipboard.getSystemClipboard().getString(); + out.set(s == null ? "" : s); + }); + return out.get(); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxCssHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxCssHost.java new file mode 100644 index 0000000..9596510 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxCssHost.java @@ -0,0 +1,122 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.scene.web.WebEngine; +import org.openautonomousconnection.luascript.hosts.CssHost; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * CSS host via internal JS bridge. + */ +public final class FxCssHost implements CssHost { + + private final WebEngine engine; + private final FxDomHost dom; + + public FxCssHost(WebEngine engine, FxDomHost dom) { + this.engine = Objects.requireNonNull(engine, "engine"); + this.dom = Objects.requireNonNull(dom, "dom"); + } + + @Override + public String getComputedStyle(String elementId, String property) { + Objects.requireNonNull(elementId, "elementId"); + Objects.requireNonNull(property, "property"); + + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var prop=" + FxWebBridge.toJsLiteral(property) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " var cs=getComputedStyle(el);" + + " var v=cs.getPropertyValue(prop) || cs[prop] || '';" + + " return String(v);" + + "})();"; + + Object ret = engine.executeScript(script); + return ret == null ? "" : String.valueOf(ret); + })); + } + + @Override + public Map getComputedStyles(String elementId, String[] properties) { + Objects.requireNonNull(elementId, "elementId"); + Objects.requireNonNull(properties, "properties"); + + Map out = new LinkedHashMap<>(); + for (String p : properties) { + if (p == null || p.isBlank()) continue; + out.put(p, getComputedStyle(elementId, p)); + } + return out; + } + + @Override + public void setInlineStyle(String elementId, String property, String value) { + Objects.requireNonNull(elementId, "elementId"); + Objects.requireNonNull(property, "property"); + + FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> { + dom.requireDocument(); + + String v = value == null ? "" : value; + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var prop=" + FxWebBridge.toJsLiteral(property) + ";" + + " var val=" + FxWebBridge.toJsLiteral(v) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " if(!val){ el.style.removeProperty(prop); return null; }" + + " el.style.setProperty(prop, val);" + + " return null;" + + "})();"; + + engine.executeScript(script); + })); + } + + @Override + public String getInlineStyle(String elementId, String property) { + Objects.requireNonNull(elementId, "elementId"); + Objects.requireNonNull(property, "property"); + + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var prop=" + FxWebBridge.toJsLiteral(property) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " var v=el.style.getPropertyValue(prop) || '';" + + " return String(v);" + + "})();"; + + Object ret = engine.executeScript(script); + return ret == null ? "" : String.valueOf(ret); + })); + } + + @Override + public void setCssVariable(String elementId, String name, String value) { + Objects.requireNonNull(name, "name"); + if (!name.trim().startsWith("--")) throw new IllegalArgumentException("CSS variable must start with '--': " + name); + setInlineStyle(elementId, name.trim(), value); + } + + @Override + public String getCssVariable(String elementId, String name) { + Objects.requireNonNull(name, "name"); + if (!name.trim().startsWith("--")) throw new IllegalArgumentException("CSS variable must start with '--': " + name); + return getComputedStyle(elementId, name.trim()); + } +} diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxGeometryHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxGeometryHost.java new file mode 100644 index 0000000..af431ce --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxGeometryHost.java @@ -0,0 +1,99 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.scene.web.WebEngine; +import org.openautonomousconnection.luascript.hosts.GeometryHost; + +import java.util.Map; +import java.util.Objects; + +/** + * Geometry/scroll host via internal JS bridge. + */ +public final class FxGeometryHost implements GeometryHost { + + private final WebEngine engine; + private final FxDomHost dom; + + public FxGeometryHost(WebEngine engine, FxDomHost dom) { + this.engine = Objects.requireNonNull(engine, "engine"); + this.dom = Objects.requireNonNull(dom, "dom"); + } + + @Override + public Map getBoundingClientRect(String elementId) { + Objects.requireNonNull(elementId, "elementId"); + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " var r=el.getBoundingClientRect();" + + " return {" + + " x:r.x, y:r.y, width:r.width, height:r.height," + + " top:r.top, left:r.left, right:r.right, bottom:r.bottom" + + " };" + + "})();"; + + Object ret = engine.executeScript(script); + return FxWebBridge.toStringObjectMap(ret); + })); + } + + @Override + public Map getViewport() { + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " return {" + + " width: window.innerWidth || 0," + + " height: window.innerHeight || 0," + + " devicePixelRatio: window.devicePixelRatio || 1," + + " scrollX: window.scrollX || 0," + + " scrollY: window.scrollY || 0" + + " };" + + "})();"; + + Object ret = engine.executeScript(script); + return FxWebBridge.toStringObjectMap(ret); + })); + } + + @Override + public void scrollTo(double x, double y) { + FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " window.scrollTo(" + FxWebBridge.toJsLiteral(x) + "," + FxWebBridge.toJsLiteral(y) + ");" + + " return null;" + + "})();"; + engine.executeScript(script); + })); + } + + @Override + public void scrollIntoView(String elementId, String align) { + Objects.requireNonNull(elementId, "elementId"); + String a = FxWebBridge.normalizeAlign(align); + + FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " el.scrollIntoView({block:" + FxWebBridge.toJsLiteral(a) + ", inline:" + FxWebBridge.toJsLiteral(a) + "});" + + " return null;" + + "})();"; + engine.executeScript(script); + })); + } +} diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxObserverHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxObserverHost.java new file mode 100644 index 0000000..5afa76c --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxObserverHost.java @@ -0,0 +1,246 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.scene.web.WebEngine; +import netscape.javascript.JSObject; +import org.openautonomousconnection.luascript.hosts.ObserverHost; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Observer host implemented via JS observers calling back into Java. + * + *

Requires JavaScript enabled (bridge only).

+ */ +public final class FxObserverHost implements ObserverHost { + + private final WebEngine engine; + private final FxDomHost dom; + + private volatile ObserverCallback callback; + + private final ConcurrentHashMap mutationObserved = new ConcurrentHashMap<>(); + private final ConcurrentHashMap resizeObserved = new ConcurrentHashMap<>(); + private final ConcurrentHashMap intersectionObserved = new ConcurrentHashMap<>(); + + public FxObserverHost(WebEngine engine, FxDomHost dom) { + this.engine = Objects.requireNonNull(engine, "engine"); + this.dom = Objects.requireNonNull(dom, "dom"); + } + + @Override + public void setCallback(ObserverCallback callback) { + this.callback = callback; + FxThreadBridge.runAndWait(this::installBridge); + } + + @Override + public void observeMutations(String elementId, boolean subtree, boolean attributes, boolean childList, boolean characterData) { + Objects.requireNonNull(elementId, "elementId"); + FxThreadBridge.runAndWait(() -> { + dom.requireDocument(); + FxWebBridge.ensureJsEnabled(engine); + installBridge(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " window.__oac_obs = window.__oac_obs || {};" + + " window.__oac_obs.muts = window.__oac_obs.muts || new Map();" + + " if(window.__oac_obs.muts.has(id)) return null;" + + " var cfg={subtree:" + (subtree ? "true" : "false") + + " ,attributes:" + (attributes ? "true" : "false") + + " ,childList:" + (childList ? "true" : "false") + + " ,characterData:" + (characterData ? "true" : "false") + + " };" + + " var mo=new MutationObserver(function(muts){" + + " try{" + + " var payload={count:muts.length};" + + " window.__oac_bridge.emit('mutation', id, JSON.stringify(payload));" + + " }catch(e){}" + + " });" + + " mo.observe(el,cfg);" + + " window.__oac_obs.muts.set(id, mo);" + + " return null;" + + "})();"; + engine.executeScript(script); + engine.setJavaScriptEnabled(false); + mutationObserved.put(elementId, true); + }); + } + + @Override + public void unobserveMutations(String elementId) { + Objects.requireNonNull(elementId, "elementId"); + FxThreadBridge.runAndWait(() -> { + dom.requireDocument(); + FxWebBridge.ensureJsEnabled(engine); + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " if(!window.__oac_obs || !window.__oac_obs.muts) return null;" + + " var mo=window.__oac_obs.muts.get(id);" + + " if(mo){ mo.disconnect(); window.__oac_obs.muts.delete(id); }" + + " return null;" + + "})();"; + engine.executeScript(script); + engine.setJavaScriptEnabled(false); + mutationObserved.remove(elementId); + }); + } + + @Override + public void observeResize(String elementId) { + Objects.requireNonNull(elementId, "elementId"); + FxThreadBridge.runAndWait(() -> { + dom.requireDocument(); + FxWebBridge.ensureJsEnabled(engine); + installBridge(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " if(!('ResizeObserver' in window)) return null;" + + " window.__oac_obs = window.__oac_obs || {};" + + " window.__oac_obs.res = window.__oac_obs.res || new Map();" + + " if(window.__oac_obs.res.has(id)) return null;" + + " var ro=new ResizeObserver(function(entries){" + + " try{" + + " var r=entries && entries[0] && entries[0].contentRect;" + + " var payload=r?{x:r.x,y:r.y,width:r.width,height:r.height}:{count:(entries?entries.length:0)};" + + " window.__oac_bridge.emit('resize', id, JSON.stringify(payload));" + + " }catch(e){}" + + " });" + + " ro.observe(el);" + + " window.__oac_obs.res.set(id, ro);" + + " return null;" + + "})();"; + engine.executeScript(script); + engine.setJavaScriptEnabled(false); + resizeObserved.put(elementId, true); + }); + } + + @Override + public void unobserveResize(String elementId) { + Objects.requireNonNull(elementId, "elementId"); + FxThreadBridge.runAndWait(() -> { + dom.requireDocument(); + FxWebBridge.ensureJsEnabled(engine); + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " if(!window.__oac_obs || !window.__oac_obs.res) return null;" + + " var ro=window.__oac_obs.res.get(id);" + + " if(ro){ ro.disconnect(); window.__oac_obs.res.delete(id); }" + + " return null;" + + "})();"; + engine.executeScript(script); + engine.setJavaScriptEnabled(false); + resizeObserved.remove(elementId); + }); + } + + @Override + public void observeIntersection(String elementId, double threshold) { + Objects.requireNonNull(elementId, "elementId"); + double t = threshold; + if (t < 0.0) t = 0.0; + if (t > 1.0) t = 1.0; + + double finalT = t; + FxThreadBridge.runAndWait(() -> { + dom.requireDocument(); + FxWebBridge.ensureJsEnabled(engine); + installBridge(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " if(!('IntersectionObserver' in window)) return null;" + + " window.__oac_obs = window.__oac_obs || {};" + + " window.__oac_obs.int = window.__oac_obs.int || new Map();" + + " if(window.__oac_obs.int.has(id)) return null;" + + " var io=new IntersectionObserver(function(entries){" + + " try{" + + " var e=entries && entries[0];" + + " var payload=e?{" + + " isIntersecting:!!e.isIntersecting," + + " ratio:(e.intersectionRatio||0)" + + " }:{count:(entries?entries.length:0)};" + + " window.__oac_bridge.emit('intersection', id, JSON.stringify(payload));" + + " }catch(ex){}" + + " }, {threshold:" + FxWebBridge.toJsLiteral(finalT) + "});" + + " io.observe(el);" + + " window.__oac_obs.int.set(id, io);" + + " return null;" + + "})();"; + engine.executeScript(script); + engine.setJavaScriptEnabled(false); + intersectionObserved.put(elementId, true); + }); + } + + @Override + public void unobserveIntersection(String elementId) { + Objects.requireNonNull(elementId, "elementId"); + FxThreadBridge.runAndWait(() -> { + dom.requireDocument(); + FxWebBridge.ensureJsEnabled(engine); + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " if(!window.__oac_obs || !window.__oac_obs.int) return null;" + + " var io=window.__oac_obs.int.get(id);" + + " if(io){ io.disconnect(); window.__oac_obs.int.delete(id); }" + + " return null;" + + "})();"; + engine.executeScript(script); + engine.setJavaScriptEnabled(false); + intersectionObserved.remove(elementId); + }); + } + + private void installBridge() { + dom.requireDocument(); + FxWebBridge.ensureJsEnabled(engine); + + JSObject win = (JSObject) engine.executeScript("window"); + win.setMember("__oac_bridge", new Bridge()); + } + + /** + * Object exposed to JS. + */ + public final class Bridge { + /** + * Emits observer events from JS to Java. + * + * @param type observer type + * @param targetId element id + * @param json payload JSON string + */ + public void emit(String type, String targetId, String json) { + ObserverCallback cb = callback; + if (cb == null) return; + + Map payload = new LinkedHashMap<>(); + payload.put("json", json == null ? "" : json); + + try { + cb.onEvent(type == null ? "" : type, targetId == null ? "" : targetId, payload); + } catch (RuntimeException ex) { + System.err.println("[observer] callback failed: " + ex.getMessage()); + ex.printStackTrace(System.err); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxSchedulerHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxSchedulerHost.java new file mode 100644 index 0000000..c7622fc --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxSchedulerHost.java @@ -0,0 +1,127 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.animation.AnimationTimer; +import javafx.application.Platform; +import org.openautonomousconnection.luascript.hosts.SchedulerHost; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; + +/** + * JavaFX-based scheduler providing setTimeout/setInterval/requestAnimationFrame. + */ +public final class FxSchedulerHost implements SchedulerHost, AutoCloseable { + + private final ScheduledExecutorService scheduler; + private final AtomicLong seq = new AtomicLong(1); + + private final ConcurrentHashMap> scheduled = new ConcurrentHashMap<>(); + private final ConcurrentHashMap rafCallbacks = new ConcurrentHashMap<>(); + private final Set canceledRaf = ConcurrentHashMap.newKeySet(); + + private final AnimationTimer rafTimer; + + public FxSchedulerHost() { + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "oac-fx-scheduler"); + t.setDaemon(true); + return t; + }); + + this.rafTimer = new AnimationTimer() { + @Override + public void handle(long now) { + // Drain RAF callbacks once per frame. + if (rafCallbacks.isEmpty()) return; + + Map snap = new ConcurrentHashMap<>(rafCallbacks); + rafCallbacks.clear(); + + for (Map.Entry e : snap.entrySet()) { + long id = e.getKey(); + Runnable cb = e.getValue(); + if (canceledRaf.remove(id)) continue; + + try { + cb.run(); + } catch (RuntimeException ex) { + System.err.println("[scheduler.raf] callback failed: " + ex.getMessage()); + ex.printStackTrace(System.err); + } + } + } + }; + + Platform.runLater(rafTimer::start); + } + + @Override + public long setTimeout(long delayMillis, Runnable callback) { + if (delayMillis < 0) delayMillis = 0; + Runnable cb = Objects.requireNonNull(callback, "callback"); + + long id = seq.getAndIncrement(); + ScheduledFuture f = scheduler.schedule(() -> safeRun(cb, "timeout"), delayMillis, TimeUnit.MILLISECONDS); + scheduled.put(id, f); + return id; + } + + @Override + public long setInterval(long intervalMillis, Runnable callback) { + if (intervalMillis <= 0) throw new IllegalArgumentException("intervalMillis must be > 0"); + Runnable cb = Objects.requireNonNull(callback, "callback"); + + long id = seq.getAndIncrement(); + ScheduledFuture f = scheduler.scheduleAtFixedRate(() -> safeRun(cb, "interval"), intervalMillis, intervalMillis, TimeUnit.MILLISECONDS); + scheduled.put(id, f); + return id; + } + + @Override + public boolean clear(long handle) { + ScheduledFuture f = scheduled.remove(handle); + if (f == null) return false; + return f.cancel(false); + } + + @Override + public long requestAnimationFrame(Runnable callback) { + Runnable cb = Objects.requireNonNull(callback, "callback"); + long id = seq.getAndIncrement(); + rafCallbacks.put(id, cb); + return id; + } + + @Override + public boolean cancelAnimationFrame(long handle) { + // If already queued, mark as canceled. + boolean existed = rafCallbacks.remove(handle) != null; + canceledRaf.add(handle); + return existed; + } + + private static void safeRun(Runnable cb, String kind) { + try { + cb.run(); + } catch (RuntimeException ex) { + System.err.println("[scheduler." + kind + "] callback failed: " + ex.getMessage()); + ex.printStackTrace(System.err); + } + } + + @Override + public void close() { + try { + Platform.runLater(rafTimer::stop); + } catch (Exception ignored) { + // ignore + } + scheduler.shutdownNow(); + scheduled.clear(); + rafCallbacks.clear(); + canceledRaf.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxSelectorHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxSelectorHost.java new file mode 100644 index 0000000..6bb5b01 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxSelectorHost.java @@ -0,0 +1,114 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.scene.web.WebEngine; +import org.openautonomousconnection.luascript.hosts.SelectorHost; + +import java.util.List; +import java.util.Objects; + +/** + * CSS selector host implemented via internal JS bridge. + */ +public final class FxSelectorHost implements SelectorHost { + + private final WebEngine engine; + private final FxDomHost dom; + + public FxSelectorHost(WebEngine engine, FxDomHost dom) { + this.engine = Objects.requireNonNull(engine, "engine"); + this.dom = Objects.requireNonNull(dom, "dom"); + } + + @Override + public String querySelector(String selector) { + Objects.requireNonNull(selector, "selector"); + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + dom.ensureAllElementsHaveId(); + + String script = "" + + "(function(){" + + " var sel = " + FxWebBridge.toJsLiteral(selector) + ";" + + " var el = document.querySelector(sel);" + + " if(!el) return null;" + + " if(!el.id){" + + " el.id='__auto_' + Math.floor(Math.random()*1e18).toString(36);" + + " }" + + " return el.id;" + + "})();"; + + Object ret = engine.executeScript(script); + return ret == null ? null : String.valueOf(ret); + })); + } + + @Override + public List querySelectorAll(String selector) { + Objects.requireNonNull(selector, "selector"); + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + dom.ensureAllElementsHaveId(); + + String script = "" + + "(function(){" + + " var sel=" + FxWebBridge.toJsLiteral(selector) + ";" + + " var els=document.querySelectorAll(sel);" + + " var out=[];" + + " for(var i=0;i FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var sel=" + FxWebBridge.toJsLiteral(selector) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " return !!el.matches(sel);" + + "})();"; + + Object ret = engine.executeScript(script); + return ret instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(ret)); + })); + } + + @Override + public String closest(String elementId, String selector) { + Objects.requireNonNull(elementId, "elementId"); + Objects.requireNonNull(selector, "selector"); + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var id=" + FxWebBridge.toJsLiteral(elementId) + ";" + + " var sel=" + FxWebBridge.toJsLiteral(selector) + ";" + + " var el=document.getElementById(id);" + + " if(!el) throw new Error('Unknown element id: '+id);" + + " var c=el.closest(sel);" + + " if(!c) return null;" + + " if(!c.id){ c.id='__auto_' + Math.floor(Math.random()*1e18).toString(36); }" + + " return c.id;" + + "})();"; + + Object ret = engine.executeScript(script); + return ret == null ? null : String.valueOf(ret); + })); + } +} diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxStorageHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxStorageHost.java new file mode 100644 index 0000000..7d00e9e --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxStorageHost.java @@ -0,0 +1,157 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.scene.web.WebEngine; +import org.openautonomousconnection.luascript.hosts.StorageHost; + +import java.util.List; +import java.util.Objects; + +/** + * Storage host backed by localStorage/sessionStorage via JS bridge. + */ +public final class FxStorageHost implements StorageHost { + + private final WebEngine engine; + private final FxDomHost dom; + + public FxStorageHost(WebEngine engine, FxDomHost dom) { + this.engine = Objects.requireNonNull(engine, "engine"); + this.dom = Objects.requireNonNull(dom, "dom"); + } + + @Override + public List localKeys() { + return keys("localStorage"); + } + + @Override + public String localGet(String key) { + return get("localStorage", key); + } + + @Override + public void localSet(String key, String value) { + set("localStorage", key, value); + } + + @Override + public void localRemove(String key) { + remove("localStorage", key); + } + + @Override + public void localClear() { + clear("localStorage"); + } + + @Override + public List sessionKeys() { + return keys("sessionStorage"); + } + + @Override + public String sessionGet(String key) { + return get("sessionStorage", key); + } + + @Override + public void sessionSet(String key, String value) { + set("sessionStorage", key, value); + } + + @Override + public void sessionRemove(String key) { + remove("sessionStorage", key); + } + + @Override + public void sessionClear() { + clear("sessionStorage"); + } + + private List keys(String storageName) { + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];" + + " if(!s) return [];" + + " var out=[];" + + " for(var i=0;i FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];" + + " if(!s) return null;" + + " var v=s.getItem(" + FxWebBridge.toJsLiteral(key) + ");" + + " return v===null?null:String(v);" + + "})();"; + + Object ret = engine.executeScript(script); + return ret == null ? null : String.valueOf(ret); + })); + } + + private void set(String storageName, String key, String value) { + Objects.requireNonNull(key, "key"); + + FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> { + dom.requireDocument(); + + if (value == null) { + String script = "" + + "(function(){" + + " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];" + + " if(!s) return null;" + + " s.removeItem(" + FxWebBridge.toJsLiteral(key) + ");" + + " return null;" + + "})();"; + engine.executeScript(script); + return; + } + + String script = "" + + "(function(){" + + " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];" + + " if(!s) return null;" + + " s.setItem(" + FxWebBridge.toJsLiteral(key) + "," + FxWebBridge.toJsLiteral(value) + ");" + + " return null;" + + "})();"; + engine.executeScript(script); + })); + } + + private void remove(String storageName, String key) { + Objects.requireNonNull(key, "key"); + set(storageName, key, null); + } + + private void clear(String storageName) { + FxThreadBridge.runAndWait(() -> FxWebBridge.runWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var s=window[" + FxWebBridge.toJsLiteral(storageName) + "];" + + " if(!s) return null;" + + " s.clear();" + + " return null;" + + "})();"; + engine.executeScript(script); + })); + } +} diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxUtilHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxUtilHost.java new file mode 100644 index 0000000..bcbadcd --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxUtilHost.java @@ -0,0 +1,137 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.scene.web.WebEngine; +import org.openautonomousconnection.luascript.hosts.UtilHost; + +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.*; + +/** + * Utility host: Base64, random, URL parsing, query parsing, JSON stringify/normalize via JS. + */ +public final class FxUtilHost implements UtilHost { + + private final WebEngine engine; + private final FxDomHost dom; + private final SecureRandom rng = new SecureRandom(); + + public FxUtilHost(WebEngine engine, FxDomHost dom) { + this.engine = Objects.requireNonNull(engine, "engine"); + this.dom = Objects.requireNonNull(dom, "dom"); + } + + @Override + public String base64Encode(String text) { + String s = text == null ? "" : text; + return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String base64Decode(String base64) { + if (base64 == null) return ""; + byte[] b = Base64.getDecoder().decode(base64); + return new String(b, StandardCharsets.UTF_8); + } + + @Override + public String randomHex(int numBytes) { + if (numBytes <= 0) throw new IllegalArgumentException("numBytes must be > 0"); + byte[] b = new byte[numBytes]; + rng.nextBytes(b); + StringBuilder sb = new StringBuilder(numBytes * 2); + for (byte x : b) sb.append(String.format("%02x", x)); + return sb.toString(); + } + + @Override + public Map parseUrl(String url) { + Objects.requireNonNull(url, "url"); + URI u = URI.create(url.trim()); + + Map out = new LinkedHashMap<>(); + out.put("scheme", safe(u.getScheme())); + out.put("host", safe(u.getHost())); + out.put("port", u.getPort() < 0 ? "" : String.valueOf(u.getPort())); + out.put("path", safe(u.getPath())); + out.put("query", safe(u.getQuery())); + out.put("fragment", safe(u.getFragment())); + return out; + } + + @Override + public Map> parseQuery(String query) { + String q = query == null ? "" : query.trim(); + if (q.startsWith("?")) q = q.substring(1); + + Map> out = new LinkedHashMap<>(); + if (q.isEmpty()) return out; + + for (String part : q.split("&")) { + if (part.isEmpty()) continue; + String k; + String v; + int idx = part.indexOf('='); + if (idx < 0) { + k = decode(part); + v = ""; + } else { + k = decode(part.substring(0, idx)); + v = decode(part.substring(idx + 1)); + } + out.computeIfAbsent(k, __ -> new ArrayList<>()).add(v); + } + return out; + } + + @Override + public String jsonStringifyExpr(String elementId, String jsExpr) { + Objects.requireNonNull(elementId, "elementId"); + Objects.requireNonNull(jsExpr, "jsExpr"); + + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + // NOTE: jsExpr is intentionally raw JS expression; this is a trusted host API. + // If you need untrusted usage, wrap/validate upstream. + String script = "" + + "(function(){" + + " var v=(" + jsExpr + ");" + + " return JSON.stringify(v);" + + "})();"; + + Object ret = engine.executeScript(script); + return ret == null ? "null" : String.valueOf(ret); + })); + } + + @Override + public String jsonNormalize(String elementId, String json) { + Objects.requireNonNull(elementId, "elementId"); + Objects.requireNonNull(json, "json"); + + return FxThreadBridge.callAndWait(() -> FxWebBridge.callWithJs(engine, () -> { + dom.requireDocument(); + + String script = "" + + "(function(){" + + " var s=" + FxWebBridge.toJsLiteral(json) + ";" + + " var obj=JSON.parse(s);" + + " return JSON.stringify(obj);" + + "})();"; + + Object ret = engine.executeScript(script); + return ret == null ? "null" : String.valueOf(ret); + })); + } + + private static String safe(String s) { + return s == null ? "" : s; + } + + private static String decode(String s) { + return URLDecoder.decode(s, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxWebBridge.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxWebBridge.java new file mode 100644 index 0000000..51c7ef7 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxWebBridge.java @@ -0,0 +1,227 @@ +package org.openautonomousconnection.luascript.fx; + +import javafx.scene.web.WebEngine; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Shared utilities for WebEngine JS bridging. + */ +public final class FxWebBridge { + + private FxWebBridge() { + } + + /** + * Ensures JavaScript is enabled for the engine. + * + * @param engine engine + */ + public static void ensureJsEnabled(WebEngine engine) { + Objects.requireNonNull(engine, "engine"); + if (!engine.isJavaScriptEnabled()) { + engine.setJavaScriptEnabled(true); + } + } + + /** + * Executes bridge code with JavaScript temporarily enabled. + * + * @param engine engine + * @param action action + */ + public static void runWithJs(WebEngine engine, Runnable action) { + Objects.requireNonNull(action, "action"); + callWithJs(engine, () -> { + action.run(); + return null; + }); + } + + /** + * Executes bridge code with JavaScript temporarily enabled and restores the prior state. + * + * @param engine engine + * @param action action + * @param result type + * @return action result + */ + public static T callWithJs(WebEngine engine, Supplier action) { + Objects.requireNonNull(engine, "engine"); + Objects.requireNonNull(action, "action"); + + boolean enabledBefore = engine.isJavaScriptEnabled(); + if (!enabledBefore) { + engine.setJavaScriptEnabled(true); + } + try { + return action.get(); + } finally { + if (!enabledBefore) { + engine.setJavaScriptEnabled(false); + } + } + } + + /** + * Converts a Java value into a safe JavaScript literal. + * + *

Supported: null, String, Boolean, Number (finite).

+ * + * @param v value + * @return JS literal + */ + public static String toJsLiteral(Object v) { + if (v == null) return "null"; + + if (v instanceof String s) { + return "'" + escapeJsSingleQuotedString(s) + "'"; + } + + if (v instanceof Boolean b) { + return b ? "true" : "false"; + } + + if (v instanceof Number n) { + double d = n.doubleValue(); + if (!Double.isFinite(d)) { + throw new IllegalArgumentException("Non-finite number is not supported for JS literal: " + d); + } + return Double.toString(d); + } + + throw new IllegalArgumentException("Unsupported value type for JS literal: " + v.getClass().getName()); + } + + /** + * Builds a JavaScript array literal from Java values. + * + * @param values values + * @return array literal + */ + public static String toJsArrayLiteral(Object[] values) { + if (values == null || values.length == 0) return "[]"; + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (int i = 0; i < values.length; i++) { + if (i > 0) sb.append(','); + sb.append(toJsLiteral(values[i])); + } + sb.append(']'); + return sb.toString(); + } + + /** + * Restricts input to a plain JavaScript identifier (prevents injection). + * + * @param s value + * @param label label + * @return identifier + */ + public static String requireJsIdentifier(String s, String label) { + if (s == null) throw new IllegalArgumentException(label + " is null"); + String v = s.trim(); + if (v.isEmpty()) throw new IllegalArgumentException(label + " is blank"); + if (!v.matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) { + throw new IllegalArgumentException(label + " must be a JS identifier: " + v); + } + return v; + } + + /** + * Normalizes alignment options for scrollIntoView. + * + * @param align input + * @return normalized + */ + public static String normalizeAlign(String align) { + if (align == null) return "nearest"; + String a = align.trim().toLowerCase(Locale.ROOT); + return switch (a) { + case "start", "center", "end", "nearest" -> a; + default -> "nearest"; + }; + } + + /** + * Converts a JS array-like object into a Java string list. + * + * @param jsValue raw JS value + * @return list of string values + */ + public static List toStringList(Object jsValue) { + if (jsValue == null) return List.of(); + if (jsValue instanceof List list) { + List out = new ArrayList<>(list.size()); + for (Object value : list) out.add(value == null ? null : String.valueOf(value)); + return out; + } + + try { + Class jsObj = Class.forName("netscape.javascript.JSObject"); + if (jsObj.isInstance(jsValue)) { + Object lenObj = jsObj.getMethod("getMember", String.class).invoke(jsValue, "length"); + int len = Integer.parseInt(String.valueOf(lenObj)); + List out = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + Object value = jsObj.getMethod("getSlot", int.class).invoke(jsValue, i); + out.add(value == null ? null : String.valueOf(value)); + } + return out; + } + } catch (Exception ignored) { + // best-effort fallback below + } + + return List.of(String.valueOf(jsValue)); + } + + /** + * Converts a JS object into a Java string-object map when possible. + * + * @param jsValue raw JS value + * @return mapped values + */ + public static Map toStringObjectMap(Object jsValue) { + if (jsValue == null) return Map.of(); + if (jsValue instanceof Map map) { + Map out = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() == null) continue; + out.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return out; + } + + return Map.of("value", jsValue); + } + + private static String escapeJsSingleQuotedString(String s) { + if (s == null || s.isEmpty()) return ""; + StringBuilder out = new StringBuilder(s.length() + 16); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\'' -> out.append("\\'"); + case '\\' -> out.append("\\\\"); + case '\n' -> out.append("\\n"); + case '\r' -> out.append("\\r"); + case '\t' -> out.append("\\t"); + case '\u0000' -> out.append("\\0"); + case '\u2028' -> out.append("\\u2028"); + case '\u2029' -> out.append("\\u2029"); + default -> { + if (c < 0x20) out.append(String.format("\\u%04x", (int) c)); + else out.append(c); + } + } + } + return out.toString(); + } +} diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/ClipboardHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/ClipboardHost.java new file mode 100644 index 0000000..665fc62 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/ClipboardHost.java @@ -0,0 +1,21 @@ +package org.openautonomousconnection.luascript.hosts; + +/** + * Clipboard access. + */ +public interface ClipboardHost { + + /** + * Sets clipboard text. + * + * @param text text + */ + void setText(String text); + + /** + * Gets clipboard text. + * + * @return text or empty string if none + */ + String getText(); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/CssHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/CssHost.java new file mode 100644 index 0000000..411cd9f --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/CssHost.java @@ -0,0 +1,63 @@ +package org.openautonomousconnection.luascript.hosts; + +import java.util.Map; + +/** + * CSSOM / computed styles access. + */ +public interface CssHost { + + /** + * Gets computed style value for a property. + * + * @param elementId element id + * @param property CSS property (kebab-case or camelCase accepted by browser) + * @return computed value (never null, may be empty) + */ + String getComputedStyle(String elementId, String property); + + /** + * Returns a computed style snapshot of selected properties. + * + * @param elementId element id + * @param properties properties to fetch + * @return map property->value + */ + Map getComputedStyles(String elementId, String[] properties); + + /** + * Sets an inline style property (style attribute). + * + * @param elementId element id + * @param property CSS property + * @param value CSS value (empty => remove) + */ + void setInlineStyle(String elementId, String property, String value); + + /** + * Gets an inline style property (from style attribute). + * + * @param elementId element id + * @param property CSS property + * @return inline value (may be empty) + */ + String getInlineStyle(String elementId, String property); + + /** + * Sets a CSS variable (custom property) on an element. + * + * @param elementId element id + * @param name variable name (e.g. "--primary") + * @param value value + */ + void setCssVariable(String elementId, String name, String value); + + /** + * Gets a CSS variable (computed) from an element. + * + * @param elementId element id + * @param name variable name (e.g. "--primary") + * @return computed var (may be empty) + */ + String getCssVariable(String elementId, String name); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/GeometryHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/GeometryHost.java new file mode 100644 index 0000000..28b4122 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/GeometryHost.java @@ -0,0 +1,42 @@ +package org.openautonomousconnection.luascript.hosts; + +import java.util.Map; + +/** + * Layout and scrolling related accessors. + * + *

All results are plain maps to keep dependencies minimal.

+ */ +public interface GeometryHost { + + /** + * Returns bounding client rect of an element. + * + * @param elementId element id + * @return map: x,y,width,height,top,left,right,bottom + */ + Map getBoundingClientRect(String elementId); + + /** + * Returns viewport metrics. + * + * @return map: width,height,devicePixelRatio,scrollX,scrollY + */ + Map getViewport(); + + /** + * Scrolls the document to coordinates. + * + * @param x scroll x + * @param y scroll y + */ + void scrollTo(double x, double y); + + /** + * Scrolls an element into view. + * + * @param elementId element id + * @param align one of: "start","center","end","nearest" (null => "nearest") + */ + void scrollIntoView(String elementId, String align); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java b/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java index c9f852f..047f198 100644 --- a/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java @@ -10,62 +10,25 @@ import java.util.Optional; */ public interface HostServices { - /** - * Returns an optional UI host. - * - * @return ui host - */ Optional ui(); - - /** - * Returns an optional DOM host. - * - * @return dom host - */ Optional dom(); - - /** - * Returns an optional event host. - * - * @return event host - */ Optional events(); - - /** - * Returns an optional resource host. - * - * @return resource host - */ Optional resources(); - - /** - * Returns an optional console host. - * - * @return console host - */ Optional console(); - - /** - * Returns an optional audio host. - * - * @return audio host - */ Optional audio(); - - /** - * Returns an optional image host. - * - * @return image host - */ Optional image(); - - /** - * Returns an optional video host. - * - * @return video host - */ Optional video(); + /* NEW */ + Optional scheduler(); + Optional selector(); + Optional geometry(); + Optional css(); + Optional storage(); + Optional util(); + Optional clipboard(); + Optional observers(); + /** * Simple immutable implementation. */ @@ -80,18 +43,15 @@ public interface HostServices { private final ImageHost imageHost; private final VideoHost videoHost; - /** - * Creates a HostServices container. - * - * @param ui ui host - * @param dom dom host - * @param events event host - * @param resources resource host - * @param console console host - * @param audioHost audio host - * @param imageHost image host - * @param videoHost video host - */ + private final SchedulerHost schedulerHost; + private final SelectorHost selectorHost; + private final GeometryHost geometryHost; + private final CssHost cssHost; + private final StorageHost storageHost; + private final UtilHost utilHost; + private final ClipboardHost clipboardHost; + private final ObserverHost observerHost; + public Default( UiHost ui, DomHost dom, @@ -100,7 +60,15 @@ public interface HostServices { ConsoleHost console, AudioHost audioHost, ImageHost imageHost, - VideoHost videoHost + VideoHost videoHost, + SchedulerHost schedulerHost, + SelectorHost selectorHost, + GeometryHost geometryHost, + CssHost cssHost, + StorageHost storageHost, + UtilHost utilHost, + ClipboardHost clipboardHost, + ObserverHost observerHost ) { this.ui = ui; this.dom = dom; @@ -110,47 +78,33 @@ public interface HostServices { this.audioHost = audioHost; this.imageHost = imageHost; this.videoHost = videoHost; + this.schedulerHost = schedulerHost; + this.selectorHost = selectorHost; + this.geometryHost = geometryHost; + this.cssHost = cssHost; + this.storageHost = storageHost; + this.utilHost = utilHost; + this.clipboardHost = clipboardHost; + this.observerHost = observerHost; } - @Override - public Optional ui() { - return Optional.ofNullable(ui); - } + @Override public Optional ui() { return Optional.ofNullable(ui); } + @Override public Optional dom() { return Optional.ofNullable(dom); } + @Override public Optional events() { return Optional.ofNullable(events); } + @Override public Optional resources() { return Optional.ofNullable(resources); } + @Override public Optional console() { return Optional.ofNullable(console); } + @Override public Optional audio() { return Optional.ofNullable(audioHost); } + @Override public Optional image() { return Optional.ofNullable(imageHost); } + @Override public Optional video() { return Optional.ofNullable(videoHost); } - @Override - public Optional dom() { - return Optional.ofNullable(dom); - } - - @Override - public Optional events() { - return Optional.ofNullable(events); - } - - @Override - public Optional resources() { - return Optional.ofNullable(resources); - } - - @Override - public Optional console() { - return Optional.ofNullable(console); - } - - @Override - public Optional audio() { - return Optional.ofNullable(audioHost); - } - - @Override - public Optional image() { - return Optional.ofNullable(imageHost); - } - - @Override - public Optional video() { - return Optional.ofNullable(videoHost); - } + @Override public Optional scheduler() { return Optional.ofNullable(schedulerHost); } + @Override public Optional selector() { return Optional.ofNullable(selectorHost); } + @Override public Optional geometry() { return Optional.ofNullable(geometryHost); } + @Override public Optional css() { return Optional.ofNullable(cssHost); } + @Override public Optional storage() { return Optional.ofNullable(storageHost); } + @Override public Optional util() { return Optional.ofNullable(utilHost); } + @Override public Optional clipboard() { return Optional.ofNullable(clipboardHost); } + @Override public Optional observers() { return Optional.ofNullable(observerHost); } } /** @@ -159,11 +113,6 @@ public interface HostServices { final class StdoutConsole implements ConsoleHost { private final String prefix; - /** - * Creates a new stdout console with a prefix. - * - * @param prefix prefix (may be empty) - */ public StdoutConsole(String prefix) { this.prefix = Objects.requireNonNull(prefix, "prefix"); } @@ -172,29 +121,10 @@ public interface HostServices { return s == null ? "" : s; } - @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)); - } + @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)); } } } \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/ObserverHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/ObserverHost.java new file mode 100644 index 0000000..23dc459 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/ObserverHost.java @@ -0,0 +1,80 @@ +package org.openautonomousconnection.luascript.hosts; + +import java.util.Map; + +/** + * Reactive observers: Mutation/Resize/Intersection. + * + *

Observers emit events into Lua via a host-provided callback.

+ */ +public interface ObserverHost { + + /** + * Callback used by the host to notify observers. + */ + @FunctionalInterface + interface ObserverCallback { + /** + * Called on observer events. + * + * @param type observer type ("mutation","resize","intersection") + * @param targetId element id + * @param data payload map + */ + void onEvent(String type, String targetId, Map data); + } + + /** + * Sets the callback to receive observer events. + * + * @param callback callback (nullable to disable) + */ + void setCallback(ObserverCallback callback); + + /** + * Observes DOM mutations on an element (subtree). + * + * @param elementId element id + * @param subtree true to include subtree + * @param attributes true to observe attributes + * @param childList true to observe childList + * @param characterData true to observe text changes + */ + void observeMutations(String elementId, boolean subtree, boolean attributes, boolean childList, boolean characterData); + + /** + * Stops mutation observing for an element. + * + * @param elementId element id + */ + void unobserveMutations(String elementId); + + /** + * Observes resize changes of an element. + * + * @param elementId element id + */ + void observeResize(String elementId); + + /** + * Stops resize observing for an element. + * + * @param elementId element id + */ + void unobserveResize(String elementId); + + /** + * Observes intersection changes of an element with viewport. + * + * @param elementId element id + * @param threshold threshold in [0..1] + */ + void observeIntersection(String elementId, double threshold); + + /** + * Stops intersection observing for an element. + * + * @param elementId element id + */ + void unobserveIntersection(String elementId); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/SchedulerHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/SchedulerHost.java new file mode 100644 index 0000000..858f15b --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/SchedulerHost.java @@ -0,0 +1,53 @@ +package org.openautonomousconnection.luascript.hosts; + +/** + * Scheduling primitives comparable to JavaScript timers. + * + *

No networking. This host only provides time-based callbacks.

+ */ +public interface SchedulerHost { + + /** + * Schedules a one-shot callback after a delay. + * + * @param delayMillis delay in milliseconds (>= 0) + * @param callback callback to run on host-defined thread (typically Lua thread) + * @return handle id + */ + long setTimeout(long delayMillis, Runnable callback); + + /** + * Schedules a repeating callback with fixed rate. + * + * @param intervalMillis interval in milliseconds (> 0) + * @param callback callback to run + * @return handle id + */ + long setInterval(long intervalMillis, Runnable callback); + + /** + * Cancels a timeout/interval handle. + * + * @param handle handle id + * @return true if canceled + */ + boolean clear(long handle); + + /** + * Schedules a callback for the next animation frame. + * + *

Comparable to requestAnimationFrame. The callback is invoked once.

+ * + * @param callback callback to run + * @return handle id + */ + long requestAnimationFrame(Runnable callback); + + /** + * Cancels a previously scheduled animation frame. + * + * @param handle handle id + * @return true if canceled + */ + boolean cancelAnimationFrame(long handle); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/SelectorHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/SelectorHost.java new file mode 100644 index 0000000..957e140 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/SelectorHost.java @@ -0,0 +1,45 @@ +package org.openautonomousconnection.luascript.hosts; + +import java.util.List; + +/** + * CSS selector based DOM querying/traversal. + * + *

All returned elements are identified by stable element ids.

+ */ +public interface SelectorHost { + + /** + * Returns the first element matching the selector, or null. + * + * @param selector CSS selector + * @return element id or null + */ + String querySelector(String selector); + + /** + * Returns all elements matching the selector. + * + * @param selector CSS selector + * @return list of element ids (never null) + */ + List querySelectorAll(String selector); + + /** + * Checks if an element matches a selector. + * + * @param elementId element id + * @param selector CSS selector + * @return true if matches + */ + boolean matches(String elementId, String selector); + + /** + * Returns the closest ancestor (including itself) matching selector, or null. + * + * @param elementId element id + * @param selector CSS selector + * @return closest element id or null + */ + String closest(String elementId, String selector); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/StorageHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/StorageHost.java new file mode 100644 index 0000000..75244e1 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/StorageHost.java @@ -0,0 +1,67 @@ +package org.openautonomousconnection.luascript.hosts; + +import java.util.List; + +/** + * Storage primitives comparable to localStorage/sessionStorage. + */ +public interface StorageHost { + + /** + * @return keys in localStorage + */ + List localKeys(); + + /** + * @param key key + * @return value or null + */ + String localGet(String key); + + /** + * @param key key + * @param value value (null removes) + */ + void localSet(String key, String value); + + /** + * Removes a key from localStorage. + * + * @param key key + */ + void localRemove(String key); + + /** + * Clears localStorage. + */ + void localClear(); + + /** + * @return keys in sessionStorage + */ + List sessionKeys(); + + /** + * @param key key + * @return value or null + */ + String sessionGet(String key); + + /** + * @param key key + * @param value value (null removes) + */ + void sessionSet(String key, String value); + + /** + * Removes a key from sessionStorage. + * + * @param key key + */ + void sessionRemove(String key); + + /** + * Clears sessionStorage. + */ + void sessionClear(); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/UtilHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/UtilHost.java new file mode 100644 index 0000000..c0abc90 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/UtilHost.java @@ -0,0 +1,70 @@ +package org.openautonomousconnection.luascript.hosts; + +import java.util.List; +import java.util.Map; + +/** + * Utility helpers commonly available in browsers. + * + *

No networking.

+ */ +public interface UtilHost { + + /** + * Encodes bytes (UTF-8) as Base64. + * + * @param text text + * @return base64 + */ + String base64Encode(String text); + + /** + * Decodes Base64 into UTF-8 text. + * + * @param base64 base64 + * @return decoded text + */ + String base64Decode(String base64); + + /** + * Generates cryptographically-strong random bytes and returns as hex string. + * + * @param numBytes number of bytes (>0) + * @return hex string + */ + String randomHex(int numBytes); + + /** + * Parses a URL string into components. + * + * @param url url string + * @return map containing scheme,host,port,path,query,fragment + */ + Map parseUrl(String url); + + /** + * Parses a query string into key->list(values). + * + * @param query query string (with or without leading '?') + * @return map key->values + */ + Map> parseQuery(String query); + + /** + * JSON.stringify via browser engine (returns JSON string). + * + * @param elementId element id that provides the JS context (ignored by some engines but kept for safety) + * @param jsExpr JS expression returning a JSON-serializable value + * @return JSON string + */ + String jsonStringifyExpr(String elementId, String jsExpr); + + /** + * JSON.parse via browser engine and returns normalized JSON string (stringify(parse(x))). + * + * @param elementId element id providing JS context + * @param json json string + * @return normalized json + */ + String jsonNormalize(String elementId, String json); +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java b/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java index cd47d0d..5f64f18 100644 --- a/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java +++ b/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java @@ -80,14 +80,27 @@ public final class FxLuaScriptEngine implements AutoCloseable { .sandbox(true) ); - HostServices.StdoutConsole console = new HostServices.StdoutConsole("[lua] "); - FxUiHost uiHost = new FxUiHost(engine, dom); - FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine); FxEventHost eventHost = new FxEventHost(dom); - FxAudioHost audioHost = new FxAudioHost(); - FxVideoHost videoHost = new FxVideoHost(engine, dom); - FxImageHost imageHost = new FxImageHost(engine, dom); - HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost, imageHost, videoHost); + + HostServices services = new HostServices.Default( + new FxUiHost(engine, dom), + dom, + eventHost, + new FxWebViewResourceHost(engine), + new HostServices.StdoutConsole("[lua] "), + new FxAudioHost(), + new FxImageHost(engine, dom), + new FxVideoHost(engine, dom), + new FxSchedulerHost(), + new FxSelectorHost(engine, dom), + new FxGeometryHost(engine, dom), + new FxCssHost(engine, dom), + new FxStorageHost(engine, dom), + new FxUtilHost(engine, dom), + new FxClipboardHost(), + new FxObserverHost(engine, dom) + ); + LuaRuntime rt = new LuaRuntime(globals, services, policy); eventHost.setRouter(rt.eventRouter()); diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/ClipboardTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/ClipboardTable.java new file mode 100644 index 0000000..e1a113c --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/ClipboardTable.java @@ -0,0 +1,43 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.*; +import org.openautonomousconnection.luascript.hosts.ClipboardHost; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +/** + * Lua table: clipboard + * + *

Functions:

+ *
    + *
  • clipboard.set(text)
  • + *
  • clipboard.get() -> text
  • + *
+ */ +public final class ClipboardTable extends ScriptTable { + + public ClipboardTable() { + super("clipboard"); + } + + @Override + protected void define(HostServices services) { + ClipboardHost host = services.clipboard().orElseThrow(() -> new IllegalStateException("ClipboardHost not provided")); + + table().set("set", new OneArgFunction() { + @Override + public LuaValue call(LuaValue text) { + host.setText(text.isnil() ? "" : text.tojstring()); + return LuaValue.NIL; + } + }); + + table().set("get", new ZeroArgFunction() { + @Override + public LuaValue call() { + return LuaValue.valueOf(host.getText()); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/CssTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/CssTable.java new file mode 100644 index 0000000..162eafb --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/CssTable.java @@ -0,0 +1,86 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.*; +import org.openautonomousconnection.luascript.events.JavaToLua; +import org.openautonomousconnection.luascript.hosts.CssHost; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +import java.util.Map; + +/** + * Lua table: css + * + *

Functions:

+ *
    + *
  • css.computed(id, prop) -> string
  • + *
  • css.computedMany(id, {props...}) -> map
  • + *
  • css.inlineGet(id, prop) -> string
  • + *
  • css.inlineSet(id, prop, value)
  • + *
  • css.varGet(id, name) -> string
  • + *
  • css.varSet(id, name, value)
  • + *
+ */ +public final class CssTable extends ScriptTable { + + public CssTable() { + super("css"); + } + + @Override + protected void define(HostServices services) { + CssHost host = services.css().orElseThrow(() -> new IllegalStateException("CssHost not provided")); + + table().set("computed", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue prop) { + return LuaValue.valueOf(host.getComputedStyle(id.checkjstring(), prop.checkjstring())); + } + }); + + table().set("computedMany", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue propsTable) { + if (!propsTable.istable()) throw new IllegalArgumentException("props must be a table"); + int n = propsTable.length(); + String[] props = new String[n]; + for (int i = 1; i <= n; i++) { + props[i - 1] = propsTable.get(i).checkjstring(); + } + Map m = host.getComputedStyles(id.checkjstring(), props); + return JavaToLua.coerce(m); + } + }); + + table().set("inlineGet", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue prop) { + return LuaValue.valueOf(host.getInlineStyle(id.checkjstring(), prop.checkjstring())); + } + }); + + table().set("inlineSet", new ThreeArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue prop, LuaValue value) { + host.setInlineStyle(id.checkjstring(), prop.checkjstring(), value.isnil() ? "" : value.tojstring()); + return LuaValue.NIL; + } + }); + + table().set("varGet", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue name) { + return LuaValue.valueOf(host.getCssVariable(id.checkjstring(), name.checkjstring())); + } + }); + + table().set("varSet", new ThreeArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue name, LuaValue value) { + host.setCssVariable(id.checkjstring(), name.checkjstring(), value.isnil() ? "" : value.tojstring()); + return LuaValue.NIL; + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/GeometryTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/GeometryTable.java new file mode 100644 index 0000000..e30ac1c --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/GeometryTable.java @@ -0,0 +1,70 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.OneArgFunction; +import org.luaj.vm2.lib.TwoArgFunction; +import org.luaj.vm2.lib.VarArgFunction; +import org.luaj.vm2.Varargs; +import org.openautonomousconnection.luascript.events.JavaToLua; +import org.openautonomousconnection.luascript.hosts.GeometryHost; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +import java.util.Map; + +/** + * Lua table: geometry + * + *

Functions:

+ *
    + *
  • geometry.rect(id) -> map
  • + *
  • geometry.viewport() -> map
  • + *
  • geometry.scrollTo(x,y)
  • + *
  • geometry.scrollIntoView(id, align?)
  • + *
+ */ +public final class GeometryTable extends ScriptTable { + + public GeometryTable() { + super("geometry"); + } + + @Override + protected void define(HostServices services) { + GeometryHost host = services.geometry().orElseThrow(() -> new IllegalStateException("GeometryHost not provided")); + + table().set("rect", new OneArgFunction() { + @Override + public LuaValue call(LuaValue id) { + Map m = host.getBoundingClientRect(id.checkjstring()); + return JavaToLua.coerce(m); + } + }); + + table().set("viewport", new org.luaj.vm2.lib.ZeroArgFunction() { + @Override + public LuaValue call() { + return JavaToLua.coerce(host.getViewport()); + } + }); + + table().set("scrollTo", new VarArgFunction() { + @Override + public Varargs invoke(Varargs args) { + host.scrollTo(args.arg(1).checkdouble(), args.arg(2).checkdouble()); + return LuaValue.NIL; + } + }); + + table().set("scrollIntoView", new VarArgFunction() { + @Override + public Varargs invoke(Varargs args) { + String id = args.arg(1).checkjstring(); + String align = args.narg() >= 2 && !args.arg(2).isnil() ? args.arg(2).tojstring() : null; + host.scrollIntoView(id, align); + return LuaValue.NIL; + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/ObserversTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/ObserversTable.java new file mode 100644 index 0000000..3491390 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/ObserversTable.java @@ -0,0 +1,103 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaFunction; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.Varargs; +import org.luaj.vm2.lib.*; +import org.openautonomousconnection.luascript.events.JavaToLua; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.hosts.ObserverHost; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +import java.util.Map; + +/** + * Lua table: observers + * + *

Functions:

+ *
    + *
  • observers.on(fn(type, targetId, dataTable))
  • + *
  • observers.mutationObserve(id, subtree, attributes, childList, characterData)
  • + *
  • observers.mutationUnobserve(id)
  • + *
  • observers.resizeObserve(id)
  • + *
  • observers.resizeUnobserve(id)
  • + *
  • observers.intersectionObserve(id, threshold)
  • + *
  • observers.intersectionUnobserve(id)
  • + *
+ */ +public final class ObserversTable extends ScriptTable { + + public ObserversTable() { + super("observers"); + } + + @Override + protected void define(HostServices services) { + ObserverHost host = services.observers().orElseThrow(() -> new IllegalStateException("ObserverHost not provided")); + + table().set("on", new OneArgFunction() { + @Override + public LuaValue call(LuaValue fn) { + LuaFunction cb = fn.checkfunction(); + host.setCallback((type, targetId, data) -> { + LuaValue luaData = JavaToLua.coerce(data); + cb.call(LuaValue.valueOf(type), LuaValue.valueOf(targetId), luaData); + }); + return LuaValue.NIL; + } + }); + + table().set("mutationObserve", new VarArgFunction() { + @Override + public Varargs invoke(Varargs args) { + String id = args.arg(1).checkjstring(); + boolean subtree = args.arg(2).optboolean(false); + boolean attributes = args.arg(3).optboolean(true); + boolean childList = args.arg(4).optboolean(true); + boolean characterData = args.arg(5).optboolean(false); + host.observeMutations(id, subtree, attributes, childList, characterData); + return LuaValue.NIL; + } + }); + + table().set("mutationUnobserve", new OneArgFunction() { + @Override + public LuaValue call(LuaValue id) { + host.unobserveMutations(id.checkjstring()); + return LuaValue.NIL; + } + }); + + table().set("resizeObserve", new OneArgFunction() { + @Override + public LuaValue call(LuaValue id) { + host.observeResize(id.checkjstring()); + return LuaValue.NIL; + } + }); + + table().set("resizeUnobserve", new OneArgFunction() { + @Override + public LuaValue call(LuaValue id) { + host.unobserveResize(id.checkjstring()); + return LuaValue.NIL; + } + }); + + table().set("intersectionObserve", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue threshold) { + host.observeIntersection(id.checkjstring(), threshold.checkdouble()); + return LuaValue.NIL; + } + }); + + table().set("intersectionUnobserve", new OneArgFunction() { + @Override + public LuaValue call(LuaValue id) { + host.unobserveIntersection(id.checkjstring()); + return LuaValue.NIL; + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/SchedulerTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/SchedulerTable.java new file mode 100644 index 0000000..5439b36 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/SchedulerTable.java @@ -0,0 +1,78 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaFunction; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.OneArgFunction; +import org.luaj.vm2.lib.TwoArgFunction; +import org.luaj.vm2.lib.VarArgFunction; +import org.luaj.vm2.Varargs; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.hosts.SchedulerHost; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +/** + * Lua table: scheduler + * + *

Functions:

+ *
    + *
  • scheduler.timeout(ms, fn) -> id
  • + *
  • scheduler.interval(ms, fn) -> id
  • + *
  • scheduler.clear(id) -> boolean
  • + *
  • scheduler.raf(fn) -> id
  • + *
  • scheduler.cancelRaf(id) -> boolean
  • + *
+ */ +public final class SchedulerTable extends ScriptTable { + + public SchedulerTable() { + super("scheduler"); + } + + @Override + protected void define(HostServices services) { + SchedulerHost host = services.scheduler().orElseThrow(() -> new IllegalStateException("SchedulerHost not provided")); + + table().set("timeout", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue ms, LuaValue fn) { + long delay = ms.checklong(); + LuaFunction cb = fn.checkfunction(); + long id = host.setTimeout(delay, () -> cb.call()); + return LuaValue.valueOf(id); + } + }); + + table().set("interval", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue ms, LuaValue fn) { + long interval = ms.checklong(); + LuaFunction cb = fn.checkfunction(); + long id = host.setInterval(interval, () -> cb.call()); + return LuaValue.valueOf(id); + } + }); + + table().set("clear", new OneArgFunction() { + @Override + public LuaValue call(LuaValue id) { + return LuaValue.valueOf(host.clear(id.checklong())); + } + }); + + table().set("raf", new OneArgFunction() { + @Override + public LuaValue call(LuaValue fn) { + LuaFunction cb = fn.checkfunction(); + long id = host.requestAnimationFrame(() -> cb.call()); + return LuaValue.valueOf(id); + } + }); + + table().set("cancelRaf", new OneArgFunction() { + @Override + public LuaValue call(LuaValue id) { + return LuaValue.valueOf(host.cancelAnimationFrame(id.checklong())); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/SelectorTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/SelectorTable.java new file mode 100644 index 0000000..bc7b9f2 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/SelectorTable.java @@ -0,0 +1,74 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.TwoArgFunction; +import org.luaj.vm2.lib.OneArgFunction; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.hosts.SelectorHost; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +import java.util.List; + +/** + * Lua table: selector + * + *

Functions:

+ *
    + *
  • selector.one(css) -> id|nil
  • + *
  • selector.all(css) -> { ids... }
  • + *
  • selector.matches(id, css) -> boolean
  • + *
  • selector.closest(id, css) -> id|nil
  • + *
+ */ +public final class SelectorTable extends ScriptTable { + + public SelectorTable() { + super("selector"); + } + + private static LuaValue toLuaArray(List values) { + LuaTable t = new LuaTable(); + if (values == null || values.isEmpty()) return t; + int i = 1; + for (String v : values) { + t.set(i++, v == null ? LuaValue.NIL : LuaValue.valueOf(v)); + } + return t; + } + + @Override + protected void define(HostServices services) { + SelectorHost host = services.selector().orElseThrow(() -> new IllegalStateException("SelectorHost not provided")); + + table().set("one", new OneArgFunction() { + @Override + public LuaValue call(LuaValue css) { + String id = host.querySelector(css.checkjstring()); + return id == null ? LuaValue.NIL : LuaValue.valueOf(id); + } + }); + + table().set("all", new OneArgFunction() { + @Override + public LuaValue call(LuaValue css) { + return toLuaArray(host.querySelectorAll(css.checkjstring())); + } + }); + + table().set("matches", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue css) { + return LuaValue.valueOf(host.matches(id.checkjstring(), css.checkjstring())); + } + }); + + table().set("closest", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue id, LuaValue css) { + String out = host.closest(id.checkjstring(), css.checkjstring()); + return out == null ? LuaValue.NIL : LuaValue.valueOf(out); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/StorageTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/StorageTable.java new file mode 100644 index 0000000..e4efa01 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/StorageTable.java @@ -0,0 +1,121 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaTable; +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.*; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.hosts.StorageHost; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +import java.util.List; + +/** + * Lua table: storage + * + *

Functions:

+ *
    + *
  • storage.localGet(key) -> string|nil
  • + *
  • storage.localSet(key, value|nil)
  • + *
  • storage.localKeys() -> {keys...}
  • + *
  • storage.localRemove(key)
  • + *
  • storage.localClear()
  • + *
  • same for session*
  • + *
+ */ +public final class StorageTable extends ScriptTable { + + public StorageTable() { + super("storage"); + } + + private static LuaValue toLuaArray(List values) { + LuaTable t = new LuaTable(); + if (values == null || values.isEmpty()) return t; + int i = 1; + for (String v : values) t.set(i++, v == null ? LuaValue.NIL : LuaValue.valueOf(v)); + return t; + } + + @Override + protected void define(HostServices services) { + StorageHost host = services.storage().orElseThrow(() -> new IllegalStateException("StorageHost not provided")); + + table().set("localKeys", new ZeroArgFunction() { + @Override + public LuaValue call() { + return toLuaArray(host.localKeys()); + } + }); + + table().set("localGet", new OneArgFunction() { + @Override + public LuaValue call(LuaValue key) { + String v = host.localGet(key.checkjstring()); + return v == null ? LuaValue.NIL : LuaValue.valueOf(v); + } + }); + + table().set("localSet", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue key, LuaValue value) { + host.localSet(key.checkjstring(), value.isnil() ? null : value.tojstring()); + return LuaValue.NIL; + } + }); + + table().set("localRemove", new OneArgFunction() { + @Override + public LuaValue call(LuaValue key) { + host.localRemove(key.checkjstring()); + return LuaValue.NIL; + } + }); + + table().set("localClear", new ZeroArgFunction() { + @Override + public LuaValue call() { + host.localClear(); + return LuaValue.NIL; + } + }); + + table().set("sessionKeys", new ZeroArgFunction() { + @Override + public LuaValue call() { + return toLuaArray(host.sessionKeys()); + } + }); + + table().set("sessionGet", new OneArgFunction() { + @Override + public LuaValue call(LuaValue key) { + String v = host.sessionGet(key.checkjstring()); + return v == null ? LuaValue.NIL : LuaValue.valueOf(v); + } + }); + + table().set("sessionSet", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue key, LuaValue value) { + host.sessionSet(key.checkjstring(), value.isnil() ? null : value.tojstring()); + return LuaValue.NIL; + } + }); + + table().set("sessionRemove", new OneArgFunction() { + @Override + public LuaValue call(LuaValue key) { + host.sessionRemove(key.checkjstring()); + return LuaValue.NIL; + } + }); + + table().set("sessionClear", new ZeroArgFunction() { + @Override + public LuaValue call() { + host.sessionClear(); + return LuaValue.NIL; + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/UtilTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/UtilTable.java new file mode 100644 index 0000000..43e51e7 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/UtilTable.java @@ -0,0 +1,83 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.*; +import org.openautonomousconnection.luascript.events.JavaToLua; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.hosts.UtilHost; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +/** + * Lua table: util + * + *

Functions:

+ *
    + *
  • util.base64Encode(text)
  • + *
  • util.base64Decode(b64)
  • + *
  • util.randomHex(bytes)
  • + *
  • util.parseUrl(url) -> map
  • + *
  • util.parseQuery(query) -> map(key->array)
  • + *
  • util.jsonStringifyExpr(elementId, jsExpr) -> jsonString
  • + *
  • util.jsonNormalize(elementId, json) -> jsonString
  • + *
+ */ +public final class UtilTable extends ScriptTable { + + public UtilTable() { + super("util"); + } + + @Override + protected void define(HostServices services) { + UtilHost host = services.util().orElseThrow(() -> new IllegalStateException("UtilHost not provided")); + + table().set("base64Encode", new OneArgFunction() { + @Override + public LuaValue call(LuaValue text) { + return LuaValue.valueOf(host.base64Encode(text.isnil() ? "" : text.tojstring())); + } + }); + + table().set("base64Decode", new OneArgFunction() { + @Override + public LuaValue call(LuaValue b64) { + return LuaValue.valueOf(host.base64Decode(b64.isnil() ? "" : b64.tojstring())); + } + }); + + table().set("randomHex", new OneArgFunction() { + @Override + public LuaValue call(LuaValue bytes) { + return LuaValue.valueOf(host.randomHex(bytes.checkint())); + } + }); + + table().set("parseUrl", new OneArgFunction() { + @Override + public LuaValue call(LuaValue url) { + return JavaToLua.coerce(host.parseUrl(url.checkjstring())); + } + }); + + table().set("parseQuery", new OneArgFunction() { + @Override + public LuaValue call(LuaValue q) { + return JavaToLua.coerce(host.parseQuery(q.isnil() ? "" : q.tojstring())); + } + }); + + table().set("jsonStringifyExpr", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue elementId, LuaValue jsExpr) { + return LuaValue.valueOf(host.jsonStringifyExpr(elementId.checkjstring(), jsExpr.checkjstring())); + } + }); + + table().set("jsonNormalize", new TwoArgFunction() { + @Override + public LuaValue call(LuaValue elementId, LuaValue json) { + return LuaValue.valueOf(host.jsonNormalize(elementId.checkjstring(), json.checkjstring())); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/values/LuaObject.java b/src/main/java/org/openautonomousconnection/luascript/values/LuaObject.java deleted file mode 100644 index d5189cb..0000000 --- a/src/main/java/org/openautonomousconnection/luascript/values/LuaObject.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.openautonomousconnection.luascript.values; - -import org.luaj.vm2.LuaValue; - -public class LuaObject extends LuaValue { - @Override - public int type() { - return 0; - } - - @Override - public String typename() { - return ""; - } -}