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>
| M | internal/ | 70 | ++++++++++++++++++------------ |
| M | internal/ | 39 | ++++++++--------- |
@@ -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 @@ -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- `<script>`,21+ `<b>var</b>`,22+ `<q>"<script>alert(1)</script>"</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"`, `<script>`} {53+ for _, want := range []string{`<diff-view><table>`, `<code><b>@@ -1 +1 @@</b></code>`, `<code><del>-old</del></code>`, `<code><ins>+<script>bad</script></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) {