From 8b139b1bed91547db89ac24be8a67e853cd4861c Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 19 May 2026 10:46:28 +0200 Subject: [PATCH] moved markdown rendering from Util to MarkdownApi Signed-off-by: Stephan Richter --- .../umbrella/backend/Application.java | 2 - .../umbrella/core/ModuleRegistry.java | 1 - .../de/srsoftware/umbrella/core/Util.java | 77 +---------- .../umbrella/core/api/MarkdownService.java | 1 + .../srsoftware/umbrella/core/model/Poll.java | 10 +- .../umbrella/legacy/NotesLegacy.java | 6 +- .../umbrella/markdown/MarkdownApi.java | 130 +++++++++++++++--- 7 files changed, 118 insertions(+), 109 deletions(-) diff --git a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java index b171dac2..82bd368b 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -63,8 +63,6 @@ public class Application { var port = config.get("umbrella.http.port", 8080); var threads = config.get("umbrella.threads", 16); - config.get("umbrella.plantuml").map(Object::toString).map(File::new).filter(File::exists).ifPresent(Util::setPlantUmlJar); - var server = HttpServer.create(new InetSocketAddress(port), 0); try { new Translations(config).bindPath("/api/translations").on(server); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java b/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java index 55e4a664..deedaae9 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java @@ -1,7 +1,6 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core; - import de.srsoftware.umbrella.core.api.*; public class ModuleRegistry { diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Util.java b/core/src/main/java/de/srsoftware/umbrella/core/Util.java index 7f38839d..5bc43929 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Util.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Util.java @@ -7,6 +7,7 @@ import static de.srsoftware.tools.PathHandler.GET; import static de.srsoftware.tools.PathHandler.POST; import static de.srsoftware.tools.Strings.hex; import static de.srsoftware.umbrella.core.Errors.INVALID_URL; +import static de.srsoftware.umbrella.core.ModuleRegistry.markdownService; import static de.srsoftware.umbrella.core.constants.Constants.TIME_FORMATTER; import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.serverError; @@ -35,14 +36,9 @@ import java.util.regex.Pattern; import org.json.JSONObject; public class Util { - public static final System.Logger LOG = System.getLogger("Util"); - private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL); - private static final Pattern SPREADSHEET_PATTERN = Pattern.compile("@startsheet(.*?)@endsheet",Pattern.DOTALL); - private static File plantumlJar = null; - private static final JParsedown MARKDOWN = new JParsedown(); - public static final String SHA1 = "SHA-1"; + public static final System.Logger LOG = System.getLogger("Util"); + public static final String SHA1 = "SHA-1"; private static final MessageDigest SHA1_DIGEST; - private static final Map umlCache = new HashMap<>(); private static final String SCRIPT = """ @@ -111,71 +107,11 @@ jspreadsheet(document.getElementById('spreadsheet'), { public static HashMap mapMarkdown(String source){ var map = new HashMap(); map.put(SOURCE,source); - map.put(RENDERED,markdown(source)); + map.put(RENDERED,markdownService().markdown(source)); return map; } - public static String markdown(String source){ - if (source == null) return source; - try { - var matcher = SPREADSHEET_PATTERN.matcher(source); - var count = 0; - while (matcher.find()){ - count++; - var sheetData = matcher.group(0).trim(); - var start = matcher.start(0); - var end = matcher.end(0); - source = source.substring(0, start) - + "
" - + sheetData.substring(11,sheetData.length()-10) - + "
" - + source.substring(end); - matcher = SPREADSHEET_PATTERN.matcher(source); - } - if (plantumlJar != null && plantumlJar.exists()) { - matcher = UML_PATTERN.matcher(source); - while (matcher.find()) { - var uml = matcher.group(0).trim(); - var start = matcher.start(0); - var end = matcher.end(0); - var umlHash = uml.hashCode(); - LOG.log(DEBUG,"Hash of Plantuml code: {0}",umlHash); - var svg = umlCache.get(umlHash); - if (svg != null){ - LOG.log(DEBUG,"Serving Plantuml generated SVG from cache…"); - source = source.substring(0, start) + svg + source.substring(end); - matcher = UML_PATTERN.matcher(source); - continue; - } - - LOG.log(DEBUG,"Cache miss. Generating SVG from plantuml code…"); - ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", plantumlJar.getAbsolutePath(), "-tsvg", "-pipe"); - var ignored = processBuilder.redirectErrorStream(); - var process = processBuilder.start(); - try (OutputStream os = process.getOutputStream()) { - os.write(uml.getBytes(UTF_8)); - os.flush(); - } - - try (InputStream is = process.getInputStream()) { - byte[] out = is.readAllBytes(); - LOG.log(DEBUG,"Generated SVG. Pushing to cache…"); - svg = new String(out, UTF_8); - umlCache.put(umlHash,svg); - source = source.substring(0, start) + svg + source.substring(end); - matcher = UML_PATTERN.matcher(source); - } - } - } - return MARKDOWN.text(source); - } catch (Throwable e){ - if (LOG.isLoggable(TRACE)){ - LOG.log(TRACE,"Failed to render markdown, input was: \n{0}",source,e); - } else LOG.log(WARNING,"Failed to render markdown. Enable TRACE log level for details."); - return source; - } - } public static HttpURLConnection open(URL url) throws IOException { var conn = (HttpURLConnection) url.openConnection(); @@ -249,11 +185,6 @@ jspreadsheet(document.getElementById('spreadsheet'), { return new Hash(hex(bytes),SHA1); } - public static void setPlantUmlJar(File file){ - LOG.log(INFO,"Using plantuml @ {0}",file.getAbsolutePath()); - plantumlJar = file; - } - public static String dateTimeOf(long epochMilis){ return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilis), ZoneId.systemDefault()).format(TIME_FORMATTER); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/MarkdownService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/MarkdownService.java index 1cc510fc..8437ad43 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/MarkdownService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/MarkdownService.java @@ -2,4 +2,5 @@ package de.srsoftware.umbrella.core.api; public interface MarkdownService { + String markdown(String source); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Poll.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Poll.java index 8b92e731..b51ef60e 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Poll.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Poll.java @@ -91,10 +91,7 @@ public class Poll implements Mappable { return Map.of( ID,id, NAME,name, - DESCRIPTION, Map.of( - SOURCE,description, - RENDERED,Util.markdown(description) - ), + DESCRIPTION, Util.mapMarkdown(description), STATUS,status ); } @@ -292,10 +289,7 @@ public class Poll implements Mappable { ID, id, Field.OWNER, owner.toMap(), NAME,name, - Field.DESCRIPTION, Map.of( - Field.SOURCE,description, - Field.RENDERED,Util.markdown(description) - ), + Field.DESCRIPTION, Util.mapMarkdown(description), Field.OPTIONS, options.stream().collect(Collectors.toMap(Option::id,Option::toMap)), Field.PERMISSION, mapPermissions(), Field.PRIVATE, isPrivate, diff --git a/legacy/src/main/java/de/srsoftware/umbrella/legacy/NotesLegacy.java b/legacy/src/main/java/de/srsoftware/umbrella/legacy/NotesLegacy.java index b9ec4cf3..b9fa3b43 100644 --- a/legacy/src/main/java/de/srsoftware/umbrella/legacy/NotesLegacy.java +++ b/legacy/src/main/java/de/srsoftware/umbrella/legacy/NotesLegacy.java @@ -3,9 +3,7 @@ package de.srsoftware.umbrella.legacy; import static de.srsoftware.tools.Optionals.nullable; -import static de.srsoftware.umbrella.core.ModuleRegistry.noteService; -import static de.srsoftware.umbrella.core.ModuleRegistry.userService; -import static de.srsoftware.umbrella.core.Util.markdown; +import static de.srsoftware.umbrella.core.ModuleRegistry.*; import static de.srsoftware.umbrella.core.constants.Field.TOKEN; import static de.srsoftware.umbrella.core.constants.Field.URI; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; @@ -72,7 +70,7 @@ public class NotesLegacy extends BaseHandler { new Tag("fieldset") .add(new Tag("legend").content(authorName)) .add(new Tag("legend").content(note.timestamp().format(DateTimeFormatter.ISO_DATE_TIME))) - .add(new Tag("div").content(markdown(note.text()))) + .add(new Tag("div").content(markdownService().markdown(note.text()))) .addTo(html); } return sendContent(ex,html.toString(2)); diff --git a/markdown/src/main/java/de/srsoftware/umbrella/markdown/MarkdownApi.java b/markdown/src/main/java/de/srsoftware/umbrella/markdown/MarkdownApi.java index 8a94feca..0c417eb8 100644 --- a/markdown/src/main/java/de/srsoftware/umbrella/markdown/MarkdownApi.java +++ b/markdown/src/main/java/de/srsoftware/umbrella/markdown/MarkdownApi.java @@ -5,8 +5,11 @@ import static de.srsoftware.tools.MimeType.MIME_HTML; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; import static de.srsoftware.umbrella.core.constants.Field.BASE_URL; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; +import static java.lang.System.Logger.Level.*; +import static java.nio.charset.StandardCharsets.UTF_8; import com.sun.net.httpserver.HttpExchange; +import com.xrbpowered.jparsedown.JParsedown; import de.srsoftware.configuration.Configuration; import de.srsoftware.tools.Path; import de.srsoftware.umbrella.core.BaseHandler; @@ -15,22 +18,38 @@ import de.srsoftware.umbrella.core.Util; import de.srsoftware.umbrella.core.api.MarkdownService; import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; + +import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; public class MarkdownApi extends BaseHandler implements MarkdownService { - private static Pattern ANCHOR = Pattern.compile("(?is)]*>.*?"); - private static Pattern HREF = Pattern.compile("(?i)\\bhref\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))"); - private static Pattern TARGET = Pattern.compile("(?i)\\btarget\\s*="); + private static final System.Logger LOG = System.getLogger(MarkdownApi.class.getSimpleName()); + private static final Pattern PATTERN_ANCHOR = Pattern.compile("(?is)]*>.*?"); + private static final Pattern PATTERN_HREF = Pattern.compile("(?i)\\bhref\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))"); + private static final Pattern PATTERN_SPREADSHEET = Pattern.compile("@startsheet(.*?)@endsheet",Pattern.DOTALL); + private static final Pattern PATTERN_TARGET = Pattern.compile("(?i)\\btarget\\s*="); + private static final Pattern PATTERN_URL = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL); + private static final Map umlCache = new HashMap<>(); + private static final JParsedown MARKDOWN = new JParsedown(); + + private final String baseUrl; + private final File plantumlJar; public MarkdownApi(Configuration config) { super(); Optional baseUrl = config.get(BASE_URL); if (baseUrl.isEmpty()) throw missingField(BASE_URL); this.baseUrl = baseUrl.get(); + plantumlJar = config.get("umbrella.plantuml").map(Object::toString).map(File::new).filter(File::exists).orElse(null); + if (plantumlJar != null) LOG.log(INFO,"Using plant uml @ {}",plantumlJar); ModuleRegistry.add(this); } @@ -41,29 +60,98 @@ public class MarkdownApi extends BaseHandler implements MarkdownService { var user = userService().refreshSession(ex); if (user.isEmpty()) throw UmbrellaException.forbidden("You must be logged in to use the markdown renderer!"); - var rendered = Util.markdown(body(ex)); - var anchors = ANCHOR.matcher(rendered); - while (anchors.find()){ - String anchor = anchors.group(); - var urls = HREF.matcher(anchor); - if (!urls.find()) continue; // no url → nothing to do + var rendered = markdown(body(ex)); - var href = urls.group(1) != null ? urls.group(1) : (urls.group(2) != null ? urls.group(2) : urls.group(3)); - var target = TARGET.matcher(anchor); - if (target.find()) continue; // target already set → leave untouched - - if (!href.startsWith("http")) continue; // relative url? good! - - if (!href.startsWith(baseUrl)) { // add target - var replacement = "" + + sheetData.substring(11,sheetData.length()-10) + + "" + + source.substring(end); + LOG.log(DEBUG,"Updated markdown with spreadsheet div."); + matcher.reset(source); + } + if (plantumlJar != null && plantumlJar.exists()) { + matcher = PATTERN_URL.matcher(source); + while (matcher.find()) { + var uml = matcher.group(0).trim(); + var start = matcher.start(0); + var end = matcher.end(0); + + var umlHash = uml.hashCode(); + LOG.log(DEBUG,"Hash of Plantuml code: {0}",umlHash); + var svg = umlCache.get(umlHash); + if (svg != null){ + LOG.log(DEBUG,"Serving Plantuml generated SVG from cache…"); + source = source.substring(0, start) + svg + source.substring(end); + matcher.reset(source); + continue; + } + + LOG.log(DEBUG,"Cache miss. Generating SVG from plantuml code…"); + ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", plantumlJar.getAbsolutePath(), "-tsvg", "-pipe"); + var ignored = processBuilder.redirectErrorStream(); + var process = processBuilder.start(); + try (OutputStream os = process.getOutputStream()) { + os.write(uml.getBytes(UTF_8)); + os.flush(); + } + + try (InputStream is = process.getInputStream()) { + byte[] out = is.readAllBytes(); + LOG.log(DEBUG,"Generated SVG. Pushing to cache…"); + svg = new String(out, UTF_8); + umlCache.put(umlHash,svg); + source = source.substring(0, start) + svg + source.substring(end); + matcher.reset(source); + } + } + } + var rendered = MARKDOWN.text(source); + if (baseUrl == null) return rendered; + var anchors = PATTERN_ANCHOR.matcher(rendered); + while (anchors.find()){ + String anchor = anchors.group(); + LOG.log(TRACE,"Processing anchor: {}",anchor); + var urls = PATTERN_HREF.matcher(anchor); + if (!urls.find()) continue; // no url → nothing to do + + var href = urls.group(1) != null ? urls.group(1) : (urls.group(2) != null ? urls.group(2) : urls.group(3)); + LOG.log(TRACE," encountered href = {}",href); + if (!href.startsWith("http")) continue; // relative url? good! + LOG.log(TRACE," {} is not a relative url!",href); + var target = PATTERN_TARGET.matcher(anchor); + if (target.find()) continue; // target already set → leave untouched + LOG.log(TRACE," anchor has no target!"); + if (href.startsWith(baseUrl)) continue; // local url → don`t touch + LOG.log(TRACE," {} is not an internal url, adding anchor…",href); + var replacement = "