clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

render pipe tables in wiki articles

Dami committed 34b6786
commit
34b6786edfc087e660f274f08a3c0da6ce482b87
parent
44d8d02d77557585e45af4381b9580d356c795ab
author
Dami <dami.etoile@gmail.com>
4 files changed, +126 -4
Mdocs/wiki.md2+-
Minternal/webui/style.css2+-
Minternal/wiki/wiki.go105+++++++++++++++++++++++++++++-
Minternal/wiki/wiki_test.go21++++++
M docs/wiki.md +1 -1
M internal/webui/style.css +1 -1
@@ -233,7 +233,7 @@ diff-view td>nostr-event{white-space:normal;max-width:720px;border:0;padding:6px
233233 /* Wiki: the page list, one article, the merge request bar and the editor. */234234 #wiki-list #rows th{white-space:nowrap}#wiki-list #rows td:first-child{width:auto;font:14px var(--sans)}#wiki-list #rows td:nth-child(3),#wiki-list #rows td:nth-child(4){width:110px;font:12px var(--mono);color:var(--muted)}235235 wiki-article{display:block;max-width:720px;overflow-wrap:anywhere}wiki-article>header{font:12px var(--mono);color:var(--muted);margin-bottom:8px}wiki-article>header nostr-key,wiki-article>header nostr-name{color:var(--ink)}236-wiki-article h1{font-size:20px;margin:18px 0 8px}wiki-article h2{font-size:17px}wiki-article h3{font-size:15px}wiki-article p{margin:0 0 10px}236+wiki-article h1{font-size:20px;margin:18px 0 8px}wiki-article h2{font-size:17px}wiki-article h3{font-size:15px}wiki-article p{margin:0 0 10px}wiki-article table{border-collapse:collapse;margin:0 0 12px;font:13px var(--sans);width:auto}wiki-article th,wiki-article td{border:1px solid var(--line);padding:4px 8px;text-align:left;vertical-align:top;height:auto;white-space:normal}wiki-article th{font-weight:600;background:var(--rail)}237237 wiki-article pre{background:var(--rail);border:1px solid var(--line);padding:10px 12px;overflow:auto;font-size:12.5px;line-height:1.5;white-space:pre}wiki-article ul,wiki-article ol{padding-left:20px;margin:0 0 10px}238238 merge-request,wiki-proposal,collaboration-proposal{display:block;border:1px solid var(--line);background:var(--soft);padding:8px 10px;margin:0 0 14px;max-width:720px;font:13px var(--sans)}merge-request nostr-key,merge-request nostr-name,wiki-proposal nostr-key,wiki-proposal nostr-name,collaboration-proposal nostr-key,collaboration-proposal nostr-name{color:var(--ink);font-weight:500}merge-request>a,merge-request button,wiki-proposal button,collaboration-proposal button{margin-left:6px;font-size:12px}239239 merge-request nostr-react,merge-request nostr-react form,wiki-proposal nostr-react,wiki-proposal nostr-react form,collaboration-proposal nostr-react,collaboration-proposal nostr-react form{display:inline}merge-request nostr-react output,wiki-proposal nostr-react output,collaboration-proposal nostr-react output{display:inline-block;margin-left:6px;font:12px var(--mono);color:var(--muted)}merge-request nostr-react output[data-error],wiki-proposal nostr-react output[data-error],collaboration-proposal nostr-react output[data-error]{color:var(--alert)}
M internal/wiki/wiki.go +103 -2
@@ -84,8 +84,8 @@ func Links(content string) []string {
8484 }8585 8686 // RenderHTML renders a subset of Djot: ATX headings, paragraphs, fenced87-// code, bullet and numbered lists, emphasis, strong, code spans, links,88-// [[wikilinks]] and reference-style links. Everything else is shown as87+// code, bullet and numbered lists, pipe tables, emphasis, strong, code spans,88+// links, [[wikilinks]] and reference-style links. Everything else is shown as8989 // escaped text. Link targets are limited to http, https, mailto, nostr and9090 // site-relative paths.9191 func RenderHTML(content string) template.HTML {
@@ -110,6 +110,34 @@ func RenderHTML(content string) template.HTML {
110110 b.WriteString("<li>" + renderInline(item, refs) + "</li>\n")111111 }112112 b.WriteString("</" + block.kind + ">\n")113+ case "table":114+ b.WriteString("<table>\n")115+ for i, row := range block.rows {116+ cell := "td"117+ if i < block.level {118+ cell = "th"119+ }120+ if i == 0 && block.level > 0 {121+ b.WriteString("<thead>\n")122+ }123+ if i == block.level {124+ if block.level > 0 {125+ b.WriteString("</thead>\n")126+ }127+ b.WriteString("<tbody>\n")128+ }129+ b.WriteString("<tr>")130+ for _, text := range row {131+ b.WriteString("<" + cell + ">" + renderInline(text, refs) + "</" + cell + ">")132+ }133+ b.WriteString("</tr>\n")134+ }135+ if block.level >= len(block.rows) {136+ b.WriteString("</thead>\n")137+ } else {138+ b.WriteString("</tbody>\n")139+ }140+ b.WriteString("</table>\n")113141 default:114142 b.WriteString("<p>" + renderInline(block.text, refs) + "</p>\n")115143 }
@@ -123,6 +151,7 @@ type block struct {
123151 lang string124152 text string125153 items []string154+ rows [][]string // table rows; level counts the header rows126155 }127156 128157 // definitions collects reference link definitions, "[label]: target", keyed
@@ -175,6 +204,31 @@ func blocks(content string) []block {
175204 out = append(out, block{kind: "def", lang: label, text: target})176205 continue177206 }207+ if tableRow(trimmed) {208+ // A table interrupts a paragraph, the way Djot block starts do.209+ flush()210+ var rows [][]string211+ header := 0212+ for ; i < len(lines); i++ {213+ current := strings.TrimSpace(lines[i])214+ if !tableRow(current) {215+ break216+ }217+ if separatorRow(current) {218+ // A separator marks every row above it as a header.219+ if header == 0 {220+ header = len(rows)221+ }222+ continue223+ }224+ rows = append(rows, tableCells(current))225+ }226+ i--227+ if len(rows) > 0 {228+ out = append(out, block{kind: "table", level: header, rows: rows})229+ }230+ continue231+ }178232 if kind, _ := listMarker(trimmed); kind != "" && len(para) == 0 {179233 var items []string180234 for ; i < len(lines); i++ {
@@ -195,6 +249,53 @@ func blocks(content string) []block {
195249 return out196250 }197251 252+// tableRow reports a Djot pipe table row: a line that starts and ends with253+// a pipe.254+func tableRow(line string) bool {255+ return len(line) >= 2 && strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|")256+}257+258+// separatorRow reports the header separator, cells of dashes with optional259+// alignment colons, such as |---|:--:|.260+func separatorRow(line string) bool {261+ cells := tableCells(line)262+ if len(cells) == 0 {263+ return false264+ }265+ for _, cell := range cells {266+ if strings.Trim(cell, "-") == "" && strings.Contains(cell, "-") {267+ continue268+ }269+ if strings.Trim(strings.TrimSuffix(strings.TrimPrefix(cell, ":"), ":"), "-") == "" && strings.Contains(cell, "-") {270+ continue271+ }272+ return false273+ }274+ return true275+}276+277+// tableCells splits a row into trimmed cells; a backslash escapes a pipe278+// inside a cell.279+func tableCells(line string) []string {280+ inner := strings.TrimSuffix(strings.TrimPrefix(line, "|"), "|")281+ var cells []string282+ var current strings.Builder283+ for i := 0; i < len(inner); i++ {284+ switch {285+ case inner[i] == '\\' && i+1 < len(inner) && inner[i+1] == '|':286+ current.WriteByte('|')287+ i++288+ case inner[i] == '|':289+ cells = append(cells, strings.TrimSpace(current.String()))290+ current.Reset()291+ default:292+ current.WriteByte(inner[i])293+ }294+ }295+ cells = append(cells, strings.TrimSpace(current.String()))296+ return cells297+}298+198299 func headingLevel(line string) int {199300 level := len(line) - len(strings.TrimLeft(line, "#"))200301 if level == 0 || level > 6 || level >= len(line) || line[level] != ' ' {
M internal/wiki/wiki_test.go +21 -0