grimoir/syntax.go

286 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"regexp"
"strings"
"github.com/charmbracelet/lipgloss"
)
// === Цвета подсветки синтаксиса ===
var (
colorKeyword = lipgloss.NewStyle().Foreground(lipgloss.Color("207")).Bold(true)
colorString = lipgloss.NewStyle().Foreground(lipgloss.Color("106"))
colorComment = lipgloss.NewStyle().Foreground(lipgloss.Color("59")).Italic(true)
colorNumber = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
colorFunc = lipgloss.NewStyle().Foreground(lipgloss.Color("81")).Bold(true)
)
// LanguageConfig описывает правила подсветки для одного языка
type LanguageConfig struct {
Keywords []string
LineCommentPrefix string
StringDelimiters []string
HasNumbers bool
CaseInsensitive bool
VarPrefix string
}
var languageConfigs = map[string]LanguageConfig{
"go": {
Keywords: []string{"func", "package", "import", "var", "const", "type", "struct", "interface", "if", "else", "for", "switch", "case", "default", "return", "break", "continue", "defer", "go", "chan", "select", "map", "range", "nil", "true", "false"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`, "`"},
HasNumbers: true,
},
"py": {
Keywords: []string{"def", "class", "if", "else", "elif", "for", "while", "import", "from", "return", "pass", "break", "continue", "lambda", "try", "except", "finally", "with", "as", "True", "False", "None", "and", "or", "not", "in", "is", "raise", "yield", "global", "nonlocal", "del", "assert"},
LineCommentPrefix: "#",
StringDelimiters: []string{`"`, `'`},
HasNumbers: true,
},
"js": {
Keywords: []string{"function", "const", "let", "var", "if", "else", "for", "while", "switch", "case", "default", "return", "break", "continue", "async", "await", "import", "export", "class", "extends", "new", "this", "super", "true", "false", "null", "undefined", "typeof", "instanceof", "void", "delete", "throw", "try", "catch", "finally"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`, `'`, "`"},
HasNumbers: true,
},
"ts": {
Keywords: []string{"function", "const", "let", "var", "if", "else", "for", "while", "switch", "case", "default", "return", "break", "continue", "async", "await", "import", "export", "class", "extends", "new", "this", "super", "true", "false", "null", "undefined", "type", "interface", "enum", "implements", "namespace", "abstract", "readonly", "public", "private", "protected"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`, `'`, "`"},
HasNumbers: true,
},
"rs": {
Keywords: []string{"fn", "let", "mut", "const", "static", "if", "else", "match", "for", "while", "loop", "break", "continue", "return", "pub", "mod", "use", "struct", "enum", "trait", "impl", "unsafe", "async", "await", "move", "ref", "self", "Self", "super", "crate", "true", "false"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"c": {
Keywords: []string{"int", "char", "void", "float", "double", "long", "short", "unsigned", "signed", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "struct", "union", "enum", "typedef", "static", "const", "extern", "volatile", "sizeof", "NULL"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"cpp": {
Keywords: []string{"int", "char", "void", "float", "double", "long", "short", "unsigned", "signed", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "struct", "union", "enum", "typedef", "static", "const", "extern", "volatile", "class", "public", "private", "protected", "virtual", "override", "new", "delete", "nullptr", "true", "false", "auto", "template", "typename", "namespace", "using", "this"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"java": {
Keywords: []string{"class", "interface", "extends", "implements", "public", "private", "protected", "static", "final", "void", "int", "boolean", "char", "double", "float", "long", "short", "byte", "if", "else", "for", "while", "do", "switch", "case", "default", "return", "break", "continue", "new", "this", "super", "null", "true", "false", "try", "catch", "finally", "throw", "throws", "import", "package", "abstract", "synchronized"},
LineCommentPrefix: "//",
StringDelimiters: []string{`"`},
HasNumbers: true,
},
"sql": {
Keywords: []string{"SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "CREATE", "TABLE", "DROP", "ALTER", "ADD", "COLUMN", "INDEX", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "ON", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN", "ORDER", "BY", "GROUP", "HAVING", "LIMIT", "OFFSET", "AS", "DISTINCT", "COUNT", "SUM", "AVG", "MAX", "MIN", "NULL", "IS", "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "CONSTRAINT", "UNIQUE", "DEFAULT"},
LineCommentPrefix: "--",
StringDelimiters: []string{`'`},
HasNumbers: true,
CaseInsensitive: true,
},
"sh": {
Keywords: []string{"if", "then", "else", "elif", "fi", "for", "do", "done", "while", "case", "esac", "function", "return", "export", "local", "readonly", "unset", "shift", "echo", "exit", "break", "continue", "true", "false"},
LineCommentPrefix: "#",
StringDelimiters: []string{`"`, `'`},
HasNumbers: false,
VarPrefix: "$",
},
}
// tokenKind — тип токена
type tokenKind int
const (
tokenPlain tokenKind = iota
tokenKeyword // ключевое слово
tokenString // строковый литерал
tokenComment // комментарий
tokenNumber // число
tokenVar // переменная ($VAR)
)
// token — фрагмент строки с типом
type token struct {
text string
kind tokenKind
}
// HighlightLine подсвечивает одну строку кода
func HighlightLine(line, fileType string) string {
cfg, ok := languageConfigs[fileType]
if !ok {
return line
}
tokens := tokenize(line, cfg)
return renderTokens(tokens, cfg)
}
// tokenize разбивает строку на токены не применяя стили
func tokenize(line string, cfg LanguageConfig) []token {
if line == "" {
return nil
}
// Шаг 1: находим комментарий — всё после него plain (красим как comment целиком)
commentStart := -1
if cfg.LineCommentPrefix != "" {
commentStart = strings.Index(line, cfg.LineCommentPrefix)
}
code := line
comment := ""
if commentStart >= 0 {
code = line[:commentStart]
comment = line[commentStart:]
}
// Шаг 2: разбиваем code на токены через span-маппинг
// Каждая позиция байта получает тип; потом склеиваем в токены
n := len(code)
kinds := make([]tokenKind, n)
// Строковые литералы
for _, delim := range cfg.StringDelimiters {
var re *regexp.Regexp
switch delim {
case `"`:
re = regexp.MustCompile(`"(?:[^"\\]|\\.)*"`)
case `'`:
re = regexp.MustCompile(`'(?:[^'\\]|\\.)*'`)
case "`":
re = regexp.MustCompile("`[^`]*`")
}
if re == nil {
continue
}
for _, loc := range re.FindAllStringIndex(code, -1) {
// Только если это место ещё не занято
already := false
for i := loc[0]; i < loc[1]; i++ {
if kinds[i] != tokenPlain {
already = true
break
}
}
if !already {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenString
}
}
}
}
// Ключевые слова
for _, kw := range cfg.Keywords {
var re *regexp.Regexp
if cfg.CaseInsensitive {
re = regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(kw) + `\b`)
} else {
re = regexp.MustCompile(`\b` + regexp.QuoteMeta(kw) + `\b`)
}
for _, loc := range re.FindAllStringIndex(code, -1) {
// Не перекрываем строки
inString := false
for i := loc[0]; i < loc[1]; i++ {
if kinds[i] == tokenString {
inString = true
break
}
}
if !inString {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenKeyword
}
}
}
}
// Числа
if cfg.HasNumbers {
re := regexp.MustCompile(`\b\d+(\.\d+)?\b`)
for _, loc := range re.FindAllStringIndex(code, -1) {
inString := false
for i := loc[0]; i < loc[1]; i++ {
if kinds[i] == tokenString {
inString = true
break
}
}
if !inString {
// Не красим если уже keyword
isKW := false
for i := loc[0]; i < loc[1]; i++ {
if kinds[i] == tokenKeyword {
isKW = true
break
}
}
if !isKW {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenNumber
}
}
}
}
}
// Переменные ($VAR)
if cfg.VarPrefix != "" {
re := regexp.MustCompile(regexp.QuoteMeta(cfg.VarPrefix) + `\w+`)
for _, loc := range re.FindAllStringIndex(code, -1) {
for i := loc[0]; i < loc[1]; i++ {
kinds[i] = tokenVar
}
}
}
// Шаг 3: склеиваем соседние байты одного типа в токены
var tokens []token
i := 0
for i < n {
k := kinds[i]
j := i + 1
for j < n && kinds[j] == k {
j++
}
tokens = append(tokens, token{text: code[i:j], kind: k})
i = j
}
// Комментарий добавляем в конец
if comment != "" {
tokens = append(tokens, token{text: comment, kind: tokenComment})
}
return tokens
}
// renderTokens применяет lipgloss стили к токенам
func renderTokens(tokens []token, cfg LanguageConfig) string {
var sb strings.Builder
for _, t := range tokens {
switch t.kind {
case tokenKeyword:
text := t.text
if cfg.CaseInsensitive {
text = strings.ToUpper(text)
}
sb.WriteString(colorKeyword.Render(text))
case tokenString:
sb.WriteString(colorString.Render(t.text))
case tokenComment:
sb.WriteString(colorComment.Render(t.text))
case tokenNumber:
sb.WriteString(colorNumber.Render(t.text))
case tokenVar:
sb.WriteString(colorFunc.Render(t.text))
default:
sb.WriteString(t.text)
}
}
return sb.String()
}