package codeview
import (
"html/template"
"strings"
)
// Highlight escapes source for an HTML text context and wraps five
// kinds of token in two-letter custom elements that the gallery's
// stylesheet colours: a tag's name with its opening bracket,
// an attribute name, an attribute value with its quotes,
// a template action's delimiters, keywords and fields, and
// a string literal inside an action.
//
// Custom elements rather than : a token costs 13 bytes this
// way and 29 the other, and on the heaviest page that difference was
// the page budget. An undefined element is inline with no role, so it
// changes nothing for assistive technology or for textContent.
//
// Only &, < and > are escaped. Quotes stay literal, which is valid in
// text and spares four bytes on every attribute a sample writes.
func Highlight(src string) template.HTML {
var b strings.Builder
for i := 0; i < len(src); {
switch {
case strings.HasPrefix(src[i:], "{{"):
end := actionEnd(src, i)
action(&b, src[i:end])
i = end
case src[i] == '<' && tagEnd(src, i) > 0:
end := tagEnd(src, i)
tag(&b, src[i:end])
i = end
default:
j := i + 1
for j < len(src) && !strings.HasPrefix(src[j:], "{{") && !(src[j] == '<' && tagEnd(src, j) > 0) {
j++
}
escape(&b, src[i:j])
i = j
}
}
return template.HTML(b.String())
}
func escape(b *strings.Builder, s string) {
for i := 0; i < len(s); i++ {
switch s[i] {
case '&':
b.WriteString("&")
case '<':
b.WriteString("<")
case '>':
b.WriteString(">")
default:
b.WriteByte(s[i])
}
}
}
func wrapped(b *strings.Builder, kind, s string) {
b.WriteString("")
escape(b, s)
b.WriteString("")
}
// tag writes one tag: its name, each attribute's name and value, and
// the whitespace and closing bracket between them as plain text.
func tag(b *strings.Builder, t string) {
n := 1
if strings.HasPrefix(t, "") {
n = 2
}
for n < len(t) && !isSpace(t[n]) && t[n] != '>' && t[n] != '/' {
n++
}
wrapped(b, "t", t[:n])
for i := n; i < len(t); {
c := t[i]
switch {
case isSpace(c) || c == '/' || c == '>':
escape(b, t[i:i+1])
i++
case c == '=':
b.WriteByte('=')
i++
j := i
if j < len(t) && (t[j] == '"' || t[j] == '\'') {
q := t[j]
j++
for j < len(t) && t[j] != q {
j++
}
if j < len(t) {
j++
}
} else {
for j < len(t) && !isSpace(t[j]) && t[j] != '>' {
j++
}
}
wrapped(b, "v", t[i:j])
i = j
default:
j := i
for j < len(t) && !isSpace(t[j]) && t[j] != '=' && t[j] != '>' && !(t[j] == '/' && j+1 < len(t) && t[j+1] == '>') {
j++
}
wrapped(b, "a", t[i:j])
i = j
}
}
}
// actionEnd returns the index just past the action starting at s[i],
// treating "}}" inside a string literal as part of the string.
func actionEnd(s string, i int) int {
for j := i + 2; j < len(s); j++ {
switch s[j] {
case '"':
for j++; j < len(s) && s[j] != '"'; j++ {
if s[j] == '\\' {
j++
}
}
case '`':
for j++; j < len(s) && s[j] != '`'; j++ {
}
case '}':
if j+1 < len(s) && s[j+1] == '}' {
return j + 2
}
}
}
return len(s)
}
// action writes one {{…}}: the delimiters, identifiers and .Fields as
// , string literals as , and everything else as text.
func action(b *strings.Builder, a string) {
body, closed := strings.CutSuffix(a[2:], "}}")
wrapped(b, "x", "{{")
for i := 0; i < len(body); {
c := body[i]
switch {
case c == '"' || c == '`':
j := i + 1
for j < len(body) && body[j] != c {
if c == '"' && body[j] == '\\' {
j++
}
j++
}
j = min(j+1, len(body))
wrapped(b, "s", body[i:j])
i = j
case isLetter(c) || c == '_' || c == '.':
j := i + 1
for j < len(body) && (isLetter(body[j]) || body[j] == '_' || body[j] == '.' || body[j] >= '0' && body[j] <= '9') {
j++
}
wrapped(b, "x", body[i:j])
i = j
default:
escape(b, body[i:i+1])
i++
}
}
if closed {
wrapped(b, "x", "}}")
}
}