tinyrelaycommit

Commit cffc196a901a

browse files at this commit | history

1diff --git a/internal/webui/source_render.go b/internal/webui/source_render.go
2index 6c50982..ec85aa3 100644
3--- a/internal/webui/source_render.go
4+++ b/internal/webui/source_render.go
5@@ -28,7 +28,8 @@ func previewLines(content string) ([]string, bool) {
6
7 // sourceHTML renders a bounded source preview. It returns template.HTML only
8 // after escaping every source byte and constructing the surrounding markup.
9-// The template should place the result inside a source-view region.
10+// The markup is wrapped in a <source-view lang="..."> element and uses only
11+// semantic elements so styling needs no class hooks.
12 func sourceHTML(value any) template.HTML {
13 m := valueMap(value)
14 content, _ := m["content"].(string)
15@@ -45,29 +46,32 @@ func sourceHTML(value any) template.HTML {
16 lines, linesTruncated := previewLines(content)
17 var out strings.Builder
18 out.Grow(len(content) + len(lines)*120)
19- out.WriteString(`<table class="source-view source-lang-`)
20+ out.WriteString(`<source-view lang="`)
21 out.WriteString(language)
22- out.WriteString(`"><tbody>`)
23+ out.WriteString(`"><table><tbody>`)
24 state := highlightState{}
25 for number, line := range lines {
26 out.WriteString(`<tr id="L`)
27 writeUint(&out, number+1)
28- out.WriteString(`"><td class="source-line"><a href="#L`)
29+ out.WriteString(`"><td><a href="#L`)
30 writeUint(&out, number+1)
31 out.WriteString(`">`)
32 writeUint(&out, number+1)
33- out.WriteString(`</a></td><td class="source-code"><code>`)
34+ out.WriteString(`</a></td><td><code>`)
35 highlightLine(&out, line, language, &state)
36 out.WriteString(`</code></td></tr>`)
37 }
38+ out.WriteString(`</tbody>`)
39 if truncated || linesTruncated {
40- 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>`)
41+ 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>`)
42 }
43- out.WriteString(`</tbody></table>`)
44+ out.WriteString(`</table></source-view>`)
45 return template.HTML(out.String())
46 }
47
48 // diffHTML renders a line-oriented diff without interpreting its content.
49+// Added lines are wrapped in <ins>, removed lines in <del>, and hunk headers
50+// in <b>; context lines carry no wrapper.
51 func diffHTML(value any) template.HTML {
52 m := valueMap(value)
53 diff, _ := m["diff"].(string)
54@@ -84,35 +88,32 @@ func diffHTML(value any) template.HTML {
55 lines, linesTruncated := previewLines(diff)
56 var out strings.Builder
57 out.Grow(len(diff) + len(lines)*140)
58- out.WriteString(`<table class="diff-view"><tbody>`)
59+ out.WriteString(`<diff-view><table><tbody>`)
60 for number, line := range lines {
61- class := "diff-context"
62+ wrapper := ""
63 switch {
64 case strings.HasPrefix(line, "@@"):
65- class = "diff-hunk"
66+ wrapper = "b"
67 case strings.HasPrefix(line, "+"):
68- class = "diff-add"
69+ wrapper = "ins"
70 case strings.HasPrefix(line, "-"):
71- class = "diff-remove"
72- case strings.HasPrefix(line, "\\"):
73- class = "diff-note"
74+ wrapper = "del"
75 }
76- out.WriteString(`<tr class="`)
77- out.WriteString(class)
78- out.WriteString(`" id="D`)
79+ out.WriteString(`<tr id="D`)
80 writeUint(&out, number+1)
81- out.WriteString(`"><td class="diff-line"><a href="#D`)
82+ out.WriteString(`"><td><a href="#D`)
83 writeUint(&out, number+1)
84 out.WriteString(`">`)
85 writeUint(&out, number+1)
86- out.WriteString(`</a></td><td class="diff-code"><code>`)
87- out.WriteString(html.EscapeString(line))
88+ out.WriteString(`</a></td><td><code>`)
89+ writeWrapped(&out, wrapper, line)
90 out.WriteString(`</code></td></tr>`)
91 }
92+ out.WriteString(`</tbody>`)
93 if truncated || linesTruncated {
94- out.WriteString(`<tr class="diff-truncated"><td colspan="2">Diff preview truncated (256 KiB or 10,000 lines).</td></tr>`)
95+ out.WriteString(`<tfoot><tr><td colspan="2">Diff preview truncated (256 KiB or 10,000 lines).</td></tr></tfoot>`)
96 }
97- out.WriteString(`</tbody></table>`)
98+ out.WriteString(`</table></diff-view>`)
99 return template.HTML(out.String())
100 }
101
102@@ -229,15 +230,26 @@ func highlightLine(out *strings.Builder, line, language string, state *highlight
103 }
104 }
105
106-func writeToken(out *strings.Builder, class, value string) {
107- if class != "" {
108- out.WriteString(`<span class="tok-`)
109- out.WriteString(class)
110- out.WriteString(`">`)
111+// tokenElements maps token kinds to the semantic element that carries them.
112+var tokenElements = map[string]string{"keyword": "b", "comment": "i", "string": "q", "number": "var"}
113+
114+func writeToken(out *strings.Builder, kind, value string) {
115+ writeWrapped(out, tokenElements[kind], value)
116+}
117+
118+// writeWrapped escapes value and, when element is not empty, wraps it in that
119+// attribute-free element.
120+func writeWrapped(out *strings.Builder, element, value string) {
121+ if element != "" {
122+ out.WriteByte('<')
123+ out.WriteString(element)
124+ out.WriteByte('>')
125 }
126 out.WriteString(html.EscapeString(value))
127- if class != "" {
128- out.WriteString(`</span>`)
129+ if element != "" {
130+ out.WriteString("</")
131+ out.WriteString(element)
132+ out.WriteByte('>')
133 }
134 }
135
136diff --git a/internal/webui/source_render_test.go b/internal/webui/source_render_test.go
137index 7bd21fc..a01779c 100644
138--- a/internal/webui/source_render_test.go
139+++ b/internal/webui/source_render_test.go
140@@ -15,18 +15,19 @@ func TestSourceHTMLIsEscapedAndAnchored(t *testing.T) {
141 })
142 html := string(got)
143 for _, want := range []string{
144- `class="source-view source-lang-go"`,
145+ `<source-view lang="go"><table>`,
146 `id="L1"`,
147 `href="#L2"`,
148- `class="tok-keyword">var</span>`,
149- `&lt;script&gt;`,
150+ `<b>var</b>`,
151+ `<q>&#34;&lt;script&gt;alert(1)&lt;/script&gt;&#34;</q>`,
152+ `</table></source-view>`,
153 } {
154 if !strings.Contains(html, want) {
155 t.Fatalf("source output missing %q: %s", want, html)
156 }
157 }
158- if strings.Contains(html, "<script>alert") {
159- t.Fatalf("source content was not escaped: %s", html)
160+ if strings.Contains(html, "<script>alert") || strings.Contains(html, "class=") {
161+ t.Fatalf("source content was not escaped or used class hooks: %s", html)
162 }
163 if _, ok := any(got).(template.HTML); !ok {
164 t.Fatalf("sourceHTML type = %T, want template.HTML", got)
165@@ -36,10 +37,10 @@ func TestSourceHTMLIsEscapedAndAnchored(t *testing.T) {
166 func TestSourceHTMLUsesPlaintextAndBoundsPreview(t *testing.T) {
167 content := strings.Repeat("<unsafe>\n", sourcePreviewLimit/len("<unsafe>\n")+10)
168 got := string(sourceHTML(map[string]any{"name": "README.unknown", "content": content}))
169- if !strings.Contains(got, `class="source-view source-lang-text"`) {
170+ if !strings.Contains(got, `<source-view lang="text">`) {
171 t.Fatalf("unknown extension should use plaintext: %s", got[:min(len(got), 200)])
172 }
173- if !strings.Contains(got, `class="source-truncated"`) {
174+ if !strings.Contains(got, `<tfoot><tr><td colspan="2">Preview truncated`) {
175 t.Fatal("bounded preview should identify truncation")
176 }
177 if strings.Contains(got, "<unsafe>") {
178@@ -49,13 +50,13 @@ func TestSourceHTMLUsesPlaintextAndBoundsPreview(t *testing.T) {
179
180 func TestDiffHTMLClassifiesLinesWithoutHTMLInjection(t *testing.T) {
181 got := string(diffHTML(map[string]any{"diff": "@@ -1 +1 @@\n-old\n+<script>bad</script>\n context"}))
182- for _, want := range []string{`class="diff-hunk"`, `class="diff-remove"`, `class="diff-add"`, `class="diff-context"`, `&lt;script&gt;`} {
183+ 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>`} {
184 if !strings.Contains(got, want) {
185 t.Fatalf("diff output missing %q: %s", want, got)
186 }
187 }
188- if strings.Contains(got, "<script>bad") {
189- t.Fatal("diff content was not escaped")
190+ if strings.Contains(got, "<script>bad") || strings.Contains(got, "class=") {
191+ t.Fatal("diff content was not escaped or used class hooks")
192 }
193 }
194
195@@ -67,7 +68,7 @@ func TestPreviewBoundsLineMarkupAndDisclosesOmission(t *testing.T) {
196 if len(got) > 2*1024*1024 {
197 t.Fatalf("a short-line preview expanded to %d bytes", len(got))
198 }
199- if !strings.Contains(got, "10,000 lines") {
200+ if !strings.Contains(got, "<tfoot>") || !strings.Contains(got, "10,000 lines") {
201 t.Fatal("line preview omission was not disclosed")
202 }
203 })
204@@ -76,7 +77,7 @@ func TestPreviewBoundsLineMarkupAndDisclosesOmission(t *testing.T) {
205
206 func TestDiffDisclosesBackendTruncation(t *testing.T) {
207 got := string(diffHTML(map[string]any{"diff": "already bounded", "truncated": true}))
208- if !strings.Contains(got, "truncated") {
209+ if !strings.Contains(got, "<tfoot>") || !strings.Contains(got, "truncated") {
210 t.Fatal("backend truncation was hidden")
211 }
212 }
213@@ -100,7 +101,7 @@ func TestSourceHTMLPreservesRenderedSourceText(t *testing.T) {
214 var got strings.Builder
215 var visit func(*html.Node)
216 visit = func(node *html.Node) {
217- if node.Type == html.ElementNode && node.Data == "td" && hasClass(node, "source-code") {
218+ if node.Type == html.ElementNode && node.Data == "td" && isCodeCell(node) {
219 appendText(&got, node)
220 got.WriteByte('\n')
221 return
222@@ -115,13 +116,11 @@ func TestSourceHTMLPreservesRenderedSourceText(t *testing.T) {
223 }
224 }
225
226-func hasClass(node *html.Node, class string) bool {
227- for _, attr := range node.Attr {
228- if attr.Key == "class" && attr.Val == class {
229- return true
230- }
231- }
232- return false
233+// isCodeCell reports whether a table cell holds rendered source rather than
234+// a line number anchor.
235+func isCodeCell(node *html.Node) bool {
236+ first := node.FirstChild
237+ return first != nil && first.Type == html.ElementNode && first.Data == "code"
238 }
239
240 func appendText(out *strings.Builder, node *html.Node) {

About

ownerc01503e8035f
visibilitypublic
headcffc196a901a

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs