From 4682e4482a8e3d24c07e90dabb0973d3bc24efc7 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 10 Jun 2025 18:58:16 +0200 Subject: [PATCH 01/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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 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 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 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 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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 ""; + } +} From 8318ede1ec5b9563d60902f09a482f0c0df66526 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 17 Oct 2025 11:14:41 +0200 Subject: [PATCH 51/57] fixed filewatcher --- src/main/java/lvp/FileWatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/lvp/FileWatcher.java b/src/main/java/lvp/FileWatcher.java index 0f4be00..e7bcd6b 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().startsWith(".")); + (_, attrs) -> attrs.isDirectory()).filter(p -> !p.toString().startsWith(".git")); } catch (IOException e) { Logger.logError("Error walking directory: " + root.toAbsolutePath(), e); return Stream.empty(); From e7a8d3bf2ec99028b3fc5206d822db8a55b48bf3 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 25 Feb 2026 23:34:57 +0100 Subject: [PATCH 52/57] chore: Dokumentation und Beispiele --- README.TurtleProgramming.png | Bin 89787 -> 0 bytes README.md | 80 ++++++++++++++--- demo.java | 81 ++++++++++++------ demo_sources.json | 4 - ...ternerSevice.java => ExternerService.java} | 1 + examples/Introduction.java | 33 ------- pom.xml | 4 +- 7 files changed, 126 insertions(+), 77 deletions(-) delete mode 100644 README.TurtleProgramming.png delete mode 100644 demo_sources.json rename examples/{ExternerSevice.java => ExternerService.java} (89%) delete mode 100644 examples/Introduction.java diff --git a/README.TurtleProgramming.png b/README.TurtleProgramming.png deleted file mode 100644 index 83e9ab721d9560a11bdb06658591055261090a1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89787 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 diff --git a/README.md b/README.md index d956f1d..1002cdb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # _Live View Programming_ mit jeder Programmiersprache -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. +Live View Programming (LVP) stellt ein einfaches Textprotokoll bereit, mit dem sich mediale Inhalte direkt im Web-Browser darstellen lassen, darunter Texte, Bilder, Grafiken, Videos und interaktive Animationen. + +Das Protokoll dient als sprachunabhängige Schnittstelle zwischen Ihrem eigenen Programm und LVP. Die Kommunikation erfolgt über Kommandos, die durch gewöhnliche Konsolenausgaben erzeugt werden. Diese Kommandos können Inhalte transformieren und sie im Browser medial aufbereitet anzeigen. + +Auf diese Weise stellt LVP praktische Funktionen bereit, die sich beispielsweise für die Code-Dokumentation, die Erzeugung von Turtle-Grafiken oder das Erstellen interaktiver HTML-Elemente nutzen lassen. + -**Eine detaillierte Übersicht des Protokolls mit allen Kommandos folgt noch.** ## 🚀 Nutze das _Live View Programming_ @@ -10,7 +14,7 @@ Wenn Sie das _Live View Programming_ ausprobieren möchten, ist Folgendes zu tun ### 1. Lade die `.jar`-Datei herunter -* Stellen Sie sicher, dasss Sie mit einem aktuellen JDK (Java Development Kit) arbeiten; es empfiehlt sich das [TemurinJDK](https://adoptium.net/temurin/releases/) +* Stellen Sie sicher, dasss Sie mit einem aktuellen JDK (Java Development Kit) arbeiten; es empfiehlt sich das [OpenJDK](https://jdk.java.net/25/) * Laden Sie die aktuelle `.jar`-Datei herunter, die Ihnen als Asset zum [aktuellen Release](https://github.com/denkspuren/LiveViewProgramming/releases) als Download angeboten wird; die Datei hat den Namen `lvp-.jar` * Laden Sie `demo.java`-Datei herunter, die Ihnen ebenfalls als Asset zum [aktuellen Release](https://github.com/denkspuren/LiveViewProgramming/releases) als Download angeboten wird. @@ -56,26 +60,74 @@ java -jar lvp-1.0.0.jar --log demo.java > Mehrere Argumente können kombiniert werden, z.B.: > `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_ +### 3. Einbinden von Quellen +Damit eigene Programme mit LVP kommunizieren können, werden sie innerhalb von LVP als Laufzeitumgebung ausgeführt. Diese Programme werden im Kontext von LVP als Quellen bezeichnet. + +Die Programme selbst benötigen keine zusätzlichen Abhängigkeiten und können in beliebigen Programmiersprachen geschrieben sein. Es gibt zwei Möglichkeiten, Quellen in LVP zu definieren. + +#### Variante 1: Übergabe über die Konsole -Die Datei `demo.java` dient als einfaches Beispiel für den Einstieg in das Live View Programming (LVP). +Wie oben gezeigt, können Quellen als Argument beim Konsolenaufruf übergeben werden. Dabei lassen sich beliebig viele Quellen definieren. Alle angegebenen Quellen werden mit dem über --cmd übergebenen Befehl ausgeführt. -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. +Wird das Argument `--cmd` nicht gesetzt, versucht LVP standardmäßig, die Quellen als Java-Programme auszuführen. -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. +#### Variante 2: Konfigurationsdatei (sources.json) -**Beispiel:** +Bei einer größeren Anzahl von Quellen oder wenn Quellen unterschiedlich gestartet werden sollen (z. B. mit verschiedenen Startbefehlen), empfiehlt sich die Verwendung einer sources.json-Datei im Wurzelverzeichnis der Ausführung. +Wird LVP mit dem Argument `--config` gestartet, werden die in dieser Datei definierten Quellen zusätzlich ausgeführt. + +Ein Beispiel für den Aufbau der Datei befindet sich im Ordner examples. + +Beide Varianten lassen sich auch kombinieren. + +### 4. Das Protokoll +Im Folgenden eine grobe Übersicht über den generellen Aufbau des Protokolls. Ein ausführlicheres Beispiel finden Sie in `demo.java`, sowie weitere kleine Beispiele in dem Ordner `examples`. + +Einzeilige Kommandos: ```java -import lvp.Clerk; +println("Markdown: # Ich bin eine Überschrift"); +``` -void main() { - println("Markdown: # Hello World"); -} +Mehrzeilige Kommandos: +```java +println(""" + Markdown: + # Ich bin eine Überschrift + Ich bin **Text**. + ~~~ +"""); ``` -Dieser einfache Aufruf rendert eine Markdown-Überschrift direkt im Browser. Weitere Ausgaben, Grafiken oder Interaktionen können durch zusätzliche Kommandos ergänzt werden. +Kommandos bestehen aus einem Namen und einem Inhalt. Dabei wird zwischen einzeiligen und mehrzeiligen Kommandos unterschieden. Ein mehrzeiliges Kommando wird durch `~~~` beendet. + +
+ +```java +println(""" + Text[template]: + ## Beispiel + Irgendein ${0} anzeigen. + ~~~ + | Markdown + + Text: Beispiel + | Text[template] | Markdown +""") + +``` +Durch Piping können die Ergebnisse eines Kommandos an ein anderes Kommando weitergegeben werden. + +### Kommandos und Anweisungen +[Hier folgt eine Übersicht über alle Kommandos] +- Register +- Markdown +- Html +- Css +- Javascript +- .... + -### Troubleshooting +## Troubleshooting > Error starting server: Address already in use: bind diff --git a/demo.java b/demo.java index 581827c..23cc2f8 100644 --- a/demo.java +++ b/demo.java @@ -1,48 +1,81 @@ +import static java.lang.IO.println; List obst = List.of("Apfel", "Birne", "Banane"); void main() { println(""" Clear - Markdown: ## Einkaufsliste + Markdown: # Wie nutze ich LVP? + Markdown: + Das neue LVP Protokoll bietet viele Möglichkeiten, um Textausgaben zu transformieren und + daraus mediale Darstellungen im Browser anzeigen zu lassen. Diese Demo soll einen Überblick über die Möglichkeiten geben, die LVP bietet. Hier sieht man wie statische Markdown Texte erstellt werden können, die dann im Browser angezeigt werden. Diese können einzeilig sein, oder auch mehrzeilig, wie hier. Wesentlich spannender ist aber die Möglichkeit, dynamische Inhalte zu erstellen, wie im folgenden Beispiel zu sehen ist. + ~~~ + Markdown: """); println(buildObstListe()); println("~~~"); + + println(""" + Markdown: + ## Kommandoinhalte durch Pipelines reichen + Ergebnisse von Kommandos können durch Pipelines an andere Kommandos weitergereicht werden, um diese weiter zu verarbeiten. Das ist aber nur für Kommandos möglich, die auch tatsächlich eine Ausgabe haben. Das Markdown Kommando hat zum Beispiel keine Ausgabe, die weiterverarbeitet werden könnte, da es sein Ergebnis direkt im Browser anzeigt. + ~~~ + """); + + println(""" Text[template]: ## Beispiel - Irgendein ${0} anzeigen. + Im folgenden Beispiel sieht man, wie das Text Kommando genutzt wird, um ein Template zu erstellen. Das Kommando selbst erzeugt keine Anzeige im Browser. Es ermöglicht aber, Platzhalter zu definieren, die dann durch die Pipeline mit ${0} gefüllt werden können. ~~~ | Markdown - Text: Text + Text: Inhalten | Text[template] | Markdown + """); + - Text[t2]: - ## Syntax Überschrift - Das ist die Syntax - ${0} - Danach + println(""" + Markdown: + Man sieht hier auch, dass sich das Text Kommando Inhalte merken kann, indem man eine ID vergibt, in diesem Fall "template". So kann ein Template mehrmals benutzt werden. ~~~ + """); - Cutout: ./syntax.md - | Text[t2] | Markdown - Text[t3]: - ```java - ${0} - ``` - ~~~ + println(""" + Text[t2]: + Jetzt fehlt nur noch die Dokumentation des Codes. Dazu kann das Codeblock Kommando genutzt werden. Es erstellt Codeblöcke, die im Browser angezeigt werden und dort auch bearbeitet werden können. Damit das funktioniert, muss das Ergebnis dieses Kommandos in einem Markdown Codeblock eingebettet werden. - Codeblock:./intro.java;// example - | Text[t3] | Markdown + ```java + ${0} + ``` + ~~~ + + Codeblock:./demo.java;// example + | Text[t2] | Markdown + """); - Register[skipId]: Counter wc - Text: - Hello World - ~~~ - | Counter | Html + println(""" + Register[skipId]: Counter wc + Text[wc-text]: + ## Externe Kommandos + In diesem Beispiel wird gezeigt, wie LVP um externe Kommandos erweitert werden kann. Durch die Register Anweisung können beliebige Kommandos registriert werden, die dann in LVP genutzt werden können. In diesem Fall wird das Kommando "wc" registriert, das die Anzahl der Wörter in einem Text zählt. Das Kommando erwartet den Text als Eingabe und gibt die Anzahl der Wörter als Ausgabe zurück. +
+ Das Ergebnis: ${0} + ~~~ + | Counter | Text[wc-text] | Markdown - """); + Markdown: ### Eigene Kommandos registrieren + + Register: Reverse java --enable-preview ./examples/ExternerService.java + Text[reverse-text]: + So kann man natürlich auch eigene Kommandos erstellen, die dann in LVP genutzt werden können. In diesem Fall wird ein Kommando registriert, das einen Text umkehrt. Das Kommando erwartet den Text als Eingabe und gibt den umgekehrten Text als Ausgabe zurück. + ~~~ + | Reverse | Markdown + """); + + //TODO: Turtle Beispiel + + //TODO: Interaktive HTML Elemente Beispiel } @@ -50,7 +83,7 @@ void main() { String buildObstListe() { String out = ""; for (String o : obst) { - out += "**" + o + "**\n"; + out += "- **" + o + "**\n"; } return out; } diff --git a/demo_sources.json b/demo_sources.json deleted file mode 100644 index 1cb1077..0000000 --- a/demo_sources.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - { "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 diff --git a/examples/ExternerSevice.java b/examples/ExternerService.java similarity index 89% rename from examples/ExternerSevice.java rename to examples/ExternerService.java index 114de50..56ce4c2 100644 --- a/examples/ExternerSevice.java +++ b/examples/ExternerService.java @@ -1,4 +1,5 @@ import java.util.Scanner; +import static java.lang.IO.println; /// Register with: /// Register: Reverse java --enable-preview external.java diff --git a/examples/Introduction.java b/examples/Introduction.java deleted file mode 100644 index b7d606a..0000000 --- a/examples/Introduction.java +++ /dev/null @@ -1,33 +0,0 @@ -import static java.io.IO.println; - -void main() { - println(""" - Clear - Text[Intro]: - # LiveViewProgramming Konzept - Auszug aus dem Readme: - ~~~ - | Markdown - - Text[Template1]: - > "${Zitat}" - ~~~ - - 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/pom.xml b/pom.xml index a247821..cd28093 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ UTF-8 - 24 + 25 @@ -52,7 +52,7 @@ maven-compiler-plugin 3.13.0 - 24 + 25 --enable-preview
From 2dd9fcf5bae01678253de527be36fc7bc0545dc0 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 25 Feb 2026 23:38:11 +0100 Subject: [PATCH 53/57] chore: adjusted java version for pipelines --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d6a636..2d4004a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '24' + java-version: '25' - name: Build JAR run: mvn clean package -DskipTests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48b1266..06b3a19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '24' + java-version: '25' - name: Determine version bump id: bump From 5aa8cbd4ad20b8e3bba921ed93ff567594669761 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 26 Feb 2026 21:07:09 +0100 Subject: [PATCH 54/57] chore: demo.java --- demo.java | 138 ++++++++++++++++++++++++++++++- src/main/resources/web/script.js | 2 +- 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/demo.java b/demo.java index 23cc2f8..527ef7b 100644 --- a/demo.java +++ b/demo.java @@ -16,7 +16,7 @@ void main() { println(""" Markdown: - ## Kommandoinhalte durch Pipelines reichen + ## Pipes Ergebnisse von Kommandos können durch Pipelines an andere Kommandos weitergereicht werden, um diese weiter zu verarbeiten. Das ist aber nur für Kommandos möglich, die auch tatsächlich eine Ausgabe haben. Das Markdown Kommando hat zum Beispiel keine Ausgabe, die weiterverarbeitet werden könnte, da es sein Ergebnis direkt im Browser anzeigt. ~~~ """); @@ -66,17 +66,147 @@ void main() { Markdown: ### Eigene Kommandos registrieren - Register: Reverse java --enable-preview ./examples/ExternerService.java + Register: Reverse java -Dsun.stdout.encoding=UTF-8 ./examples/ExternerService.java Text[reverse-text]: So kann man natürlich auch eigene Kommandos erstellen, die dann in LVP genutzt werden können. In diesem Fall wird ein Kommando registriert, das einen Text umkehrt. Das Kommando erwartet den Text als Eingabe und gibt den umgekehrten Text als Ausgabe zurück. ~~~ | Reverse | Markdown """); - //TODO: Turtle Beispiel + + println(""" + Markdown: + ## Turtle + Nun zum spannendsten Teil, der Turtle. Mit diesem Kommando können Grafiken erstellt werden, die im Browser angezeigt werden. + Das Turtle Kommando erwartet eine Reihe von Anweisungen, die dann in eine Grafik umgesetzt werden. Es gibt verschiedene Anweisungen, wie zum Beispiel "forward", "left", "right" und "color". Mit diesen + Anweisungen können Linien gezeichnet und die Farbe geändert werden. Der Zeichenbereich wird durch "init" definiert, das die Größe des Zeichenbereichs und die Startposition der Turtle festlegt. + In diesem Beispiel wird ein Dreieck gezeichnet, indem die Turtle 25 Einheiten vorwärts bewegt und dann um 120 Grad nach links gedreht wird. Das wird dreimal wiederholt, um ein Dreieck zu zeichnen. + Gleichzeitig wird die Farbe der Turtle auf Grün gesetzt, indem die "color" Anweisung genutzt wird. + Normalerweise kann man die Anweisungen direkt in das Turtle Kommando schreiben, aber hier wird wieder ein Template genutzt, um die Anweisungen zu definieren. So können die Anweisungen mehrmals verarbeitet werden. + ~~~ + + Text[turtle-example]: + init 0 200 0 26 50 1 0 + """ + + + "color 37 255 37 1" // turtle color + + + """ + + forward 25 + left 120 + forward 25 + left 120 + forward 25 + timeline + ~~~ + | Turtle | Html + + Markdown: + Zu Erst werden die Anweisungen an das Turtle Kommando weitergegeben, welches daraus eine SVG Grafik erstellt, die dann durch das Html Kommando im Browser angezeigt wird. + Danach werden die Anweisungen direkt an Markdown weitergegeben. Hier sieht man, dass die Zeilenumbrüche nicht erhalten bleiben, wodurch die Anweisungen nicht gut lesbar sind. + Daher werden die gleichen Anweisungen in einen Markdown Codeblock eingebettet. + ~~~ + + Text[turtle-example] + | Markdown + + Text[turtle-codeblock]: + ``` + ${0} + ``` + ~~~ + + Text[turtle-example] + | Text[turtle-codeblock] | Markdown + """); + + println(""" + Markdown: + ### Interaktive Codebearbeitung durch Buttons + Interaktive Elemente, wie Buttons, können genutzt werden, um Code zu verändern. In diesem Beispiel werden zwei Buttons erstellt, die die Farbe der Turtle ändern, + indem sie die Farbanweisung im Java Code ersetzen. Das funktioniert, indem die Anweisung, die ersetzt werden soll, mit einem Label versehen wird, + in diesem Fall `// turtle color`. Wenn der Button geklickt wird, wird die Anweisung durch die Anweisung ersetzt, die im "replacement" Feld definiert ist. + + ~~~ + Button: + Text: Green + width: 200 + height: 50 + path: demo.java + label: "// turtle color" + replacement: "color 37 255 37 1" + ~~~ + | Html + + Button: + Text: Red + width: 200 + height: 50 + path: demo.java + label: "// turtle color" + replacement: "color 255 37 37 1" + ~~~ + | Html + """); + + int n = 55; // input + boolean b = true; // bool + println(""" + Markdown: + ## Interaktive Html Eingabefelder + Neben Buttons können auch andere interaktive Elemente, wie Eingabefelder oder Checkboxen genutzt werden, um Code zu verändern. Das funktioniert ähnlich wie bei den Buttons, indem + die Anweisung, die ersetzt werden soll, mit einem Label versehen wird, und die neue Anweisung im "replacement" Feld definiert wird. Anders als bei Buttons, die nur eine + vordefinierte Anweisung einsetzen können, können bei Eingabefeldern auch Werte aus dem Eingabefeld genutzt werden. Zu diesem Zweck können Platzhalter in der Replacement Anweisung definiert + werden, die dann durch die Eingabe ersetzt werden. + ~~~ + + Input: + path: demo.java + label: "// input" + placeholder: Enter a number + template: int n = $; + type: text + ~~~ + | Html + + Checkbox: + path: demo.java + label: "// bool" + template: boolean b = $; + """ + + + "checked:" + b + + + """ + + ~~~ + | Html + """); - //TODO: Interaktive HTML Elemente Beispiel + println(""" + Markdown: + ## Graphen mit Dot + Mit dem Dot Kommando können Graphen erstellt werden, die im Browser angezeigt werden. Das Dot Kommando erwartet eine Beschreibung des Graphen in der Dot Sprache, die dann in eine Grafik umgesetzt wird. + ~~~ + Dot: + digraph G { + A -> B; + B -> C; + } + ~~~ + """); + + println(""" + Markdown: + ## Fazit + Das LVP Protokoll bietet viele Möglichkeiten, um Textausgaben zu transformieren und daraus mediale Darstellungen im Browser anzeigen zu lassen. + Hier wurden nur einige Beispiele gezeigt, aber es gibt noch viele weitere Möglichkeiten, wie zum Beispiel das Definieren von blockierenden Eingaben, das Testen von Code Snippets + oder das Einbinden von CSS. Einige weitere Beispiele sind im `examples` Ordner zu finden. Auch gibt es die Möglichkeit durch das Nutzen von mehreren Quellen die Browseransicht + in mehrere sogennanter "Subviews" aufzuteilen. Eine Übersicht aller Kommandos ist im `README.md` zu finden. + ~~~ + """); } // example diff --git a/src/main/resources/web/script.js b/src/main/resources/web/script.js index e3899d1..05decdd 100644 --- a/src/main/resources/web/script.js +++ b/src/main/resources/web/script.js @@ -134,7 +134,7 @@ function setUp() { newElement.id = id; subView.appendChild(newElement); Viz.instance().then(function(viz) { - newElement.appendChild(viz.renderSVGElement(dotString)); + newElement.appendChild(viz.renderSVGElement(data)); }); break; } From 02e4e724c2b69b4bd1fa294e381c37df593090e2 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 26 Feb 2026 21:54:40 +0100 Subject: [PATCH 55/57] =?UTF-8?q?chore:=20Kommando=C3=BCbersicht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 156 ++++++++++++++++++++++++++++-- examples/BlockierendeEingabe.java | 13 --- 2 files changed, 147 insertions(+), 22 deletions(-) delete mode 100644 examples/BlockierendeEingabe.java diff --git a/README.md b/README.md index 1002cdb..ef5c4b1 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ println(""" ~~~ """); ``` -Kommandos bestehen aus einem Namen und einem Inhalt. Dabei wird zwischen einzeiligen und mehrzeiligen Kommandos unterschieden. Ein mehrzeiliges Kommando wird durch `~~~` beendet. +Kommandos bestehen aus einem Namen und einem Inhalt. Dabei wird zwischen einzeiligen und mehrzeiligen Kommandos unterschieden. Ein mehrzeiliges Kommando wird durch `~~~` beendet. Die meisten Kommandos können sowohl einzeilig als auch mehrzeilig genutzt werden. Es gibt hier jedoch auch einige Ausnahmen, wie das Turtle-Kommando, das nur mehrzeilig genutzt werden kann oder das Codeblock-Kommando, das nur einzeilig genutzt werden kann.
@@ -117,15 +117,153 @@ println(""" ``` Durch Piping können die Ergebnisse eines Kommandos an ein anderes Kommando weitergegeben werden. -### Kommandos und Anweisungen -[Hier folgt eine Übersicht über alle Kommandos] -- Register -- Markdown -- Html -- Css -- Javascript -- .... +## Kommandos und Anweisungen +### Channelkommandos +Kommandos, wie `Markdown`, `Html` oder `Dot`, die Inhalte im Browser anzeigen, werden als Channelkommandos bezeichnet. Sie haben keine Ausgabe, die weiterverarbeitet werden könnte, da ihr Ergebnis direkt im Browser landet. + +#### Markdown +**Eingabe:** Markdown-Text + +**Ergebnis:** Umwandlung des Markdown-Textes in HTML und Anzeige im Browser. + +#### Html +**Eingabe:** HTML-Text + +**Ergebnis:** Einfügen des HTML-Textes in den Html-Body. + +#### Css +**Eingabe:** CSS-Text + +**Ergebnis:** Einbettung des CSS-Textes in einen Style-Tag im Browser. + +#### JavaScript +**Eingabe:** JavaScript-Code + +**Ergebnis:** Einfügen des JavaScript-Codes in einen Script-Tag im Browser. + +#### JavaScriptCall +**Eingabe:** JavaScript-Code + +**Ergebnis:** Ausführung des JavaScript-Codes im Browser. + +#### SubViewStyle +**Eingabe:** CSS-Text + +**Ergebnis:** Einbettung des CSS-Textes in eine CSS-Klasse, die auf die SubView angewendet wird, in der das Kommando ausgeführt wird. + +#### Dot +**Eingabe:** Graphenbeschreibung in der Dot-Sprache + +**Ergebnis:** Umwandlung der Graphenbeschreibung in eine Grafik und Anzeige im Browser. + +#### Clear +**Eingabe:** Keine + +**Ergebnis:** Löschen aller Inhalte im Browser. + +### Servicekommandos +Servicekommandos, wie `Text` oder `Codeblock`, haben eine Ausgabe, die weiterverarbeitet werden kann. Sie können zum Beispiel durch Piping an Channelkommandos weitergegeben werden, um sie im Browser anzuzeigen. + +#### Text +**Eingabe:** Text + +**Ausgabe:** Der eingegebene Text wird als Ausgabe zurückgegeben und kann weiterverarbeitet werden. + +Dieses Kommando hat zwei besondere Eigenschaften: +1. Es kann sich Inhalte merken, wenn eine ID angegeben wird. Diese Inhalte können später durch die Angabe der ID wieder abgerufen werden. +2. Es kann als Template genutzt werden, indem Platzhalter definiert werden. Diese werden aufgefüllt, wenn der gespeicherte Inhalt durch Piping weitere Eingaben erhält. + +#### Codeblock +**Eingabe:** `path;label` +- Path: Pfad zu einer Quelldatei +- Label: Kommentarlabel, das den Bereich in der Quelldatei markiert, der angezeigt werden soll. + +**Ausgabe:** Der Code-Abschnitt, der durch das Label markiert ist, wird als Ausgabe zurückgegeben, dazu Metainhalte, um den Code-Block im Browser interaktiv zu machen. + +#### Cutout +**Eingabe:** `path;label` +- Path: Pfad zu einer Quelldatei +- Label: Kommentarlabel, das den Bereich in der Quelldatei markiert, der angezeigt werden soll. + +**Ausgabe:** Der Code-Abschnitt, der durch das Label markiert ist, wird als Ausgabe zurückgegeben. + +(Anders als beim Codeblock-Kommando können hier keine interaktiven Code-Blöcke erzeugt werden) + +#### Turtle +**Eingabe:** +- `init WIDTH HEIGHT` -> Initialisiert die Zeichenfläche mit der angegebenen Breite und Höhe +- `init XMIN XMAX YMIN YMAX STARTX STARTY STARTANGLE` -> Initialisiert die Zeichenfläche mit den angegebenen Koordinaten und der Startposition der Schildkröte +- `penup` -> Hebt den Stift der Schildkröte +- `pendown` -> Senkt den Stift der Schildkröte +- `forward DISTANCE` -> Bewegt die Schildkröte um die angegebene Distanz vorwärts +- `backward DISTANCE` -> Bewegt die Schildkröte um die angegebene Distanz rückwärts +- `left ANGLE` -> Dreht die Schildkröte um den angegebenen Winkel nach links +- `right ANGLE` -> Dreht die Schildkröte um den angegebenen Winkel +- `color RGB[A]` -> Setzt die Farbe des Stifts der Schildkröte auf die angegebene Farbe, definiert durch die RGB-Werte und optional einem Alpha-Wert für die Transparenz +- `text TEXT [FONT]` -> Zeichnet den angegebenen Text an der aktuellen Position der Schildkröte, optional mit einer Schriftart +- `width WIDTH` -> Setzt die Breite des Stifts der Schildkröte auf die angegebene Breite +- `push` -> Speichert die aktuelle Position und Ausrichtung der Schildkröte auf einem Stack +- `pop` -> Stellt die zuletzt gespeicherte Position und Ausrichtung der Schildkröte wieder her +- `timeline` -> Erzeugt einen Slider im Browser, mit dem durch den Ausführungsverlauf der Turtle-Befehle navigiert werden kann +- `save NAME` -> Speichert die Turtle-Grafik unter dem angegebenen Namen als SVG-Datei + +**Ausgabe:** Die Turtle-Grafik als SVG-Text + +#### Test +Dieses Kommando ermöglicht es, Java-Code zu testen, indem es die Ausgabe des Codes mit einer erwarteten Ausgabe vergleicht. + +**Eingabe:** +- `Send CODE` -> Java-Code, der ausgewertet werden soll +- `Expect STRING` -> Erwartete Ausgabe des Java-Codes +- `Type TYPE` -> Optionaler Parameter, der den Typ des Vergleichs angibt: + - `Exact` (Standard): Erwartete Ausgabe muss genau mit der tatsächlichen Ausgabe übereinstimmen (Equals) + - `same`: Erwartete Ausgabe muss mit der tatsächlichen Ausgabe identisch sein (==) + - `oneof`: Erwartete Ausgabe muss in der tatsächlichen Ausgabe enthalten sein (contains) + +**Ausgabe:** Die Zusammenfassung und das Ergebnis des Tests. + +#### Button +**Eingabe:** +- `Text: TEXT` -> Text, der auf dem Button angezeigt wird +- `[width: WIDTH]` -> Optionaler Parameter, der die Breite des Buttons angibt +- `[height: HEIGHT]` -> Optionaler Parameter, der die Höhe des Buttons angibt +- `path: PATH` -> Pfad zu einer Quelldatei, in der Code ausgetauscht werden soll, wenn der Button geklickt wird +- `label: "LABEL"` -> Kommentarlabel, das den Bereich in der Quelldatei markiert, der ausgetauscht werden soll +- `replacement: REPLACEMENT` -> Der Code, der in die Quelldatei eingesetzt werden soll, wenn der Button geklickt wird. + +**Ausgabe:** HTML-Text für einen Button mit dazugehörigem JavaScript, der die angegebene Ersetzung in der Quelldatei vornimmt, wenn der Button geklickt wird. + +#### Input +**Eingabe:** +- `path: PATH` -> Pfad zu einer Quelldatei, in der Code ausgetauscht werden soll, wenn eine Eingabe bestätigt wird +- `label: "LABEL"` -> Kommentarlabel, das den Bereich in der Quelldatei markiert, der ausgetauscht werden soll +- `template: TEMPLATE` -> Das Template, in das die Eingabe des Input-Feldes eingesetzt werden soll. Der Platzhalter `$` im Template wird durch die Eingabe ersetzt. Das Resultat wird in die Quelldatei eingesetzt. +- `[placeholder: PLACEHOLDER]` -> Optionaler Parameter, der einen Platzhaltertext für das Eingabefeld angibt +- `[type: TYPE]` -> Optionaler Parameter, der den Typ des Eingabefelds angibt (z.B. "text", "number", "email") + +**Ausgabe:** HTML-Text für ein Eingabefeld mit dazugehörigem JavaScript, der die angegebene Ersetzung in der Quelldatei vornimmt, wenn eine Eingabe bestätigt wird. + +#### Checkbox +**Eingabe:** +- `path: PATH` -> Pfad zu einer Quelldatei, in der Code ausgetauscht werden soll, wenn die Checkbox aktiviert oder deaktiviert wird +- `label: "LABEL"` -> Kommentarlabel, das den Bereich in der Quelldatei markiert, der ausgetauscht werden soll +- `template: TEMPLATE` -> Das Template, in das der aktuelle Wert der Checkbox eingesetzt werden soll. Der Platzhalter `$` im Template wird durch den aktuellen Wert der Checkbox (true oder false) ersetzt. Das Resultat wird in die Quelldatei eingesetzt. + +**Ausgabe:** HTML-Text für eine Checkbox mit dazugehörigem JavaScript, der die angegebene Ersetzung in der Quelldatei vornimmt, wenn die Checkbox aktiviert oder deaktiviert wird. + +### Scankommandos +Scankommandos werden genutzt, um Informationen zurück an die Quelle zu senden. + +#### CommandScan +**Eingabe:** Keine (Kann nur durch Piping einen Input erhalten) + +**Ausgabe:** Das Kommando empfängt die Ausgabe des vorherigen Kommandos und sendet sie zurück an die Quelle, die das Kommando ursprünglich ausgeführt hat. + +#### InputScan +**Eingabe:** Keine + +**Ausgabe:** Das Kommando erzeugt ein Eingabefeld im Browser und sendet die Benutzereingabe zurück an die Quelle, die das Kommando ursprünglich ausgeführt hat. ## Troubleshooting diff --git a/examples/BlockierendeEingabe.java b/examples/BlockierendeEingabe.java deleted file mode 100644 index a7f1674..0000000 --- a/examples/BlockierendeEingabe.java +++ /dev/null @@ -1,13 +0,0 @@ -import static java.io.IO.println; - -import java.util.Scanner; - -void main() { - println("Clear"); - println("Markdown: # Blocking Input"); - println("Scan:"); - Scanner scanner = new Scanner(System.in); - String d = scanner.nextLine(); - - println("Markdown: Your input was: **" + d + "**"); -} \ No newline at end of file From 285fde0f35b67256f2a32baeea3d8c5852a2d2eb Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 26 Feb 2026 22:00:53 +0100 Subject: [PATCH 56/57] chore: Register und IDs --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index ef5c4b1..485b810 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,17 @@ println(""" ``` Durch Piping können die Ergebnisse eines Kommandos an ein anderes Kommando weitergegeben werden. +Desweiteren können Kommandos IDs zugewiesen werden. Das hat unterschiedliche Kommandoabhängige Auswirkungen, z.B. können Kommandos mit IDs später durch die ID wieder aufgerufen werden, oder sie werden im Browser mit dieser ID versehen, um sie z.B. durch JavaScript oder CSS ansprechen zu können. + ## Kommandos und Anweisungen +### Register +Das Kommando `Register` ermöglicht es, eigene CLI-Anwendungen unter einem bestimmten Namen zu registrieren. Auf diese Weise können eigene Kommandos definiert und genutzt werden. Die Kommandoinhalte werden über STDIN an die registrierte Anwendung übergeben. Das Ergebnis der Anwendung wird über STDOUT ausgelesen und kann weiterverarbeitet werden. +Normalerweise erwarten die Kommandos neben dem Inhalt auch eine Kommando ID. Dieses Verhalten kann beim Registrieren unterdrückt werden. + +- `Register NAME CMD` +- `Register[skipId] NAME CMD` + ### Channelkommandos Kommandos, wie `Markdown`, `Html` oder `Dot`, die Inhalte im Browser anzeigen, werden als Channelkommandos bezeichnet. Sie haben keine Ausgabe, die weiterverarbeitet werden könnte, da ihr Ergebnis direkt im Browser landet. From 1512031fd17ec58a257a63c48ce1e1786c002e1e Mon Sep 17 00:00:00 2001 From: denkspuren Date: Fri, 10 Apr 2026 18:23:37 +0200 Subject: [PATCH 57/57] Formatierung angepasst --- README.md | 63 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 485b810..3412a89 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Das Protokoll dient als sprachunabhängige Schnittstelle zwischen Ihrem eigenen Auf diese Weise stellt LVP praktische Funktionen bereit, die sich beispielsweise für die Code-Dokumentation, die Erzeugung von Turtle-Grafiken oder das Erstellen interaktiver HTML-Elemente nutzen lassen. - - ## 🚀 Nutze das _Live View Programming_ Wenn Sie das _Live View Programming_ ausprobieren möchten, ist Folgendes zu tun: @@ -131,67 +129,82 @@ Normalerweise erwarten die Kommandos neben dem Inhalt auch eine Kommando ID. Die ### Channelkommandos Kommandos, wie `Markdown`, `Html` oder `Dot`, die Inhalte im Browser anzeigen, werden als Channelkommandos bezeichnet. Sie haben keine Ausgabe, die weiterverarbeitet werden könnte, da ihr Ergebnis direkt im Browser landet. -#### Markdown +#### 🟩 `Markdown` + **Eingabe:** Markdown-Text **Ergebnis:** Umwandlung des Markdown-Textes in HTML und Anzeige im Browser. -#### Html +#### 🟩 `Html` + **Eingabe:** HTML-Text **Ergebnis:** Einfügen des HTML-Textes in den Html-Body. -#### Css +#### 🟩 `Css` + **Eingabe:** CSS-Text **Ergebnis:** Einbettung des CSS-Textes in einen Style-Tag im Browser. -#### JavaScript +#### 🟩 `JavaScript` + **Eingabe:** JavaScript-Code **Ergebnis:** Einfügen des JavaScript-Codes in einen Script-Tag im Browser. -#### JavaScriptCall +#### 🟩 `JavaScriptCall` + **Eingabe:** JavaScript-Code **Ergebnis:** Ausführung des JavaScript-Codes im Browser. -#### SubViewStyle +#### 🟩 `SubViewStyle` + **Eingabe:** CSS-Text **Ergebnis:** Einbettung des CSS-Textes in eine CSS-Klasse, die auf die SubView angewendet wird, in der das Kommando ausgeführt wird. -#### Dot +#### 🟩 `Dot` + **Eingabe:** Graphenbeschreibung in der Dot-Sprache **Ergebnis:** Umwandlung der Graphenbeschreibung in eine Grafik und Anzeige im Browser. -#### Clear +#### 🟩 `Clear` + **Eingabe:** Keine **Ergebnis:** Löschen aller Inhalte im Browser. ### Servicekommandos + Servicekommandos, wie `Text` oder `Codeblock`, haben eine Ausgabe, die weiterverarbeitet werden kann. Sie können zum Beispiel durch Piping an Channelkommandos weitergegeben werden, um sie im Browser anzuzeigen. -#### Text +#### 🟦 `Text` + **Eingabe:** Text **Ausgabe:** Der eingegebene Text wird als Ausgabe zurückgegeben und kann weiterverarbeitet werden. Dieses Kommando hat zwei besondere Eigenschaften: + 1. Es kann sich Inhalte merken, wenn eine ID angegeben wird. Diese Inhalte können später durch die Angabe der ID wieder abgerufen werden. 2. Es kann als Template genutzt werden, indem Platzhalter definiert werden. Diese werden aufgefüllt, wenn der gespeicherte Inhalt durch Piping weitere Eingaben erhält. -#### Codeblock +#### 🟦 `Codeblock` + **Eingabe:** `path;label` + - Path: Pfad zu einer Quelldatei - Label: Kommentarlabel, das den Bereich in der Quelldatei markiert, der angezeigt werden soll. **Ausgabe:** Der Code-Abschnitt, der durch das Label markiert ist, wird als Ausgabe zurückgegeben, dazu Metainhalte, um den Code-Block im Browser interaktiv zu machen. -#### Cutout +#### 🟦 `Cutout` + **Eingabe:** `path;label` + - Path: Pfad zu einer Quelldatei - Label: Kommentarlabel, das den Bereich in der Quelldatei markiert, der angezeigt werden soll. @@ -199,7 +212,8 @@ Dieses Kommando hat zwei besondere Eigenschaften: (Anders als beim Codeblock-Kommando können hier keine interaktiven Code-Blöcke erzeugt werden) -#### Turtle +#### 🟦 `Turtle` + **Eingabe:** - `init WIDTH HEIGHT` -> Initialisiert die Zeichenfläche mit der angegebenen Breite und Höhe - `init XMIN XMAX YMIN YMAX STARTX STARTY STARTANGLE` -> Initialisiert die Zeichenfläche mit den angegebenen Koordinaten und der Startposition der Schildkröte @@ -219,10 +233,11 @@ Dieses Kommando hat zwei besondere Eigenschaften: **Ausgabe:** Die Turtle-Grafik als SVG-Text -#### Test +#### 🟦 `Test` Dieses Kommando ermöglicht es, Java-Code zu testen, indem es die Ausgabe des Codes mit einer erwarteten Ausgabe vergleicht. **Eingabe:** + - `Send CODE` -> Java-Code, der ausgewertet werden soll - `Expect STRING` -> Erwartete Ausgabe des Java-Codes - `Type TYPE` -> Optionaler Parameter, der den Typ des Vergleichs angibt: @@ -232,8 +247,10 @@ Dieses Kommando ermöglicht es, Java-Code zu testen, indem es die Ausgabe des Co **Ausgabe:** Die Zusammenfassung und das Ergebnis des Tests. -#### Button +#### 🟦 `Button` + **Eingabe:** + - `Text: TEXT` -> Text, der auf dem Button angezeigt wird - `[width: WIDTH]` -> Optionaler Parameter, der die Breite des Buttons angibt - `[height: HEIGHT]` -> Optionaler Parameter, der die Höhe des Buttons angibt @@ -243,8 +260,10 @@ Dieses Kommando ermöglicht es, Java-Code zu testen, indem es die Ausgabe des Co **Ausgabe:** HTML-Text für einen Button mit dazugehörigem JavaScript, der die angegebene Ersetzung in der Quelldatei vornimmt, wenn der Button geklickt wird. -#### Input +#### 🟦 `Input` + **Eingabe:** + - `path: PATH` -> Pfad zu einer Quelldatei, in der Code ausgetauscht werden soll, wenn eine Eingabe bestätigt wird - `label: "LABEL"` -> Kommentarlabel, das den Bereich in der Quelldatei markiert, der ausgetauscht werden soll - `template: TEMPLATE` -> Das Template, in das die Eingabe des Input-Feldes eingesetzt werden soll. Der Platzhalter `$` im Template wird durch die Eingabe ersetzt. Das Resultat wird in die Quelldatei eingesetzt. @@ -253,8 +272,10 @@ Dieses Kommando ermöglicht es, Java-Code zu testen, indem es die Ausgabe des Co **Ausgabe:** HTML-Text für ein Eingabefeld mit dazugehörigem JavaScript, der die angegebene Ersetzung in der Quelldatei vornimmt, wenn eine Eingabe bestätigt wird. -#### Checkbox +#### 🟦 `Checkbox` + **Eingabe:** + - `path: PATH` -> Pfad zu einer Quelldatei, in der Code ausgetauscht werden soll, wenn die Checkbox aktiviert oder deaktiviert wird - `label: "LABEL"` -> Kommentarlabel, das den Bereich in der Quelldatei markiert, der ausgetauscht werden soll - `template: TEMPLATE` -> Das Template, in das der aktuelle Wert der Checkbox eingesetzt werden soll. Der Platzhalter `$` im Template wird durch den aktuellen Wert der Checkbox (true oder false) ersetzt. Das Resultat wird in die Quelldatei eingesetzt. @@ -264,12 +285,14 @@ Dieses Kommando ermöglicht es, Java-Code zu testen, indem es die Ausgabe des Co ### Scankommandos Scankommandos werden genutzt, um Informationen zurück an die Quelle zu senden. -#### CommandScan +#### 🟧 `CommandScan` + **Eingabe:** Keine (Kann nur durch Piping einen Input erhalten) **Ausgabe:** Das Kommando empfängt die Ausgabe des vorherigen Kommandos und sendet sie zurück an die Quelle, die das Kommando ursprünglich ausgeführt hat. -#### InputScan +#### 🟧 `InputScan` + **Eingabe:** Keine **Ausgabe:** Das Kommando erzeugt ein Eingabefeld im Browser und sendet die Benutzereingabe zurück an die Quelle, die das Kommando ursprünglich ausgeführt hat.