Tue, 06 Jan 2026 20:08:54 +0100
add Markdown preview - resolves #774
--- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt Tue Jan 06 20:08:54 2026 +0100 @@ -29,7 +29,7 @@ /** * A data in yyyy-mm-dd format to identify the release. */ - const val VERSION_DATE = "2025-10-09" + const val VERSION_DATE = "2026-01-06" /** * The path where the JSP files reside.
--- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Jan 06 20:08:54 2026 +0100 @@ -221,6 +221,11 @@ response.writer.write(json) } + fun renderText(text: String, type: String = "plain") { + response.contentType = "text/${type}; charset=utf-8" + response.writer.write(text) + } + fun render(page: String? = null) { page?.let { contentPage = it } forward("site")
--- a/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt Tue Jan 06 20:08:54 2026 +0100 @@ -1,5 +1,11 @@ package de.uapcore.lightpit.logic +import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension +import com.vladsch.flexmark.ext.tables.TablesExtension +import com.vladsch.flexmark.html.HtmlRenderer +import com.vladsch.flexmark.parser.Parser +import com.vladsch.flexmark.util.data.MutableDataSet +import com.vladsch.flexmark.util.data.SharedDataKeys import de.uapcore.lightpit.HttpRequest import de.uapcore.lightpit.dao.DataAccessObject import de.uapcore.lightpit.dateOptValidator @@ -283,4 +289,25 @@ // always pretend that the operation was successful - if there was nothing to remove, it's okay http.renderCommit("${pathInfos.issuesHref}${issue.id}") -} \ No newline at end of file +} + +fun formatMarkdown(text: String?): String { + if (text == null) return "" + + val options = MutableDataSet() + .set(SharedDataKeys.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create())) + val parser = Parser.builder(options).build() + val renderer = HtmlRenderer.builder( + options + .set(HtmlRenderer.ESCAPE_HTML, true) + ).build() + + val formatEmojis = { text: String -> + text + .replace("(/)", "✅") + .replace("(x)", "❌") + .replace("(!)", "⚡") + } + + return renderer.render(parser.parse(formatEmojis(text))) +}
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt Tue Jan 06 20:08:54 2026 +0100 @@ -17,6 +17,7 @@ get("/", this::issues) get("/search", this::issueSearch) + post("/preview-markdown", this::previewMarkdown) get("/%issue", this::issue) get("/%issue/edit", this::issueForm) get("/%issue/progress", this::issueProgress) @@ -90,6 +91,10 @@ ) } + private fun previewMarkdown(http: HttpRequest, dao: DataAccessObject) { + http.renderText(formatMarkdown(http.body), "markdown") + } + private fun issueForm(http: HttpRequest, dao: DataAccessObject) { val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) if (issue == null) {
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt Tue Jan 06 20:08:54 2026 +0100 @@ -25,17 +25,12 @@ package de.uapcore.lightpit.viewmodel -import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension -import com.vladsch.flexmark.ext.tables.TablesExtension -import com.vladsch.flexmark.html.HtmlRenderer -import com.vladsch.flexmark.parser.Parser -import com.vladsch.flexmark.util.data.MutableDataSet -import com.vladsch.flexmark.util.data.SharedDataKeys import de.uapcore.lightpit.HttpRequest import de.uapcore.lightpit.OptionalPathInfo import de.uapcore.lightpit.dao.DataAccessObject import de.uapcore.lightpit.entities.* import de.uapcore.lightpit.logic.compareEtaTo +import de.uapcore.lightpit.logic.formatMarkdown import de.uapcore.lightpit.types.* import java.sql.Timestamp import kotlin.math.roundToInt @@ -132,19 +127,8 @@ val commitLinks: List<CommitLink> val openedVariant: Variant? = ((pathInfos as? PathInfosFull)?.variantInfo as? OptionalPathInfo.Specific)?.elem - private val parser: Parser - private val renderer: HtmlRenderer - init { - val options = MutableDataSet() - .set(SharedDataKeys.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create())) - parser = Parser.builder(options).build() - renderer = HtmlRenderer.builder( - options - .set(HtmlRenderer.ESCAPE_HTML, true) - ).build() - - issue.description = formatMarkdown(issue.description ?: "") + issue.description = formatMarkdown(issue.description) for (comment in comments) { comment.commentFormatted = formatMarkdown(comment.comment) } @@ -165,14 +149,6 @@ append(hash) toString() } - - private fun formatEmojis(text: String) = text - .replace("(/)", "✅") - .replace("(x)", "❌") - .replace("(!)", "⚡") - - private fun formatMarkdown(text: String) = - renderer.render(parser.parse(formatEmojis(text))) } class IssueEditView(
--- a/src/main/resources/localization/strings.properties Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/resources/localization/strings.properties Tue Jan 06 20:08:54 2026 +0100 @@ -33,6 +33,7 @@ button.component.create=New Component button.component.edit=Edit Component button.dismiss=Dismiss +button.edit=Edit button.issue.all=All Issues button.issue.create.another=Create another Issue button.issue.create=New Issue @@ -41,6 +42,7 @@ button.issue.progress=Progress button.issue.resolve=Resolve button.okay=OK +button.preview=Preview button.project.create=New Project button.project.edit=Edit Project button.remove=Remove
--- a/src/main/resources/localization/strings_de.properties Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/resources/localization/strings_de.properties Tue Jan 06 20:08:54 2026 +0100 @@ -33,6 +33,7 @@ button.component.create=Neue Komponente button.component.edit=Komponente Bearbeiten button.dismiss=Nicht Jetzt +button.edit=Bearbeiten button.issue.all=Alle Vorg\u00e4nge button.issue.create.another=Weiteren Vorgang erstellen button.issue.create=Neuer Vorgang @@ -41,6 +42,7 @@ button.issue.progress=In Arbeit button.issue.resolve=Erledigt button.okay=OK +button.preview=Vorschau button.project.create=Neues Projekt button.project.edit=Projekt Bearbeiten button.remove=Entfernen
--- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf Tue Jan 06 20:08:54 2026 +0100 @@ -34,6 +34,7 @@ <li>Schaltflächen hinzugefügt, die schnelleren Zugang zu den Editoren für Versionen, Komponenten und Varianten bieten.</li> <li>Es können nun neue Vorgänge direkt mit einer Verknüpfung zu einem existierenden Vorgang erstellt werden.</li> <li>Neuen Filter "zeige nur nicht-blockierte" hinzugefügt.</li> + <li>Vorschau für Markdown hinzugefügt.</li> <li>Vorgänge können nun auch direkt über die Vorgangsnummer (anstatt Raute + Nummer) verlinkt werden.</li> <li>Die Vorschläge in den Suchfeldern für Vorgänge sind nun absteigend nach Vorgangsnummer sortiert.</li> <li>Die Standardkategorie für neue Vorgänge in veröffentlichten Versionen ist nun "Fehler" anstelle von "Feature".</li>
--- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf Tue Jan 06 20:08:54 2026 +0100 @@ -34,6 +34,7 @@ <li>Add buttons and hover-icons to quickly access the editor for versions, components, and variants.</li> <li>Add the possibility to create new related issues with one click.</li> <li>Add new filter "show only non-blocked".</li> + <li>Add markdown preview.</li> <li>Change that you can now relate issues by just submitting their number (instead of hash + number).</li> <li>Change that issues suggested by the search boxes are now sorted by ID in descending order.</li> <li>Change that the default category for new issues in released versions is Bug instead of Feature.</li>
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Tue Jan 06 20:08:54 2026 +0100 @@ -139,7 +139,11 @@ <tr> <th class="vtop"><label for="issue-description"><fmt:message key="issue.description"/></label></th> <td> - <textarea id="issue-description" name="description" rows="10"><c:out value="${issue.description}"/></textarea> + <c:set var="mde_id" value="issue-description" /> + <c:set var="mde_name" value="description" /> + <c:set var="mde_rows" value="10"/> + <c:set var="mde_value" value="${issue.description}"/> + <%@include file="../jspf/markdown-editor.jspf" %> </td> </tr> <tr> @@ -193,7 +197,12 @@ <c:if test="${issue.id ge 0}"> <tr> <th class="vtop"><label for="issue-comment"><fmt:message key="issue.comment"/></label> </th> - <td><textarea id="issue-comment" rows="3" name="comment"></textarea></td> + <td> + <c:set var="mde_id" value="issue-comment" /> + <c:set var="mde_name" value="comment" /> + <c:set var="mde_rows" value="3"/> + <%@include file="../jspf/markdown-editor.jspf" %> + </td> </tr> </c:if> </tbody>
--- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp Tue Jan 06 20:08:54 2026 +0100 @@ -334,7 +334,12 @@ <table class="formtable fullwidth"> <tbody> <tr> - <td><textarea rows="3" name="comment" required></textarea></td> + <td> + <c:set var="mde_id" value="new-comment" /> + <c:set var="mde_name" value="comment" /> + <c:set var="mde_rows" value="3"/> + <%@include file="../jspf/markdown-editor.jspf" %> + </td> </tr> </tbody> <tfoot> @@ -383,7 +388,11 @@ <tbody> <tr> <td> - <textarea rows="5" name="comment" required><c:out value="${comment.comment}"/></textarea> + <c:set var="mde_id" value="edit-comment-${comment.id}" /> + <c:set var="mde_name" value="comment" /> + <c:set var="mde_rows" value="5"/> + <c:set var="mde_value" value="${comment.comment}" /> + <%@include file="../jspf/markdown-editor.jspf" %> </td> </tr> </tbody>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/markdown-editor.jspf Tue Jan 06 20:08:54 2026 +0100 @@ -0,0 +1,22 @@ +<%-- + mde_id: String + mde_name: String + mde_rows: String + mde_value: String +--%> +<div> + <div class="mde-toolbar"> + <button type="button" id="${mde_id}-btn-preview"><fmt:message key="button.preview" /></button> + <button type="button" id="${mde_id}-btn-edit"><fmt:message key="button.edit" /></button> + </div> + <textarea id="${mde_id}" name="${mde_name}" rows="${mde_rows}"><c:out value="${mde_value}"/></textarea> + <div id="${mde_id}-preview" class="markdown-styled mde-preview"></div> + <script type="text/javascript"> + initMarkdownEditor('${mde_id}'); + </script> +</div> + +<c:remove var="mde_id" /> +<c:remove var="mde_name" /> +<c:remove var="mde_rows" /> +<c:remove var="mde_value" />
--- a/src/main/webapp/issue-editor.js Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/webapp/issue-editor.js Tue Jan 06 20:08:54 2026 +0100 @@ -56,6 +56,44 @@ } } +function initMarkdownEditor(id) { + const btn_preview = document.getElementById(`${id}-btn-preview`); + const btn_edit = document.getElementById(`${id}-btn-edit`); + const textarea = document.getElementById(`${id}`); + const preview = document.getElementById(`${id}-preview`); + btn_preview.addEventListener('click', () => { + textarea.style.display = 'none'; + preview.style.display = 'block'; + textarea.dataset.original = textarea.value; + btn_preview.style.display = 'none'; + btn_edit.style.display = 'inline-block'; + const req = new XMLHttpRequest(); + req.addEventListener("load", (evt) => { + if (evt.target.status === 200) { + preview.innerHTML = evt.target.responseText; + } else { + // revert the button click as fallback + setTimeout(() => btn_edit.click(), 100); + } + }); + let url = baseHref + 'issues/preview-markdown'; + req.open("POST", url); + req.setRequestHeader("Content-Type", "text/plain"); + req.send(textarea.value); + }); + btn_edit.addEventListener('click', () => { + textarea.readOnly = false; + textarea.value = textarea.dataset.original; + textarea.style.display = 'block'; + preview.style.display = 'none'; + textarea.focus(); + btn_preview.style.display = 'inline-block'; + btn_edit.style.display = 'none'; + }); + btn_edit.style.display = 'none'; + preview.style.display = 'none'; +} + window.addEventListener("load", () => { toggleVariantStatus(); const sbox = document.getElementById('linkable-issues');
--- a/src/main/webapp/projects.css Tue Jan 06 19:14:24 2026 +0100 +++ b/src/main/webapp/projects.css Tue Jan 06 20:08:54 2026 +0100 @@ -174,6 +174,22 @@ color: #556080; } +div.mde-toolbar { + margin-bottom: .25em; +} + +div.mde-toolbar button { + font-size: small; +} + +div.mde-preview { + border-style: solid; + border-width: 2px; + border-color: silver; + padding: .25em; + border-radius: 8px; +} + span.eta-overdue { color: red; }