Files
wehub-resource-sync c4536f7e05
CI / test (push) Failing after 1s
CI / macOS amd64 (push) Has been cancelled
CI / macOS arm64 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:31 +08:00

454 lines
12 KiB
Go

package syntax
import (
"go/scanner"
"go/token"
"image/color"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)
const (
StyleGoLand = "goland-light"
StyleDarcula = "darcula"
StyleMono = "mono"
)
type Palette struct {
Plain color.NRGBA
Keyword color.NRGBA
Builtin color.NRGBA
String color.NRGBA
Number color.NRGBA
Comment color.NRGBA
Operator color.NRGBA
Register color.NRGBA
Mnemonic color.NRGBA
Symbol color.NRGBA
LineNumber color.NRGBA
CallTarget color.NRGBA
}
func NormalizeStyle(style string) string {
switch strings.ToLower(strings.TrimSpace(style)) {
case "", "goland", "goland-light", "jetbrains":
return StyleGoLand
case "darcula", "goland-dark", "jetbrains-dark":
return StyleDarcula
case "mono", "monochrome", "plain":
return StyleMono
default:
return StyleGoLand
}
}
func StyleLabel(style string) string {
switch NormalizeStyle(style) {
case StyleDarcula:
return "Darcula"
case StyleMono:
return "Mono"
default:
return "GoLand"
}
}
// Colors are the theme colors the palettes derive from.
type Colors struct {
Text color.NRGBA
MutedText color.NRGBA
Background color.NRGBA
}
func PaletteFor(style string, colors Colors) Palette {
switch NormalizeStyle(style) {
case StyleDarcula:
if !syntaxBackgroundDark(colors.Background) {
return Palette{
Plain: color.NRGBA{R: 0x2b, G: 0x2d, B: 0x30, A: 0xff},
Keyword: color.NRGBA{R: 0xa8, G: 0x55, B: 0x22, A: 0xff},
Builtin: color.NRGBA{R: 0x3a, G: 0x72, B: 0x80, A: 0xff},
String: color.NRGBA{R: 0x3c, G: 0x78, B: 0x35, A: 0xff},
Number: color.NRGBA{R: 0x17, G: 0x50, B: 0xeb, A: 0xff},
Comment: color.NRGBA{R: 0x7a, G: 0x7a, B: 0x7a, A: 0xff},
Operator: color.NRGBA{R: 0x2b, G: 0x2d, B: 0x30, A: 0xff},
Register: color.NRGBA{R: 0x66, G: 0x0e, B: 0x7a, A: 0xff},
Mnemonic: color.NRGBA{R: 0xa8, G: 0x55, B: 0x22, A: 0xff},
Symbol: color.NRGBA{R: 0x2b, G: 0x68, B: 0x88, A: 0xff},
LineNumber: colors.MutedText,
CallTarget: color.NRGBA{R: 0x00, G: 0x00, B: 0xee, A: 0xff},
}
}
return Palette{
Plain: color.NRGBA{R: 0xa9, G: 0xb7, B: 0xc6, A: 0xff},
Keyword: color.NRGBA{R: 0xcc, G: 0x78, B: 0x32, A: 0xff},
Builtin: color.NRGBA{R: 0x88, G: 0xc0, B: 0xd0, A: 0xff},
String: color.NRGBA{R: 0x6a, G: 0x87, B: 0x59, A: 0xff},
Number: color.NRGBA{R: 0x68, G: 0x97, B: 0xbb, A: 0xff},
Comment: color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff},
Operator: color.NRGBA{R: 0xa9, G: 0xb7, B: 0xc6, A: 0xff},
Register: color.NRGBA{R: 0x98, G: 0x76, B: 0xaa, A: 0xff},
Mnemonic: color.NRGBA{R: 0xff, G: 0xc6, B: 0x6d, A: 0xff},
Symbol: color.NRGBA{R: 0xa5, G: 0xc2, B: 0x61, A: 0xff},
LineNumber: colors.MutedText,
CallTarget: color.NRGBA{R: 0x62, G: 0x9c, B: 0xf6, A: 0xff},
}
case StyleMono:
return Palette{
Plain: colors.Text,
Keyword: colors.Text,
Builtin: colors.Text,
String: colors.Text,
Number: colors.Text,
Comment: colors.MutedText,
Operator: colors.Text,
Register: colors.Text,
Mnemonic: colors.Text,
Symbol: colors.Text,
LineNumber: colors.MutedText,
CallTarget: colors.Text,
}
default:
if syntaxBackgroundDark(colors.Background) {
return Palette{
Plain: color.NRGBA{R: 0xba, G: 0xbe, B: 0xc7, A: 0xff},
Keyword: color.NRGBA{R: 0xcf, G: 0x8e, B: 0x6d, A: 0xff},
Builtin: color.NRGBA{R: 0xbc, G: 0x94, B: 0xf9, A: 0xff},
String: color.NRGBA{R: 0x6a, G: 0xab, B: 0x73, A: 0xff},
Number: color.NRGBA{R: 0x2a, G: 0xa1, B: 0xf3, A: 0xff},
Comment: color.NRGBA{R: 0x7a, G: 0x80, B: 0x87, A: 0xff},
Operator: color.NRGBA{R: 0xba, G: 0xbe, B: 0xc7, A: 0xff},
Register: color.NRGBA{R: 0xbc, G: 0x94, B: 0xf9, A: 0xff},
Mnemonic: color.NRGBA{R: 0xcf, G: 0x8e, B: 0x6d, A: 0xff},
Symbol: color.NRGBA{R: 0x56, G: 0xa8, B: 0xf5, A: 0xff},
LineNumber: colors.MutedText,
CallTarget: color.NRGBA{R: 0x56, G: 0xa8, B: 0xf5, A: 0xff},
}
}
return Palette{
Plain: color.NRGBA{R: 0x08, G: 0x08, B: 0x08, A: 0xff},
Keyword: color.NRGBA{R: 0x00, G: 0x00, B: 0x80, A: 0xff},
Builtin: color.NRGBA{R: 0x87, G: 0x10, B: 0x94, A: 0xff},
String: color.NRGBA{R: 0x00, G: 0x80, B: 0x00, A: 0xff},
Number: color.NRGBA{R: 0x17, G: 0x50, B: 0xeb, A: 0xff},
Comment: color.NRGBA{R: 0x8c, G: 0x8c, B: 0x8c, A: 0xff},
Operator: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff},
Register: color.NRGBA{R: 0x66, G: 0x0e, B: 0x7a, A: 0xff},
Mnemonic: color.NRGBA{R: 0x00, G: 0x00, B: 0x80, A: 0xff},
Symbol: color.NRGBA{R: 0x87, G: 0x10, B: 0x94, A: 0xff},
LineNumber: colors.MutedText,
CallTarget: color.NRGBA{R: 0x00, G: 0x00, B: 0xee, A: 0xff},
}
}
}
func syntaxBackgroundDark(bg color.NRGBA) bool {
return int(bg.R)*299+int(bg.G)*587+int(bg.B)*114 < 128000
}
func HighlightSource(lineNo int, line string, palette Palette) []Span {
spans := []Span{{
Text: lineNumberPrefix(lineNo),
Color: palette.LineNumber,
}}
return append(spans, HighlightGo(line, palette)...)
}
func HighlightGo(line string, palette Palette) []Span {
if line == "" {
return nil
}
var spans []Span
fset := token.NewFileSet()
file := fset.AddFile("", -1, len(line))
var scan scanner.Scanner
scan.Init(file, []byte(line), nil, scanner.ScanComments)
cursor := 0
for {
pos, tok, lit := scan.Scan()
if tok == token.EOF {
break
}
if tok == token.SEMICOLON && lit == "\n" {
continue
}
start := file.Offset(pos)
if start > len(line) {
break
}
if start < cursor {
continue
}
if start > cursor {
appendSyntaxSpan(&spans, line[cursor:start], palette.Plain, false, false)
}
text := lit
if text == "" {
text = tok.String()
}
if text == "" {
continue
}
end := start + len(text)
if end > len(line) {
end = len(line)
text = line[start:end]
}
col, italic := goTokenStyle(tok, text, palette)
appendSyntaxSpan(&spans, text, col, false, italic)
cursor = end
}
if cursor < len(line) {
appendSyntaxSpan(&spans, line[cursor:], palette.Plain, false, false)
}
return spans
}
func HighlightAsm(line string, callTarget string, palette Palette) []Span {
return highlightAsmLine(line, callTarget, palette)
}
func lineNumberPrefix(lineNo int) string {
var b strings.Builder
b.WriteString(strconv.Itoa(lineNo))
for b.Len() < 4 {
b.WriteByte(' ')
}
b.WriteByte(' ')
return b.String()
}
func goTokenStyle(tok token.Token, text string, palette Palette) (color.NRGBA, bool) {
switch {
case tok.IsKeyword():
return palette.Keyword, false
case tok == token.COMMENT:
return palette.Comment, true
case tok == token.STRING || tok == token.CHAR:
return palette.String, false
case tok == token.INT || tok == token.FLOAT || tok == token.IMAG:
return palette.Number, false
case tok == token.IDENT && goBuiltinNames[text]:
return palette.Builtin, false
case tok.IsOperator():
return palette.Operator, false
default:
return palette.Plain, false
}
}
func highlightAsmLine(line string, callTarget string, palette Palette) []Span {
if line == "" {
return nil
}
code, comment := splitAsmComment(line)
spans := highlightAsmCode(code, callTarget, palette)
if comment != "" {
appendSyntaxSpan(&spans, comment, palette.Comment, false, true)
}
return spans
}
func splitAsmComment(line string) (string, string) {
commentAt := len(line)
for _, marker := range []string{"//", ";"} {
if ix := strings.Index(line, marker); ix >= 0 && ix < commentAt {
commentAt = ix
}
}
if commentAt == len(line) {
return line, ""
}
return line[:commentAt], line[commentAt:]
}
func highlightAsmCode(code string, callTarget string, palette Palette) []Span {
var spans []Span
seenMnemonic := false
for i := 0; i < len(code); {
r, size := runeAt(code, i)
if unicode.IsSpace(r) {
start := i
i += size
for i < len(code) {
r, size = runeAt(code, i)
if !unicode.IsSpace(r) {
break
}
i += size
}
appendSyntaxSpan(&spans, code[start:i], palette.Plain, false, false)
continue
}
if isAsmPunctuation(r) {
appendSyntaxSpan(&spans, code[i:i+size], palette.Operator, false, false)
i += size
continue
}
start := i
i += size
for i < len(code) {
r, size = runeAt(code, i)
if unicode.IsSpace(r) || isAsmPunctuation(r) {
break
}
i += size
}
word := code[start:i]
col := asmWordColor(word, seenMnemonic, callTarget, palette)
bold := false
if !seenMnemonic && strings.TrimSpace(word) != "" {
seenMnemonic = true
bold = true
}
appendSyntaxSpan(&spans, word, col, bold, false)
}
return spans
}
func asmWordColor(word string, seenMnemonic bool, callTarget string, palette Palette) color.NRGBA {
trimmed := strings.TrimSpace(word)
trimmed = strings.Trim(trimmed, "[]{}")
upper := strings.ToUpper(strings.TrimPrefix(trimmed, "%"))
switch {
case trimmed == "":
return palette.Plain
case !seenMnemonic:
return palette.Mnemonic
case callTarget != "" && strings.Contains(trimmed, callTarget):
return palette.CallTarget
case isAsmNumber(trimmed):
return palette.Number
case isAsmRegister(upper):
return palette.Register
case strings.ContainsAny(trimmed, "./:<>@") || strings.Contains(trimmed, "(SB)"):
return palette.Symbol
default:
return palette.Plain
}
}
func isAsmNumber(word string) bool {
word = strings.TrimPrefix(word, "$")
word = strings.TrimPrefix(word, "#")
word = strings.TrimPrefix(word, "-")
word = strings.TrimPrefix(word, "+")
if word == "" {
return false
}
if strings.HasPrefix(word, "0x") || strings.HasPrefix(word, "0X") {
if len(word) <= 2 {
return false
}
for _, r := range word[2:] {
if !('0' <= r && r <= '9') && !('a' <= r && r <= 'f') && !('A' <= r && r <= 'F') {
return false
}
}
return true
}
for _, r := range word {
if !('0' <= r && r <= '9') {
return false
}
}
return true
}
func isAsmRegister(word string) bool {
if asmRegisterNames[word] {
return true
}
if len(word) >= 2 {
prefix := word[0]
if prefix == 'R' || prefix == 'X' || prefix == 'W' || prefix == 'V' || prefix == 'Q' || prefix == 'D' || prefix == 'S' || prefix == 'B' {
for _, r := range word[1:] {
if r < '0' || r > '9' {
return false
}
}
return true
}
}
return false
}
func isAsmPunctuation(r rune) bool {
switch r {
case ',', '(', ')', '[', ']', '{', '}', '+', '-', '*':
return true
default:
return false
}
}
func appendSyntaxSpan(spans *[]Span, text string, col color.NRGBA, bold, italic bool) {
if text == "" {
return
}
last := len(*spans) - 1
if last >= 0 {
prev := &(*spans)[last]
if prev.Color == col && prev.Bold == bold && prev.Italic == italic {
prev.Text += text
return
}
}
*spans = append(*spans, Span{
Text: text,
Color: col,
Bold: bold,
Italic: italic,
})
}
func runeAt(s string, i int) (rune, int) {
r := rune(s[i])
if r < utf8.RuneSelf {
return r, 1
}
return utf8.DecodeRuneInString(s[i:])
}
var goBuiltinNames = map[string]bool{
"any": true, "append": true, "bool": true, "byte": true, "cap": true,
"clear": true, "close": true, "comparable": true, "complex": true,
"complex64": true, "complex128": true, "copy": true, "delete": true,
"error": true, "false": true, "float32": true, "float64": true,
"imag": true, "int": true, "int8": true, "int16": true, "int32": true,
"int64": true, "iota": true, "len": true, "make": true, "max": true,
"min": true, "new": true, "nil": true, "panic": true, "print": true,
"println": true, "real": true, "recover": true, "rune": true,
"string": true, "true": true, "uint": true, "uint8": true,
"uint16": true, "uint32": true, "uint64": true, "uintptr": true,
}
var asmRegisterNames = map[string]bool{
"AH": true, "AL": true, "AX": true, "BH": true, "BL": true, "BP": true,
"BX": true, "CH": true, "CL": true, "CS": true, "CX": true, "DH": true,
"DI": true, "DL": true, "DS": true, "DX": true, "ES": true, "FLAGS": true,
"FP": true, "FS": true, "GS": true, "IP": true, "LR": true, "PC": true,
"SB": true, "SI": true, "SP": true, "TLS": true, "ZR": true,
"EAX": true, "EBP": true, "EBX": true, "ECX": true, "EDI": true,
"EDX": true, "EFLAGS": true, "EIP": true, "ESI": true, "ESP": true,
"RAX": true, "RBP": true, "RBX": true, "RCX": true, "RDI": true,
"RDX": true, "RIP": true, "RSB": true, "RSI": true, "RSP": true,
}
type Span struct {
Text string
Color color.NRGBA
Italic bool
Bold bool
}