From 4682e4482a8e3d24c07e90dabb0973d3bc24efc7 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 10 Jun 2025 18:58:16 +0200 Subject: [PATCH 01/50] initial draft --- src/main/java/lvp/Clerk.java | 3 - src/main/java/lvp/FileWatcher.java | 37 +++----- src/main/java/lvp/Main.java | 18 ++-- src/main/java/lvp/Processor.java | 115 ++++++++++++++++++++++++ src/main/java/lvp/Server.java | 22 +---- src/main/java/lvp/skills/IdGen.java | 16 ++++ src/main/java/lvp/views/MarkdownIt.java | 20 ----- syntax.md | 80 +++++++++++++++++ 8 files changed, 235 insertions(+), 76 deletions(-) create mode 100644 src/main/java/lvp/Processor.java create mode 100644 src/main/java/lvp/skills/IdGen.java delete mode 100644 src/main/java/lvp/views/MarkdownIt.java create mode 100644 syntax.md diff --git a/src/main/java/lvp/Clerk.java b/src/main/java/lvp/Clerk.java index 0e71310..3f43d0c 100644 --- a/src/main/java/lvp/Clerk.java +++ b/src/main/java/lvp/Clerk.java @@ -5,7 +5,6 @@ import java.util.Random; import java.util.stream.Collectors; -import lvp.views.MarkdownIt; public interface Clerk { public static String generateID(int n) { // random alphanumeric string of size n @@ -22,7 +21,5 @@ public static String generateID(int n) { // random alphanumeric string of size n public static void script(String javascript) { out(SSEType.SCRIPT, javascript); } public static void clear() { out(SSEType.CLEAR, ""); } - public static void markdown(String text) { new MarkdownIt().write(text); } - public static void out(SSEType event, String data) { System.out.println(event + ":" + Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8))); } } \ No newline at end of file diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index f97204f..93dd32c 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -10,7 +10,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; -import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; @@ -33,10 +32,12 @@ public class FileWatcher { private boolean isRunning = true; Path dir; String fileNamePattern; + Processor processor; - public FileWatcher(Path dir, String fileNamePattern, Server server) throws IOException{ + public FileWatcher(Path dir, String fileNamePattern, Processor processor) throws IOException{ this.dir = dir; this.fileNamePattern = fileNamePattern; + this.processor = processor; watcher = FileSystems.getDefault().newWatchService(); dir.register(watcher, @@ -46,11 +47,11 @@ public FileWatcher(Path dir, String fileNamePattern, Server server) throws IOExc for (Path path : getMatchingFiles()) { Logger.logInfo("Running initial file: " + path.toAbsolutePath().normalize()); - runJava(path, server); + run(path); } } - public void watchLoop(Server server) { + public void watchLoop() { PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + fileNamePattern); debounceExecutor = Executors.newSingleThreadScheduledExecutor(); long debounceDelay = 200; @@ -59,8 +60,7 @@ public void watchLoop(Server server) { try { key = watcher.take(); } catch (InterruptedException | ClosedWatchServiceException e) { - if (isRunning) - Logger.logError("Watcher loop terminated due to exception: " + e.getMessage(), e); + Logger.logError("Watcher loop terminated due to exception: " + e.getMessage(), e); break; } for (WatchEvent ev : key.pollEvents()) { @@ -69,7 +69,7 @@ public void watchLoop(Server server) { Logger.logInfo("Event für Datei: " + changed.toAbsolutePath() + " (" + ev.kind().name() + ")"); ScheduledFuture prev = pendingTask.getAndSet( - debounceExecutor.schedule(() -> runJava(dir.resolve(changed), server), debounceDelay, TimeUnit.MILLISECONDS) + debounceExecutor.schedule(() -> run(dir.resolve(changed)), debounceDelay, TimeUnit.MILLISECONDS) ); if (prev != null && !prev.isDone()) prev.cancel(false); } @@ -85,29 +85,20 @@ public void stop() { if (debounceExecutor != null) debounceExecutor.shutdownNow(); } - private void runJava(Path path, Server server) { + private void run(Path path) { try { - Path jarLocation = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).toAbsolutePath().normalize(); - server.events.clear(); - Logger.logInfo("Executing java --enable-preview --class-path " + jarLocation + " " + path.normalize().toString()); - ProcessBuilder pb = new ProcessBuilder("java", "--enable-preview", "--class-path", jarLocation.toString(), path.normalize().toString()) + processor.init(); + Logger.logInfo("Executing java --enable-preview " + path.normalize().toString()); + ProcessBuilder pb = new ProcessBuilder("java", "--enable-preview", path.normalize().toString()) .redirectErrorStream(true); Process process = pb.start(); - - try(BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - Logger.logDebug("(JavaClient) " + line); - server.read(line); - } - Logger.logInfo("Execution finished"); - } - + processor.process(process); boolean finished = process.waitFor(30, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); Logger.logError("Timeout: process killed"); + } else { + Logger.logInfo("Process finished successfully"); } } catch (Exception e) { diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index d4c972a..1eb0ce2 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -26,11 +26,11 @@ public static void main(String[] args) { try { Server server = new Server(Math.abs(cfg.port()), cfg.logLevel().equals(LogLevel.Debug)); Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); - + Processor processor = new Processor(server); if(cfg.path() != null) { - FileWatcher watcher = new FileWatcher(cfg.path(), cfg.fileNamePattern(), server); + FileWatcher watcher = new FileWatcher(cfg.path(), cfg.fileNamePattern(), processor); Runtime.getRuntime().addShutdownHook(new Thread(watcher::stop)); - watcher.watchLoop(server); + watcher.watchLoop(); } } catch (IOException e) { System.err.println("Error starting server: " + e.getMessage()); @@ -52,16 +52,13 @@ private static Config parseArgs(String[] args) { String value = parts.length > 1 ? parts[1].trim() : ""; switch (key) { - case "-l": - case "--log": + case "-l", "--log": logLevel = value.isBlank() ? LogLevel.Info : LogLevel.fromString(value); break; - case "-p": - case "--pattern": + case "-p", "--pattern": fileNamePattern = value.isBlank() ? "*" : value; break; - case "--watch": - case "-w": + case "--watch", "-w": path = value.isBlank() ? Paths.get(".") : Paths.get(value).normalize(); break; default: @@ -96,8 +93,7 @@ private static Config parseArgs(String[] args) { } public static boolean isLatestRelease() { - try { - HttpClient client = HttpClient.newHttpClient(); + try (HttpClient client = HttpClient.newHttpClient()) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.github.com/repos/denkspuren/LiveViewProgramming/releases/latest")) .header("Accept", "application/vnd.github+json") diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java new file mode 100644 index 0000000..2ee5436 --- /dev/null +++ b/src/main/java/lvp/Processor.java @@ -0,0 +1,115 @@ +package lvp; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import lvp.logging.Logger; +import lvp.skills.IdGen; +public class Processor { + Server server; + Map> services = new HashMap<>(Map.of("Text", content -> content)); + Map> targets = Map.of( + "Markdown", this::consumeMarkdown, + "Html", this::consumeHTML, + "JavaScript", this::consumeJS, + "JavaScriptCall", this::consumeJSCall, + "Clear", this::consumeClear); + + public Processor(Server server) { + this.server = server; + } + + void process(Process process) { + try(BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + String commandName = ""; + String content = ""; + boolean inBlock = false; + while ((line = reader.readLine()) != null) { + Logger.logDebug("(Source) " + line); + if (!inBlock) { + int i = line.trim().indexOf(":"); + if (i == -1) { + Logger.logError("(Source) " + line); + continue; + } + String name = line.trim().substring(0, i); + if (name.equals("Register")) { + //TODO: Register + continue; + } else if (!services.containsKey(name) && name.equals("Text") && !targets.containsKey(name)) { + Logger.logError("CommandName not found: " + name); + continue; + } + commandName = name; + + if (line.trim().length() == name.length() + 1) { + inBlock = true; + continue; + } + + content = line.trim().substring(i + 1); + if (targets.containsKey(commandName)) targets.get(commandName).accept(content); + else services.get(commandName).apply(content); + content = ""; + } else { + if (line.trim().equals("~~~")) { + inBlock = false; + if (targets.containsKey(commandName)) targets.get(commandName).accept(content); + else services.get(commandName).apply(content); + content = ""; + continue; + } + + content += line + '\n'; + } + } + } + catch (Exception e) { + Logger.logError("Error reading process output: " + e.getMessage()); + } + } + + void init() { + server.events.clear(); + } + + void consumeClear(String content) { + server.sendServerEvent(SSEType.CLEAR, ""); + } + + void consumeHTML(String content) { + server.sendServerEvent(SSEType.WRITE, content); + } + + void consumeJS(String content) { + server.sendServerEvent(SSEType.SCRIPT, content); + } + + void consumeJSCall(String content) { + server.sendServerEvent(SSEType.CALL, content); + } + + void consumeMarkdown(String content) { + String ID = IdGen.generateID(10); + consumeHTML(""); + // Using `preformatted` is a hack to get a Java String into the Browser without interpretation + + consumeJSCall("var scriptElement = document.getElementById('" + ID + "');" + + + """ + var divElement = document.createElement('div'); + divElement.id = scriptElement.id; + divElement.innerHTML = window.md.render(scriptElement.textContent); + scriptElement.parentNode.replaceChild(divElement, scriptElement); + """ + ); + } + +} diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index 213206b..1c50398 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -148,32 +148,16 @@ private void handleRoot(HttpExchange exchange) throws IOException { } } - public void read(String message) { - String[] parts = message.split(":", 2); - Optional event = Optional.empty(); - if (parts.length == 2) { - event = Arrays.stream(SSEType.values()) - .filter(sseType -> sseType.name().equals(parts[0])) - .findFirst(); - } - if (event.isEmpty()) Logger.logError("Error: + " + message); - - SSEType eventMessage = event.orElse(SSEType.LOG); - String data = event.isEmpty() ? Base64.getEncoder().encodeToString(message.getBytes(StandardCharsets.UTF_8)) : parts[1]; - - events.add(new EventMessage(eventMessage, data)); - if (webClients.isEmpty()) return; - sendServerEvent(eventMessage, data); - } - public void sendServerEvent(SSEType sseType, String data) { + events.add(new EventMessage(sseType, data)); + if (webClients.isEmpty()) return; webClients.removeIf(connection -> !sendMessageToClient(connection, sseType, data)); } private boolean sendMessageToClient(HttpExchange connection, SSEType event, String data) { Logger.logDebug("Event: " + event + " with data: " + data); try { - String message = "data: " + event + ":" + data + "\n\n"; + String message = "data: " + event + ":" + Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)) + "\n\n"; OutputStream os = connection.getResponseBody(); os.write(message.getBytes(StandardCharsets.UTF_8)); os.flush(); diff --git a/src/main/java/lvp/skills/IdGen.java b/src/main/java/lvp/skills/IdGen.java new file mode 100644 index 0000000..b8f0f76 --- /dev/null +++ b/src/main/java/lvp/skills/IdGen.java @@ -0,0 +1,16 @@ +package lvp.skills; + +import java.util.Random; +import java.util.stream.Collectors; + +public class IdGen { + private static final Random RANDOM = new Random(); + + public static String generateID(int n) { // random alphanumeric string of size n + return RANDOM.ints(n, 0, 36). + mapToObj(i -> Integer.toString(i, 36)). + collect(Collectors.joining()); + } + + public static String getHashID(Object o) { return Integer.toHexString(o.hashCode()); } +} diff --git a/src/main/java/lvp/views/MarkdownIt.java b/src/main/java/lvp/views/MarkdownIt.java deleted file mode 100644 index c53894f..0000000 --- a/src/main/java/lvp/views/MarkdownIt.java +++ /dev/null @@ -1,20 +0,0 @@ -package lvp.views; -import lvp.Clerk; - -public record MarkdownIt() implements Clerk { - public String write(String markdownText) { - String ID = Clerk.generateID(10); - // Using `preformatted` is a hack to get a Java String into the Browser without interpretation - Clerk.write(""); - Clerk.call("var scriptElement = document.getElementById('" + ID + "');" - + - """ - var divElement = document.createElement('div'); - divElement.id = scriptElement.id; - divElement.innerHTML = window.md.render(scriptElement.textContent); - scriptElement.parentNode.replaceChild(divElement, scriptElement); - """ - ); - return ID; - } -} diff --git a/syntax.md b/syntax.md new file mode 100644 index 0000000..f365ba7 --- /dev/null +++ b/syntax.md @@ -0,0 +1,80 @@ +# LVP Syntax +## Grammatik +``` +INSTRUCTION ::= COMMAND | REGISTER +``` +### Command +``` +COMMAND ::= SERVICE | TARGET + +COMMANDNAME ::= STRING +CONTENT ::= STRING +``` + +``` +SERVICE ::= SERVICENAME':' CONTENT + +SERVICE ::= SERVICENAME':' + CONTENT + ~~~ + +TARGET ::= TARGETNAME':' CONTENT + +TARGET ::= TARGETNAME':' + CONTENT + ~~~ +``` + +Grundidee: +Service: String -> String +Target: String -> {} +### Register +``` +NAME ::= STRING +CALL ::= STRING +``` + +``` +REGISTER ::= 'Register:' NAME CALL +``` + +### Pipe +``` +COMMAND +'|' COMMAND ['|' COMMAND '|' ...] +``` + + +## Default Services +``` +Cutout: +PATH +LABEL +~~~ + +Test: +Send: SNIPPET +Expect: STRING +~~~ + +Test: +Send: SNIPPET +Expect: +STRING1 +STRING2 +~~~ + +Turtle: +COMMANDS +~~~ +``` + + +## Targets +``` +Markdown +Html +JavaScript +JavaScriptCall +Clear +``` \ No newline at end of file From f21997f64a0c59f52fa1679b265621e2d46bb0cf Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 11 Jun 2025 22:13:47 +0200 Subject: [PATCH 02/50] parsing an processing --- pom.xml | 4 + src/main/java/lvp/FileWatcher.java | 3 +- src/main/java/lvp/InstructionParser.java | 158 +++++++++++++++++++++++ src/main/java/lvp/Processor.java | 139 ++++++++++++-------- 4 files changed, 248 insertions(+), 56 deletions(-) create mode 100644 src/main/java/lvp/InstructionParser.java diff --git a/pom.xml b/pom.xml index 77177d2..c66d6f2 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,10 @@ maven-compiler-plugin 3.13.0 + + 24 + --enable-preview + maven-jar-plugin diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 93dd32c..0688721 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -89,10 +89,11 @@ private void run(Path path) { try { processor.init(); Logger.logInfo("Executing java --enable-preview " + path.normalize().toString()); - ProcessBuilder pb = new ProcessBuilder("java", "--enable-preview", path.normalize().toString()) + ProcessBuilder pb = new ProcessBuilder("java", "-Dfile.encoding=UTF-8", "--enable-preview", path.normalize().toString()) .redirectErrorStream(true); Process process = pb.start(); processor.process(process); + boolean finished = process.waitFor(30, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); diff --git a/src/main/java/lvp/InstructionParser.java b/src/main/java/lvp/InstructionParser.java new file mode 100644 index 0000000..e1e18cc --- /dev/null +++ b/src/main/java/lvp/InstructionParser.java @@ -0,0 +1,158 @@ +package lvp; + +import java.util.StringJoiner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.stream.Gatherer.Downstream; +import java.util.stream.Gatherer; +import java.util.List; +import java.util.Objects; +import java.util.Arrays; + +import lvp.logging.Logger; +import lvp.skills.IdGen; + +public class InstructionParser { + + // ---- Instruction Types ---- + public sealed interface Instruction permits Command, Register, Pipe {} + + public record Command(String name, String id, String content) implements Instruction {} + public record Register(String name, String call) implements Instruction {} + public record Pipe(List commands) implements Instruction {} + + public record CommandRef(String name, String id) {} + + // ---- Patterns ---- + private static final Pattern SINGLE_LINE_COMMAND = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*(.+)$"); + private static final Pattern BLOCK_START = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*$"); + private static final Pattern REGISTER = Pattern.compile("^Register:\\s+(\\w+)\\s+(.+)$"); + private static final Pattern PIPE_LINE = Pattern.compile("^\\s*\\|(.+)$"); + private static final Pattern PIPE_ENTRY = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?$"); + + // ---- Block Parsing State ---- + private static class BlockState { + String name = null; + String id = null; + StringJoiner content = null; + boolean inBlock = false; + + void init(String name, String id) { + this.name = name; + this.id = id; + this.content = new StringJoiner("\n"); + this.inBlock = true; + Logger.logDebug("Started block command: " + name + formatId(id)); + } + + void append(String line) { + content.add(line); + } + + void reset() { + name = null; + id = null; + content = null; + inBlock = false; + } + } + + // ---- Main Entry Point ---- + public static Stream parse(Stream lines) { + return lines.gather(Gatherer.ofSequential( + BlockState::new, + (state, line, downstream) -> { + handleLine(state, line, downstream); + return true; + } + )); + } + + // ---- Dispatcher ---- + private static void handleLine(BlockState state, String line, Downstream out) { + if (line.isBlank()) return; + if (state.inBlock) { + handleBlockContent(state, line, out); + return; + } + + if (tryPipe(line, out)) return; + if (tryRegister(line, out)) return; + if (tryBlockStart(state, line)) return; + if (trySingleCommand(line, out)) return; + + Logger.logError("Ignored unrecognized line: " + line); + } + + // ---- Handlers ---- + + private static boolean tryPipe(String line, Downstream out) { + Matcher matcher = PIPE_LINE.matcher(line); + if (!matcher.matches()) return false; + + List commands = Arrays.stream(matcher.group(1).split("\\|")) + .map(String::trim) + .map(cmd -> { + Matcher m = PIPE_ENTRY.matcher(cmd); + if (!m.matches()) { + Logger.logError("Invalid pipe format: " + cmd); + return null; + } + String id = m.group(2) == null ? IdGen.generateID(10) : m.group(2); + return new CommandRef(m.group(1), id); + }) + .filter(Objects::nonNull) + .toList(); + + if (!commands.isEmpty()) { + Logger.logDebug("Parsed pipe: " + commands); + out.push(new Pipe(commands)); + } else { + Logger.logError("Pipe instruction without valid commands: " + line); + } + + return true; + } + + private static boolean tryRegister(String line, Downstream out) { + Matcher matcher = REGISTER.matcher(line); + if (!matcher.matches()) return false; + + Logger.logDebug("Parsed register: " + matcher.group(1) + " -> " + matcher.group(2)); + out.push(new Register(matcher.group(1), matcher.group(2))); + return true; + } + + private static boolean trySingleCommand(String line, Downstream out) { + Matcher matcher = SINGLE_LINE_COMMAND.matcher(line); + if (!matcher.matches()) return false; + String id = matcher.group(2) == null ? IdGen.generateID(10) : matcher.group(2); + Logger.logDebug("Parsed single-line command: " + matcher.group(1) + formatId(id)); + out.push(new Command(matcher.group(1), id, matcher.group(3))); + return true; + } + + private static boolean tryBlockStart(BlockState state, String line) { + Matcher matcher = BLOCK_START.matcher(line); + if (!matcher.matches()) return false; + + String id = matcher.group(2) == null ? IdGen.generateID(10) : matcher.group(2); + state.init(matcher.group(1), id); + return true; + } + + private static void handleBlockContent(BlockState state, String line, Downstream out) { + if (line.equals("~~~")) { + Logger.logDebug("Parsed block command: " + state.name + formatId(state.id)); + out.push(new Command(state.name, state.id, state.content.toString())); + state.reset(); + } else { + state.append(line); + } + } + + private static String formatId(String id) { + return id != null ? "{" + id + "}" : ""; + } +} diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 2ee5436..854a5ed 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -5,20 +5,27 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Gatherers; +import lvp.InstructionParser.Command; +import lvp.InstructionParser.CommandRef; +import lvp.InstructionParser.Pipe; import lvp.logging.Logger; import lvp.skills.IdGen; public class Processor { Server server; - Map> services = new HashMap<>(Map.of("Text", content -> content)); - Map> targets = Map.of( + Map> services = new HashMap<>(Map.of("Text", this::text)); + Map> targets = Map.of( "Markdown", this::consumeMarkdown, "Html", this::consumeHTML, "JavaScript", this::consumeJS, "JavaScriptCall", this::consumeJSCall, "Clear", this::consumeClear); + Map templates = new HashMap<>(); public Processor(Server server) { this.server = server; @@ -27,81 +34,103 @@ public Processor(Server server) { void process(Process process) { try(BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - String commandName = ""; - String content = ""; - boolean inBlock = false; - while ((line = reader.readLine()) != null) { - Logger.logDebug("(Source) " + line); - if (!inBlock) { - int i = line.trim().indexOf(":"); - if (i == -1) { - Logger.logError("(Source) " + line); - continue; - } - String name = line.trim().substring(0, i); - if (name.equals("Register")) { - //TODO: Register - continue; - } else if (!services.containsKey(name) && name.equals("Text") && !targets.containsKey(name)) { - Logger.logError("CommandName not found: " + name); - continue; - } - commandName = name; - - if (line.trim().length() == name.length() + 1) { - inBlock = true; - continue; - } - - content = line.trim().substring(i + 1); - if (targets.containsKey(commandName)) targets.get(commandName).accept(content); - else services.get(commandName).apply(content); - content = ""; - } else { - if (line.trim().equals("~~~")) { - inBlock = false; - if (targets.containsKey(commandName)) targets.get(commandName).accept(content); - else services.get(commandName).apply(content); - content = ""; - continue; - } - - content += line + '\n'; - } - } + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); } + // InstructionParser.parse(reader.lines()).gather(Gatherers.fold(() -> "", (prev, curr) -> + // switch (curr) { + // case Command cmd -> processCommands(cmd); + // case Pipe pipe -> processPipe(pipe, prev); + // default -> null; + // })).findFirst(); + } catch (Exception e) { Logger.logError("Error reading process output: " + e.getMessage()); } } + String processCommands(Command command) { + Logger.logDebug("Command: " + command.name() + "{" + command.id() + "}, " + command.content()); + + if (targets.containsKey(command.name())) { + targets.get(command.name()).accept(command.id(), command.content()); + } + else if (services.containsKey(command.name())) { + return services.get(command.name()).apply(command.id(), command.content()); + } else { + Logger.logError("Command not found: " + command.name()); + } + + return null; + } + + String processPipe(Pipe pipe, String input) { + if (input == null) return null; + String current = input; + for (CommandRef ref : pipe.commands()) { + Logger.logDebug("Command: " + ref.name() + "{" + ref.id() + "}, " + current); + if (targets.containsKey(ref.name())) { + targets.get(ref.name()).accept(ref.id() == null ? IdGen.generateID(10) : ref.id(), current); + return null; + } + else if (services.containsKey(ref.name())) { + current = services.get(ref.name()).apply(ref.id(), current); + } else { + Logger.logError("Command not found: " + ref.name()); + } + } + return current; + } + void init() { server.events.clear(); } - void consumeClear(String content) { + String text(String id, String content) { + String newValue = templates.merge(id, content, this::fillOut); + return newValue == null ? content : newValue; + } + + String fillOut(String template, String replacement) { + Pattern pattern = Pattern.compile("\\$\\{(.*?)\\}"); // `${}` + Matcher matcher = pattern.matcher(template); + StringBuffer result = new StringBuffer(); + String key = ""; + + while (matcher.find()) { + String group = matcher.group(1); + if (key.isBlank()) key = group; + if (!key.equals(group)) continue; + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } + + void consumeClear(String id, String content) { server.sendServerEvent(SSEType.CLEAR, ""); } - void consumeHTML(String content) { + void consumeHTML(String id, String content) { server.sendServerEvent(SSEType.WRITE, content); } - void consumeJS(String content) { + void consumeJS(String id, String content) { server.sendServerEvent(SSEType.SCRIPT, content); } - void consumeJSCall(String content) { + void consumeJSCall(String id, String content) { server.sendServerEvent(SSEType.CALL, content); } - void consumeMarkdown(String content) { - String ID = IdGen.generateID(10); - consumeHTML(""); + void consumeMarkdown(String id, String content) { + consumeHTML(id, ""); // Using `preformatted` is a hack to get a Java String into the Browser without interpretation - consumeJSCall("var scriptElement = document.getElementById('" + ID + "');" + consumeJSCall(id, "var scriptElement = document.getElementById('" + id + "');" + """ var divElement = document.createElement('div'); From f43957c628346a3d6a36d7e0f65b8768fe809aad Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 11 Jun 2025 22:55:26 +0200 Subject: [PATCH 03/50] enforce utf8 encoding for console output in subprocess and clear templates before processing --- src/main/java/lvp/FileWatcher.java | 2 +- src/main/java/lvp/Processor.java | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 0688721..bf56c24 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -89,7 +89,7 @@ private void run(Path path) { try { processor.init(); Logger.logInfo("Executing java --enable-preview " + path.normalize().toString()); - ProcessBuilder pb = new ProcessBuilder("java", "-Dfile.encoding=UTF-8", "--enable-preview", path.normalize().toString()) + ProcessBuilder pb = new ProcessBuilder("java", "-Dsun.stdout.encoding=UTF-8", "--enable-preview", path.normalize().toString()) .redirectErrorStream(true); Process process = pb.start(); processor.process(process); diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 854a5ed..88b07cc 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -32,18 +32,15 @@ public Processor(Server server) { } void process(Process process) { + templates.clear(); try(BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - System.out.println(line); - } - // InstructionParser.parse(reader.lines()).gather(Gatherers.fold(() -> "", (prev, curr) -> - // switch (curr) { - // case Command cmd -> processCommands(cmd); - // case Pipe pipe -> processPipe(pipe, prev); - // default -> null; - // })).findFirst(); + InstructionParser.parse(reader.lines()).gather(Gatherers.fold(() -> "", (prev, curr) -> + switch (curr) { + case Command cmd -> processCommands(cmd); + case Pipe pipe -> processPipe(pipe, prev); + default -> null; + })).findFirst(); } catch (Exception e) { Logger.logError("Error reading process output: " + e.getMessage()); From 49ad29127e488f5f481951fc13dfe8e685697231 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 11 Jun 2025 22:55:58 +0200 Subject: [PATCH 04/50] added demo --- newdemo.java | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 newdemo.java diff --git a/newdemo.java b/newdemo.java new file mode 100644 index 0000000..20bdc8f --- /dev/null +++ b/newdemo.java @@ -0,0 +1,32 @@ +void main() { + println("Clear:~"); + println("Markdown: # Hello World!"); + println(""" + Markdown: + ## Hello World! + This is a simple example of a markdown block. + + ~~~ + """); + println(""" + Text{0}: + # Text und Pipes + Der ${0} Command ${1} es ${0} Templates zu definieren. + In diesen Templates können Platzhalter genutzt werden, die + später durch Pipes mit Content befüllt werden. + Dieser ${0} kann zum Beispiel in die Markdown View "gepiped" werden. + ~~~ + | Markdown + """); + + + println(""" + Text: Text + | Text{0} | Markdown + """); + + println(""" + Text: erlaubt + | Text{0} | Markdown + """); +} \ No newline at end of file From 13f42ceaa455e93b66fd995322a80504a448cf8e Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 13:06:53 +0200 Subject: [PATCH 05/50] added codeblock command --- newdemo.java | 16 +++++++++++++++- src/main/java/lvp/Processor.java | 16 +++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/newdemo.java b/newdemo.java index 20bdc8f..d832a52 100644 --- a/newdemo.java +++ b/newdemo.java @@ -29,4 +29,18 @@ void main() { Text: erlaubt | Text{0} | Markdown """); -} \ No newline at end of file + +// ex1 + println(""" + Text{1}: + # Codeblocks + This is a codeblock example: + ```java + ${0} + ``` + ~~~ + Codeblock: newdemo.java:// ex1 + | Text{1} | Markdown + """); +// ex1 +} diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 88b07cc..74aa396 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -16,9 +16,10 @@ import lvp.InstructionParser.Pipe; import lvp.logging.Logger; import lvp.skills.IdGen; +import lvp.skills.Text; public class Processor { Server server; - Map> services = new HashMap<>(Map.of("Text", this::text)); + Map> services = new HashMap<>(Map.of("Text", this::text, "Codeblock", this::codeblock)); Map> targets = Map.of( "Markdown", this::consumeMarkdown, "Html", this::consumeHTML, @@ -40,10 +41,10 @@ void process(Process process) { case Command cmd -> processCommands(cmd); case Pipe pipe -> processPipe(pipe, prev); default -> null; - })).findFirst(); + })).forEachOrdered(_->{}); } catch (Exception e) { - Logger.logError("Error reading process output: " + e.getMessage()); + Logger.logError("Error reading process output: " + e.getMessage(), e); } } @@ -84,6 +85,15 @@ void init() { server.events.clear(); } + String codeblock(String id, String content) { + String[] parts = content.split(":"); + if (parts.length != 2) { + Logger.logError("Invalid Codeblock Format."); + return null; + } + return Text.codeBlock(parts[0].trim(), parts[1].trim()); + } + String text(String id, String content) { String newValue = templates.merge(id, content, this::fillOut); return newValue == null ? content : newValue; From f0f1cd4031830347a10dc4e4b15d74dec118ddf7 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 13:07:49 +0200 Subject: [PATCH 06/50] removed unused views and skills --- src/main/java/lvp/skills/ObjectInspector.java | 341 ------------------ src/main/java/lvp/skills/RGB.java | 40 -- src/main/java/lvp/skills/Text.java | 6 - .../java/lvp/views/CanvasTurtle.java.disabled | 93 ----- 4 files changed, 480 deletions(-) delete mode 100644 src/main/java/lvp/skills/ObjectInspector.java delete mode 100644 src/main/java/lvp/skills/RGB.java delete mode 100644 src/main/java/lvp/views/CanvasTurtle.java.disabled diff --git a/src/main/java/lvp/skills/ObjectInspector.java b/src/main/java/lvp/skills/ObjectInspector.java deleted file mode 100644 index 0ce03c4..0000000 --- a/src/main/java/lvp/skills/ObjectInspector.java +++ /dev/null @@ -1,341 +0,0 @@ -// https://gist.github.com/RamonDevPrivate/3bb187ef89b2666b1b1d00232100f5ee -// Author: https://github.com/RamonDevPrivate, Version 1, CC BY-NC-SA -package lvp.skills; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -abstract class ObjectNode_425 { - String name; - Optional value; - String identifier; - boolean isDotted; - ObjectNode_425[] children; - /** - * @param name - custom name to uniquely identify node in dot graph - * @param value - values of primitive types/strings or classname; displayed inside the dot node - * @param identifier - variable name; displayed on dot arrow - * @param isDotted - dot node with dotted lines - * @param children - child nodes - */ - ObjectNode_425(String name, Optional value, String identifier, boolean isDotted, ObjectNode_425... children) { - this.name = name; - this.value = value; - this.identifier = identifier; - this.children = children; - this.isDotted = isDotted; - } - - @Override - public String toString() { - String output = ""; - if (children != null && children.length > 0) { - for (ObjectNode_425 child : children) { - if (child == null) continue; - output += child.toString(); - output += this.name + " -> " + child.name + "[label=\" "+ child.identifier + "\",style=" + (child.isDotted ? "dashed" : "solid") +"] ;\n"; - } - } - return output; - } -} - -class RootNode_425 extends ObjectNode_425 { - /** - * root / start of the graph; start point is pointing on this node. - * @param name - custom name to uniquely identify node in dot graph - * @param value - values of primitive types/strings or classname; displayed inside the dot node - * @param identifier - variable name; displayed on dot arrow - * @param children - child nodes - */ - RootNode_425(String name, Optional value, String identifier, ObjectNode_425... children) { - super(name, value, identifier, false, children); - } - - @Override - public String toString() { - String output = "start[shape=circle,label=\"\",height=.25];\n"; - output += this.name + (value.isPresent() ? " [label=\""+ this.value.get() + "\"];\n" : " [label=\"\",shape=point,height=.25];\n"); - output += "start -> " + name + "[label=\" "+ identifier + "\"] ;\n"; - - return output + super.toString(); - } -} - -class ChildNode_425 extends ObjectNode_425 { - /** - * child nodes - * @param name - custom name to uniquely identify node in dot graph - * @param value - values of primitive types/strings or classname; displayed inside the dot node - * @param identifier - variable name; displayed on dot arrow - * @param isDotted - dot node with dotted lines - * @param children - child nodes - */ - ChildNode_425(String name, Optional value, String identifier, boolean isDotted, ObjectNode_425... children) { - super(name, value, identifier, isDotted, children); - } - - @Override - public String toString() { - String output = this.name + (value.isPresent() ? " [label=\""+ this.value.get() + "\",style=" + (isDotted ? "dashed" : "solid") +"];\n" : " [label=\"\",shape=point,height=.25];\n"); - - return output + super.toString(); - } -} - -class ArrayNode extends ObjectNode_425 { - int length; - /** - * array nodes are displayed as a box and have an additional value for the array length - * can aswell be used to display any collection or map - * @param name - custom name to uniquely identify node in dot graph - * @param value - values of primitive types/strings or classname; displayed inside the dot node - * @param identifier - variable name; displayed on dot arrow - * @param length - array length - * @param isDotted - dot node with dotted lines - * @param children - child nodes - */ - ArrayNode(String name, Optional value, String identifier, int length, boolean isDotted, ObjectNode_425... children) { - super(name, value, identifier, isDotted, children); - this.length = length; - } - - @Override - public String toString() { - String output = this.name + (value.isPresent() ? " [label=\""+ this.value.get() + "\",shape=box,style=" + (isDotted ? "dashed" : "solid") +"];\n" : " [label=\"\",shape=point,height=.25];\n"); - output += this.name + "length[label=\""+ this.length + "\"];\n"; - output += this.name + "->" + this.name + "length[label=\"length\"]\n"; - return output + super.toString(); - } -} - -public class ObjectInspector { - private int nodeCounter = 0; //used to generate an unique node name - - // save inspected objects to prevent infinite loops in case of recursion and identify already used objects - private Map inspectedObject = new HashMap<>(); - - private ObjectNode_425 root; - - private boolean hideGeneratedVars, inspectSuperClasses; - - private ObjectInspector(){} - - /** - * Inspect the object using reflections and store it in a tree structure of Nodes - * IMPORTANT: Only public properties can be inspected! - * @param objectToBeInspected - root object of the tree structure; - * @param identifier - variable name referencing the object - * @return instance of NodeGenerator - */ - public static ObjectInspector inspect(Object objectToBeInspected, String identifier) { - return inspect(objectToBeInspected, identifier, true, true); - } - - /** - * Inspect the object using reflections and store it in a tree structure of Nodes - * IMPORTANT: Only public properties can be inspected! - * @param objectToBeInspected - root object of the tree structure; - * @param identifier - variable name referencing the object - * @param inspectSuperClasses - true -> super class fields are inspected too - * @return instance of NodeGenerator - */ - public static ObjectInspector inspect(Object objectToBeInspected, String identifier, boolean inspectSuperClasses) { - return inspect(objectToBeInspected, identifier, inspectSuperClasses, true); - } - - /** - * Inspect the object using reflections and store it in a tree structure of Nodes - * IMPORTANT: Only public properties can be inspected! - * @param objectToBeInspected - root object of the tree structure; - * @param identifier - variable name referencing the object - * @param inspectSuperClasses - true -> super class fields are inspected too - * @param hideGeneratedVars - true -> compiler generated vars are hidden - * @return instance of NodeGenerator - */ - public static ObjectInspector inspect(Object objectToBeInspected, String identifier, boolean inspectSuperClasses, boolean hideGeneratedVars) { - assert !objectToBeInspected.getClass().getPackageName().startsWith("java") : "Can't inspect Java owned objects!"; - ObjectInspector g = new ObjectInspector(); - g.hideGeneratedVars = hideGeneratedVars; - g.inspectSuperClasses = inspectSuperClasses; - g.root = g.objectReferenceToNodeTree(objectToBeInspected, identifier, true, false); - return g; - } - - /** - * Convert Node tree into a dot graph and save it in the working directory - * @param root - root node of the Node tree - */ - public void toGraph() { - String dotSource = "digraph G {\n" + root.toString() + "}"; - File dot; - File img; - try { - dot = writeDotSourceToFile(dotSource); - if (dot != null) { - img = File.createTempFile("graph_", ".png", new File("./")); - Runtime rt = Runtime.getRuntime(); - String[] cmd = {"dot", "-Tpng", dot.getAbsolutePath(), "-o", img.getAbsolutePath()}; - Process p = rt.exec(cmd); - p.waitFor(); - // dot.delete(); // Delete dot file - remove this line to view the dot file - } - } catch (IOException | InterruptedException e) { - System.err.println(e.getMessage()); - } - } - - public ObjectNode_425 root() { - return root; - } - - @Override - public String toString() { - return "digraph G {\n" + root.toString() + "}"; - } - - private Field[] combineFields(Class classToBeInspected, Field[] fields) { - Field[] classFields = classToBeInspected.getDeclaredFields(); - Field[] combinedFields = new Field[fields.length + classFields.length]; - for (int i = 0; i < combinedFields.length; i++) { - combinedFields[i] = i < fields.length ? fields[i] : classFields[i - fields.length]; - } - - Class superclass = classToBeInspected.getSuperclass(); - if (superclass != null && inspectSuperClasses) return combineFields(superclass, combinedFields); - return combinedFields; - } - - private ObjectNode_425 objectReferenceToNodeTree(Object objectToBeInspected, String identifier, boolean isRoot, boolean isDotted) { - Class classToBeInspected = objectToBeInspected.getClass(); - - // reuse same node for identical objects - if (inspectedObject.keySet().stream().anyMatch(key -> key == objectToBeInspected)) { - return new ChildNode_425(inspectedObject.get(objectToBeInspected).name, inspectedObject.get(objectToBeInspected).value, identifier, inspectedObject.get(objectToBeInspected).isDotted); - } - - ObjectNode_425 result = isRoot - ? new RootNode_425("n"+nodeCounter++, Optional.of(classToBeInspected.getSimpleName()), identifier) - : new ChildNode_425("n"+nodeCounter++, Optional.of(classToBeInspected.getSimpleName()), identifier, isDotted); - - // Identify when the same object is used - inspectedObject.put(objectToBeInspected, result); - - Field[] fields = combineFields(classToBeInspected, new Field[0]); - ObjectNode_425[] childs = new ObjectNode_425[fields.length]; - - for(int i = 0; i < fields.length; i++) { - if (!fields[i].getName().startsWith("$") && !fields[i].canAccess(objectToBeInspected)) - continue; //ignore inaccessible fields - if (fields[i].getName().startsWith("$") && hideGeneratedVars) - continue; //ignore intern vars - - try { - Object fieldObj = fields[i].get(objectToBeInspected); - if (fieldObj != null) { - // reuse same node for identical fields - if (inspectedObject.keySet().stream().anyMatch(key -> key == fieldObj)) { - childs[i] = new ChildNode_425(inspectedObject.get(fieldObj).name, inspectedObject.get(fieldObj).value, fields[i].getName(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i])); - continue; - } - - // special cases like array, collections and maps - if (fields[i].getType().isArray()) { - childs[i] = processArray(fieldObj, fields[i].getName(), Optional.empty(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i])); - continue; - } - if (Collection.class.isAssignableFrom(fieldObj.getClass())) { - childs[i] = processArray(((Collection)fieldObj).toArray(), fields[i].getName(), Optional.empty(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i])); - childs[i].value = Optional.of(fieldObj.getClass().getSimpleName()); - continue; - } - if (Map.class.isAssignableFrom(fieldObj.getClass())) { - childs[i] = processArray(((Map)fieldObj).values().toArray(), fields[i].getName(), - Optional.of(((Map)fieldObj).keySet().toArray()), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i])); - childs[i].value = Optional.of(fieldObj.getClass().getSimpleName()); - continue; - } - } - - // regular values / objects - childs[i] = processTypes(fields[i].getType().getTypeName(), fieldObj, fields[i].getName(), !Arrays.asList(classToBeInspected.getDeclaredFields()).contains(fields[i])); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - result.children = childs; - return result; - } - - private ObjectNode_425 processTypes(String typename, Object obj, String identifier, boolean isDotted) { - // special cases for primitive types and strings - return switch (typename) { - case "int", "java.lang.Integer" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Integer)obj).toString()), identifier, isDotted); - case "boolean", "java.lang.Boolean" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Boolean)obj).toString()), identifier, isDotted); - case "float", "java.lang.Float" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Float)obj).toString()), identifier, isDotted); - case "double", "java.lang.Double" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Double)obj).toString()), identifier, isDotted); - case "char", "java.lang.Character" -> new ChildNode_425("n" + nodeCounter++, Optional.of(((Character)obj).toString()), identifier, isDotted); - case "String", "java.lang.String" -> new ChildNode_425("n" + nodeCounter++, Optional.of("\\\"" + ((String)obj) + "\\\""), identifier, isDotted); - default -> (obj != null) // if object is null display it as point - ? (!obj.getClass().getPackageName().startsWith("java") // recursivly travel through objects that are not part of java - ? objectReferenceToNodeTree(obj, identifier, false, isDotted) - : new ChildNode_425("n" + nodeCounter++, Optional.of(obj.getClass().getSimpleName()), identifier, isDotted)) - : new ChildNode_425("n" + nodeCounter++, Optional.empty(), identifier, isDotted); - }; - } - - private ObjectNode_425 processArray(Object obj, String identifier, Optional index, boolean isDotted) { - int arrayLength = Array.getLength(obj); - ObjectNode_425[] arrayChilds = new ObjectNode_425[arrayLength]; - for (int j = 0; j < arrayLength; j++) { - Object element = Array.get(obj, j); - ObjectNode_425 child = (element != null) - ? (element.getClass().getPackageName().startsWith("java") // recursivly travel through objects that are not part of java - ? processTypes(element.getClass().getTypeName(), element, index.isPresent() //display regular index or custom one for e.g. maps - ? index.get()[j].toString() - : Integer.valueOf(j).toString(), isDotted) - : objectReferenceToNodeTree(element, index.isPresent() //display regular index or custom one for e.g. maps - ? index.get()[j].toString() - : Integer.valueOf(j).toString(), false, isDotted)) - : new ChildNode_425("n" + nodeCounter++, Optional.empty(), index.isPresent() //display regular index or custom one for e.g. maps - ? index.get()[j].toString() - : Integer.valueOf(j).toString(), isDotted); - arrayChilds[j] = child; - } - return new ArrayNode("n" + nodeCounter++, Optional.of(obj.getClass().getSimpleName()), identifier, arrayLength, isDotted, arrayChilds); - } - - private File writeDotSourceToFile(String str) throws IOException { - File temp = File.createTempFile("temp", ".dot", new File("./")); - FileWriter fw = new FileWriter(temp); - fw.write(str); - fw.close(); - return temp; - } -} - - - - - -// jshell -R-ea - - -// MyObject myObject = new MyObject(); - - -// NodeGenerator g = NodeGenerator.inspect(myObject, "myObject"); -// NodeGenerator g = NodeGenerator.inspect(myObject, "myObject", true, false); - - -// g.toGraph(); // generate dot image -// g.root(); // generated node structure -// g.toString(); // generated dot string diff --git a/src/main/java/lvp/skills/RGB.java b/src/main/java/lvp/skills/RGB.java deleted file mode 100644 index bf27145..0000000 --- a/src/main/java/lvp/skills/RGB.java +++ /dev/null @@ -1,40 +0,0 @@ -package lvp.skills; - -public enum RGB { // https://w3schools.sinsixx.com/css/css_colornames.asp.htm - AQUA(0x00FFFF), - BLACK(0x000000), - BLUE(0, 0, 255), - FUCHSIA(0xFF00FF), - GRAY(0x808080), - GREEN(0, 255, 0), - LIME(0x00FF00), - MAROON(0x800000), - NAVY(0x000080), - OLIVE(0x808000), - PURPLE(0x800080), - RED(255, 0, 0), - SILVER(0xC0C0C0), - TEAL(0x008080), - WHITE(0xFF, 0xFF ,0xFF), - YELLOW(0xFFFF00); - - final int colorCode; - - private RGB(int red, int green, int blue) { colorCode = color(red, green, blue); } - private RGB(int code) { this(red(code), green(code), blue(code)); } - - public static int color(int code) { - return color(red(code), green(code), blue(code)); - } - - public static int color(int red, int green, int blue) { - return ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF); - } - - public static int red(int colorCode) { return (colorCode & 0b11111111_00000000_00000000) >> 16; } - public static int green(int colorCode) { return (colorCode & 0b11111111_00000000) >> 8; } - public static int blue(int colorCode) { return colorCode & 0b11111111; } - - public static String hex(int code) { return String.format("0x%06x", color(code)); } - public String hex() { return hex(this.colorCode); } -} \ No newline at end of file diff --git a/src/main/java/lvp/skills/Text.java b/src/main/java/lvp/skills/Text.java index 19c38d7..65d9502 100644 --- a/src/main/java/lvp/skills/Text.java +++ b/src/main/java/lvp/skills/Text.java @@ -69,12 +69,6 @@ public static String read(String fileName) { return cutOut(fileName, true, true, ""); } - public static String escapeHtml(String text) { - return text.replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">"); - } - public static String codeBlock(String fileName, String label) { return fillOut(""" src-info: ${0}:${1}:multi ||| diff --git a/src/main/java/lvp/views/CanvasTurtle.java.disabled b/src/main/java/lvp/views/CanvasTurtle.java.disabled deleted file mode 100644 index 4ddca9c..0000000 --- a/src/main/java/lvp/views/CanvasTurtle.java.disabled +++ /dev/null @@ -1,93 +0,0 @@ -package lvp.views; - -import lvp.Clerk; -import lvp.views.canvasturtle.Font; - -public class CanvasTurtle implements Clerk { - public final String ID; - final int width, height; - Font textFont = Font.SANSSERIF; - double textSize = 10; - Font.Align textAlign = Font.Align.CENTER; - - public CanvasTurtle(int width, int height) { - this.width = Math.max(1, Math.abs(width)); // width is at least of size 1 - this.height = Math.max(1, Math.abs(height)); // height is at least of size 1 - ID = Clerk.getHashID(this); - Clerk.load("views/canvasturtle/turtle.js"); - Clerk.write(""); - Clerk.script("let turtle" + ID + " = new Turtle(document.getElementById('turtleCanvas" + ID + "'));"); - } - - public CanvasTurtle() { this(500, 500); } - - public CanvasTurtle penDown() { - Clerk.call("turtle" + ID + ".penDown();"); - return this; - } - - public CanvasTurtle penUp() { - Clerk.call("turtle" + ID + ".penUp();"); - return this; - } - - public CanvasTurtle forward(double distance) { - Clerk.call("turtle" + ID + ".forward(" + distance + ");"); - return this; - } - - public CanvasTurtle backward(double distance) { - Clerk.call("turtle" + ID + ".backward(" + distance + ");"); - return this; - } - - public CanvasTurtle left(double degrees) { - Clerk.call("turtle" + ID + ".left(" + degrees + ");"); - return this; - } - - public CanvasTurtle right(double degrees) { - Clerk.call("turtle" + ID + ".right(" + degrees + ");"); - return this; - } - - public CanvasTurtle color(int red, int green, int blue) { - Clerk.call("turtle" + ID + ".color('rgb(" + (red & 0xFF) + ", " + (green & 0xFF) + ", " + (blue & 0xFF) + ")');"); - return this; - } - - public CanvasTurtle color(int rgb) { - color((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); - return this; - } - - public CanvasTurtle lineWidth(double width) { - Clerk.call("turtle" + ID + ".lineWidth('" + width + "');"); - return this; - } - - public CanvasTurtle reset() { - Clerk.call("turtle" + ID + ".reset();"); - return this; - } - - public CanvasTurtle text(String text, Font font, double size, Font.Align align) { - textFont = font; - textSize = size; - textAlign = align; - Clerk.call("turtle" + ID + ".text('" + text + "', '" + "" + size + "px " + font + "', '" + align + "')"); - return this; - } - - public CanvasTurtle text(String text) { return text(text, textFont, textSize, textAlign); } - - public CanvasTurtle moveTo(double x, double y) { - Clerk.call("turtle" + ID + ".moveTo(" + x + ", " + y + ");"); - return this; - } - - public CanvasTurtle lineTo(double x, double y) { - Clerk.call("turtle" + ID + ".lineTo(" + x + ", " + y + ");"); - return this; - } -} \ No newline at end of file From 07fc184b7502769a4382d8ea45ce131dae43adf7 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 13:10:33 +0200 Subject: [PATCH 07/50] Removed Clerk Interface --- src/main/java/lvp/Clerk.java | 25 --------------- src/main/java/lvp/views/Dot.java | 12 ++++---- src/main/java/lvp/views/Turtle.java | 48 ++++++++++++++--------------- 3 files changed, 30 insertions(+), 55 deletions(-) delete mode 100644 src/main/java/lvp/Clerk.java diff --git a/src/main/java/lvp/Clerk.java b/src/main/java/lvp/Clerk.java deleted file mode 100644 index 3f43d0c..0000000 --- a/src/main/java/lvp/Clerk.java +++ /dev/null @@ -1,25 +0,0 @@ -package lvp; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Random; -import java.util.stream.Collectors; - - -public interface Clerk { - public static String generateID(int n) { // random alphanumeric string of size n - return new Random().ints(n, 0, 36). - mapToObj(i -> Integer.toString(i, 36)). - collect(Collectors.joining()); - } - - public static String getHashID(Object o) { return Integer.toHexString(o.hashCode()); } - - - public static void write(String html) { out(SSEType.WRITE, html); } - public static void call(String javascript) { out(SSEType.CALL, javascript); } - public static void script(String javascript) { out(SSEType.SCRIPT, javascript); } - public static void clear() { out(SSEType.CLEAR, ""); } - - public static void out(SSEType event, String data) { System.out.println(event + ":" + Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8))); } -} \ No newline at end of file diff --git a/src/main/java/lvp/views/Dot.java b/src/main/java/lvp/views/Dot.java index 5c38f6b..dcf00e0 100644 --- a/src/main/java/lvp/views/Dot.java +++ b/src/main/java/lvp/views/Dot.java @@ -1,25 +1,25 @@ package lvp.views; -import lvp.Clerk; +import lvp.skills.IdGen; -public class Dot implements Clerk { +public class Dot { final String ID; int width, height; public Dot(int width, int height) { this.width = width; this.height = height; - ID = Clerk.getHashID(this); + ID = IdGen.getHashID(this); - Clerk.write("
"); - Clerk.script("clerk.dot" + ID + " = new Dot(document.getElementById('dotContainer" + ID + "'), " + this.width + ", " + this.height + ");"); + //Clerk.write("
"); + //Clerk.script("clerk.dot" + ID + " = new Dot(document.getElementById('dotContainer" + ID + "'), " + this.width + ", " + this.height + ");"); } public Dot() { this(500, 500); } public Dot draw(String dotString) { String escaped = dotString.replaceAll("\\\"", "\\\\\"").replaceAll("\\n", ""); - Clerk.script("clerk.dot" + ID + ".draw(\"" + escaped + "\")"); + //Clerk.script("clerk.dot" + ID + ".draw(\"" + escaped + "\")"); return this; } } \ No newline at end of file diff --git a/src/main/java/lvp/views/Turtle.java b/src/main/java/lvp/views/Turtle.java index 0469323..565c39c 100644 --- a/src/main/java/lvp/views/Turtle.java +++ b/src/main/java/lvp/views/Turtle.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Locale; -import lvp.Clerk; +import lvp.skills.IdGen; import lvp.skills.Interaction; import lvp.skills.Text; @@ -20,8 +20,8 @@ * Daher werden die Y-Koordinaten beim Export invertiert. * Die einzelnen graphischen Elemente werden durchnummeriert in der Reihenfolge ihrer Erzeugung. */ -public class Turtle implements Clerk{ - public final String ID = Clerk.getHashID(this); +public class Turtle { + public final String ID = IdGen.getHashID(this); private final double xFrom, yFrom, viewWidth, viewHeight; private final List elements = new ArrayList<>(); private int elementCounter = 0; @@ -150,31 +150,31 @@ public Turtle pop() { } public Turtle write() { - Clerk.write(Text.fillOut("
${1}
", ID, toString())); + // Clerk.write(Text.fillOut("
${1}
", ID, toString())); return this; } public Turtle timelineSlider() { - Clerk.write(Text.fillOut(""" -
- Linien sichtbar: ${1} / ${1} -
- """, ID, elements.size())); - Clerk.write( - Interaction.slider(ID, 0, elements.size(), elements.size(), Text.fillOut(""" - ((e) => { - const n = e.target.value; - const statusCurrent = document.getElementById("currentLine${0}"); - statusCurrent.textContent = n; - const svgElement = document.getElementById("turtle${0}"); - const lineIds = Array.from(svgElement.querySelectorAll("[svg-id]")).map(el => el.getAttribute("svg-id")); - lineIds.forEach((id,i) => { - const el = svgElement.querySelector(`[svg-id="` + id + `"]`); - if (el) el.style.display = i < n ? "" : "none"; - }); - })(event) - """, ID)) - ); + // Clerk.write(Text.fillOut(""" + //
+ // Linien sichtbar: ${1} / ${1} + //
+ // """, ID, elements.size())); + // Clerk.write( + // Interaction.slider(ID, 0, elements.size(), elements.size(), Text.fillOut(""" + // ((e) => { + // const n = e.target.value; + // const statusCurrent = document.getElementById("currentLine${0}"); + // statusCurrent.textContent = n; + // const svgElement = document.getElementById("turtle${0}"); + // const lineIds = Array.from(svgElement.querySelectorAll("[svg-id]")).map(el => el.getAttribute("svg-id")); + // lineIds.forEach((id,i) => { + // const el = svgElement.querySelector(`[svg-id="` + id + `"]`); + // if (el) el.style.display = i < n ? "" : "none"; + // }); + // })(event) + // """, ID)) + // ); return this; } From 99bb367c489e2576022f339852280efa48727deb Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 13:11:17 +0200 Subject: [PATCH 08/50] removed canvas turtle deps --- .../java/lvp/views/canvasturtle/Font.java | 24 ----- .../lvp/views/canvasturtle/TurtleExample.png | Bin 15234 -> 0 bytes .../java/lvp/views/canvasturtle/index.html | 25 ----- .../java/lvp/views/canvasturtle/turtle.js | 91 ------------------ 4 files changed, 140 deletions(-) delete mode 100644 src/main/java/lvp/views/canvasturtle/Font.java delete mode 100644 src/main/java/lvp/views/canvasturtle/TurtleExample.png delete mode 100644 src/main/java/lvp/views/canvasturtle/index.html delete mode 100644 src/main/java/lvp/views/canvasturtle/turtle.js diff --git a/src/main/java/lvp/views/canvasturtle/Font.java b/src/main/java/lvp/views/canvasturtle/Font.java deleted file mode 100644 index 8d08285..0000000 --- a/src/main/java/lvp/views/canvasturtle/Font.java +++ /dev/null @@ -1,24 +0,0 @@ -package lvp.views.canvasturtle; - -public enum Font { - ARIAL("Arial"), - VERDANA("Verdana"), - TIMES("Times New Roman"), - COURIER("Courier New"), - SERIF("serif"), - SANSSERIF("sans-serif"); - - final String fullName; - - private Font(String fullName) { this.fullName = fullName; } - - @Override - public String toString() { return fullName;} - - public enum Align { - CENTER, LEFT, RIGHT; - - @Override - public String toString() { return name().toLowerCase(); } - } -} diff --git a/src/main/java/lvp/views/canvasturtle/TurtleExample.png b/src/main/java/lvp/views/canvasturtle/TurtleExample.png deleted file mode 100644 index 44d1ae161de2eb884b8a1e46c4b2b96c697e5a18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15234 zcmeHubyQrz(kB)e+(K{CEZAiRGq*UR<`DFa70P2vBC=Cj?a7iKEKnElo+E~RgE(urr%pv4G7R4 zM(5KjV|!MR`~%k)agr*{y1(J|WW+53!84yvAt6*axvOsXABv<^4`!z3oLohRf4}PFbFlUt1O;_chaZ;gX*Bm1oVv*me9veiNLCrjYtHXj zyHseu;a$*-fzT92`pT+ohoqv+rZ_-E!LL4B{7TFVii}Xh6wxHJIn)uZB0)tnM91Eq z{h)WLJcVy_q{h;e4jXN?SDn0I;Iv_q&(CCD>>MJC47&e)wbMYQgMpT^8*7p-#EP&? z^i>YT7WdYY1X>Oyexq@Baj}qQaEjc7Qn_8eWiGS$T}E>wE$fCDIbJoDom=hZs-VPr zh|banbDv3KGS0lYRHBa58vFB^L2dB|Q~}DR+-ua5VG|6TJ|n=>aGK`Yau!NTaE!n+ z2o65f3JwW)f(Je%zy}TvF+Kzi1^C7TK9XM${;L(<=nLY%p4A@-->ON-$pPPLrcUPO z_Rb%{E_iyM#DK0Qtkkt#w3QSEOu=@n#%5p>b5@9*13(D}Cj=1y9_`FsjHw`Yw)V~f z5Mi1p3IX8x@n<#~swWZ`8(|u4B~>a3u#-6z4=Xz>JBN5*bWfs zBJ%H%`6u&#HvW@Qi0#qx|M0{=-2C(_;AasuA+~>wnFyLvhGrNX9Pc+d$+zkd_`OV& z`sY#;A?WpAWYu4W(IB9qF)}jJd_scPC%`wc&_T3$fjb&OOdCNoIqs@tqfNUW=D!F< z!y~|ziv5BVx0|%N0Dg-(z|Q6FS+D^^UGT_~o7WiE0K59RZo3R9Hi!yyapK~@>d1)bhc{FWgtO2vX)QbMFOr zLoof)#%(6kYj$|1dap4_qwEDgllYR$B1uxg2Q(2FN!DFLT{XS&s|_0^#gHVw^n+G2 z;mUMd+ly~~N`mV8m0y!<1GG8_9&~NpKPFSxeoVZab<4+p)QOWWmKUL^5>leGCT#Yo zOe_=9vADki-U1rIpL9DW4fdpnKj}0FK3B(Uwhs=0F|pkw2gvi3HWTS*Ft5}{LlR#E zI5pgBtV$ZA1l3uu(WrPQkXqhrR}B_38!%9_mQ<zlHHVN1vAJWQGVg}T@GuYH1s7EH`rlP9Xh)H3Ys}i^Z7;OS44Pdc zlv0w%(fj-sJe$F1j0Ag(q1ve^*c7(c7YASik2L}=_kF#StBvegl*-|-=`*g|Def>5Yd4q& zVbwtGY>4mfCc=W>1q--F?pf6xmS9S@#Q=AnD7kSO&n%OG&V+B)-6(UFe$rmkeKAf| z%+5p!m$Su#nf2H=t3OGz_220Qj5)1xPd1&6ab>3w>F3xzj?GBj#B49--3>c;LMeSe zHX8>`*`bqK760w*$+xITAFZ-fVXB2RCmZ^DNZKBSd0-H8cJ2FZRNV()n>G{9}$bfdXezEsy8Igs)|YA!%NJfL2xH(d~3!(y@ilINap1?6Op>wyD> zqG`C1p)Ybs*sgZIw$iH7OU?PFgg3?Ke11s1>>X|R$cTc|g0Oqq$zbagw|!;fmFJ(A zT$`l<>nZyTU$s7dngh39{hS6rXj&vAH})vGPTKLNmi72ljNcb#`>~8r=1KdSWI=Xu zPi_u=6kpaJ{GHD5d$+(MdH&n)VbuHbUJ%y*vDw^9^T1g#*BOZnSGl z1;!8|xDLF~QefEX%Y&M`BaS_IA{8XJGjiFNyQ|TyOF5cNpL*fEeruL>Kh)YEfKn5q zcUVzNr>0rVtwyiHPQavj+%$*m#!GSJK3b4w;J1U%ptUA$`>8Q|5kL54Yk#o_Pw z>I{{72lWRkdNZq~X6xp;X;bd!>(<+;1N(=2DZt9Rj%iCL`gIFt-?hq&2RB`72Tpoy z*wQax9;140-ERukd{I)|n{~QZpdx15WAnd#>EFp&i9ilmn2w{kchn8Ogf z(Y*1o-nkwbbiD|O=4ss&ppH>a7~IfC=uc+O9q_30J)*c|zOW%hh52}W+gi9l zkdV#0AQtW?7KUaYrQ3v1W6 z?~~IOe06s#d%UIUl?C=?)3cfshcj6|RR5jx9CdXw9KwfkV9)!N=`^~XFeayRS*An| ziOx<0a~d29c_F7{Zw$O)a67PwZ17pcto0TtzF)y+O6>d0>KMwdkj#8!!Jt$uyYjj* z6rI}pv|W(LyVGBz#M5>359urDt%7@*C@d`T?E=|EOP-b2)KUn$%FyT_r{MU)bQ2-f zK%H7E523ziFtNj^U7XWd0XwQ~$p<+nF*E#bk2{(Svp6}16$ZMzXmGt$I=B^BQ@!z4zusF3(#jfm?PQ{VI*?DWh4`~i9qN-eF{nu>SSF%?3! zg3CKqLrqy%4i$k+dPa{Kw_(=J`1lO+_u=%kg@Fy6mKrPu(#F?+?tnQ^ss*M(*i$@B zu|?UZns!jP7uf2%^c)-w+4|Y1F`7L5=LvVV+`)$sn2k#I30nolIul#7k%9NUPWmaf z3iFM=XG<#I5|gvs!u`5rSuaM`Rw z&vWM|XD=p5%hCd|`Nzge>JpUjZPMqLg!Mi4Fma)^KY1~Imn%Z;$UH(n)bx8~9kS^D z9BUk|aa?>`e&lvx)zI2!Pp^FYhZ|hszFPG#;Fs+&Lbjq*lL?^)6=u%O!@fx%EqI>1 z*;$%*9YQq7aMm3?`;}MXUJp4ct4+c8%CNrobfX00z@}oxe`ux4Q2ZS73oC))(|oMg zr5)Y-mWK&D`x?G7klK825$rxwr#4^%_QfG)wjL|CvajD~blzIi@(AqrWJ)CT6j!+n z>CLg5O;4KzgC+R)grLuDU?!BKr9LP(_x^}j$IC~Bvk*bj!`h`2m>@VMFGghWxdt;Q zW%EPnmFKY}*XmNQQjPWaz^$P#OkU4svdE^dbF_5dqkAf*#%5OQLcP&VpX+)O_k5)+ zivda}Z;HPuha`kxg~Q0)EN*5;?>iEL#-x==m= z(`T95XC8rH_-tGk+co_y2XEV{+8|$jC1i7(=dILQISR92ecAmjGEs-0RfO+~f3?Ub zF{TFU)>SakPsze~LMbe|C+W6*2G_MY)vi=T?%QsM(v{iHkP$kF>4=wiqRK(8ZDBPN zMY?P>Ew&P76~qgEmgivCSgSqE54`$*(ZuWICx2`A6blFEnNiYT^{B1w)mAOR*V*i) zy>VLcOm+zT*ENZoX_-d!UuwyM=6vrLDxy#1S57(#_Eqm~J z7d*g=*mncF8GU(@wWe}kg#@Fm+Hr z#^pCcE6>RU%22vsZ;!IR#UzYgDQza*k?AuVZ)WqF7cX{G9|ooHglGSKy6LKETezj@ z!1XZf@9H=)@6~4(UF+Z!yG9;cGv7lYdz0=#5%QyoA`P2f*>9GG*JlHT6o{675ZJ+2 z)Rnr$3`LS;!?Iu{DsFgIr6>Z^V#p$?&2UIcCBmoJz{s0;G+@O_qTm{c=`w4 zG}(w#)wsL;!FF7@9wmRH(LuHeV2n(vmOFJE<9k-O&;sh zg%5VEMotgS?$boB*atn|tIS5~t36q45so%ebo!scv%>h)V#eYZPNL69appcs_~UA0 z1h~>fPDZ--`N1%hd+6OUI3xlK9`Y=HK2Hnm>hzSk6fN=aQD zq;c2xs*bQx z-{k)4@7+Y6RGWRXPwX8hqQ_xS1N=of!);m1Z7xfQP?styrGrGYxgn+BWfs9uLNlp> zikG3}QM%w~zx0RZ#(-ah68nCe9N?txv-OPDEQO~ z3=~zf$^utoEE6YLKKobe_TchOS5Is%w`J}|Gw75cFC)jc)2x2r01J>O-aBd-Hby&i zyg{^&3BtbAoHf5I<$#I^b?J>C(DYT$zU!S5sUmJ-K#p8#8h?S23&!*r_r(olvmPte z@1>Jt#0;PJSTdk}wyD&P1G;riEJj?PwNHt>sw@GHJ8rXFLqb*KBI5Y0CG)B4_#D$x z3Xye;`O@R^j(#_j9u2zY$2^i{_~(U7yq}UwqC{)X3YE@*tN3wzwzNf_Yi5Le0VZ zSbk&nH;xTjZsXrIcC{k!ZjN51mNHxml#2Rr9_Z``x5@Md=^Kb6cj=h=NTrP9-P(ut zDG)Je3QUl7CZ07O+3sgdyX8kPF$s`6o&woil}-2xCim~J;jCKO=#gy}&t!AvL0}<; zYBqx`v$`r~+w=&6+UqymzKX&yXxYmA^3_jmHiI&B2|g&_#L;Um+mi) z=^hb1fs5_d&DL{!Y4SR&>waIt2iJvaCl8b6_sTR!bSh1Ekn%S+_LM)S66+euKK3%f zRX{Co&ylb#E4q?>e8J=S(Y{KjNfslVke&~Qdu$A;-1n!Rh7cqKueRw=VGDuUKWP}T zmNZy1DNUgEx;l64f`*jzl8$4;^Lan2$1R(Rb6~tvwSRGQdP3U<}g!i-dbH|nLJFtGyE)h0N^Z(kkSdlt|p7Dg+iOoU<{lnF#}X~r44t|%f@T%AGDux z-dR5a8NZkVn(&Q6(c#y#6X?2P!VILukSe3aU})ut1CyU|jqiKHP#y{@wsf0V!ynru zzbJxMo8gjCHOSQ;PxtXuv7?4vvbjF;GzfqyItpS488!NO5#j!zRY7=Ehn9=-83q8j zr~yzEo!K|vQvf?!@MGaNys&@xJTZLv7;>P3R#p4?u?^s&4se1Qw%OkK?R!kAzL9$r zQW@4FAu2 z>6O~!$$O#SrmWdwqRbE+0G=vuk0RwARLtPxSRpTWT9fAjAR|?1l~Fsw2>>IJ%*4IE zIT;ci$l!P8^o%mwQTBScT!ES;FzLU>(Qb4rxdI>|F3+!znRy+|vya)ZMJTUVuJ%wx z zVs<03IqtBNvetVNcfVqCO5e+zhOL5hgf-K{t0BL8QvsJvoF(`6XM{w5BFv$}=PCSK zUlaNPFHqRH|Ct4HB(v1|7U8u-wOylSG%^m8pso4Fx)Rfc7M}qCDzb_ZKGz=2lY#z% z+a3wIyWXuDvVT`+KmSw5AVU;mkpzhc|KVog!9GUx{*vX|{siU#^LeA$c9ciEZRSe5eSlloce=9Wc5?tem#mSs~Jvb`&d zbn8m_Zx1_ZXK%rWMh*3pez%Sf>v#*unUW(~OQXU&uK zUl?!IHcT!ro7{X3U{kc2u3USO+tSC{n9_Q?#ug{RE_S**J?gSQH~k2d)q5^Fnvbj$ zB3Nd+e&#>w!l!k;x@di{+i!jF6$~_nYXy|1xk0;&7G%?VF#FUC^E$A5b`VAz^Gosw zwXf@@=g3)8UEM4Mi`<&E`Sg9=>%B=jh0jx*p9GAG$oOL6oZ-4&NI>#RtjE8*sjdA< z%5(3&joQ2}(Q9yuJHDG8^}$w^ma{zB7^+kIEP=(}WjC{2<9&5pn}FLxrCDJjHCk@m zNu@($RX6W8n{q%)%G=yzw5e=VN% za}A{s8hQ`^-B(N(tebNrVo&1*+l%@hwxhVwub&efGi@MqF$&cye#n!K#+a0|BFTYA zhHaiy`rcjVY)kLW)Yw%4DkmMS6PRCvsw@UL15vPR9Tkx30QjuHb!Xzd9Aqz7`*E0t zDWt0=Y!*+T4PRkGvsl~MzU9i|ac;}LC>SClbQGHPd$_Bc=|&aHoc|#Te`dCQe0wr9 zyG4meK)Z>rB4q4)y~RLUU^Q1?(XbRkdOMBK{IDF!)<3EuY6pc9I?Y%W>QMKfcb`vN zQj&I1lXaaI0i%u#&eq-&zL+z$7|H^+cMDR%$Z6*xVN;59PJN8~XkJ(tXdfzi{}Jac zG7+mk;HFvSyCX3Q@58scFJQlEp+Y8w1woobZHKw3xcp#*;je5)!R_I(J+t6eJ9?EY z+Y>UEzi-_PH;_M$WTpH(H=%X5spqE}3W$RhzR!bo*OgzlN5+IrM7b;eOH^a6+xU z428#6;-z4Jdc_o@K8u3OIp-mrkg=p%Cz0~#zZB9R!*tM%Yx5P7XY9f_&&+sa(?J?W z;WL9`L(BujS#JS�QD~^|e^l78>}H9bdE)Be?&u z?=>d8vIi(nt>M)j_T+HwIShYUW-d16@JCsTb;^W+&)K+KFGlztt}}f`d+@**8THaj z`5v5zNY-1Unp*<)g5|@MXI!$U`_@P|vtIq*h(U7qIb+kUu>_l-^uI(K**56ZFH$%l?|CZR zkwqj?kRicDfrWjYS0H@Jc{^Gmx6PbT5{sZY35Q9W+*%nyxz%oB6ZS$MJ&EdI-}RqH z4l8LJDH#tH7`WZ9O*Q{8NAOT@4*mtj+^i<3%>2dQ7`;4NVX;JZDAmuvfYDxN1nx`w z(hSJfmMTLs8Fsgm!Y31-rNl+X^*$J-FW_)V<4dODXI*4`9PB(Uj6Tak_fI#@kkke) zNid8N1q8DEFZ2Mwi&o=WLAs6qK?1R$M6vqyh6 z(Itbf%fEM3%$h=li$BhE!SE?W=WeNag}l1(mR4w7H%=xE{p?T}jz>fYp(Q$11uQWu zG_W~Q#}KW_X0MB|!Y~Ju``G7J^~ij^?4^jn-ICqx%uqFAUe>-|IcXDvyRC?G0_l-k zPer^{qrwo=sE@nwaq6*ttK&itL>Li1kk7oA6B&6%+@O90Y9E56VBfKF7GD~;LDr5= zQ4K{|?q@QA5*}Ps;#(naB@{9F$fw?mgpJ3r*XEObY9l@A)hNPHV|nWy7B%>Vb_-+)k#)gg&~LJ;R_NO)%=WdGYWz@R?Z%ZGMlA-B+Ygd4;$S6{%*LN1 zRs&0$N+x2tm^a+*6C53rGGHV4e_IL9cI+X(pAUfbqIs#p^>%U5VH~cfgGmZ%!q$B* z<}Ldy<)elTnm9YCwR7|3Gj0`UIgqtKqyNO8w2wLpf-^l_;ahE=bBqzW{^5 zY}9UqZ7B)QHkEqvXUiglGkNJ@8f?k5V@is5CtwjS%zu zajd};)1QCD{E6UZ^%d<=GZql(PXlnV+D{_S?@h?l?Z`U2y;Ikh?x?;N5sltJLW~4V zam=jOSwFb*X7F%6V>pkf3opbQI)uwe4$+dajVtn^IbnNd`lo*^e#Jo%gKcaYP|5Py z{5a{sb0{&qLJv+>{7B`W?$lO$Xh zM_5Qs5|pxLY=|n;!x-Fq3L$2^Xq*EsJ*O#F9-QmaOnx5&(}{^FRONa;hF!7U zKP|luZvOh)@0}xsfb#~_%XI@ekXg-ZpO78mP?bXZc9=u;p?m+2nS(hMgZiR4o4p}x z6aDOY`HVOS7o5KG^W+bU`OB_=25r6rTn&2>L50<>uRnZrs z5!}_V-QO+o<3+xtkQJ(t2*s+k+ow5%+ON7&MB>LoMXrAppa>CyV5EdZpJa*JORUSo zy;Z`nvM6~Oe{I-P;N*3HM5$~c<3Y+P@zh3PF9Q6U+%-9j!gxk=C9~>mpk6*3neG@A zFCBH=LwT|;c7-%zx-q#ey=TLC1fYZY&RL%nHjwah++jjD_2H{gjT`-Nhw=ECU0*{3 zNLN8QlIc6NwWEF+mmsF|zY^HQ*HMC3_wAz|<6YFaJV;QXE|#Mi{L}|;=M60vPf`4b z(`?ls-O+yjw(fmo?rQ>5e>%}6qz+J5`(5A8;SBp^-Fe_em)nM_1O8C3{S=f}Nmwtt zK{eo{MQ(-fr;|Qu231_on)_t<&F;qH%no0JPpP*%vpbnz16@sKwXgG;L)pIeK9`lF z!ENvPMkw(uuI3c3o0yhEND9l!twRFVfYC$EqygzLhPe3Gjh^q$<%`=f`tcSo)wIM5 zG4PakZe6i&e4c?nc&k+gFE``qB9T!%XTT%GD${cTVTCy0TC_!PARy9YF}RC{lR8(> zD28!FuPXB?bkqwDynngKiHCP!o{B~IEbF=ZGA>OMegP`vN3n;bdi~b(y^_BU>HEui^Yv1dfGzDX+Z2WAWXjxl{ZB#Piaxxj3)koZ)XyR_LhCA#Qqn!wCJqBfj;V2;izK z(`@L!$5tj0((DRA55!G=mtcAY)=pHnJYOkN#j-1;y>nHe_xsB|mRWmUxKN5AgtQ&t zu9z9mp{zaQuT5_QIGY$pk8CEJki%hAv369HC{WUh=u>xF0N*t~2ON#$a7Z~vN@7=( z^IT2x2^uVQ|6ucH!8QMUZK0FiCA&js@=m&Q^u^#*ugCKpZ>b>Dc`{cIkLW_;d+sBc zGZvTs)zB!!YINR!5h25#Ze^`7;ShxTR}pp_=I(7bgpB}640@F!T>kzUc5(np8DUwx z&U-n8j2|!28uq6DTI!aj{|O;^_6>2=x8u2DjTucWBfL0z}Wdi1gI5z{k^ z0jQj9H7XxpUcRJ)R}PaAd;}AzoB^s-(;x9qFVp~lCoVMe`VmM>rKN%&B-2e|etbbi zmG#G89#(z=EXe?>^{MY>k1w*afRZ>+x5uKb|C1?He_Lrzu)hPPUIAT5mkK~>)JqAf zSU9yW%B5!Ij7m9pu?g?L31@$3B={s7o$dsHmtkTvfNEB>>hMoyk^n-j=MohjoiRcW z7(kkX2FS+=7z6yLwo;(?M8yM8&44tXD#*S*LX#xCYj}@T)qnw`(%%pOZb_;)fd9to zUnf60~Hw2=HHFk5a=El^sA;jr$$$$;@cLe{byn&^=OF0qALAwCXc} zJ50ld%_n$6SM-6p1watw8B6f&l;GS*+G^KxS@LDXC>*vb27krP)Yw?AcISs^ zE~f(~qCx$I77w+*zkJjJBq@eK21>?znotlxy`>6uxkvBdgaPW|u+lxvjzhq0ou)f^+aIsk*Pp!xDo{uBfH4RCl}@#w-J zV4{*yC?vgnbWQ-!FXGaBlP67}fIy+oj870U0?;o?M}F6nCKP}`2(w1~BRdie&~K1J z_|ps&0tEiIA(B-8JtF^qPOVpHi66q%=-K`5y-$8KWTybQEXNvWE>{f6n?H}JMX_GP zh1Ck)%eLwCH@%hsv@Pw8O+Ns0_5-lV zz;bt_QiIc4zkTE03$%2dDvQ+L2@Isy`S0?i&)?;U2dn;JO%zAQAi}X$bBzv);5tB0 zK*?xly$3ME;>o8YW04xeXrnuLw8`T{70TzjJr>rc^0PU(?^s7+R#hkWvl$fJzpG%YDGqa z>4CET61(!^7+oHy{0`L1XUE^(=gWu5gd2{1*Z8q}RD_sA6O_F&`CWs_mwpd52?#Fg z86JiyNPI!8uySJw$;4qH549o*eq@B%G;FtiQX zftPLkBky$HEBWBkhQ}=G+IlbLC?vD4e?n(vzdE$zLdFRqY^Rb|ByDs*$U?%T!v(gN zNPnbyr1&b!q1r4Hpd?f&Tf|3RHdD8eSeCWr(xj7j6Qn@(t8xFQjr|+(8iD{a`Y*vb zbS@MfT?hQ-o>Yd>wZqagjKB@;D`6nkBNa0!XMEJcqEZF0WUo(&4b722`{(gt3!r#f zYzw$)WLS)n`IRV?-E27NJtM2T_Dqn*lK{A1jfLwqj>0(-OK}5?wb9AdkI zJ@(kg>H8UAi#UiP<|@9Vl4R~<M+A59|>~+xWp2l~=Ca5@9$*ukSX=7JC1d7iU`UzE3UZlg>8X++~Iv;|B zX5b$RT)(7oTbXQ6mQhlS3o)VcuzLd6Q76Ub)-j+&mVi%$ivS{?Hc_IRY{$0RHf-EQ z>GkKo`oxgG#})sa;!i}kp4Uthiv}zq#<4`1$7DjB3fT1Fb0eNoIQ7RAE~h}~DK$U< zs6aRxK~KqA2A^RK#(M$eoEnh zDN`t}1W@5neFr@yXTiYO5?KGCeWaoWs6yf1NdeqpVvLU|oT?hdQ_2zmP$7je zia(`rz^Ew)ryW00fdDFMF?HNWB_B;Gm)YCM_EQelf4hd8_i5YyO>H9Rc{SWn2LI?i z`7m&8q$6MNe8JAX3=TJ<2c|nHRO~If1CY`w_QsH7`qhc}-d;L{tsB2C55O({Hw8OL z(0$*QgxB8o!O5pH1anDsAn(0V$HH(YNb@m!J_J2)f}8yIt`n{AR~&80-=I()QF(E1 zw=@7hy3_@)W8ah3*71(Y$A!6Um@iL2dO;@WX8SgTrv6BJHvFzb9I)=` zduJ141&DqOw|kNjvvKX6RyUFtiXEeVWHczM9n==vAZ`qc0g zFXn))>sMtpaM9Hxmn52Qm8a__J!=kFmdXH(bQ=*{y=KXWXSnFn-c~>Co9Otcs}+#M z9}gRF)xhO44~4v*3lK$7->;vu&a9iXR_g#dsR{rdrE{RC@cjE0WjbF6K4%u!c>ib|N8M zgXQMF&n%G`FM*x>ckuRxBspCh|FDvdnjESFntuDj$)Yfg+08*~E5Qt7%b~fVtS})#Ur;oJ`seX5AV>ynljrwc`7{lVi@I^;c^oo^t>gvx&aO0UQ02jo0 zI&}UWHwCagE&IejpWDifrHTqXpSsR zS}H4nkits3-{7f^>hSt>PDS+2EI9se+qZXl;x|3S4^zXX`huj}VpF6%w%TYV!_;`d zsfi&s&u17=Ygm%=GWAV8P&t8p2I@;@tFwudU{MdwR|JA2=g&|5ui#mK*=yx-NdfP# zE3b4)eWMseZ!}7P1KTfnAQF;ozSMXJ#7Y!IhasQX@J^!(d9zJlv Xc(6=DtW6&O_fk$uS+Z2z=+plI>y%8Q diff --git a/src/main/java/lvp/views/canvasturtle/index.html b/src/main/java/lvp/views/canvasturtle/index.html deleted file mode 100644 index 8ce8eab..0000000 --- a/src/main/java/lvp/views/canvasturtle/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Turtle Graphics - - - - - - - diff --git a/src/main/java/lvp/views/canvasturtle/turtle.js b/src/main/java/lvp/views/canvasturtle/turtle.js deleted file mode 100644 index 357f9b8..0000000 --- a/src/main/java/lvp/views/canvasturtle/turtle.js +++ /dev/null @@ -1,91 +0,0 @@ -class Turtle { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.reset(); - } - - reset() { - this.ctx.reset(); - this.x = this.canvas.width / 2; - this.y = this.canvas.height / 2; - this.angle = 0; - this.penDown(); - this.color("black"); - } - - penDown() { - this.isPenDown = true; - } - - penUp() { - this.isPenDown = false; - } - - forward(distance) { - const radians = (this.angle * Math.PI) / 180; - const newX = this.x + distance * Math.cos(radians); - const newY = this.y + distance * Math.sin(radians); - - if (this.isPenDown) { - this.ctx.beginPath(); - this.ctx.moveTo(this.x, this.y); - this.ctx.lineTo(newX, newY); - this.ctx.stroke(); - } - - this.x = newX; - this.y = newY; - } - - backward(distance) { - this.forward(-distance); - } - - right(degrees) { - this.angle += degrees; - } - - left(degrees) { - this.angle -= degrees; - } - - color(color) { - this.ctx.strokeStyle = color; - } - - lineWidth(width) { - this.ctx.lineWidth = width; - } - - text(text, font = '10px sans-serif', align = 'center') { - const radians = (this.angle * Math.PI) / 180 + Math.PI / 2.0; - this.ctx.save(); - this.ctx.translate(this.x, this.y); - this.ctx.rotate(radians); - this.ctx.font = font; - this.ctx.fillStyle = this.ctx.strokeStyle; - this.ctx.textAlign = align; - this.ctx.fillText(text, 0, 0); - this.ctx.restore(); - } - moveTo(x, y) { - this.x = x; - this.y = y; - } - - lineTo(x, y) { - const originalPenState = this.isPenDown; - this.isPenDown = true; - - this.ctx.beginPath(); - this.ctx.moveTo(this.x, this.y); - this.ctx.lineTo(x, y); - this.ctx.stroke(); - - this.x = x; - this.y = y; - - this.isPenDown = originalPenState; - } -} From 2d2657d068f83042816668ec5ce28003bcd4937c Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 13:13:18 +0200 Subject: [PATCH 09/50] fixed idgen in Interaction Skill --- src/main/java/lvp/skills/Interaction.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/lvp/skills/Interaction.java b/src/main/java/lvp/skills/Interaction.java index da12092..6a91951 100644 --- a/src/main/java/lvp/skills/Interaction.java +++ b/src/main/java/lvp/skills/Interaction.java @@ -4,7 +4,6 @@ import java.nio.file.Path; import java.util.Base64; -import lvp.Clerk; public class Interaction { public static String eventFunction(String path, String label, String replacement) { @@ -37,7 +36,7 @@ public static String input(String path, String label, String template, String pl return input(Path.of(path), label, template, placeholder, type); } public static String input(Path path, String label, String template, String placeholder, String type) { - String id = Clerk.generateID(10); + String id = IdGen.generateID(10); String inputField = Text.fillOut(""" @@ -59,7 +58,7 @@ public static String checkbox(String path, String label, String template, boolea return checkbox(Path.of(path), label, template, checked); } public static String checkbox(Path path, String label, String template, boolean checked) { - String id = Clerk.generateID(10); + String id = IdGen.generateID(10); return Text.fillOut(""" " + content + ""); - // Using `preformatted` is a hack to get a Java String into the Browser without interpretation - - consumeJSCall(id, "var scriptElement = document.getElementById('" + id + "');" - + - """ - var divElement = document.createElement('div'); - divElement.id = scriptElement.id; - divElement.innerHTML = window.md.render(scriptElement.textContent); - scriptElement.parentNode.replaceChild(divElement, scriptElement); - """ - ); + Text.clear(); } } diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java new file mode 100644 index 0000000..aa300bf --- /dev/null +++ b/src/main/java/lvp/commands/services/Text.java @@ -0,0 +1,50 @@ +package lvp.commands.services; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lvp.logging.Logger; +import lvp.skills.TextUtils; + +public class Text { + private Text() {} + static Map templates = new HashMap<>(); + + public static void clear() { + templates.clear(); + } + + public static String codeblock(String id, String content) { + String[] parts = content.split(":"); + if (parts.length != 2) { + Logger.logError("Invalid Codeblock Format."); + return null; + } + return TextUtils.codeBlock(parts[0].trim(), parts[1].trim()); + } + + public static String of(String id, String content) { + String newValue = templates.merge(id, content, Text::fillOut); + return newValue == null ? content : newValue; + } + + static String fillOut(String template, String replacement) { + Pattern pattern = Pattern.compile("\\$\\{(.*?)\\}"); // `${}` + Matcher matcher = pattern.matcher(template); + StringBuffer result = new StringBuffer(); + String key = ""; + + while (matcher.find()) { + String group = matcher.group(1); + if (key.isBlank()) key = group; + if (!key.equals(group)) continue; + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } +} diff --git a/src/main/java/lvp/views/Turtle.java b/src/main/java/lvp/commands/services/Turtle.java similarity index 79% rename from src/main/java/lvp/views/Turtle.java rename to src/main/java/lvp/commands/services/Turtle.java index 565c39c..d87b761 100644 --- a/src/main/java/lvp/views/Turtle.java +++ b/src/main/java/lvp/commands/services/Turtle.java @@ -1,4 +1,4 @@ -package lvp.views; +package lvp.commands.services; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; @@ -11,7 +11,7 @@ import lvp.skills.IdGen; import lvp.skills.Interaction; -import lvp.skills.Text; +import lvp.skills.TextUtils; /** * Turtle ermöglicht das Erstellen einfacher Turtle-Grafiken als SVG-Datei. @@ -21,18 +21,21 @@ * Die einzelnen graphischen Elemente werden durchnummeriert in der Reihenfolge ihrer Erzeugung. */ public class Turtle { - public final String ID = IdGen.getHashID(this); + public final String id; private final double xFrom, yFrom, viewWidth, viewHeight; private final List elements = new ArrayList<>(); private int elementCounter = 0; private State state; private final Deque stack = new ArrayDeque<>(); - public Turtle() { - this(500, 500); + public static String of(String id, String content) { + + Turtle turtle = new Turtle(id, 0, 0); + return turtle.toString() + turtle.timelineSlider(); } - public Turtle(int width, int height) { - this(0, width, 0, height, width / 2.0, height / 2.0, 0); + + public Turtle(String id, int width, int height) { + this(id, 0, width, 0, height, width / 2.0, height / 2.0, 0); } /** @@ -44,8 +47,9 @@ public Turtle(int width, int height) { * @param startY Start-Y-Koordinate der Turtle * @param startAngle Blickrichtung in Grad (0°=rechts, 90°=oben, gegen den Uhrzeigersinn) */ - public Turtle(double xFrom, double xTo, double yFrom, double yTo, + public Turtle(String id, double xFrom, double xTo, double yFrom, double yTo, double startX, double startY, double startAngle) { + this.id = id; this.xFrom = xFrom; this.yFrom = yFrom; this.viewWidth = xTo - xFrom; @@ -149,50 +153,33 @@ public Turtle pop() { return this; } - public Turtle write() { - // Clerk.write(Text.fillOut("
${1}
", ID, toString())); - return this; - } - - public Turtle timelineSlider() { - // Clerk.write(Text.fillOut(""" - //
- // Linien sichtbar: ${1} / ${1} - //
- // """, ID, elements.size())); - // Clerk.write( - // Interaction.slider(ID, 0, elements.size(), elements.size(), Text.fillOut(""" - // ((e) => { - // const n = e.target.value; - // const statusCurrent = document.getElementById("currentLine${0}"); - // statusCurrent.textContent = n; - // const svgElement = document.getElementById("turtle${0}"); - // const lineIds = Array.from(svgElement.querySelectorAll("[svg-id]")).map(el => el.getAttribute("svg-id")); - // lineIds.forEach((id,i) => { - // const el = svgElement.querySelector(`[svg-id="` + id + `"]`); - // if (el) el.style.display = i < n ? "" : "none"; - // }); - // })(event) - // """, ID)) - // ); - - return this; + public String timelineSlider() { + String status = TextUtils.fillOut(""" +
+ Linien sichtbar: ${1} / ${1} +
+ """, id, elements.size()); + String slider = Interaction.slider(id, 0, elements.size(), elements.size(), TextUtils.fillOut(""" + ((e) => { + const n = e.target.value; + const statusCurrent = document.getElementById("currentLine${0}"); + statusCurrent.textContent = n; + const svgElement = document.getElementById("turtle${0}"); + const lineIds = Array.from(svgElement.querySelectorAll("[svg-id]")).map(el => el.getAttribute("svg-id")); + lineIds.forEach((id,i) => { + const el = svgElement.querySelector(`[svg-id="` + id + `"]`); + if (el) el.style.display = i < n ? "" : "none"; + }); + })(event) + """, id)); + + return status + slider; } public void save(String filename) throws IOException { Path path = Path.of(filename); try (BufferedWriter writer = Files.newBufferedWriter(path)) { - writer.write( - String.format(Locale.US, - """ - - - """, xFrom, yFrom, viewWidth, viewHeight) - ); - for (Element e : elements) { - writer.write(elementString(e)); - } - writer.write("\n"); + writer.write(toString()); } } diff --git a/src/main/java/lvp/commands/targets/Targets.java b/src/main/java/lvp/commands/targets/Targets.java new file mode 100644 index 0000000..3ad76fb --- /dev/null +++ b/src/main/java/lvp/commands/targets/Targets.java @@ -0,0 +1,55 @@ +package lvp.commands.targets; + +import lvp.SSEType; +import lvp.Server; +import lvp.commands.targets.dot.GraphSpec; + +public class Targets { + Server server; + + public static Targets of(Server server) { return new Targets(server); } + + private Targets(Server server) { + this.server = server; + } + + public void consumeClear(String id, String content) { + server.sendServerEvent(SSEType.CLEAR, ""); + } + + public void consumeHTML(String id, String content) { + server.sendServerEvent(SSEType.WRITE, content); + } + + public void consumeJS(String id, String content) { + server.sendServerEvent(SSEType.SCRIPT, content); + } + + public void consumeJSCall(String id, String content) { + server.sendServerEvent(SSEType.CALL, content); + } + + public void consumeMarkdown(String id, String content) { + consumeHTML("container" + id, ""); + // Using `preformatted` is a hack to get a Java String into the Browser without interpretation + + consumeJSCall("call" + id, "var scriptElement = document.getElementById('" + id + "');" + + + """ + var divElement = document.createElement('div'); + divElement.id = scriptElement.id; + divElement.innerHTML = window.md.render(scriptElement.textContent); + scriptElement.parentNode.replaceChild(divElement, scriptElement); + """ + ); + } + + public void consumeDot(String id, String content) { + GraphSpec specs = GraphSpec.fromContent(content); + + consumeHTML("container" + id, "
"); + consumeJS("script" + id, "clerk.dot" + id + " = new Dot(document.getElementById('dotContainer" + id + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); + consumeJSCall("call" + id, "clerk.dot" + id + ".draw(\"" + specs.dot() + "\")"); + } + +} diff --git a/src/main/java/lvp/commands/targets/dot/GraphSpec.java b/src/main/java/lvp/commands/targets/dot/GraphSpec.java new file mode 100644 index 0000000..44e3989 --- /dev/null +++ b/src/main/java/lvp/commands/targets/dot/GraphSpec.java @@ -0,0 +1,37 @@ +package lvp.commands.targets.dot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public record GraphSpec(Optional width, Optional height, String dot) { + + public static GraphSpec fromContent(String content) { + Optional width = Optional.empty(); + Optional height = Optional.empty(); + + List dotLines = new ArrayList<>(); + + for (String line : content.lines().toList()) { + String trimmed = line.trim(); + if (trimmed.startsWith("width:")) { + width = tryInt(trimmed.substring(6)); + } else if (trimmed.startsWith("height:")) { + height = tryInt(trimmed.substring(7)); + } else { + dotLines.add(line); + } + } + + String dot = String.join(" ", dotLines).trim(); + return new GraphSpec(width, height, dot); + } + + private static Optional tryInt(String s) { + try { + return Optional.of(Integer.parseInt(s.trim())); + } catch (NumberFormatException _) { + return Optional.empty(); + } + } +} \ No newline at end of file diff --git a/src/main/java/lvp/views/dot/dot.js b/src/main/java/lvp/commands/targets/dot/dot.js similarity index 100% rename from src/main/java/lvp/views/dot/dot.js rename to src/main/java/lvp/commands/targets/dot/dot.js diff --git a/src/main/java/lvp/views/dot/vis-network.min.js b/src/main/java/lvp/commands/targets/dot/vis-network.min.js similarity index 100% rename from src/main/java/lvp/views/dot/vis-network.min.js rename to src/main/java/lvp/commands/targets/dot/vis-network.min.js diff --git a/src/main/java/lvp/views/markdown/CompileMathjax3.md b/src/main/java/lvp/commands/targets/markdown/CompileMathjax3.md similarity index 100% rename from src/main/java/lvp/views/markdown/CompileMathjax3.md rename to src/main/java/lvp/commands/targets/markdown/CompileMathjax3.md diff --git a/src/main/java/lvp/views/markdown/default.min.css b/src/main/java/lvp/commands/targets/markdown/default.min.css similarity index 100% rename from src/main/java/lvp/views/markdown/default.min.css rename to src/main/java/lvp/commands/targets/markdown/default.min.css diff --git a/src/main/java/lvp/views/markdown/highlight.min.js b/src/main/java/lvp/commands/targets/markdown/highlight.min.js similarity index 100% rename from src/main/java/lvp/views/markdown/highlight.min.js rename to src/main/java/lvp/commands/targets/markdown/highlight.min.js diff --git a/src/main/java/lvp/views/markdown/interactiveCodeblocks.js b/src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js similarity index 100% rename from src/main/java/lvp/views/markdown/interactiveCodeblocks.js rename to src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js diff --git a/src/main/java/lvp/views/markdown/markdown-it.min.js b/src/main/java/lvp/commands/targets/markdown/markdown-it.min.js similarity index 100% rename from src/main/java/lvp/views/markdown/markdown-it.min.js rename to src/main/java/lvp/commands/targets/markdown/markdown-it.min.js diff --git a/src/main/java/lvp/views/markdown/mathjax3.js b/src/main/java/lvp/commands/targets/markdown/mathjax3.js similarity index 100% rename from src/main/java/lvp/views/markdown/mathjax3.js rename to src/main/java/lvp/commands/targets/markdown/mathjax3.js diff --git a/src/main/java/lvp/views/markdown/vs.css b/src/main/java/lvp/commands/targets/markdown/vs.css similarity index 100% rename from src/main/java/lvp/views/markdown/vs.css rename to src/main/java/lvp/commands/targets/markdown/vs.css diff --git a/src/main/java/lvp/skills/Interaction.java b/src/main/java/lvp/skills/Interaction.java index 6a91951..516d87f 100644 --- a/src/main/java/lvp/skills/Interaction.java +++ b/src/main/java/lvp/skills/Interaction.java @@ -10,22 +10,22 @@ public static String eventFunction(String path, String label, String replacement return eventFunction(Path.of(path), label, replacement); } public static String eventFunction(Path path, String label, String replacement) { - return Text.fillOut("fetch(\"interact\", { method: \"post\", body: \"${0}:${1}:single:${2}\" }).catch(console.error);", + return TextUtils.fillOut("fetch(\"interact\", { method: \"post\", body: \"${0}:${1}:single:${2}\" }).catch(console.error);", Base64.getEncoder().encodeToString(path.normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), Base64.getEncoder().encodeToString(label.getBytes(StandardCharsets.UTF_8)), Base64.getEncoder().encodeToString(replacement.getBytes(StandardCharsets.UTF_8))); } public static String button(String text, int width, int height, String onClick) { - return Text.fillOut("", width, height, onClick, text); + return TextUtils.fillOut("", width, height, onClick, text); } public static String button(String text, String onClick) { - return Text.fillOut("", onClick, text); + return TextUtils.fillOut("", onClick, text); } public static String slider(String id, double min, double max, double value, String onInput) { - return Text.fillOut("", + return TextUtils.fillOut("", id, min, max, value, onInput); } @@ -37,11 +37,11 @@ public static String input(String path, String label, String template, String pl } public static String input(Path path, String label, String template, String placeholder, String type) { String id = IdGen.generateID(10); - String inputField = Text.fillOut(""" + String inputField = TextUtils.fillOut(""" """, id, placeholder, type, label.replaceFirst("//", "").trim()); - String button = button("Send", Text.fillOut(""" + String button = button("Send", TextUtils.fillOut(""" (() => { const input = document.getElementById("input${0}"); const result = `${3}`.replace("$", input.value); @@ -59,7 +59,7 @@ public static String checkbox(String path, String label, String template, boolea } public static String checkbox(Path path, String label, String template, boolean checked) { String id = IdGen.generateID(10); - return Text.fillOut(""" + return TextUtils.fillOut(""" "); - //Clerk.script("clerk.dot" + ID + " = new Dot(document.getElementById('dotContainer" + ID + "'), " + this.width + ", " + this.height + ");"); - } - - public Dot() { this(500, 500); } - - public Dot draw(String dotString) { - String escaped = dotString.replaceAll("\\\"", "\\\\\"").replaceAll("\\n", ""); - //Clerk.script("clerk.dot" + ID + ".draw(\"" + escaped + "\")"); - return this; - } -} \ No newline at end of file diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index 2c46f01..f36b5e5 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -6,13 +6,13 @@ Clerk in Java Prototype - - - - - - - + + + + + + + From c8771ef2365bf3aad426a32bf252d1d61c98585d Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 20:00:45 +0200 Subject: [PATCH 11/50] added turtle --- newdemo.java | 16 ++ src/main/java/lvp/Processor.java | 12 +- .../java/lvp/commands/services/Turtle.java | 21 ++- .../lvp/{ => skills}/InstructionParser.java | 3 +- src/main/java/lvp/skills/TurtleParser.java | 138 ++++++++++++++++++ 5 files changed, 179 insertions(+), 11 deletions(-) rename src/main/java/lvp/{ => skills}/InstructionParser.java (99%) create mode 100644 src/main/java/lvp/skills/TurtleParser.java diff --git a/newdemo.java b/newdemo.java index d51b9d3..763cd75 100644 --- a/newdemo.java +++ b/newdemo.java @@ -1,3 +1,5 @@ +import static java.io.IO.println; + void main() { println("Clear:~"); println("Markdown: # Hello World!"); @@ -44,6 +46,20 @@ void main() { """); // ex1 + println(""" + Text: + init 0 200 0 25 50 0 0 + forward 25 + right 60 + backward 25 + right 60 + forward 25 + timeline + ~~~ + | Turtle | Html + """); + + println(""" Dot: width: 1000 diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 428c541..4d255f3 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -9,18 +9,22 @@ import java.util.function.BiFunction; import java.util.stream.Gatherers; -import lvp.InstructionParser.Command; -import lvp.InstructionParser.CommandRef; -import lvp.InstructionParser.Pipe; import lvp.commands.services.Text; import lvp.commands.services.Turtle; import lvp.commands.targets.Targets; import lvp.logging.Logger; +import lvp.skills.InstructionParser; +import lvp.skills.InstructionParser.Command; +import lvp.skills.InstructionParser.CommandRef; +import lvp.skills.InstructionParser.Pipe; public class Processor { Server server; Targets targetProcessor; - Map> services = new HashMap<>(Map.of("Text", Text::of, "Codeblock", Text::codeblock, "Turtle", Turtle::of)); Map> targets; + Map> services = new HashMap<>(Map.of( + "Text", Text::of, + "Codeblock", Text::codeblock, + "Turtle", Turtle::of)); public Processor(Server server) { this.server = server; diff --git a/src/main/java/lvp/commands/services/Turtle.java b/src/main/java/lvp/commands/services/Turtle.java index d87b761..267f660 100644 --- a/src/main/java/lvp/commands/services/Turtle.java +++ b/src/main/java/lvp/commands/services/Turtle.java @@ -12,6 +12,7 @@ import lvp.skills.IdGen; import lvp.skills.Interaction; import lvp.skills.TextUtils; +import lvp.skills.TurtleParser; /** * Turtle ermöglicht das Erstellen einfacher Turtle-Grafiken als SVG-Datei. @@ -26,12 +27,12 @@ public class Turtle { private final List elements = new ArrayList<>(); private int elementCounter = 0; private State state; + private boolean showTimeline = false; private final Deque stack = new ArrayDeque<>(); public static String of(String id, String content) { - - Turtle turtle = new Turtle(id, 0, 0); - return turtle.toString() + turtle.timelineSlider(); + Turtle turtle = TurtleParser.parse(id, content); + return turtle.toString(); } public Turtle(String id, int width, int height) { @@ -153,6 +154,11 @@ public Turtle pop() { return this; } + public Turtle timeline() { + showTimeline = true; + return this; + } + public String timelineSlider() { String status = TextUtils.fillOut("""
@@ -173,7 +179,7 @@ public String timelineSlider() { })(event) """, id)); - return status + slider; + return String.join(System.lineSeparator(), status, slider); } public void save(String filename) throws IOException { @@ -200,7 +206,12 @@ public String toString() { out += "\n"; - return out; + return TextUtils.fillOut(""" +
+ ${1} +
+ ${2} + """, id, out, showTimeline ? timelineSlider() : ""); } private String elementString(Element e) { diff --git a/src/main/java/lvp/InstructionParser.java b/src/main/java/lvp/skills/InstructionParser.java similarity index 99% rename from src/main/java/lvp/InstructionParser.java rename to src/main/java/lvp/skills/InstructionParser.java index e1e18cc..78ff026 100644 --- a/src/main/java/lvp/InstructionParser.java +++ b/src/main/java/lvp/skills/InstructionParser.java @@ -1,4 +1,4 @@ -package lvp; +package lvp.skills; import java.util.StringJoiner; import java.util.regex.Matcher; @@ -11,7 +11,6 @@ import java.util.Arrays; import lvp.logging.Logger; -import lvp.skills.IdGen; public class InstructionParser { diff --git a/src/main/java/lvp/skills/TurtleParser.java b/src/main/java/lvp/skills/TurtleParser.java new file mode 100644 index 0000000..ed88c2b --- /dev/null +++ b/src/main/java/lvp/skills/TurtleParser.java @@ -0,0 +1,138 @@ +package lvp.skills; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import lvp.commands.services.Turtle; +import lvp.logging.Logger; + +public class TurtleParser { + private TurtleParser() {} + + private static final Pattern INIT_PATTERN = Pattern.compile("^init\\s+(\\d+)\\s+(\\d+)$"); + private static final Pattern INIT_ALT_PATTERN = Pattern.compile("^init\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)$"); + + public static Turtle parse(String id, String content) { + Stream lines = content.lines().map(String::trim).filter(line -> !line.isEmpty()); + + java.util.Iterator iterator = lines.iterator(); + Optional turtleOptional = Optional.empty(); + + while (iterator.hasNext()) { + String line = iterator.next(); + Logger.logDebug("Parsing line: " + line); + + if (turtleOptional.isEmpty()) { + turtleOptional = parseInit(id, line); + if (turtleOptional.isPresent()) { + Logger.logDebug("Turtle initialized."); + continue; + } + Logger.logDebug("No init line detected, using default size."); + turtleOptional = Optional.of(new Turtle(id, 400, 400)); // Fallback + } + + Turtle turtle = turtleOptional.get(); + parseCommand(turtle, line); + } + + return turtleOptional.orElse(new Turtle(id, 400, 400)); + } + + private static Optional parseInit(String id, String line) { + Matcher mSimple = INIT_PATTERN.matcher(line); + if (mSimple.matches()) { + int width = Integer.parseInt(mSimple.group(1)); + int height = Integer.parseInt(mSimple.group(2)); + return Optional.of(new Turtle(id, width, height)); + } + + Matcher mExtended = INIT_ALT_PATTERN.matcher(line); + if (mExtended.matches()) { + double xFrom = Double.parseDouble(mExtended.group(1)); + double xTo = Double.parseDouble(mExtended.group(2)); + double yFrom = Double.parseDouble(mExtended.group(3)); + double yTo = Double.parseDouble(mExtended.group(4)); + double startX = Double.parseDouble(mExtended.group(5)); + double startY = Double.parseDouble(mExtended.group(6)); + double angle = Double.parseDouble(mExtended.group(7)); + + return Optional.of(new Turtle(id, xFrom, xTo, yFrom, yTo, startX, startY, angle)); + } + + return Optional.empty(); + } + + private static void parseCommand(Turtle turtle, String line) { + String[] parts = line.split("\\s+"); + if (parts.length == 0) return; + + try { + switch (parts[0]) { + case "penUp": + turtle.penUp(); + break; + case "penDown": + turtle.penDown(); + break; + case "forward": + turtle.forward(Double.parseDouble(parts[1])); + break; + case "backward": + turtle.backward(Double.parseDouble(parts[1])); + break; + case "right": + turtle.right(Double.parseDouble(parts[1])); + break; + case "left": + turtle.left(Double.parseDouble(parts[1])); + break; + case "color": + int r = Integer.parseInt(parts[1]); + int g = Integer.parseInt(parts[2]); + int b = Integer.parseInt(parts[3]); + if (parts.length == 5) { + double a = Double.parseDouble(parts[4]); + turtle.color(r, g, b, a); + } else { + turtle.color(r, g, b); + } + break; + case "text": + String text = parts[1]; + if (parts.length >= 3) { + String font = parts[2]; + turtle.text(text, font); + } else { + turtle.text(text); + } + break; + case "width": + turtle.width(Double.parseDouble(parts[1])); + break; + case "push": + turtle.push(); + break; + case "pop": + turtle.pop(); + break; + case "timeline": + turtle.timeline(); + break; + case "save": + if (parts.length >= 2) { + String filename = parts[1]; + turtle.save(filename); + Logger.logInfo("Saved SVG to: " + filename); + } + break; + default: + Logger.logError("Unknown command: '" + line + "'"); + } + } catch (Exception e) { + Logger.logError("Failed to parse command: '" + line + "' → " + e.getMessage(), e); + } + } +} From 47c900baba774cda22f0151d2e5eba2e2c1879a9 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 20:02:37 +0200 Subject: [PATCH 12/50] remove unused import for IdGen in Turtle.java --- src/main/java/lvp/commands/services/Turtle.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/lvp/commands/services/Turtle.java b/src/main/java/lvp/commands/services/Turtle.java index c75224e..64a0f4c 100644 --- a/src/main/java/lvp/commands/services/Turtle.java +++ b/src/main/java/lvp/commands/services/Turtle.java @@ -9,7 +9,6 @@ import java.util.List; import java.util.Locale; -import lvp.skills.IdGen; import lvp.skills.Interaction; import lvp.skills.TextUtils; import lvp.skills.TurtleParser; From c784cccbb83d6b0b65a1c6e8b38c64c44d2352d5 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 20:17:39 +0200 Subject: [PATCH 13/50] moved logging to skills --- newdemo.java | 4 +++- src/main/java/lvp/FileWatcher.java | 2 +- src/main/java/lvp/Main.java | 7 ++++--- src/main/java/lvp/Processor.java | 2 +- src/main/java/lvp/Server.java | 4 ++-- src/main/java/lvp/commands/services/Text.java | 2 +- src/main/java/lvp/skills/InstructionParser.java | 5 +++-- src/main/java/lvp/skills/TextUtils.java | 1 - src/main/java/lvp/skills/TurtleParser.java | 2 +- .../java/lvp/{ => skills}/logging/ConsoleDestination.java | 2 +- .../java/lvp/{ => skills}/logging/FileDestination.java | 2 +- src/main/java/lvp/{ => skills}/logging/LogDestination.java | 2 +- src/main/java/lvp/{ => skills}/logging/LogEntry.java | 2 +- src/main/java/lvp/{ => skills}/logging/LogLevel.java | 2 +- src/main/java/lvp/{ => skills}/logging/Logger.java | 2 +- 15 files changed, 22 insertions(+), 19 deletions(-) rename src/main/java/lvp/{ => skills}/logging/ConsoleDestination.java (91%) rename src/main/java/lvp/{ => skills}/logging/FileDestination.java (97%) rename src/main/java/lvp/{ => skills}/logging/LogDestination.java (72%) rename src/main/java/lvp/{ => skills}/logging/LogEntry.java (72%) rename src/main/java/lvp/{ => skills}/logging/LogLevel.java (92%) rename src/main/java/lvp/{ => skills}/logging/Logger.java (98%) diff --git a/newdemo.java b/newdemo.java index 763cd75..b5d76ef 100644 --- a/newdemo.java +++ b/newdemo.java @@ -47,7 +47,7 @@ void main() { // ex1 println(""" - Text: + Text{2}: init 0 200 0 25 50 0 0 forward 25 right 60 @@ -57,6 +57,8 @@ void main() { timeline ~~~ | Turtle | Html + Text{2}: ~ + | Markdown """); diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index bf56c24..9d56804 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import lvp.logging.Logger; +import lvp.skills.logging.Logger; public class FileWatcher { private WatchService watcher; diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index 1eb0ce2..5c170a8 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -5,14 +5,15 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.regex.Matcher; + +import lvp.skills.logging.LogLevel; +import lvp.skills.logging.Logger; + import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import lvp.logging.LogLevel; -import lvp.logging.Logger; - public class Main { private record Config(Path path, String fileNamePattern, int port, LogLevel logLevel){} public static void main(String[] args) { diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 4d255f3..5fa632e 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -12,11 +12,11 @@ import lvp.commands.services.Text; import lvp.commands.services.Turtle; import lvp.commands.targets.Targets; -import lvp.logging.Logger; import lvp.skills.InstructionParser; import lvp.skills.InstructionParser.Command; import lvp.skills.InstructionParser.CommandRef; import lvp.skills.InstructionParser.Pipe; +import lvp.skills.logging.Logger; public class Processor { Server server; Targets targetProcessor; diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index 1c50398..d77d750 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -18,8 +18,8 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; -import lvp.logging.LogLevel; -import lvp.logging.Logger; +import lvp.skills.logging.LogLevel; +import lvp.skills.logging.Logger; public class Server { diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index aa300bf..302798f 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -5,8 +5,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import lvp.logging.Logger; import lvp.skills.TextUtils; +import lvp.skills.logging.Logger; public class Text { private Text() {} diff --git a/src/main/java/lvp/skills/InstructionParser.java b/src/main/java/lvp/skills/InstructionParser.java index 78ff026..4639815 100644 --- a/src/main/java/lvp/skills/InstructionParser.java +++ b/src/main/java/lvp/skills/InstructionParser.java @@ -5,13 +5,14 @@ import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.stream.Gatherer.Downstream; + +import lvp.skills.logging.Logger; + import java.util.stream.Gatherer; import java.util.List; import java.util.Objects; import java.util.Arrays; -import lvp.logging.Logger; - public class InstructionParser { // ---- Instruction Types ---- diff --git a/src/main/java/lvp/skills/TextUtils.java b/src/main/java/lvp/skills/TextUtils.java index 2676a7d..2f7f0df 100644 --- a/src/main/java/lvp/skills/TextUtils.java +++ b/src/main/java/lvp/skills/TextUtils.java @@ -18,7 +18,6 @@ public class TextUtils { // Class with static methods for file operations private TextUtils(){} - //TODO: Move to Text Service public static void write(String fileName, String text) { try { diff --git a/src/main/java/lvp/skills/TurtleParser.java b/src/main/java/lvp/skills/TurtleParser.java index ed88c2b..6474bca 100644 --- a/src/main/java/lvp/skills/TurtleParser.java +++ b/src/main/java/lvp/skills/TurtleParser.java @@ -6,7 +6,7 @@ import java.util.stream.Stream; import lvp.commands.services.Turtle; -import lvp.logging.Logger; +import lvp.skills.logging.Logger; public class TurtleParser { private TurtleParser() {} diff --git a/src/main/java/lvp/logging/ConsoleDestination.java b/src/main/java/lvp/skills/logging/ConsoleDestination.java similarity index 91% rename from src/main/java/lvp/logging/ConsoleDestination.java rename to src/main/java/lvp/skills/logging/ConsoleDestination.java index 6b4863a..1689e6c 100644 --- a/src/main/java/lvp/logging/ConsoleDestination.java +++ b/src/main/java/lvp/skills/logging/ConsoleDestination.java @@ -1,4 +1,4 @@ -package lvp.logging; +package lvp.skills.logging; public class ConsoleDestination implements LogDestination { diff --git a/src/main/java/lvp/logging/FileDestination.java b/src/main/java/lvp/skills/logging/FileDestination.java similarity index 97% rename from src/main/java/lvp/logging/FileDestination.java rename to src/main/java/lvp/skills/logging/FileDestination.java index db177d9..2f73633 100644 --- a/src/main/java/lvp/logging/FileDestination.java +++ b/src/main/java/lvp/skills/logging/FileDestination.java @@ -1,4 +1,4 @@ -package lvp.logging; +package lvp.skills.logging; import java.io.BufferedWriter; import java.io.FileWriter; diff --git a/src/main/java/lvp/logging/LogDestination.java b/src/main/java/lvp/skills/logging/LogDestination.java similarity index 72% rename from src/main/java/lvp/logging/LogDestination.java rename to src/main/java/lvp/skills/logging/LogDestination.java index 253e590..73c3f52 100644 --- a/src/main/java/lvp/logging/LogDestination.java +++ b/src/main/java/lvp/skills/logging/LogDestination.java @@ -1,4 +1,4 @@ -package lvp.logging; +package lvp.skills.logging; public interface LogDestination { void log(String formattedMessage); diff --git a/src/main/java/lvp/logging/LogEntry.java b/src/main/java/lvp/skills/logging/LogEntry.java similarity index 72% rename from src/main/java/lvp/logging/LogEntry.java rename to src/main/java/lvp/skills/logging/LogEntry.java index a222dea..445971d 100644 --- a/src/main/java/lvp/logging/LogEntry.java +++ b/src/main/java/lvp/skills/logging/LogEntry.java @@ -1,4 +1,4 @@ -package lvp.logging; +package lvp.skills.logging; public record LogEntry(String time, LogLevel level, String message) { } diff --git a/src/main/java/lvp/logging/LogLevel.java b/src/main/java/lvp/skills/logging/LogLevel.java similarity index 92% rename from src/main/java/lvp/logging/LogLevel.java rename to src/main/java/lvp/skills/logging/LogLevel.java index 0fb7f28..1b32841 100644 --- a/src/main/java/lvp/logging/LogLevel.java +++ b/src/main/java/lvp/skills/logging/LogLevel.java @@ -1,4 +1,4 @@ -package lvp.logging; +package lvp.skills.logging; public enum LogLevel { Debug, diff --git a/src/main/java/lvp/logging/Logger.java b/src/main/java/lvp/skills/logging/Logger.java similarity index 98% rename from src/main/java/lvp/logging/Logger.java rename to src/main/java/lvp/skills/logging/Logger.java index 9309d59..e98600a 100644 --- a/src/main/java/lvp/logging/Logger.java +++ b/src/main/java/lvp/skills/logging/Logger.java @@ -1,4 +1,4 @@ -package lvp.logging; +package lvp.skills.logging; import java.io.PrintWriter; import java.io.StringWriter; From b05bea90f186d7dac2ec45bb8a36bec39c95a2be Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 12 Jun 2025 20:18:03 +0200 Subject: [PATCH 14/50] added some pipe examples --- newdemo.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/newdemo.java b/newdemo.java index b5d76ef..d1ee4d4 100644 --- a/newdemo.java +++ b/newdemo.java @@ -59,6 +59,13 @@ void main() { | Turtle | Html Text{2}: ~ | Markdown + Text{3}: + ``` + ${0} + ``` + ~~~ + Text{2}: - + | Text{3} | Markdown """); From e048d1a80eb7e2682914fcc8b0ca6d13f7dcc13b Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 13 Jun 2025 17:24:52 +0200 Subject: [PATCH 15/50] Interaction Elements --- newdemo.java | 52 ++++++ src/main/java/lvp/FileWatcher.java | 30 ++-- src/main/java/lvp/Main.java | 2 +- src/main/java/lvp/Processor.java | 6 +- .../lvp/commands/services/Interaction.java | 159 ++++++++++++++++++ .../java/lvp/commands/services/Turtle.java | 4 +- .../lvp/commands/targets/dot/GraphSpec.java | 14 +- src/main/java/lvp/skills/HTMLElements.java | 37 ++++ src/main/java/lvp/skills/Interaction.java | 77 --------- 9 files changed, 278 insertions(+), 103 deletions(-) create mode 100644 src/main/java/lvp/commands/services/Interaction.java create mode 100644 src/main/java/lvp/skills/HTMLElements.java delete mode 100644 src/main/java/lvp/skills/Interaction.java diff --git a/newdemo.java b/newdemo.java index d1ee4d4..b035fd0 100644 --- a/newdemo.java +++ b/newdemo.java @@ -49,6 +49,12 @@ void main() { println(""" Text{2}: init 0 200 0 25 50 0 0 + """ + + + "color 37 255 37 1" // turtle color + + + """ + forward 25 right 60 backward 25 @@ -68,6 +74,52 @@ void main() { | Text{3} | Markdown """); + println(""" + Button: + Text: Green + width: 200 + height: 50 + path: newdemo.java + label: "// turtle color" + replacement: "color 37 255 37 1" + ~~~ + | Html + Button: + Text: Red + width: 200 + height: 50 + path: newdemo.java + label: "// turtle color" + replacement: "color 255 37 37 1" + ~~~ + | Html + """); + + int n = 55; // input + boolean b = true; // bool + println(""" + Input: + path: newdemo.java + label: "// input" + placeholder: Enter a number + template: int n = $; + type: text + ~~~ + | Html + Checkbox: + path: newdemo.java + label: "// bool" + template: boolean b = $; + """ + + + "checked:" + b + + + """ + + ~~~ + | Html + """); + println(""" Dot: diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 9d56804..4fa656b 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -1,9 +1,6 @@ package lvp; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.nio.file.ClosedWatchServiceException; import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; @@ -63,19 +60,22 @@ public void watchLoop() { Logger.logError("Watcher loop terminated due to exception: " + e.getMessage(), e); break; } - for (WatchEvent ev : key.pollEvents()) { - Path changed = (Path) ev.context(); - if (matcher.matches(changed) && !Files.isDirectory(changed)) { - Logger.logInfo("Event für Datei: " + changed.toAbsolutePath() + " (" + ev.kind().name() + ")"); - - ScheduledFuture prev = pendingTask.getAndSet( - debounceExecutor.schedule(() -> run(dir.resolve(changed)), debounceDelay, TimeUnit.MILLISECONDS) - ); - if (prev != null && !prev.isDone()) prev.cancel(false); - } + processWatchKeyEvents(key, matcher, debounceDelay); + if (!key.reset()) isRunning = false; + } + } + + private void processWatchKeyEvents(WatchKey key, PathMatcher matcher, long debounceDelay) { + for (WatchEvent ev : key.pollEvents()) { + Path changed = (Path) ev.context(); + if (matcher.matches(changed) && !Files.isDirectory(changed)) { + Logger.logInfo("Event für Datei: " + changed.toAbsolutePath() + " (" + ev.kind().name() + ")"); + + ScheduledFuture prev = pendingTask.getAndSet( + debounceExecutor.schedule(() -> run(dir.resolve(changed)), debounceDelay, TimeUnit.MILLISECONDS) + ); + if (prev != null && !prev.isDone()) prev.cancel(false); } - - if (!key.reset()) break; } } diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index 5c170a8..a9032a4 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -34,7 +34,7 @@ public static void main(String[] args) { watcher.watchLoop(); } } catch (IOException e) { - System.err.println("Error starting server: " + e.getMessage()); + System.err.println("Error starting lvp: " + e.getMessage()); e.printStackTrace(); System.exit(1); } diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 5fa632e..79cfe87 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -11,6 +11,7 @@ import lvp.commands.services.Text; import lvp.commands.services.Turtle; +import lvp.commands.services.Interaction; import lvp.commands.targets.Targets; import lvp.skills.InstructionParser; import lvp.skills.InstructionParser.Command; @@ -24,7 +25,10 @@ public class Processor { Map> services = new HashMap<>(Map.of( "Text", Text::of, "Codeblock", Text::codeblock, - "Turtle", Turtle::of)); + "Turtle", Turtle::of, + "Button", Interaction::button, + "Input", Interaction::input, + "Checkbox", Interaction::checkbox)); public Processor(Server server) { this.server = server; diff --git a/src/main/java/lvp/commands/services/Interaction.java b/src/main/java/lvp/commands/services/Interaction.java new file mode 100644 index 0000000..ab58a2f --- /dev/null +++ b/src/main/java/lvp/commands/services/Interaction.java @@ -0,0 +1,159 @@ +package lvp.commands.services; + +import java.nio.charset.StandardCharsets; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Collectors; + +import lvp.skills.HTMLElements; +import lvp.skills.TextUtils; +import lvp.skills.logging.Logger; + +public class Interaction { + private Interaction() {} + public static String button(String id, String content) { + Map fields = content.lines() + .filter(line -> !line.isBlank()) + .map(line -> line.split(":", 2)) + .filter(parts -> parts.length == 2) + .map(parts -> Map.entry(parts[0].strip().toLowerCase(), parts[1].strip())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + String text = fields.get("text"); + String pathString = fields.get("path"); + String label = fields.get("label"); + String replacement = fields.get("replacement"); + + if (text == null || pathString == null || label == null || replacement == null) { + Logger.logError("Missing required button field (text, path, label, or replacement)"); + return null; + } + + Optional path = tryPath(pathString); + if (path.isEmpty()) { + Logger.logError("Invalid path in button command"); + return null; + } + OptionalInt width = tryInt(fields.get("width")); + OptionalInt height = tryInt(fields.get("height")); + + Logger.logDebug("Parsed button with text=" + text + ", path=" + path + ", label=" + label + ", size=" + + (width.isPresent() ? width.getAsInt() + "x" + height.getAsInt() : "default")); + + String func = eventFunction(path.get(), stripQuotes(label), replacement); + + return width.isPresent() || height.isPresent() + ? HTMLElements.button(id, text, width.orElse(height.getAsInt()), height.orElse(width.getAsInt()), func) + : HTMLElements.button(id, text, func); + } + + public static String input(String id, String content) { + Map fields = content.lines() + .filter(line -> !line.isBlank()) + .map(line -> line.split(":", 2)) + .filter(parts -> parts.length == 2) + .map(parts -> Map.entry(parts[0].strip().toLowerCase(), parts[1].strip())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + String pathString = fields.get("path"); + String label = fields.get("label"); + String template = fields.get("template"); + String placeholder = fields.getOrDefault("placeholder", ""); + String type = fields.getOrDefault("type", "text"); + + if (pathString == null || label == null || template == null) { + Logger.logError("Missing required field(s): path, label, and template are mandatory."); + return null; + } + + Optional path = tryPath(pathString); + if (path.isEmpty()) { + Logger.logError("Invalid path in input command"); + return null; + } + + Logger.logDebug("Parsed input with path=" + path + ", label=" + label + ", type=" + type); + String inputElement = HTMLElements.input(id, placeholder, type, stripQuotes(label).replaceFirst("//", "").strip()); + String button = HTMLElements.button("button" + id, "Send", TextUtils.fillOut(""" + (() => { + const input = document.getElementById("input${0}"); + const result = `${3}`.replace("$", input.value); + fetch("interact", { method: "post", body: "${1}:${2}:single:" + btoa(String.fromCharCode(...new TextEncoder().encode(result))) }).catch(console.error); + })() + """, id, + Base64.getEncoder().encodeToString(path.get().normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(stripQuotes(label).getBytes(StandardCharsets.UTF_8)), + template)); + return inputElement + button; + } + + public static String checkbox(String id, String content) { + Map fields = content.lines() + .filter(line -> !line.isBlank()) + .map(line -> line.split(":", 2)) + .filter(parts -> parts.length == 2) + .map(parts -> Map.entry(parts[0].strip().toLowerCase(), parts[1].strip())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + String pathString = fields.get("path"); + String label = fields.get("label"); + String template = fields.get("template"); + + if (pathString == null || label == null || template == null) { + Logger.logError("Missing required checkbox field (path, label, or template)"); + return null; + } + + Optional path = tryPath(pathString); + if (path.isEmpty()) { + Logger.logError("Invalid path in checkbox command"); + return null; + } + + boolean checked = Boolean.parseBoolean(fields.getOrDefault("checked", "false")); + + Logger.logDebug("Parsed checkbox with path=" + path + ", label=" + label + ", checked=" + checked); + return HTMLElements.checkbox(id, stripQuotes(label).replaceFirst("//", "").strip(), checked, TextUtils.fillOut(""" + (() => { + const result = `${2}`.replace("$", this.checked); + fetch("interact", { method: "post", body: "${0}:${1}:single:" + btoa(String.fromCharCode(...new TextEncoder().encode(result))) }).catch(console.error); + })() + """, Base64.getEncoder().encodeToString(path.get().normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(stripQuotes(label).getBytes(StandardCharsets.UTF_8)), + template)); + } + + private static String eventFunction(Path path, String label, String replacement) { + return TextUtils.fillOut("fetch(\"interact\", { method: \"post\", body: \"${0}:${1}:single:${2}\" }).catch(console.error);", + Base64.getEncoder().encodeToString(path.normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(label.getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(replacement.getBytes(StandardCharsets.UTF_8))); + } + + private static OptionalInt tryInt(String s) { + try { + return OptionalInt.of(Integer.parseInt(s.strip())); + } catch (NumberFormatException _) { + return OptionalInt.empty(); + } + } + + private static Optional tryPath(String s) { + try { + return Optional.of(Path.of(s)); + } catch (InvalidPathException _) { + return Optional.empty(); + } + } + + private static String stripQuotes(String s) { + if ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))) { + return s.substring(1, s.length() - 1).strip(); + } + return s; + } +} diff --git a/src/main/java/lvp/commands/services/Turtle.java b/src/main/java/lvp/commands/services/Turtle.java index 64a0f4c..de8344c 100644 --- a/src/main/java/lvp/commands/services/Turtle.java +++ b/src/main/java/lvp/commands/services/Turtle.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Locale; -import lvp.skills.Interaction; +import lvp.skills.HTMLElements; import lvp.skills.TextUtils; import lvp.skills.TurtleParser; @@ -164,7 +164,7 @@ public String timelineSlider() { Linien sichtbar: ${1} / ${1}
""", id, elements.size()); - String slider = Interaction.slider(id, 0, elements.size(), elements.size(), TextUtils.fillOut(""" + String slider = HTMLElements.slider(id, 0, elements.size(), elements.size(), TextUtils.fillOut(""" ((e) => { const n = Math.round(e.target.value); const statusCurrent = document.getElementById("currentLine${0}"); diff --git a/src/main/java/lvp/commands/targets/dot/GraphSpec.java b/src/main/java/lvp/commands/targets/dot/GraphSpec.java index 44e3989..a5aca47 100644 --- a/src/main/java/lvp/commands/targets/dot/GraphSpec.java +++ b/src/main/java/lvp/commands/targets/dot/GraphSpec.java @@ -2,13 +2,13 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; +import java.util.OptionalInt; -public record GraphSpec(Optional width, Optional height, String dot) { +public record GraphSpec(OptionalInt width, OptionalInt height, String dot) { public static GraphSpec fromContent(String content) { - Optional width = Optional.empty(); - Optional height = Optional.empty(); + OptionalInt width = OptionalInt.empty(); + OptionalInt height = OptionalInt.empty(); List dotLines = new ArrayList<>(); @@ -27,11 +27,11 @@ public static GraphSpec fromContent(String content) { return new GraphSpec(width, height, dot); } - private static Optional tryInt(String s) { + private static OptionalInt tryInt(String s) { try { - return Optional.of(Integer.parseInt(s.trim())); + return OptionalInt.of(Integer.parseInt(s.trim())); } catch (NumberFormatException _) { - return Optional.empty(); + return OptionalInt.empty(); } } } \ No newline at end of file diff --git a/src/main/java/lvp/skills/HTMLElements.java b/src/main/java/lvp/skills/HTMLElements.java new file mode 100644 index 0000000..5399b86 --- /dev/null +++ b/src/main/java/lvp/skills/HTMLElements.java @@ -0,0 +1,37 @@ +package lvp.skills; + + +public class HTMLElements { + private HTMLElements() {} + public static String button(String id, String text, int width, int height, String onClick) { + return TextUtils.fillOut("", id, width, height, onClick, text); + } + + public static String button(String id, String text, String onClick) { + return TextUtils.fillOut("", id, onClick, text); + } + + public static String slider(String id, double min, double max, double value, String onInput) { + return TextUtils.fillOut("", + id, min, max, value, onInput); + } + + public static String input(String id, String placeholder, String type, String label) { + return TextUtils.fillOut(""" + + + """, id, type, placeholder, label); + } + public static String input(String id, String placeholder, String type, String label, String eventType, String event) { + return TextUtils.fillOut(""" + + + """, id, type, placeholder, eventType, event, label); + } + public static String checkbox(String id, String label, boolean checked, String event) { + return TextUtils.fillOut(""" + + + """, id, label, checked ? "checked" : "", event); + } +} diff --git a/src/main/java/lvp/skills/Interaction.java b/src/main/java/lvp/skills/Interaction.java deleted file mode 100644 index 516d87f..0000000 --- a/src/main/java/lvp/skills/Interaction.java +++ /dev/null @@ -1,77 +0,0 @@ -package lvp.skills; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Base64; - - -public class Interaction { - public static String eventFunction(String path, String label, String replacement) { - return eventFunction(Path.of(path), label, replacement); - } - public static String eventFunction(Path path, String label, String replacement) { - return TextUtils.fillOut("fetch(\"interact\", { method: \"post\", body: \"${0}:${1}:single:${2}\" }).catch(console.error);", - Base64.getEncoder().encodeToString(path.normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(label.getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(replacement.getBytes(StandardCharsets.UTF_8))); - } - - public static String button(String text, int width, int height, String onClick) { - return TextUtils.fillOut("", width, height, onClick, text); - } - - public static String button(String text, String onClick) { - return TextUtils.fillOut("", onClick, text); - } - - public static String slider(String id, double min, double max, double value, String onInput) { - return TextUtils.fillOut("", - id, min, max, value, onInput); - } - - public static String input(String path, String label, String template, String placeholder) { - return input(path, label, template, placeholder, "text"); - } - public static String input(String path, String label, String template, String placeholder, String type) { - return input(Path.of(path), label, template, placeholder, type); - } - public static String input(Path path, String label, String template, String placeholder, String type) { - String id = IdGen.generateID(10); - String inputField = TextUtils.fillOut(""" - - - """, id, placeholder, type, label.replaceFirst("//", "").trim()); - String button = button("Send", TextUtils.fillOut(""" - (() => { - const input = document.getElementById("input${0}"); - const result = `${3}`.replace("$", input.value); - fetch("interact", { method: "post", body: "${1}:${2}:single:" + btoa(String.fromCharCode(...new TextEncoder().encode(result))) }).catch(console.error); - })() - """, id, - Base64.getEncoder().encodeToString(path.normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(label.getBytes(StandardCharsets.UTF_8)), - template)); - return inputField + button; - } - - public static String checkbox(String path, String label, String template, boolean checked) { - return checkbox(Path.of(path), label, template, checked); - } - public static String checkbox(Path path, String label, String template, boolean checked) { - String id = IdGen.generateID(10); - return TextUtils.fillOut(""" - - - """, id, - Base64.getEncoder().encodeToString(path.normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(label.getBytes(StandardCharsets.UTF_8)), - template, - checked ? "checked" : "", - label.replaceFirst("//", "").trim()); - } - - -} From 8badca5e71cff115de947a7adce5f4384ad7d846 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 13 Jun 2025 18:43:37 +0200 Subject: [PATCH 16/50] blocking interactions --- newdemo.java | 9 ++ src/main/java/lvp/Processor.java | 17 +++ src/main/java/lvp/Server.java | 107 +++++++----------- .../java/lvp/commands/services/Turtle.java | 2 +- src/main/java/lvp/skills/HTMLElements.java | 20 ++-- .../java/lvp/skills/InstructionParser.java | 15 ++- src/main/java/lvp/skills/TextUtils.java | 72 ++++++++++++ 7 files changed, 165 insertions(+), 77 deletions(-) diff --git a/newdemo.java b/newdemo.java index b035fd0..61a6759 100644 --- a/newdemo.java +++ b/newdemo.java @@ -1,5 +1,7 @@ import static java.io.IO.println; +import java.util.Scanner; + void main() { println("Clear:~"); println("Markdown: # Hello World!"); @@ -46,6 +48,13 @@ void main() { """); // ex1 + println("Markdown: # Blocking Input"); + println("Read:"); + Scanner scanner = new Scanner(System.in); + String d = scanner.nextLine(); + + println("Markdown: Your input was: " + d); + println(""" Text{2}: init 0 200 0 25 50 0 0 diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 79cfe87..4f923e7 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -13,10 +13,13 @@ import lvp.commands.services.Turtle; import lvp.commands.services.Interaction; import lvp.commands.targets.Targets; +import lvp.skills.HTMLElements; import lvp.skills.InstructionParser; +import lvp.skills.TextUtils; import lvp.skills.InstructionParser.Command; import lvp.skills.InstructionParser.CommandRef; import lvp.skills.InstructionParser.Pipe; +import lvp.skills.InstructionParser.Read; import lvp.skills.logging.Logger; public class Processor { Server server; @@ -49,6 +52,7 @@ void process(Process process) { switch (curr) { case Command cmd -> processCommands(cmd); case Pipe pipe -> processPipe(pipe, prev); + case Read read -> processRead(read, process); default -> null; })).forEachOrdered(_->{}); } @@ -90,6 +94,19 @@ else if (services.containsKey(ref.name())) { return current; } + String processRead(Read read, Process process) { + server.waitingStreams.put(read.id(), process.getOutputStream()); + String inputField = HTMLElements.input("input" + read.id()); + String button = HTMLElements.button("button" + read.id(), "Send", TextUtils.fillOut(""" + (()=>{ + const input = document.getElementById("input${0}"); + fetch("read", { method: "post", body: "${0}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); + })() + """,read.id())); + targetProcessor.consumeHTML(read.id(), inputField + button); + return null; + } + void init() { server.events.clear(); Text.clear(); diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index d77d750..4a73196 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -10,7 +10,9 @@ import java.util.Arrays; import java.util.Base64; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.stream.IntStream; @@ -18,14 +20,13 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; +import lvp.skills.TextUtils; +import lvp.skills.TextUtils.ReplacementType; import lvp.skills.logging.LogLevel; import lvp.skills.logging.Logger; public class Server { - private enum ReplacementType { - SINGLE, MULTI, BLOCK - } private record EventMessage(SSEType event, String data) {} private final HttpServer httpServer; @@ -39,6 +40,7 @@ private record EventMessage(SSEType event, String data) {} public final List webClients = new CopyOnWriteArrayList<>(); // thread-safe variant of ArrayList; List events = new CopyOnWriteArrayList<>(); + Map waitingStreams = new ConcurrentHashMap<>(); boolean isVerbose = false; @@ -51,6 +53,7 @@ public Server(int port, boolean isVerbose) throws IOException { httpServer.createContext("/log", this::handleLog); httpServer.createContext("/interact", this::handleInteract); + httpServer.createContext("/read", this::handleRead); httpServer.createContext("/events", this::handleEvents); httpServer.createContext("/", this::handleRoot); @@ -76,6 +79,38 @@ private void handleLog(HttpExchange exchange) throws IOException { Logger.log(LogLevel.fromString(parts[0]), parts[1]); } + private void handleRead(HttpExchange exchange) throws IOException { + String message = readRequestBody(exchange); + if (message == null) return; + String[] parts = message.split(":", 2); + if (parts.length != 2) { + exchange.sendResponseHeaders(400, -1); // Bad Request + exchange.close(); + Logger.logError("Illegal read message: " + message); + return; + } + + Logger.logInfo(message); + + exchange.sendResponseHeaders(200, 0); + exchange.close(); + + OutputStream stream = waitingStreams.get(parts[0]); + if (stream == null) { + Logger.logError("Stream not found: " + message); + return; + } + try { + stream.write(Base64.getDecoder().decode(parts[1])); + } catch (IOException e) { + Logger.logError("Error while writing stream for: " + parts[0], e); + } finally { + stream.close(); + waitingStreams.remove(parts[0]); + } + + } + private void handleInteract(HttpExchange exchange) throws IOException { String message = readRequestBody(exchange); if (message == null) return; @@ -103,7 +138,7 @@ private void handleInteract(HttpExchange exchange) throws IOException { } String replacement = new String(Base64.getDecoder().decode(parts[3]), StandardCharsets.UTF_8); - updateFile(path, label, rType.get(), replacement); + TextUtils.updateFile(path, label, rType.get(), replacement); } private void handleEvents(HttpExchange exchange) throws IOException { @@ -206,70 +241,6 @@ private String readRequestBody(HttpExchange exchange) throws IOException { return null; } - private void updateFile(String path, String label, ReplacementType rType, String replacement) { - try { - Path filePath = Path.of(path); - List lines = Files.readAllLines(filePath, StandardCharsets.UTF_8); - switch (rType) { - case SINGLE: - updateSingleLine(lines, label, replacement); - break; - case MULTI: - updateMultiLine(lines, label, replacement); - break; - default: - break; - } - Files.write(filePath, lines, StandardCharsets.UTF_8); - } catch (IOException e) { - Logger.logError("Error updating file: " + path, e); - } - } - - private void updateSingleLine(List lines, String label, String replacement) { - for (int i = 0; i < lines.size(); i++) { - if (lines.get(i).trim().endsWith(label)) { - String line = lines.get(i); - int spaces = (int) IntStream.range(0, line.length()) - .takeWhile(pos -> line.charAt(pos) == ' ') - .count(); - lines.set(i, " ".repeat(spaces) + replacement + " " + label); - } - } - } - - private void updateMultiLine(List lines, String label, String replacement) { - int openingLabel = -1; - int closingLabel = -1; - for (int i = 0; i < lines.size(); i++) { - if (lines.get(i).trim().equals(label)) { - if (openingLabel == -1) { - openingLabel = i; - } else { - closingLabel = i; - break; - } - } - } - if (openingLabel == -1 || closingLabel == -1) { - Logger.logError("Labels not found for multi-line replacement: " + label); - return; - } - if (closingLabel <= openingLabel) { - Logger.logError("Closing label is before opening label for multi-line replacement: " + label); - return; - } - String startingLine = lines.get(openingLabel + 1); - int spaces = (int) IntStream.range(0, startingLine.length()) - .takeWhile(pos -> startingLine.charAt(pos) == ' ') - .count(); - - for (int i = openingLabel + 1; i < closingLabel; i++) { - lines.remove(openingLabel + 1); - } - lines.add(openingLabel + 1, " ".repeat(spaces) + replacement); - } - public void stop() { Logger.logInfo("Closing Server on port '" + port + "'"); for (HttpExchange connection : webClients) { diff --git a/src/main/java/lvp/commands/services/Turtle.java b/src/main/java/lvp/commands/services/Turtle.java index de8344c..7bc5c56 100644 --- a/src/main/java/lvp/commands/services/Turtle.java +++ b/src/main/java/lvp/commands/services/Turtle.java @@ -164,7 +164,7 @@ public String timelineSlider() { Linien sichtbar: ${1} / ${1} """, id, elements.size()); - String slider = HTMLElements.slider(id, 0, elements.size(), elements.size(), TextUtils.fillOut(""" + String slider = HTMLElements.slider("slider" + id, 0, elements.size(), elements.size(), TextUtils.fillOut(""" ((e) => { const n = Math.round(e.target.value); const statusCurrent = document.getElementById("currentLine${0}"); diff --git a/src/main/java/lvp/skills/HTMLElements.java b/src/main/java/lvp/skills/HTMLElements.java index 5399b86..051a85a 100644 --- a/src/main/java/lvp/skills/HTMLElements.java +++ b/src/main/java/lvp/skills/HTMLElements.java @@ -12,26 +12,32 @@ public static String button(String id, String text, String onClick) { } public static String slider(String id, double min, double max, double value, String onInput) { - return TextUtils.fillOut("", + return TextUtils.fillOut("", id, min, max, value, onInput); } + public static String input(String id) { + return TextUtils.fillOut(""" + + """, id); + } + public static String input(String id, String placeholder, String type, String label) { return TextUtils.fillOut(""" - - + + """, id, type, placeholder, label); } public static String input(String id, String placeholder, String type, String label, String eventType, String event) { return TextUtils.fillOut(""" - - + + """, id, type, placeholder, eventType, event, label); } public static String checkbox(String id, String label, boolean checked, String event) { return TextUtils.fillOut(""" - - + + """, id, label, checked ? "checked" : "", event); } } diff --git a/src/main/java/lvp/skills/InstructionParser.java b/src/main/java/lvp/skills/InstructionParser.java index 4639815..3d4981c 100644 --- a/src/main/java/lvp/skills/InstructionParser.java +++ b/src/main/java/lvp/skills/InstructionParser.java @@ -16,10 +16,11 @@ public class InstructionParser { // ---- Instruction Types ---- - public sealed interface Instruction permits Command, Register, Pipe {} + public sealed interface Instruction permits Command, Register, Read, Pipe {} public record Command(String name, String id, String content) implements Instruction {} public record Register(String name, String call) implements Instruction {} + public record Read(String id) implements Instruction {} public record Pipe(List commands) implements Instruction {} public record CommandRef(String name, String id) {} @@ -27,6 +28,7 @@ public record CommandRef(String name, String id) {} // ---- Patterns ---- private static final Pattern SINGLE_LINE_COMMAND = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*(.+)$"); private static final Pattern BLOCK_START = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*$"); + private static final Pattern READ = Pattern.compile("^Read(?:\\{([^}]+)\\})?:\\s*$"); private static final Pattern REGISTER = Pattern.compile("^Register:\\s+(\\w+)\\s+(.+)$"); private static final Pattern PIPE_LINE = Pattern.compile("^\\s*\\|(.+)$"); private static final Pattern PIPE_ENTRY = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?$"); @@ -79,6 +81,7 @@ private static void handleLine(BlockState state, String line, Downstream return true; } + private static boolean tryRead(String line, Downstream out) { + Matcher matcher = READ.matcher(line); + if (!matcher.matches()) return false; + + String id = matcher.group(1) == null ? IdGen.generateID(10) : matcher.group(1); + Logger.logDebug("Parsed Read" + formatId(id)); + out.push(new Read(id)); + return true; + } + private static boolean trySingleCommand(String line, Downstream out) { Matcher matcher = SINGLE_LINE_COMMAND.matcher(line); if (!matcher.matches()) return false; diff --git a/src/main/java/lvp/skills/TextUtils.java b/src/main/java/lvp/skills/TextUtils.java index 2f7f0df..7c5add4 100644 --- a/src/main/java/lvp/skills/TextUtils.java +++ b/src/main/java/lvp/skills/TextUtils.java @@ -15,6 +15,8 @@ import java.util.regex.Pattern; import java.util.stream.IntStream; +import lvp.skills.logging.Logger; + public class TextUtils { // Class with static methods for file operations private TextUtils(){} @@ -107,4 +109,74 @@ public static String fillOut(String template, Object... replacements) { .forEach(i -> m.put(Integer.toString(i), replacements[i])); return fillOut(m, template); } + + + + public enum ReplacementType { + SINGLE, MULTI, BLOCK + } + + public static void updateFile(String path, String label, ReplacementType rType, String replacement) { + try { + Path filePath = Path.of(path); + List lines = Files.readAllLines(filePath, StandardCharsets.UTF_8); + switch (rType) { + case SINGLE: + updateSingleLine(lines, label, replacement); + break; + case MULTI: + updateMultiLine(lines, label, replacement); + break; + default: + break; + } + Files.write(filePath, lines, StandardCharsets.UTF_8); + } catch (IOException e) { + Logger.logError("Error updating file: " + path, e); + } + } + + private static void updateSingleLine(List lines, String label, String replacement) { + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).trim().endsWith(label)) { + String line = lines.get(i); + int spaces = (int) IntStream.range(0, line.length()) + .takeWhile(pos -> line.charAt(pos) == ' ') + .count(); + lines.set(i, " ".repeat(spaces) + replacement + " " + label); + } + } + } + + private static void updateMultiLine(List lines, String label, String replacement) { + int openingLabel = -1; + int closingLabel = -1; + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).trim().equals(label)) { + if (openingLabel == -1) { + openingLabel = i; + } else { + closingLabel = i; + break; + } + } + } + if (openingLabel == -1 || closingLabel == -1) { + Logger.logError("Labels not found for multi-line replacement: " + label); + return; + } + if (closingLabel <= openingLabel) { + Logger.logError("Closing label is before opening label for multi-line replacement: " + label); + return; + } + String startingLine = lines.get(openingLabel + 1); + int spaces = (int) IntStream.range(0, startingLine.length()) + .takeWhile(pos -> startingLine.charAt(pos) == ' ') + .count(); + + for (int i = openingLabel + 1; i < closingLabel; i++) { + lines.remove(openingLabel + 1); + } + lines.add(openingLabel + 1, " ".repeat(spaces) + replacement); + } } From bb657cb27a964012cb3a3babfe08cd10e256fb45 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 13 Jun 2025 22:50:33 +0200 Subject: [PATCH 17/50] register and test --- .gitignore | 1 + external.java | 11 +++ registerdemo.java | 9 ++ src/main/java/lvp/Processor.java | 43 ++++++++- .../lvp/commands/services/Interaction.java | 44 +++------ src/main/java/lvp/commands/services/Test.java | 91 +++++++++++++++++++ src/main/java/lvp/skills/ParsingTools.java | 33 +++++++ testdemo.java | 17 ++++ 8 files changed, 215 insertions(+), 34 deletions(-) create mode 100644 external.java create mode 100644 registerdemo.java create mode 100644 src/main/java/lvp/commands/services/Test.java create mode 100644 src/main/java/lvp/skills/ParsingTools.java create mode 100644 testdemo.java diff --git a/.gitignore b/.gitignore index 6a43c0a..ddb9e19 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ target/ build test.java Test.java +!**/services/Test.java .vscode \ No newline at end of file diff --git a/external.java b/external.java new file mode 100644 index 0000000..05300ad --- /dev/null +++ b/external.java @@ -0,0 +1,11 @@ +import static java.io.IO.println; + +import java.util.Scanner; + +void main() { + Scanner scanner = new Scanner(System.in); + String id = scanner.nextLine(); + String content = scanner.nextLine(); + + println(new StringBuilder(content).reverse().toString()); +} \ No newline at end of file diff --git a/registerdemo.java b/registerdemo.java new file mode 100644 index 0000000..6ad67c8 --- /dev/null +++ b/registerdemo.java @@ -0,0 +1,9 @@ +void main() { + println(""" + Clear: ~ + Markdown: # Register Test + Register: Reverse java --enable-preview external.java + Reverse: Hello World + | Markdown + """); +} \ No newline at end of file diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 4f923e7..0f6faf6 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -1,15 +1,20 @@ package lvp; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.stream.Collectors; import java.util.stream.Gatherers; import lvp.commands.services.Text; +import lvp.commands.services.Test; import lvp.commands.services.Turtle; import lvp.commands.services.Interaction; import lvp.commands.targets.Targets; @@ -20,6 +25,7 @@ import lvp.skills.InstructionParser.CommandRef; import lvp.skills.InstructionParser.Pipe; import lvp.skills.InstructionParser.Read; +import lvp.skills.InstructionParser.Register; import lvp.skills.logging.Logger; public class Processor { Server server; @@ -31,7 +37,8 @@ public class Processor { "Turtle", Turtle::of, "Button", Interaction::button, "Input", Interaction::input, - "Checkbox", Interaction::checkbox)); + "Checkbox", Interaction::checkbox, + "Test", Test::test)); public Processor(Server server) { this.server = server; @@ -53,6 +60,7 @@ void process(Process process) { case Command cmd -> processCommands(cmd); case Pipe pipe -> processPipe(pipe, prev); case Read read -> processRead(read, process); + case Register register -> processRegister(register); default -> null; })).forEachOrdered(_->{}); } @@ -107,6 +115,39 @@ String processRead(Read read, Process process) { return null; } + String processRegister(Register register) { + services.put(register.name(), (id, content) -> { + boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + String out = null; + try { + Logger.logInfo("Executing " + register.call()); + ProcessBuilder pb = new ProcessBuilder(isWindows ? new String[]{"cmd.exe", "/c", register.call()} : new String[]{"sh", "-c", register.call()}) + .redirectErrorStream(true); + Process process = pb.start(); + + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(id + "\n"); + writer.write(content + "\n"); + writer.flush(); + } + try (var reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + out = reader.lines().collect(Collectors.joining("\n")); + } + boolean finished = process.waitFor(10, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + Logger.logError("Timeout: process " + register.name() + " killed"); + } + } catch (Exception e) { + Logger.logError("Error in " + register.name(), e); + } + return out; + }); + return null; + } + void init() { server.events.clear(); Text.clear(); diff --git a/src/main/java/lvp/commands/services/Interaction.java b/src/main/java/lvp/commands/services/Interaction.java index ab58a2f..202b8d7 100644 --- a/src/main/java/lvp/commands/services/Interaction.java +++ b/src/main/java/lvp/commands/services/Interaction.java @@ -10,6 +10,7 @@ import java.util.stream.Collectors; import lvp.skills.HTMLElements; +import lvp.skills.ParsingTools; import lvp.skills.TextUtils; import lvp.skills.logging.Logger; @@ -33,18 +34,18 @@ public static String button(String id, String content) { return null; } - Optional path = tryPath(pathString); + Optional path = ParsingTools.tryPath(pathString); if (path.isEmpty()) { Logger.logError("Invalid path in button command"); return null; } - OptionalInt width = tryInt(fields.get("width")); - OptionalInt height = tryInt(fields.get("height")); + OptionalInt width = ParsingTools.tryInt(fields.get("width")); + OptionalInt height = ParsingTools.tryInt(fields.get("height")); Logger.logDebug("Parsed button with text=" + text + ", path=" + path + ", label=" + label + ", size=" + (width.isPresent() ? width.getAsInt() + "x" + height.getAsInt() : "default")); - String func = eventFunction(path.get(), stripQuotes(label), replacement); + String func = eventFunction(path.get(), ParsingTools.stripQuotes(label), replacement); return width.isPresent() || height.isPresent() ? HTMLElements.button(id, text, width.orElse(height.getAsInt()), height.orElse(width.getAsInt()), func) @@ -70,14 +71,14 @@ public static String input(String id, String content) { return null; } - Optional path = tryPath(pathString); + Optional path = ParsingTools.tryPath(pathString); if (path.isEmpty()) { Logger.logError("Invalid path in input command"); return null; } Logger.logDebug("Parsed input with path=" + path + ", label=" + label + ", type=" + type); - String inputElement = HTMLElements.input(id, placeholder, type, stripQuotes(label).replaceFirst("//", "").strip()); + String inputElement = HTMLElements.input(id, placeholder, type, ParsingTools.stripQuotes(label).replaceFirst("//", "").strip()); String button = HTMLElements.button("button" + id, "Send", TextUtils.fillOut(""" (() => { const input = document.getElementById("input${0}"); @@ -86,7 +87,7 @@ public static String input(String id, String content) { })() """, id, Base64.getEncoder().encodeToString(path.get().normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(stripQuotes(label).getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(ParsingTools.stripQuotes(label).getBytes(StandardCharsets.UTF_8)), template)); return inputElement + button; } @@ -108,7 +109,7 @@ public static String checkbox(String id, String content) { return null; } - Optional path = tryPath(pathString); + Optional path = ParsingTools.tryPath(pathString); if (path.isEmpty()) { Logger.logError("Invalid path in checkbox command"); return null; @@ -117,13 +118,13 @@ public static String checkbox(String id, String content) { boolean checked = Boolean.parseBoolean(fields.getOrDefault("checked", "false")); Logger.logDebug("Parsed checkbox with path=" + path + ", label=" + label + ", checked=" + checked); - return HTMLElements.checkbox(id, stripQuotes(label).replaceFirst("//", "").strip(), checked, TextUtils.fillOut(""" + return HTMLElements.checkbox(id, ParsingTools.stripQuotes(label).replaceFirst("//", "").strip(), checked, TextUtils.fillOut(""" (() => { const result = `${2}`.replace("$", this.checked); fetch("interact", { method: "post", body: "${0}:${1}:single:" + btoa(String.fromCharCode(...new TextEncoder().encode(result))) }).catch(console.error); })() """, Base64.getEncoder().encodeToString(path.get().normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(stripQuotes(label).getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(ParsingTools.stripQuotes(label).getBytes(StandardCharsets.UTF_8)), template)); } @@ -133,27 +134,4 @@ private static String eventFunction(Path path, String label, String replacement) Base64.getEncoder().encodeToString(label.getBytes(StandardCharsets.UTF_8)), Base64.getEncoder().encodeToString(replacement.getBytes(StandardCharsets.UTF_8))); } - - private static OptionalInt tryInt(String s) { - try { - return OptionalInt.of(Integer.parseInt(s.strip())); - } catch (NumberFormatException _) { - return OptionalInt.empty(); - } - } - - private static Optional tryPath(String s) { - try { - return Optional.of(Path.of(s)); - } catch (InvalidPathException _) { - return Optional.empty(); - } - } - - private static String stripQuotes(String s) { - if ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))) { - return s.substring(1, s.length() - 1).strip(); - } - return s; - } } diff --git a/src/main/java/lvp/commands/services/Test.java b/src/main/java/lvp/commands/services/Test.java new file mode 100644 index 0000000..45da73f --- /dev/null +++ b/src/main/java/lvp/commands/services/Test.java @@ -0,0 +1,91 @@ +package lvp.commands.services; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lvp.skills.TextUtils; +import lvp.skills.logging.Logger; + +//TODO: multiple actual and expect +public class Test { + private static final String JSHELL_PROMPT = "jshell>"; + public static String test(String id, String content) { + Map fields = content.lines() + .filter(line -> !line.isBlank()) + .map(line -> line.split(":", 2)) + .filter(parts -> parts.length == 2) + .map(parts -> Map.entry(parts[0].strip().toLowerCase(), parts[1].strip())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + String send = fields.get("send"); + String expect = fields.get("expect"); + + if (send == null || expect == null) { + Logger.logError("Test command requires 'Send' and 'Expect' fields."); + return null; + } + + Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect); + String actual = executeJshell(send); + if (actual == null) return "No Result"; + String actualParsed = actual.lines().map(Test::parseJshellOutput).findFirst().orElse(""); + + return TextUtils.fillOut(""" + Result for Test ${0}: + Input: ${1} + Response: ${2} + Actual: ${3} + Expected: ${4} + Status: ${5} + """, id, send, actual, actualParsed, expect, actualParsed.equals(expect) ? "Success" : "Failure"); + } + + private static String executeJshell(String send) { + String result = null; + try { + Logger.logInfo("Executing jshell --enable-preview -R-ea"); + ProcessBuilder pb = new ProcessBuilder("jshell", "--enable-preview", "-R-ea") + .redirectErrorStream(true); + Process process = pb.start(); + + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(send + "\n"); + writer.write("/ex"); + writer.flush(); + } + try (var reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + result = reader.lines() + .filter(line -> line.startsWith(JSHELL_PROMPT) && line.strip().length() > JSHELL_PROMPT.length()) + .collect(Collectors.joining("\n")); + } + boolean finished = process.waitFor(10, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + Logger.logError("Timeout: process jshell killed"); + } + } catch (Exception e) { + Logger.logError("Error in jshell", e); + } + return result; + } + + private static String parseJshellOutput(String line) { + int idx = line.indexOf("==>"); + if (idx != -1 && idx + 3 < line.length()) { + return line.substring(idx + 3).strip(); + } else if (line.startsWith(JSHELL_PROMPT + " |")) { + return line.substring(9).strip(); + } + return ""; + } +} diff --git a/src/main/java/lvp/skills/ParsingTools.java b/src/main/java/lvp/skills/ParsingTools.java new file mode 100644 index 0000000..bbf3076 --- /dev/null +++ b/src/main/java/lvp/skills/ParsingTools.java @@ -0,0 +1,33 @@ +package lvp.skills; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.OptionalInt; + +public class ParsingTools { + private ParsingTools() {} + + public static OptionalInt tryInt(String s) { + try { + return OptionalInt.of(Integer.parseInt(s.strip())); + } catch (NumberFormatException _) { + return OptionalInt.empty(); + } + } + + public static Optional tryPath(String s) { + try { + return Optional.of(Path.of(s)); + } catch (InvalidPathException _) { + return Optional.empty(); + } + } + + public static String stripQuotes(String s) { + if ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))) { + return s.substring(1, s.length() - 1).strip(); + } + return s; + } +} diff --git a/testdemo.java b/testdemo.java new file mode 100644 index 0000000..12bfc1d --- /dev/null +++ b/testdemo.java @@ -0,0 +1,17 @@ +void main() { + println(""" + Clear: - + Markdown: # Test Demo + Text{0}: + ``` + ${0} + ``` + ~~~ + Test{0}: + Send: int i = 2; + Expect: 2 + ~~~ + | Text{0} | Markdown + + """); +} \ No newline at end of file From ecb2ec2204445fb82adb5e329fd2e8b350c8c8ea Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 16 Jun 2025 12:38:49 +0200 Subject: [PATCH 18/50] small fixes and adjustments --- blockingdemo.java | 13 ++++++++++ newdemo.java | 9 +------ src/main/java/lvp/Processor.java | 2 +- src/main/java/lvp/Server.java | 1 - .../lvp/commands/services/Interaction.java | 2 +- src/main/java/lvp/commands/services/Text.java | 25 ++----------------- src/main/java/lvp/skills/TextUtils.java | 18 +++++++++++++ src/main/java/lvp/skills/TurtleParser.java | 6 ++--- 8 files changed, 39 insertions(+), 37 deletions(-) create mode 100644 blockingdemo.java diff --git a/blockingdemo.java b/blockingdemo.java new file mode 100644 index 0000000..99e2a42 --- /dev/null +++ b/blockingdemo.java @@ -0,0 +1,13 @@ +import static java.io.IO.println; + +import java.util.Scanner; + +void main() { + println("Clear: ~"); + println("Markdown: # Blocking Input"); + println("Read:"); + Scanner scanner = new Scanner(System.in); + String d = scanner.nextLine(); + + println("Markdown: Your input was: **" + d + "**"); +} \ No newline at end of file diff --git a/newdemo.java b/newdemo.java index 61a6759..c51304c 100644 --- a/newdemo.java +++ b/newdemo.java @@ -43,18 +43,11 @@ void main() { ${0} ``` ~~~ - Codeblock: newdemo.java:// ex1 + Codeblock: newdemo.java;// ex1 | Text{1} | Markdown """); // ex1 - println("Markdown: # Blocking Input"); - println("Read:"); - Scanner scanner = new Scanner(System.in); - String d = scanner.nextLine(); - - println("Markdown: Your input was: " + d); - println(""" Text{2}: init 0 200 0 25 50 0 0 diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 0f6faf6..8788b36 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -78,7 +78,7 @@ String processCommands(Command command) { else if (services.containsKey(command.name())) { return services.get(command.name()).apply(command.id(), command.content()); } else { - Logger.logError("Command not found: " + command.name()); + Logger.logError("Command not found: " + command.name()); } return null; diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index 4a73196..f33056b 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -15,7 +15,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; -import java.util.stream.IntStream; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; diff --git a/src/main/java/lvp/commands/services/Interaction.java b/src/main/java/lvp/commands/services/Interaction.java index 202b8d7..a0be508 100644 --- a/src/main/java/lvp/commands/services/Interaction.java +++ b/src/main/java/lvp/commands/services/Interaction.java @@ -78,7 +78,7 @@ public static String input(String id, String content) { } Logger.logDebug("Parsed input with path=" + path + ", label=" + label + ", type=" + type); - String inputElement = HTMLElements.input(id, placeholder, type, ParsingTools.stripQuotes(label).replaceFirst("//", "").strip()); + String inputElement = HTMLElements.input("input" + id, placeholder, type, ParsingTools.stripQuotes(label).replaceFirst("//", "").strip()); String button = HTMLElements.button("button" + id, "Send", TextUtils.fillOut(""" (() => { const input = document.getElementById("input${0}"); diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index 302798f..9dc8d08 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -2,9 +2,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import lvp.skills.TextUtils; import lvp.skills.logging.Logger; @@ -17,7 +14,7 @@ public static void clear() { } public static String codeblock(String id, String content) { - String[] parts = content.split(":"); + String[] parts = content.split(";"); if (parts.length != 2) { Logger.logError("Invalid Codeblock Format."); return null; @@ -26,25 +23,7 @@ public static String codeblock(String id, String content) { } public static String of(String id, String content) { - String newValue = templates.merge(id, content, Text::fillOut); + String newValue = templates.merge(id, content, TextUtils::linearFillOut); return newValue == null ? content : newValue; } - - static String fillOut(String template, String replacement) { - Pattern pattern = Pattern.compile("\\$\\{(.*?)\\}"); // `${}` - Matcher matcher = pattern.matcher(template); - StringBuffer result = new StringBuffer(); - String key = ""; - - while (matcher.find()) { - String group = matcher.group(1); - if (key.isBlank()) key = group; - if (!key.equals(group)) continue; - - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } - matcher.appendTail(result); - - return result.toString(); - } } diff --git a/src/main/java/lvp/skills/TextUtils.java b/src/main/java/lvp/skills/TextUtils.java index 7c5add4..63e9a5d 100644 --- a/src/main/java/lvp/skills/TextUtils.java +++ b/src/main/java/lvp/skills/TextUtils.java @@ -110,6 +110,24 @@ public static String fillOut(String template, Object... replacements) { return fillOut(m, template); } + public static String linearFillOut(String template, String replacement) { + Pattern pattern = Pattern.compile("\\$\\{(.*?)\\}"); // `${}` + Matcher matcher = pattern.matcher(template); + StringBuffer result = new StringBuffer(); + String key = ""; + + while (matcher.find()) { + String group = matcher.group(1); + if (key.isBlank()) key = group; + if (!key.equals(group)) continue; + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } + public enum ReplacementType { diff --git a/src/main/java/lvp/skills/TurtleParser.java b/src/main/java/lvp/skills/TurtleParser.java index 6474bca..938b237 100644 --- a/src/main/java/lvp/skills/TurtleParser.java +++ b/src/main/java/lvp/skills/TurtleParser.java @@ -70,11 +70,11 @@ private static void parseCommand(Turtle turtle, String line) { if (parts.length == 0) return; try { - switch (parts[0]) { - case "penUp": + switch (parts[0].toLowerCase()) { + case "penup": turtle.penUp(); break; - case "penDown": + case "pendown": turtle.penDown(); break; case "forward": From eee81dc1b702ab3c8451c264dc0ecea33d0acf77 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 16 Jun 2025 12:39:02 +0200 Subject: [PATCH 19/50] updated syntax information --- syntax.md | 131 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 41 deletions(-) diff --git a/syntax.md b/syntax.md index f365ba7..db7431b 100644 --- a/syntax.md +++ b/syntax.md @@ -1,80 +1,129 @@ # LVP Syntax ## Grammatik +- `[]` -> Optional +- `::=` -> Definiert als +- `''` -> Literal ``` -INSTRUCTION ::= COMMAND | REGISTER +INSTRUCTION ::= COMMAND | REGISTER | PIPE ``` ### Command ``` -COMMAND ::= SERVICE | TARGET +COMMAND ::= COMMANDNAME['{'ID'}']':' CONTENT +COMMAND ::= COMMANDNAME['{'ID'}']':' + CONTENT + '~~~' -COMMANDNAME ::= STRING -CONTENT ::= STRING -``` - -``` -SERVICE ::= SERVICENAME':' CONTENT - -SERVICE ::= SERVICENAME':' - CONTENT - ~~~ - -TARGET ::= TARGETNAME':' CONTENT - -TARGET ::= TARGETNAME':' - CONTENT - ~~~ +COMMANDNAME ::= STRING +ID ::= STRING +CONTENT ::= STRING ``` Grundidee: -Service: String -> String -Target: String -> {} +Aufteilung in Service und Target oder Function und Consumer +- **Service:** String -> String +- **Target:** String -> {} ### Register ``` -NAME ::= STRING -CALL ::= STRING -``` +REGISTER ::= 'Register:' COMMANDNAME CALL -``` -REGISTER ::= 'Register:' NAME CALL +CALL ::= STRING ``` ### Pipe ``` -COMMAND -'|' COMMAND ['|' COMMAND '|' ...] +PIPE ::= '|' COMMAND ['|' COMMAND '|' ...] ``` ## Default Services + +- Text +- Codeblock +- Turtle +- Button +- Input +- Checkbox +- Test + +### Turtle +``` +init XFROM XTO YFROM YTO STARTX STARTY STARTANGLE +init WIDTH HEIGHT + +penup +pendown +forward DISTANCE +backward DISTANCE +right ANGLE +left ANGLE +color R G B [A] +text TEXT [FONT] +width WIDTH +push +pop +timeline +save +``` + +### Codeblock ``` -Cutout: +Codeblock: PATH;LABEL + +Codeblock: PATH LABEL ~~~ +``` +### Test +``` Test: Send: SNIPPET Expect: STRING ~~~ +``` -Test: -Send: SNIPPET -Expect: -STRING1 -STRING2 +### Interaction Elements +``` +Button: +Text: TEXT +[width: WIDTH] +[height: HEIGHT] +path: PATH +label: "LABEL" +replacement: REPLACEMENT ~~~ -Turtle: -COMMANDS +Input: +path: PATH +label: "LABEL" +placeholder: PLACEHOLDER +template: TEMPLATE (with Placeholder '$') +type: TYPE (Text, Email, Number, etc) ~~~ -``` +Checkbox: +path: PATH +label: "LABEL" +template: TEMPLATE (with Placeholder '$') +checked: BOOLEAN +~~~ +``` ## Targets + +- Markdown +- Html +- JavaScript +- JavaScriptCall +- Clear +- Dot + +### Dot ``` -Markdown -Html -JavaScript -JavaScriptCall -Clear +Dot: +[width: WIDTH] +[height: HEIGHT] +GRAPH +~~~ ``` \ No newline at end of file From 9e5bd943fd107bb538bddb639dd567a849c4e118 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 17 Jun 2025 13:52:30 +0200 Subject: [PATCH 20/50] trim -> strip --- src/main/java/lvp/commands/services/Text.java | 2 +- src/main/java/lvp/skills/InstructionParser.java | 2 +- testdemo.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index 9dc8d08..0dd4f67 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -19,7 +19,7 @@ public static String codeblock(String id, String content) { Logger.logError("Invalid Codeblock Format."); return null; } - return TextUtils.codeBlock(parts[0].trim(), parts[1].trim()); + return TextUtils.codeBlock(parts[0].strip(), parts[1].strip()); } public static String of(String id, String content) { diff --git a/src/main/java/lvp/skills/InstructionParser.java b/src/main/java/lvp/skills/InstructionParser.java index 3d4981c..0f7e5b2 100644 --- a/src/main/java/lvp/skills/InstructionParser.java +++ b/src/main/java/lvp/skills/InstructionParser.java @@ -95,7 +95,7 @@ private static boolean tryPipe(String line, Downstream out if (!matcher.matches()) return false; List commands = Arrays.stream(matcher.group(1).split("\\|")) - .map(String::trim) + .map(String::strip) .map(cmd -> { Matcher m = PIPE_ENTRY.matcher(cmd); if (!m.matches()) { diff --git a/testdemo.java b/testdemo.java index 12bfc1d..2702987 100644 --- a/testdemo.java +++ b/testdemo.java @@ -8,7 +8,7 @@ void main() { ``` ~~~ Test{0}: - Send: int i = 2; + Send: 1 + 1 Expect: 2 ~~~ | Text{0} | Markdown From 51314fe81c1cff1c9b7d96a7e8b4ff9f71f18003 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 17 Jun 2025 13:53:09 +0200 Subject: [PATCH 21/50] allow register to skip id, to open calls to cli tools --- registerdemo.java | 6 ++++++ src/main/java/lvp/Processor.java | 2 +- src/main/java/lvp/skills/InstructionParser.java | 7 ++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/registerdemo.java b/registerdemo.java index 6ad67c8..b33496c 100644 --- a/registerdemo.java +++ b/registerdemo.java @@ -5,5 +5,11 @@ void main() { Register: Reverse java --enable-preview external.java Reverse: Hello World | Markdown + Register{skipId}: Wc wc + Wc: + Hello World + Test + ~~~ + | Html """); } \ No newline at end of file diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 8788b36..4fd4eb2 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -127,7 +127,7 @@ String processRegister(Register register) { try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { - writer.write(id + "\n"); + if (!register.skipId()) writer.write(id + "\n"); writer.write(content + "\n"); writer.flush(); } diff --git a/src/main/java/lvp/skills/InstructionParser.java b/src/main/java/lvp/skills/InstructionParser.java index 0f7e5b2..93a0a74 100644 --- a/src/main/java/lvp/skills/InstructionParser.java +++ b/src/main/java/lvp/skills/InstructionParser.java @@ -19,7 +19,7 @@ public class InstructionParser { public sealed interface Instruction permits Command, Register, Read, Pipe {} public record Command(String name, String id, String content) implements Instruction {} - public record Register(String name, String call) implements Instruction {} + public record Register(String name, String call, boolean skipId) implements Instruction {} public record Read(String id) implements Instruction {} public record Pipe(List commands) implements Instruction {} @@ -29,7 +29,7 @@ public record CommandRef(String name, String id) {} private static final Pattern SINGLE_LINE_COMMAND = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*(.+)$"); private static final Pattern BLOCK_START = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*$"); private static final Pattern READ = Pattern.compile("^Read(?:\\{([^}]+)\\})?:\\s*$"); - private static final Pattern REGISTER = Pattern.compile("^Register:\\s+(\\w+)\\s+(.+)$"); + private static final Pattern REGISTER = Pattern.compile("^Register(?:\\{([^}]+)\\})?:\\s+(\\w+)\\s+(.+)$"); private static final Pattern PIPE_LINE = Pattern.compile("^\\s*\\|(.+)$"); private static final Pattern PIPE_ENTRY = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?$"); @@ -122,8 +122,9 @@ private static boolean tryRegister(String line, Downstream Matcher matcher = REGISTER.matcher(line); if (!matcher.matches()) return false; + String skipIdFlag = matcher.group(1); Logger.logDebug("Parsed register: " + matcher.group(1) + " -> " + matcher.group(2)); - out.push(new Register(matcher.group(1), matcher.group(2))); + out.push(new Register(matcher.group(2), matcher.group(3), skipIdFlag != null && skipIdFlag.equals("skipId"))); return true; } From 4f99250baf2a70722edff055ac07ecb2aba93aaf Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 13:22:06 +0200 Subject: [PATCH 22/50] improved handeling of multiple sources, changed args --- src/main/java/lvp/FileWatcher.java | 154 +++++++++++------- src/main/java/lvp/Main.java | 90 ++++++---- src/main/java/lvp/Processor.java | 24 +-- src/main/java/lvp/Server.java | 6 +- .../java/lvp/commands/services/Turtle.java | 2 +- .../java/lvp/skills/parser/ConfigParser.java | 71 ++++++++ .../{ => parser}/InstructionParser.java | 15 +- .../java/lvp/skills/parser/PathParser.java | 92 +++++++++++ .../lvp/skills/{ => parser}/TurtleParser.java | 2 +- 9 files changed, 346 insertions(+), 110 deletions(-) create mode 100644 src/main/java/lvp/skills/parser/ConfigParser.java rename src/main/java/lvp/skills/{ => parser}/InstructionParser.java (94%) create mode 100644 src/main/java/lvp/skills/parser/PathParser.java rename src/main/java/lvp/skills/{ => parser}/TurtleParser.java (99%) diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 4fa656b..161e861 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.nio.file.ClosedWatchServiceException; -import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; @@ -11,90 +10,144 @@ import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; -import java.util.ArrayList; +import java.time.Duration; +import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; import lvp.skills.logging.Logger; +import lvp.skills.parser.ConfigParser.Source; public class FileWatcher { private WatchService watcher; - private ScheduledExecutorService debounceExecutor; - private final AtomicReference> pendingTask = new AtomicReference<>(); - + private ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + private Map lastModified = new ConcurrentHashMap<>(); private boolean isRunning = true; - Path dir; - String fileNamePattern; + private static final Duration DEBOUNCE_DURATION = Duration.ofMillis(500); + + List sources; Processor processor; - - public FileWatcher(Path dir, String fileNamePattern, Processor processor) throws IOException{ - this.dir = dir; - this.fileNamePattern = fileNamePattern; + Optional watchFilter; + boolean sourceOnly; + + public FileWatcher(List sources, Optional watchFilter, boolean sourceOnly, Processor processor) throws IOException{ this.processor = processor; - + this.sources = sources; + this.watchFilter = watchFilter.isEmpty() ? Optional.empty() : + Optional.of(FileSystems.getDefault().getPathMatcher("glob:" + watchFilter.get())); + this.sourceOnly = sourceOnly; + watcher = FileSystems.getDefault().newWatchService(); - dir.register(watcher, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_MODIFY); - Logger.logInfo("Watching in " + dir.normalize().toAbsolutePath() + ""); + sources.stream() + .map(Source::path) + .map(Path::getParent) + .filter(Objects::nonNull) + .map(Path::normalize) + .flatMap(root -> { + try { + return Files.find(root, Integer.MAX_VALUE, + (_, attrs) -> attrs.isDirectory()); + } catch (IOException e) { + Logger.logError("Error walking directory: " + root.toAbsolutePath(), e); + return Stream.empty(); + } + }) + .distinct() + .forEach(dir -> { + try { + dir.register(watcher, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY); + Logger.logInfo("Watching in " + dir.toAbsolutePath() + ""); + } catch (IOException e) { + Logger.logError("Error registering directory for watching: " + dir.toAbsolutePath(), e); + } + }); - for (Path path : getMatchingFiles()) { - Logger.logInfo("Running initial file: " + path.toAbsolutePath().normalize()); - run(path); + } + + public void start() { + for (Source source : sources) { + Logger.logInfo("Running initial file: " + source.path()); + lastModified.put(source.path(), Instant.now()); + executor.submit(() -> run(source)); } + executor.submit(this::watchLoop); } - public void watchLoop() { - PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + fileNamePattern); - debounceExecutor = Executors.newSingleThreadScheduledExecutor(); - long debounceDelay = 200; + private void watchLoop() { while (isRunning) { WatchKey key; try { key = watcher.take(); - } catch (InterruptedException | ClosedWatchServiceException e) { + processWatchKeyEvents(key); + } catch (ClosedWatchServiceException | InterruptedException e) { Logger.logError("Watcher loop terminated due to exception: " + e.getMessage(), e); + if (e instanceof InterruptedException) Thread.currentThread().interrupt(); break; } - processWatchKeyEvents(key, matcher, debounceDelay); if (!key.reset()) isRunning = false; } } - private void processWatchKeyEvents(WatchKey key, PathMatcher matcher, long debounceDelay) { + private void processWatchKeyEvents(WatchKey key) { for (WatchEvent ev : key.pollEvents()) { Path changed = (Path) ev.context(); - if (matcher.matches(changed) && !Files.isDirectory(changed)) { - Logger.logInfo("Event für Datei: " + changed.toAbsolutePath() + " (" + ev.kind().name() + ")"); - - ScheduledFuture prev = pendingTask.getAndSet( - debounceExecutor.schedule(() -> run(dir.resolve(changed)), debounceDelay, TimeUnit.MILLISECONDS) - ); - if (prev != null && !prev.isDone()) prev.cancel(false); + if (Files.isDirectory(changed)) continue; + + Path dir = (Path) key.watchable(); + Path fullPath = dir.resolve(changed).normalize().toAbsolutePath(); + + Instant now = Instant.now(); + Instant last = lastModified.getOrDefault(fullPath, Instant.EPOCH); + Logger.logDebug(last + " -> " + now + " (" + Duration.between(last, now).toMillis() + "ms)"); + if (Duration.between(last, now).compareTo(DEBOUNCE_DURATION) < 0) return; + lastModified.put(fullPath, now); + + Optional source = sources.stream() + .filter(s -> s.path().equals(fullPath)) + .findFirst(); + if (source.isPresent()) { + Logger.logInfo("Event for source: " + fullPath + " (" + ev.kind().name() + ")"); + executor.submit(() -> run(source.get())); + } + else if (!sourceOnly && (watchFilter.isEmpty() || watchFilter.get().matches(changed))) { + Logger.logInfo("Event for file: " + fullPath + " (" + ev.kind().name() + ")"); + execute(sources); } } } + private void execute(List sources) { + for (Source source : sources) { + executor.submit(() -> run(source)); + } + } + public void stop() { isRunning = false; - if (watcher != null) try { watcher.close(); } catch (IOException e) { e.printStackTrace(); } - if (debounceExecutor != null) debounceExecutor.shutdownNow(); + if (watcher != null) try { watcher.close(); } catch (IOException _) { } + if (executor != null) executor.shutdownNow(); } - private void run(Path path) { + private void run(Source source) { + processor.init(); try { - processor.init(); - Logger.logInfo("Executing java --enable-preview " + path.normalize().toString()); - ProcessBuilder pb = new ProcessBuilder("java", "-Dsun.stdout.encoding=UTF-8", "--enable-preview", path.normalize().toString()) + boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + Logger.logInfo("Running: " + source.cmd() + " " + source.path()); + ProcessBuilder pb = new ProcessBuilder(isWindows ? new String[]{"cmd.exe", "/c", source.cmd(), source.path().toString()} : new String[]{"sh", "-c", source.cmd(), source.path().toString()}) .redirectErrorStream(true); Process process = pb.start(); - processor.process(process); + processor.process(process, source.path()); - boolean finished = process.waitFor(30, TimeUnit.SECONDS); + boolean finished = process.waitFor(10, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); Logger.logError("Timeout: process killed"); @@ -106,17 +159,4 @@ private void run(Path path) { Logger.logError("Error in Java Process", e); } } - - public List getMatchingFiles() throws IOException { - List matchingFiles = new ArrayList<>(); - PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + fileNamePattern); - try (DirectoryStream stream = Files.newDirectoryStream(dir)) { - for (Path entry : stream) { - if (!Files.isDirectory(entry) && matcher.matches(entry.getFileName())) { - matchingFiles.add(entry); - } - } - } - return matchingFiles; - } } diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index a9032a4..cace634 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -3,11 +3,16 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import lvp.skills.logging.LogLevel; import lvp.skills.logging.Logger; +import lvp.skills.parser.ConfigParser; +import lvp.skills.parser.PathParser; +import lvp.skills.parser.ConfigParser.Source; import java.net.URI; import java.net.http.HttpClient; @@ -15,10 +20,11 @@ import java.net.http.HttpResponse; public class Main { - private record Config(Path path, String fileNamePattern, int port, LogLevel logLevel){} + private record Config(List sources, int port, LogLevel logLevel, Optional watchFilter, boolean sourceOnly){} + + private static final Path LVP_CONFIG_PATH = Path.of("./sources.json"); public static void main(String[] args) { Config cfg = parseArgs(args); - Logger.setLogLevel(cfg.logLevel()); if (!isLatestRelease()) { System.out.println("Warning: You are not using the latest release of Live View Programming. Please visit https://github.com/denkspuren/LiveViewProgramming/releases"); @@ -28,11 +34,9 @@ public static void main(String[] args) { Server server = new Server(Math.abs(cfg.port()), cfg.logLevel().equals(LogLevel.Debug)); Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); Processor processor = new Processor(server); - if(cfg.path() != null) { - FileWatcher watcher = new FileWatcher(cfg.path(), cfg.fileNamePattern(), processor); - Runtime.getRuntime().addShutdownHook(new Thread(watcher::stop)); - watcher.watchLoop(); - } + FileWatcher watcher = new FileWatcher(cfg.sources(), cfg.watchFilter(), cfg.sourceOnly(), processor); + Runtime.getRuntime().addShutdownHook(new Thread(watcher::stop)); + watcher.start(); } catch (IOException e) { System.err.println("Error starting lvp: " + e.getMessage()); e.printStackTrace(); @@ -41,56 +45,82 @@ public static void main(String[] args) { } private static Config parseArgs(String[] args) { - String fileNamePattern = null; - Path fileName = null; - Path path = null; + List files = new ArrayList<>(); + Optional cmd = Optional.empty(); int port = Server.getDefaultPort(); LogLevel logLevel = LogLevel.Error; + Optional> sources = Optional.empty(); + Optional watchFilter = Optional.empty(); + boolean sourceOnly = false; for (String arg : args) { String[] parts = arg.split("=", 2); - String key = parts[0].trim(); - String value = parts.length > 1 ? parts[1].trim() : ""; + String key = parts[0].strip(); + String value = parts.length > 1 ? parts[1].strip() : ""; switch (key) { case "-l", "--log": logLevel = value.isBlank() ? LogLevel.Info : LogLevel.fromString(value); break; - case "-p", "--pattern": - fileNamePattern = value.isBlank() ? "*" : value; + case "--port", "-p": + try { port = Integer.parseInt(value); } catch(NumberFormatException _) {} + break; + case "--cmd": + cmd = value.isBlank() ? Optional.empty() : Optional.of(value); break; - case "--watch", "-w": - path = value.isBlank() ? Paths.get(".") : Paths.get(value).normalize(); + case "--config", "-c": + sources = loadConfig(); + break; + case "--watch-filter", "-w": + watchFilter = value.isBlank() ? Optional.empty() : Optional.of(value); + break; + case "--source-only", "-s": + sourceOnly = true; break; default: - try { port = Integer.parseInt(arg.trim()); } catch(NumberFormatException _) {} + if (!arg.isBlank()) files.add(arg.strip()); break; } } + Logger.setLogLevel(logLevel); + if (port < 1 || port > 65535) { System.err.println("Error: Invalid port number. Must be between 1 and 65535."); System.exit(1); } + Logger.logDebug(files.isEmpty() ? "No files provided." : "Files to execute: " + files); + Optional> paths = getFilePaths(files); - if (path == null) return new Config(null, null, port, logLevel); - - if (!Files.exists(path)) { - System.err.println("Error: Path not found " + path); + if (paths.isEmpty() && sources.isEmpty()) { + System.err.println("Error: No valid files to execute."); System.exit(1); } - if(!Files.isDirectory(path)) { - if (path.getFileName().toString().endsWith(".java")) fileName = path.getFileName(); - path = path.getParent() != null ? path.getParent() : Paths.get("."); + if (!paths.isEmpty()) { + sources = Optional.of(sources.orElseGet(ArrayList::new)); + String c = cmd.orElse("java -Dsun.stdout.encoding=UTF-8 --enable-preview"); + List sourcesFromPaths = paths.get().stream().map(path -> new Source(path, c)).toList(); + sources.ifPresent(lst -> lst.addAll(sourcesFromPaths)); } - if (fileName == null && fileNamePattern == null) { - System.err.println("Error: No Java file or pattern specified."); - System.exit(1); + return new Config(sources.get(), port, logLevel, watchFilter, sourceOnly); + } + + private static Optional> getFilePaths(List files) { + List paths = new ArrayList<>(); + for (String file : files) { + PathParser.parse(file).ifPresent(paths::addAll); } + return paths.isEmpty() ? Optional.empty() : Optional.of(paths); + } - return new Config(path, fileNamePattern != null ? fileNamePattern : fileName.toString(), port, logLevel); + private static Optional> loadConfig() { + if (!Files.isRegularFile(LVP_CONFIG_PATH) || !Files.exists(LVP_CONFIG_PATH)) { + Logger.logError("Config not found at: " + LVP_CONFIG_PATH.normalize().toAbsolutePath()); + return Optional.empty(); + } + return ConfigParser.parse(LVP_CONFIG_PATH); } public static boolean isLatestRelease() { @@ -116,7 +146,7 @@ public static boolean isLatestRelease() { } } - public static String extractJsonField(String json, String field) { + private static String extractJsonField(String json, String field) { Matcher matcher = java.util.regex.Pattern .compile("\"" + field + "\"\\s*:\\s*\"([^\"]+)\"") .matcher(json); diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 4fd4eb2..49c6204 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -5,6 +5,7 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -19,14 +20,14 @@ import lvp.commands.services.Interaction; import lvp.commands.targets.Targets; import lvp.skills.HTMLElements; -import lvp.skills.InstructionParser; import lvp.skills.TextUtils; -import lvp.skills.InstructionParser.Command; -import lvp.skills.InstructionParser.CommandRef; -import lvp.skills.InstructionParser.Pipe; -import lvp.skills.InstructionParser.Read; -import lvp.skills.InstructionParser.Register; import lvp.skills.logging.Logger; +import lvp.skills.parser.InstructionParser; +import lvp.skills.parser.InstructionParser.Command; +import lvp.skills.parser.InstructionParser.CommandRef; +import lvp.skills.parser.InstructionParser.Pipe; +import lvp.skills.parser.InstructionParser.Read; +import lvp.skills.parser.InstructionParser.Register; public class Processor { Server server; Targets targetProcessor; @@ -52,17 +53,18 @@ public Processor(Server server) { "Clear", targetProcessor::consumeClear); } - void process(Process process) { + void process(Process process, Path path) { try(BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { InstructionParser.parse(reader.lines()).gather(Gatherers.fold(() -> "", (prev, curr) -> switch (curr) { case Command cmd -> processCommands(cmd); case Pipe pipe -> processPipe(pipe, prev); - case Read read -> processRead(read, process); + case Read read -> processRead(read, process, path); case Register register -> processRegister(register); default -> null; })).forEachOrdered(_->{}); + } catch (Exception e) { Logger.logError("Error reading process output: " + e.getMessage(), e); @@ -102,15 +104,15 @@ else if (services.containsKey(ref.name())) { return current; } - String processRead(Read read, Process process) { - server.waitingStreams.put(read.id(), process.getOutputStream()); + String processRead(Read read, Process process, Path path) { + server.waitingProcesses.put(path, process); String inputField = HTMLElements.input("input" + read.id()); String button = HTMLElements.button("button" + read.id(), "Send", TextUtils.fillOut(""" (()=>{ const input = document.getElementById("input${0}"); fetch("read", { method: "post", body: "${0}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); })() - """,read.id())); + """, path)); targetProcessor.consumeHTML(read.id(), inputField + button); return null; } diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index f33056b..15f610f 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -39,7 +39,7 @@ private record EventMessage(SSEType event, String data) {} public final List webClients = new CopyOnWriteArrayList<>(); // thread-safe variant of ArrayList; List events = new CopyOnWriteArrayList<>(); - Map waitingStreams = new ConcurrentHashMap<>(); + Map waitingProcesses = new ConcurrentHashMap<>(); boolean isVerbose = false; @@ -94,7 +94,7 @@ private void handleRead(HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(200, 0); exchange.close(); - OutputStream stream = waitingStreams.get(parts[0]); + OutputStream stream = waitingProcesses.get(parts[0]).getOutputStream(); if (stream == null) { Logger.logError("Stream not found: " + message); return; @@ -105,7 +105,7 @@ private void handleRead(HttpExchange exchange) throws IOException { Logger.logError("Error while writing stream for: " + parts[0], e); } finally { stream.close(); - waitingStreams.remove(parts[0]); + waitingProcesses.remove(parts[0]); } } diff --git a/src/main/java/lvp/commands/services/Turtle.java b/src/main/java/lvp/commands/services/Turtle.java index 7bc5c56..a96f869 100644 --- a/src/main/java/lvp/commands/services/Turtle.java +++ b/src/main/java/lvp/commands/services/Turtle.java @@ -11,7 +11,7 @@ import lvp.skills.HTMLElements; import lvp.skills.TextUtils; -import lvp.skills.TurtleParser; +import lvp.skills.parser.TurtleParser; /** * Turtle ermöglicht das Erstellen einfacher Turtle-Grafiken als SVG-Datei. diff --git a/src/main/java/lvp/skills/parser/ConfigParser.java b/src/main/java/lvp/skills/parser/ConfigParser.java new file mode 100644 index 0000000..d43bdb6 --- /dev/null +++ b/src/main/java/lvp/skills/parser/ConfigParser.java @@ -0,0 +1,71 @@ +package lvp.skills.parser; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lvp.skills.logging.Logger; + +public class ConfigParser { + public record Source(Path path, String cmd) {} + + private static final Pattern OBJECT_PATTERN = Pattern.compile( + "\\{\\s*\"path\"\\s*:\\s*\"(.*?)\"\\s*,\\s*\"cmd\"\\s*:\\s*\"(.*?)\"\\s*\\}," + ); + + public static Optional> parse(Path path) { + try { + String content = Files.readString(path).strip(); + return parseJson(content); + } catch (IOException e) { + Logger.logError("Error reading file: " + e.getMessage(), e); + } + return Optional.empty(); + } + + private static Optional> parseJson(String json) { + if (!json.startsWith("[") || !json.endsWith("]")) { + Logger.logError("Expected JSON array."); + return Optional.empty(); + } + + String arrayContent = json.substring(1, json.length() - 1).strip(); + if (arrayContent.isEmpty()) { + Logger.logError("JSON array empty."); + return Optional.empty(); + } + + Matcher matcher = OBJECT_PATTERN.matcher(arrayContent); + List sources = new ArrayList<>(); + + StringBuilder cleaned = new StringBuilder(); + + while (matcher.find()) { + String pathString = matcher.group(1); + Optional> paths = PathParser.parse(pathString); + if (paths.isEmpty()) { + Logger.logError("Invalid Path in JSON: " + pathString); + return Optional.empty(); + } + + String cmd = matcher.group(2); + sources.addAll(paths.get().stream() + .map(path -> new Source(path, cmd)) + .toList()); + matcher.appendReplacement(cleaned, ""); + } + matcher.appendTail(cleaned); + + String remaining = cleaned.toString().replaceAll("[\\s]*", ""); + if (!remaining.isEmpty()) { + Logger.logError("Unexpected content in JSON: " + remaining); + return Optional.empty(); + } + return sources.isEmpty() ? Optional.empty() : Optional.of(sources); + } +} diff --git a/src/main/java/lvp/skills/InstructionParser.java b/src/main/java/lvp/skills/parser/InstructionParser.java similarity index 94% rename from src/main/java/lvp/skills/InstructionParser.java rename to src/main/java/lvp/skills/parser/InstructionParser.java index 93a0a74..c0d30f7 100644 --- a/src/main/java/lvp/skills/InstructionParser.java +++ b/src/main/java/lvp/skills/parser/InstructionParser.java @@ -1,4 +1,4 @@ -package lvp.skills; +package lvp.skills.parser; import java.util.StringJoiner; import java.util.regex.Matcher; @@ -6,6 +6,7 @@ import java.util.stream.Stream; import java.util.stream.Gatherer.Downstream; +import lvp.skills.IdGen; import lvp.skills.logging.Logger; import java.util.stream.Gatherer; @@ -45,7 +46,7 @@ void init(String name, String id) { this.id = id; this.content = new StringJoiner("\n"); this.inBlock = true; - Logger.logDebug("Started block command: " + name + formatId(id)); + Logger.logDebug("Started block command: " + name + formatFlag(id)); } void append(String line) { @@ -123,7 +124,7 @@ private static boolean tryRegister(String line, Downstream if (!matcher.matches()) return false; String skipIdFlag = matcher.group(1); - Logger.logDebug("Parsed register: " + matcher.group(1) + " -> " + matcher.group(2)); + Logger.logDebug("Parsed register" + formatFlag(skipIdFlag) + ": " + matcher.group(2) + " -> " + matcher.group(3)); out.push(new Register(matcher.group(2), matcher.group(3), skipIdFlag != null && skipIdFlag.equals("skipId"))); return true; } @@ -133,7 +134,7 @@ private static boolean tryRead(String line, Downstream out if (!matcher.matches()) return false; String id = matcher.group(1) == null ? IdGen.generateID(10) : matcher.group(1); - Logger.logDebug("Parsed Read" + formatId(id)); + Logger.logDebug("Parsed Read" + formatFlag(id)); out.push(new Read(id)); return true; } @@ -142,7 +143,7 @@ private static boolean trySingleCommand(String line, Downstream out) { if (line.equals("~~~")) { - Logger.logDebug("Parsed block command: " + state.name + formatId(state.id)); + Logger.logDebug("Parsed block command: " + state.name + formatFlag(state.id)); out.push(new Command(state.name, state.id, state.content.toString())); state.reset(); } else { @@ -166,7 +167,7 @@ private static void handleBlockContent(BlockState state, String line, Downstream } } - private static String formatId(String id) { + private static String formatFlag(String id) { return id != null ? "{" + id + "}" : ""; } } diff --git a/src/main/java/lvp/skills/parser/PathParser.java b/src/main/java/lvp/skills/parser/PathParser.java new file mode 100644 index 0000000..d64d9c4 --- /dev/null +++ b/src/main/java/lvp/skills/parser/PathParser.java @@ -0,0 +1,92 @@ +package lvp.skills.parser; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import lvp.skills.ParsingTools; +import lvp.skills.logging.Logger; + +public class PathParser { + private PathParser() {} + + public static Optional> parse(String file) { + if (file.contains("*") || file.contains("?") || file.contains("[")) + return resolveGlob(file); + + Optional path = ParsingTools.tryPath(file); + if (path.isEmpty()) { + Logger.logError("Invalid Path: '" + file + "'"); + return Optional.empty(); + } + + Path normalizedPath = path.get().normalize(); + + if (!Files.exists(normalizedPath)) { + Logger.logError("File does not exists: '" + normalizedPath.toAbsolutePath() + "'"); + return Optional.empty(); + } + + if (Files.isDirectory(normalizedPath)) { + Logger.logError("File is a directory: '" + normalizedPath.toAbsolutePath() + "'"); + return Optional.empty(); + } + + return Optional.of(List.of(normalizedPath.toAbsolutePath())); + } + + private static Optional> resolveGlob(String file) { + int[] indices = { + file.indexOf('*'), + file.indexOf('?'), + file.indexOf('['), + file.indexOf(']') + }; + int firstGlob = Arrays.stream(indices).filter(i -> i >= 0).min().orElse(0); + int lastSlash = Math.max(file.substring(0, firstGlob).lastIndexOf('/'), file.substring(0, firstGlob).lastIndexOf('\\')); + String validPart = lastSlash >= 0 ? file.substring(0, lastSlash) : "."; + + Optional path = ParsingTools.tryPath(validPart); + if (path.isEmpty()) { + Logger.logError("Invalid Path: '" + validPart + "'"); + return Optional.empty(); + } + Path dir = path.get().normalize(); + + if (!Files.isDirectory(dir)) { + Logger.logError("Invalid Path: '" + file + "'. '" + validPart + "' is not a directory."); + return Optional.empty(); + } + + Logger.logDebug("Valid Part: '" + validPart + "' -> Directory: '" + dir.toAbsolutePath() + "'"); + return walkDir(dir, file); + } + + private static Optional> walkDir(Path dir, String globPart) { + try (DirectoryStream stream = Files.newDirectoryStream(dir)) { + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + globPart); + List matchingFiles = new ArrayList<>(); + for (Path entry : stream) { + Logger.logDebug("Checking file: " + entry); + if (Files.isDirectory(entry)) { + matchingFiles.addAll(walkDir(entry, globPart).orElse(List.of())); + } + if (Files.isRegularFile(entry) && matcher.matches(entry)) { + Logger.logDebug("Match found."); + matchingFiles.add(entry.toAbsolutePath()); + } + } + return matchingFiles.isEmpty() ? Optional.empty() : Optional.of(matchingFiles); + } catch (IOException e) { + Logger.logError("Invalid Path: '" + dir + "'", e); + return Optional.empty(); + } + } +} diff --git a/src/main/java/lvp/skills/TurtleParser.java b/src/main/java/lvp/skills/parser/TurtleParser.java similarity index 99% rename from src/main/java/lvp/skills/TurtleParser.java rename to src/main/java/lvp/skills/parser/TurtleParser.java index 938b237..254c9f7 100644 --- a/src/main/java/lvp/skills/TurtleParser.java +++ b/src/main/java/lvp/skills/parser/TurtleParser.java @@ -1,4 +1,4 @@ -package lvp.skills; +package lvp.skills.parser; import java.util.Optional; import java.util.regex.Matcher; From 7f806ca3899f063448b72790f51f31963886bfb0 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 13:39:56 +0200 Subject: [PATCH 23/50] added demos for multi source support --- .gitignore | 3 ++- demo/demo.java | 5 +++++ demo/sub1/demo.bat | 2 ++ demo/sub1/demo.java | 6 ++++++ demo/sub1/demo.sh | 1 + demo/sub2/demo.java | 5 +++++ demo/sub2/support.java | 3 +++ demo_sources.json | 4 ++++ 8 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 demo/demo.java create mode 100644 demo/sub1/demo.bat create mode 100644 demo/sub1/demo.java create mode 100644 demo/sub1/demo.sh create mode 100644 demo/sub2/demo.java create mode 100644 demo/sub2/support.java create mode 100644 demo_sources.json diff --git a/.gitignore b/.gitignore index ddb9e19..bdd3865 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ build test.java Test.java !**/services/Test.java -.vscode \ No newline at end of file +.vscode +sources.json diff --git a/demo/demo.java b/demo/demo.java new file mode 100644 index 0000000..d12f52f --- /dev/null +++ b/demo/demo.java @@ -0,0 +1,5 @@ +void main() { + println(""" + Markdown: # Demo + """); +} \ No newline at end of file diff --git a/demo/sub1/demo.bat b/demo/sub1/demo.bat new file mode 100644 index 0000000..352f18b --- /dev/null +++ b/demo/sub1/demo.bat @@ -0,0 +1,2 @@ +@echo off +echo Markdown: # PowerShell Demo \ No newline at end of file diff --git a/demo/sub1/demo.java b/demo/sub1/demo.java new file mode 100644 index 0000000..3751f7d --- /dev/null +++ b/demo/sub1/demo.java @@ -0,0 +1,6 @@ +void main() { + println(""" + Clear: ~ + Markdown: # Demo Sub 1 + """); +} \ No newline at end of file diff --git a/demo/sub1/demo.sh b/demo/sub1/demo.sh new file mode 100644 index 0000000..0257c68 --- /dev/null +++ b/demo/sub1/demo.sh @@ -0,0 +1 @@ +echo "Markdown: # Shell Demo" \ No newline at end of file diff --git a/demo/sub2/demo.java b/demo/sub2/demo.java new file mode 100644 index 0000000..3cd240c --- /dev/null +++ b/demo/sub2/demo.java @@ -0,0 +1,5 @@ +void main() { + println(""" + Markdown: # Demo Sub2 + """); +} \ No newline at end of file diff --git a/demo/sub2/support.java b/demo/sub2/support.java new file mode 100644 index 0000000..b2a61aa --- /dev/null +++ b/demo/sub2/support.java @@ -0,0 +1,3 @@ +public class support { + +} diff --git a/demo_sources.json b/demo_sources.json new file mode 100644 index 0000000..1cb1077 --- /dev/null +++ b/demo_sources.json @@ -0,0 +1,4 @@ +[ + { "path": "demo/**/demo.java", "cmd": "java --enable-preview -Dsun.stdout.encoding=UTF-8" }, + { "path": "demo/sub1/demo.bat", "cmd": "" } +] \ No newline at end of file From 4b5ab66b98bea80377dc2d3170e51e83078b49ad Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 13:40:05 +0200 Subject: [PATCH 24/50] fixed regex --- src/main/java/lvp/skills/parser/ConfigParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/lvp/skills/parser/ConfigParser.java b/src/main/java/lvp/skills/parser/ConfigParser.java index d43bdb6..1a63da4 100644 --- a/src/main/java/lvp/skills/parser/ConfigParser.java +++ b/src/main/java/lvp/skills/parser/ConfigParser.java @@ -15,7 +15,7 @@ public class ConfigParser { public record Source(Path path, String cmd) {} private static final Pattern OBJECT_PATTERN = Pattern.compile( - "\\{\\s*\"path\"\\s*:\\s*\"(.*?)\"\\s*,\\s*\"cmd\"\\s*:\\s*\"(.*?)\"\\s*\\}," + "\\{\\s*\"path\"\\s*:\\s*\"(.*?)\"\\s*,\\s*\"cmd\"\\s*:\\s*\"(.*?)\"\\s*\\},?" ); public static Optional> parse(Path path) { From 19ec728aea0df49c3e62c13df91102db16b88573 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 15:11:49 +0200 Subject: [PATCH 25/50] use sourceId in targets --- src/main/java/lvp/FileWatcher.java | 2 +- src/main/java/lvp/Processor.java | 28 ++++++++--------- src/main/java/lvp/Server.java | 29 ++++++++++------- .../java/lvp/commands/targets/Targets.java | 31 ++++++++++--------- .../java/lvp/skills/parser/ConfigParser.java | 8 ++++- 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 161e861..3cc0f50 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -145,7 +145,7 @@ private void run(Source source) { ProcessBuilder pb = new ProcessBuilder(isWindows ? new String[]{"cmd.exe", "/c", source.cmd(), source.path().toString()} : new String[]{"sh", "-c", source.cmd(), source.path().toString()}) .redirectErrorStream(true); Process process = pb.start(); - processor.process(process, source.path()); + processor.process(process, source.id()); boolean finished = process.waitFor(10, TimeUnit.SECONDS); if (!finished) { diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 49c6204..a61ade9 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -5,7 +5,6 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -19,6 +18,7 @@ import lvp.commands.services.Turtle; import lvp.commands.services.Interaction; import lvp.commands.targets.Targets; +import lvp.commands.targets.Targets.MetaInformation; import lvp.skills.HTMLElements; import lvp.skills.TextUtils; import lvp.skills.logging.Logger; @@ -31,7 +31,7 @@ public class Processor { Server server; Targets targetProcessor; - Map> targets; + Map> targets; Map> services = new HashMap<>(Map.of( "Text", Text::of, "Codeblock", Text::codeblock, @@ -53,14 +53,14 @@ public Processor(Server server) { "Clear", targetProcessor::consumeClear); } - void process(Process process, Path path) { + void process(Process process, String sourceId) { try(BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { InstructionParser.parse(reader.lines()).gather(Gatherers.fold(() -> "", (prev, curr) -> switch (curr) { - case Command cmd -> processCommands(cmd); - case Pipe pipe -> processPipe(pipe, prev); - case Read read -> processRead(read, process, path); + case Command cmd -> processCommands(cmd, sourceId); + case Pipe pipe -> processPipe(pipe, prev, sourceId); + case Read read -> processRead(read, process, sourceId); case Register register -> processRegister(register); default -> null; })).forEachOrdered(_->{}); @@ -71,11 +71,11 @@ void process(Process process, Path path) { } } - String processCommands(Command command) { + String processCommands(Command command, String sourceId) { Logger.logDebug("Command: " + command.name() + "{" + command.id() + "}, " + command.content()); if (targets.containsKey(command.name())) { - targets.get(command.name()).accept(command.id(), command.content()); + targets.get(command.name()).accept(new MetaInformation(sourceId, command.id()), command.content()); } else if (services.containsKey(command.name())) { return services.get(command.name()).apply(command.id(), command.content()); @@ -86,13 +86,13 @@ else if (services.containsKey(command.name())) { return null; } - String processPipe(Pipe pipe, String input) { + String processPipe(Pipe pipe, String input, String sourceId) { if (input == null) return null; String current = input; for (CommandRef ref : pipe.commands()) { Logger.logDebug("Command: " + ref.name() + "{" + ref.id() + "}, " + current); if (targets.containsKey(ref.name())) { - targets.get(ref.name()).accept(ref.id(), current); + targets.get(ref.name()).accept(new MetaInformation(sourceId, ref.id()), current); return null; } else if (services.containsKey(ref.name())) { @@ -104,16 +104,16 @@ else if (services.containsKey(ref.name())) { return current; } - String processRead(Read read, Process process, Path path) { - server.waitingProcesses.put(path, process); + String processRead(Read read, Process process, String sourceId) { + server.waitingProcesses.put(sourceId, process); String inputField = HTMLElements.input("input" + read.id()); String button = HTMLElements.button("button" + read.id(), "Send", TextUtils.fillOut(""" (()=>{ const input = document.getElementById("input${0}"); fetch("read", { method: "post", body: "${0}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); })() - """, path)); - targetProcessor.consumeHTML(read.id(), inputField + button); + """, sourceId)); + targetProcessor.consumeHTML(new MetaInformation(sourceId, read.id()), inputField + button); return null; } diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index 15f610f..af53b44 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -16,6 +16,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; +import com.sun.jdi.event.Event; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; @@ -26,7 +27,7 @@ public class Server { - private record EventMessage(SSEType event, String data) {} + private record EventMessage(SSEType type, String data, String id, String sourceId) {} private final HttpServer httpServer; @@ -39,7 +40,7 @@ private record EventMessage(SSEType event, String data) {} public final List webClients = new CopyOnWriteArrayList<>(); // thread-safe variant of ArrayList; List events = new CopyOnWriteArrayList<>(); - Map waitingProcesses = new ConcurrentHashMap<>(); + Map waitingProcesses = new ConcurrentHashMap<>(); boolean isVerbose = false; @@ -153,11 +154,9 @@ private void handleEvents(HttpExchange exchange) throws IOException { exchange.getResponseHeaders().add("Connection", "keep-alive"); exchange.sendResponseHeaders(200, 0); - if (isVerbose) sendMessageToClient(exchange, SSEType.DEBUG, ""); - webClients.add(exchange); if (!events.isEmpty()) { - events.forEach(event -> sendMessageToClient(exchange, event.event, event.data)); + events.forEach(event -> sendMessageToClient(exchange, event)); } } @@ -182,16 +181,24 @@ private void handleRoot(HttpExchange exchange) throws IOException { } } - public void sendServerEvent(SSEType sseType, String data) { - events.add(new EventMessage(sseType, data)); + public void sendServerEvent(SSEType type, String data, String id, String sourceId) { + Logger.logDebug("Event: " + type + " with data: " + data + " to " + sourceId); + sendServerEvent(new EventMessage(type, Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)), id, sourceId)); + } + + private void sendServerEvent(EventMessage event) { + events.add(event); if (webClients.isEmpty()) return; - webClients.removeIf(connection -> !sendMessageToClient(connection, sseType, data)); + webClients.removeIf(connection -> !sendMessageToClient(connection, event)); } - private boolean sendMessageToClient(HttpExchange connection, SSEType event, String data) { - Logger.logDebug("Event: " + event + " with data: " + data); + private boolean sendMessageToClient(HttpExchange connection, EventMessage event) { try { - String message = "data: " + event + ":" + Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)) + "\n\n"; + String message = "data: " + event.type() + + ":" + event.sourceId() + + ":" + event.id() + + ":" + event.data() + + "\n\n"; OutputStream os = connection.getResponseBody(); os.write(message.getBytes(StandardCharsets.UTF_8)); os.flush(); diff --git a/src/main/java/lvp/commands/targets/Targets.java b/src/main/java/lvp/commands/targets/Targets.java index 3ad76fb..0ae8aa6 100644 --- a/src/main/java/lvp/commands/targets/Targets.java +++ b/src/main/java/lvp/commands/targets/Targets.java @@ -5,6 +5,7 @@ import lvp.commands.targets.dot.GraphSpec; public class Targets { + public record MetaInformation(String sourceId, String id) {} Server server; public static Targets of(Server server) { return new Targets(server); } @@ -13,27 +14,27 @@ private Targets(Server server) { this.server = server; } - public void consumeClear(String id, String content) { - server.sendServerEvent(SSEType.CLEAR, ""); + public void consumeClear(MetaInformation meta, String content) { + server.sendServerEvent(SSEType.CLEAR, "", meta.id(), meta.sourceId()); } - public void consumeHTML(String id, String content) { - server.sendServerEvent(SSEType.WRITE, content); + public void consumeHTML(MetaInformation meta, String content) { + server.sendServerEvent(SSEType.WRITE, content, meta.id(), meta.sourceId()); } - public void consumeJS(String id, String content) { - server.sendServerEvent(SSEType.SCRIPT, content); + public void consumeJS(MetaInformation meta, String content) { + server.sendServerEvent(SSEType.SCRIPT, content, meta.id(), meta.sourceId()); } - public void consumeJSCall(String id, String content) { - server.sendServerEvent(SSEType.CALL, content); + public void consumeJSCall(MetaInformation meta, String content) { + server.sendServerEvent(SSEType.CALL, content, meta.id(), meta.sourceId()); } - public void consumeMarkdown(String id, String content) { - consumeHTML("container" + id, ""); + public void consumeMarkdown(MetaInformation meta, String content) { + consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id()), ""); // Using `preformatted` is a hack to get a Java String into the Browser without interpretation - consumeJSCall("call" + id, "var scriptElement = document.getElementById('" + id + "');" + consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id()), "var scriptElement = document.getElementById('" + meta.id() + "');" + """ var divElement = document.createElement('div'); @@ -44,12 +45,12 @@ public void consumeMarkdown(String id, String content) { ); } - public void consumeDot(String id, String content) { + public void consumeDot(MetaInformation meta, String content) { GraphSpec specs = GraphSpec.fromContent(content); - consumeHTML("container" + id, "
"); - consumeJS("script" + id, "clerk.dot" + id + " = new Dot(document.getElementById('dotContainer" + id + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); - consumeJSCall("call" + id, "clerk.dot" + id + ".draw(\"" + specs.dot() + "\")"); + consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id()), "
"); + consumeJS(new MetaInformation(meta.sourceId(), "script" + meta.id()), "clerk.dot" + meta.id() + " = new Dot(document.getElementById('dotContainer" + meta.id() + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); + consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id()), "clerk.dot" + meta.id() + ".draw(\"" + specs.dot() + "\")"); } } diff --git a/src/main/java/lvp/skills/parser/ConfigParser.java b/src/main/java/lvp/skills/parser/ConfigParser.java index 1a63da4..52405f9 100644 --- a/src/main/java/lvp/skills/parser/ConfigParser.java +++ b/src/main/java/lvp/skills/parser/ConfigParser.java @@ -1,9 +1,11 @@ package lvp.skills.parser; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; @@ -12,7 +14,11 @@ import lvp.skills.logging.Logger; public class ConfigParser { - public record Source(Path path, String cmd) {} + public record Source(Path path, String cmd) { + public String id() { + return Base64.getEncoder().encodeToString(path().toString().getBytes(StandardCharsets.UTF_8)); + } + } private static final Pattern OBJECT_PATTERN = Pattern.compile( "\\{\\s*\"path\"\\s*:\\s*\"(.*?)\"\\s*,\\s*\"cmd\"\\s*:\\s*\"(.*?)\"\\s*\\},?" From 3001604a385f5852551c0b9e27b18a8a9d4a139c Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 18:18:28 +0200 Subject: [PATCH 26/50] subview system and css events --- demo/sub1/demo.java | 1 + demo/sub2/demo.java | 8 ++ src/main/java/lvp/FileWatcher.java | 2 +- src/main/java/lvp/Main.java | 2 +- src/main/java/lvp/Processor.java | 9 +- src/main/java/lvp/SSEType.java | 2 +- src/main/java/lvp/Server.java | 12 +-- .../java/lvp/commands/targets/Targets.java | 12 ++- src/main/java/lvp/skills/ParsingTools.java | 1 + .../java/lvp/skills/parser/ConfigParser.java | 2 +- src/main/resources/web/clerk.css | 18 ++++ src/main/resources/web/script.js | 101 ++++++++++++------ 12 files changed, 123 insertions(+), 47 deletions(-) diff --git a/demo/sub1/demo.java b/demo/sub1/demo.java index 3751f7d..16cc54f 100644 --- a/demo/sub1/demo.java +++ b/demo/sub1/demo.java @@ -2,5 +2,6 @@ void main() { println(""" Clear: ~ Markdown: # Demo Sub 1 + Markdown: This is a demo submodule. """); } \ No newline at end of file diff --git a/demo/sub2/demo.java b/demo/sub2/demo.java index 3cd240c..7281307 100644 --- a/demo/sub2/demo.java +++ b/demo/sub2/demo.java @@ -1,5 +1,13 @@ void main() { println(""" + Markdown: # Demo Sub2 + Dot: + digraph G { + a -> b; + b -> c; + c -> a; + } + ~~~ """); } \ No newline at end of file diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 3cc0f50..60460ff 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -138,7 +138,7 @@ public void stop() { } private void run(Source source) { - processor.init(); + processor.init(source.id()); try { boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); Logger.logInfo("Running: " + source.cmd() + " " + source.path()); diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index cace634..1287586 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -31,7 +31,7 @@ public static void main(String[] args) { } try { - Server server = new Server(Math.abs(cfg.port()), cfg.logLevel().equals(LogLevel.Debug)); + Server server = new Server(Math.abs(cfg.port())); Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); Processor processor = new Processor(server); FileWatcher watcher = new FileWatcher(cfg.sources(), cfg.watchFilter(), cfg.sourceOnly(), processor); diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index a61ade9..892a7ef 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -49,7 +49,9 @@ public Processor(Server server) { "Dot", targetProcessor::consumeDot, "Html", targetProcessor::consumeHTML, "JavaScript", targetProcessor::consumeJS, - "JavaScriptCall", targetProcessor::consumeJSCall, + "JavaScriptCall", targetProcessor::consumeJSCall, + "Css", targetProcessor::consumeCss, + "SubViewStyle", targetProcessor::consumeSubViewStyle, "Clear", targetProcessor::consumeClear); } @@ -150,8 +152,9 @@ String processRegister(Register register) { return null; } - void init() { - server.events.clear(); + void init(String sourceId) { + server.clearEvents(sourceId); + server.sendServerEvent(SSEType.CLEAR, "", "", sourceId); Text.clear(); } diff --git a/src/main/java/lvp/SSEType.java b/src/main/java/lvp/SSEType.java index ccba6e3..8c33d62 100644 --- a/src/main/java/lvp/SSEType.java +++ b/src/main/java/lvp/SSEType.java @@ -1,3 +1,3 @@ package lvp; -public enum SSEType { WRITE, CALL, SCRIPT, LOAD, CLEAR, DEBUG, LOG; } \ No newline at end of file +public enum SSEType { WRITE, CALL, SCRIPT, CLEAR, CSS, LOG; } \ No newline at end of file diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index af53b44..c7c2b0b 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -16,7 +16,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; -import com.sun.jdi.event.Event; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; @@ -38,15 +37,12 @@ private record EventMessage(SSEType type, String data, String id, String sourceI static void setDefaultPort(int port) { defaultPort = port != 0 ? Math.abs(port) : 50_001; } static int getDefaultPort() { return defaultPort; } - public final List webClients = new CopyOnWriteArrayList<>(); // thread-safe variant of ArrayList; + public final List webClients = new CopyOnWriteArrayList<>(); List events = new CopyOnWriteArrayList<>(); Map waitingProcesses = new ConcurrentHashMap<>(); - boolean isVerbose = false; - - public Server(int port, boolean isVerbose) throws IOException { + public Server(int port) throws IOException { this.port = port; - this.isVerbose = isVerbose; httpServer = HttpServer.create(new InetSocketAddress("localhost", port), 0); System.out.println("Open http://localhost:" + port + " in your browser"); @@ -247,6 +243,10 @@ private String readRequestBody(HttpExchange exchange) throws IOException { return null; } + public void clearEvents(String sourceId) { + events.removeIf(event -> event.sourceId().equals(sourceId)); + } + public void stop() { Logger.logInfo("Closing Server on port '" + port + "'"); for (HttpExchange connection : webClients) { diff --git a/src/main/java/lvp/commands/targets/Targets.java b/src/main/java/lvp/commands/targets/Targets.java index 0ae8aa6..ae6b837 100644 --- a/src/main/java/lvp/commands/targets/Targets.java +++ b/src/main/java/lvp/commands/targets/Targets.java @@ -30,6 +30,14 @@ public void consumeJSCall(MetaInformation meta, String content) { server.sendServerEvent(SSEType.CALL, content, meta.id(), meta.sourceId()); } + public void consumeCss(MetaInformation meta, String content) { + server.sendServerEvent(SSEType.CSS, content, meta.id(), meta.sourceId()); + } + + public void consumeSubViewStyle(MetaInformation meta, String content) { + consumeCss(meta, "#subViewContainer-" + meta.sourceId() + " { " + content + " }"); + } + public void consumeMarkdown(MetaInformation meta, String content) { consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id()), ""); // Using `preformatted` is a hack to get a Java String into the Browser without interpretation @@ -49,8 +57,8 @@ public void consumeDot(MetaInformation meta, String content) { GraphSpec specs = GraphSpec.fromContent(content); consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id()), "
"); - consumeJS(new MetaInformation(meta.sourceId(), "script" + meta.id()), "clerk.dot" + meta.id() + " = new Dot(document.getElementById('dotContainer" + meta.id() + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); - consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id()), "clerk.dot" + meta.id() + ".draw(\"" + specs.dot() + "\")"); + consumeJS(new MetaInformation(meta.sourceId(), "script" + meta.id()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + " = new Dot(document.getElementById('dotContainer" + meta.id() + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); + consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + ".draw(\"" + specs.dot() + "\")"); } } diff --git a/src/main/java/lvp/skills/ParsingTools.java b/src/main/java/lvp/skills/ParsingTools.java index bbf3076..bcd6e34 100644 --- a/src/main/java/lvp/skills/ParsingTools.java +++ b/src/main/java/lvp/skills/ParsingTools.java @@ -2,6 +2,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.Base64; import java.util.Optional; import java.util.OptionalInt; diff --git a/src/main/java/lvp/skills/parser/ConfigParser.java b/src/main/java/lvp/skills/parser/ConfigParser.java index 52405f9..de496ae 100644 --- a/src/main/java/lvp/skills/parser/ConfigParser.java +++ b/src/main/java/lvp/skills/parser/ConfigParser.java @@ -16,7 +16,7 @@ public class ConfigParser { public record Source(Path path, String cmd) { public String id() { - return Base64.getEncoder().encodeToString(path().toString().getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(path().toString().getBytes(StandardCharsets.UTF_8)); } } diff --git a/src/main/resources/web/clerk.css b/src/main/resources/web/clerk.css index f9aef34..9be33b9 100644 --- a/src/main/resources/web/clerk.css +++ b/src/main/resources/web/clerk.css @@ -116,4 +116,22 @@ th { display: flex; flex-direction: column; line-height: 1; +} + +#events { + display: flex; + flex-direction: column; + gap: 5rem; +} + +.section { + display: flex; + flex-direction: column; +} + +.section-marker { + font-size: 0.8rem; + margin-left: auto; + color: #333; + opacity: 0.6; } \ No newline at end of file diff --git a/src/main/resources/web/script.js b/src/main/resources/web/script.js index ff80ad0..ad63223 100644 --- a/src/main/resources/web/script.js +++ b/src/main/resources/web/script.js @@ -23,18 +23,69 @@ function errorLog(message) { .catch(console.error); } -function setUp() { +function clearErrorLog() { + const errors = document.getElementById("errors"); + errors.parentNode.style.display = "none"; + while (errors.firstChild) { + errors.removeChild(errors.firstChild); + } +} + +function clear(sourceId, global) { + const element = !global ? document.getElementById(sourceId) : document.getElementById("events"); + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + const styleElements = document.querySelectorAll(global ? 'style' : `style.${sourceId}`); + styleElements.forEach(el => el.parentNode.removeChild(el)); + const scriptElements = document.body.querySelectorAll(global ? 'script' : `script.${sourceId}`); + scriptElements.forEach(el => el.parentNode.removeChild(el)); + + + if (!global) { + for (const prop of Object.getOwnPropertyNames(clerk[sourceId])) { + delete clerk[sourceId][prop]; + } + } else { + for (const prop of Object.getOwnPropertyNames(clerk)) { + delete clerk[prop]; + } + } + +} + +function splitEventMessage(message) { + const parts = message.split(':'); + if (parts.length <= 4) return parts; + + const firstThree = parts.slice(0, 3); + const rest = parts.slice(3).join(':'); + return [...firstThree, rest]; +} +function setUp() { if (window.EventSource) { const source = new EventSource(`/events`); source.onmessage = function (event) { - const splitPos = event.data.indexOf(":"); - const action = event.data.slice(0, splitPos); - const base64Data = event.data.slice(splitPos + 1); + const [action, sourceId, id, base64Data] = splitEventMessage(event.data); const data = new TextDecoder("utf-8").decode(Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))); - debugLog(`Action: ${action}\nData: ${data}`); + debugLog(`Action: ${action}\nSourceId: ${sourceId}\nId: ${id}\nData: ${data}`); + + let subView = document.getElementById(sourceId); + if (!subView) { + const subViewContainer = document.createElement("div"); + subViewContainer.innerHTML = `${new TextDecoder("utf-8").decode(Uint8Array.from(atob(sourceId), c => c.charCodeAt(0)))}`; + subViewContainer.classList.add("section"); + subViewContainer.id = `subViewContainer-${sourceId}`; + subView = document.createElement("div"); + subView.id = sourceId; + subViewContainer.appendChild(subView); + clerk[sourceId] = {}; + document.getElementById("events").appendChild(subViewContainer); + } switch (action) { case "CALL": { @@ -44,45 +95,31 @@ function setUp() { case "SCRIPT": { const newElement = document.createElement("script"); newElement.innerHTML = data; + newElement.id = id; + newElement.classList.add(sourceId); document.body.appendChild(newElement); break; } case "WRITE": { const newElement = document.createElement("div"); newElement.innerHTML = data; - document.getElementById("events").appendChild(newElement); + newElement.id = id; + subView.appendChild(newElement); + break; + } + case "CSS": { + const newElement = document.createElement("style"); + newElement.innerHTML = data; + newElement.id = id; + newElement.classList.add(sourceId); + document.head.appendChild(newElement); break; } case "CLEAR": { scrollPosition = window.scrollY; - const element = document.getElementById("events"); - while (element.firstChild) { - element.removeChild(element.firstChild); - } - - const errors = document.getElementById("errors"); - errors.parentNode.style.display = "none"; - while (errors.firstChild) { - errors.removeChild(errors.firstChild); - } - - const toRemove = []; - for (const node of document.body.children) { - if (node.classList == null || !node.classList.contains("persistent")) { - toRemove.push(node); - } - } - toRemove.forEach(x => document.body.removeChild(x)); - - for (const prop of Object.getOwnPropertyNames(clerk)) { - delete clerk[prop]; - } - + clear(sourceId, id === "-1" || id === "all"); break; } - case "DEBUG": - debug = true; - break; case "LOG": { const newElement = document.createElement("div"); newElement.innerText = data; From 7726e3a4bf9a7de5da5dff071b8ba7bcef620462 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 18:53:07 +0200 Subject: [PATCH 27/50] fixed read --- src/main/java/lvp/Processor.java | 4 ++-- src/main/java/lvp/Server.java | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 892a7ef..e6df9b8 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -112,9 +112,9 @@ String processRead(Read read, Process process, String sourceId) { String button = HTMLElements.button("button" + read.id(), "Send", TextUtils.fillOut(""" (()=>{ const input = document.getElementById("input${0}"); - fetch("read", { method: "post", body: "${0}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); + fetch("read", { method: "post", body: "${1}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); })() - """, sourceId)); + """, read.id(), sourceId)); targetProcessor.consumeHTML(new MetaInformation(sourceId, read.id()), inputField + button); return null; } diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index c7c2b0b..c8e88dc 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -98,6 +98,7 @@ private void handleRead(HttpExchange exchange) throws IOException { } try { stream.write(Base64.getDecoder().decode(parts[1])); + stream.flush(); } catch (IOException e) { Logger.logError("Error while writing stream for: " + parts[0], e); } finally { @@ -245,6 +246,10 @@ private String readRequestBody(HttpExchange exchange) throws IOException { public void clearEvents(String sourceId) { events.removeIf(event -> event.sourceId().equals(sourceId)); + if (waitingProcesses.containsKey(sourceId)) { + waitingProcesses.get(sourceId).destroyForcibly(); + waitingProcesses.remove(sourceId); + } } public void stop() { From 1508b5abeeaede19c910116ca659e51c0a8e8afc Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 18:53:31 +0200 Subject: [PATCH 28/50] limit watch dirs to source dirs in sourceOnly mode --- src/main/java/lvp/FileWatcher.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 60460ff..b5ec3a9 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -52,6 +52,7 @@ public FileWatcher(List sources, Optional watchFilter, boolean s .map(Path::normalize) .flatMap(root -> { try { + if (sourceOnly) return Stream.of(root); return Files.find(root, Integer.MAX_VALUE, (_, attrs) -> attrs.isDirectory()); } catch (IOException e) { From 7c50fe733a24d263e9195976d9089bad3ce01bc0 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 19:06:01 +0200 Subject: [PATCH 29/50] refactor: rename Read to Scan in Processor, Server, and InstructionParser --- src/main/java/lvp/Processor.java | 16 ++++++++-------- src/main/java/lvp/Server.java | 4 ++-- .../lvp/skills/parser/InstructionParser.java | 17 +++++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index e6df9b8..f8946de 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -26,7 +26,7 @@ import lvp.skills.parser.InstructionParser.Command; import lvp.skills.parser.InstructionParser.CommandRef; import lvp.skills.parser.InstructionParser.Pipe; -import lvp.skills.parser.InstructionParser.Read; +import lvp.skills.parser.InstructionParser.Scan; import lvp.skills.parser.InstructionParser.Register; public class Processor { Server server; @@ -62,7 +62,7 @@ void process(Process process, String sourceId) { switch (curr) { case Command cmd -> processCommands(cmd, sourceId); case Pipe pipe -> processPipe(pipe, prev, sourceId); - case Read read -> processRead(read, process, sourceId); + case Scan scan -> processScan(scan, process, sourceId); case Register register -> processRegister(register); default -> null; })).forEachOrdered(_->{}); @@ -106,16 +106,16 @@ else if (services.containsKey(ref.name())) { return current; } - String processRead(Read read, Process process, String sourceId) { + String processScan(Scan scan, Process process, String sourceId) { server.waitingProcesses.put(sourceId, process); - String inputField = HTMLElements.input("input" + read.id()); - String button = HTMLElements.button("button" + read.id(), "Send", TextUtils.fillOut(""" + String inputField = HTMLElements.input("input" + scan.id()); + String button = HTMLElements.button("button" + scan.id(), "Send", TextUtils.fillOut(""" (()=>{ const input = document.getElementById("input${0}"); - fetch("read", { method: "post", body: "${1}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); + fetch("scan", { method: "post", body: "${1}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); })() - """, read.id(), sourceId)); - targetProcessor.consumeHTML(new MetaInformation(sourceId, read.id()), inputField + button); + """, scan.id(), sourceId)); + targetProcessor.consumeHTML(new MetaInformation(sourceId, scan.id()), inputField + button); return null; } diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index c8e88dc..7123c2f 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -49,7 +49,7 @@ public Server(int port) throws IOException { httpServer.createContext("/log", this::handleLog); httpServer.createContext("/interact", this::handleInteract); - httpServer.createContext("/read", this::handleRead); + httpServer.createContext("/scan", this::handleScan); httpServer.createContext("/events", this::handleEvents); httpServer.createContext("/", this::handleRoot); @@ -75,7 +75,7 @@ private void handleLog(HttpExchange exchange) throws IOException { Logger.log(LogLevel.fromString(parts[0]), parts[1]); } - private void handleRead(HttpExchange exchange) throws IOException { + private void handleScan(HttpExchange exchange) throws IOException { String message = readRequestBody(exchange); if (message == null) return; String[] parts = message.split(":", 2); diff --git a/src/main/java/lvp/skills/parser/InstructionParser.java b/src/main/java/lvp/skills/parser/InstructionParser.java index c0d30f7..e7359a6 100644 --- a/src/main/java/lvp/skills/parser/InstructionParser.java +++ b/src/main/java/lvp/skills/parser/InstructionParser.java @@ -17,11 +17,11 @@ public class InstructionParser { // ---- Instruction Types ---- - public sealed interface Instruction permits Command, Register, Read, Pipe {} + public sealed interface Instruction permits Command, Register, Scan, Pipe {} public record Command(String name, String id, String content) implements Instruction {} public record Register(String name, String call, boolean skipId) implements Instruction {} - public record Read(String id) implements Instruction {} + public record Scan(String id) implements Instruction {} public record Pipe(List commands) implements Instruction {} public record CommandRef(String name, String id) {} @@ -29,7 +29,8 @@ public record CommandRef(String name, String id) {} // ---- Patterns ---- private static final Pattern SINGLE_LINE_COMMAND = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*(.+)$"); private static final Pattern BLOCK_START = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*$"); - private static final Pattern READ = Pattern.compile("^Read(?:\\{([^}]+)\\})?:\\s*$"); + private static final Pattern SINGLE_LINE_COMMAND_CONTENTLESS = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?\\s*$"); + private static final Pattern SCAN = Pattern.compile("^Scan(?:\\{([^}]+)\\})?:\\s*$"); private static final Pattern REGISTER = Pattern.compile("^Register(?:\\{([^}]+)\\})?:\\s+(\\w+)\\s+(.+)$"); private static final Pattern PIPE_LINE = Pattern.compile("^\\s*\\|(.+)$"); private static final Pattern PIPE_ENTRY = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?$"); @@ -82,9 +83,9 @@ private static void handleLine(BlockState state, String line, Downstream return true; } - private static boolean tryRead(String line, Downstream out) { - Matcher matcher = READ.matcher(line); + private static boolean tryScan(String line, Downstream out) { + Matcher matcher = SCAN.matcher(line); if (!matcher.matches()) return false; String id = matcher.group(1) == null ? IdGen.generateID(10) : matcher.group(1); Logger.logDebug("Parsed Read" + formatFlag(id)); - out.push(new Read(id)); + out.push(new Scan(id)); return true; } From 27409b6ff3508c141c363434af8d54028831ad01 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 19:06:57 +0200 Subject: [PATCH 30/50] fix: improve command parsing to handle contentless single-line commands --- src/main/java/lvp/skills/parser/InstructionParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/lvp/skills/parser/InstructionParser.java b/src/main/java/lvp/skills/parser/InstructionParser.java index e7359a6..9ecb3cd 100644 --- a/src/main/java/lvp/skills/parser/InstructionParser.java +++ b/src/main/java/lvp/skills/parser/InstructionParser.java @@ -141,11 +141,11 @@ private static boolean tryScan(String line, Downstream out } private static boolean trySingleCommand(String line, Downstream out) { - Matcher matcher = SINGLE_LINE_COMMAND.matcher(line); + Matcher matcher = SINGLE_LINE_COMMAND.matcher(line).matches() ? SINGLE_LINE_COMMAND.matcher(line) : SINGLE_LINE_COMMAND_CONTENTLESS.matcher(line); if (!matcher.matches()) return false; String id = matcher.group(2) == null ? IdGen.generateID(10) : matcher.group(2); Logger.logDebug("Parsed single-line command: " + matcher.group(1) + formatFlag(id)); - out.push(new Command(matcher.group(1), id, matcher.group(3))); + out.push(new Command(matcher.group(1), id, matcher.groupCount() == 3 ? matcher.group(3) : "")); return true; } From e889f542ad230fec025259fca871a8d1c1211a65 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 21:17:07 +0200 Subject: [PATCH 31/50] changed text command behavior --- newdemo.java | 263 ++++++++++-------- src/main/java/lvp/Processor.java | 19 +- src/main/java/lvp/Server.java | 1 + .../lvp/commands/services/Interaction.java | 20 +- src/main/java/lvp/commands/services/Test.java | 7 +- src/main/java/lvp/commands/services/Text.java | 19 +- .../java/lvp/commands/services/Turtle.java | 5 +- .../java/lvp/commands/targets/Targets.java | 12 +- src/main/java/lvp/skills/TextUtils.java | 2 +- src/main/resources/web/script.js | 2 +- 10 files changed, 192 insertions(+), 158 deletions(-) diff --git a/newdemo.java b/newdemo.java index c51304c..22fde93 100644 --- a/newdemo.java +++ b/newdemo.java @@ -1,136 +1,157 @@ import static java.io.IO.println; -import java.util.Scanner; - +// ex1 void main() { - println("Clear:~"); - println("Markdown: # Hello World!"); - println(""" - Markdown: - ## Hello World! - This is a simple example of a markdown block. + println(""" + Markdown: # Text Demo + Text: newdemo.java;// ex1 + | Codeblock | Text{example} + Text{title}: Codeblocks + Text{template}: + ## ${0} + ```java + ${1} + ``` + ~~~ + | Text{title} | Text{example} | Markdown + Text{template}: Hello World! + | Markdown + Text{template} + | Markdown - ~~~ - """); - println(""" - Text{0}: - # Text und Pipes - Der ${0} Command ${1} es ${0} Templates zu definieren. - In diesen Templates können Platzhalter genutzt werden, die - später durch Pipes mit Content befüllt werden. - Dieser ${0} kann zum Beispiel in die Markdown View "gepiped" werden. - ~~~ - | Markdown - """); + """); +} +// ex1 +// void main() { +// println("Clear:~"); +// println("Markdown: # Hello World!"); +// println(""" +// Markdown: +// ## Hello World! +// This is a simple example of a markdown block. - println(""" - Text: Text - | Text{0} | Markdown - """); +// ~~~ +// """); +// println(""" +// Text{0}: +// # Text und Pipes +// Der ${0} Command ${1} es ${0} Templates zu definieren. +// In diesen Templates können Platzhalter genutzt werden, die +// später durch Pipes mit Content befüllt werden. +// Dieser ${0} kann zum Beispiel in die Markdown View "gepiped" werden. +// ~~~ +// | Markdown +// """); - println(""" - Text: erlaubt - | Text{0} | Markdown - """); -// ex1 - println(""" - Text{1}: - # Codeblocks - This is a codeblock example: - ```java - ${0} - ``` - ~~~ - Codeblock: newdemo.java;// ex1 - | Text{1} | Markdown - """); -// ex1 +// println(""" +// Text: Text +// | Text{0} | Markdown +// """); + +// println(""" +// Text: erlaubt +// | Text{0} | Markdown +// """); - println(""" - Text{2}: - init 0 200 0 25 50 0 0 - """ - + - "color 37 255 37 1" // turtle color - + - """ +// // ex1 +// println(""" +// Text{1}: +// # Codeblocks +// This is a codeblock example: +// ```java +// ${0} +// ``` +// ~~~ +// Codeblock: newdemo.java;// ex1 +// | Text{1} | Markdown +// """); +// // ex1 + +// println(""" +// Text{2}: +// init 0 200 0 25 50 0 0 +// """ +// + +// "color 37 255 37 1" // turtle color +// + +// """ - forward 25 - right 60 - backward 25 - right 60 - forward 25 - timeline - ~~~ - | Turtle | Html - Text{2}: ~ - | Markdown - Text{3}: - ``` - ${0} - ``` - ~~~ - Text{2}: - - | Text{3} | Markdown - """); +// forward 25 +// right 60 +// backward 25 +// right 60 +// forward 25 +// timeline +// ~~~ +// | Turtle | Html +// Text{2}: ~ +// | Markdown +// Text{3}: +// ``` +// ${0} +// ``` +// ~~~ +// Text{2}: - +// | Text{3} | Markdown +// """); - println(""" - Button: - Text: Green - width: 200 - height: 50 - path: newdemo.java - label: "// turtle color" - replacement: "color 37 255 37 1" - ~~~ - | Html - Button: - Text: Red - width: 200 - height: 50 - path: newdemo.java - label: "// turtle color" - replacement: "color 255 37 37 1" - ~~~ - | Html - """); +// println(""" +// Button: +// Text: Green +// width: 200 +// height: 50 +// path: newdemo.java +// label: "// turtle color" +// replacement: "color 37 255 37 1" +// ~~~ +// | Html +// Button: +// Text: Red +// width: 200 +// height: 50 +// path: newdemo.java +// label: "// turtle color" +// replacement: "color 255 37 37 1" +// ~~~ +// | Html +// """); - int n = 55; // input - boolean b = true; // bool - println(""" - Input: - path: newdemo.java - label: "// input" - placeholder: Enter a number - template: int n = $; - type: text - ~~~ - | Html - Checkbox: - path: newdemo.java - label: "// bool" - template: boolean b = $; - """ - + - "checked:" + b - + - """ +// int n = 55; // input +// boolean b = true; // bool +// println(""" +// Input: +// path: newdemo.java +// label: "// input" +// placeholder: Enter a number +// template: int n = $; +// type: text +// ~~~ +// | Html +// Checkbox: +// path: newdemo.java +// label: "// bool" +// template: boolean b = $; +// """ +// + +// "checked:" + b +// + +// """ - ~~~ - | Html - """); +// ~~~ +// | Html +// """); - println(""" - Dot: - width: 1000 - height: 600 - digraph G { - A -> B; - B -> C; - } - ~~~ - """); -} +// println(""" +// Dot: +// width: 1000 +// height: 600 +// digraph G { +// A -> B; +// B -> C; +// } +// ~~~ +// """); +// } diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index f8946de..e072ff7 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -18,7 +18,6 @@ import lvp.commands.services.Turtle; import lvp.commands.services.Interaction; import lvp.commands.targets.Targets; -import lvp.commands.targets.Targets.MetaInformation; import lvp.skills.HTMLElements; import lvp.skills.TextUtils; import lvp.skills.logging.Logger; @@ -29,10 +28,12 @@ import lvp.skills.parser.InstructionParser.Scan; import lvp.skills.parser.InstructionParser.Register; public class Processor { + public record MetaInformation(String sourceId, String id, boolean standalone) {} + Server server; Targets targetProcessor; Map> targets; - Map> services = new HashMap<>(Map.of( + Map> services = new HashMap<>(Map.of( "Text", Text::of, "Codeblock", Text::codeblock, "Turtle", Turtle::of, @@ -77,10 +78,10 @@ String processCommands(Command command, String sourceId) { Logger.logDebug("Command: " + command.name() + "{" + command.id() + "}, " + command.content()); if (targets.containsKey(command.name())) { - targets.get(command.name()).accept(new MetaInformation(sourceId, command.id()), command.content()); + targets.get(command.name()).accept(new MetaInformation(sourceId, command.id(), true), command.content()); } else if (services.containsKey(command.name())) { - return services.get(command.name()).apply(command.id(), command.content()); + return services.get(command.name()).apply(new MetaInformation(sourceId, command.id(), true), command.content()); } else { Logger.logError("Command not found: " + command.name()); } @@ -94,11 +95,11 @@ String processPipe(Pipe pipe, String input, String sourceId) { for (CommandRef ref : pipe.commands()) { Logger.logDebug("Command: " + ref.name() + "{" + ref.id() + "}, " + current); if (targets.containsKey(ref.name())) { - targets.get(ref.name()).accept(new MetaInformation(sourceId, ref.id()), current); + targets.get(ref.name()).accept(new MetaInformation(sourceId, ref.id(), false), current); return null; } else if (services.containsKey(ref.name())) { - current = services.get(ref.name()).apply(ref.id(), current); + current = services.get(ref.name()).apply(new MetaInformation(sourceId, ref.id(), false), current); } else { Logger.logError("Command not found: " + ref.name()); } @@ -115,12 +116,12 @@ String processScan(Scan scan, Process process, String sourceId) { fetch("scan", { method: "post", body: "${1}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); })() """, scan.id(), sourceId)); - targetProcessor.consumeHTML(new MetaInformation(sourceId, scan.id()), inputField + button); + targetProcessor.consumeHTML(new MetaInformation(sourceId, scan.id(), true), inputField + button); return null; } String processRegister(Register register) { - services.put(register.name(), (id, content) -> { + services.put(register.name(), (meta, content) -> { boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); String out = null; try { @@ -131,7 +132,7 @@ String processRegister(Register register) { try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { - if (!register.skipId()) writer.write(id + "\n"); + if (!register.skipId()) writer.write(meta.id() + "\n"); writer.write(content + "\n"); writer.flush(); } diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/Server.java index 7123c2f..09e22f1 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/Server.java @@ -152,6 +152,7 @@ private void handleEvents(HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(200, 0); webClients.add(exchange); + sendMessageToClient(exchange, new EventMessage(SSEType.CLEAR, "", "all", "server")); if (!events.isEmpty()) { events.forEach(event -> sendMessageToClient(exchange, event)); } diff --git a/src/main/java/lvp/commands/services/Interaction.java b/src/main/java/lvp/commands/services/Interaction.java index a0be508..6f2bef9 100644 --- a/src/main/java/lvp/commands/services/Interaction.java +++ b/src/main/java/lvp/commands/services/Interaction.java @@ -1,7 +1,6 @@ package lvp.commands.services; import java.nio.charset.StandardCharsets; -import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Base64; import java.util.Map; @@ -9,6 +8,7 @@ import java.util.OptionalInt; import java.util.stream.Collectors; +import lvp.Processor.MetaInformation; import lvp.skills.HTMLElements; import lvp.skills.ParsingTools; import lvp.skills.TextUtils; @@ -16,7 +16,7 @@ public class Interaction { private Interaction() {} - public static String button(String id, String content) { + public static String button(MetaInformation meta, String content) { Map fields = content.lines() .filter(line -> !line.isBlank()) .map(line -> line.split(":", 2)) @@ -48,11 +48,11 @@ public static String button(String id, String content) { String func = eventFunction(path.get(), ParsingTools.stripQuotes(label), replacement); return width.isPresent() || height.isPresent() - ? HTMLElements.button(id, text, width.orElse(height.getAsInt()), height.orElse(width.getAsInt()), func) - : HTMLElements.button(id, text, func); + ? HTMLElements.button(meta.id(), text, width.orElse(height.getAsInt()), height.orElse(width.getAsInt()), func) + : HTMLElements.button(meta.id(), text, func); } - public static String input(String id, String content) { + public static String input(MetaInformation meta, String content) { Map fields = content.lines() .filter(line -> !line.isBlank()) .map(line -> line.split(":", 2)) @@ -78,21 +78,21 @@ public static String input(String id, String content) { } Logger.logDebug("Parsed input with path=" + path + ", label=" + label + ", type=" + type); - String inputElement = HTMLElements.input("input" + id, placeholder, type, ParsingTools.stripQuotes(label).replaceFirst("//", "").strip()); - String button = HTMLElements.button("button" + id, "Send", TextUtils.fillOut(""" + String inputElement = HTMLElements.input("input" + meta.id(), placeholder, type, ParsingTools.stripQuotes(label).replaceFirst("//", "").strip()); + String button = HTMLElements.button("button" + meta.id(), "Send", TextUtils.fillOut(""" (() => { const input = document.getElementById("input${0}"); const result = `${3}`.replace("$", input.value); fetch("interact", { method: "post", body: "${1}:${2}:single:" + btoa(String.fromCharCode(...new TextEncoder().encode(result))) }).catch(console.error); })() - """, id, + """, meta.id(), Base64.getEncoder().encodeToString(path.get().normalize().toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8)), Base64.getEncoder().encodeToString(ParsingTools.stripQuotes(label).getBytes(StandardCharsets.UTF_8)), template)); return inputElement + button; } - public static String checkbox(String id, String content) { + public static String checkbox(MetaInformation meta, String content) { Map fields = content.lines() .filter(line -> !line.isBlank()) .map(line -> line.split(":", 2)) @@ -118,7 +118,7 @@ public static String checkbox(String id, String content) { boolean checked = Boolean.parseBoolean(fields.getOrDefault("checked", "false")); Logger.logDebug("Parsed checkbox with path=" + path + ", label=" + label + ", checked=" + checked); - return HTMLElements.checkbox(id, ParsingTools.stripQuotes(label).replaceFirst("//", "").strip(), checked, TextUtils.fillOut(""" + return HTMLElements.checkbox(meta.id(), ParsingTools.stripQuotes(label).replaceFirst("//", "").strip(), checked, TextUtils.fillOut(""" (() => { const result = `${2}`.replace("$", this.checked); fetch("interact", { method: "post", body: "${0}:${1}:single:" + btoa(String.fromCharCode(...new TextEncoder().encode(result))) }).catch(console.error); diff --git a/src/main/java/lvp/commands/services/Test.java b/src/main/java/lvp/commands/services/Test.java index 45da73f..9b777d9 100644 --- a/src/main/java/lvp/commands/services/Test.java +++ b/src/main/java/lvp/commands/services/Test.java @@ -5,19 +5,18 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import lvp.Processor.MetaInformation; import lvp.skills.TextUtils; import lvp.skills.logging.Logger; //TODO: multiple actual and expect public class Test { private static final String JSHELL_PROMPT = "jshell>"; - public static String test(String id, String content) { + public static String test(MetaInformation meta, String content) { Map fields = content.lines() .filter(line -> !line.isBlank()) .map(line -> line.split(":", 2)) @@ -45,7 +44,7 @@ public static String test(String id, String content) { Actual: ${3} Expected: ${4} Status: ${5} - """, id, send, actual, actualParsed, expect, actualParsed.equals(expect) ? "Success" : "Failure"); + """, meta.id(), send, actual, actualParsed, expect, actualParsed.equals(expect) ? "Success" : "Failure"); } private static String executeJshell(String send) { diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index 0dd4f67..08e8742 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -2,6 +2,8 @@ import java.util.HashMap; import java.util.Map; + +import lvp.Processor.MetaInformation; import lvp.skills.TextUtils; import lvp.skills.logging.Logger; @@ -13,7 +15,7 @@ public static void clear() { templates.clear(); } - public static String codeblock(String id, String content) { + public static String codeblock(MetaInformation meta, String content) { String[] parts = content.split(";"); if (parts.length != 2) { Logger.logError("Invalid Codeblock Format."); @@ -22,8 +24,17 @@ public static String codeblock(String id, String content) { return TextUtils.codeBlock(parts[0].strip(), parts[1].strip()); } - public static String of(String id, String content) { - String newValue = templates.merge(id, content, TextUtils::linearFillOut); - return newValue == null ? content : newValue; + public static String of(MetaInformation meta, String content) { + String existing = templates.get(meta.id()); + if (existing == null || meta.standalone() && !content.isBlank()) { + templates.put(meta.id(), content); + return content; + } + + if (content.isBlank()) { + return existing; + } + + return TextUtils.linearFillOut(existing, content); } } diff --git a/src/main/java/lvp/commands/services/Turtle.java b/src/main/java/lvp/commands/services/Turtle.java index a96f869..4f670ee 100644 --- a/src/main/java/lvp/commands/services/Turtle.java +++ b/src/main/java/lvp/commands/services/Turtle.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Locale; +import lvp.Processor.MetaInformation; import lvp.skills.HTMLElements; import lvp.skills.TextUtils; import lvp.skills.parser.TurtleParser; @@ -29,8 +30,8 @@ public class Turtle { private boolean showTimeline = false; private final Deque stack = new ArrayDeque<>(); - public static String of(String id, String content) { - Turtle turtle = TurtleParser.parse(id, content); + public static String of(MetaInformation meta, String content) { + Turtle turtle = TurtleParser.parse(meta.id(), content); return turtle.toString(); } diff --git a/src/main/java/lvp/commands/targets/Targets.java b/src/main/java/lvp/commands/targets/Targets.java index ae6b837..d270a72 100644 --- a/src/main/java/lvp/commands/targets/Targets.java +++ b/src/main/java/lvp/commands/targets/Targets.java @@ -1,11 +1,11 @@ package lvp.commands.targets; +import lvp.Processor.MetaInformation; import lvp.SSEType; import lvp.Server; import lvp.commands.targets.dot.GraphSpec; public class Targets { - public record MetaInformation(String sourceId, String id) {} Server server; public static Targets of(Server server) { return new Targets(server); } @@ -39,10 +39,10 @@ public void consumeSubViewStyle(MetaInformation meta, String content) { } public void consumeMarkdown(MetaInformation meta, String content) { - consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id()), ""); + consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id(), meta.standalone()), ""); // Using `preformatted` is a hack to get a Java String into the Browser without interpretation - consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id()), "var scriptElement = document.getElementById('" + meta.id() + "');" + consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id(), meta.standalone()), "var scriptElement = document.getElementById('" + meta.id() + "');" + """ var divElement = document.createElement('div'); @@ -56,9 +56,9 @@ public void consumeMarkdown(MetaInformation meta, String content) { public void consumeDot(MetaInformation meta, String content) { GraphSpec specs = GraphSpec.fromContent(content); - consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id()), "
"); - consumeJS(new MetaInformation(meta.sourceId(), "script" + meta.id()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + " = new Dot(document.getElementById('dotContainer" + meta.id() + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); - consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + ".draw(\"" + specs.dot() + "\")"); + consumeHTML(new MetaInformation(meta.sourceId(), "container" + meta.id(), meta.standalone()), "
"); + consumeJS(new MetaInformation(meta.sourceId(), "script" + meta.id(), meta.standalone()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + " = new Dot(document.getElementById('dotContainer" + meta.id() + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); + consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id(), meta.standalone()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + ".draw(\"" + specs.dot() + "\")"); } } diff --git a/src/main/java/lvp/skills/TextUtils.java b/src/main/java/lvp/skills/TextUtils.java index 63e9a5d..36bd236 100644 --- a/src/main/java/lvp/skills/TextUtils.java +++ b/src/main/java/lvp/skills/TextUtils.java @@ -110,7 +110,7 @@ public static String fillOut(String template, Object... replacements) { return fillOut(m, template); } - public static String linearFillOut(String template, String replacement) { + public static String linearFillOut(String replacement, String template) { Pattern pattern = Pattern.compile("\\$\\{(.*?)\\}"); // `${}` Matcher matcher = pattern.matcher(template); StringBuffer result = new StringBuffer(); diff --git a/src/main/resources/web/script.js b/src/main/resources/web/script.js index ad63223..5d7d782 100644 --- a/src/main/resources/web/script.js +++ b/src/main/resources/web/script.js @@ -117,7 +117,7 @@ function setUp() { } case "CLEAR": { scrollPosition = window.scrollY; - clear(sourceId, id === "-1" || id === "all"); + clear(sourceId, id === "-1" || id === "all" || id === "global"); break; } case "LOG": { From 3043d0220df7e410849d76764490aba90a49ac37 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 21:22:36 +0200 Subject: [PATCH 32/50] fix: update instruction patterns to use square brackets for parameterization --- newdemo.java | 12 ++++++------ .../java/lvp/skills/parser/InstructionParser.java | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/newdemo.java b/newdemo.java index 22fde93..bdd0d68 100644 --- a/newdemo.java +++ b/newdemo.java @@ -5,18 +5,18 @@ void main() { println(""" Markdown: # Text Demo Text: newdemo.java;// ex1 - | Codeblock | Text{example} - Text{title}: Codeblocks - Text{template}: + | Codeblock | Text[example] + Text[title]: Codeblocks + Text[template]: ## ${0} ```java ${1} ``` ~~~ - | Text{title} | Text{example} | Markdown - Text{template}: Hello World! + | Text[title] | Text[example] | Markdown + Text[template]: Hello World! | Markdown - Text{template} + Text[template] | Markdown """); diff --git a/src/main/java/lvp/skills/parser/InstructionParser.java b/src/main/java/lvp/skills/parser/InstructionParser.java index 9ecb3cd..494afd0 100644 --- a/src/main/java/lvp/skills/parser/InstructionParser.java +++ b/src/main/java/lvp/skills/parser/InstructionParser.java @@ -27,13 +27,13 @@ public record Pipe(List commands) implements Instruction {} public record CommandRef(String name, String id) {} // ---- Patterns ---- - private static final Pattern SINGLE_LINE_COMMAND = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*(.+)$"); - private static final Pattern BLOCK_START = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?:\\s*$"); - private static final Pattern SINGLE_LINE_COMMAND_CONTENTLESS = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?\\s*$"); - private static final Pattern SCAN = Pattern.compile("^Scan(?:\\{([^}]+)\\})?:\\s*$"); - private static final Pattern REGISTER = Pattern.compile("^Register(?:\\{([^}]+)\\})?:\\s+(\\w+)\\s+(.+)$"); + private static final Pattern SINGLE_LINE_COMMAND = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?:\\s*(.+)$"); + private static final Pattern BLOCK_START = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?:\\s*$"); + private static final Pattern SINGLE_LINE_COMMAND_CONTENTLESS = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?\\s*$"); + private static final Pattern SCAN = Pattern.compile("^Scan(?:\\[([^}]+)\\])?:\\s*$"); + private static final Pattern REGISTER = Pattern.compile("^Register(?:\\[([^}]+)\\])?:\\s+(\\w+)\\s+(.+)$"); private static final Pattern PIPE_LINE = Pattern.compile("^\\s*\\|(.+)$"); - private static final Pattern PIPE_ENTRY = Pattern.compile("^(\\w+)(?:\\{([^}]+)\\})?$"); + private static final Pattern PIPE_ENTRY = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?$"); // ---- Block Parsing State ---- private static class BlockState { From b30e43b5d3547b619c650bcfb71325486d65b58f Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 22:35:49 +0200 Subject: [PATCH 33/50] server input --- src/main/java/lvp/FileWatcher.java | 3 +- src/main/java/lvp/Main.java | 58 ++++++++++++++++++++++++++++-- src/main/java/lvp/Processor.java | 16 +++++---- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index b5ec3a9..c061bb5 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -90,7 +90,8 @@ private void watchLoop() { key = watcher.take(); processWatchKeyEvents(key); } catch (ClosedWatchServiceException | InterruptedException e) { - Logger.logError("Watcher loop terminated due to exception: " + e.getMessage(), e); + if (isRunning) + Logger.logError("Watcher loop terminated due to exception: " + e.getMessage(), e); if (e instanceof InterruptedException) Thread.currentThread().interrupt(); break; } diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index 1287586..6993c0e 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -1,16 +1,22 @@ package lvp; +import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Optional; +import java.util.Scanner; import java.util.regex.Matcher; +import java.util.stream.Stream; import lvp.skills.logging.LogLevel; import lvp.skills.logging.Logger; import lvp.skills.parser.ConfigParser; +import lvp.skills.parser.InstructionParser; import lvp.skills.parser.PathParser; import lvp.skills.parser.ConfigParser.Source; @@ -29,12 +35,16 @@ public static void main(String[] args) { if (!isLatestRelease()) { System.out.println("Warning: You are not using the latest release of Live View Programming. Please visit https://github.com/denkspuren/LiveViewProgramming/releases"); } + + Server server = null; + FileWatcher watcher = null; + Processor processor = null; try { - Server server = new Server(Math.abs(cfg.port())); + server = new Server(Math.abs(cfg.port())); Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); - Processor processor = new Processor(server); - FileWatcher watcher = new FileWatcher(cfg.sources(), cfg.watchFilter(), cfg.sourceOnly(), processor); + processor = new Processor(server); + watcher = new FileWatcher(cfg.sources(), cfg.watchFilter(), cfg.sourceOnly(), processor); Runtime.getRuntime().addShutdownHook(new Thread(watcher::stop)); watcher.start(); } catch (IOException e) { @@ -42,6 +52,48 @@ public static void main(String[] args) { e.printStackTrace(); System.exit(1); } + + Scanner scanner = new Scanner(System.in); + while(true) { + String input = scanner.nextLine().strip(); + if (input.startsWith("/")) + handleServerCommands(input.substring(1).strip()); + else if (!input.isBlank() && !input.startsWith("Scan")) { + processor.process(Stream.of(input), Base64.getUrlEncoder().withoutPadding().encodeToString("stdin".getBytes(StandardCharsets.UTF_8)), null); + } else { + System.err.println("Error: Invalid command. Use '/help' for available commands."); + } + } + } + + private static void handleServerCommands(String command) { + String[] parts = command.split(" ", 2); + if (command.isBlank() || parts.length == 0) { + System.out.println("No command entered. Type '/help' for available commands."); + return; + } + + switch (parts[0].toLowerCase()) { + case "exit" -> { + System.out.println("Exiting Live View Programming..."); + System.exit(0); + } + case "log" -> { + if (parts.length < 2) { + System.out.println("Usage: /log "); + return; + } + LogLevel level = LogLevel.fromString(parts[1]); + if (level == null) { + System.out.println("Invalid log level."); + } else { + Logger.setLogLevel(level); + System.out.println("Log level set to: " + level); + } + } + case "help" -> System.out.println("Available commands: /exit, /help, /log"); + default -> System.out.println("Unknown command: " + command); + } } private static Config parseArgs(String[] args) { diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index e072ff7..21fc764 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -12,6 +12,7 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.Gatherers; +import java.util.stream.Stream; import lvp.commands.services.Text; import lvp.commands.services.Test; @@ -59,7 +60,15 @@ public Processor(Server server) { void process(Process process, String sourceId) { try(BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - InstructionParser.parse(reader.lines()).gather(Gatherers.fold(() -> "", (prev, curr) -> + process(reader.lines(), sourceId, process); + } + catch (Exception e) { + Logger.logError("Error reading process output: " + e.getMessage(), e); + } + } + + void process(Stream input, String sourceId, Process process) { + InstructionParser.parse(input).gather(Gatherers.fold(() -> "", (prev, curr) -> switch (curr) { case Command cmd -> processCommands(cmd, sourceId); case Pipe pipe -> processPipe(pipe, prev, sourceId); @@ -67,11 +76,6 @@ void process(Process process, String sourceId) { case Register register -> processRegister(register); default -> null; })).forEachOrdered(_->{}); - - } - catch (Exception e) { - Logger.logError("Error reading process output: " + e.getMessage(), e); - } } String processCommands(Command command, String sourceId) { From 9104abbb600a5b1035767b5617d3e66c245178d3 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Jun 2025 23:16:40 +0200 Subject: [PATCH 34/50] server input, error overlay and Text clear per source --- newdemo.java | 2 ++ src/main/java/lvp/Main.java | 4 ++-- src/main/java/lvp/Processor.java | 11 ++++++++-- src/main/java/lvp/commands/services/Text.java | 21 ++++++++++++------- .../java/lvp/commands/targets/Targets.java | 4 ++++ .../lvp/skills/parser/InstructionParser.java | 5 +++-- src/main/resources/web/script.js | 19 ++++++++--------- 7 files changed, 43 insertions(+), 23 deletions(-) diff --git a/newdemo.java b/newdemo.java index bdd0d68..c207f36 100644 --- a/newdemo.java +++ b/newdemo.java @@ -2,7 +2,9 @@ // ex1 void main() { + println(""" + Clear Markdown: # Text Demo Text: newdemo.java;// ex1 | Codeblock | Text[example] diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index 6993c0e..c1927e8 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -16,7 +16,6 @@ import lvp.skills.logging.LogLevel; import lvp.skills.logging.Logger; import lvp.skills.parser.ConfigParser; -import lvp.skills.parser.InstructionParser; import lvp.skills.parser.PathParser; import lvp.skills.parser.ConfigParser.Source; @@ -55,7 +54,8 @@ public static void main(String[] args) { Scanner scanner = new Scanner(System.in); while(true) { - String input = scanner.nextLine().strip(); + String input = null; + try { input = scanner.nextLine().strip(); } catch (Exception _) { break;} if (input.startsWith("/")) handleServerCommands(input.substring(1).strip()); else if (!input.isBlank() && !input.startsWith("Scan")) { diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 21fc764..b8856bc 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -27,6 +27,7 @@ import lvp.skills.parser.InstructionParser.CommandRef; import lvp.skills.parser.InstructionParser.Pipe; import lvp.skills.parser.InstructionParser.Scan; +import lvp.skills.parser.InstructionParser.Unknown; import lvp.skills.parser.InstructionParser.Register; public class Processor { public record MetaInformation(String sourceId, String id, boolean standalone) {} @@ -74,6 +75,7 @@ void process(Stream input, String sourceId, Process process) { case Pipe pipe -> processPipe(pipe, prev, sourceId); case Scan scan -> processScan(scan, process, sourceId); case Register register -> processRegister(register); + case Unknown unknown -> processUnknown(unknown, sourceId); default -> null; })).forEachOrdered(_->{}); } @@ -88,6 +90,7 @@ else if (services.containsKey(command.name())) { return services.get(command.name()).apply(new MetaInformation(sourceId, command.id(), true), command.content()); } else { Logger.logError("Command not found: " + command.name()); + targetProcessor.consumeError(new MetaInformation(sourceId, "", true), command.name() + command.content()); } return null; @@ -157,10 +160,14 @@ String processRegister(Register register) { return null; } + String processUnknown(Unknown unknown, String sourceId) { + targetProcessor.consumeError(new MetaInformation(sourceId, "", true), unknown.message()); + return null; + } + void init(String sourceId) { server.clearEvents(sourceId); - server.sendServerEvent(SSEType.CLEAR, "", "", sourceId); - Text.clear(); + Text.clear(sourceId); } } diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index 08e8742..9d0b680 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -1,7 +1,8 @@ package lvp.commands.services; -import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import lvp.Processor.MetaInformation; import lvp.skills.TextUtils; @@ -9,10 +10,16 @@ public class Text { private Text() {} - static Map templates = new HashMap<>(); - - public static void clear() { - templates.clear(); + static Map templates = new ConcurrentHashMap<>(); + + public static void clear(String sourceId) { + Iterator iterator = templates.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (key.startsWith(sourceId + ":")) { + iterator.remove(); + } + } } public static String codeblock(MetaInformation meta, String content) { @@ -25,9 +32,9 @@ public static String codeblock(MetaInformation meta, String content) { } public static String of(MetaInformation meta, String content) { - String existing = templates.get(meta.id()); + String existing = templates.get(meta.sourceId() + ":" + meta.id()); if (existing == null || meta.standalone() && !content.isBlank()) { - templates.put(meta.id(), content); + templates.put(meta.sourceId() + ":" + meta.id(), content); return content; } diff --git a/src/main/java/lvp/commands/targets/Targets.java b/src/main/java/lvp/commands/targets/Targets.java index d270a72..54c4100 100644 --- a/src/main/java/lvp/commands/targets/Targets.java +++ b/src/main/java/lvp/commands/targets/Targets.java @@ -14,6 +14,10 @@ private Targets(Server server) { this.server = server; } + public void consumeError(MetaInformation meta, String content) { + server.sendServerEvent(SSEType.LOG, content, meta.id(), meta.sourceId()); + } + public void consumeClear(MetaInformation meta, String content) { server.sendServerEvent(SSEType.CLEAR, "", meta.id(), meta.sourceId()); } diff --git a/src/main/java/lvp/skills/parser/InstructionParser.java b/src/main/java/lvp/skills/parser/InstructionParser.java index 494afd0..87cbe93 100644 --- a/src/main/java/lvp/skills/parser/InstructionParser.java +++ b/src/main/java/lvp/skills/parser/InstructionParser.java @@ -17,12 +17,13 @@ public class InstructionParser { // ---- Instruction Types ---- - public sealed interface Instruction permits Command, Register, Scan, Pipe {} + public sealed interface Instruction permits Command, Register, Scan, Pipe, Unknown {} public record Command(String name, String id, String content) implements Instruction {} public record Register(String name, String call, boolean skipId) implements Instruction {} public record Scan(String id) implements Instruction {} public record Pipe(List commands) implements Instruction {} + public record Unknown(String message) implements Instruction {} public record CommandRef(String name, String id) {} @@ -86,7 +87,7 @@ private static void handleLine(BlockState state, String line, Downstream el.parentNode.removeChild(el)); const scriptElements = document.body.querySelectorAll(global ? 'script' : `script.${sourceId}`); scriptElements.forEach(el => el.parentNode.removeChild(el)); + + const errors = document.getElementById("errors"); if (!global) { + errors?.querySelectorAll(`.${sourceId}`).forEach(el => el.parentNode.removeChild(el)); for (const prop of Object.getOwnPropertyNames(clerk[sourceId])) { delete clerk[sourceId][prop]; } } else { + while (errors.firstChild) { + errors.removeChild(errors.firstChild); + } for (const prop of Object.getOwnPropertyNames(clerk)) { delete clerk[prop]; } } + if (!errors.hasChildNodes()) errors.parentNode.style.display = "none"; + } function splitEventMessage(message) { @@ -123,11 +123,10 @@ function setUp() { case "LOG": { const newElement = document.createElement("div"); newElement.innerText = data; + newElement.classList.add(sourceId); const errors = document.getElementById("errors"); errors.appendChild(newElement); errors.parentNode.style.display = ""; - scrollPosition = 0; - window.scrollTo(0, 0); break; } default: From 62b89f18d23daf45c7f3302d6ae55d4fc30a1144 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 22 Jun 2025 10:50:31 +0200 Subject: [PATCH 35/50] reverted text pipe behavior --- src/main/java/lvp/commands/services/Text.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index 9d0b680..e12f146 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -10,10 +10,10 @@ public class Text { private Text() {} - static Map templates = new ConcurrentHashMap<>(); + static Map memory = new ConcurrentHashMap<>(); public static void clear(String sourceId) { - Iterator iterator = templates.keySet().iterator(); + Iterator iterator = memory.keySet().iterator(); while (iterator.hasNext()) { String key = iterator.next(); if (key.startsWith(sourceId + ":")) { @@ -32,9 +32,9 @@ public static String codeblock(MetaInformation meta, String content) { } public static String of(MetaInformation meta, String content) { - String existing = templates.get(meta.sourceId() + ":" + meta.id()); + String existing = memory.get(meta.sourceId() + ":" + meta.id()); if (existing == null || meta.standalone() && !content.isBlank()) { - templates.put(meta.sourceId() + ":" + meta.id(), content); + memory.put(meta.sourceId() + ":" + meta.id(), content); return content; } @@ -42,6 +42,6 @@ public static String of(MetaInformation meta, String content) { return existing; } - return TextUtils.linearFillOut(existing, content); + return TextUtils.linearFillOut(content, existing); } } From a82623c394a5ec60e424c71a6463788a131726ad Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 22 Jun 2025 10:50:39 +0200 Subject: [PATCH 36/50] multiline tests --- src/main/java/lvp/commands/services/Test.java | 35 +++++++++++++------ src/main/java/lvp/commands/services/Text.java | 2 +- .../targets/markdown/interactiveCodeblocks.js | 2 +- testdemo.java | 11 +++--- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/main/java/lvp/commands/services/Test.java b/src/main/java/lvp/commands/services/Test.java index 9b777d9..561fa26 100644 --- a/src/main/java/lvp/commands/services/Test.java +++ b/src/main/java/lvp/commands/services/Test.java @@ -5,6 +5,9 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -13,19 +16,30 @@ import lvp.skills.TextUtils; import lvp.skills.logging.Logger; -//TODO: multiple actual and expect public class Test { + private Test() {} private static final String JSHELL_PROMPT = "jshell>"; public static String test(MetaInformation meta, String content) { - Map fields = content.lines() - .filter(line -> !line.isBlank()) - .map(line -> line.split(":", 2)) - .filter(parts -> parts.length == 2) - .map(parts -> Map.entry(parts[0].strip().toLowerCase(), parts[1].strip())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + Map> fields = new HashMap<>(); + String currentKey = null; - String send = fields.get("send"); - String expect = fields.get("expect"); + for (String line: content.lines().toList()) { + if (line.isBlank()) continue; + if (line.strip().startsWith("Send:") || line.strip().startsWith("Expect:")) { + String[] parts = line.split(":", 2); + currentKey = parts[0].strip().toLowerCase(); + String value = parts[1].strip(); + fields.computeIfAbsent(currentKey, _ -> new ArrayList<>()); + if (!value.isEmpty()) fields.get(currentKey).add(value); + } else if (currentKey != null) { + fields.get(currentKey).add(line); + } else { + Logger.logError("Unexpected line " + line); + return null; + } + } + String send = String.join("\n", fields.get("send")); + List expect = fields.get("expect"); if (send == null || expect == null) { Logger.logError("Test command requires 'Send' and 'Expect' fields."); @@ -35,7 +49,7 @@ public static String test(MetaInformation meta, String content) { Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect); String actual = executeJshell(send); if (actual == null) return "No Result"; - String actualParsed = actual.lines().map(Test::parseJshellOutput).findFirst().orElse(""); + List actualParsed = actual.lines().map(Test::parseJshellOutput).toList(); return TextUtils.fillOut(""" Result for Test ${0}: @@ -74,6 +88,7 @@ private static String executeJshell(String send) { } } catch (Exception e) { Logger.logError("Error in jshell", e); + Thread.currentThread().interrupt(); } return result; } diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index e12f146..30682b5 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -25,7 +25,7 @@ public static void clear(String sourceId) { public static String codeblock(MetaInformation meta, String content) { String[] parts = content.split(";"); if (parts.length != 2) { - Logger.logError("Invalid Codeblock Format."); + Logger.logError("(" + meta.id() + ") Invalid Codeblock Format."); return null; } return TextUtils.codeBlock(parts[0].strip(), parts[1].strip()); diff --git a/src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js b/src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js index ba4a5e7..f14b22e 100644 --- a/src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js +++ b/src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js @@ -7,7 +7,7 @@ function convertCodeBlock (renderer) { return match != null ? `
` + original + `` + - `` + + `` + `` + `
` : original; } diff --git a/testdemo.java b/testdemo.java index 2702987..a4ed49a 100644 --- a/testdemo.java +++ b/testdemo.java @@ -2,16 +2,19 @@ void main() { println(""" Clear: - Markdown: # Test Demo - Text{0}: + Text[0]: ``` ${0} ``` ~~~ - Test{0}: + Test[0]: Send: 1 + 1 - Expect: 2 + 2 + 2 + Expect: + 2 + 4 ~~~ - | Text{0} | Markdown + | Text[0] | Markdown """); } \ No newline at end of file From a0f08874271dd8935c8d8c00e5d726c2061fab0a Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 22 Jun 2025 12:15:08 +0200 Subject: [PATCH 37/50] documentation for test, codeblock and text --- demo/demo.java | 5 - demo/sub1/demo.bat | 2 - demo/sub1/demo.java | 7 - demo/sub1/demo.sh | 1 - demo/sub2/demo.java | 13 -- demo/sub2/support.java | 3 - examples/CodeDokuMitMarkdown.java | 174 ++++------------- examples/CodeDokuMitMarkdown.java.alt | 183 ++++++++++++++++++ examples/CodeTests.java | 43 ++++ examples/Interactions.java | 58 ------ examples/MatheInMarkdown.java | 9 +- examples/TextTest.java | 23 --- src/main/java/lvp/Processor.java | 6 +- src/main/java/lvp/commands/services/Test.java | 28 ++- src/main/java/lvp/commands/services/Text.java | 10 + testdemo.java | 20 -- 16 files changed, 303 insertions(+), 282 deletions(-) delete mode 100644 demo/demo.java delete mode 100644 demo/sub1/demo.bat delete mode 100644 demo/sub1/demo.java delete mode 100644 demo/sub1/demo.sh delete mode 100644 demo/sub2/demo.java delete mode 100644 demo/sub2/support.java create mode 100644 examples/CodeDokuMitMarkdown.java.alt create mode 100644 examples/CodeTests.java delete mode 100644 examples/Interactions.java delete mode 100644 examples/TextTest.java delete mode 100644 testdemo.java diff --git a/demo/demo.java b/demo/demo.java deleted file mode 100644 index d12f52f..0000000 --- a/demo/demo.java +++ /dev/null @@ -1,5 +0,0 @@ -void main() { - println(""" - Markdown: # Demo - """); -} \ No newline at end of file diff --git a/demo/sub1/demo.bat b/demo/sub1/demo.bat deleted file mode 100644 index 352f18b..0000000 --- a/demo/sub1/demo.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -echo Markdown: # PowerShell Demo \ No newline at end of file diff --git a/demo/sub1/demo.java b/demo/sub1/demo.java deleted file mode 100644 index 16cc54f..0000000 --- a/demo/sub1/demo.java +++ /dev/null @@ -1,7 +0,0 @@ -void main() { - println(""" - Clear: ~ - Markdown: # Demo Sub 1 - Markdown: This is a demo submodule. - """); -} \ No newline at end of file diff --git a/demo/sub1/demo.sh b/demo/sub1/demo.sh deleted file mode 100644 index 0257c68..0000000 --- a/demo/sub1/demo.sh +++ /dev/null @@ -1 +0,0 @@ -echo "Markdown: # Shell Demo" \ No newline at end of file diff --git a/demo/sub2/demo.java b/demo/sub2/demo.java deleted file mode 100644 index 7281307..0000000 --- a/demo/sub2/demo.java +++ /dev/null @@ -1,13 +0,0 @@ -void main() { - println(""" - - Markdown: # Demo Sub2 - Dot: - digraph G { - a -> b; - b -> c; - c -> a; - } - ~~~ - """); -} \ No newline at end of file diff --git a/demo/sub2/support.java b/demo/sub2/support.java deleted file mode 100644 index b2a61aa..0000000 --- a/demo/sub2/support.java +++ /dev/null @@ -1,3 +0,0 @@ -public class support { - -} diff --git a/examples/CodeDokuMitMarkdown.java b/examples/CodeDokuMitMarkdown.java index 154fa81..1c20e39 100644 --- a/examples/CodeDokuMitMarkdown.java +++ b/examples/CodeDokuMitMarkdown.java @@ -1,24 +1,19 @@ -import lvp.Clerk; -import lvp.skills.Text; - void main() { - Clerk.clear(); - Clerk.markdown(""" - # Die Code-Dokumentation mit Markdown - - Für die Code-Dokumentation mit Markdown sind Textblöcke und der Text-Skill entscheidende Hilfsmittel. - - * Mit Textblöcken lassen sich String-Literale als Textblöcke über mehrere Zeilen hinweg angeben. Ein solcher Textblock beginnt und endet mit drei Anführungszeichen `\"""`. + println(""" + Clear + Markdown: + # Die Code-Dokumentation mit Markdown - * Das LVP bringt einen Text-Skill mit, der hauptsächlich dafür da ist, - - um Text aus einer Datei auszuschneiden (Methode `cutOut`); der Bereich, der ausgeschnitten werden soll, wird durch Textmarken (Labels) ausgewiesen. - - um Text mit Aufüllfeldern zu versehen (Methode `fillOut`), in die Ergebnisse aus Auswertungen als Zeichenkette eingefügt werden. + Für die Code-Dokumentation mit Markdown sind Textblöcke, sowie die Text und Codeblock Kommandos entscheidende Hilfsmittel. - > In den Java-Versionen 21 und 22 gab es [String-Templates](https://docs.oracle.com/en/java/javase/22/language/string-templates.html) als Preview-Feature. Damit ließen sich sehr elegant die Auswertungen von Ausdrücken mitten in einen String einfügen. Das wird in anderen Programmiersprachen auch als String-Interpolation bezeichnet. Leider sind die String-Templates mit Java 23 wieder entfernt worden -- ein einmaliger Vorgang für Preview-Features in der Historie von Java. Als leichtgewichtigen Ersatz gibt es deshalb im Text-Skill die statische Methode `fillOut`. + * Mit Textblöcken lassen sich String-Literale als Textblöcke über mehrere Zeilen hinweg angeben. Ein solcher Textblock beginnt und endet mit drei Anführungszeichen `\"""`. - """); - - + * Das Kommando 'Codeblock' ist hauptsächlich dafür da, um Text aus einer Datei auszuschneiden und in einem Markdown Codeblock einzufügen, welcher im Browser editiert werden kann; der Bereich, der ausgeschnitten werden soll, wird durch Textmarken (Labels) ausgewiesen. + - Für einen nicht editierbaren Codeblock kann das Kommando `Cutout` verwendet werden. + * Das Kommando 'Text' erlaubt es definierte Strings zu speichern und mit Aufüllfeldern versehene Texte, mit weiteren Inhalten aufzufüllen. + ~~~ + """); + // Testfälle assert factorial(0) == 1 && factorial(1) == 1; assert factorial(2) == 2 && factorial(3) == 6; @@ -28,132 +23,35 @@ void main() { String s = "Die Fakultät von " + (num = 6) + " ist " + factorial(num) + "."; // Beispiel - - - - Clerk.markdown(Text.fillOut(""" - ## Dynamische Inhalte in Zeichenketten einbetten - - Wenn Inhalte in einer Zeichenkette dynamisch berechnet und eingefügt werden sollen, kann man das beispielsweise wie folgt machen: - - ``` - ${Beispiel} - ``` - - Das Ergebnis der Zeichenkette `s` ist - - ``` - ${Resultat} - ``` - - Das sieht dann, wenn man die Zeichenkette im Markdown einfügt (mit `Text.fillOut`), so aus: ${Resultat} - - Diese Technik der Einbettung von dynamischen Inhalten in eine Zeichenkette lässt sich ausreizen mit der Text-Skill. Damit kann der Java-Quelltext sich zur Laufzeit selbst ausschneiden zur Einbettung in Markdown! Das ist der Schlüssel zu sich selbst dokumentierendem Programmcode. - """, Map.of("Beispiel", Text.cutOut("examples/CodeDokuMitMarkdown.java", "// Beispiel"), - "Resultat", s))); - - Clerk.markdown(Text.fillOut(Map.of( - "LabelCff", - Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelCff"), - "ResultLabelCff", - // LabelCff - Text.cutOut("examples/CodeDokuMitMarkdown.java", false, false, "// LabelC") - // LabelCff - , "LabelCft", - Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelCft"), - "ResultLabelCft", - // LabelCft - Text.cutOut("examples/CodeDokuMitMarkdown.java", false, true, "// LabelC") - // LabelCft - , "LabelAB", - Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelAB"), - "ResultLabelAB", - // LabelAB - Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelA", "// LabelB") - // LabelAB - , "TextCutOut", - Text.cutOut("src/main/java/lvp/skills/Text.java", "// core method", "// end") - ), """ - ## Texte ausschneiden mit `Text.cut` - - Mit dem Skill `Text` kann Text aus einer Datei ausgeschnitten werden. Der Methodenkopf von `cutOut` erwartet einen Dateinamen, zwei boolsche Werte und eine beliebige Anzahl an Labels. - - ``` - static String cutOut(String fileName, boolean includeStartLabel, boolean includeEndLabel, String... labels) - ``` - - Labels sind Zeichenketten, nach denen als vollständige Textzeile in der angegebenen Datei gesucht wird. Mit den boolschen Werten wird angegeben, ob das öffnende bzw. schliessende Label beim Ausschnitt mit inkludiert, d.h. einbezogen werden soll oder nicht. - - ### Beispiele + println(""" + Text[Template]: + ## Dynamische Inhalte in Zeichenketten einbetten - Nehmen wir eine Datei mit folgendem Inhalt: + Wenn Inhalte in einer Zeichenkette dynamisch berechnet und eingefügt werden sollen, kann man das beispielsweise wie folgt machen: - ```text - // LabelA - 1. Textstelle, gerahmt von einem LabelA und einem LabelB - // LabelB - // LabelC - Textstelle, umschlossen von einem LabelC - // LabelC - // LabelA - 2. Textstelle, gerahmt von einem LabelA und einem LabelB - // LabelB - ``` + ``` + ${Beispiel} + ``` - Der folgende Aufruf + Das Ergebnis der Zeichenkette `s` ist - ``` - ${LabelCff} - ``` + ``` + ${Resultat} + ``` - liefert als Zeichenkette diesen Auszug (Snippet) aus der Datei zurück: - - ``` - ${ResultLabelCff} - ``` - - > Der Witz an diesem Beispiel ist das, was man hier nicht sieht, aber wichtig für die Idee einer eingebetteten, dynamischen Dokumentation ist: Der obige Aufruf ist tatsächlich ein Snippet von dem Code, der das resultierende Snippet erzeugt. Das klingt ein wenig seltsam, aber das ist genau der Kunstgriff, der garantiert, dass der Aufruf wirklich der ist, der das Ergebnis produziert. Wenn Sie einen Blick in die Java-Datei werfen, die diese View im Browser erzeugt hat, werden Sie das vermutlich verstehen und nachvollziehen können. Vergleichen Sie den Java-Quellcode mit dem Text im Browser. - - Setzt man einen der boolschen Werte auf `true`, wird das entsprechende Label mit übernommen. - - ``` - ${LabelCft} - ``` - - Das Ergebnis sieht so aus: - - ``` - ${ResultLabelCft} - ``` - - Sind mehrere Stellen mit dem gleichen Label belegt, kann man diese Bereiche ausschneiden. Wenn die boolschen Werte beide `false` sind, kann man den Aufruf verkürzen. - - ``` - ${LabelAB} - ``` - - Zunächst wird die erste Textstelle zwischen `LabelA` und `LabelB` ausgeschnitten, dann die zweite. - - ``` - ${ResultLabelAB} - ``` - - ### Der Algorithmus zu `Text.cutOut` - - Der Algorithmus zu `Text.cutOut(...)`, um einen Bereich aus einer Textdatei zu schneiden und ein sogenanntes Snippet davon zu erstellen, funktioniert wie folgt: - - 0. Starte im Modus, die Textzeilen einer Datei zu überspringen: `skipLines = true`. - 1. Gehe die Datei Textzeile für Textzeile durch. - 2. Wenn die Textzeile einem Label entspricht, dann gehe wie folgt vor: (a) Wenn entweder `skipLines` und `includeStartLabel` wahr sind, oder wenn `!skipLines` und `includeEndLabel` wahr sind, dann ergänze die Labelzeile zum Snippet. (b) Wechsel den Modus `skipLines = !skipLines` und gehe zur nächsten Textzeile (Schritt 1). - 3. Entspricht die Textzeile keinem Label, dann: (a) Füge die Zeile nur dann dem Snippet hinzu, wenn `skipLines` nicht wahr ist. (b) Gehe zur nächsten Textzeile (Schritt 1). - - Als Java-Methode: - - ```java - ${TextCutOut} - ``` - """)); + Das sieht dann, wenn man die Zeichenkette im Markdown einfügt, so aus: ${Resultat} + Diese Technik der Einbettung von dynamischen Inhalten in eine Zeichenkette lässt sich ausreizen mit den Kommandos `Codeblock` oder `Cutout`. Damit kann der Java-Quelltext sich zur Laufzeit selbst ausschneiden zur Einbettung in Markdown! Das ist der Schlüssel zu sich selbst dokumentierendem Programmcode. + ~~~ + """); + println("Text[Resultat]: " + s); + println(""" + + Codeblock: examples/CodeDokuMitMarkdown.java; // Beispiel + | Text[Template] | Text[TemplateMitBeispiel] + Text[Resultat] + | Text[TemplateMitBeispiel] | Markdown + """); } // Fakultätsfunktion @@ -162,4 +60,4 @@ long factorial(int n) { if (n == 1 || n == 0) return 1; return n * factorial(n - 1); } -// Ende Fakultätsfunktion +// Fakultätsfunktion diff --git a/examples/CodeDokuMitMarkdown.java.alt b/examples/CodeDokuMitMarkdown.java.alt new file mode 100644 index 0000000..096a0a0 --- /dev/null +++ b/examples/CodeDokuMitMarkdown.java.alt @@ -0,0 +1,183 @@ +import lvp.Clerk; +import lvp.skills.Text; + +void main() { + Clerk.clear(); + Clerk.markdown(""" + # Die Code-Dokumentation mit Markdown + + Für die Code-Dokumentation mit Markdown sind Textblöcke und der Text-Skill entscheidende Hilfsmittel. + + * Mit Textblöcken lassen sich String-Literale als Textblöcke über mehrere Zeilen hinweg angeben. Ein solcher Textblock beginnt und endet mit drei Anführungszeichen `\"""`. + + * Das LVP bringt einen Text-Skill mit, der hauptsächlich dafür da ist, + - um Text aus einer Datei auszuschneiden (Methode `cutOut`); der Bereich, der ausgeschnitten werden soll, wird durch Textmarken (Labels) ausgewiesen. + - um Text mit Aufüllfeldern zu versehen (Methode `fillOut`), in die Ergebnisse aus Auswertungen als Zeichenkette eingefügt werden. + + > In den Java-Versionen 21 und 22 gab es [String-Templates](https://docs.oracle.com/en/java/javase/22/language/string-templates.html) als Preview-Feature. Damit ließen sich sehr elegant die Auswertungen von Ausdrücken mitten in einen String einfügen. Das wird in anderen Programmiersprachen auch als String-Interpolation bezeichnet. Leider sind die String-Templates mit Java 23 wieder entfernt worden -- ein einmaliger Vorgang für Preview-Features in der Historie von Java. Als leichtgewichtigen Ersatz gibt es deshalb im Text-Skill die statische Methode `fillOut`. + + """); + + + // Testfälle + assert factorial(0) == 1 && factorial(1) == 1; + assert factorial(2) == 2 && factorial(3) == 6; + assert factorial(4) == 24 && factorial(5) == 120; + // Beispiel + int num; + String s = "Die Fakultät von " + (num = 6) + " ist " + factorial(num) + "."; + // Beispiel + + + + + Clerk.markdown(Text.fillOut(""" + ## Dynamische Inhalte in Zeichenketten einbetten + + Wenn Inhalte in einer Zeichenkette dynamisch berechnet und eingefügt werden sollen, kann man das beispielsweise wie folgt machen: + + ``` + ${Beispiel} + ``` + + Das Ergebnis der Zeichenkette `s` ist + + ``` + ${Resultat} + ``` + + Das sieht dann, wenn man die Zeichenkette im Markdown einfügt (mit `Text.fillOut`), so aus: ${Resultat} + + Diese Technik der Einbettung von dynamischen Inhalten in eine Zeichenkette lässt sich ausreizen mit der Text-Skill. Damit kann der Java-Quelltext sich zur Laufzeit selbst ausschneiden zur Einbettung in Markdown! Das ist der Schlüssel zu sich selbst dokumentierendem Programmcode. + """, Map.of("Beispiel", Text.cutOut("examples/CodeDokuMitMarkdown.java", "// Beispiel"), + "Resultat", s))); + + + + + + + + + + + + + + + + + + + + Clerk.markdown(Text.fillOut(Map.of( + "LabelCff", + Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelCff"), + "ResultLabelCff", + // LabelCff + Text.cutOut("examples/CodeDokuMitMarkdown.java", false, false, "// LabelC") + // LabelCff + , "LabelCft", + Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelCft"), + "ResultLabelCft", + // LabelCft + Text.cutOut("examples/CodeDokuMitMarkdown.java", false, true, "// LabelC") + // LabelCft + , "LabelAB", + Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelAB"), + "ResultLabelAB", + // LabelAB + Text.cutOut("examples/CodeDokuMitMarkdown.java", "// LabelA", "// LabelB") + // LabelAB + , "TextCutOut", + Text.cutOut("src/main/java/lvp/skills/Text.java", "// core method", "// end") + ), """ + ## Texte ausschneiden mit `Text.cut` + + Mit dem Skill `Text` kann Text aus einer Datei ausgeschnitten werden. Der Methodenkopf von `cutOut` erwartet einen Dateinamen, zwei boolsche Werte und eine beliebige Anzahl an Labels. + + ``` + static String cutOut(String fileName, boolean includeStartLabel, boolean includeEndLabel, String... labels) + ``` + + Labels sind Zeichenketten, nach denen als vollständige Textzeile in der angegebenen Datei gesucht wird. Mit den boolschen Werten wird angegeben, ob das öffnende bzw. schliessende Label beim Ausschnitt mit inkludiert, d.h. einbezogen werden soll oder nicht. + + ### Beispiele + + Nehmen wir eine Datei mit folgendem Inhalt: + + ```text + // LabelA + 1. Textstelle, gerahmt von einem LabelA und einem LabelB + // LabelB + // LabelC + Textstelle, umschlossen von einem LabelC + // LabelC + // LabelA + 2. Textstelle, gerahmt von einem LabelA und einem LabelB + // LabelB + ``` + + Der folgende Aufruf + + ``` + ${LabelCff} + ``` + + liefert als Zeichenkette diesen Auszug (Snippet) aus der Datei zurück: + + ``` + ${ResultLabelCff} + ``` + + > Der Witz an diesem Beispiel ist das, was man hier nicht sieht, aber wichtig für die Idee einer eingebetteten, dynamischen Dokumentation ist: Der obige Aufruf ist tatsächlich ein Snippet von dem Code, der das resultierende Snippet erzeugt. Das klingt ein wenig seltsam, aber das ist genau der Kunstgriff, der garantiert, dass der Aufruf wirklich der ist, der das Ergebnis produziert. Wenn Sie einen Blick in die Java-Datei werfen, die diese View im Browser erzeugt hat, werden Sie das vermutlich verstehen und nachvollziehen können. Vergleichen Sie den Java-Quellcode mit dem Text im Browser. + + Setzt man einen der boolschen Werte auf `true`, wird das entsprechende Label mit übernommen. + + ``` + ${LabelCft} + ``` + + Das Ergebnis sieht so aus: + + ``` + ${ResultLabelCft} + ``` + + Sind mehrere Stellen mit dem gleichen Label belegt, kann man diese Bereiche ausschneiden. Wenn die boolschen Werte beide `false` sind, kann man den Aufruf verkürzen. + + ``` + ${LabelAB} + ``` + + Zunächst wird die erste Textstelle zwischen `LabelA` und `LabelB` ausgeschnitten, dann die zweite. + + ``` + ${ResultLabelAB} + ``` + + ### Der Algorithmus zu `Text.cutOut` + + Der Algorithmus zu `Text.cutOut(...)`, um einen Bereich aus einer Textdatei zu schneiden und ein sogenanntes Snippet davon zu erstellen, funktioniert wie folgt: + + 0. Starte im Modus, die Textzeilen einer Datei zu überspringen: `skipLines = true`. + 1. Gehe die Datei Textzeile für Textzeile durch. + 2. Wenn die Textzeile einem Label entspricht, dann gehe wie folgt vor: (a) Wenn entweder `skipLines` und `includeStartLabel` wahr sind, oder wenn `!skipLines` und `includeEndLabel` wahr sind, dann ergänze die Labelzeile zum Snippet. (b) Wechsel den Modus `skipLines = !skipLines` und gehe zur nächsten Textzeile (Schritt 1). + 3. Entspricht die Textzeile keinem Label, dann: (a) Füge die Zeile nur dann dem Snippet hinzu, wenn `skipLines` nicht wahr ist. (b) Gehe zur nächsten Textzeile (Schritt 1). + + Als Java-Methode: + + ```java + ${TextCutOut} + ``` + """)); + +} + +// Fakultätsfunktion +long factorial(int n) { + assert n >= 0 : "Positive Ganzzahl erforderlich"; + if (n == 1 || n == 0) return 1; + return n * factorial(n - 1); +} +// Ende Fakultätsfunktion diff --git a/examples/CodeTests.java b/examples/CodeTests.java new file mode 100644 index 0000000..5475292 --- /dev/null +++ b/examples/CodeTests.java @@ -0,0 +1,43 @@ +import static java.io.IO.println; + +void main() { + println(""" + Clear: - + Markdown: # Test Demo + Text[0]: + ``` + ${0} + ``` + ~~~ + Test[0]: + Send: 1 + 1 + 2 + 2 + Expect: + 2 + 4 + ~~~ + | Text[0] | Markdown + + Text[FactorialMethod]: + Send: ${0} + factorial(5) + Expect: 120 + ~~~ + + Cutout: examples/CodeDokuMitMarkdown.java; // Fakultätsfunktion + | Text[FactorialMethod] | Test | Text[0] | Markdown + + Test: + Send: 2 + 2 + int j = 0; + for(int i = 0; i < $1; i++) { + j = 2 * i; + } + j + Expect: 6 + Type: oneof + ~~~ + | Text[0] | Markdown + + """); +} \ No newline at end of file diff --git a/examples/Interactions.java b/examples/Interactions.java deleted file mode 100644 index 2115aef..0000000 --- a/examples/Interactions.java +++ /dev/null @@ -1,58 +0,0 @@ -import lvp.Clerk; -import lvp.views.*; -import lvp.skills.*; - -public class TestClass { - public TestClass child; - public int value; - public TestClass(TestClass child, int value) { - this.child = child; - this.value = value; - } -} - -public void main() { - Clerk.clear(); - Clerk.markdown("# Hello World"); // hello - Clerk.write(Interaction.button("Click me", 200, 50, Interaction.eventFunction("./examples/Interactions.java", "// hello", "Clerk.markdown(\"# Goodbye World\");"))); - - Clerk.markdown("## Dot Example with Object Inspector"); - TestClass t1 = new TestClass(null, 1); - TestClass t2 = new TestClass(t1, 2); - ObjectInspector ng = ObjectInspector.inspect(t2, "t2"); - Dot d = new Dot(1200, 500); - d.draw(ng.toString()); - - Clerk.markdown("## Interactive Turtle"); - var turtle = new Turtle(0, 300, 0, 200, 150, 25, 0); - drawing(turtle, 100); - turtle.write().timelineSlider(); - Clerk.markdown("### Choose a Color"); - Clerk.write(Interaction.button("Red", Interaction.eventFunction("./examples/Interactions.java", "// turtle color", "turtle.color(255, i * 256 / 37, i * 256 / 37, 1);"))); - Clerk.write(Interaction.button("Green", Interaction.eventFunction("./examples/Interactions.java", "// turtle color", "turtle.color(i * 256 / 37, 255, i * 256 / 37, 1);"))); - Clerk.write(Interaction.button("Blue", Interaction.eventFunction("./examples/Interactions.java", "// turtle color", "turtle.color(i * 256 / 37, i * 256 / 37, 255, 1);"))); - - Clerk.markdown(Text.fillOut( - """ - ## Interactive Code Blocks - ```java - ${0} - ``` - """, Text.codeBlock("./examples/Interactions.java", "// drawing") - )); -} - -void triangle(Turtle turtle, double size) { - turtle.forward(size).right(60).backward(size).right(60).forward(size).right(60 + 180); -} - -// drawing -void drawing(Turtle turtle, double size) { - for (int i = 1; i <= 36; i++) { - turtle.color(255, i * 256 / 37, i * 256 / 37, 1); // turtle color - turtle.width(1.0 - 1.0 / 36.0 * i); - triangle(turtle, size + 1 - 2 * i); - turtle.left(10).forward(10); - } -} -// drawing diff --git a/examples/MatheInMarkdown.java b/examples/MatheInMarkdown.java index 249a024..56ca349 100644 --- a/examples/MatheInMarkdown.java +++ b/examples/MatheInMarkdown.java @@ -1,8 +1,7 @@ -import lvp.Clerk; - void main() { - Clerk.clear(); - Clerk.markdown(""" + println(""" + Clear + Markdown: # Mathe Beispiele ## Simple Math Man kann Formeln wie $2x_1+5x_2 = 12$ direkt in der Textzeile unterbringen. @@ -31,6 +30,6 @@ void main() { \\Large 1x^2 + 2x = 5 $$ - + ~~~ """); } \ No newline at end of file diff --git a/examples/TextTest.java b/examples/TextTest.java deleted file mode 100644 index 809ac98..0000000 --- a/examples/TextTest.java +++ /dev/null @@ -1,23 +0,0 @@ -void main() { - assert Text.fillOut( - "Das Ergebnis ist ${#1} oder ${#2}.", - Map.of("#1", 2 + 3, "#2", 42)).equals( - "Das Ergebnis ist 5 oder 42."); - - assert Text.fillOut( - Map.of("#1", 2 + 3, "#2", 42), - "Das Ergebnis ist ${#1} oder ${#2}.").equals( - "Das Ergebnis ist 5 oder 42."); - - assert Text.fillOut( - "Das Ergebnis ist ${0} oder ${1}.", 2 + 3, 42).equals( - "Das Ergebnis ist 5 oder 42."); - - assert Text.fillOut( // You'll get a WARNING on std.err - "Das Ergebnis ist ${0} oder ${value}.", 2 + 3).equals( - "Das Ergebnis ist 5 oder ${value}."); - - assert Text.fillOut( - "Das Ergebnis ist ${0} oder ${value}.", Map.of("0", 2 + 3, "value", "${value}")).equals( - "Das Ergebnis ist 5 oder ${value}."); -} \ No newline at end of file diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index b8856bc..297fe78 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -37,7 +37,8 @@ public record MetaInformation(String sourceId, String id, boolean standalone) {} Map> targets; Map> services = new HashMap<>(Map.of( "Text", Text::of, - "Codeblock", Text::codeblock, + "Codeblock", Text::codeblock, + "Cutout", Text::cutout, "Turtle", Turtle::of, "Button", Interaction::button, "Input", Interaction::input, @@ -97,10 +98,11 @@ else if (services.containsKey(command.name())) { } String processPipe(Pipe pipe, String input, String sourceId) { - if (input == null) return null; String current = input; for (CommandRef ref : pipe.commands()) { Logger.logDebug("Command: " + ref.name() + "{" + ref.id() + "}, " + current); + if (current == null) return null; + if (targets.containsKey(ref.name())) { targets.get(ref.name()).accept(new MetaInformation(sourceId, ref.id(), false), current); return null; diff --git a/src/main/java/lvp/commands/services/Test.java b/src/main/java/lvp/commands/services/Test.java index 561fa26..110a7ef 100644 --- a/src/main/java/lvp/commands/services/Test.java +++ b/src/main/java/lvp/commands/services/Test.java @@ -7,10 +7,12 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.IntStream; import lvp.Processor.MetaInformation; import lvp.skills.TextUtils; @@ -25,12 +27,13 @@ public static String test(MetaInformation meta, String content) { for (String line: content.lines().toList()) { if (line.isBlank()) continue; - if (line.strip().startsWith("Send:") || line.strip().startsWith("Expect:")) { + if (line.strip().startsWith("Send:") || line.strip().startsWith("Expect:") || line.strip().startsWith("Type:")) { String[] parts = line.split(":", 2); currentKey = parts[0].strip().toLowerCase(); String value = parts[1].strip(); fields.computeIfAbsent(currentKey, _ -> new ArrayList<>()); if (!value.isEmpty()) fields.get(currentKey).add(value); + if (currentKey.equals("type")) currentKey = null; } else if (currentKey != null) { fields.get(currentKey).add(line); } else { @@ -40,16 +43,18 @@ public static String test(MetaInformation meta, String content) { } String send = String.join("\n", fields.get("send")); List expect = fields.get("expect"); + List typeL = fields.get("type"); + String type = typeL != null && typeL.size() == 1 ? typeL.getFirst() : "exact"; if (send == null || expect == null) { Logger.logError("Test command requires 'Send' and 'Expect' fields."); return null; } - Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect); + Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect + ", type=" + type); String actual = executeJshell(send); if (actual == null) return "No Result"; - List actualParsed = actual.lines().map(Test::parseJshellOutput).toList(); + List actualParsed = actual.lines().map(Test::parseJshellOutput).filter(s -> !s.isBlank()).toList(); return TextUtils.fillOut(""" Result for Test ${0}: @@ -57,8 +62,21 @@ public static String test(MetaInformation meta, String content) { Response: ${2} Actual: ${3} Expected: ${4} - Status: ${5} - """, meta.id(), send, actual, actualParsed, expect, actualParsed.equals(expect) ? "Success" : "Failure"); + Comparison: '${5}' + Status: ${6} + """, meta.id(), send, actual, actualParsed, expect, type, compare(actualParsed, expect, type) ? "Success" : "Failure"); + } + + private static boolean compare(List actual, List expected, String type) { + return switch(type) { + case "exact" -> actual.equals(expected); + case "oneof" -> expected.stream().allMatch(actual::contains); + case "same" -> actual.size() == expected.size() && new HashSet<>(actual).equals(new HashSet<>(expected)); + default -> { + Logger.logError("Unknown Comparison Type."); + yield false; + } + }; } private static String executeJshell(String send) { diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index 30682b5..b01fc03 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -31,6 +31,16 @@ public static String codeblock(MetaInformation meta, String content) { return TextUtils.codeBlock(parts[0].strip(), parts[1].strip()); } + //TODO: Allow multiple label + public static String cutout(MetaInformation meta, String content) { + String[] parts = content.split(";"); + if (parts.length != 2) { + Logger.logError("(" + meta.id() + ") Invalid Codeblock Format."); + return null; + } + return TextUtils.cutOut(parts[0].strip(), parts[1].strip()); + } + public static String of(MetaInformation meta, String content) { String existing = memory.get(meta.sourceId() + ":" + meta.id()); if (existing == null || meta.standalone() && !content.isBlank()) { diff --git a/testdemo.java b/testdemo.java deleted file mode 100644 index a4ed49a..0000000 --- a/testdemo.java +++ /dev/null @@ -1,20 +0,0 @@ -void main() { - println(""" - Clear: - - Markdown: # Test Demo - Text[0]: - ``` - ${0} - ``` - ~~~ - Test[0]: - Send: 1 + 1 - 2 + 2 - Expect: - 2 - 4 - ~~~ - | Text[0] | Markdown - - """); -} \ No newline at end of file From 641854146928020004dfdf121853484b19938c3e Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 22 Jun 2025 12:51:44 +0200 Subject: [PATCH 38/50] better cutout and added more doku examples --- examples/Introduction.java | 12 ++++++++++++ src/main/java/lvp/commands/services/Text.java | 8 +++++--- src/main/java/lvp/skills/TextUtils.java | 2 +- syntax.md | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 examples/Introduction.java diff --git a/examples/Introduction.java b/examples/Introduction.java new file mode 100644 index 0000000..c47ad44 --- /dev/null +++ b/examples/Introduction.java @@ -0,0 +1,12 @@ +void main() { + println(""" + Clear + Text[Template]: + # LiveViewProgramming Concepts + ${0} + ~~~ + + Cutout: syntax.md + | Text[Template] | Markdown + """); +} \ No newline at end of file diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/commands/services/Text.java index b01fc03..449bd01 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/commands/services/Text.java @@ -1,5 +1,6 @@ package lvp.commands.services; +import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -31,14 +32,15 @@ public static String codeblock(MetaInformation meta, String content) { return TextUtils.codeBlock(parts[0].strip(), parts[1].strip()); } - //TODO: Allow multiple label public static String cutout(MetaInformation meta, String content) { String[] parts = content.split(";"); - if (parts.length != 2) { + if (parts.length < 1) { Logger.logError("(" + meta.id() + ") Invalid Codeblock Format."); return null; } - return TextUtils.cutOut(parts[0].strip(), parts[1].strip()); + if (parts.length == 1) + return TextUtils.read(parts[0].strip()); + return TextUtils.cutOut(parts[0].strip(), Arrays.stream(parts).skip(1).map(String::strip).toArray(String[]::new)); } public static String of(MetaInformation meta, String content) { diff --git a/src/main/java/lvp/skills/TextUtils.java b/src/main/java/lvp/skills/TextUtils.java index 36bd236..e956e2c 100644 --- a/src/main/java/lvp/skills/TextUtils.java +++ b/src/main/java/lvp/skills/TextUtils.java @@ -38,7 +38,7 @@ public static String cutOut(Path path, boolean includeStartLabel, boolean includ try { List lines = Files.readAllLines(path); for (String line : lines) { - isInLabels = Arrays.stream(labels).anyMatch(label -> line.trim().equals(label)); + isInLabels = labels == null || labels.length == 0 || labels[0].isBlank() || Arrays.stream(labels).anyMatch(label -> line.trim().equals(label)); if (isInLabels) { if (skipLines && includeStartLabel) snippet.add(line); diff --git a/syntax.md b/syntax.md index db7431b..30b4e2e 100644 --- a/syntax.md +++ b/syntax.md @@ -3,6 +3,7 @@ - `[]` -> Optional - `::=` -> Definiert als - `''` -> Literal + ``` INSTRUCTION ::= COMMAND | REGISTER | PIPE ``` From ab7ebbabcb7a5cf55a622433e0c28946bfec848e Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 22 Jun 2025 15:29:19 +0200 Subject: [PATCH 39/50] update README.md args section --- README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f635d22..3aa504f 100644 --- a/README.md +++ b/README.md @@ -31,26 +31,30 @@ Sie können die `.jar`-Datei auch selber generieren, wenn Sie zudem die Versions Passen Sie den Beispielaufruf an die aktuelle Version an: ``` -java -jar lvp-.jar --log --watch=demo.java +java -jar lvp-.jar --log demo.java ``` Wenn Sie die Version `lvp-0.5.0.jar` heruntergeladen haben, lautet der Aufruf: ``` -java -jar lvp-0.5.0.jar --log --watch=demo.java +java -jar lvp-0.5.0.jar --log demo.java ``` #### Übersicht der möglichen Kommandozeilenargumente -| Argument | Alias | Bedeutung | Beispiel | -|------------------|---------|-------------------------------------------|-----------------------------------------------| -| --watch=DATEI | -w | Zu überwachende Datei oder Verzeichnis | --watch=path/to/
--watch=demo.java | -| --pattern=PATTERN| -p | Dateinamensmuster (z.B. *.java) | --pattern=*.java | -| --log[=LEVEL] | -l | Log-Level (Error, Info, Debug) | --log=Debug | -| PORT | | Portnummer für den Server | 50001 | +| Argument | Alias | Bedeutung | Beispiel | +|----------------------------|-------|---------------------------------------------------------------------------|------------------------------------------| +| `--cmd=CMD` | | Startbefehl für die Ausführung (z. B. Java mit Optionen) | `--cmd="java --enable-preview"` | +| `--log[=LEVEL]` | `-l` | Log-Level (`Error`, `Info`, `Debug`) | `--log=Debug` | +| `--port` | `-p` | Portnummer für den Server | `--port=50002` | +| `--config` | `-c` | Lädt Konfiguration aus `sources.json` | `--config` | +| `--source-only` | `-s` | Ignoriert alle Nicht-Source-Dateien | `--source-only` | +| `--watch-filter=PATTERN` | `-w` | Filter für Dateien, die ein Neuladen der Inhalte auslösen können | `--watch-filter=./deps/*.java` | +| `SOURCES` | | Quellen, die durch LVP ausgeführt werden | `demo1.java demo2.java`
`sources/*.java` | + > Mehrere Argumente können kombiniert werden, z.B.: -> `java -jar lvp-.jar --watch=src --pattern=*.java --log=Debug 50001` +> `java -jar lvp-.jar --watch-filter=src/lib/**/*.java --log=Debug --port=50001 --config src/*View.java` ### 3. So nutzt man das _Live View Programming_ From a1cd3093b1e533254f3c451506bcb67f3bf09905 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 23 Jun 2025 12:46:07 +0200 Subject: [PATCH 40/50] root dir as watch root and more examples --- .../BlockierendeEingabe.java | 0 external.java => examples/ExternerSevice.java | 5 +-- examples/Introduction.java | 31 +++++++++++++--- src/main/java/lvp/FileWatcher.java | 36 +++++++++++-------- 4 files changed, 51 insertions(+), 21 deletions(-) rename blockingdemo.java => examples/BlockierendeEingabe.java (100%) rename external.java => examples/ExternerSevice.java (74%) diff --git a/blockingdemo.java b/examples/BlockierendeEingabe.java similarity index 100% rename from blockingdemo.java rename to examples/BlockierendeEingabe.java diff --git a/external.java b/examples/ExternerSevice.java similarity index 74% rename from external.java rename to examples/ExternerSevice.java index 05300ad..114de50 100644 --- a/external.java +++ b/examples/ExternerSevice.java @@ -1,7 +1,8 @@ -import static java.io.IO.println; - import java.util.Scanner; +/// Register with: +/// Register: Reverse java --enable-preview external.java + void main() { Scanner scanner = new Scanner(System.in); String id = scanner.nextLine(); diff --git a/examples/Introduction.java b/examples/Introduction.java index c47ad44..b7d606a 100644 --- a/examples/Introduction.java +++ b/examples/Introduction.java @@ -1,12 +1,33 @@ +import static java.io.IO.println; + void main() { println(""" Clear - Text[Template]: - # LiveViewProgramming Concepts - ${0} + Text[Intro]: + # LiveViewProgramming Konzept + Auszug aus dem Readme: + ~~~ + | Markdown + + Text[Template1]: + > "${Zitat}" ~~~ - Cutout: syntax.md - | Text[Template] | Markdown + Cutout: README.md;## 💟 Motivation: Views bereichern das Programmieren;### Views und Skills zum Programmverständnis + | Text[Template1] | Markdown + + Markdown: + ## Ziele + - Visualisierung von Programmierung + - Programmdokumentation + - Einfache Interaktion + - Sprachunabhängigkeit + - Erweiterbarkeit + ~~~ + + Markdown: # Umsetzung + + Cutout: syntax.md; # LVP Syntax + | Markdown """); } \ No newline at end of file diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index c061bb5..15888e5 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -45,21 +45,9 @@ public FileWatcher(List sources, Optional watchFilter, boolean s this.sourceOnly = sourceOnly; watcher = FileSystems.getDefault().newWatchService(); - sources.stream() - .map(Source::path) - .map(Path::getParent) - .filter(Objects::nonNull) + + (sourceOnly ? getSourceFolder(sources) : getFolderTree()) .map(Path::normalize) - .flatMap(root -> { - try { - if (sourceOnly) return Stream.of(root); - return Files.find(root, Integer.MAX_VALUE, - (_, attrs) -> attrs.isDirectory()); - } catch (IOException e) { - Logger.logError("Error walking directory: " + root.toAbsolutePath(), e); - return Stream.empty(); - } - }) .distinct() .forEach(dir -> { try { @@ -74,6 +62,26 @@ public FileWatcher(List sources, Optional watchFilter, boolean s } + private Stream getSourceFolder(List input) { + return input.stream() + .map(Source::path) + .map(Path::getParent) + .filter(Objects::nonNull); + } + + private Stream getFolderTree() { + return Stream.of(Path.of(".")) + .flatMap(root -> { + try { + return Files.find(root, Integer.MAX_VALUE, + (_, attrs) -> attrs.isDirectory()).filter(p -> !p.toString().contains(".git")); + } catch (IOException e) { + Logger.logError("Error walking directory: " + root.toAbsolutePath(), e); + return Stream.empty(); + } + }); + } + public void start() { for (Source source : sources) { Logger.logInfo("Running initial file: " + source.path()); From 9c0bdefe63f181a0a5918b28b7a183158301c3e3 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 6 Jul 2025 15:42:38 +0200 Subject: [PATCH 41/50] Server as Sink --- examples/BlockierendeEingabe.java | 4 +- intro.java | 57 ++++++++ newdemo.java | 6 +- src/main/java/lvp/Main.java | 13 +- src/main/java/lvp/Processor.java | 98 ++++++-------- src/main/java/lvp/commands/services/Test.java | 123 ------------------ src/main/java/lvp/sinks/Sink.java | 16 +++ .../server_sink/HttpChannel.java} | 28 +++- .../lvp/{ => sinks/server_sink}/SSEType.java | 2 +- .../lvp/{ => sinks/server_sink}/Server.java | 16 +-- .../lvp/sinks/server_sink/ServerSink.java | 67 ++++++++++ .../server_sink}/dot/GraphSpec.java | 2 +- .../targets => sinks/server_sink}/dot/dot.js | 0 .../server_sink}/dot/vis-network.min.js | 0 .../server_sink}/markdown/CompileMathjax3.md | 0 .../server_sink}/markdown/default.min.css | 0 .../server_sink}/markdown/highlight.min.js | 0 .../markdown/interactiveCodeblocks.js | 0 .../server_sink}/markdown/markdown-it.min.js | 0 .../server_sink}/markdown/mathjax3.js | Bin .../server_sink}/markdown/vs.css | 0 src/main/java/lvp/skills/TriConsumer.java | 6 + .../lvp/skills/parser/InstructionParser.java | 15 +-- .../java/lvp/skills/parser/TurtleParser.java | 2 +- .../services => transformer}/Interaction.java | 2 +- .../services => transformer}/Text.java | 2 +- .../services => transformer}/Turtle.java | 2 +- syntax.md | 34 ++--- 28 files changed, 249 insertions(+), 246 deletions(-) create mode 100644 intro.java delete mode 100644 src/main/java/lvp/commands/services/Test.java create mode 100644 src/main/java/lvp/sinks/Sink.java rename src/main/java/lvp/{commands/targets/Targets.java => sinks/server_sink/HttpChannel.java} (74%) rename src/main/java/lvp/{ => sinks/server_sink}/SSEType.java (66%) rename src/main/java/lvp/{ => sinks/server_sink}/Server.java (94%) create mode 100644 src/main/java/lvp/sinks/server_sink/ServerSink.java rename src/main/java/lvp/{commands/targets => sinks/server_sink}/dot/GraphSpec.java (96%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/dot/dot.js (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/dot/vis-network.min.js (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/markdown/CompileMathjax3.md (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/markdown/default.min.css (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/markdown/highlight.min.js (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/markdown/interactiveCodeblocks.js (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/markdown/markdown-it.min.js (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/markdown/mathjax3.js (100%) rename src/main/java/lvp/{commands/targets => sinks/server_sink}/markdown/vs.css (100%) create mode 100644 src/main/java/lvp/skills/TriConsumer.java rename src/main/java/lvp/{commands/services => transformer}/Interaction.java (99%) rename src/main/java/lvp/{commands/services => transformer}/Text.java (98%) rename src/main/java/lvp/{commands/services => transformer}/Turtle.java (99%) diff --git a/examples/BlockierendeEingabe.java b/examples/BlockierendeEingabe.java index 99e2a42..a7f1674 100644 --- a/examples/BlockierendeEingabe.java +++ b/examples/BlockierendeEingabe.java @@ -3,9 +3,9 @@ import java.util.Scanner; void main() { - println("Clear: ~"); + println("Clear"); println("Markdown: # Blocking Input"); - println("Read:"); + println("Scan:"); Scanner scanner = new Scanner(System.in); String d = scanner.nextLine(); diff --git a/intro.java b/intro.java new file mode 100644 index 0000000..581827c --- /dev/null +++ b/intro.java @@ -0,0 +1,57 @@ +List obst = List.of("Apfel", "Birne", "Banane"); +void main() { + println(""" + Clear + Markdown: ## Einkaufsliste + Markdown: + """); + println(buildObstListe()); + println("~~~"); + println(""" + Text[template]: + ## Beispiel + Irgendein ${0} anzeigen. + ~~~ + | Markdown + + Text: Text + | Text[template] | Markdown + + Text[t2]: + ## Syntax Überschrift + Das ist die Syntax + ${0} + Danach + ~~~ + + Cutout: ./syntax.md + | Text[t2] | Markdown + + Text[t3]: + ```java + ${0} + ``` + ~~~ + + Codeblock:./intro.java;// example + | Text[t3] | Markdown + + Register[skipId]: Counter wc + Text: + Hello World + ~~~ + | Counter | Html + + """); + +} + +// example +String buildObstListe() { + String out = ""; + for (String o : obst) { + out += "**" + o + "**\n"; + } + return out; +} +// example diff --git a/newdemo.java b/newdemo.java index c207f36..3bcfeb3 100644 --- a/newdemo.java +++ b/newdemo.java @@ -1,11 +1,15 @@ -import static java.io.IO.println; // ex1 void main() { println(""" Clear + + + + Markdown: # Text Demo + Text: newdemo.java;// ex1 | Codeblock | Text[example] Text[title]: Codeblocks diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index c1927e8..dd6382b 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -1,6 +1,5 @@ package lvp; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -13,6 +12,8 @@ import java.util.regex.Matcher; import java.util.stream.Stream; +import lvp.sinks.server_sink.Server; +import lvp.sinks.server_sink.ServerSink; import lvp.skills.logging.LogLevel; import lvp.skills.logging.Logger; import lvp.skills.parser.ConfigParser; @@ -35,15 +36,11 @@ public static void main(String[] args) { System.out.println("Warning: You are not using the latest release of Live View Programming. Please visit https://github.com/denkspuren/LiveViewProgramming/releases"); } - Server server = null; - FileWatcher watcher = null; Processor processor = null; - try { - server = new Server(Math.abs(cfg.port())); - Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); - processor = new Processor(server); - watcher = new FileWatcher(cfg.sources(), cfg.watchFilter(), cfg.sourceOnly(), processor); + processor = new Processor(); + processor.registerSink(new ServerSink(cfg.port())); + FileWatcher watcher = new FileWatcher(cfg.sources(), cfg.watchFilter(), cfg.sourceOnly(), processor); Runtime.getRuntime().addShutdownHook(new Thread(watcher::stop)); watcher.start(); } catch (IOException e) { diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 297fe78..c4de374 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -2,10 +2,12 @@ import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; @@ -14,49 +16,28 @@ import java.util.stream.Gatherers; import java.util.stream.Stream; -import lvp.commands.services.Text; -import lvp.commands.services.Test; -import lvp.commands.services.Turtle; -import lvp.commands.services.Interaction; -import lvp.commands.targets.Targets; -import lvp.skills.HTMLElements; -import lvp.skills.TextUtils; +import lvp.sinks.Sink; +import lvp.skills.TriConsumer; import lvp.skills.logging.Logger; import lvp.skills.parser.InstructionParser; -import lvp.skills.parser.InstructionParser.Command; -import lvp.skills.parser.InstructionParser.CommandRef; -import lvp.skills.parser.InstructionParser.Pipe; -import lvp.skills.parser.InstructionParser.Scan; -import lvp.skills.parser.InstructionParser.Unknown; -import lvp.skills.parser.InstructionParser.Register; +import lvp.skills.parser.InstructionParser.*; +import lvp.transformer.*; public class Processor { public record MetaInformation(String sourceId, String id, boolean standalone) {} - - Server server; - Targets targetProcessor; - Map> targets; - Map> services = new HashMap<>(Map.of( + + Map> channel = new HashMap<>(); + Map> transformer = new HashMap<>(Map.of( "Text", Text::of, "Codeblock", Text::codeblock, "Cutout", Text::cutout, "Turtle", Turtle::of, - "Button", Interaction::button, - "Input", Interaction::input, - "Checkbox", Interaction::checkbox, "Test", Test::test)); + Map> scans = new HashMap<>(Map.of( + "CommandScan", this::consumeCommandScan + )); + List sinks = List.of(); - public Processor(Server server) { - this.server = server; - targetProcessor = Targets.of(server); - targets = Map.of( - "Markdown", targetProcessor::consumeMarkdown, - "Dot", targetProcessor::consumeDot, - "Html", targetProcessor::consumeHTML, - "JavaScript", targetProcessor::consumeJS, - "JavaScriptCall", targetProcessor::consumeJSCall, - "Css", targetProcessor::consumeCss, - "SubViewStyle", targetProcessor::consumeSubViewStyle, - "Clear", targetProcessor::consumeClear); + public Processor() { } void process(Process process, String sourceId) { @@ -74,7 +55,6 @@ void process(Stream input, String sourceId, Process process) { switch (curr) { case Command cmd -> processCommands(cmd, sourceId); case Pipe pipe -> processPipe(pipe, prev, sourceId); - case Scan scan -> processScan(scan, process, sourceId); case Register register -> processRegister(register); case Unknown unknown -> processUnknown(unknown, sourceId); default -> null; @@ -84,14 +64,14 @@ void process(Stream input, String sourceId, Process process) { String processCommands(Command command, String sourceId) { Logger.logDebug("Command: " + command.name() + "{" + command.id() + "}, " + command.content()); - if (targets.containsKey(command.name())) { - targets.get(command.name()).accept(new MetaInformation(sourceId, command.id(), true), command.content()); + if (channel.containsKey(command.name())) { + channel.get(command.name()).accept(new MetaInformation(sourceId, command.id(), true), command.content()); } - else if (services.containsKey(command.name())) { - return services.get(command.name()).apply(new MetaInformation(sourceId, command.id(), true), command.content()); + else if (transformer.containsKey(command.name())) { + return transformer.get(command.name()).apply(new MetaInformation(sourceId, command.id(), true), command.content()); } else { Logger.logError("Command not found: " + command.name()); - targetProcessor.consumeError(new MetaInformation(sourceId, "", true), command.name() + command.content()); + sinks.forEach(s -> s.error(new MetaInformation(sourceId, "", true), command.name() + command.content())); } return null; @@ -103,12 +83,12 @@ String processPipe(Pipe pipe, String input, String sourceId) { Logger.logDebug("Command: " + ref.name() + "{" + ref.id() + "}, " + current); if (current == null) return null; - if (targets.containsKey(ref.name())) { - targets.get(ref.name()).accept(new MetaInformation(sourceId, ref.id(), false), current); + if (channel.containsKey(ref.name())) { + channel.get(ref.name()).accept(new MetaInformation(sourceId, ref.id(), false), current); return null; } - else if (services.containsKey(ref.name())) { - current = services.get(ref.name()).apply(new MetaInformation(sourceId, ref.id(), false), current); + else if (transformer.containsKey(ref.name())) { + current = transformer.get(ref.name()).apply(new MetaInformation(sourceId, ref.id(), false), current); } else { Logger.logError("Command not found: " + ref.name()); } @@ -116,21 +96,21 @@ else if (services.containsKey(ref.name())) { return current; } - String processScan(Scan scan, Process process, String sourceId) { - server.waitingProcesses.put(sourceId, process); - String inputField = HTMLElements.input("input" + scan.id()); - String button = HTMLElements.button("button" + scan.id(), "Send", TextUtils.fillOut(""" - (()=>{ - const input = document.getElementById("input${0}"); - fetch("scan", { method: "post", body: "${1}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); - })() - """, scan.id(), sourceId)); - targetProcessor.consumeHTML(new MetaInformation(sourceId, scan.id(), true), inputField + button); + String consumeCommandScan(MetaInformation meta, Process process, String prev) { + if (prev != null) { + try { + process.getOutputStream().write(prev.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + Logger.logError("Error writeing to output stream of '" + meta.sourceId() + "'", e); + } + } else { + Logger.logError("No previous command output to can."); + } return null; } String processRegister(Register register) { - services.put(register.name(), (meta, content) -> { + transformer.put(register.name(), (meta, content) -> { boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); String out = null; try { @@ -163,13 +143,19 @@ String processRegister(Register register) { } String processUnknown(Unknown unknown, String sourceId) { - targetProcessor.consumeError(new MetaInformation(sourceId, "", true), unknown.message()); + sinks.forEach(s -> s.error(new MetaInformation(sourceId, "", true), unknown.message())); return null; } void init(String sourceId) { - server.clearEvents(sourceId); + sinks.forEach(s -> s.clear(sourceId)); Text.clear(sourceId); } + + void registerSink(Sink sink) { + channel.putAll(sink.registerChannel()); + transformer.putAll(sink.registerTransformer()); + sinks.add(sink); + } } diff --git a/src/main/java/lvp/commands/services/Test.java b/src/main/java/lvp/commands/services/Test.java deleted file mode 100644 index 110a7ef..0000000 --- a/src/main/java/lvp/commands/services/Test.java +++ /dev/null @@ -1,123 +0,0 @@ -package lvp.commands.services; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import lvp.Processor.MetaInformation; -import lvp.skills.TextUtils; -import lvp.skills.logging.Logger; - -public class Test { - private Test() {} - private static final String JSHELL_PROMPT = "jshell>"; - public static String test(MetaInformation meta, String content) { - Map> fields = new HashMap<>(); - String currentKey = null; - - for (String line: content.lines().toList()) { - if (line.isBlank()) continue; - if (line.strip().startsWith("Send:") || line.strip().startsWith("Expect:") || line.strip().startsWith("Type:")) { - String[] parts = line.split(":", 2); - currentKey = parts[0].strip().toLowerCase(); - String value = parts[1].strip(); - fields.computeIfAbsent(currentKey, _ -> new ArrayList<>()); - if (!value.isEmpty()) fields.get(currentKey).add(value); - if (currentKey.equals("type")) currentKey = null; - } else if (currentKey != null) { - fields.get(currentKey).add(line); - } else { - Logger.logError("Unexpected line " + line); - return null; - } - } - String send = String.join("\n", fields.get("send")); - List expect = fields.get("expect"); - List typeL = fields.get("type"); - String type = typeL != null && typeL.size() == 1 ? typeL.getFirst() : "exact"; - - if (send == null || expect == null) { - Logger.logError("Test command requires 'Send' and 'Expect' fields."); - return null; - } - - Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect + ", type=" + type); - String actual = executeJshell(send); - if (actual == null) return "No Result"; - List actualParsed = actual.lines().map(Test::parseJshellOutput).filter(s -> !s.isBlank()).toList(); - - return TextUtils.fillOut(""" - Result for Test ${0}: - Input: ${1} - Response: ${2} - Actual: ${3} - Expected: ${4} - Comparison: '${5}' - Status: ${6} - """, meta.id(), send, actual, actualParsed, expect, type, compare(actualParsed, expect, type) ? "Success" : "Failure"); - } - - private static boolean compare(List actual, List expected, String type) { - return switch(type) { - case "exact" -> actual.equals(expected); - case "oneof" -> expected.stream().allMatch(actual::contains); - case "same" -> actual.size() == expected.size() && new HashSet<>(actual).equals(new HashSet<>(expected)); - default -> { - Logger.logError("Unknown Comparison Type."); - yield false; - } - }; - } - - private static String executeJshell(String send) { - String result = null; - try { - Logger.logInfo("Executing jshell --enable-preview -R-ea"); - ProcessBuilder pb = new ProcessBuilder("jshell", "--enable-preview", "-R-ea") - .redirectErrorStream(true); - Process process = pb.start(); - - try (BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { - writer.write(send + "\n"); - writer.write("/ex"); - writer.flush(); - } - try (var reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - result = reader.lines() - .filter(line -> line.startsWith(JSHELL_PROMPT) && line.strip().length() > JSHELL_PROMPT.length()) - .collect(Collectors.joining("\n")); - } - boolean finished = process.waitFor(10, TimeUnit.SECONDS); - if (!finished) { - process.destroyForcibly(); - Logger.logError("Timeout: process jshell killed"); - } - } catch (Exception e) { - Logger.logError("Error in jshell", e); - Thread.currentThread().interrupt(); - } - return result; - } - - private static String parseJshellOutput(String line) { - int idx = line.indexOf("==>"); - if (idx != -1 && idx + 3 < line.length()) { - return line.substring(idx + 3).strip(); - } else if (line.startsWith(JSHELL_PROMPT + " |")) { - return line.substring(9).strip(); - } - return ""; - } -} diff --git a/src/main/java/lvp/sinks/Sink.java b/src/main/java/lvp/sinks/Sink.java new file mode 100644 index 0000000..ed52a19 --- /dev/null +++ b/src/main/java/lvp/sinks/Sink.java @@ -0,0 +1,16 @@ +package lvp.sinks; + +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import lvp.Processor.MetaInformation; +import lvp.skills.TriConsumer; + +public interface Sink { + void clear(String sourceId); + void error(MetaInformation meta, String message); + Map> registerTransformer(); + Map> registerChannel(); + Map> registerScan(); +} diff --git a/src/main/java/lvp/commands/targets/Targets.java b/src/main/java/lvp/sinks/server_sink/HttpChannel.java similarity index 74% rename from src/main/java/lvp/commands/targets/Targets.java rename to src/main/java/lvp/sinks/server_sink/HttpChannel.java index 54c4100..bae5af8 100644 --- a/src/main/java/lvp/commands/targets/Targets.java +++ b/src/main/java/lvp/sinks/server_sink/HttpChannel.java @@ -1,16 +1,18 @@ -package lvp.commands.targets; +package lvp.sinks.server_sink; + import lvp.Processor.MetaInformation; -import lvp.SSEType; -import lvp.Server; -import lvp.commands.targets.dot.GraphSpec; +import lvp.sinks.server_sink.dot.GraphSpec; +import lvp.skills.HTMLElements; +import lvp.skills.TextUtils; -public class Targets { +public class HttpChannel { Server server; - public static Targets of(Server server) { return new Targets(server); } - private Targets(Server server) { + public static HttpChannel of(Server server) { return new HttpChannel(server); } + + private HttpChannel(Server server) { this.server = server; } @@ -64,5 +66,17 @@ public void consumeDot(MetaInformation meta, String content) { consumeJS(new MetaInformation(meta.sourceId(), "script" + meta.id(), meta.standalone()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + " = new Dot(document.getElementById('dotContainer" + meta.id() + "'), " + specs.width().orElse(500) + ", " + specs.height().orElse(500) + ");"); consumeJSCall(new MetaInformation(meta.sourceId(), "call" + meta.id(), meta.standalone()), "clerk['" + meta.sourceId() + "'].dot" + meta.id() + ".draw(\"" + specs.dot() + "\")"); } + + public void consumeInputScan(MetaInformation meta, Process process, String content) { + server.waitingProcesses.put(meta.sourceId(), process); + String inputField = HTMLElements.input("input" + meta.id()); + String button = HTMLElements.button("button" + meta.id(), "Send", TextUtils.fillOut(""" + (()=>{ + const input = document.getElementById("input${0}"); + fetch("scan", { method: "post", body: "${1}:" + btoa(String.fromCharCode(...new TextEncoder().encode(input.value))) }).catch(console.error); + })() + """, meta.id(), meta.sourceId())); + consumeHTML(meta, inputField + button); + } } diff --git a/src/main/java/lvp/SSEType.java b/src/main/java/lvp/sinks/server_sink/SSEType.java similarity index 66% rename from src/main/java/lvp/SSEType.java rename to src/main/java/lvp/sinks/server_sink/SSEType.java index 8c33d62..1bf9022 100644 --- a/src/main/java/lvp/SSEType.java +++ b/src/main/java/lvp/sinks/server_sink/SSEType.java @@ -1,3 +1,3 @@ -package lvp; +package lvp.sinks.server_sink; public enum SSEType { WRITE, CALL, SCRIPT, CLEAR, CSS, LOG; } \ No newline at end of file diff --git a/src/main/java/lvp/Server.java b/src/main/java/lvp/sinks/server_sink/Server.java similarity index 94% rename from src/main/java/lvp/Server.java rename to src/main/java/lvp/sinks/server_sink/Server.java index 09e22f1..53fbc84 100644 --- a/src/main/java/lvp/Server.java +++ b/src/main/java/lvp/sinks/server_sink/Server.java @@ -1,4 +1,4 @@ -package lvp; +package lvp.sinks.server_sink; import java.io.IOException; import java.io.InputStream; @@ -26,7 +26,7 @@ public class Server { - private record EventMessage(SSEType type, String data, String id, String sourceId) {} + record EventMessage(SSEType type, String data, String id, String sourceId) {} private final HttpServer httpServer; @@ -34,8 +34,8 @@ private record EventMessage(SSEType type, String data, String id, String sourceI static int defaultPort = 50_001; static final String INDEX = "/web/index.html"; - static void setDefaultPort(int port) { defaultPort = port != 0 ? Math.abs(port) : 50_001; } - static int getDefaultPort() { return defaultPort; } + public static void setDefaultPort(int port) { defaultPort = port != 0 ? Math.abs(port) : 50_001; } + public static int getDefaultPort() { return defaultPort; } public final List webClients = new CopyOnWriteArrayList<>(); List events = new CopyOnWriteArrayList<>(); @@ -245,14 +245,6 @@ private String readRequestBody(HttpExchange exchange) throws IOException { return null; } - public void clearEvents(String sourceId) { - events.removeIf(event -> event.sourceId().equals(sourceId)); - if (waitingProcesses.containsKey(sourceId)) { - waitingProcesses.get(sourceId).destroyForcibly(); - waitingProcesses.remove(sourceId); - } - } - public void stop() { Logger.logInfo("Closing Server on port '" + port + "'"); for (HttpExchange connection : webClients) { diff --git a/src/main/java/lvp/sinks/server_sink/ServerSink.java b/src/main/java/lvp/sinks/server_sink/ServerSink.java new file mode 100644 index 0000000..22ad1ca --- /dev/null +++ b/src/main/java/lvp/sinks/server_sink/ServerSink.java @@ -0,0 +1,67 @@ +package lvp.sinks.server_sink; + +import java.io.IOException; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import lvp.Processor.MetaInformation; +import lvp.sinks.Sink; +import lvp.skills.TriConsumer; +import lvp.transformer.Interaction; + +public class ServerSink implements Sink { + + Server server; + HttpChannel channel; + + public ServerSink(int port) throws IOException { + server = new Server(Math.abs(port)); + channel = HttpChannel.of(server); + Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); + } + + + @Override + public Map> registerTransformer() { + return Map.of( + "Button", Interaction::button, + "Input", Interaction::input, + "Checkbox", Interaction::checkbox); + } + @Override + public Map> registerChannel() { + return Map.of( + "Markdown", channel::consumeMarkdown, + "Dot", channel::consumeDot, + "Html", channel::consumeHTML, + "JavaScript", channel::consumeJS, + "JavaScriptCall", channel::consumeJSCall, + "Css", channel::consumeCss, + "SubViewStyle", channel::consumeSubViewStyle, + "Clear", channel::consumeClear); + } + + @Override + public void clear(String sourceId) { + server.events.removeIf(event -> event.sourceId().equals(sourceId)); + if (server.waitingProcesses.containsKey(sourceId)) { + server.waitingProcesses.get(sourceId).destroyForcibly(); + server.waitingProcesses.remove(sourceId); + } + } + + @Override + public void error(MetaInformation meta, String message) { + channel.consumeHTML(meta, message); + } + + + @Override + public Map> registerScan() { + return Map.of( + "InputScan", channel::consumeInputScan + ); + } + +} diff --git a/src/main/java/lvp/commands/targets/dot/GraphSpec.java b/src/main/java/lvp/sinks/server_sink/dot/GraphSpec.java similarity index 96% rename from src/main/java/lvp/commands/targets/dot/GraphSpec.java rename to src/main/java/lvp/sinks/server_sink/dot/GraphSpec.java index a5aca47..7f1b9b4 100644 --- a/src/main/java/lvp/commands/targets/dot/GraphSpec.java +++ b/src/main/java/lvp/sinks/server_sink/dot/GraphSpec.java @@ -1,4 +1,4 @@ -package lvp.commands.targets.dot; +package lvp.sinks.server_sink.dot; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/lvp/commands/targets/dot/dot.js b/src/main/java/lvp/sinks/server_sink/dot/dot.js similarity index 100% rename from src/main/java/lvp/commands/targets/dot/dot.js rename to src/main/java/lvp/sinks/server_sink/dot/dot.js diff --git a/src/main/java/lvp/commands/targets/dot/vis-network.min.js b/src/main/java/lvp/sinks/server_sink/dot/vis-network.min.js similarity index 100% rename from src/main/java/lvp/commands/targets/dot/vis-network.min.js rename to src/main/java/lvp/sinks/server_sink/dot/vis-network.min.js diff --git a/src/main/java/lvp/commands/targets/markdown/CompileMathjax3.md b/src/main/java/lvp/sinks/server_sink/markdown/CompileMathjax3.md similarity index 100% rename from src/main/java/lvp/commands/targets/markdown/CompileMathjax3.md rename to src/main/java/lvp/sinks/server_sink/markdown/CompileMathjax3.md diff --git a/src/main/java/lvp/commands/targets/markdown/default.min.css b/src/main/java/lvp/sinks/server_sink/markdown/default.min.css similarity index 100% rename from src/main/java/lvp/commands/targets/markdown/default.min.css rename to src/main/java/lvp/sinks/server_sink/markdown/default.min.css diff --git a/src/main/java/lvp/commands/targets/markdown/highlight.min.js b/src/main/java/lvp/sinks/server_sink/markdown/highlight.min.js similarity index 100% rename from src/main/java/lvp/commands/targets/markdown/highlight.min.js rename to src/main/java/lvp/sinks/server_sink/markdown/highlight.min.js diff --git a/src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js b/src/main/java/lvp/sinks/server_sink/markdown/interactiveCodeblocks.js similarity index 100% rename from src/main/java/lvp/commands/targets/markdown/interactiveCodeblocks.js rename to src/main/java/lvp/sinks/server_sink/markdown/interactiveCodeblocks.js diff --git a/src/main/java/lvp/commands/targets/markdown/markdown-it.min.js b/src/main/java/lvp/sinks/server_sink/markdown/markdown-it.min.js similarity index 100% rename from src/main/java/lvp/commands/targets/markdown/markdown-it.min.js rename to src/main/java/lvp/sinks/server_sink/markdown/markdown-it.min.js diff --git a/src/main/java/lvp/commands/targets/markdown/mathjax3.js b/src/main/java/lvp/sinks/server_sink/markdown/mathjax3.js similarity index 100% rename from src/main/java/lvp/commands/targets/markdown/mathjax3.js rename to src/main/java/lvp/sinks/server_sink/markdown/mathjax3.js diff --git a/src/main/java/lvp/commands/targets/markdown/vs.css b/src/main/java/lvp/sinks/server_sink/markdown/vs.css similarity index 100% rename from src/main/java/lvp/commands/targets/markdown/vs.css rename to src/main/java/lvp/sinks/server_sink/markdown/vs.css diff --git a/src/main/java/lvp/skills/TriConsumer.java b/src/main/java/lvp/skills/TriConsumer.java new file mode 100644 index 0000000..af264fe --- /dev/null +++ b/src/main/java/lvp/skills/TriConsumer.java @@ -0,0 +1,6 @@ +package lvp.skills; + +@FunctionalInterface +public interface TriConsumer { + void accept(T t, U u, V v); +} diff --git a/src/main/java/lvp/skills/parser/InstructionParser.java b/src/main/java/lvp/skills/parser/InstructionParser.java index 87cbe93..80f8c0e 100644 --- a/src/main/java/lvp/skills/parser/InstructionParser.java +++ b/src/main/java/lvp/skills/parser/InstructionParser.java @@ -17,11 +17,10 @@ public class InstructionParser { // ---- Instruction Types ---- - public sealed interface Instruction permits Command, Register, Scan, Pipe, Unknown {} + public sealed interface Instruction permits Command, Register, Pipe, Unknown {} public record Command(String name, String id, String content) implements Instruction {} public record Register(String name, String call, boolean skipId) implements Instruction {} - public record Scan(String id) implements Instruction {} public record Pipe(List commands) implements Instruction {} public record Unknown(String message) implements Instruction {} @@ -31,7 +30,6 @@ public record CommandRef(String name, String id) {} private static final Pattern SINGLE_LINE_COMMAND = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?:\\s*(.+)$"); private static final Pattern BLOCK_START = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?:\\s*$"); private static final Pattern SINGLE_LINE_COMMAND_CONTENTLESS = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?\\s*$"); - private static final Pattern SCAN = Pattern.compile("^Scan(?:\\[([^}]+)\\])?:\\s*$"); private static final Pattern REGISTER = Pattern.compile("^Register(?:\\[([^}]+)\\])?:\\s+(\\w+)\\s+(.+)$"); private static final Pattern PIPE_LINE = Pattern.compile("^\\s*\\|(.+)$"); private static final Pattern PIPE_ENTRY = Pattern.compile("^(\\w+)(?:\\[([^}]+)\\])?$"); @@ -84,7 +82,6 @@ private static void handleLine(BlockState state, String line, Downstream return true; } - private static boolean tryScan(String line, Downstream out) { - Matcher matcher = SCAN.matcher(line); - if (!matcher.matches()) return false; - - String id = matcher.group(1) == null ? IdGen.generateID(10) : matcher.group(1); - Logger.logDebug("Parsed Read" + formatFlag(id)); - out.push(new Scan(id)); - return true; - } - private static boolean trySingleCommand(String line, Downstream out) { Matcher matcher = SINGLE_LINE_COMMAND.matcher(line).matches() ? SINGLE_LINE_COMMAND.matcher(line) : SINGLE_LINE_COMMAND_CONTENTLESS.matcher(line); if (!matcher.matches()) return false; diff --git a/src/main/java/lvp/skills/parser/TurtleParser.java b/src/main/java/lvp/skills/parser/TurtleParser.java index 254c9f7..cb1ae0e 100644 --- a/src/main/java/lvp/skills/parser/TurtleParser.java +++ b/src/main/java/lvp/skills/parser/TurtleParser.java @@ -5,8 +5,8 @@ import java.util.regex.Pattern; import java.util.stream.Stream; -import lvp.commands.services.Turtle; import lvp.skills.logging.Logger; +import lvp.transformer.Turtle; public class TurtleParser { private TurtleParser() {} diff --git a/src/main/java/lvp/commands/services/Interaction.java b/src/main/java/lvp/transformer/Interaction.java similarity index 99% rename from src/main/java/lvp/commands/services/Interaction.java rename to src/main/java/lvp/transformer/Interaction.java index 6f2bef9..614d2e0 100644 --- a/src/main/java/lvp/commands/services/Interaction.java +++ b/src/main/java/lvp/transformer/Interaction.java @@ -1,4 +1,4 @@ -package lvp.commands.services; +package lvp.transformer; import java.nio.charset.StandardCharsets; import java.nio.file.Path; diff --git a/src/main/java/lvp/commands/services/Text.java b/src/main/java/lvp/transformer/Text.java similarity index 98% rename from src/main/java/lvp/commands/services/Text.java rename to src/main/java/lvp/transformer/Text.java index 449bd01..7a0c0a1 100644 --- a/src/main/java/lvp/commands/services/Text.java +++ b/src/main/java/lvp/transformer/Text.java @@ -1,4 +1,4 @@ -package lvp.commands.services; +package lvp.transformer; import java.util.Arrays; import java.util.Iterator; diff --git a/src/main/java/lvp/commands/services/Turtle.java b/src/main/java/lvp/transformer/Turtle.java similarity index 99% rename from src/main/java/lvp/commands/services/Turtle.java rename to src/main/java/lvp/transformer/Turtle.java index 4f670ee..a99457c 100644 --- a/src/main/java/lvp/commands/services/Turtle.java +++ b/src/main/java/lvp/transformer/Turtle.java @@ -1,4 +1,4 @@ -package lvp.commands.services; +package lvp.transformer; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; diff --git a/syntax.md b/syntax.md index 30b4e2e..a976607 100644 --- a/syntax.md +++ b/syntax.md @@ -35,6 +35,23 @@ CALL ::= STRING PIPE ::= '|' COMMAND ['|' COMMAND '|' ...] ``` +## Targets + +- Markdown +- Html +- JavaScript +- JavaScriptCall +- Clear +- Dot + +### Dot +``` +Dot: +[width: WIDTH] +[height: HEIGHT] +GRAPH +~~~ +``` ## Default Services @@ -111,20 +128,3 @@ checked: BOOLEAN ~~~ ``` -## Targets - -- Markdown -- Html -- JavaScript -- JavaScriptCall -- Clear -- Dot - -### Dot -``` -Dot: -[width: WIDTH] -[height: HEIGHT] -GRAPH -~~~ -``` \ No newline at end of file From 46f70ac489cca1fb1300dedba1a1581456a2243d Mon Sep 17 00:00:00 2001 From: Ramon Date: Sat, 19 Jul 2025 21:15:27 +0200 Subject: [PATCH 42/50] Scans --- scantest.java | 24 ++++++++ src/main/java/lvp/Processor.java | 60 ++++++++++--------- .../java/lvp/sinks/server_sink/Server.java | 11 ++-- src/main/java/lvp/skills/Scan.java | 16 +++++ src/main/resources/web/index.html | 14 ++--- 5 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 scantest.java create mode 100644 src/main/java/lvp/skills/Scan.java diff --git a/scantest.java b/scantest.java new file mode 100644 index 0000000..2eaca59 --- /dev/null +++ b/scantest.java @@ -0,0 +1,24 @@ +import java.util.Scanner; + +void main() { + + Scanner scanner = new Scanner(System.in); + println("Clear"); + println("Markdown[test]: # Hello World"); + println(""" + Text: Hello There + | CommandScan + """); + String input = scanner.nextLine(); + println("Markdown: " + input); + println(""" + InputScan + """); + String newInput = scanner.nextLine(); + println("Markdown: " + newInput); + println(""" + InputScan + """); + String newInput2 = scanner.nextLine(); + println("Markdown: " + newInput2); +} \ No newline at end of file diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index c4de374..4070333 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -6,6 +6,7 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,6 +18,7 @@ import java.util.stream.Stream; import lvp.sinks.Sink; +import lvp.skills.Scan; import lvp.skills.TriConsumer; import lvp.skills.logging.Logger; import lvp.skills.parser.InstructionParser; @@ -35,7 +37,7 @@ public record MetaInformation(String sourceId, String id, boolean standalone) {} Map> scans = new HashMap<>(Map.of( "CommandScan", this::consumeCommandScan )); - List sinks = List.of(); + List sinks = new ArrayList<>(); public Processor() { } @@ -53,55 +55,58 @@ void process(Process process, String sourceId) { void process(Stream input, String sourceId, Process process) { InstructionParser.parse(input).gather(Gatherers.fold(() -> "", (prev, curr) -> switch (curr) { - case Command cmd -> processCommands(cmd, sourceId); - case Pipe pipe -> processPipe(pipe, prev, sourceId); + case Command cmd -> processCommands(cmd, sourceId, process); + case Pipe pipe -> processPipe(pipe, prev, sourceId, process); case Register register -> processRegister(register); case Unknown unknown -> processUnknown(unknown, sourceId); default -> null; })).forEachOrdered(_->{}); } - String processCommands(Command command, String sourceId) { + String processCommands(Command command, String sourceId, Process process) { Logger.logDebug("Command: " + command.name() + "{" + command.id() + "}, " + command.content()); - - if (channel.containsKey(command.name())) { - channel.get(command.name()).accept(new MetaInformation(sourceId, command.id(), true), command.content()); - } - else if (transformer.containsKey(command.name())) { - return transformer.get(command.name()).apply(new MetaInformation(sourceId, command.id(), true), command.content()); - } else { - Logger.logError("Command not found: " + command.name()); - sinks.forEach(s -> s.error(new MetaInformation(sourceId, "", true), command.name() + command.content())); - } + MetaInformation meta = new MetaInformation(sourceId, command.id(), true); - return null; + return executeCommand(command.name(), command.content(), meta, process); } - String processPipe(Pipe pipe, String input, String sourceId) { + String processPipe(Pipe pipe, String input, String sourceId, Process process) { String current = input; for (CommandRef ref : pipe.commands()) { Logger.logDebug("Command: " + ref.name() + "{" + ref.id() + "}, " + current); if (current == null) return null; + MetaInformation meta = new MetaInformation(sourceId, ref.id(), false); - if (channel.containsKey(ref.name())) { - channel.get(ref.name()).accept(new MetaInformation(sourceId, ref.id(), false), current); - return null; - } - else if (transformer.containsKey(ref.name())) { - current = transformer.get(ref.name()).apply(new MetaInformation(sourceId, ref.id(), false), current); - } else { - Logger.logError("Command not found: " + ref.name()); - } + current = executeCommand(ref.name(), current, meta, process); } return current; } + String executeCommand(String name, String content, MetaInformation meta, Process process) { + if (channel.containsKey(name)) { + channel.get(name).accept(meta, content); + return null; + } + else if (transformer.containsKey(name)) { + return transformer.get(name).apply(meta, content); + } + else if (scans.containsKey(name)) { + scans.get(name).accept(meta, process, content); + return null; + } + else { + Logger.logError("Command not found: " + name); + sinks.forEach(s -> s.error(meta, name + content)); + } + return null; + } + String consumeCommandScan(MetaInformation meta, Process process, String prev) { if (prev != null) { try { - process.getOutputStream().write(prev.getBytes(StandardCharsets.UTF_8)); + Scan.sendToSource(process, prev); } catch (IOException e) { - Logger.logError("Error writeing to output stream of '" + meta.sourceId() + "'", e); + Logger.logError("Error while writing in OutputStream for " + meta.sourceId(), e); } } else { Logger.logError("No previous command output to can."); @@ -155,6 +160,7 @@ void init(String sourceId) { void registerSink(Sink sink) { channel.putAll(sink.registerChannel()); transformer.putAll(sink.registerTransformer()); + scans.putAll(sink.registerScan()); sinks.add(sink); } diff --git a/src/main/java/lvp/sinks/server_sink/Server.java b/src/main/java/lvp/sinks/server_sink/Server.java index 53fbc84..78353a0 100644 --- a/src/main/java/lvp/sinks/server_sink/Server.java +++ b/src/main/java/lvp/sinks/server_sink/Server.java @@ -19,6 +19,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; +import lvp.skills.Scan; import lvp.skills.TextUtils; import lvp.skills.TextUtils.ReplacementType; import lvp.skills.logging.LogLevel; @@ -91,18 +92,16 @@ private void handleScan(HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(200, 0); exchange.close(); - OutputStream stream = waitingProcesses.get(parts[0]).getOutputStream(); - if (stream == null) { - Logger.logError("Stream not found: " + message); + Process process = waitingProcesses.get(parts[0]); + if (process == null) { + Logger.logError("Process not found: " + message); return; } try { - stream.write(Base64.getDecoder().decode(parts[1])); - stream.flush(); + Scan.sendToSource(process, new String(Base64.getDecoder().decode(parts[1]), StandardCharsets.UTF_8)); } catch (IOException e) { Logger.logError("Error while writing stream for: " + parts[0], e); } finally { - stream.close(); waitingProcesses.remove(parts[0]); } diff --git a/src/main/java/lvp/skills/Scan.java b/src/main/java/lvp/skills/Scan.java new file mode 100644 index 0000000..17b67d3 --- /dev/null +++ b/src/main/java/lvp/skills/Scan.java @@ -0,0 +1,16 @@ +package lvp.skills; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; + +public class Scan { + private Scan() {} + public static void sendToSource(Process source, String content) throws IOException { + BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(source.getOutputStream(), StandardCharsets.UTF_8)); + writer.write(content + "\n"); + writer.flush(); + } +} diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index f36b5e5..094e13e 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -6,13 +6,13 @@ Clerk in Java Prototype - - - - - - - + + + + + + + From 8d33b7924864ec881715c32147e74cceea3971c4 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sat, 19 Jul 2025 21:33:14 +0200 Subject: [PATCH 43/50] errors to error channel --- src/main/java/lvp/sinks/server_sink/ServerSink.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/lvp/sinks/server_sink/ServerSink.java b/src/main/java/lvp/sinks/server_sink/ServerSink.java index 22ad1ca..aafd74a 100644 --- a/src/main/java/lvp/sinks/server_sink/ServerSink.java +++ b/src/main/java/lvp/sinks/server_sink/ServerSink.java @@ -53,7 +53,7 @@ public void clear(String sourceId) { @Override public void error(MetaInformation meta, String message) { - channel.consumeHTML(meta, message); + channel.consumeError(meta, message); } From 30ae0074f1d5c7f5c94ebed1457391e74dd6d717 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sat, 19 Jul 2025 21:35:14 +0200 Subject: [PATCH 44/50] fixed gitignore --- .gitignore | 2 +- src/main/java/lvp/transformer/Test.java | 122 ++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/java/lvp/transformer/Test.java diff --git a/.gitignore b/.gitignore index bdd3865..22fcfbc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ target/ build test.java Test.java -!**/services/Test.java +!**/transformer/Test.java .vscode sources.json diff --git a/src/main/java/lvp/transformer/Test.java b/src/main/java/lvp/transformer/Test.java new file mode 100644 index 0000000..deeaadf --- /dev/null +++ b/src/main/java/lvp/transformer/Test.java @@ -0,0 +1,122 @@ +package lvp.transformer; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lvp.Processor.MetaInformation; +import lvp.skills.TextUtils; +import lvp.skills.logging.Logger; + +public class Test { + private Test() {} + private static final String JSHELL_PROMPT = "jshell>"; + public static String test(MetaInformation meta, String content) { + Map> fields = new HashMap<>(); + String currentKey = null; + + for (String line: content.lines().toList()) { + if (line.isBlank()) continue; + if (line.strip().startsWith("Send:") || line.strip().startsWith("Expect:") || line.strip().startsWith("Type:")) { + String[] parts = line.split(":", 2); + currentKey = parts[0].strip().toLowerCase(); + String value = parts[1].strip(); + fields.computeIfAbsent(currentKey, _ -> new ArrayList<>()); + if (!value.isEmpty()) fields.get(currentKey).add(value); + if (currentKey.equals("type")) currentKey = null; + } else if (currentKey != null) { + fields.get(currentKey).add(line); + } else { + Logger.logError("Unexpected line " + line); + return null; + } + } + String send = String.join("\n", fields.get("send")); + List expect = fields.get("expect"); + List typeL = fields.get("type"); + String type = typeL != null && typeL.size() == 1 ? typeL.getFirst() : "exact"; + + if (send == null || expect == null) { + Logger.logError("Test command requires 'Send' and 'Expect' fields."); + return null; + } + + Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect + ", type=" + type); + String actual = executeJshell(send); + if (actual == null) return "No Result"; + List actualParsed = actual.lines().map(Test::parseJshellOutput).filter(s -> !s.isBlank()).toList(); + + return TextUtils.fillOut(""" + Result for Test ${0}: + Input: ${1} + Response: ${2} + Actual: ${3} + Expected: ${4} + Comparison: '${5}' + Status: ${6} + """, meta.id(), send, actual, actualParsed, expect, type, compare(actualParsed, expect, type) ? "Success" : "Failure"); + } + + private static boolean compare(List actual, List expected, String type) { + return switch(type) { + case "exact" -> actual.equals(expected); + case "oneof" -> expected.stream().allMatch(actual::contains); + case "same" -> actual.size() == expected.size() && new HashSet<>(actual).equals(new HashSet<>(expected)); + default -> { + Logger.logError("Unknown Comparison Type."); + yield false; + } + }; + } + + private static String executeJshell(String send) { + String result = null; + try { + Logger.logInfo("Executing jshell --enable-preview -R-ea"); + ProcessBuilder pb = new ProcessBuilder("jshell", "--enable-preview", "-R-ea") + .redirectErrorStream(true); + Process process = pb.start(); + + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(send + "\n"); + writer.write("/ex"); + writer.flush(); + } + try (var reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + result = reader.lines() + .filter(line -> line.startsWith(JSHELL_PROMPT) && line.strip().length() > JSHELL_PROMPT.length()) + .collect(Collectors.joining("\n")); + } + boolean finished = process.waitFor(10, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + Logger.logError("Timeout: process jshell killed"); + } + } catch (Exception e) { + Logger.logError("Error in jshell", e); + Thread.currentThread().interrupt(); + } + return result; + } + + private static String parseJshellOutput(String line) { + int idx = line.indexOf("==>"); + if (idx != -1 && idx + 3 < line.length()) { + return line.substring(idx + 3).strip(); + } else if (line.startsWith(JSHELL_PROMPT + " |")) { + return line.substring(9).strip(); + } + return ""; + } +} From 57685b702ccf1295ca29301c45061aee939f1995 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 6 Aug 2025 08:51:54 +0200 Subject: [PATCH 45/50] Removed Commands from Input Shell --- src/main/java/lvp/Main.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index dd6382b..cf29728 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -55,9 +55,7 @@ public static void main(String[] args) { try { input = scanner.nextLine().strip(); } catch (Exception _) { break;} if (input.startsWith("/")) handleServerCommands(input.substring(1).strip()); - else if (!input.isBlank() && !input.startsWith("Scan")) { - processor.process(Stream.of(input), Base64.getUrlEncoder().withoutPadding().encodeToString("stdin".getBytes(StandardCharsets.UTF_8)), null); - } else { + else { System.err.println("Error: Invalid command. Use '/help' for available commands."); } } From 5e10a4e9fd487a58f338dd1a5508c67f68a249c5 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 6 Aug 2025 08:52:04 +0200 Subject: [PATCH 46/50] fixed register demo --- registerdemo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registerdemo.java b/registerdemo.java index b33496c..a36434c 100644 --- a/registerdemo.java +++ b/registerdemo.java @@ -5,7 +5,7 @@ void main() { Register: Reverse java --enable-preview external.java Reverse: Hello World | Markdown - Register{skipId}: Wc wc + Register[skipId]: Wc wc Wc: Hello World Test From 51de72e0117019c26d9849016ae3b6a4e4ff3e7d Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 6 Aug 2025 08:52:28 +0200 Subject: [PATCH 47/50] Simplified Processor --- src/main/java/lvp/Processor.java | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 4070333..9481a3c 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -39,30 +39,24 @@ public record MetaInformation(String sourceId, String id, boolean standalone) {} )); List sinks = new ArrayList<>(); - public Processor() { - } - void process(Process process, String sourceId) { try(BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - process(reader.lines(), sourceId, process); + + InstructionParser.parse(reader.lines()).gather(Gatherers.fold(() -> "", (prev, curr) -> + switch (curr) { + case Command cmd -> processCommands(cmd, sourceId, process); + case Pipe pipe -> processPipe(pipe, prev, sourceId, process); + case Register register -> processRegister(register); + case Unknown unknown -> processUnknown(unknown, sourceId); + default -> null; + })).forEachOrdered(_->{}); } catch (Exception e) { Logger.logError("Error reading process output: " + e.getMessage(), e); } } - void process(Stream input, String sourceId, Process process) { - InstructionParser.parse(input).gather(Gatherers.fold(() -> "", (prev, curr) -> - switch (curr) { - case Command cmd -> processCommands(cmd, sourceId, process); - case Pipe pipe -> processPipe(pipe, prev, sourceId, process); - case Register register -> processRegister(register); - case Unknown unknown -> processUnknown(unknown, sourceId); - default -> null; - })).forEachOrdered(_->{}); - } - String processCommands(Command command, String sourceId, Process process) { Logger.logDebug("Command: " + command.name() + "{" + command.id() + "}, " + command.content()); MetaInformation meta = new MetaInformation(sourceId, command.id(), true); From 35142ac12f3b6bcdae30d2d2506130d356e749d3 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 22 Sep 2025 09:25:22 +0200 Subject: [PATCH 48/50] adjusted naming --- src/main/java/lvp/FileWatcher.java | 12 +- src/main/java/lvp/Main.java | 48 +++---- src/main/java/lvp/Processor.java | 15 +-- .../Interaction.java | 2 +- .../lvp/{transformer => services}/Text.java | 2 +- .../lvp/{transformer => services}/Turtle.java | 2 +- .../java/lvp/sinks/server_sink/Server.java | 16 ++- .../lvp/sinks/server_sink/ServerSink.java | 2 +- .../java/lvp/skills/parser/TurtleParser.java | 2 +- src/main/java/lvp/transformer/Test.java | 122 ------------------ 10 files changed, 48 insertions(+), 175 deletions(-) rename src/main/java/lvp/{transformer => services}/Interaction.java (99%) rename src/main/java/lvp/{transformer => services}/Text.java (98%) rename src/main/java/lvp/{transformer => services}/Turtle.java (99%) delete mode 100644 src/main/java/lvp/transformer/Test.java diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 15888e5..0f4be00 100644 --- a/src/main/java/lvp/FileWatcher.java +++ b/src/main/java/lvp/FileWatcher.java @@ -74,7 +74,7 @@ private Stream getFolderTree() { .flatMap(root -> { try { return Files.find(root, Integer.MAX_VALUE, - (_, attrs) -> attrs.isDirectory()).filter(p -> !p.toString().contains(".git")); + (_, attrs) -> attrs.isDirectory()).filter(p -> !p.toString().startsWith(".")); } catch (IOException e) { Logger.logError("Error walking directory: " + root.toAbsolutePath(), e); return Stream.empty(); @@ -109,11 +109,11 @@ private void watchLoop() { private void processWatchKeyEvents(WatchKey key) { for (WatchEvent ev : key.pollEvents()) { - Path changed = (Path) ev.context(); - if (Files.isDirectory(changed)) continue; + Path changedFile = (Path) ev.context(); + if (Files.isDirectory(changedFile)) continue; - Path dir = (Path) key.watchable(); - Path fullPath = dir.resolve(changed).normalize().toAbsolutePath(); + Path watchedDir = (Path) key.watchable(); + Path fullPath = watchedDir.resolve(changedFile).normalize().toAbsolutePath(); Instant now = Instant.now(); Instant last = lastModified.getOrDefault(fullPath, Instant.EPOCH); @@ -128,7 +128,7 @@ private void processWatchKeyEvents(WatchKey key) { Logger.logInfo("Event for source: " + fullPath + " (" + ev.kind().name() + ")"); executor.submit(() -> run(source.get())); } - else if (!sourceOnly && (watchFilter.isEmpty() || watchFilter.get().matches(changed))) { + else if (!sourceOnly && (watchFilter.isEmpty() || watchFilter.get().matches(changedFile))) { Logger.logInfo("Event for file: " + fullPath + " (" + ev.kind().name() + ")"); execute(sources); } diff --git a/src/main/java/lvp/Main.java b/src/main/java/lvp/Main.java index cf29728..5411de7 100644 --- a/src/main/java/lvp/Main.java +++ b/src/main/java/lvp/Main.java @@ -1,17 +1,13 @@ package lvp; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.Scanner; import java.util.regex.Matcher; -import java.util.stream.Stream; - import lvp.sinks.server_sink.Server; import lvp.sinks.server_sink.ServerSink; import lvp.skills.logging.LogLevel; @@ -28,7 +24,7 @@ public class Main { private record Config(List sources, int port, LogLevel logLevel, Optional watchFilter, boolean sourceOnly){} - private static final Path LVP_CONFIG_PATH = Path.of("./sources.json"); + private static final Path LVP_SOURCES_PATH = Path.of("./sources.json"); public static void main(String[] args) { Config cfg = parseArgs(args); @@ -91,6 +87,7 @@ private static void handleServerCommands(String command) { } } + // Documentation in README private static Config parseArgs(String[] args) { List files = new ArrayList<>(); Optional cmd = Optional.empty(); @@ -106,27 +103,19 @@ private static Config parseArgs(String[] args) { String value = parts.length > 1 ? parts[1].strip() : ""; switch (key) { - case "-l", "--log": - logLevel = value.isBlank() ? LogLevel.Info : LogLevel.fromString(value); - break; - case "--port", "-p": - try { port = Integer.parseInt(value); } catch(NumberFormatException _) {} - break; - case "--cmd": - cmd = value.isBlank() ? Optional.empty() : Optional.of(value); - break; - case "--config", "-c": - sources = loadConfig(); - break; - case "--watch-filter", "-w": - watchFilter = value.isBlank() ? Optional.empty() : Optional.of(value); - break; - case "--source-only", "-s": - sourceOnly = true; - break; - default: + case "-l", "--log" -> logLevel = value.isBlank() ? LogLevel.Info : LogLevel.fromString(value); + case "--port", "-p" -> { + try { port = Integer.parseInt(value); } catch(NumberFormatException _) { + System.err.println("Error: Invalid port number. Not a number: " + value); + } + } + case "--cmd" -> cmd = value.isBlank() ? Optional.empty() : Optional.of(value); + case "--config", "-c" -> sources = loadWatchConfig(); + case "--watch-filter", "-w" -> watchFilter = value.isBlank() ? Optional.empty() : Optional.of(value); + case "--source-only", "-s" -> sourceOnly = true; + default -> { if (!arg.isBlank()) files.add(arg.strip()); - break; + } } } @@ -162,14 +151,15 @@ private static Optional> getFilePaths(List files) { return paths.isEmpty() ? Optional.empty() : Optional.of(paths); } - private static Optional> loadConfig() { - if (!Files.isRegularFile(LVP_CONFIG_PATH) || !Files.exists(LVP_CONFIG_PATH)) { - Logger.logError("Config not found at: " + LVP_CONFIG_PATH.normalize().toAbsolutePath()); + private static Optional> loadWatchConfig() { + if (!Files.isRegularFile(LVP_SOURCES_PATH) || !Files.exists(LVP_SOURCES_PATH)) { + Logger.logError("Config not found at: " + LVP_SOURCES_PATH.normalize().toAbsolutePath()); return Optional.empty(); } - return ConfigParser.parse(LVP_CONFIG_PATH); + return ConfigParser.parse(LVP_SOURCES_PATH); } + // Returns false only, when a new version is available. If the version can't be checked, it will return true public static boolean isLatestRelease() { try (HttpClient client = HttpClient.newHttpClient()) { HttpRequest request = HttpRequest.newBuilder() diff --git a/src/main/java/lvp/Processor.java b/src/main/java/lvp/Processor.java index 9481a3c..1c4e72f 100644 --- a/src/main/java/lvp/Processor.java +++ b/src/main/java/lvp/Processor.java @@ -15,20 +15,19 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.Gatherers; -import java.util.stream.Stream; +import lvp.services.*; import lvp.sinks.Sink; import lvp.skills.Scan; import lvp.skills.TriConsumer; import lvp.skills.logging.Logger; import lvp.skills.parser.InstructionParser; import lvp.skills.parser.InstructionParser.*; -import lvp.transformer.*; public class Processor { public record MetaInformation(String sourceId, String id, boolean standalone) {} Map> channel = new HashMap<>(); - Map> transformer = new HashMap<>(Map.of( + Map> services = new HashMap<>(Map.of( "Text", Text::of, "Codeblock", Text::codeblock, "Cutout", Text::cutout, @@ -50,7 +49,7 @@ void process(Process process, String sourceId) { case Register register -> processRegister(register); case Unknown unknown -> processUnknown(unknown, sourceId); default -> null; - })).forEachOrdered(_->{}); + })).forEachOrdered(_ -> {}); } catch (Exception e) { Logger.logError("Error reading process output: " + e.getMessage(), e); @@ -81,8 +80,8 @@ String executeCommand(String name, String content, MetaInformation meta, Process channel.get(name).accept(meta, content); return null; } - else if (transformer.containsKey(name)) { - return transformer.get(name).apply(meta, content); + else if (services.containsKey(name)) { + return services.get(name).apply(meta, content); } else if (scans.containsKey(name)) { scans.get(name).accept(meta, process, content); @@ -109,7 +108,7 @@ String consumeCommandScan(MetaInformation meta, Process process, String prev) { } String processRegister(Register register) { - transformer.put(register.name(), (meta, content) -> { + services.put(register.name(), (meta, content) -> { boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); String out = null; try { @@ -153,7 +152,7 @@ void init(String sourceId) { void registerSink(Sink sink) { channel.putAll(sink.registerChannel()); - transformer.putAll(sink.registerTransformer()); + services.putAll(sink.registerTransformer()); scans.putAll(sink.registerScan()); sinks.add(sink); } diff --git a/src/main/java/lvp/transformer/Interaction.java b/src/main/java/lvp/services/Interaction.java similarity index 99% rename from src/main/java/lvp/transformer/Interaction.java rename to src/main/java/lvp/services/Interaction.java index 614d2e0..564c384 100644 --- a/src/main/java/lvp/transformer/Interaction.java +++ b/src/main/java/lvp/services/Interaction.java @@ -1,4 +1,4 @@ -package lvp.transformer; +package lvp.services; import java.nio.charset.StandardCharsets; import java.nio.file.Path; diff --git a/src/main/java/lvp/transformer/Text.java b/src/main/java/lvp/services/Text.java similarity index 98% rename from src/main/java/lvp/transformer/Text.java rename to src/main/java/lvp/services/Text.java index 7a0c0a1..38d2de3 100644 --- a/src/main/java/lvp/transformer/Text.java +++ b/src/main/java/lvp/services/Text.java @@ -1,4 +1,4 @@ -package lvp.transformer; +package lvp.services; import java.util.Arrays; import java.util.Iterator; diff --git a/src/main/java/lvp/transformer/Turtle.java b/src/main/java/lvp/services/Turtle.java similarity index 99% rename from src/main/java/lvp/transformer/Turtle.java rename to src/main/java/lvp/services/Turtle.java index c7ab0f3..3cdbd07 100644 --- a/src/main/java/lvp/transformer/Turtle.java +++ b/src/main/java/lvp/services/Turtle.java @@ -1,4 +1,4 @@ -package lvp.transformer; +package lvp.services; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/lvp/sinks/server_sink/Server.java b/src/main/java/lvp/sinks/server_sink/Server.java index 78353a0..af4b54a 100644 --- a/src/main/java/lvp/sinks/server_sink/Server.java +++ b/src/main/java/lvp/sinks/server_sink/Server.java @@ -168,11 +168,17 @@ private void handleRoot(HttpExchange exchange) throws IOException { Logger.logDebug("Sending '" + resourcePath + "'"); try (final InputStream stream = Server.class.getResourceAsStream(resourcePath)) { - final byte[] bytes = stream.readAllBytes(); - exchange.getResponseHeaders().add("Content-Type", Files.probeContentType(Path.of(resourcePath)) + "; charset=utf-8"); - exchange.sendResponseHeaders(200, bytes.length); - exchange.getResponseBody().write(bytes); - exchange.getResponseBody().flush(); + + if (stream == null) { + exchange.sendResponseHeaders(404, -1); + } + else { + final byte[] bytes = stream.readAllBytes(); + exchange.getResponseHeaders().add("Content-Type", Files.probeContentType(Path.of(resourcePath)) + "; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + exchange.getResponseBody().write(bytes); + } + exchange.getResponseBody().flush(); } finally { exchange.close(); } diff --git a/src/main/java/lvp/sinks/server_sink/ServerSink.java b/src/main/java/lvp/sinks/server_sink/ServerSink.java index aafd74a..bd734cf 100644 --- a/src/main/java/lvp/sinks/server_sink/ServerSink.java +++ b/src/main/java/lvp/sinks/server_sink/ServerSink.java @@ -6,9 +6,9 @@ import java.util.function.BiFunction; import lvp.Processor.MetaInformation; +import lvp.services.Interaction; import lvp.sinks.Sink; import lvp.skills.TriConsumer; -import lvp.transformer.Interaction; public class ServerSink implements Sink { diff --git a/src/main/java/lvp/skills/parser/TurtleParser.java b/src/main/java/lvp/skills/parser/TurtleParser.java index cb1ae0e..c5bfa72 100644 --- a/src/main/java/lvp/skills/parser/TurtleParser.java +++ b/src/main/java/lvp/skills/parser/TurtleParser.java @@ -5,8 +5,8 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import lvp.services.Turtle; import lvp.skills.logging.Logger; -import lvp.transformer.Turtle; public class TurtleParser { private TurtleParser() {} diff --git a/src/main/java/lvp/transformer/Test.java b/src/main/java/lvp/transformer/Test.java deleted file mode 100644 index deeaadf..0000000 --- a/src/main/java/lvp/transformer/Test.java +++ /dev/null @@ -1,122 +0,0 @@ -package lvp.transformer; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import lvp.Processor.MetaInformation; -import lvp.skills.TextUtils; -import lvp.skills.logging.Logger; - -public class Test { - private Test() {} - private static final String JSHELL_PROMPT = "jshell>"; - public static String test(MetaInformation meta, String content) { - Map> fields = new HashMap<>(); - String currentKey = null; - - for (String line: content.lines().toList()) { - if (line.isBlank()) continue; - if (line.strip().startsWith("Send:") || line.strip().startsWith("Expect:") || line.strip().startsWith("Type:")) { - String[] parts = line.split(":", 2); - currentKey = parts[0].strip().toLowerCase(); - String value = parts[1].strip(); - fields.computeIfAbsent(currentKey, _ -> new ArrayList<>()); - if (!value.isEmpty()) fields.get(currentKey).add(value); - if (currentKey.equals("type")) currentKey = null; - } else if (currentKey != null) { - fields.get(currentKey).add(line); - } else { - Logger.logError("Unexpected line " + line); - return null; - } - } - String send = String.join("\n", fields.get("send")); - List expect = fields.get("expect"); - List typeL = fields.get("type"); - String type = typeL != null && typeL.size() == 1 ? typeL.getFirst() : "exact"; - - if (send == null || expect == null) { - Logger.logError("Test command requires 'Send' and 'Expect' fields."); - return null; - } - - Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect + ", type=" + type); - String actual = executeJshell(send); - if (actual == null) return "No Result"; - List actualParsed = actual.lines().map(Test::parseJshellOutput).filter(s -> !s.isBlank()).toList(); - - return TextUtils.fillOut(""" - Result for Test ${0}: - Input: ${1} - Response: ${2} - Actual: ${3} - Expected: ${4} - Comparison: '${5}' - Status: ${6} - """, meta.id(), send, actual, actualParsed, expect, type, compare(actualParsed, expect, type) ? "Success" : "Failure"); - } - - private static boolean compare(List actual, List expected, String type) { - return switch(type) { - case "exact" -> actual.equals(expected); - case "oneof" -> expected.stream().allMatch(actual::contains); - case "same" -> actual.size() == expected.size() && new HashSet<>(actual).equals(new HashSet<>(expected)); - default -> { - Logger.logError("Unknown Comparison Type."); - yield false; - } - }; - } - - private static String executeJshell(String send) { - String result = null; - try { - Logger.logInfo("Executing jshell --enable-preview -R-ea"); - ProcessBuilder pb = new ProcessBuilder("jshell", "--enable-preview", "-R-ea") - .redirectErrorStream(true); - Process process = pb.start(); - - try (BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { - writer.write(send + "\n"); - writer.write("/ex"); - writer.flush(); - } - try (var reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - result = reader.lines() - .filter(line -> line.startsWith(JSHELL_PROMPT) && line.strip().length() > JSHELL_PROMPT.length()) - .collect(Collectors.joining("\n")); - } - boolean finished = process.waitFor(10, TimeUnit.SECONDS); - if (!finished) { - process.destroyForcibly(); - Logger.logError("Timeout: process jshell killed"); - } - } catch (Exception e) { - Logger.logError("Error in jshell", e); - Thread.currentThread().interrupt(); - } - return result; - } - - private static String parseJshellOutput(String line) { - int idx = line.indexOf("==>"); - if (idx != -1 && idx + 3 < line.length()) { - return line.substring(idx + 3).strip(); - } else if (line.startsWith(JSHELL_PROMPT + " |")) { - return line.substring(9).strip(); - } - return ""; - } -} From 22f897890e5f15bbc4187729b8e5873a8d400444 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 22 Sep 2025 09:37:44 +0200 Subject: [PATCH 49/50] Examples and Readme --- README.md | 18 +-- demo.java | 195 +++++------------------ syntax.md => docs/syntax.md | 0 scantest.java => examples/scantest.java | 0 intro.java | 57 ------- logo.java | 200 ------------------------ newdemo.java | 163 ------------------- registerdemo.java | 15 -- 8 files changed, 52 insertions(+), 596 deletions(-) rename syntax.md => docs/syntax.md (100%) rename scantest.java => examples/scantest.java (100%) delete mode 100644 intro.java delete mode 100644 logo.java delete mode 100644 newdemo.java delete mode 100644 registerdemo.java diff --git a/README.md b/README.md index 3aa504f..d956f1d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# _Live View Programming_ mit Java +# _Live View Programming_ mit jeder Programmiersprache -Das _Live View Programming_ (LVP) bietet Ihnen für die Java-Programmierung _Views_ und _Skills_ an. Views sind dazu da, um mediale Inhalte im Web-Browser darzustellen, also Texte, Bilder, Grafiken, Videos, inteaktive Animationen etc. Skills stellen nützliche Fähigkeiten bereit, die man in Kombination mit Views (z.B. zur Dokumentation von Code) gebrauchen kann. +Das _Live View Programming_ (LVP) bietet Ihnen für die Programmierung ein einfaches Textprotokoll an, um mediale Inhalte im Web-Browser darzustellen, also Texte, Bilder, Grafiken, Videos, inteaktive Animationen etc. Kommandos stellen auch nützliche Fähigkeiten bereit, die man z.B. zur Dokumentation von Code gebrauchen kann. -All diese Views und Skills nutzt man programmierend mit Java. Mit jeder Code-Änderung wird die Ansicht im Browser _live_ aktualisiert. Es ist – ehrlich gesagt – ziemlich cool, wenn man die Veränderungen dann im Browser sieht. Probieren Sie die Demo aus! +**Eine detaillierte Übersicht des Protokolls mit allen Kommandos folgt noch.** ## 🚀 Nutze das _Live View Programming_ @@ -34,10 +34,10 @@ Passen Sie den Beispielaufruf an die aktuelle Version an: java -jar lvp-.jar --log demo.java ``` -Wenn Sie die Version `lvp-0.5.0.jar` heruntergeladen haben, lautet der Aufruf: +Wenn Sie die Version `lvp-1.0.0.jar` heruntergeladen haben, lautet der Aufruf: ``` -java -jar lvp-0.5.0.jar --log demo.java +java -jar lvp-1.0.0.jar --log demo.java ``` #### Übersicht der möglichen Kommandozeilenargumente @@ -62,7 +62,7 @@ Die Datei `demo.java` dient als einfaches Beispiel für den Einstieg in das Live Damit LVP funktioniert, **muss der Server die Datei beobachten (watchen)** – sobald Änderungen erkannt werden, wird der Code automatisch neu ausgeführt und die Ausgabe aktualisiert. -Innerhalb einer [`void main()`-Methode](https://openjdk.org/jeps/495) lassen sich interaktive Inhalte erzeugen, indem man Methoden des `Clerk`-Interfaces verwendet. Diese Inhalte werden anschließend im Browser angezeigt. +Innerhalb einer [`void main()`-Methode](https://openjdk.org/jeps/495) lassen sich interaktive Inhalte erzeugen, indem man `println`-Ausgaben entsprechend dem Protokoll erzeugt. Diese Inhalte werden anschließend im Browser angezeigt. **Beispiel:** @@ -70,10 +70,10 @@ Innerhalb einer [`void main()`-Methode](https://openjdk.org/jeps/495) lassen sic import lvp.Clerk; void main() { - Clerk.markdown("# Hello World"); + println("Markdown: # Hello World"); } ``` -Dieser einfache Aufruf rendert eine Markdown-Überschrift direkt im Browser. Weitere Ausgaben, Grafiken oder Interaktionen können durch zusätzliche Clerk-Methoden, Views oder Skills ergänzt werden. +Dieser einfache Aufruf rendert eine Markdown-Überschrift direkt im Browser. Weitere Ausgaben, Grafiken oder Interaktionen können durch zusätzliche Kommandos ergänzt werden. ### Troubleshooting @@ -106,7 +106,7 @@ kill -9 11840 ``` Dabei ist 11840 durch die ermittelte PID zu ersetzen. -## 💟 Motivation: Views bereichern das Programmieren +## 💟 Motivation: Views bereichern das Programmieren (Outdated) Das _Live View Programming_ versteht sich als ein Angebot, in ein bestehendes Programm _Views_ einzubauen und zu verwenden, die im Web-Browser angezeigt werden. Es macht nicht nur Spaß, wenn man zum Beispiel Grafiken im Browser erzeugen kann -- man sieht auch die Programmierfehler, die einem unterlaufen. Wenn man etwa in der Turtle-View eine Schildkröte mit einem Stift über die Zeichenfläche schickt, zeigt sich unmittelbar, ob man Wiederholungen über Schleifen richtig aufgesetzt oder die Rekursion korrekt umgesetzt hat. Die visuelle Repräsentation gibt über das Auge eine direkte Rückmeldung. Feedback motiviert und hilft beim Verständnis. diff --git a/demo.java b/demo.java index b58c2b2..581827c 100644 --- a/demo.java +++ b/demo.java @@ -1,166 +1,57 @@ -import lvp.Clerk; -import lvp.skills.Text; -import lvp.skills.Interaction; -import lvp.views.Dot; -import lvp.views.Turtle; - - +List obst = List.of("Apfel", "Birne", "Banane"); void main() { - Clerk.clear(); - // Markdown 1 - Clerk.markdown(Text.fillOut(""" - # Interaktive LVP Demo - - ## Markdown - Die Markdown-View erlaubt es, Markdown-Text direkt im Browser darzustellen. Der folgende Code zeigt ein einfaches Beispiel, wie Text im Markdown-Format - an den Browser gesendet und dort automatisch als HTML gerendert wird: - ```java - ${0} - ``` - Der Aufruf `Clerk.markdown(text)` elaubt den einfachen Zugriff auf die Markdown-View. - In diesem Beispiel werden zusätzlich zwei unterstützende Skills verwendet: - - `Text.fillOut(...)`: Zum Befüllen von String-Vorlagen mit dynamischen Inhalten, indem Platzhalter (z.B. ${2}) durch die Auswertung von übergebenen Ausdrücken ersetzt werden. - - `Text.codeBlock(...)`: Zum Einbinden von Codeabschnitten als interaktive Blöcke im Markdown-Text. - - ## Turtle - Die Turtle-View ermöglicht das Zeichnen und Anzeigen von SVG-Grafiken im Browser. Diese können Schritt für Schritt aufgebaut werden: - ```java - ${1} - ``` - """, Text.codeBlock("./demo.java", "// Markdown 1"), Text.codeBlock("./demo.java", "// Turtle 1"), "${0}")); - // Markdown 1 - - // Label Turtle 1 - // Turtle 1 - var turtle = new Turtle(0, 200, 0, 25, 50, 0, 0); - turtle.forward(25).right(60).backward(25).right(60).forward(25).write(); - // Turtle 1 - // Label Turtle 1 - - Clerk.markdown(Text.fillOut(""" - - ## Interaktionen - Die Live-View ist nicht nur ein Anzeigewerkzeug, sondern dient auch als interaktiver Editor. Änderungen an eingebetteten Code-Blöcken wirken sich direkt auf die zugrunde liegende - Datei aus. Dadurch kann der dokumentierte Code live ausprobiert und bearbeitet werden. - Ein interaktiver Code-Block wird mithilfe von `Text.codeBlock(...)` definiert. Der entsprechende Code im Quelltext muss durch Kommentar-Labels (z.B. `// Turtle 1`) markiert werden: - ```java - ${0} - ``` - Dieser markierte Block kann anschließend über `Text.codeBlock("./demo.java", "// Turtle 1")` eingebunden werden. Wird dieser Block in einen Markdown-Abschnitt eingefügt, erscheint - er in der Live-View als editierbarer Code-Bereich. - - Zusätzlich können JavaScript-Funktionen eingebunden werden, die gezielt Teile des Quelltexts verändern. Dafür wird `Interaction.eventFunction(...)` verwendet. Dieser Skill liefert - eine Funktion, die anhand des Dateipfads, eines Labels und des neuen Codes eine markierte Zeile ersetzt. - - Um solche Funktionen interaktiv nutzbar zu machen, kann `Interaction.button(...)` verwendet werden. Damit lässt sich ein Button erstellen, der bei Klick eine bestimmte Stelle im Code anpasst: - ```java - ${1} - ``` - - ### Color Change - Im folgenden Beispiel wird eine Turtle-Grafik dargestellt: - ```java - ${2} - ``` - - """, Text.codeBlock("./demo.java", "// Label Turtle 1"), Text.codeBlock("./demo.java", "// Buttons"), Text.codeBlock("./demo.java", "// Turtle triangle"))); - - // Turtle 2 - var turtle2 = new Turtle(0, 200, 0, 50, 100, 12, 0); - drawing(turtle2, 24); - turtle2.write(); - // Turtle 2 - - Clerk.markdown(""" - Darunter befinden sich drei Buttons, die jeweils die Farbe der Turtle ändern. Die zu ersetzende Stelle im Quellcode ist durch das Label `// turtle color` markiert. Beim Klick auf einen Button wird - dieser Teil des Codes automatisch angepasst. + println(""" + Clear + Markdown: ## Einkaufsliste + Markdown: """); + println(buildObstListe()); + println("~~~"); + println(""" + Text[template]: + ## Beispiel + Irgendein ${0} anzeigen. + ~~~ + | Markdown + + Text: Text + | Text[template] | Markdown + + Text[t2]: + ## Syntax Überschrift + Das ist die Syntax + ${0} + Danach + ~~~ - // Buttons - Clerk.write(Interaction.button("Red", 200, 50, Interaction.eventFunction("./demo.java", "// turtle color", "turtle.color(255, i * 256 / 37, i * 256 / 37, 1);"))); - Clerk.write(Interaction.button("Green", 200, 50, Interaction.eventFunction("./demo.java", "// turtle color", "turtle.color(i * 256 / 37, 255, i * 256 / 37, 1);"))); - Clerk.write(Interaction.button("Blue", 200, 50, Interaction.eventFunction("./demo.java", "// turtle color", "turtle.color(i * 256 / 37, i * 256 / 37, 255, 1);"))); - // Buttons + Cutout: ./syntax.md + | Text[t2] | Markdown - Clerk.markdown(Text.fillOut(""" - ### Turtle mit Timeline - Die Turtle-View unterstützt außerdem eine Timeline, über die sich die Zeichenreihenfolge der Grafik Schritt für Schritt nachvollziehen lässt: + Text[t3]: ```java ${0} ``` - """, Text.codeBlock("./demo.java", "// Turtle 3"))); + ~~~ - // Turtle 3 - var turtle3 = new Turtle(0, 200, 0, 50, 100, 12, 0); - drawing(turtle3, 24); - turtle3.write().timelineSlider(); - // Turtle 3 + Codeblock:./intro.java;// example + | Text[t3] | Markdown - Clerk.markdown(Text.fillOut(""" - ### Input - Initialisierung von Variablen über ein Eingabefeld: - ```java - ${0} - ``` - Der Skill `Interaction.input(...)` ermöglicht es, Eingabefelder zu erstellen, die genutzt werden können, um Werte in den Quelltext einzufügen. - Dazu wird Pfad und Label angegeben, um die Zeile zu makieren, in der der Wert eingefügt werden soll. Gleichzeitig wird das Label als Beschriftung des Eingabefelds verwendet. - Ein Template wird angegeben, das den Platzhalter `$` enthält, der durch den eingegebenen Wert ersetzt wird. Optional kann ein Platzhaltertext angegeben werden, - der im Eingabefeld angezeigt wird. Zusätzlich kann der Type des Eingabefelds angegeben werden (z.B. `text`, `number`, `email`). - - """, Text.codeBlock("./demo.java", "// Input"))); - - // Input - int exampleValue = 0; // Input Example - Clerk.write(Interaction.input("./demo.java", "// Input Example", "int exampleValue = $;", "Geben Sie eine Zahl ein")); - - String exampleString; // Input String Example - Clerk.write(Interaction.input("./demo.java", "// Input String Example", "String exampleString = \"$\";", "Geben Sie einen String ein")); - // Input - - Clerk.markdown(Text.fillOut(""" - #### Checkbox - Für Checkboxen kann `Interaction.checkbox(...)` verwendet werden. Diese triggern die Änderung des Quelltextes, wenn sie angeklickt werden. - ```java - ${0} - ``` - """, Text.codeBlock("./demo.java", "// Checkbox"))); - - // Checkbox - boolean booleanValue = false; // Boolean Example - Clerk.write(Interaction.checkbox("./demo.java", "// Boolean Example", "boolean booleanValue = $;", booleanValue)); - // Checkbox - - Clerk.markdown(Text.fillOut(""" - ## Dot View - Die Dot-View erlaubt das Anzeigen von Graphen, die im [DOT-Format](https://graphviz.org/doc/info/lang.html) beschrieben sind. - ```java - ${0} - ``` - """, Text.codeBlock("./demo.java", "// Dot"))); + Register[skipId]: Counter wc + Text: + Hello World + ~~~ + | Counter | Html - // Dot - Dot dot = new Dot(); - dot.draw(""" - digraph G { - A -> B; - B -> C; - } - """); - // Dot -} - - -// Turtle triangle -void triangle(Turtle turtle, double size) { - turtle.forward(size).right(60).backward(size).right(60).forward(size).right(60 + 180); + """); + } -void drawing(Turtle turtle, double size) { - for (int i = 1; i <= 18; i++) { - turtle.color(255, i * 256 / 37, i * 256 / 37, 1); // turtle color - turtle.width(1.0 - 1.0 / 36.0 * i); - triangle(turtle, size + 1 - 2 * i); - turtle.left(20).forward(5); +// example +String buildObstListe() { + String out = ""; + for (String o : obst) { + out += "**" + o + "**\n"; } + return out; } -// Turtle triangle +// example diff --git a/syntax.md b/docs/syntax.md similarity index 100% rename from syntax.md rename to docs/syntax.md diff --git a/scantest.java b/examples/scantest.java similarity index 100% rename from scantest.java rename to examples/scantest.java diff --git a/intro.java b/intro.java deleted file mode 100644 index 581827c..0000000 --- a/intro.java +++ /dev/null @@ -1,57 +0,0 @@ -List obst = List.of("Apfel", "Birne", "Banane"); -void main() { - println(""" - Clear - Markdown: ## Einkaufsliste - Markdown: - """); - println(buildObstListe()); - println("~~~"); - println(""" - Text[template]: - ## Beispiel - Irgendein ${0} anzeigen. - ~~~ - | Markdown - - Text: Text - | Text[template] | Markdown - - Text[t2]: - ## Syntax Überschrift - Das ist die Syntax - ${0} - Danach - ~~~ - - Cutout: ./syntax.md - | Text[t2] | Markdown - - Text[t3]: - ```java - ${0} - ``` - ~~~ - - Codeblock:./intro.java;// example - | Text[t3] | Markdown - - Register[skipId]: Counter wc - Text: - Hello World - ~~~ - | Counter | Html - - """); - -} - -// example -String buildObstListe() { - String out = ""; - for (String o : obst) { - out += "**" + o + "**\n"; - } - return out; -} -// example diff --git a/logo.java b/logo.java deleted file mode 100644 index 9db07bf..0000000 --- a/logo.java +++ /dev/null @@ -1,200 +0,0 @@ -import lvp.skills.Text; -import lvp.views.MarkdownIt; -import lvp.views.Turtle; - -import java.time.Duration; - -import lvp.Clerk; - -void main() { - Clerk.clear(); - Clerk.markdown( - Text.fillOut( - """ - # Turtle-Programmierungg - - _Dominikus Herzberg_, _Technische Hochschule Mittelhessen_ - - Bei der Programmiersprache [Logo](https://de.wikipedia.org/wiki/Logo_(Programmiersprache)) steht eine Schildkröte (_turtle_) im Mittelpunkt – und zwar im wahrsten Sinne des Wortes. Auf einer weißen Fläche ist in der Mitte die Schildkröte platziert. An ihr ist ein Stift befestigt und sie ist zu Beginn nach rechts ausgerichtet, sie blickt Richtung Osten. - - Die Schildkröte kennt die folgenden Kommandos: - - Befehl | Bedeutung - -------|---------- - `penDown()` | Setze den Stift auf die Zeichenfläche (Anfangseinstellung) - `penUp()` | Hebe den Stift von der Zeichenfläche ab - `forward(double distance)` | Bewege dich um _distance_ vorwärts - `backward(double distance)` | Bewege dich um _distance_ rückwärts - `right(double degrees)` | Drehe dich um die Gradzahl _degrees_ nach rechts - `left(double degrees)` | Drehe dich um die Gradzahl _degrees_ nach links - `color(int red, int green, int blue)` | Setze Stiftfarbe mit den RGB-Farbanteilen _red_, _green_ und _blue_ - `color(int rgb)` | Setze Stiftfarbe auf den kodierten RGB-Farbwert _rgb_ - `lineWidth(double width)` | Setze Stiftbreite auf _width_ - `text(String text, Font font, double size, Font.Align align)` | Schreibe Text vor deinen Kopf mit Angabe des Text-Fonts, der Größe und der Ausrichtung - `text(String text)` | Schreibe Text vor deinen Kopf - `reset()` | Lösche Zeichenfläche, gehe zurück in Bildmitte - - - Mit diesen Kommandos wird die Schildkröte über die Zeichenfläche geschickt und das Zeichnen gesteuert. Wenn man Abfolgen von diesen Kommandos programmiert, kann man teils mit sehr wenig Code interessante Zeichnungen erstellen. - - > Wenn man die Befehle in der JShell zur Verfügung hat, benötigt man kein weiteres Wissen zu Logo. Man kann mit den Sprachkonstrukten von Java arbeiten. - - ## Beispiel 1: Ein Quadrat aus Pfeilennn - - Mit `new Turtle(300,300)` wird eine neue Schildkröte mittig auf eine Zeichenfläche der angegebenen Größe (Breite, Höhe) gesetzt. In den Grundeinstellungen sind die Breite und die Höhe auf 500 gesetzt. - - Die folgende Logo-Anwendung demonstriert, wie man mittels Methoden schrittweise graphische Einheiten erstellen und zusammensetzen kann. - - ```java - ${0} - ... - ${1} - ``` - - Das Ergebnis sieht dann so aus: ein Quadrat aus Pfeilen, wobei absichtlich kleine Zwischenräume gelassen wurden, mit Angaben der Pfeilausrichtung. - """, Text.cutOut("./logo.java", "// first turtle methods"), Text.cutOut("./logo.java", "// myFirstTurtle"))); - - // myFirstTurtle - Turtle myFirstTurtle = new Turtle(300, 300); - myFirstTurtle = edge(myFirstTurtle, 100, 5); - myFirstTurtle = write(myFirstTurtle, "East").right(90); - myFirstTurtle = edge(myFirstTurtle, 100, 5); - myFirstTurtle = write(myFirstTurtle, "South").right(90); - myFirstTurtle = edge(myFirstTurtle, 100, 5); - myFirstTurtle = write(myFirstTurtle, "West").right(90); - myFirstTurtle = edge(myFirstTurtle, 100, 5); - myFirstTurtle = write(myFirstTurtle, "North").right(90); - myFirstTurtle.write(); - // myFirstTurtle - - Clerk.markdown( - Text.fillOut( - """ - ## Beispiel 2: Umsetzung eines Logo-Programms in Java - - Die Programmiersprache Logo ist nicht so schwer zu verstehen, wie das nachstehende Beispiel zeigt, das von dieser [Webseite](https://calormen.com/jslogo/) stammt. Auch wenn man kein Logo spricht, der Code ist leicht in Java umzusetzen. - - ```logo - TO tree :size - if :size < 5 [forward :size back :size stop] - forward :size/3 - left 30 tree :size*2/3 right 30 - forward :size/6 - right 25 tree :size/2 left 25 - forward :size/3 - right 25 tree :size/2 left 25 - forward :size/6 - back :size - END - clearscreen - tree 150 - ``` - - Die Java-Methode `tree` bildet das obige Logo-Programm nach; lediglich aus praktischen Überlegungen lasse ich den Rekursionsabbruch etwas früher greifen. - - ```java - ${turtle_tree} - ... - ${turtle_tree2} - ``` - - Der Aufruf der Methode `tree` erzeugt etwas, was einem "Baum" ähnelt. - - ```java - ${tree} - ``` - - """, Map.of("turtle_tree", Text.cutOut("./logo.java", "// turtle tree"), - "turtle_tree2", Text.cutOut("./logo.java", "// turtle tree2"), - "tree", Text.cutOut("./logo.java", "// tree")))); - - // turtle tree - Turtle turtle = new Turtle().left(90); - - - // turtle tree - - // tree - tree(turtle, 150); - turtle.write(); - // tree - - Clerk.markdown( - Text.fillOut( - """ - ## Beispiel 3: Es kommt Farbe ins Spiel - - Mit Farbe wird die Welt bunter und interessanter, und die Strichstärke kann man ebenfalls für Effekte einsetzen. Im nachfolgenden Beispiel verblasst die Farbe zunehmend und die Strichstärke lässt allmählich nach. - - ```java - ${0} - ... - ${1} - ``` - """, Text.cutOut("./logo.java", "// triangles"), Text.cutOut("./logo.java", "// triangles2"))); - - // triangles - turtle = new Turtle(300,350); - - drawing(turtle, 100); - turtle.write(); - // triangles - - - Clerk.markdown(""" - Soviel möge als Demo vorerst genügen! _More features to come_ 😉 - """); -} - -// triangles2 -void triangle(Turtle turtle, double size) { - turtle.forward(size).right(60).backward(size).right(60).forward(size).right(60 + 180); -} - -void drawing(Turtle turtle, double size) { - for (int i = 1; i <= 36; i++) { - turtle.color(255,i * 256 / 37, i * 256 / 37); - turtle.lineWidth(1.0 - 1.0 / 36.0 * i); - triangle(turtle, size + 1 - 2 * i); - turtle.left(10).forward(10); - } -} -// triangles2 - -// turtle tree2 -void tree(Turtle turtle, double size) { - if (size < 10) { - turtle.forward(size).backward(size); - return; - } - turtle.forward(size / 3).left(30); - tree(turtle, size * 2.0 / 3.0); - turtle.right(30); - - turtle.forward(size / 6).right(25); - tree(turtle, size / 2.0); - turtle.left(25); - - turtle.forward(size / 3).right(25); - tree(turtle, size / 2.0); - turtle.left(25); - - turtle.forward(size / 6).backward(size); -} -// turtle tree2 - -// first turtle methods -Turtle arrowhead(Turtle t) { - return t.right(30).backward(10).forward(10). - left(60).backward(10).forward(10).right(30); -} -Turtle arrow(Turtle t, double length) { - return arrowhead(t.forward(length)); -} -Turtle edge(Turtle t, double length, double space) { - return arrow(t, length).penUp().forward(space).penDown(); -} -Turtle write(Turtle t, String text) { - return t.penUp().forward(10).text(text).backward(10).penDown(); -} -// first turtle methods \ No newline at end of file diff --git a/newdemo.java b/newdemo.java deleted file mode 100644 index 3bcfeb3..0000000 --- a/newdemo.java +++ /dev/null @@ -1,163 +0,0 @@ - -// ex1 -void main() { - - println(""" - Clear - - - - - Markdown: # Text Demo - - Text: newdemo.java;// ex1 - | Codeblock | Text[example] - Text[title]: Codeblocks - Text[template]: - ## ${0} - ```java - ${1} - ``` - ~~~ - | Text[title] | Text[example] | Markdown - Text[template]: Hello World! - | Markdown - Text[template] - | Markdown - - """); -} -// ex1 - -// void main() { -// println("Clear:~"); -// println("Markdown: # Hello World!"); -// println(""" -// Markdown: -// ## Hello World! -// This is a simple example of a markdown block. - -// ~~~ -// """); -// println(""" -// Text{0}: -// # Text und Pipes -// Der ${0} Command ${1} es ${0} Templates zu definieren. -// In diesen Templates können Platzhalter genutzt werden, die -// später durch Pipes mit Content befüllt werden. -// Dieser ${0} kann zum Beispiel in die Markdown View "gepiped" werden. -// ~~~ -// | Markdown -// """); - - -// println(""" -// Text: Text -// | Text{0} | Markdown -// """); - -// println(""" -// Text: erlaubt -// | Text{0} | Markdown -// """); - -// // ex1 -// println(""" -// Text{1}: -// # Codeblocks -// This is a codeblock example: -// ```java -// ${0} -// ``` -// ~~~ -// Codeblock: newdemo.java;// ex1 -// | Text{1} | Markdown -// """); -// // ex1 - -// println(""" -// Text{2}: -// init 0 200 0 25 50 0 0 -// """ -// + -// "color 37 255 37 1" // turtle color -// + -// """ - -// forward 25 -// right 60 -// backward 25 -// right 60 -// forward 25 -// timeline -// ~~~ -// | Turtle | Html -// Text{2}: ~ -// | Markdown -// Text{3}: -// ``` -// ${0} -// ``` -// ~~~ -// Text{2}: - -// | Text{3} | Markdown -// """); - -// println(""" -// Button: -// Text: Green -// width: 200 -// height: 50 -// path: newdemo.java -// label: "// turtle color" -// replacement: "color 37 255 37 1" -// ~~~ -// | Html -// Button: -// Text: Red -// width: 200 -// height: 50 -// path: newdemo.java -// label: "// turtle color" -// replacement: "color 255 37 37 1" -// ~~~ -// | Html -// """); - -// int n = 55; // input -// boolean b = true; // bool -// println(""" -// Input: -// path: newdemo.java -// label: "// input" -// placeholder: Enter a number -// template: int n = $; -// type: text -// ~~~ -// | Html -// Checkbox: -// path: newdemo.java -// label: "// bool" -// template: boolean b = $; -// """ -// + -// "checked:" + b -// + -// """ - -// ~~~ -// | Html -// """); - - -// println(""" -// Dot: -// width: 1000 -// height: 600 -// digraph G { -// A -> B; -// B -> C; -// } -// ~~~ -// """); -// } diff --git a/registerdemo.java b/registerdemo.java deleted file mode 100644 index a36434c..0000000 --- a/registerdemo.java +++ /dev/null @@ -1,15 +0,0 @@ -void main() { - println(""" - Clear: ~ - Markdown: # Register Test - Register: Reverse java --enable-preview external.java - Reverse: Hello World - | Markdown - Register[skipId]: Wc wc - Wc: - Hello World - Test - ~~~ - | Html - """); -} \ No newline at end of file From abeaa12479bdab4b052afc0f8a32368496918961 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 22 Sep 2025 09:39:57 +0200 Subject: [PATCH 50/50] fixed gitignore --- .gitignore | 2 +- src/main/java/lvp/services/Test.java | 122 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/java/lvp/services/Test.java diff --git a/.gitignore b/.gitignore index 22fcfbc..bdd3865 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ target/ build test.java Test.java -!**/transformer/Test.java +!**/services/Test.java .vscode sources.json diff --git a/src/main/java/lvp/services/Test.java b/src/main/java/lvp/services/Test.java new file mode 100644 index 0000000..8819804 --- /dev/null +++ b/src/main/java/lvp/services/Test.java @@ -0,0 +1,122 @@ +package lvp.services; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import lvp.Processor.MetaInformation; +import lvp.skills.TextUtils; +import lvp.skills.logging.Logger; + +public class Test { + private Test() {} + private static final String JSHELL_PROMPT = "jshell>"; + public static String test(MetaInformation meta, String content) { + Map> fields = new HashMap<>(); + String currentKey = null; + + for (String line: content.lines().toList()) { + if (line.isBlank()) continue; + if (line.strip().startsWith("Send:") || line.strip().startsWith("Expect:") || line.strip().startsWith("Type:")) { + String[] parts = line.split(":", 2); + currentKey = parts[0].strip().toLowerCase(); + String value = parts[1].strip(); + fields.computeIfAbsent(currentKey, _ -> new ArrayList<>()); + if (!value.isEmpty()) fields.get(currentKey).add(value); + if (currentKey.equals("type")) currentKey = null; + } else if (currentKey != null) { + fields.get(currentKey).add(line); + } else { + Logger.logError("Unexpected line " + line); + return null; + } + } + String send = String.join("\n", fields.get("send")); + List expect = fields.get("expect"); + List typeL = fields.get("type"); + String type = typeL != null && typeL.size() == 1 ? typeL.getFirst() : "exact"; + + if (send == null || expect == null) { + Logger.logError("Test command requires 'Send' and 'Expect' fields."); + return null; + } + + Logger.logDebug("Parsed test command: send=" + send + ", expect=" + expect + ", type=" + type); + String actual = executeJshell(send); + if (actual == null) return "No Result"; + List actualParsed = actual.lines().map(Test::parseJshellOutput).filter(s -> !s.isBlank()).toList(); + + return TextUtils.fillOut(""" + Result for Test ${0}: + Input: ${1} + Response: ${2} + Actual: ${3} + Expected: ${4} + Comparison: '${5}' + Status: ${6} + """, meta.id(), send, actual, actualParsed, expect, type, compare(actualParsed, expect, type) ? "Success" : "Failure"); + } + + private static boolean compare(List actual, List expected, String type) { + return switch(type) { + case "exact" -> actual.equals(expected); + case "oneof" -> expected.stream().allMatch(actual::contains); + case "same" -> actual.size() == expected.size() && new HashSet<>(actual).equals(new HashSet<>(expected)); + default -> { + Logger.logError("Unknown Comparison Type."); + yield false; + } + }; + } + + private static String executeJshell(String send) { + String result = null; + try { + Logger.logInfo("Executing jshell --enable-preview -R-ea"); + ProcessBuilder pb = new ProcessBuilder("jshell", "--enable-preview", "-R-ea") + .redirectErrorStream(true); + Process process = pb.start(); + + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(send + "\n"); + writer.write("/ex"); + writer.flush(); + } + try (var reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + result = reader.lines() + .filter(line -> line.startsWith(JSHELL_PROMPT) && line.strip().length() > JSHELL_PROMPT.length()) + .collect(Collectors.joining("\n")); + } + boolean finished = process.waitFor(10, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + Logger.logError("Timeout: process jshell killed"); + } + } catch (Exception e) { + Logger.logError("Error in jshell", e); + Thread.currentThread().interrupt(); + } + return result; + } + + private static String parseJshellOutput(String line) { + int idx = line.indexOf("==>"); + if (idx != -1 && idx + 3 < line.length()) { + return line.substring(idx + 3).strip(); + } else if (line.startsWith(JSHELL_PROMPT + " |")) { + return line.substring(9).strip(); + } + return ""; + } +}