Skip to content

Commit 5873365

Browse files
committed
⭐️ syntax highlighting
1 parent 856f255 commit 5873365

File tree

3 files changed

+512
-42
lines changed

3 files changed

+512
-42
lines changed

cli/shell/highlight.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package shell
5+
6+
import (
7+
"regexp"
8+
"strings"
9+
10+
"github.com/charmbracelet/lipgloss"
11+
)
12+
13+
// MQL syntax highlighting colors
14+
var (
15+
hlKeyword = lipgloss.NewStyle().Foreground(lipgloss.Color("204")) // Pink for keywords
16+
hlResource = lipgloss.NewStyle().Foreground(lipgloss.Color("39")) // Blue for resources
17+
hlField = lipgloss.NewStyle().Foreground(lipgloss.Color("156")) // Light green for fields
18+
hlString = lipgloss.NewStyle().Foreground(lipgloss.Color("221")) // Yellow for strings
19+
hlNumber = lipgloss.NewStyle().Foreground(lipgloss.Color("141")) // Purple for numbers
20+
hlOperator = lipgloss.NewStyle().Foreground(lipgloss.Color("203")) // Red for operators
21+
hlBracket = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) // Gray for brackets
22+
hlComment = lipgloss.NewStyle().Foreground(lipgloss.Color("242")) // Dark gray for comments
23+
)
24+
25+
// MQL keywords
26+
var mqlKeywords = map[string]bool{
27+
"if": true, "else": true, "return": true, "where": true,
28+
"contains": true, "in": true, "not": true, "and": true, "or": true,
29+
"true": true, "false": true, "null": true, "props": true,
30+
}
31+
32+
// Common MQL resources (top-level)
33+
var mqlResources = map[string]bool{
34+
"asset": true, "mondoo": true, "users": true, "groups": true,
35+
"packages": true, "services": true, "processes": true, "ports": true,
36+
"files": true, "file": true, "command": true, "parse": true,
37+
"platform": true, "kernel": true, "sshd": true, "os": true,
38+
"aws": true, "gcp": true, "azure": true, "k8s": true, "terraform": true,
39+
"arista": true, "github": true, "gitlab": true, "okta": true, "ms365": true,
40+
"vsphere": true, "docker": true, "container": true, "image": true,
41+
}
42+
43+
// highlightMQL applies syntax highlighting to MQL code
44+
func highlightMQL(code string) string {
45+
// Handle empty input
46+
if code == "" {
47+
return code
48+
}
49+
50+
var result strings.Builder
51+
i := 0
52+
53+
for i < len(code) {
54+
// Check for comments
55+
if i+1 < len(code) && code[i:i+2] == "//" {
56+
end := strings.Index(code[i:], "\n")
57+
if end == -1 {
58+
result.WriteString(hlComment.Render(code[i:]))
59+
break
60+
}
61+
result.WriteString(hlComment.Render(code[i : i+end]))
62+
i += end
63+
continue
64+
}
65+
66+
// Check for strings (double quotes)
67+
if code[i] == '"' {
68+
end := i + 1
69+
for end < len(code) && code[end] != '"' {
70+
if code[end] == '\\' && end+1 < len(code) {
71+
end += 2
72+
} else {
73+
end++
74+
}
75+
}
76+
if end < len(code) {
77+
end++ // include closing quote
78+
}
79+
result.WriteString(hlString.Render(code[i:end]))
80+
i = end
81+
continue
82+
}
83+
84+
// Check for strings (single quotes)
85+
if code[i] == '\'' {
86+
end := i + 1
87+
for end < len(code) && code[end] != '\'' {
88+
if code[end] == '\\' && end+1 < len(code) {
89+
end += 2
90+
} else {
91+
end++
92+
}
93+
}
94+
if end < len(code) {
95+
end++ // include closing quote
96+
}
97+
result.WriteString(hlString.Render(code[i:end]))
98+
i = end
99+
continue
100+
}
101+
102+
// Check for numbers
103+
if isDigit(code[i]) {
104+
end := i
105+
for end < len(code) && (isDigit(code[end]) || code[end] == '.') {
106+
end++
107+
}
108+
result.WriteString(hlNumber.Render(code[i:end]))
109+
i = end
110+
continue
111+
}
112+
113+
// Check for operators
114+
if isOperator(code[i]) {
115+
// Handle multi-character operators
116+
op := string(code[i])
117+
if i+1 < len(code) {
118+
twoChar := code[i : i+2]
119+
if twoChar == "==" || twoChar == "!=" || twoChar == ">=" ||
120+
twoChar == "<=" || twoChar == "&&" || twoChar == "||" ||
121+
twoChar == "=~" || twoChar == "!~" {
122+
op = twoChar
123+
}
124+
}
125+
result.WriteString(hlOperator.Render(op))
126+
i += len(op)
127+
continue
128+
}
129+
130+
// Check for brackets
131+
if isBracket(code[i]) {
132+
result.WriteString(hlBracket.Render(string(code[i])))
133+
i++
134+
continue
135+
}
136+
137+
// Check for identifiers (words)
138+
if isAlpha(code[i]) || code[i] == '_' {
139+
end := i
140+
for end < len(code) && (isAlphaNum(code[end]) || code[end] == '_') {
141+
end++
142+
}
143+
word := code[i:end]
144+
145+
// Check if it's followed by a dot (field access)
146+
isFieldAccess := end < len(code) && code[end] == '.'
147+
148+
// Check what comes before (to detect if it's a field after a dot)
149+
isAfterDot := i > 0 && code[i-1] == '.'
150+
151+
if mqlKeywords[word] {
152+
result.WriteString(hlKeyword.Render(word))
153+
} else if mqlResources[word] && !isAfterDot {
154+
result.WriteString(hlResource.Render(word))
155+
} else if isAfterDot || isFieldAccess {
156+
result.WriteString(hlField.Render(word))
157+
} else {
158+
result.WriteString(word)
159+
}
160+
i = end
161+
continue
162+
}
163+
164+
// Default: pass through
165+
result.WriteByte(code[i])
166+
i++
167+
}
168+
169+
return result.String()
170+
}
171+
172+
// highlightMQLMultiline handles multiline MQL with prompts
173+
func highlightMQLMultiline(input string, prefix string, continuation string) string {
174+
lines := strings.Split(input, "\n")
175+
var result []string
176+
177+
for i, line := range lines {
178+
var prompt string
179+
if i == 0 {
180+
prompt = prefix
181+
} else {
182+
prompt = continuation
183+
}
184+
result = append(result, prompt+highlightMQL(line))
185+
}
186+
187+
return strings.Join(result, "\n")
188+
}
189+
190+
func isDigit(c byte) bool {
191+
return c >= '0' && c <= '9'
192+
}
193+
194+
func isAlpha(c byte) bool {
195+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
196+
}
197+
198+
func isAlphaNum(c byte) bool {
199+
return isAlpha(c) || isDigit(c)
200+
}
201+
202+
func isOperator(c byte) bool {
203+
return c == '=' || c == '!' || c == '<' || c == '>' ||
204+
c == '+' || c == '-' || c == '*' || c == '/' ||
205+
c == '&' || c == '|' || c == '~'
206+
}
207+
208+
func isBracket(c byte) bool {
209+
return c == '{' || c == '}' || c == '[' || c == ']' || c == '(' || c == ')'
210+
}
211+
212+
// Regex for more complex patterns (unused but available)
213+
var (
214+
_ = regexp.MustCompile(`"[^"]*"`)
215+
_ = regexp.MustCompile(`'[^']*'`)
216+
)

0 commit comments

Comments
 (0)