228 lines
7.0 KiB
Java
228 lines
7.0 KiB
Java
|
|
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();
|
||
|
|
}
|
||
|
|
}
|