✨ (markdown): Render GFM pipe tables in Markdown
Changes
5 files changed, +62 -2
MODIFY
README.md
+1 -1
@@ -14,7 +14,7 @@
14
14
- push and private read authenticate with **personal access tokens** (HTTP Basic password)
15
15
- Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
16
16
- public-key authentication only; keys managed per user in the UI
17
-- Web UI: an auth-aware header nav (a "Log in" button for visitors; for signed-in users a top-level Following link plus an Account dropdown holding Profile, SSH keys, Access tokens, and Logout — a JS-free `<details>` menu), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files; Markdown files render to HTML by default with a Rendered/Code toggle), a rendered README (commonmark-java, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Issues, Merge requests, plus Collaborators for the owner) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
17
+- Web UI: an auth-aware header nav (a "Log in" button for visitors; for signed-in users a top-level Following link plus an Account dropdown holding Profile, SSH keys, Access tokens, and Logout — a JS-free `<details>` menu), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files; Markdown files render to HTML by default with a Rendered/Code toggle), a rendered README (commonmark-java with GFM tables, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Issues, Merge requests, plus Collaborators for the owner) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
18
18
- Per-repository issues: title, optional description (rendered as Markdown, XSS-safe), per-repo sequential number (`#1`, `#2`, …), and author; created and managed by the repo owner and collaborators, readable by anyone who can read the repo, via a dedicated "New issue" page
19
19
- Issues move through a fixed lifecycle (Planned → In development → Done); the repo navigation shows the open (Planned + In development) issue count, and Done issues collapse into an "Archive" section on the issues page
20
20
- Issues auto-close from pushed commit messages, GitHub-style (`close(s|d)`/`fix(es|ed)`/`resolve(s|d)` + `#<number>`, e.g. `fixes #12`), over both HTTP and SSH pushes
MODIFY
pom.xml
+5 -0
@@ -90,6 +90,11 @@
90
90
<version>${commonmark.version}</version>
91
91
</dependency>
92
92
<dependency>
93
+ <groupId>org.commonmark</groupId>
94
+ <artifactId>commonmark-ext-gfm-tables</artifactId>
95
+ <version>${commonmark.version}</version>
96
+ </dependency>
97
+ <dependency>
93
98
<groupId>org.apache.sshd</groupId>
94
99
<artifactId>sshd-core</artifactId>
95
100
<version>${sshd.version}</version>
MODIFY
src/main/java/de/workaround/web/Markdown.java
+8 -1
@@ -1,16 +1,23 @@
1
1
package de.workaround.web;
2
2
3
+import java.util.List;
4
+
5
+import org.commonmark.Extension;
6
+import org.commonmark.ext.gfm.tables.TablesExtension;
3
7
import org.commonmark.parser.Parser;
4
8
import org.commonmark.renderer.html.HtmlRenderer;
5
9
6
10
/** Shared Markdown-to-HTML rendering for server-rendered pages (README, markdown blobs, issue descriptions). */
7
11
final class Markdown
8
12
{
9
- private static final Parser PARSER = Parser.builder().build();
13
+ private static final List<Extension> EXTENSIONS = List.of(TablesExtension.create());
14
+
15
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
10
16
11
17
// escapeHtml + sanitizeUrls: markdown content is untrusted user input rendered into our page,
12
18
// so raw HTML blocks are escaped and javascript:/data: link targets are stripped.
13
19
private static final HtmlRenderer RENDERER = HtmlRenderer.builder()
20
+ .extensions(EXTENSIONS)
14
21
.escapeHtml(true)
15
22
.sanitizeUrls(true)
16
23
.build();
MODIFY
src/main/resources/META-INF/resources/shark.css
+6 -0
@@ -1783,6 +1783,12 @@
1783
1783
margin: 0 0 .9em;
1784
1784
}
1785
1785
1786
+/* GFM tables: the global table style sizes to the container; inside prose only be as wide as the content */
1787
+.readme-body table {
1788
+ margin: 0 0 .9em;
1789
+ width: auto;
1790
+}
1791
+
1786
1792
.readme-body ul,
1787
1793
.readme-body ol {
1788
1794
padding-left: 1.6em;
ADD
src/test/java/de/workaround/web/MarkdownTest.java
+42 -0
@@ -0,0 +1,42 @@
1
+package de.workaround.web;
2
+
3
+import org.junit.jupiter.api.Test;
4
+
5
+import static org.junit.jupiter.api.Assertions.assertFalse;
6
+import static org.junit.jupiter.api.Assertions.assertTrue;
7
+
8
+class MarkdownTest
9
+{
10
+
11
+ @Test
12
+ void rendersGfmPipeTablesAsHtmlTables()
13
+ {
14
+ String html = Markdown.render("""
15
+ | Column A | Column B |
16
+ |----------|----------|
17
+ | foo | bar |
18
+ """);
19
+ assertTrue(html.contains("<table>"), () -> "expected a <table> element, got: " + html);
20
+ assertTrue(html.contains("<th>Column A</th>"), () -> "expected a header cell, got: " + html);
21
+ assertTrue(html.contains("<td>foo</td>"), () -> "expected a body cell, got: " + html);
22
+ }
23
+
24
+ @Test
25
+ void tableCellContentStaysHtmlEscaped()
26
+ {
27
+ String html = Markdown.render("""
28
+ | A |
29
+ |---|
30
+ | <script>alert('xss')</script> |
31
+ """);
32
+ assertFalse(html.contains("<script>"), () -> "raw HTML must stay escaped, got: " + html);
33
+ }
34
+
35
+ @Test
36
+ void basicMarkdownStillRenders()
37
+ {
38
+ String html = Markdown.render("Some **bold** text");
39
+ assertTrue(html.contains("<strong>bold</strong>"));
40
+ }
41
+
42
+}