rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package codeview
2
3import (
4 "html/template"
5 "strings"
6)
7
8// Highlight escapes source for an HTML text context and wraps five
9// kinds of token in two-letter custom elements that the gallery's
10// stylesheet colours: <ds-t> a tag's name with its opening bracket,
11// <ds-a> an attribute name, <ds-v> an attribute value with its quotes,
12// <ds-x> a template action's delimiters, keywords and fields, and <ds-s>
13// a string literal inside an action.
14//
15// Custom elements rather than <span class>: a token costs 13 bytes this
16// way and 29 the other, and on the heaviest page that difference was
17// the page budget. An undefined element is inline with no role, so it
18// changes nothing for assistive technology or for textContent.
19//
20// Only &, < and > are escaped. Quotes stay literal, which is valid in
21// text and spares four bytes on every attribute a sample writes.
22func Highlight(src string) template.HTML {
23 var b strings.Builder
24 for i := 0; i < len(src); {
25 switch {
26 case strings.HasPrefix(src[i:], "{{"):
27 end := actionEnd(src, i)
28 action(&b, src[i:end])
29 i = end
30 case src[i] == '<' && tagEnd(src, i) > 0:
31 end := tagEnd(src, i)
32 tag(&b, src[i:end])
33 i = end
34 default:
35 j := i + 1
36 for j < len(src) && !strings.HasPrefix(src[j:], "{{") && !(src[j] == '<' && tagEnd(src, j) > 0) {
37 j++
38 }
39 escape(&b, src[i:j])
40 i = j
41 }
42 }
43 return template.HTML(b.String())
44}
45
46func escape(b *strings.Builder, s string) {
47 for i := 0; i < len(s); i++ {
48 switch s[i] {
49 case '&':
50 b.WriteString("&amp;")
51 case '<':
52 b.WriteString("&lt;")
53 case '>':
54 b.WriteString("&gt;")
55 default:
56 b.WriteByte(s[i])
57 }
58 }
59}
60
61func wrapped(b *strings.Builder, kind, s string) {
62 b.WriteString("<ds-" + kind + ">")
63 escape(b, s)
64 b.WriteString("</ds-" + kind + ">")
65}
66
67// tag writes one tag: its name, each attribute's name and value, and
68// the whitespace and closing bracket between them as plain text.
69func tag(b *strings.Builder, t string) {
70 n := 1
71 if strings.HasPrefix(t, "</") {
72 n = 2
73 }
74 for n < len(t) && !isSpace(t[n]) && t[n] != '>' && t[n] != '/' {
75 n++
76 }
77 wrapped(b, "t", t[:n])
78 for i := n; i < len(t); {
79 c := t[i]
80 switch {
81 case isSpace(c) || c == '/' || c == '>':
82 escape(b, t[i:i+1])
83 i++
84 case c == '=':
85 b.WriteByte('=')
86 i++
87 j := i
88 if j < len(t) && (t[j] == '"' || t[j] == '\'') {
89 q := t[j]
90 j++
91 for j < len(t) && t[j] != q {
92 j++
93 }
94 if j < len(t) {
95 j++
96 }
97 } else {
98 for j < len(t) && !isSpace(t[j]) && t[j] != '>' {
99 j++
100 }
101 }
102 wrapped(b, "v", t[i:j])
103 i = j
104 default:
105 j := i
106 for j < len(t) && !isSpace(t[j]) && t[j] != '=' && t[j] != '>' && !(t[j] == '/' && j+1 < len(t) && t[j+1] == '>') {
107 j++
108 }
109 wrapped(b, "a", t[i:j])
110 i = j
111 }
112 }
113}
114
115// actionEnd returns the index just past the action starting at s[i],
116// treating "}}" inside a string literal as part of the string.
117func actionEnd(s string, i int) int {
118 for j := i + 2; j < len(s); j++ {
119 switch s[j] {
120 case '"':
121 for j++; j < len(s) && s[j] != '"'; j++ {
122 if s[j] == '\\' {
123 j++
124 }
125 }
126 case '`':
127 for j++; j < len(s) && s[j] != '`'; j++ {
128 }
129 case '}':
130 if j+1 < len(s) && s[j+1] == '}' {
131 return j + 2
132 }
133 }
134 }
135 return len(s)
136}
137
138// action writes one {{…}}: the delimiters, identifiers and .Fields as
139// <ds-x>, string literals as <ds-s>, and everything else as text.
140func action(b *strings.Builder, a string) {
141 body, closed := strings.CutSuffix(a[2:], "}}")
142 wrapped(b, "x", "{{")
143 for i := 0; i < len(body); {
144 c := body[i]
145 switch {
146 case c == '"' || c == '`':
147 j := i + 1
148 for j < len(body) && body[j] != c {
149 if c == '"' && body[j] == '\\' {
150 j++
151 }
152 j++
153 }
154 j = min(j+1, len(body))
155 wrapped(b, "s", body[i:j])
156 i = j
157 case isLetter(c) || c == '_' || c == '.':
158 j := i + 1
159 for j < len(body) && (isLetter(body[j]) || body[j] == '_' || body[j] == '.' || body[j] >= '0' && body[j] <= '9') {
160 j++
161 }
162 wrapped(b, "x", body[i:j])
163 i = j
164 default:
165 escape(b, body[i:i+1])
166 i++
167 }
168 }
169 if closed {
170 wrapped(b, "x", "}}")
171 }
172}
173