clone

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

render source and diff views with semantic elements

Dami committed cffc196
Replace class hooks in the source renderer with source-view and diff-view
wrappers, b/i/q/var token elements, ins/del diff lines, and a tfoot
truncation notice so the stylesheet can target elements only.
commit
cffc196a901a81bc880bba3c328dad4b247de164
parent
7e47fdd2d777c82a8600ee9295cf52523ac00288
author
Dami <dami.etoile@gmail.com>
2 files changed, +60 -49
Minternal/webui/source_render.go70++++++++++++++++++------------
Minternal/webui/source_render_test.go39++++++++---------
M internal/webui/source_render.go +41 -29
@@ -28,7 +28,8 @@ func previewLines(content string) ([]string, bool) {
2828 2929 // sourceHTML renders a bounded source preview. It returns template.HTML only3030 // after escaping every source byte and constructing the surrounding markup.31-// The template should place the result inside a source-view region.31+// The markup is wrapped in a <source-view lang="..."> element and uses only32+// semantic elements so styling needs no class hooks.3233 func sourceHTML(value any) template.HTML {3334 m := valueMap(value)3435 content, _ := m["content"].(string)
@@ -45,29 +46,32 @@ func sourceHTML(value any) template.HTML {
4546 lines, linesTruncated := previewLines(content)4647 var out strings.Builder4748 out.Grow(len(content) + len(lines)*120)48- out.WriteString(`<table class="source-view source-lang-`)49+ out.WriteString(`<source-view lang="`)4950 out.WriteString(language)50- out.WriteString(`"><tbody>`)51+ out.WriteString(`"><table><tbody>`)5152 state := highlightState{}5253 for number, line := range lines {5354 out.WriteString(`<tr id="L`)5455 writeUint(&out, number+1)55- out.WriteString(`"><td class="source-line"><a href="#L`)56+ out.WriteString(`"><td><a href="#L`)5657 writeUint(&out, number+1)5758 out.WriteString(`">`)5859 writeUint(&out, number+1)59- out.WriteString(`</a></td><td class="source-code"><code>`)60+ out.WriteString(`</a></td><td><code>`)6061 highlightLine(&out, line, language, &state)6162 out.WriteString(`</code></td></tr>`)6263 }64+ out.WriteString(`</tbody>`)6365 if truncated || linesTruncated {64- out.WriteString(`<tr class="source-truncated"><td colspan="2">Preview truncated (256 KiB or 10,000 lines). Use the raw/download control to view the complete file.</td></tr>`)66+ out.WriteString(`<tfoot><tr><td colspan="2">Preview truncated (256 KiB or 10,000 lines). Use the raw/download control to view the complete file.</td></tr></tfoot>`)6567 }66- out.WriteString(`</tbody></table>`)68+ out.WriteString(`</table></source-view>`)6769 return template.HTML(out.String())6870 }6971 7072 // diffHTML renders a line-oriented diff without interpreting its content.73+// Added lines are wrapped in <ins>, removed lines in <del>, and hunk headers74+// in <b>; context lines carry no wrapper.7175 func diffHTML(value any) template.HTML {7276 m := valueMap(value)7377 diff, _ := m["diff"].(string)
@@ -84,35 +88,32 @@ func diffHTML(value any) template.HTML {
8488 lines, linesTruncated := previewLines(diff)8589 var out strings.Builder8690 out.Grow(len(diff) + len(lines)*140)87- out.WriteString(`<table class="diff-view"><tbody>`)91+ out.WriteString(`<diff-view><table><tbody>`)8892 for number, line := range lines {89- class := "diff-context"93+ wrapper := ""9094 switch {9195 case strings.HasPrefix(line, "@@"):92- class = "diff-hunk"96+ wrapper = "b"9397 case strings.HasPrefix(line, "+"):94- class = "diff-add"98+ wrapper = "ins"9599 case strings.HasPrefix(line, "-"):96- class = "diff-remove"97- case strings.HasPrefix(line, "\\"):98- class = "diff-note"100+ wrapper = "del"99101 }100- out.WriteString(`<tr class="`)101- out.WriteString(class)102- out.WriteString(`" id="D`)102+ out.WriteString(`<tr id="D`)103103 writeUint(&out, number+1)104- out.WriteString(`"><td class="diff-line"><a href="#D`)104+ out.WriteString(`"><td><a href="#D`)105105 writeUint(&out, number+1)106106 out.WriteString(`">`)107107 writeUint(&out, number+1)108- out.WriteString(`</a></td><td class="diff-code"><code>`)109- out.WriteString(html.EscapeString(line))108+ out.WriteString(`</a></td><td><code>`)109+ writeWrapped(&out, wrapper, line)110110 out.WriteString(`</code></td></tr>`)111111 }112+ out.WriteString(`</tbody>`)112113 if truncated || linesTruncated {113- out.WriteString(`<tr class="diff-truncated"><td colspan="2">Diff preview truncated (256 KiB or 10,000 lines).</td></tr>`)114+ out.WriteString(`<tfoot><tr><td colspan="2">Diff preview truncated (256 KiB or 10,000 lines).</td></tr></tfoot>`)114115 }115- out.WriteString(`</tbody></table>`)116+ out.WriteString(`</table></diff-view>`)116117 return template.HTML(out.String())117118 }118119
@@ -229,15 +230,26 @@ func highlightLine(out *strings.Builder, line, language string, state *highlight
229230 }230231 }231232 232-func writeToken(out *strings.Builder, class, value string) {233- if class != "" {234- out.WriteString(`<span class="tok-`)235- out.WriteString(class)236- out.WriteString(`">`)233+// tokenElements maps token kinds to the semantic element that carries them.234+var tokenElements = map[string]string{"keyword": "b", "comment": "i", "string": "q", "number": "var"}235+236+func writeToken(out *strings.Builder, kind, value string) {237+ writeWrapped(out, tokenElements[kind], value)238+}239+240+// writeWrapped escapes value and, when element is not empty, wraps it in that241+// attribute-free element.242+func writeWrapped(out *strings.Builder, element, value string) {243+ if element != "" {244+ out.WriteByte('<')245+ out.WriteString(element)246+ out.WriteByte('>')237247 }238248 out.WriteString(html.EscapeString(value))239- if class != "" {240- out.WriteString(`</span>`)249+ if element != "" {250+ out.WriteString("</")251+ out.WriteString(element)252+ out.WriteByte('>')241253 }242254 }243255
M internal/webui/source_render_test.go +19 -20
@@ -15,18 +15,19 @@ func TestSourceHTMLIsEscapedAndAnchored(t *testing.T) {
1515 })1616 html := string(got)1717 for _, want := range []string{18- `class="source-view source-lang-go"`,18+ `<source-view lang="go"><table>`,1919 `id="L1"`,2020 `href="#L2"`,21- `class="tok-keyword">var</span>`,22- `&lt;script&gt;`,21+ `<b>var</b>`,22+ `<q>&#34;&lt;script&gt;alert(1)&lt;/script&gt;&#34;</q>`,23+ `</table></source-view>`,2324 } {2425 if !strings.Contains(html, want) {2526 t.Fatalf("source output missing %q: %s", want, html)2627 }2728 }28- if strings.Contains(html, "<script>alert") {29- t.Fatalf("source content was not escaped: %s", html)29+ if strings.Contains(html, "<script>alert") || strings.Contains(html, "class=") {30+ t.Fatalf("source content was not escaped or used class hooks: %s", html)3031 }3132 if _, ok := any(got).(template.HTML); !ok {3233 t.Fatalf("sourceHTML type = %T, want template.HTML", got)
@@ -36,10 +37,10 @@ func TestSourceHTMLIsEscapedAndAnchored(t *testing.T) {
3637 func TestSourceHTMLUsesPlaintextAndBoundsPreview(t *testing.T) {3738 content := strings.Repeat("<unsafe>\n", sourcePreviewLimit/len("<unsafe>\n")+10)3839 got := string(sourceHTML(map[string]any{"name": "README.unknown", "content": content}))39- if !strings.Contains(got, `class="source-view source-lang-text"`) {40+ if !strings.Contains(got, `<source-view lang="text">`) {4041 t.Fatalf("unknown extension should use plaintext: %s", got[:min(len(got), 200)])4142 }42- if !strings.Contains(got, `class="source-truncated"`) {43+ if !strings.Contains(got, `<tfoot><tr><td colspan="2">Preview truncated`) {4344 t.Fatal("bounded preview should identify truncation")4445 }4546 if strings.Contains(got, "<unsafe>") {
@@ -49,13 +50,13 @@ func TestSourceHTMLUsesPlaintextAndBoundsPreview(t *testing.T) {
4950 5051 func TestDiffHTMLClassifiesLinesWithoutHTMLInjection(t *testing.T) {5152 got := string(diffHTML(map[string]any{"diff": "@@ -1 +1 @@\n-old\n+<script>bad</script>\n context"}))52- for _, want := range []string{`class="diff-hunk"`, `class="diff-remove"`, `class="diff-add"`, `class="diff-context"`, `&lt;script&gt;`} {53+ for _, want := range []string{`<diff-view><table>`, `<code><b>@@ -1 +1 @@</b></code>`, `<code><del>-old</del></code>`, `<code><ins>+&lt;script&gt;bad&lt;/script&gt;</ins></code>`, `<code> context</code>`, `</table></diff-view>`} {5354 if !strings.Contains(got, want) {5455 t.Fatalf("diff output missing %q: %s", want, got)5556 }5657 }57- if strings.Contains(got, "<script>bad") {58- t.Fatal("diff content was not escaped")58+ if strings.Contains(got, "<script>bad") || strings.Contains(got, "class=") {59+ t.Fatal("diff content was not escaped or used class hooks")5960 }6061 }6162
@@ -67,7 +68,7 @@ func TestPreviewBoundsLineMarkupAndDisclosesOmission(t *testing.T) {
6768 if len(got) > 2*1024*1024 {6869 t.Fatalf("a short-line preview expanded to %d bytes", len(got))6970 }70- if !strings.Contains(got, "10,000 lines") {71+ if !strings.Contains(got, "<tfoot>") || !strings.Contains(got, "10,000 lines") {7172 t.Fatal("line preview omission was not disclosed")7273 }7374 })
@@ -76,7 +77,7 @@ func TestPreviewBoundsLineMarkupAndDisclosesOmission(t *testing.T) {
7677 7778 func TestDiffDisclosesBackendTruncation(t *testing.T) {7879 got := string(diffHTML(map[string]any{"diff": "already bounded", "truncated": true}))79- if !strings.Contains(got, "truncated") {80+ if !strings.Contains(got, "<tfoot>") || !strings.Contains(got, "truncated") {8081 t.Fatal("backend truncation was hidden")8182 }8283 }
@@ -100,7 +101,7 @@ func TestSourceHTMLPreservesRenderedSourceText(t *testing.T) {
100101 var got strings.Builder101102 var visit func(*html.Node)102103 visit = func(node *html.Node) {103- if node.Type == html.ElementNode && node.Data == "td" && hasClass(node, "source-code") {104+ if node.Type == html.ElementNode && node.Data == "td" && isCodeCell(node) {104105 appendText(&got, node)105106 got.WriteByte('\n')106107 return
@@ -115,13 +116,11 @@ func TestSourceHTMLPreservesRenderedSourceText(t *testing.T) {
115116 }116117 }117118 118-func hasClass(node *html.Node, class string) bool {119- for _, attr := range node.Attr {120- if attr.Key == "class" && attr.Val == class {121- return true122- }123- }124- return false119+// isCodeCell reports whether a table cell holds rendered source rather than120+// a line number anchor.121+func isCodeCell(node *html.Node) bool {122+ first := node.FirstChild123+ return first != nil && first.Type == html.ElementNode && first.Data == "code"125124 }126125 127126 func appendText(out *strings.Builder, node *html.Node) {