4 Commits
dev ... main

Author SHA1 Message Date
UnlegitDqrk
e795aa3041 Small fixes 2026-02-28 17:52:10 +01:00
UnlegitDqrk
9338865185 Small fixes 2026-02-28 17:42:18 +01:00
UnlegitDqrk
a9b0ccb8a7 Many new things 2026-02-28 17:39:42 +01:00
UnlegitDqrk
a84c626416 Added image/video control and property manipulation 2026-02-28 16:56:30 +01:00
47 changed files with 4152 additions and 167 deletions

49
.idea/misc.xml generated
View File

@@ -8,6 +8,55 @@
</list>
</option>
</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">
<output url="file://$PROJECT_DIR$/out" />
</component>

21
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>org.openautonomousconnection</groupId>
<artifactId>LuaScript</artifactId>
<version>0.0.0-STABLE.1.3</version>
<version>0.0.0-STABLE.1.4</version>
<organization>
<name>Open Autonomous Connection</name>
@@ -16,8 +16,8 @@
<description>The default DNS-Server</description>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -66,16 +66,9 @@
<repositories>
<repository>
<id>unlegitdqrk</id>
<id>repounlegitdqrk</id>
<url>https://repo.unlegitdqrk.dev/api/packages/UnlegitDqrk/maven</url>
</repository>
<repository>
<id>oac</id>
<url>https://repo.open-autonomous-connection.org/api/packages/open-autonomous-connection/maven</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
@@ -130,13 +123,17 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.3</version>
<configuration>
<failOnError>false</failOnError>
<failOnError>true</failOnError>
<failOnWarnings>false</failOnWarnings>
<doclint>none</doclint>
<locale>en_US</locale>
<encoding>UTF-8</encoding>
<docencoding>UTF-8</docencoding>
<charset>UTF-8</charset>
<additionalOptions>
<additionalOption>--allow-script-in-comments</additionalOption>
</additionalOptions>
</configuration>
<executions>
<execution>

View File

@@ -3,6 +3,8 @@ package org.openautonomousconnection.luascript.events;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -17,12 +19,18 @@ public final class JavaToLua {
public static LuaValue coerce(Object v) {
if (v == null) return LuaValue.NIL;
if (v instanceof LuaValue lv) return lv;
if (v instanceof String s) return LuaValue.valueOf(s);
if (v instanceof Boolean b) return LuaValue.valueOf(b);
if (v instanceof Integer i) return LuaValue.valueOf(i);
if (v instanceof Long l) return LuaValue.valueOf(l);
if (v instanceof Float f) return LuaValue.valueOf(f);
if (v instanceof Double d) return LuaValue.valueOf(d);
if (v instanceof Byte n) return LuaValue.valueOf(n.intValue());
if (v instanceof Short n) return LuaValue.valueOf(n.intValue());
if (v instanceof Integer n) return LuaValue.valueOf(n);
if (v instanceof Long n) return LuaValue.valueOf(n);
if (v instanceof Float n) return LuaValue.valueOf(n.doubleValue());
if (v instanceof Double n) return LuaValue.valueOf(n);
if (v instanceof Number n) return LuaValue.valueOf(n.doubleValue());
if (v instanceof Map<?, ?> m) {
LuaTable t = new LuaTable();
@@ -43,6 +51,24 @@ public final class JavaToLua {
return t;
}
if (v instanceof Collection<?> col) {
LuaTable t = new LuaTable();
int i = 1;
for (Object o : col) {
t.set(i++, coerce(o));
}
return t;
}
if (v.getClass().isArray()) {
LuaTable t = new LuaTable();
int len = Array.getLength(v);
for (int i = 0; i < len; i++) {
t.set(i + 1, coerce(Array.get(v, i)));
}
return t;
}
return LuaValue.valueOf(String.valueOf(v));
}
}
}

View File

@@ -0,0 +1,212 @@
package org.openautonomousconnection.luascript.fx;
import javafx.application.Platform;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import org.openautonomousconnection.luascript.hosts.AudioHost;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
/**
* JavaFX MediaPlayer based AudioHost.
*
* <p>Note: JavaFX Media does not reliably use {@link java.net.URLStreamHandler} for custom schemes.
* Therefore, for {@code web://} this host resolves via {@link URLConnection} (your installed handler),
* spools to a temporary file and plays that file via {@code file://}.</p>
*/
public final class FxAudioHost implements AudioHost {
private static final Set<String> AUDIO_EXTENSIONS = Set.of(
"mp3", "wav", "ogg", "m4a", "opus", "web"
);
private volatile MediaPlayer player;
private volatile boolean loop;
private volatile double volume = 1.0;
private volatile Path lastTempFile;
@Override
public void playFile(File file) {
Objects.requireNonNull(file, "file");
if (!file.isFile()) {
throw new IllegalArgumentException("Audio file not found: " + file.getAbsolutePath());
}
String ext = MediaExtensions.extensionOf(file.getName());
ensureAllowedAudioExtension(ext, file.getName());
playUri(file.toURI());
}
@Override
public void playUrl(String url) {
Objects.requireNonNull(url, "url");
String u = url.trim();
if (u.isEmpty()) throw new IllegalArgumentException("URL is empty");
URI uri = URI.create(u);
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
String ext = MediaExtensions.extensionOf(uri.getPath());
ensureAllowedAudioExtension(ext, uri.getPath());
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
playUri(uri);
return;
}
if ("web".equals(scheme)) {
Path tmp = null;
try {
tmp = downloadToTempFile(u, ext);
rememberTemp(tmp);
playUri(tmp.toUri());
return;
} catch (IOException e) {
safeDelete(tmp);
throw new RuntimeException("Failed to load web audio: " + e.getMessage(), e);
}
}
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
}
@Override
public void pause() {
Platform.runLater(() -> {
MediaPlayer p = player;
if (p != null) p.pause();
});
}
@Override
public void stop() {
Platform.runLater(this::stopInternal);
}
@Override
public void setVolume(double volume) {
double v = clamp01(volume);
this.volume = v;
Platform.runLater(() -> {
MediaPlayer p = player;
if (p != null) p.setVolume(v);
});
}
@Override
public void setLoop(boolean loop) {
this.loop = loop;
Platform.runLater(() -> {
MediaPlayer p = player;
if (p != null) p.setCycleCount(loop ? MediaPlayer.INDEFINITE : 1);
});
}
private void playUri(URI uri) {
Platform.runLater(() -> {
stopInternal();
Media media = new Media(uri.toString());
MediaPlayer p = new MediaPlayer(media);
p.setCycleCount(loop ? MediaPlayer.INDEFINITE : 1);
p.setVolume(volume);
p.setOnEndOfMedia(() -> {
if (!loop) {
try {
p.dispose();
} catch (Exception ignored) {
// ignore
}
}
});
this.player = p;
p.play();
});
}
private void stopInternal() {
MediaPlayer p = this.player;
this.player = null;
if (p != null) {
try {
p.stop();
} catch (Exception ignored) {
// ignore
}
try {
p.dispose();
} catch (Exception ignored) {
// ignore
}
}
Path tmp = this.lastTempFile;
this.lastTempFile = null;
safeDelete(tmp);
}
private static Path downloadToTempFile(String url, String ext) throws IOException {
URL u = new URL(url);
URLConnection con = u.openConnection();
con.setUseCaches(false);
String safeExt = (ext == null || ext.isBlank()) ? "bin" : ext.toLowerCase(Locale.ROOT);
Path tmp = Files.createTempFile("oac-audio-", "." + safeExt);
try (InputStream in = con.getInputStream()) {
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
safeDelete(tmp);
throw e;
}
Files.write(tmp, new byte[0], StandardOpenOption.APPEND);
return tmp;
}
private void rememberTemp(Path tmp) {
Path prev = this.lastTempFile;
this.lastTempFile = tmp;
safeDelete(prev);
}
private static void safeDelete(Path p) {
if (p == null) return;
try {
Files.deleteIfExists(p);
} catch (IOException ignored) {
// best-effort
}
}
private static double clamp01(double v) {
if (v < 0.0) return 0.0;
if (v > 1.0) return 1.0;
return v;
}
private static void ensureAllowedAudioExtension(String ext, String source) {
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
if (e.isEmpty() || !AUDIO_EXTENSIONS.contains(e)) {
throw new IllegalArgumentException("Unsupported audio format '" + e + "' for: " + source);
}
}
}

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

@@ -10,7 +10,9 @@ import java.util.concurrent.atomic.AtomicLong;
/**
* DomHost implementation backed by JavaFX WebView's W3C DOM (WebEngine#getDocument()).
*
* <p>No jsoup and no JavaScript. All operations are performed via W3C DOM APIs.</p>
* <p>Uses W3C DOM for structure/attributes and a small JavaScript bridge for DOM properties and method calls
* (required for HTMLMediaElement like video/audio). Lua remains the scripting language; JavaScript is only
* used internally to access DOM properties/methods that are not available via W3C DOM APIs.</p>
*
* <p>Element identity is the {@code id} attribute. This host auto-assigns stable ids to elements that
* do not have one, ensuring addressability for Lua bindings and event routing.</p>
@@ -77,6 +79,24 @@ public final class FxDomHost implements DomHost {
return v;
}
private static String requireId(String id) {
if (id == null) throw new IllegalArgumentException("elementId is null");
String v = id.trim();
if (v.isEmpty()) throw new IllegalArgumentException("elementId is blank");
return v;
}
private 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");
// Prevent JS injection: only simple identifier.
if (!v.matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) {
throw new IllegalArgumentException(label + " must be a JS identifier: " + v);
}
return v;
}
/**
* Ensures every element has a stable id.
*/
@@ -314,6 +334,77 @@ public final class FxDomHost implements DomHost {
});
}
@Override
public void setProperty(String elementId, String property, Object value) {
final String id = requireId(elementId);
final String prop = requireJsIdentifier(property, "property");
final String jsValue = toJsLiteral(value);
FxThreadBridge.runAndWait(() -> {
requireDocument();
ensureJsEnabled();
String script = ""
+ "(function(){"
+ " var el = document.getElementById(" + toJsLiteral(id) + ");"
+ " if(!el) throw new Error('Unknown element id: ' + " + toJsLiteral(id) + ");"
+ " el[" + toJsLiteral(prop) + "] = " + jsValue + ";"
+ " return null;"
+ "})();";
engine.executeScript(script);
engine.setJavaScriptEnabled(false);
});
}
@Override
public Object getProperty(String elementId, String property) {
final String id = requireId(elementId);
final String prop = requireJsIdentifier(property, "property");
return FxThreadBridge.callAndWait(() -> {
requireDocument();
ensureJsEnabled();
String script = ""
+ "(function(){"
+ " var el = document.getElementById(" + toJsLiteral(id) + ");"
+ " if(!el) throw new Error('Unknown element id: ' + " + toJsLiteral(id) + ");"
+ " return el[" + toJsLiteral(prop) + "];"
+ "})();";
Object ret = engine.executeScript(script);
engine.setJavaScriptEnabled(false);
return ret;
});
}
@Override
public Object call(String elementId, String method, Object... args) {
final String id = requireId(elementId);
final String m = requireJsIdentifier(method, "method");
final Object[] safeArgs = (args == null) ? new Object[0] : args.clone();
final String argvLiteral = toJsArrayLiteral(safeArgs);
return FxThreadBridge.callAndWait(() -> {
requireDocument();
ensureJsEnabled();
String script = ""
+ "(function(){"
+ " var el = document.getElementById(" + toJsLiteral(id) + ");"
+ " if(!el) throw new Error('Unknown element id: ' + " + toJsLiteral(id) + ");"
+ " var fn = el[" + toJsLiteral(m) + "];"
+ " if(typeof fn !== 'function') throw new Error('Not a function: ' + " + toJsLiteral(m) + ");"
+ " return fn.apply(el, " + argvLiteral + ");"
+ "})();";
Object ret = engine.executeScript(script);
engine.setJavaScriptEnabled(false);
return ret;
});
}
/**
* Exposes the current document (FX thread access required by callers).
*
@@ -339,6 +430,13 @@ public final class FxDomHost implements DomHost {
return el;
}
private void ensureJsEnabled() {
// Required for HTMLMediaElement and DOM property access beyond W3C DOM.
if (!engine.isJavaScriptEnabled()) {
engine.setJavaScriptEnabled(true);
}
}
private String generateUniqueId(Document doc) {
while (true) {
String id = "__auto_" + autoIdSeq.getAndIncrement();
@@ -353,4 +451,104 @@ public final class FxDomHost implements DomHost {
}
return generateUniqueId(doc);
}
}
/**
* Converts a Java value into a safe JavaScript literal.
*
* <p>Supported:</p>
* <ul>
* <li>null</li>
* <li>String</li>
* <li>Boolean</li>
* <li>Number (finite)</li>
* </ul>
*
* @param v value
* @return JS literal
*/
private 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 Byte || v instanceof Short || v instanceof Integer || v instanceof Long) {
return String.valueOf(((Number) v).longValue());
}
if (v instanceof Float || v instanceof Double) {
double d = ((Number) v).doubleValue();
if (!Double.isFinite(d)) {
throw new IllegalArgumentException("Non-finite number is not supported for JS literal: " + d);
}
// Use plain Java formatting; JS accepts it.
return Double.toString(d);
}
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 arguments.
*
* @param args args (nullable)
* @return JS array literal
*/
private static String toJsArrayLiteral(Object[] args) {
if (args == null || args.length == 0) return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (int i = 0; i < args.length; i++) {
if (i > 0) sb.append(',');
sb.append(toJsLiteral(args[i]));
}
sb.append(']');
return sb.toString();
}
/**
* Escapes a string for inclusion inside a single-quoted JavaScript string literal.
*
* @param s raw string
* @return escaped string
*/
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 -> {
// Keep printable chars; escape other control chars.
if (c < 0x20) {
out.append(String.format("\\u%04x", (int) c));
} else {
out.append(c);
}
}
}
}
return out.toString();
}
}

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,270 @@
package org.openautonomousconnection.luascript.fx;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.hosts.ImageHost;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
/**
* ImageHost implementation that renders images inside the WebView DOM using an {@code <img>} element.
*
* <p>No JavaFX overlay panes are used. Positioning is done by setting CSS via {@code style} attribute.</p>
*
* <p>For {@code web://} URLs this host downloads the resource via {@link URLConnection} (your installed handler),
* stores it as a temporary file, then sets {@code src=file://...} because WebView won't reliably load custom schemes.</p>
*/
public final class FxImageHost implements ImageHost {
private static final Set<String> IMAGE_EXTENSIONS = Set.of(
"png", "jpg", "jpeg", "ico", "bmp", "avif", "heif", "heic", "webp"
);
private static final String IMG_ID = "__oac_image";
private final WebEngine engine;
private final FxDomHost dom;
private volatile double x;
private volatile double y;
private volatile double w = 320;
private volatile double h = 240;
private volatile boolean preserveRatio = true;
private volatile boolean smooth = true;
private volatile double opacity = 1.0;
private volatile Path lastTempFile;
/**
* Creates a new image host.
*
* @param engine web engine
* @param dom fx dom host
*/
public FxImageHost(WebEngine engine, FxDomHost dom) {
this.engine = Objects.requireNonNull(engine, "engine");
this.dom = Objects.requireNonNull(dom, "dom");
// Ensure element exists once document is present; if not yet loaded, calls will create on demand.
}
@Override
public void showFile(File file) {
Objects.requireNonNull(file, "file");
if (!file.isFile()) {
throw new IllegalArgumentException("Image file not found: " + file.getAbsolutePath());
}
String ext = MediaExtensions.extensionOf(file.getName());
ensureAllowed(ext, "image", file.getName());
URI uri = file.toURI();
setSource(uri.toString(), null);
}
@Override
public void showUrl(String url) {
Objects.requireNonNull(url, "url");
String u = url.trim();
if (u.isEmpty()) throw new IllegalArgumentException("URL is empty");
URI uri = URI.create(u);
String ext = MediaExtensions.extensionOf(uri.getPath());
ensureAllowed(ext, "image", uri.getPath());
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
setSource(uri.toString(), null);
return;
}
if ("web".equals(scheme)) {
Path tmp = null;
try {
tmp = downloadToTempFile(u, ext, "oac-img-");
rememberTemp(tmp);
setSource(tmp.toUri().toString(), tmp);
return;
} catch (IOException e) {
safeDelete(tmp);
throw new RuntimeException("Failed to load web image: " + e.getMessage(), e);
}
}
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
}
@Override
public void hide() {
FxThreadBridge.runAndWait(() -> {
Element img = ensureImgElement();
img.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
img.removeAttribute("src");
});
Path tmp = this.lastTempFile;
this.lastTempFile = null;
safeDelete(tmp);
}
@Override
public void setRect(double x, double y, double w, double h) {
this.x = x;
this.y = y;
this.w = Math.max(0.0, w);
this.h = Math.max(0.0, h);
applyStyle(true);
}
@Override
public void setPreserveRatio(boolean preserve) {
this.preserveRatio = preserve;
applyStyle(true);
}
@Override
public void setSmooth(boolean smooth) {
this.smooth = smooth;
applyStyle(true);
}
@Override
public void setOpacity(double opacity) {
this.opacity = clamp01(opacity);
applyStyle(true);
}
private void setSource(String src, Path tempFileKept) {
FxThreadBridge.runAndWait(() -> {
Element img = ensureImgElement();
img.setAttribute("src", src);
img.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
});
// Keep temp file reference (so previous gets cleaned up)
if (tempFileKept != null) {
rememberTemp(tempFileKept);
}
}
private void applyStyle(boolean visible) {
FxThreadBridge.runAndWait(() -> {
Element img = ensureImgElement();
String src = img.getAttribute("src");
boolean show = visible && src != null && !src.isBlank();
img.setAttribute("style", mergeStyle(baseStyle(show), rectStyle(), visualStyle()));
});
}
private Element ensureImgElement() {
Document doc = dom.requireDocument();
Element img = doc.getElementById(IMG_ID);
if (img != null) return img;
Element body = (Element) doc.getElementsByTagName("body").item(0);
if (body == null) throw new IllegalStateException("No <body> element available");
img = doc.createElement("img");
img.setAttribute("id", IMG_ID);
img.setAttribute("draggable", "false");
img.setAttribute("alt", "");
body.appendChild(img);
return img;
}
private String baseStyle(boolean visible) {
return "position:fixed;"
+ "left:0;top:0;"
+ "z-index:2147483647;"
+ "display:" + (visible ? "block" : "none") + ";";
}
private String rectStyle() {
return "left:" + x + "px;"
+ "top:" + y + "px;"
+ "width:" + w + "px;"
+ "height:" + h + "px;";
}
private String visualStyle() {
String fit;
if (preserveRatio) {
fit = "object-fit:contain;";
} else {
fit = "object-fit:fill;";
}
String smoothing = smooth ? "image-rendering:auto;" : "image-rendering:pixelated;";
return fit + smoothing + "opacity:" + opacity + ";";
}
private static String mergeStyle(String... parts) {
StringBuilder sb = new StringBuilder(256);
for (String p : parts) {
if (p == null || p.isBlank()) continue;
String s = p.trim();
if (!s.endsWith(";")) s += ";";
sb.append(s);
}
return sb.toString();
}
private static Path downloadToTempFile(String url, String ext, String prefix) throws IOException {
URL u = new URL(url);
URLConnection con = u.openConnection();
con.setUseCaches(false);
String safeExt = (ext == null || ext.isBlank()) ? "bin" : ext.toLowerCase(Locale.ROOT);
Path tmp = Files.createTempFile(prefix, "." + safeExt);
try (InputStream in = con.getInputStream()) {
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
safeDelete(tmp);
throw e;
}
return tmp;
}
private void rememberTemp(Path tmp) {
Path prev = this.lastTempFile;
this.lastTempFile = tmp;
safeDelete(prev);
}
private static void safeDelete(Path p) {
if (p == null) return;
try {
Files.deleteIfExists(p);
} catch (IOException ignored) {
// best-effort
}
}
private static void ensureAllowed(String ext, String kind, String source) {
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
if (e.isEmpty() || !IMAGE_EXTENSIONS.contains(e)) {
throw new IllegalArgumentException("Unsupported " + kind + " format '" + e + "' for: " + source);
}
}
private static double clamp01(double v) {
if (v < 0.0) return 0.0;
if (v > 1.0) return 1.0;
return v;
}
}

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,313 @@
package org.openautonomousconnection.luascript.fx;
import javafx.scene.web.WebEngine;
import org.openautonomousconnection.luascript.hosts.VideoHost;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
/**
* VideoHost implementation that renders video inside the WebView DOM using a {@code <video>} element.
*
* <p>No JavaFX overlay panes are used. Positioning is done by setting CSS via {@code style} attribute.</p>
*
* <p>Playback control is done via {@link FxDomHost#setProperty(String, String, Object)} and
* {@link FxDomHost#call(String, String, Object...)}, i.e. HTMLMediaElement methods.</p>
*
* <p>For {@code web://} URLs this host downloads the resource via {@link URLConnection} (your installed handler),
* stores it as a temporary file, then sets {@code src=file://...} because WebView won't reliably load custom schemes.</p>
*/
public final class FxVideoHost implements VideoHost {
private static final Set<String> VIDEO_EXTENSIONS = Set.of(
"mp4", "mov", "ogg", "ogv", "gif", "gifv", "avi", "m4v"
);
private static final String VIDEO_ID = "__oac_video";
private final WebEngine engine;
private final FxDomHost dom;
private volatile boolean loop;
private volatile double volume = 1.0;
private volatile double x;
private volatile double y;
private volatile double w = 640;
private volatile double h = 360;
private volatile Path lastTempFile;
/**
* Creates a new video host.
*
* @param engine web engine
* @param dom fx dom host
*/
public FxVideoHost(WebEngine engine, FxDomHost dom) {
this.engine = Objects.requireNonNull(engine, "engine");
this.dom = Objects.requireNonNull(dom, "dom");
}
@Override
public void playFile(File file) {
Objects.requireNonNull(file, "file");
if (!file.isFile()) {
throw new IllegalArgumentException("Video file not found: " + file.getAbsolutePath());
}
String ext = MediaExtensions.extensionOf(file.getName());
ensureAllowed(ext, "video", file.getName());
setSource(file.toURI().toString(), null);
resume();
}
@Override
public void playUrl(String url) {
Objects.requireNonNull(url, "url");
String u = url.trim();
if (u.isEmpty()) throw new IllegalArgumentException("URL is empty");
URI uri = URI.create(u);
String ext = MediaExtensions.extensionOf(uri.getPath());
ensureAllowed(ext, "video", uri.getPath());
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
setSource(uri.toString(), null);
resume();
return;
}
if ("web".equals(scheme)) {
Path tmp = null;
try {
tmp = downloadToTempFile(u, ext, "oac-video-");
rememberTemp(tmp);
setSource(tmp.toUri().toString(), tmp);
resume();
return;
} catch (IOException e) {
safeDelete(tmp);
throw new RuntimeException("Failed to load web video: " + e.getMessage(), e);
}
}
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
}
@Override
public void pause() {
FxThreadBridge.runAndWait(() -> {
ensureVideoElement();
dom.call(VIDEO_ID, "pause");
});
}
@Override
public void resume() {
FxThreadBridge.runAndWait(() -> {
Element v = ensureVideoElement();
applyMediaProps();
v.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
dom.call(VIDEO_ID, "play");
});
}
@Override
public void stop() {
FxThreadBridge.runAndWait(() -> {
Element v = ensureVideoElement();
dom.call(VIDEO_ID, "pause");
dom.setProperty(VIDEO_ID, "currentTime", 0);
v.removeAttribute("src");
v.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
});
Path tmp = this.lastTempFile;
this.lastTempFile = null;
safeDelete(tmp);
}
@Override
public void hide() {
FxThreadBridge.runAndWait(() -> {
Element v = ensureVideoElement();
v.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
});
}
@Override
public void setRect(double x, double y, double w, double h) {
this.x = x;
this.y = y;
this.w = Math.max(0.0, w);
this.h = Math.max(0.0, h);
FxThreadBridge.runAndWait(() -> {
Element v = ensureVideoElement();
boolean visible = "block".equalsIgnoreCase(extractDisplay(v.getAttribute("style")));
v.setAttribute("style", mergeStyle(baseStyle(visible), rectStyle(), visualStyle()));
});
}
@Override
public void setVolume(double volume) {
this.volume = clamp01(volume);
FxThreadBridge.runAndWait(this::applyMediaProps);
}
@Override
public void setLoop(boolean loop) {
this.loop = loop;
FxThreadBridge.runAndWait(this::applyMediaProps);
}
@Override
public void seek(double seconds) {
double s = Math.max(0.0, seconds);
FxThreadBridge.runAndWait(() -> {
ensureVideoElement();
dom.setProperty(VIDEO_ID, "currentTime", s);
});
}
private void setSource(String src, Path tempFileKept) {
FxThreadBridge.runAndWait(() -> {
Element v = ensureVideoElement();
v.setAttribute("src", src);
applyMediaProps();
v.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
});
if (tempFileKept != null) {
rememberTemp(tempFileKept);
}
}
private void applyMediaProps() {
ensureVideoElement();
dom.setProperty(VIDEO_ID, "loop", loop);
dom.setProperty(VIDEO_ID, "volume", volume);
}
private Element ensureVideoElement() {
Document doc = dom.requireDocument();
Element v = doc.getElementById(VIDEO_ID);
if (v != null) return v;
Element body = (Element) doc.getElementsByTagName("body").item(0);
if (body == null) throw new IllegalStateException("No <body> element available");
v = doc.createElement("video");
v.setAttribute("id", VIDEO_ID);
// Default: no controls; script can enable via dom.setAttr(id,"controls","controls") if needed.
v.setAttribute("preload", "metadata");
body.appendChild(v);
return v;
}
private String baseStyle(boolean visible) {
return "position:fixed;"
+ "left:0;top:0;"
+ "z-index:2147483647;"
+ "background-color:black;"
+ "display:" + (visible ? "block" : "none") + ";";
}
private String rectStyle() {
return "left:" + x + "px;"
+ "top:" + y + "px;"
+ "width:" + w + "px;"
+ "height:" + h + "px;";
}
private String visualStyle() {
return "object-fit:contain;";
}
private static String mergeStyle(String... parts) {
StringBuilder sb = new StringBuilder(256);
for (String p : parts) {
if (p == null || p.isBlank()) continue;
String s = p.trim();
if (!s.endsWith(";")) s += ";";
sb.append(s);
}
return sb.toString();
}
private static String extractDisplay(String style) {
if (style == null || style.isBlank()) return "";
String[] parts = style.split(";");
for (String part : parts) {
String p = part.trim();
if (p.isEmpty()) continue;
int idx = p.indexOf(':');
if (idx <= 0) continue;
String k = p.substring(0, idx).trim().toLowerCase(Locale.ROOT);
if ("display".equals(k)) return p.substring(idx + 1).trim().toLowerCase(Locale.ROOT);
}
return "";
}
private static Path downloadToTempFile(String url, String ext, String prefix) throws IOException {
URL u = new URL(url);
URLConnection con = u.openConnection();
con.setUseCaches(false);
String safeExt = (ext == null || ext.isBlank()) ? "bin" : ext.toLowerCase(Locale.ROOT);
Path tmp = Files.createTempFile(prefix, "." + safeExt);
try (InputStream in = con.getInputStream()) {
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
safeDelete(tmp);
throw e;
}
return tmp;
}
private void rememberTemp(Path tmp) {
Path prev = this.lastTempFile;
this.lastTempFile = tmp;
safeDelete(prev);
}
private static void safeDelete(Path p) {
if (p == null) return;
try {
Files.deleteIfExists(p);
} catch (IOException ignored) {
// best-effort
}
}
private static void ensureAllowed(String ext, String kind, String source) {
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
if (e.isEmpty() || !VIDEO_EXTENSIONS.contains(e)) {
throw new IllegalArgumentException("Unsupported " + kind + " format '" + e + "' for: " + source);
}
}
private static double clamp01(double v) {
if (v < 0.0) return 0.0;
if (v > 1.0) return 1.0;
return v;
}
}

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,34 @@
package org.openautonomousconnection.luascript.fx;
import java.util.Locale;
/**
* Shared extension helper for media sources.
*/
public final class MediaExtensions {
/**
* Extracts a lower-case extension without dot, or empty string if none.
*
* @param pathOrName path or file name
* @return extension without dot, lower-case, or empty string
*/
public static String extensionOf(String pathOrName) {
if (pathOrName == null) return "";
String s = pathOrName.trim();
if (s.isEmpty()) return "";
int q = s.indexOf('?');
if (q >= 0) s = s.substring(0, q);
int h = s.indexOf('#');
if (h >= 0) s = s.substring(0, h);
int slash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
String name = (slash >= 0) ? s.substring(slash + 1) : s;
int dot = name.lastIndexOf('.');
if (dot < 0 || dot == name.length() - 1) return "";
return name.substring(dot + 1).toLowerCase(Locale.ROOT);
}
}

View File

@@ -2,8 +2,53 @@ package org.openautonomousconnection.luascript.hosts;
import java.io.File;
/**
* Host services for audio playback.
*/
public interface AudioHost {
void play(File audioFile);
/**
* Plays an audio file from the local filesystem.
*
* @param file local audio file
*/
void playFile(File file);
}
/**
* Plays an audio resource identified by a URL string.
*
* <p>Supported schemes depend on the host implementation. Typical schemes:</p>
* <ul>
* <li>http://, https://</li>
* <li>file://</li>
* <li>web:// (custom, via installed URL handler)</li>
* </ul>
*
* @param url URL string
*/
void playUrl(String url);
/**
* Pauses playback (if any).
*/
void pause();
/**
* Stops playback (if any).
*/
void stop();
/**
* Sets volume in range [0.0, 1.0].
*
* @param volume volume
*/
void setVolume(double volume);
/**
* Enables/disables looping.
*
* @param loop true to loop
*/
void setLoop(boolean loop);
}

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

@@ -147,4 +147,19 @@ public interface DomHost {
* @return list of ids (never null)
*/
List<String> queryByClass(String className);
/**
* Sets a DOM property (e.g. video.currentTime, video.volume).
*/
void setProperty(String elementId, String property, Object value);
/**
* Gets a DOM property.
*/
Object getProperty(String elementId, String property);
/**
* Invokes a DOM method (e.g. video.play()).
*/
Object call(String elementId, String method, Object... args);
}

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,42 +10,24 @@ import java.util.Optional;
*/
public interface HostServices {
/**
* Returns an optional UI host.
*
* @return ui host
*/
Optional<UiHost> ui();
/**
* Returns an optional DOM host.
*
* @return dom host
*/
Optional<DomHost> dom();
/**
* Returns an optional event host.
*
* @return event host
*/
Optional<EventHost> events();
/**
* Returns an optional resource host.
*
* @return resource host
*/
Optional<ResourceHost> resources();
/**
* Returns an optional console host.
*
* @return console host
*/
Optional<ConsoleHost> console();
Optional<AudioHost> audio();
Optional<ImageHost> image();
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.
@@ -56,55 +38,73 @@ public interface HostServices {
private final EventHost events;
private final ResourceHost resources;
private final ConsoleHost console;
private final AudioHost audioHost;
/**
* Creates a HostServices container.
*
* @param ui ui host
* @param dom dom host
* @param events event host
* @param resources resource host
* @param console console host
*/
public Default(UiHost ui, DomHost dom, EventHost events, ResourceHost resources, ConsoleHost console, AudioHost audioHost) {
private final AudioHost audioHost;
private final ImageHost imageHost;
private final VideoHost videoHost;
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,
EventHost events,
ResourceHost resources,
ConsoleHost console,
AudioHost audioHost,
ImageHost imageHost,
VideoHost videoHost,
SchedulerHost schedulerHost,
SelectorHost selectorHost,
GeometryHost geometryHost,
CssHost cssHost,
StorageHost storageHost,
UtilHost utilHost,
ClipboardHost clipboardHost,
ObserverHost observerHost
) {
this.ui = ui;
this.dom = dom;
this.events = events;
this.resources = resources;
this.console = console;
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<UiHost> ui() {
return Optional.ofNullable(ui);
}
@Override public Optional<UiHost> ui() { return Optional.ofNullable(ui); }
@Override public Optional<DomHost> dom() { return Optional.ofNullable(dom); }
@Override public Optional<EventHost> events() { return Optional.ofNullable(events); }
@Override public Optional<ResourceHost> resources() { return Optional.ofNullable(resources); }
@Override public Optional<ConsoleHost> console() { return Optional.ofNullable(console); }
@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
public Optional<DomHost> dom() {
return Optional.ofNullable(dom);
}
@Override
public Optional<EventHost> events() {
return Optional.ofNullable(events);
}
@Override
public Optional<ResourceHost> resources() {
return Optional.ofNullable(resources);
}
@Override
public Optional<ConsoleHost> console() {
return Optional.ofNullable(console);
}
@Override
public Optional<AudioHost> audio() {
return Optional.ofNullable(audioHost);
}
@Override public Optional<SchedulerHost> scheduler() { return Optional.ofNullable(schedulerHost); }
@Override public Optional<SelectorHost> selector() { return Optional.ofNullable(selectorHost); }
@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 public Optional<UtilHost> util() { return Optional.ofNullable(utilHost); }
@Override public Optional<ClipboardHost> clipboard() { return Optional.ofNullable(clipboardHost); }
@Override public Optional<ObserverHost> observers() { return Optional.ofNullable(observerHost); }
}
/**
@@ -113,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");
}
@@ -126,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)); }
}
}

View File

@@ -0,0 +1,59 @@
package org.openautonomousconnection.luascript.hosts;
import java.io.File;
/**
* Host services for image rendering in an overlay layer above the WebView.
*/
public interface ImageHost {
/**
* Shows an image from the local filesystem.
*
* @param file image file
*/
void showFile(File file);
/**
* Shows an image from a URL string (http/https/file/web).
*
* @param url url string
*/
void showUrl(String url);
/**
* Hides the current image overlay.
*/
void hide();
/**
* Sets the drawing rectangle (in WebView pixel coordinates).
*
* @param x x
* @param y y
* @param w width
* @param h height
*/
void setRect(double x, double y, double w, double h);
/**
* If true, preserves aspect ratio within the rectangle.
*
* @param preserve preserve aspect ratio
*/
void setPreserveRatio(boolean preserve);
/**
* If true, image is smoothed when scaled.
*
* @param smooth smooth
*/
void setSmooth(boolean smooth);
/**
* Sets opacity in range [0..1].
*
* @param opacity opacity
*/
void setOpacity(double opacity);
}

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

@@ -0,0 +1,74 @@
package org.openautonomousconnection.luascript.hosts;
import java.io.File;
/**
* Host services for video playback in an overlay layer above the WebView.
*/
public interface VideoHost {
/**
* Plays a video from the local filesystem.
*
* @param file video file
*/
void playFile(File file);
/**
* Plays a video from a URL string (http/https/file/web).
*
* @param url url string
*/
void playUrl(String url);
/**
* Pauses playback.
*/
void pause();
/**
* Resumes playback (if paused).
*/
void resume();
/**
* Stops playback and hides the view.
*/
void stop();
/**
* Hides the video view (does not necessarily stop decoding).
*/
void hide();
/**
* Sets the drawing rectangle (in WebView pixel coordinates).
*
* @param x x
* @param y y
* @param w width
* @param h height
*/
void setRect(double x, double y, double w, double h);
/**
* Sets volume in range [0..1].
*
* @param volume volume
*/
void setVolume(double volume);
/**
* Enables/disables looping.
*
* @param loop loop
*/
void setLoop(boolean loop);
/**
* Seeks to a position in seconds.
*
* @param seconds seconds
*/
void seek(double seconds);
}

View File

@@ -3,10 +3,7 @@ package org.openautonomousconnection.luascript.runtime;
import javafx.concurrent.Worker;
import javafx.scene.web.WebEngine;
import org.luaj.vm2.Globals;
import org.openautonomousconnection.luascript.fx.FxDomHost;
import org.openautonomousconnection.luascript.fx.FxEventHost;
import org.openautonomousconnection.luascript.fx.FxUiHost;
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
import org.openautonomousconnection.luascript.fx.*;
import org.openautonomousconnection.luascript.hosts.AudioHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
@@ -52,7 +49,7 @@ public final class FxLuaScriptEngine implements AutoCloseable {
/**
* Installs a load hook that bootstraps Lua when a page finished loading.
*/
public void install() {
public void install(FxVideoHost videoHost, FxImageHost imageHost) {
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
if (newState == Worker.State.SUCCEEDED) {
bootstrapped.set(false);
@@ -83,12 +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);
// TODO: Default implementation or parameter for "audioHost"
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost);
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());

View File

@@ -5,9 +5,7 @@ import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.luascript.security.LuaSecurityManager;
import org.openautonomousconnection.luascript.tables.DomTable;
import org.openautonomousconnection.luascript.tables.EventsTable;
import org.openautonomousconnection.luascript.tables.UiTable;
import org.openautonomousconnection.luascript.tables.*;
import org.openautonomousconnection.luascript.tables.console.ConsoleTable;
import java.util.Objects;
@@ -51,6 +49,9 @@ public final class LuaRuntime implements AutoCloseable {
new ConsoleTable().inject(globals, services, overwrite);
new EventsTable(dispatcher).inject(globals, services, overwrite);
new DomTable().inject(globals, services, overwrite);
new AudioTable().inject(globals, services, overwrite);
new VideoTable().inject(globals, services, overwrite);
new ImageTable().inject(globals, services, overwrite);
}
public void bootstrapFromDom() {

View File

@@ -2,28 +2,90 @@ package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.ZeroArgFunction;
import org.openautonomousconnection.luascript.hosts.AudioHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.io.File;
public class AudioTable extends ScriptTable {
protected AudioTable() {
/**
* Lua table: audio
*
* <p>Functions:</p>
* <ul>
* <li>audio.play(pathOrUrl)</li>
* <li>audio.pause()</li>
* <li>audio.stop()</li>
* <li>audio.volume(v) (0..1)</li>
* <li>audio.loop(boolean)</li>
* </ul>
*/
public final class AudioTable extends ScriptTable {
public AudioTable() {
super("audio");
}
@Override
protected void define(HostServices services) {
AudioHost audioHost = services.audio().orElseThrow(() -> new IllegalStateException("AudioHost not provided"));
AudioHost audioHost = services.audio()
.orElseThrow(() -> new IllegalStateException("AudioHost not provided"));
table().set("play", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
String fileName = arg.checkjstring();
audioHost.play(new File(fileName));
String s = arg.checkjstring();
if (looksLikeUrl(s)) {
audioHost.playUrl(s);
return LuaValue.NIL;
}
audioHost.playFile(new File(s));
return LuaValue.NIL;
}
});
table().set("pause", new ZeroArgFunction() {
@Override
public LuaValue call() {
audioHost.pause();
return LuaValue.NIL;
}
});
table().set("stop", new ZeroArgFunction() {
@Override
public LuaValue call() {
audioHost.stop();
return LuaValue.NIL;
}
});
table().set("volume", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
double v = arg.checkdouble();
if (v < 0.0) v = 0.0;
if (v > 1.0) v = 1.0;
audioHost.setVolume(v);
return LuaValue.NIL;
}
});
table().set("loop", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
audioHost.setLoop(arg.checkboolean());
return LuaValue.NIL;
}
});
}
}
private static boolean looksLikeUrl(String s) {
if (s == null) return false;
int idx = s.indexOf("://");
return idx > 0 && idx < 16;
}
}

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

@@ -1,11 +1,8 @@
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.ThreeArgFunction;
import org.luaj.vm2.lib.TwoArgFunction;
import org.luaj.vm2.lib.ZeroArgFunction;
import org.luaj.vm2.*;
import org.luaj.vm2.lib.*;
import org.openautonomousconnection.luascript.events.JavaToLua;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
@@ -35,6 +32,20 @@ public final class DomTable extends ScriptTable {
return t;
}
private static Object luaToJavaScalar(LuaValue v) {
if (v == null || v.isnil()) return null;
if (v.isboolean()) return v.toboolean();
if (v.isnumber()) {
// Preserve integer when possible; otherwise double.
if (v.isint()) return v.toint();
if (v.islong()) return v.tolong();
return v.todouble();
}
if (v.isstring()) return v.tojstring();
// Reject complex types for safety (no silent assumptions).
throw new LuaError("Unsupported value type for dom.setProp/call argument: " + v.typename());
}
@Override
protected void define(HostServices services) {
DomHost dom = services.dom().orElseThrow(() -> new IllegalStateException("DomHost not provided"));
@@ -125,8 +136,7 @@ public final class DomTable extends ScriptTable {
table().set("children", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
List<String> children = dom.getChildrenIds(id.checkjstring());
return toLuaArray(children);
return toLuaArray(dom.getChildrenIds(id.checkjstring()));
}
});
@@ -178,5 +188,38 @@ public final class DomTable extends ScriptTable {
return LuaValue.NIL;
}
});
table().set("setProp", new ThreeArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue prop, LuaValue value) {
dom.setProperty(id.checkjstring(), prop.checkjstring(), luaToJavaScalar(value));
return LuaValue.NIL;
}
});
table().set("getProp", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue prop) {
Object ret = dom.getProperty(id.checkjstring(), prop.checkjstring());
return JavaToLua.coerce(ret);
}
});
table().set("call", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
String id = args.arg(1).checkjstring();
String method = args.arg(2).checkjstring();
int n = args.narg();
Object[] argv = new Object[Math.max(0, n - 2)];
for (int i = 3; i <= n; i++) {
argv[i - 3] = luaToJavaScalar(args.arg(i));
}
Object ret = dom.call(id, method, argv);
return JavaToLua.coerce(ret);
}
});
}
}
}

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,100 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.VarArgFunction;
import org.luaj.vm2.lib.ZeroArgFunction;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.ImageHost;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.io.File;
/**
* Lua table: image
*
* <p>Functions:</p>
* <ul>
* <li>image.show(pathOrUrl)</li>
* <li>image.hide()</li>
* <li>image.rect(x, y, w, h)</li>
* <li>image.opacity(v)</li>
* <li>image.preserveRatio(boolean)</li>
* <li>image.smooth(boolean)</li>
* </ul>
*/
public final class ImageTable extends ScriptTable {
public ImageTable() {
super("image");
}
@Override
protected void define(HostServices services) {
ImageHost host = services.image()
.orElseThrow(() -> new IllegalStateException("ImageHost not provided"));
table().set("show", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
String src = args.arg(1).checkjstring();
if (looksLikeUrl(src)) host.showUrl(src);
else host.showFile(new File(src));
return LuaValue.NIL;
}
});
table().set("hide", new ZeroArgFunction() {
@Override
public LuaValue call() {
host.hide();
return LuaValue.NIL;
}
});
table().set("rect", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
double x = args.arg(1).checkdouble();
double y = args.arg(2).checkdouble();
double w = args.arg(3).checkdouble();
double h = args.arg(4).checkdouble();
host.setRect(x, y, w, h);
return LuaValue.NIL;
}
});
table().set("opacity", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
double o = args.arg(1).checkdouble();
host.setOpacity(o);
return LuaValue.NIL;
}
});
table().set("preserveRatio", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
boolean b = args.arg(1).checkboolean();
host.setPreserveRatio(b);
return LuaValue.NIL;
}
});
table().set("smooth", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
boolean b = args.arg(1).checkboolean();
host.setSmooth(b);
return LuaValue.NIL;
}
});
}
private static boolean looksLikeUrl(String s) {
if (s == null) return false;
int idx = s.indexOf("://");
return idx > 0 && idx < 16;
}
}

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

@@ -0,0 +1,127 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.VarArgFunction;
import org.luaj.vm2.lib.ZeroArgFunction;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.VideoHost;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import java.io.File;
/**
* Lua table: video
*
* <p>Functions:</p>
* <ul>
* <li>video.play(pathOrUrl)</li>
* <li>video.pause()</li>
* <li>video.resume()</li>
* <li>video.stop()</li>
* <li>video.hide()</li>
* <li>video.rect(x, y, w, h)</li>
* <li>video.volume(v)</li>
* <li>video.loop(boolean)</li>
* <li>video.seek(seconds)</li>
* </ul>
*/
public final class VideoTable extends ScriptTable {
public VideoTable() {
super("video");
}
@Override
protected void define(HostServices services) {
VideoHost host = services.video()
.orElseThrow(() -> new IllegalStateException("VideoHost not provided"));
table().set("play", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
String src = args.arg(1).checkjstring();
if (looksLikeUrl(src)) host.playUrl(src);
else host.playFile(new File(src));
return LuaValue.NIL;
}
});
table().set("pause", new ZeroArgFunction() {
@Override
public LuaValue call() {
host.pause();
return LuaValue.NIL;
}
});
table().set("resume", new ZeroArgFunction() {
@Override
public LuaValue call() {
host.resume();
return LuaValue.NIL;
}
});
table().set("stop", new ZeroArgFunction() {
@Override
public LuaValue call() {
host.stop();
return LuaValue.NIL;
}
});
table().set("hide", new ZeroArgFunction() {
@Override
public LuaValue call() {
host.hide();
return LuaValue.NIL;
}
});
table().set("rect", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
double x = args.arg(1).checkdouble();
double y = args.arg(2).checkdouble();
double w = args.arg(3).checkdouble();
double h = args.arg(4).checkdouble();
host.setRect(x, y, w, h);
return LuaValue.NIL;
}
});
table().set("volume", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
double v = args.arg(1).checkdouble();
host.setVolume(v);
return LuaValue.NIL;
}
});
table().set("loop", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
boolean b = args.arg(1).checkboolean();
host.setLoop(b);
return LuaValue.NIL;
}
});
table().set("seek", new VarArgFunction() {
@Override
public Varargs invoke(Varargs args) {
double s = args.arg(1).checkdouble();
host.seek(s);
return LuaValue.NIL;
}
});
}
private static boolean looksLikeUrl(String s) {
if (s == null) return false;
int idx = s.indexOf("://");
return idx > 0 && idx < 16;
}
}

View File

@@ -2,23 +2,27 @@ package org.openautonomousconnection.luascript.tables.console;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.openautonomousconnection.luascript.hosts.ConsoleHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
public class ConsoleLogTable extends ScriptTable {
/**
* Creates a new script table with the given global name.
*/
/**
* Lua table: console.log
*/
public final class ConsoleLogTable extends ScriptTable {
public ConsoleLogTable() {
super("log");
}
@Override
protected void define(HostServices services) {
ConsoleHost console = services.console().orElseThrow(() -> new IllegalStateException("ConsoleHost not provided"));
table().set("info", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().info(arg.isnil() ? "nil" : arg.tojstring());
console.info(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
@@ -26,7 +30,7 @@ public class ConsoleLogTable extends ScriptTable {
table().set("log", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().log(arg.isnil() ? "nil" : arg.tojstring());
console.log(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
@@ -34,9 +38,9 @@ public class ConsoleLogTable extends ScriptTable {
table().set("warn", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().warn(arg.isnil() ? "nil" : arg.tojstring());
console.warn(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
}
}
}

View File

@@ -2,23 +2,27 @@ package org.openautonomousconnection.luascript.tables.console;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.openautonomousconnection.luascript.hosts.ConsoleHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
public class ConsoleStacktraceTable extends ScriptTable {
/**
* Creates a new script table with the given global name.
*/
/**
* Lua table: console.stacktrace
*/
public final class ConsoleStacktraceTable extends ScriptTable {
public ConsoleStacktraceTable() {
super("stacktrace");
}
@Override
protected void define(HostServices services) {
ConsoleHost console = services.console().orElseThrow(() -> new IllegalStateException("ConsoleHost not provided"));
table().set("print", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().error(arg.isnil() ? "nil" : arg.tojstring());
console.error(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
@@ -26,9 +30,9 @@ public class ConsoleStacktraceTable extends ScriptTable {
table().set("exception", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().exception(arg.isnil() ? "nil" : arg.tojstring());
console.exception(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
}
}
}

View File

@@ -3,10 +3,11 @@ package org.openautonomousconnection.luascript.tables.console;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
public class ConsoleTable extends ScriptTable {
/**
* Creates a new script table with the given global name.
*/
/**
* Lua table: console
*/
public final class ConsoleTable extends ScriptTable {
public ConsoleTable() {
super("console");
}
@@ -16,4 +17,4 @@ public class ConsoleTable extends ScriptTable {
injectChild(new ConsoleLogTable(), services, true);
injectChild(new ConsoleStacktraceTable(), services, true);
}
}
}