Many new things

This commit is contained in:
UnlegitDqrk
2026-02-28 17:39:42 +01:00
parent a84c626416
commit a9b0ccb8a7
30 changed files with 2490 additions and 150 deletions

49
.idea/misc.xml generated
View File

@@ -8,6 +8,55 @@
</list> </list>
</option> </option>
</component> </component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
<option name="myOrdered" value="false" />
<option name="myNullables">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="5" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="11" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="14" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="15" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="16">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="2" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="3" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="5" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="11" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
<item index="13" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="14" class="java.lang.String" itemvalue="lombok.NonNull" />
<item index="15" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>

View File

@@ -6,7 +6,7 @@
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>LuaScript</artifactId> <artifactId>LuaScript</artifactId>
<version>0.0.0-STABLE.1.3</version> <version>0.0.0-STABLE.1.4</version>
<organization> <organization>
<name>Open Autonomous Connection</name> <name>Open Autonomous Connection</name>

View File

@@ -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<String> out = new AtomicReference<>("");
FxThreadBridge.runAndWait(() -> {
String s = Clipboard.getSystemClipboard().getString();
out.set(s == null ? "" : s);
});
return out.get();
}
}

View File

@@ -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<String, String> getComputedStyles(String elementId, String[] properties) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(properties, "properties");
Map<String, String> 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());
}
}

View File

@@ -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<String, Object> 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<String, Object> 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);
}));
}
}

View File

@@ -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.
*
* <p>Requires JavaScript enabled (bridge only).</p>
*/
public final class FxObserverHost implements ObserverHost {
private final WebEngine engine;
private final FxDomHost dom;
private volatile ObserverCallback callback;
private final ConcurrentHashMap<String, Boolean> mutationObserved = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Boolean> resizeObserved = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Boolean> 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<String, Object> 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);
}
}
}
}

View File

@@ -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<Long, ScheduledFuture<?>> scheduled = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Runnable> rafCallbacks = new ConcurrentHashMap<>();
private final Set<Long> 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<Long, Runnable> snap = new ConcurrentHashMap<>(rafCallbacks);
rafCallbacks.clear();
for (Map.Entry<Long, Runnable> 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();
}
}

View File

@@ -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<String> 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<els.length;i++){"
+ " var el=els[i];"
+ " if(!el.id){ el.id='__auto_' + Math.floor(Math.random()*1e18).toString(36); }"
+ " out.push(el.id);"
+ " }"
+ " return out;"
+ "})();";
Object ret = engine.executeScript(script);
return FxWebBridge.toStringList(ret);
}));
}
@Override
public boolean matches(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);"
+ " 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);
}));
}
}

View File

@@ -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<String> 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<String> 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<String> 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<s.length;i++){ out.push(String(s.key(i))); }"
+ " return out;"
+ "})();";
Object ret = engine.executeScript(script);
return FxWebBridge.toStringList(ret);
}));
}
private String get(String storageName, String key) {
Objects.requireNonNull(key, "key");
return FxThreadBridge.callAndWait(() -> 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);
}));
}
}

View File

@@ -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<String, String> parseUrl(String url) {
Objects.requireNonNull(url, "url");
URI u = URI.create(url.trim());
Map<String, String> 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<String, List<String>> parseQuery(String query) {
String q = query == null ? "" : query.trim();
if (q.startsWith("?")) q = q.substring(1);
Map<String, List<String>> 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);
}
}

View File

@@ -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 <T> result type
* @return action result
*/
public static <T> T callWithJs(WebEngine engine, Supplier<T> 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.
*
* <p>Supported: null, String, Boolean, Number (finite).</p>
*
* @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<String> toStringList(Object jsValue) {
if (jsValue == null) return List.of();
if (jsValue instanceof List<?> list) {
List<String> 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<String> 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<String, Object> toStringObjectMap(Object jsValue) {
if (jsValue == null) return Map.of();
if (jsValue instanceof Map<?, ?> map) {
Map<String, Object> 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();
}
}

View File

@@ -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();
}

View File

@@ -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<String, String> 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);
}

View File

@@ -0,0 +1,42 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.Map;
/**
* Layout and scrolling related accessors.
*
* <p>All results are plain maps to keep dependencies minimal.</p>
*/
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<String, Object> getBoundingClientRect(String elementId);
/**
* Returns viewport metrics.
*
* @return map: width,height,devicePixelRatio,scrollX,scrollY
*/
Map<String, Object> 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);
}

View File

@@ -10,62 +10,25 @@ import java.util.Optional;
*/ */
public interface HostServices { public interface HostServices {
/**
* Returns an optional UI host.
*
* @return ui host
*/
Optional<UiHost> ui(); Optional<UiHost> ui();
/**
* Returns an optional DOM host.
*
* @return dom host
*/
Optional<DomHost> dom(); Optional<DomHost> dom();
/**
* Returns an optional event host.
*
* @return event host
*/
Optional<EventHost> events(); Optional<EventHost> events();
/**
* Returns an optional resource host.
*
* @return resource host
*/
Optional<ResourceHost> resources(); Optional<ResourceHost> resources();
/**
* Returns an optional console host.
*
* @return console host
*/
Optional<ConsoleHost> console(); Optional<ConsoleHost> console();
/**
* Returns an optional audio host.
*
* @return audio host
*/
Optional<AudioHost> audio(); Optional<AudioHost> audio();
/**
* Returns an optional image host.
*
* @return image host
*/
Optional<ImageHost> image(); Optional<ImageHost> image();
/**
* Returns an optional video host.
*
* @return video host
*/
Optional<VideoHost> video(); Optional<VideoHost> video();
/* NEW */
Optional<SchedulerHost> scheduler();
Optional<SelectorHost> selector();
Optional<GeometryHost> geometry();
Optional<CssHost> css();
Optional<StorageHost> storage();
Optional<UtilHost> util();
Optional<ClipboardHost> clipboard();
Optional<ObserverHost> observers();
/** /**
* Simple immutable implementation. * Simple immutable implementation.
*/ */
@@ -80,18 +43,15 @@ public interface HostServices {
private final ImageHost imageHost; private final ImageHost imageHost;
private final VideoHost videoHost; private final VideoHost videoHost;
/** private final SchedulerHost schedulerHost;
* Creates a HostServices container. private final SelectorHost selectorHost;
* private final GeometryHost geometryHost;
* @param ui ui host private final CssHost cssHost;
* @param dom dom host private final StorageHost storageHost;
* @param events event host private final UtilHost utilHost;
* @param resources resource host private final ClipboardHost clipboardHost;
* @param console console host private final ObserverHost observerHost;
* @param audioHost audio host
* @param imageHost image host
* @param videoHost video host
*/
public Default( public Default(
UiHost ui, UiHost ui,
DomHost dom, DomHost dom,
@@ -100,7 +60,15 @@ public interface HostServices {
ConsoleHost console, ConsoleHost console,
AudioHost audioHost, AudioHost audioHost,
ImageHost imageHost, 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.ui = ui;
this.dom = dom; this.dom = dom;
@@ -110,47 +78,33 @@ public interface HostServices {
this.audioHost = audioHost; this.audioHost = audioHost;
this.imageHost = imageHost; this.imageHost = imageHost;
this.videoHost = videoHost; 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 @Override public Optional<UiHost> ui() { return Optional.ofNullable(ui); }
public Optional<UiHost> ui() { @Override public Optional<DomHost> dom() { return Optional.ofNullable(dom); }
return Optional.ofNullable(ui); @Override public Optional<EventHost> events() { return Optional.ofNullable(events); }
} @Override public Optional<ResourceHost> resources() { return Optional.ofNullable(resources); }
@Override public Optional<ConsoleHost> console() { return Optional.ofNullable(console); }
@Override public Optional<AudioHost> audio() { return Optional.ofNullable(audioHost); }
@Override public Optional<ImageHost> image() { return Optional.ofNullable(imageHost); }
@Override public Optional<VideoHost> video() { return Optional.ofNullable(videoHost); }
@Override @Override public Optional<SchedulerHost> scheduler() { return Optional.ofNullable(schedulerHost); }
public Optional<DomHost> dom() { @Override public Optional<SelectorHost> selector() { return Optional.ofNullable(selectorHost); }
return Optional.ofNullable(dom); @Override public Optional<GeometryHost> geometry() { return Optional.ofNullable(geometryHost); }
} @Override public Optional<CssHost> css() { return Optional.ofNullable(cssHost); }
@Override public Optional<StorageHost> storage() { return Optional.ofNullable(storageHost); }
@Override @Override public Optional<UtilHost> util() { return Optional.ofNullable(utilHost); }
public Optional<EventHost> events() { @Override public Optional<ClipboardHost> clipboard() { return Optional.ofNullable(clipboardHost); }
return Optional.ofNullable(events); @Override public Optional<ObserverHost> observers() { return Optional.ofNullable(observerHost); }
}
@Override
public Optional<ResourceHost> resources() {
return Optional.ofNullable(resources);
}
@Override
public Optional<ConsoleHost> console() {
return Optional.ofNullable(console);
}
@Override
public Optional<AudioHost> audio() {
return Optional.ofNullable(audioHost);
}
@Override
public Optional<ImageHost> image() {
return Optional.ofNullable(imageHost);
}
@Override
public Optional<VideoHost> video() {
return Optional.ofNullable(videoHost);
}
} }
/** /**
@@ -159,11 +113,6 @@ public interface HostServices {
final class StdoutConsole implements ConsoleHost { final class StdoutConsole implements ConsoleHost {
private final String prefix; private final String prefix;
/**
* Creates a new stdout console with a prefix.
*
* @param prefix prefix (may be empty)
*/
public StdoutConsole(String prefix) { public StdoutConsole(String prefix) {
this.prefix = Objects.requireNonNull(prefix, "prefix"); this.prefix = Objects.requireNonNull(prefix, "prefix");
} }
@@ -172,29 +121,10 @@ public interface HostServices {
return s == null ? "" : s; return s == null ? "" : s;
} }
@Override @Override public void info(String message) { System.out.println(prefix + "[info] " + safe(message)); }
public void info(String message) { @Override public void log(String message) { System.out.println(prefix + "[log] " + safe(message)); }
System.out.println(prefix + "[info] " + 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 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));
}
} }
} }

View File

@@ -0,0 +1,80 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.Map;
/**
* Reactive observers: Mutation/Resize/Intersection.
*
* <p>Observers emit events into Lua via a host-provided callback.</p>
*/
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<String, Object> 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);
}

View File

@@ -0,0 +1,53 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Scheduling primitives comparable to JavaScript timers.
*
* <p>No networking. This host only provides time-based callbacks.</p>
*/
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.
*
* <p>Comparable to requestAnimationFrame. The callback is invoked once.</p>
*
* @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);
}

View File

@@ -0,0 +1,45 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.List;
/**
* CSS selector based DOM querying/traversal.
*
* <p>All returned elements are identified by stable element ids.</p>
*/
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<String> 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);
}

View File

@@ -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<String> 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<String> 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();
}

View File

@@ -0,0 +1,70 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.List;
import java.util.Map;
/**
* Utility helpers commonly available in browsers.
*
* <p>No networking.</p>
*/
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<String, String> parseUrl(String url);
/**
* Parses a query string into key->list(values).
*
* @param query query string (with or without leading '?')
* @return map key->values
*/
Map<String, List<String>> 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);
}

View File

@@ -80,14 +80,27 @@ public final class FxLuaScriptEngine implements AutoCloseable {
.sandbox(true) .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); FxEventHost eventHost = new FxEventHost(dom);
FxAudioHost audioHost = new FxAudioHost();
FxVideoHost videoHost = new FxVideoHost(engine, dom); HostServices services = new HostServices.Default(
FxImageHost imageHost = new FxImageHost(engine, dom); new FxUiHost(engine, dom),
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost, imageHost, videoHost); 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); LuaRuntime rt = new LuaRuntime(globals, services, policy);
eventHost.setRouter(rt.eventRouter()); eventHost.setRouter(rt.eventRouter());

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>clipboard.set(text)</li>
* <li>clipboard.get() -> text</li>
* </ul>
*/
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());
}
});
}
}

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>css.computed(id, prop) -> string</li>
* <li>css.computedMany(id, {props...}) -> map</li>
* <li>css.inlineGet(id, prop) -> string</li>
* <li>css.inlineSet(id, prop, value)</li>
* <li>css.varGet(id, name) -> string</li>
* <li>css.varSet(id, name, value)</li>
* </ul>
*/
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<String, String> 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;
}
});
}
}

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>geometry.rect(id) -> map</li>
* <li>geometry.viewport() -> map</li>
* <li>geometry.scrollTo(x,y)</li>
* <li>geometry.scrollIntoView(id, align?)</li>
* </ul>
*/
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<String, Object> 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;
}
});
}
}

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>observers.on(fn(type, targetId, dataTable))</li>
* <li>observers.mutationObserve(id, subtree, attributes, childList, characterData)</li>
* <li>observers.mutationUnobserve(id)</li>
* <li>observers.resizeObserve(id)</li>
* <li>observers.resizeUnobserve(id)</li>
* <li>observers.intersectionObserve(id, threshold)</li>
* <li>observers.intersectionUnobserve(id)</li>
* </ul>
*/
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;
}
});
}
}

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>scheduler.timeout(ms, fn) -> id</li>
* <li>scheduler.interval(ms, fn) -> id</li>
* <li>scheduler.clear(id) -> boolean</li>
* <li>scheduler.raf(fn) -> id</li>
* <li>scheduler.cancelRaf(id) -> boolean</li>
* </ul>
*/
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()));
}
});
}
}

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>selector.one(css) -> id|nil</li>
* <li>selector.all(css) -> { ids... }</li>
* <li>selector.matches(id, css) -> boolean</li>
* <li>selector.closest(id, css) -> id|nil</li>
* </ul>
*/
public final class SelectorTable extends ScriptTable {
public SelectorTable() {
super("selector");
}
private static LuaValue toLuaArray(List<String> 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);
}
});
}
}

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>storage.localGet(key) -> string|nil</li>
* <li>storage.localSet(key, value|nil)</li>
* <li>storage.localKeys() -> {keys...}</li>
* <li>storage.localRemove(key)</li>
* <li>storage.localClear()</li>
* <li>same for session*</li>
* </ul>
*/
public final class StorageTable extends ScriptTable {
public StorageTable() {
super("storage");
}
private static LuaValue toLuaArray(List<String> 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;
}
});
}
}

View File

@@ -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
*
* <p>Functions:</p>
* <ul>
* <li>util.base64Encode(text)</li>
* <li>util.base64Decode(b64)</li>
* <li>util.randomHex(bytes)</li>
* <li>util.parseUrl(url) -> map</li>
* <li>util.parseQuery(query) -> map(key->array)</li>
* <li>util.jsonStringifyExpr(elementId, jsExpr) -> jsonString</li>
* <li>util.jsonNormalize(elementId, json) -> jsonString</li>
* </ul>
*/
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()));
}
});
}
}

View File

@@ -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 "";
}
}