From 4ca854f6c2ed1e2fa136fa8aface968be50537a2 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Mon, 18 May 2026 13:05:00 +0200 Subject: [PATCH] some fixes for markdown --- .../cz/kamma/kfmanager/ui/FileEditor.java | 186 ++++++++++++++++-- 1 file changed, 172 insertions(+), 14 deletions(-) diff --git a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java index 771a215..ecc361c 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java @@ -831,6 +831,9 @@ public class FileEditor extends JFrame { styles.addRule("blockquote { margin: 0 0 10px 12px; padding-left: 10px; border-left: 3px solid #808080; }"); styles.addRule("pre { font-family: Monospaced; font-size: " + fontSize + "pt; margin: 0 0 10px 0; padding: 8px; }"); styles.addRule("code { font-family: Monospaced; }"); + styles.addRule("table { border-collapse: collapse; margin: 0 0 10px 0; }"); + styles.addRule("th, td { border: 1px solid #808080; padding: 4px 8px; }"); + styles.addRule("th { font-weight: bold; }"); styles.addRule("a { color: #2f7ed8; }"); return kit; } @@ -838,15 +841,17 @@ public class FileEditor extends JFrame { private String markdownToHtml(String markdown) { StringBuilder body = new StringBuilder(); String[] lines = markdown.replace("\r\n", "\n").replace('\r', '\n').split("\n", -1); - StringBuilder paragraph = new StringBuilder(); + java.util.List paragraph = new java.util.ArrayList<>(); StringBuilder list = null; String listTag = null; boolean inFence = false; StringBuilder fence = new StringBuilder(); - for (String line : lines) { + for (int lineIndex = 0; lineIndex < lines.length; lineIndex++) { + String line = lines[lineIndex]; String trimmed = line.trim(); - if (trimmed.startsWith("```")) { + String fenceLine = normalizeFenceMarker(trimmed); + if (fenceLine.startsWith("```")) { if (inFence) { body.append("
").append(escapeHtml(fence.toString())).append("
"); fence.setLength(0); @@ -855,7 +860,14 @@ public class FileEditor extends JFrame { flushParagraph(body, paragraph); list = flushList(body, list, listTag); listTag = null; - inFence = true; + String openerRest = fenceLine.substring(3).trim(); + int sameLineClose = findFenceClose(openerRest); + if (sameLineClose >= 0) { + String code = stripFenceLanguage(openerRest.substring(0, sameLineClose).trim()); + body.append("
").append(escapeHtml(code)).append("
"); + } else { + inFence = true; + } } continue; } @@ -888,6 +900,14 @@ public class FileEditor extends JFrame { continue; } + if (isTableStart(lines, lineIndex)) { + flushParagraph(body, paragraph); + list = flushList(body, list, listTag); + listTag = null; + lineIndex = appendMarkdownTable(body, lines, lineIndex); + continue; + } + Matcher unordered = Pattern.compile("^[-*+]\\s+(.+)$").matcher(trimmed); Matcher ordered = Pattern.compile("^\\d+[.)]\\s+(.+)$").matcher(trimmed); if (unordered.matches() || ordered.matches()) { @@ -914,10 +934,11 @@ public class FileEditor extends JFrame { continue; } - if (paragraph.length() > 0) { - paragraph.append(' '); + if (list != null) { + list = flushList(body, list, listTag); + listTag = null; } - paragraph.append(trimmed); + paragraph.add(trimmed); } if (inFence) { @@ -928,10 +949,112 @@ public class FileEditor extends JFrame { return "" + body + ""; } - private void flushParagraph(StringBuilder body, StringBuilder paragraph) { - if (paragraph.length() == 0) return; - body.append("

").append(renderInline(paragraph.toString())).append("

"); - paragraph.setLength(0); + private String normalizeFenceMarker(String text) { + return text.replace("\\`", "`"); + } + + private int findFenceClose(String text) { + int plain = text.indexOf("```"); + int escaped = text.indexOf("\\`\\`\\`"); + if (plain < 0) return escaped; + if (escaped < 0) return plain; + return Math.min(plain, escaped); + } + + private String stripFenceLanguage(String text) { + int spaceIndex = text.indexOf(' '); + if (spaceIndex <= 0) return text; + String firstWord = text.substring(0, spaceIndex); + if (firstWord.matches("[A-Za-z][A-Za-z0-9_+.-]*")) { + return text.substring(spaceIndex + 1).trim(); + } + return text; + } + + private boolean isTableStart(String[] lines, int lineIndex) { + if (lineIndex + 1 >= lines.length) return false; + java.util.List header = splitMarkdownTableRow(lines[lineIndex].trim()); + java.util.List separator = splitMarkdownTableRow(lines[lineIndex + 1].trim()); + if (header.size() < 2 || separator.size() != header.size()) return false; + for (String cell : separator) { + if (!cell.trim().matches(":?-{3,}:?")) { + return false; + } + } + return true; + } + + private int appendMarkdownTable(StringBuilder body, String[] lines, int startIndex) { + java.util.List header = splitMarkdownTableRow(lines[startIndex].trim()); + body.append(""); + for (String cell : header) { + body.append(""); + } + body.append(""); + + int lineIndex = startIndex + 2; + while (lineIndex < lines.length) { + String trimmed = lines[lineIndex].trim(); + if (trimmed.isEmpty()) break; + java.util.List cells = splitMarkdownTableRow(trimmed); + if (cells.size() != header.size()) break; + body.append(""); + for (String cell : cells) { + body.append(""); + } + body.append(""); + lineIndex++; + } + + body.append("
").append(renderInline(cell.trim())).append("
").append(renderInline(cell.trim())).append("
"); + return lineIndex - 1; + } + + private java.util.List splitMarkdownTableRow(String row) { + java.util.List cells = new java.util.ArrayList<>(); + if (!row.contains("|")) return cells; + String content = row; + if (content.startsWith("|")) { + content = content.substring(1); + } + if (content.endsWith("|")) { + content = content.substring(0, content.length() - 1); + } + + StringBuilder cell = new StringBuilder(); + boolean escaped = false; + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + if (escaped) { + cell.append(c); + escaped = false; + } else if (c == '\\') { + escaped = true; + } else if (c == '|') { + cells.add(cell.toString()); + cell.setLength(0); + } else { + cell.append(c); + } + } + if (escaped) { + cell.append('\\'); + } + cells.add(cell.toString()); + return cells; + } + + private void flushParagraph(StringBuilder body, java.util.List paragraph) { + if (paragraph.isEmpty()) return; + body.append("

"); + for (int i = 0; i < paragraph.size(); i++) { + if (i > 0) { + body.append("
"); + } + body.append(renderInline(paragraph.get(i))); + } + body.append("

"); + paragraph.clear(); } private StringBuilder flushList(StringBuilder body, StringBuilder list, String listTag) { @@ -952,25 +1075,60 @@ public class FileEditor extends JFrame { Matcher codeMatcher = Pattern.compile("`([^`]+)`").matcher(text); StringBuffer protectedText = new StringBuffer(); while (codeMatcher.find()) { - String token = "{{KF_MD_CODE_" + codeSpans.size() + "}}"; + String token = "%%KFCODE" + codeSpans.size() + "%%"; codeSpans.add("" + escapeHtml(codeMatcher.group(1)) + ""); codeMatcher.appendReplacement(protectedText, Matcher.quoteReplacement(token)); } codeMatcher.appendTail(protectedText); - String html = escapeHtml(protectedText.toString()); + java.util.List escapedChars = new java.util.ArrayList<>(); + String escapesProtected = protectMarkdownEscapes(protectedText.toString(), escapedChars); + String html = escapeHtml(escapesProtected); html = html.replaceAll("\\*\\*([^*]+)\\*\\*", "$1"); html = html.replaceAll("__([^_]+)__", "$1"); html = html.replaceAll("(?$1"); html = html.replaceAll("(?$1"); html = replaceLinks(html); + for (int i = 0; i < escapedChars.size(); i++) { + html = html.replace("%%KFESC" + i + "%%", escapedChars.get(i)); + } for (int i = 0; i < codeSpans.size(); i++) { - html = html.replace("{{KF_MD_CODE_" + i + "}}", codeSpans.get(i)); + html = html.replace("%%KFCODE" + i + "%%", codeSpans.get(i)); } return html; } + private String protectMarkdownEscapes(String text, java.util.List escapedChars) { + StringBuilder sb = new StringBuilder(text.length()); + boolean escaped = false; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (escaped) { + if (isMarkdownEscapable(c)) { + String token = "%%KFESC" + escapedChars.size() + "%%"; + escapedChars.add(escapeHtml(String.valueOf(c))); + sb.append(token); + } else { + sb.append('\\').append(c); + } + escaped = false; + } else if (c == '\\') { + escaped = true; + } else { + sb.append(c); + } + } + if (escaped) { + sb.append('\\'); + } + return sb.toString(); + } + + private boolean isMarkdownEscapable(char c) { + return "\\`*_{}[]<>()#+-.!|/:".indexOf(c) >= 0; + } + private String replaceLinks(String html) { Matcher m = Pattern.compile("\\[([^\\]]+)]\\(([^\\s)]+)\\)").matcher(html); StringBuffer sb = new StringBuffer();