package webui import ( "html" "strings" ) // Go syntax highlighting, for the code samples a documentation page shows. // // It is a LEXER, not a parser: it classifies tokens and gives up gracefully on anything // it does not understand, because a highlighter that can fail to render is worse than // one that occasionally paints an identifier the wrong colour. Unterminated strings and // comments run to the end of the input rather than throwing. // // Output is HTML, and every run of source text passes through html.EscapeString on the // way out — the input is Go source, which is full of `<`, `>` and `&`, and one of the // snippets this is meant to display is literally a block of HTML. // A code block is dark in BOTH themes — a light code block on a light page is a // different kind of thing, and switching it with the theme means the snippet you were // reading changes colour under you. So these are fixed on-dark colours, not theme // tokens: the surface they sit on never changes. const ( goCommentClass = "text-neutral-400" goStringClass = "text-emerald-300" goKeywordClass = "text-sky-300" goNumberClass = "text-amber-300" goFuncClass = "text-violet-300" ) var goKeywords = map[string]bool{ "break": true, "case": true, "chan": true, "const": true, "continue": true, "default": true, "defer": true, "else": true, "fallthrough": true, "for": true, "func": true, "go": true, "goto": true, "if": true, "import": true, "interface": true, "map": true, "package": true, "range": true, "return": true, "select": true, "struct": true, "switch": true, "type": true, "var": true, // Not keywords to the Go spec — predeclared identifiers — but every editor colours // them, and a reader looking for `nil` is looking for the same kind of thing. "nil": true, "true": true, "false": true, "iota": true, "string": true, "int": true, "int64": true, "float64": true, "bool": true, "byte": true, "rune": true, "any": true, "error": true, } // HighlightGo turns Go source into HTML with the tokens wrapped in coloured spans. // // The result is meant for vdom.Raw inside a
: it contains no block elements and
// preserves the source's whitespace exactly, so the 
 does the layout.
func HighlightGo(src string) string {
	var b strings.Builder
	b.Grow(len(src) * 2)

	i := 0
	for i < len(src) {
		c := src[i]

		switch {
		// Line comment — including the //gowasm: directives, which are the most
		// important line in several of these snippets.
		case c == '/' && i+1 < len(src) && src[i+1] == '/':
			end := strings.IndexByte(src[i:], '\n')
			if end < 0 {
				end = len(src)
			} else {
				end += i
			}
			span(&b, goCommentClass, src[i:end])
			i = end

		// Block comment.
		case c == '/' && i+1 < len(src) && src[i+1] == '*':
			end := strings.Index(src[i+2:], "*/")
			if end < 0 {
				end = len(src)
			} else {
				end = i + 2 + end + 2
			}
			span(&b, goCommentClass, src[i:end])
			i = end

		// Interpreted string. Ends at the closing quote or the line's end — an
		// unterminated string is a typo in a snippet, not a reason to paint the rest of
		// the file green.
		case c == '"':
			i = quoted(&b, src, i, '"', true)

		// Raw string: no escapes, and it may span lines.
		case c == '`':
			i = quoted(&b, src, i, '`', false)

		// Rune literal.
		case c == '\'':
			i = quoted(&b, src, i, '\'', true)

		case isDigit(c):
			j := i
			for j < len(src) && (isDigit(src[j]) || isHexish(src[j])) {
				j++
			}
			span(&b, goNumberClass, src[i:j])
			i = j

		case isIdentStart(c):
			j := i
			for j < len(src) && isIdentPart(src[j]) {
				j++
			}
			word := src[i:j]
			switch {
			case goKeywords[word]:
				span(&b, goKeywordClass, word)
			case callAhead(src, j):
				// An identifier immediately followed by "(" is being called (or is a type
				// being converted to). Colouring it is what makes the shape of a snippet
				// readable at a glance.
				span(&b, goFuncClass, word)
			default:
				b.WriteString(html.EscapeString(word))
			}
			i = j

		default:
			b.WriteString(html.EscapeString(string(c)))
			i++
		}
	}
	return b.String()
}

// quoted consumes a quoted literal starting at i and writes it as a string span.
// escapes reports whether a backslash escapes the next byte (false for raw strings).
func quoted(b *strings.Builder, src string, i int, quote byte, escapes bool) int {
	j := i + 1
	for j < len(src) {
		if escapes && src[j] == '\\' && j+1 < len(src) {
			j += 2
			continue
		}
		if src[j] == quote {
			j++
			break
		}
		if escapes && src[j] == '\n' {
			break // unterminated: stop at the line end rather than eating the file
		}
		j++
	}
	span(b, goStringClass, src[i:j])
	return j
}

// callAhead reports whether the next non-space byte at or after i is an opening paren.
func callAhead(src string, i int) bool {
	for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
		i++
	}
	return i < len(src) && src[i] == '('
}

func span(b *strings.Builder, class, text string) {
	b.WriteString(``)
	b.WriteString(html.EscapeString(text))
	b.WriteString(``)
}

// isDigit already exists in the package (autotable.go) — reused rather than shadowed.

func isHexish(c byte) bool {
	return c == '.' || c == 'x' || c == 'X' || c == '_' ||
		(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
func isIdentStart(c byte) bool { return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') }
func isIdentPart(c byte) bool  { return isIdentStart(c) || isDigit(c) }