tinyrelaycommit

Commit 34b6786edfc0

browse files at this commit | history

1diff --git a/docs/wiki.md b/docs/wiki.md
2index e39b5b8..8a14fe4 100644
3--- a/docs/wiki.md
4+++ b/docs/wiki.md
5@@ -8,7 +8,7 @@ A page is identified by its name, the `d` tag of a kind 30818 article. Names are
6
7 An article carries a `title` for display, an optional `summary` for lists and its content in [Djot](https://djot.net/) markup. Two link forms are specific to the wiki: `[[Page Name]]` or `[[Page Name|shown text]]` links to another page, and a reference-style link with no definition, such as `[proof of work][]`, does the same. Links to profiles and events use `nostr:` addresses, such as `[Bob](nostr:npub1...)`. The page list searches titles and summaries.
8
9-The relay renders headings, paragraphs, emphasis, strong text, code spans and blocks, bullet and numbered lists, links, wikilinks and `nostr:` links. Other markup and any HTML in an article is shown as plain text. Links are limited to `http`, `https`, `mailto`, `nostr` and relay paths.
10+The relay renders headings, paragraphs, emphasis, strong text, code spans and blocks, bullet and numbered lists, pipe tables, links, wikilinks and `nostr:` links. Other markup and any HTML in an article is shown as plain text. Links are limited to `http`, `https`, `mailto`, `nostr` and relay paths.
11
12 ## Versions and forks
13
14diff --git a/internal/webui/style.css b/internal/webui/style.css
15index 7e0a15a..f4ebea3 100644
16--- a/internal/webui/style.css
17+++ b/internal/webui/style.css
18@@ -233,7 +233,7 @@ diff-view td>nostr-event{white-space:normal;max-width:720px;border:0;padding:6px
19 /* Wiki: the page list, one article, the merge request bar and the editor. */
20 #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)}
21 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)}
22-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}
23+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)}
24 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}
25 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}
26 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)}
27diff --git a/internal/wiki/wiki.go b/internal/wiki/wiki.go
28index b78bb05..887ad54 100644
29--- a/internal/wiki/wiki.go
30+++ b/internal/wiki/wiki.go
31@@ -84,8 +84,8 @@ func Links(content string) []string {
32 }
33
34 // RenderHTML renders a subset of Djot: ATX headings, paragraphs, fenced
35-// code, bullet and numbered lists, emphasis, strong, code spans, links,
36-// [[wikilinks]] and reference-style links. Everything else is shown as
37+// code, bullet and numbered lists, pipe tables, emphasis, strong, code spans,
38+// links, [[wikilinks]] and reference-style links. Everything else is shown as
39 // escaped text. Link targets are limited to http, https, mailto, nostr and
40 // site-relative paths.
41 func RenderHTML(content string) template.HTML {
42@@ -110,6 +110,34 @@ func RenderHTML(content string) template.HTML {
43 b.WriteString("<li>" + renderInline(item, refs) + "</li>\n")
44 }
45 b.WriteString("</" + block.kind + ">\n")
46+ case "table":
47+ b.WriteString("<table>\n")
48+ for i, row := range block.rows {
49+ cell := "td"
50+ if i < block.level {
51+ cell = "th"
52+ }
53+ if i == 0 && block.level > 0 {
54+ b.WriteString("<thead>\n")
55+ }
56+ if i == block.level {
57+ if block.level > 0 {
58+ b.WriteString("</thead>\n")
59+ }
60+ b.WriteString("<tbody>\n")
61+ }
62+ b.WriteString("<tr>")
63+ for _, text := range row {
64+ b.WriteString("<" + cell + ">" + renderInline(text, refs) + "</" + cell + ">")
65+ }
66+ b.WriteString("</tr>\n")
67+ }
68+ if block.level >= len(block.rows) {
69+ b.WriteString("</thead>\n")
70+ } else {
71+ b.WriteString("</tbody>\n")
72+ }
73+ b.WriteString("</table>\n")
74 default:
75 b.WriteString("<p>" + renderInline(block.text, refs) + "</p>\n")
76 }
77@@ -123,6 +151,7 @@ type block struct {
78 lang string
79 text string
80 items []string
81+ rows [][]string // table rows; level counts the header rows
82 }
83
84 // definitions collects reference link definitions, "[label]: target", keyed
85@@ -175,6 +204,31 @@ func blocks(content string) []block {
86 out = append(out, block{kind: "def", lang: label, text: target})
87 continue
88 }
89+ if tableRow(trimmed) {
90+ // A table interrupts a paragraph, the way Djot block starts do.
91+ flush()
92+ var rows [][]string
93+ header := 0
94+ for ; i < len(lines); i++ {
95+ current := strings.TrimSpace(lines[i])
96+ if !tableRow(current) {
97+ break
98+ }
99+ if separatorRow(current) {
100+ // A separator marks every row above it as a header.
101+ if header == 0 {
102+ header = len(rows)
103+ }
104+ continue
105+ }
106+ rows = append(rows, tableCells(current))
107+ }
108+ i--
109+ if len(rows) > 0 {
110+ out = append(out, block{kind: "table", level: header, rows: rows})
111+ }
112+ continue
113+ }
114 if kind, _ := listMarker(trimmed); kind != "" && len(para) == 0 {
115 var items []string
116 for ; i < len(lines); i++ {
117@@ -195,6 +249,53 @@ func blocks(content string) []block {
118 return out
119 }
120
121+// tableRow reports a Djot pipe table row: a line that starts and ends with
122+// a pipe.
123+func tableRow(line string) bool {
124+ return len(line) >= 2 && strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|")
125+}
126+
127+// separatorRow reports the header separator, cells of dashes with optional
128+// alignment colons, such as |---|:--:|.
129+func separatorRow(line string) bool {
130+ cells := tableCells(line)
131+ if len(cells) == 0 {
132+ return false
133+ }
134+ for _, cell := range cells {
135+ if strings.Trim(cell, "-") == "" && strings.Contains(cell, "-") {
136+ continue
137+ }
138+ if strings.Trim(strings.TrimSuffix(strings.TrimPrefix(cell, ":"), ":"), "-") == "" && strings.Contains(cell, "-") {
139+ continue
140+ }
141+ return false
142+ }
143+ return true
144+}
145+
146+// tableCells splits a row into trimmed cells; a backslash escapes a pipe
147+// inside a cell.
148+func tableCells(line string) []string {
149+ inner := strings.TrimSuffix(strings.TrimPrefix(line, "|"), "|")
150+ var cells []string
151+ var current strings.Builder
152+ for i := 0; i < len(inner); i++ {
153+ switch {
154+ case inner[i] == '\\' && i+1 < len(inner) && inner[i+1] == '|':
155+ current.WriteByte('|')
156+ i++
157+ case inner[i] == '|':
158+ cells = append(cells, strings.TrimSpace(current.String()))
159+ current.Reset()
160+ default:
161+ current.WriteByte(inner[i])
162+ }
163+ }
164+ cells = append(cells, strings.TrimSpace(current.String()))
165+ return cells
166+}
167+
168 func headingLevel(line string) int {
169 level := len(line) - len(strings.TrimLeft(line, "#"))
170 if level == 0 || level > 6 || level >= len(line) || line[level] != ' ' {
171diff --git a/internal/wiki/wiki_test.go b/internal/wiki/wiki_test.go
172index e40121f..9af4455 100644
173--- a/internal/wiki/wiki_test.go
174+++ b/internal/wiki/wiki_test.go
175@@ -141,3 +141,24 @@ func TestRenderHTMLHandlesEdgeCases(t *testing.T) {
176 }
177 }
178 }
179+
180+func TestRenderHTMLPipeTables(t *testing.T) {
181+ got := string(RenderHTML("Before.\n| Proven | Activity | Evidence |\n| --- | :---: | ---: |\n| yes | Discovery, *auth* | NIP-98 succeeded. |\n| no | Wiki fork \\| merge | remains |\nAfter."))
182+ for _, want := range []string{
183+ "<p>Before.</p>\n<table>\n<thead>\n<tr><th>Proven</th><th>Activity</th><th>Evidence</th></tr>\n</thead>\n<tbody>\n",
184+ "<tr><td>yes</td><td>Discovery, <strong>auth</strong></td><td>NIP-98 succeeded.</td></tr>\n",
185+ "<tr><td>no</td><td>Wiki fork | merge</td><td>remains</td></tr>\n</tbody>\n</table>\n<p>After.</p>\n",
186+ } {
187+ if !strings.Contains(got, want) {
188+ t.Errorf("missing %q in %s", want, got)
189+ }
190+ }
191+ // A table without a separator is all body rows; a pipe inside a paragraph is text.
192+ got = string(RenderHTML("| a | b |\n| c | d |\n\ntext | with a pipe"))
193+ if !strings.Contains(got, "<table>\n<tbody>\n<tr><td>a</td><td>b</td></tr>\n<tr><td>c</td><td>d</td></tr>\n</tbody>\n</table>") || !strings.Contains(got, "<p>text | with a pipe</p>") {
194+ t.Fatalf("headerless table: %s", got)
195+ }
196+ if strings.Contains(string(RenderHTML("| <script>x</script> |\n| --- |")), "<script>") {
197+ t.Fatal("table cells were not escaped")
198+ }
199+}

About

ownerc01503e8035f
visibilitypublic
head34b6786edfc0

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs