highlight.go 5.3 KiB raw
1
package main
2
3
import (
4
	"html"
5
	"html/template"
6
	"path"
7
	"strings"
8
9
	"github.com/alecthomas/chroma/v2"
10
	"github.com/alecthomas/chroma/v2/lexers"
11
)
12
13
var radianceLexer = chroma.MustNewLexer(
14
	&chroma.Config{Name: "Radiance"},
15
	func() chroma.Rules {
16
		return chroma.Rules{"root": {
17
			{Pattern: `//[^\n]*`, Type: chroma.CommentSingle},
18
			{Pattern: `"(?:\\.|[^"\\])*"|'[^']{1,2}'`, Type: chroma.LiteralString},
19
			{Pattern: `\b(?:0x[0-9a-fA-F]+|0b[01]+|[0-9]+(?:\.[0-9]+)?)\b`, Type: chroma.LiteralNumber},
20
			{Pattern: `@[a-zA-Z]+`, Type: chroma.NameBuiltin},
21
			{Pattern: `\b(?:fn|if|else|for|while|break|switch|match|set|record|union|constant|align|let|use|mod|module|case|continue|return|true|false|loop|extern|panic|device|register|bit|catch|throw|throws|test|trait|instance|at|mut|nil|undefined|static|in|is|where|export|as|and|or|xor|not|try|atomic|select|of|assert)\b`, Type: chroma.Keyword},
22
			{Pattern: `\b(?:bool|u8|u16|u32|u64|i8|i16|i32|i64|f32|void|opaque)\b`, Type: chroma.KeywordType},
23
			{Pattern: `->|=>|::|\.\.|<>|\?{1,2}|!{1,2}|>=?|<=?|[&*.=+\-/%]`, Type: chroma.Operator},
24
			{Pattern: `[(){}\[\]]`, Type: chroma.Punctuation},
25
			{Pattern: `\s+`, Type: chroma.TextWhitespace},
26
			{Pattern: `.`, Type: chroma.Text},
27
		}}
28
	},
29
)
30
31
var radianceILLexer = chroma.MustNewLexer(
32
	&chroma.Config{Name: "Radiance IL"},
33
	func() chroma.Rules {
34
		return chroma.Rules{"root": {
35
			{Pattern: `//[^\n]*`, Type: chroma.CommentSingle},
36
			{Pattern: `"[^"\n]*"`, Type: chroma.LiteralString},
37
			{Pattern: `%[0-9]+`, Type: chroma.NameVariable},
38
			{Pattern: `@[A-Za-z_][A-Za-z#0-9_]*`, Type: chroma.NameLabel},
39
			{Pattern: `\$[A-Za-z_][A-Za-z0-9_]*`, Type: chroma.Name},
40
			{Pattern: `\b(?:-?[0-9]+|0x[a-fA-F0-9]+)\b`, Type: chroma.LiteralNumber},
41
			{Pattern: `\b(?:fn|data|extern|mut|align)\b`, Type: chroma.Keyword},
42
			{Pattern: `\b(?:br\.eq|br\.ne|br\.slt|br\.ult|reserve|load|sload|store|blit|copy|add|sub|mul|sdiv|udiv|srem|urem|neg|eq|ne|slt|sge|ult|uge|and|or|xor|shl|sshr|ushr|not|zext|sext|call|ret|jmp|switch|unreachable|ecall|ebreak)\b`, Type: chroma.NameBuiltin},
43
			{Pattern: `\b(?:str|sym|undef|w8|w16|w32|w64)\b`, Type: chroma.KeywordType},
44
			{Pattern: `[{}();:,]`, Type: chroma.Punctuation},
45
			{Pattern: `\s+`, Type: chroma.TextWhitespace},
46
			{Pattern: `.`, Type: chroma.Text},
47
		}}
48
	},
49
)
50
51
// highlightLines returns one escaped HTML fragment for each source line.
52
func highlightLines(filename string, lines []string) []template.HTML {
53
	if len(lines) == 0 {
54
		return nil
55
	}
56
57
	lexer := lexerForFilename(filename)
58
	iterator, err := lexer.Tokenise(nil, strings.Join(lines, "\n"))
59
	if err != nil {
60
		return escapeLines(lines)
61
	}
62
63
	highlighted := make([]strings.Builder, len(lines))
64
	line := 0
65
	for token := iterator(); token != chroma.EOF; token = iterator() {
66
		parts := strings.Split(token.Value, "\n")
67
		class := tokenClass(token.Type)
68
		for i, part := range parts {
69
			if i > 0 {
70
				line++
71
			}
72
			if line >= len(highlighted) || part == "" {
73
				continue
74
			}
75
			if class != "" {
76
				highlighted[line].WriteString(`<span class="`)
77
				highlighted[line].WriteString(class)
78
				highlighted[line].WriteString(`">`)
79
			}
80
			highlighted[line].WriteString(html.EscapeString(part))
81
			if class != "" {
82
				highlighted[line].WriteString(`</span>`)
83
			}
84
		}
85
	}
86
87
	result := make([]template.HTML, len(highlighted))
88
	for i := range highlighted {
89
		result[i] = template.HTML(highlighted[i].String())
90
	}
91
	return result
92
}
93
94
func highlightDiff(file DiffFile, lines []DiffLine) {
95
	oldSource := make([]string, 0, len(lines))
96
	newSource := make([]string, 0, len(lines))
97
	for i := range lines {
98
		if lines[i].OldNum > 0 {
99
			oldSource = append(oldSource, lines[i].Content)
100
		}
101
		if lines[i].NewNum > 0 {
102
			newSource = append(newSource, lines[i].Content)
103
		}
104
	}
105
106
	oldHighlighted := highlightLines(file.OldName, oldSource)
107
	newHighlighted := highlightLines(file.NewName, newSource)
108
	oldIndex, newIndex := 0, 0
109
	for i := range lines {
110
		line := &lines[i]
111
		if line.Type == "del" {
112
			line.Highlighted = oldHighlighted[oldIndex]
113
		} else {
114
			line.Highlighted = newHighlighted[newIndex]
115
		}
116
		if line.OldNum > 0 {
117
			oldIndex++
118
		}
119
		if line.NewNum > 0 {
120
			newIndex++
121
		}
122
	}
123
}
124
125
func lexerForFilename(filename string) chroma.Lexer {
126
	switch strings.ToLower(path.Ext(filename)) {
127
	case ".rad":
128
		return radianceLexer
129
	case ".ril":
130
		return radianceILLexer
131
	}
132
	if lexer := lexers.Match(filename); lexer != nil {
133
		return chroma.Coalesce(lexer)
134
	}
135
	return lexers.Fallback
136
}
137
138
func tokenClass(tokenType chroma.TokenType) string {
139
	switch {
140
	case tokenType.InCategory(chroma.Keyword):
141
		return "syntax-keyword"
142
	case tokenType.InSubCategory(chroma.NameBuiltin):
143
		return "syntax-builtin"
144
	case tokenType.InSubCategory(chroma.LiteralString):
145
		return "syntax-string"
146
	case tokenType.InSubCategory(chroma.LiteralNumber):
147
		return "syntax-number"
148
	case tokenType.InCategory(chroma.Comment):
149
		return "syntax-comment"
150
	case tokenType.InCategory(chroma.Operator):
151
		return "syntax-operator"
152
	case tokenType.InCategory(chroma.Punctuation):
153
		return "syntax-punctuation"
154
	case tokenType.InSubCategory(chroma.NameVariable), tokenType == chroma.NameLabel:
155
		return "syntax-name"
156
	default:
157
		return ""
158
	}
159
}
160
161
func escapeLines(lines []string) []template.HTML {
162
	escaped := make([]template.HTML, len(lines))
163
	for i, line := range lines {
164
		escaped[i] = template.HTML(html.EscapeString(line))
165
	}
166
	return escaped
167
}