Skip to content

Commit efbce6e

Browse files
committed
Make syntax coloring available to WASM
1 parent 07120de commit efbce6e

2 files changed

Lines changed: 249 additions & 242 deletions

File tree

pkg/repl/color.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package repl
2+
3+
import (
4+
"strings"
5+
6+
"github.com/glojurelang/glojure/pkg/lang"
7+
)
8+
9+
// ANSI color codes for syntax highlighting.
10+
const (
11+
colorReset = "\x1b[0m"
12+
colorGreen = "\x1b[32m"
13+
colorCyan = "\x1b[36m"
14+
colorMagenta = "\x1b[35m"
15+
colorBlue = "\x1b[38;5;69m"
16+
colorBoldYellow = "\x1b[1;33m"
17+
colorGray = "\x1b[90m"
18+
)
19+
20+
// Rainbow parentheses colors (Calva-style), cycling through depth levels.
21+
var rainbowColors = []string{
22+
"\x1b[38;2;204;204;204m", // light gray (#ccc)
23+
"\x1b[38;2;0;152;230m", // blue (#0098e6)
24+
"\x1b[38;2;225;109;109m", // salmon (#e16d6d)
25+
"\x1b[38;2;63;164;85m", // green (#3fa455)
26+
"\x1b[38;2;201;104;230m", // purple (#c968e6)
27+
"\x1b[38;2;153;153;153m", // gray (#999)
28+
"\x1b[38;2;206;126;0m", // orange (#ce7e00)
29+
}
30+
31+
// Style for mismatched/unmatched closing brackets: white on red background.
32+
const colorMismatch = "\x1b[97;41m"
33+
34+
// specialForms is the set of Clojure special forms and commonly
35+
// highlighted macros, used for bold-yellow highlighting.
36+
var specialForms = map[string]bool{
37+
"def": true, "defn": true, "defn-": true, "defmacro": true,
38+
"defonce": true, "defmethod": true, "defmulti": true,
39+
"defprotocol": true, "defrecord": true, "deftype": true,
40+
"defstruct": true, "fn": true, "fn*": true, "let": true,
41+
"let*": true, "loop": true, "recur": true, "if": true,
42+
"if-let": true, "if-not": true, "when": true, "when-let": true,
43+
"when-not": true, "when-first": true, "cond": true,
44+
"condp": true, "case": true, "do": true, "quote": true,
45+
"var": true, "try": true, "catch": true, "finally": true,
46+
"throw": true, "ns": true, "require": true, "import": true,
47+
"use": true, "refer": true, "in-ns": true, "for": true,
48+
"doseq": true, "dotimes": true, "while": true, "binding": true,
49+
"with-open": true, "with-local-vars": true,
50+
}
51+
52+
// isClojureCoreSym returns true if the symbol resolves to a var in a
53+
// clojure.* namespace within the given environment.
54+
func isClojureCoreSym(env lang.Environment, token string) (result bool) {
55+
if env == nil {
56+
return false
57+
}
58+
defer func() {
59+
if recover() != nil {
60+
result = false
61+
}
62+
}()
63+
sym := lang.NewSymbol(token)
64+
if symNS := sym.Namespace(); symNS != "" {
65+
// Qualified symbol: look up the namespace directly.
66+
ns := lang.FindNamespace(lang.NewSymbol(symNS))
67+
if ns == nil {
68+
// Try as alias in current namespace.
69+
ns = env.CurrentNamespace().LookupAlias(lang.NewSymbol(symNS))
70+
}
71+
if ns == nil {
72+
return false
73+
}
74+
nsName := ns.Name().Name()
75+
return strings.HasPrefix(nsName, "clojure.")
76+
}
77+
// Unqualified symbol: check current namespace mappings.
78+
ns := env.CurrentNamespace()
79+
v, ok := ns.Mappings().ValAt(sym).(*lang.Var)
80+
if !ok {
81+
return false
82+
}
83+
nsName := v.Namespace().Name().Name()
84+
return strings.HasPrefix(nsName, "clojure.")
85+
}
86+
87+
// ColorSyntax returns an ANSI-colored version of the input
88+
// for Clojure syntax highlighting.
89+
func ColorSyntax(line []rune, env lang.Environment) string {
90+
var buf strings.Builder
91+
buf.Grow(len(line) * 2)
92+
i := 0
93+
n := len(line)
94+
var bracketStack []rune // tracks open bracket types for matching
95+
96+
for i < n {
97+
ch := line[i]
98+
99+
// String literal
100+
if ch == '"' {
101+
buf.WriteString(colorGreen)
102+
buf.WriteRune(ch)
103+
i++
104+
for i < n {
105+
c := line[i]
106+
buf.WriteRune(c)
107+
if c == '\\' && i+1 < n {
108+
i++
109+
buf.WriteRune(line[i])
110+
} else if c == '"' {
111+
break
112+
}
113+
i++
114+
}
115+
buf.WriteString(colorReset)
116+
i++
117+
continue
118+
}
119+
120+
// Comment
121+
if ch == ';' {
122+
buf.WriteString(colorGray)
123+
for i < n && line[i] != '\n' {
124+
buf.WriteRune(line[i])
125+
i++
126+
}
127+
buf.WriteString(colorReset)
128+
continue
129+
}
130+
131+
// Keyword
132+
if ch == ':' && (i == 0 || !isSymbolChar(line[i-1])) {
133+
start := i
134+
i++ // skip ':'
135+
if i < n && line[i] == ':' {
136+
i++ // skip second ':' for ::keyword
137+
}
138+
for i < n && isSymbolChar(line[i]) {
139+
i++
140+
}
141+
buf.WriteString(colorCyan)
142+
buf.WriteString(string(line[start:i]))
143+
buf.WriteString(colorReset)
144+
continue
145+
}
146+
147+
// Opening brackets: rainbow color at current depth, then push
148+
if ch == '(' || ch == '[' || ch == '{' {
149+
depth := len(bracketStack)
150+
buf.WriteString(rainbowColors[depth%len(rainbowColors)])
151+
buf.WriteRune(ch)
152+
buf.WriteString(colorReset)
153+
bracketStack = append(bracketStack, ch)
154+
i++
155+
continue
156+
}
157+
158+
// Closing brackets: check type match, pop, and color
159+
if ch == ')' || ch == ']' || ch == '}' {
160+
depth := len(bracketStack)
161+
if depth == 0 {
162+
// Unmatched closer
163+
buf.WriteString(colorMismatch)
164+
} else {
165+
open := bracketStack[depth-1]
166+
matched := (open == '(' && ch == ')') ||
167+
(open == '[' && ch == ']') ||
168+
(open == '{' && ch == '}')
169+
if matched {
170+
bracketStack = bracketStack[:depth-1]
171+
buf.WriteString(rainbowColors[(depth-1)%len(rainbowColors)])
172+
} else {
173+
// Type mismatch
174+
buf.WriteString(colorMismatch)
175+
}
176+
}
177+
buf.WriteRune(ch)
178+
buf.WriteString(colorReset)
179+
i++
180+
continue
181+
}
182+
183+
// Dispatch: #, characters, deref @, quote ', etc. -- pass through
184+
if ch == '\'' || ch == '`' ||
185+
ch == '@' || ch == '^' || ch == '~' || ch == '#' {
186+
buf.WriteRune(ch)
187+
i++
188+
continue
189+
}
190+
191+
// Whitespace
192+
if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == ',' {
193+
buf.WriteRune(ch)
194+
i++
195+
continue
196+
}
197+
198+
// Symbol or number token
199+
start := i
200+
for i < n && isSymbolChar(line[i]) {
201+
i++
202+
}
203+
if i == start {
204+
// Single non-symbol character, pass through
205+
buf.WriteRune(ch)
206+
i++
207+
continue
208+
}
209+
210+
token := string(line[start:i])
211+
212+
// Booleans and nil
213+
if token == "true" || token == "false" || token == "nil" {
214+
buf.WriteString(colorMagenta)
215+
buf.WriteString(token)
216+
buf.WriteString(colorReset)
217+
continue
218+
}
219+
220+
// Special forms
221+
if specialForms[token] {
222+
buf.WriteString(colorBoldYellow)
223+
buf.WriteString(token)
224+
buf.WriteString(colorReset)
225+
continue
226+
}
227+
228+
// Number: starts with digit, or starts with - followed by digit
229+
if isNumber(token) {
230+
buf.WriteString(colorMagenta)
231+
buf.WriteString(token)
232+
buf.WriteString(colorReset)
233+
continue
234+
}
235+
236+
// clojure.* core symbol
237+
if isClojureCoreSym(env, token) {
238+
buf.WriteString(colorBlue)
239+
buf.WriteString(token)
240+
buf.WriteString(colorReset)
241+
continue
242+
}
243+
244+
// Regular symbol -- no color
245+
buf.WriteString(token)
246+
}
247+
248+
return buf.String()
249+
}

0 commit comments

Comments
 (0)