Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/web/highlight.go

Raw
package web

import (
	"html/template"
	"strings"
)

// HighlightLog renders a log tail as HTML with structural lines accented:
// [luci] markers, step failures, and common error shapes stand out, build
// noise recedes. All content stays escaped.
func HighlightLog(log string) template.HTML {
	var b strings.Builder
	for i, line := range strings.Split(log, "\n") {
		if i > 0 {
			b.WriteByte('\n')
		}
		if line == "" {
			continue
		}
		escaped := template.HTMLEscapeString(line)
		switch {
		case strings.HasPrefix(line, "[luci] step ") && (strings.Contains(line, " failed after ") || strings.Contains(line, "done in")):
			b.WriteString(`<span class="log-mark">` + escaped + `</span>`)
		case strings.HasPrefix(line, "[luci]"):
			b.WriteString(`<span class="log-mark">` + escaped + `</span>`)
		case isErrorLine(line):
			b.WriteString(`<span class="log-fail">` + escaped + `</span>`)
		case isNoiseLine(line):
			b.WriteString(`<span class="log-dim">` + escaped + `</span>`)
		default:
			b.WriteString(escaped)
		}
	}
	return template.HTML(b.String())
}

func isErrorLine(line string) bool {
	for _, prefix := range []string{"error", "Error", "ERROR", "error[", "failed", "FAILED", "Failed", "panic:", "warning:", "warning: ", "✖", "exit status"} {
		if strings.HasPrefix(line, prefix) {
			return true
		}
	}
	return false
}

func isNoiseLine(line string) bool {
	for _, prefix := range []string{"   Compiling", " Downloaded", " Downloading", "  Building", "   Building", "info:", "notice:"} {
		if strings.HasPrefix(line, prefix) {
			return true
		}
	}
	return false
}

// HighlightKDL accents syntax without interpreting configuration content.
// Every source byte is escaped before it reaches the template.
func HighlightKDL(source string) template.HTML {
	var out strings.Builder
	for i := 0; i < len(source); {
		end := i
		class := ""
		switch source[i] {
		case '"':
			end++
			for end < len(source) {
				if source[end] == '\\' && end+1 < len(source) {
					end += 2
					continue
				}
				if source[end] == '"' {
					end++
					break
				}
				end++
			}
			class = "kdl-string"
		case '/':
			if i+1 < len(source) && source[i+1] == '/' {
				end = strings.IndexByte(source[i:], '\n')
				if end < 0 {
					end = len(source)
				} else {
					end += i
				}
				class = "kdl-comment"
			}
		}
		if end == i {
			for end < len(source) && (source[end] == '_' || source[end] == '-' || (source[end] >= 'a' && source[end] <= 'z') || (source[end] >= 'A' && source[end] <= 'Z')) {
				end++
			}
			if end == i {
				end++
			} else if isKDLKeyword(source[i:end]) {
				class = "kdl-keyword"
			}
		}
		text := template.HTMLEscapeString(source[i:end])
		if class != "" {
			out.WriteString(`<span class="` + class + `">` + text + `</span>`)
		} else {
			out.WriteString(text)
		}
		i = end
	}
	return template.HTML(out.String())
}

func isKDLKeyword(value string) bool {
	switch value {
	case "job", "image", "run", "trigger", "push", "branch", "tag", "include", "exclude", "schedule", "matrix", "cache", "key", "file", "secret", "publish", "from", "to":
		return true
	}
	return false
}