1import deepmerge from "deepmerge"
2import { FontWeight, fontWeights } from "../common"
3import { ColorScheme } from "./colorScheme"
4import chroma from "chroma-js"
5
6export interface SyntaxHighlightStyle {
7 color?: string
8 weight?: FontWeight
9 underline?: boolean
10 italic?: boolean
11}
12
13export interface Syntax {
14 // == Text Styles ====== /
15 comment: SyntaxHighlightStyle
16 // elixir: doc comment
17 "comment.doc": SyntaxHighlightStyle
18 primary: SyntaxHighlightStyle
19 predictive: SyntaxHighlightStyle
20
21 // === Formatted Text ====== /
22 emphasis: SyntaxHighlightStyle
23 "emphasis.strong": SyntaxHighlightStyle
24 title: SyntaxHighlightStyle
25 linkUri: SyntaxHighlightStyle
26 linkText: SyntaxHighlightStyle
27 /** md: indented_code_block, fenced_code_block, code_span */
28 "text.literal": SyntaxHighlightStyle
29
30 // == Punctuation ====== /
31 punctuation: SyntaxHighlightStyle
32 /** Example: `(`, `[`, `{`...*/
33 "punctuation.bracket": SyntaxHighlightStyle
34 /**., ;*/
35 "punctuation.delimiter": SyntaxHighlightStyle
36 // js, ts: ${, } in a template literal
37 // yaml: *, &, ---, ...
38 "punctuation.special": SyntaxHighlightStyle
39 // md: list_marker_plus, list_marker_dot, etc
40 "punctuation.list_marker": SyntaxHighlightStyle
41
42 // == Strings ====== /
43
44 string: SyntaxHighlightStyle
45 // css: color_value
46 // js: this, super
47 // toml: offset_date_time, local_date_time...
48 "string.special": SyntaxHighlightStyle
49 // elixir: atom, quoted_atom, keyword, quoted_keyword
50 // ruby: simple_symbol, delimited_symbol...
51 "string.special.symbol"?: SyntaxHighlightStyle
52 // elixir, python, yaml...: escape_sequence
53 "string.escape"?: SyntaxHighlightStyle
54 // Regular expressions
55 "string.regex"?: SyntaxHighlightStyle
56
57 // == Types ====== /
58 // We allow Function here because all JS objects literals have this property
59 constructor: SyntaxHighlightStyle | Function
60 variant: SyntaxHighlightStyle
61 type: SyntaxHighlightStyle
62 // js: predefined_type
63 "type.builtin"?: SyntaxHighlightStyle
64
65 // == Values
66 variable: SyntaxHighlightStyle
67 // this, ...
68 // css: -- (var(--foo))
69 // lua: self
70 "variable.special"?: SyntaxHighlightStyle
71 // c: statement_identifier,
72 label: SyntaxHighlightStyle
73 // css: tag_name, nesting_selector, universal_selector...
74 tag: SyntaxHighlightStyle
75 // css: attribute, pseudo_element_selector (tag_name),
76 attribute: SyntaxHighlightStyle
77 // css: class_name, property_name, namespace_name...
78 property: SyntaxHighlightStyle
79 // true, false, null, nullptr
80 constant: SyntaxHighlightStyle
81 // css: @media, @import, @supports...
82 // js: declare, implements, interface, keyof, public...
83 keyword: SyntaxHighlightStyle
84 // note: js enum is currently defined as a keyword
85 enum: SyntaxHighlightStyle
86 // -, --, ->, !=, &&, ||, <=...
87 operator: SyntaxHighlightStyle
88 number: SyntaxHighlightStyle
89 boolean: SyntaxHighlightStyle
90 // elixir: __MODULE__, __DIR__, __ENV__, etc
91 // go: nil, iota
92 "constant.builtin"?: SyntaxHighlightStyle
93
94 // == Functions ====== /
95
96 function: SyntaxHighlightStyle
97 // lua: assert, error, loadfile, tostring, unpack...
98 "function.builtin"?: SyntaxHighlightStyle
99 // go: call_expression, method_declaration
100 // js: call_expression, method_definition, pair (key, arrow function)
101 // rust: function_item name: (identifier)
102 "function.definition"?: SyntaxHighlightStyle
103 // rust: macro_definition name: (identifier)
104 "function.special.definition"?: SyntaxHighlightStyle
105 "function.method"?: SyntaxHighlightStyle
106 // ruby: identifier/"defined?" // Nate note: I don't fully understand this one.
107 "function.method.builtin"?: SyntaxHighlightStyle
108
109 // == Unsorted ====== /
110 // lua: hash_bang_line
111 preproc: SyntaxHighlightStyle
112 // elixir, python: interpolation (ex: foo in ${foo})
113 // js: template_substitution
114 embedded: SyntaxHighlightStyle
115}
116
117export type ThemeSyntax = Partial<Syntax>
118
119const defaultSyntaxHighlightStyle: Omit<SyntaxHighlightStyle, "color"> = {
120 weight: "normal",
121 underline: false,
122 italic: false,
123}
124
125function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
126 // Make a temporary object that is allowed to be missing
127 // the "color" property for each style
128 const syntax: {
129 [key: string]: Omit<SyntaxHighlightStyle, "color">
130 } = {}
131
132 const light = colorScheme.isLight
133
134 // then spread the default to each style
135 for (const key of Object.keys({} as Syntax)) {
136 syntax[key as keyof Syntax] = {
137 ...defaultSyntaxHighlightStyle,
138 }
139 }
140
141 // Mix the neutral and blue colors to get a
142 // predictive color distinct from any other color in the theme
143 const predictive = chroma
144 .mix(
145 colorScheme.ramps.neutral(0.4).hex(),
146 colorScheme.ramps.blue(0.4).hex(),
147 0.45,
148 "lch"
149 )
150 .hex()
151
152 const color = {
153 primary: colorScheme.ramps.neutral(1).hex(),
154 comment: colorScheme.ramps.neutral(0.71).hex(),
155 punctuation: colorScheme.ramps.neutral(0.86).hex(),
156 predictive: predictive,
157 emphasis: colorScheme.ramps.blue(0.5).hex(),
158 string: colorScheme.ramps.orange(0.5).hex(),
159 function: colorScheme.ramps.yellow(0.5).hex(),
160 type: colorScheme.ramps.cyan(0.5).hex(),
161 constructor: colorScheme.ramps.blue(0.5).hex(),
162 variant: colorScheme.ramps.blue(0.5).hex(),
163 property: colorScheme.ramps.blue(0.5).hex(),
164 enum: colorScheme.ramps.orange(0.5).hex(),
165 operator: colorScheme.ramps.orange(0.5).hex(),
166 number: colorScheme.ramps.green(0.5).hex(),
167 boolean: colorScheme.ramps.green(0.5).hex(),
168 constant: colorScheme.ramps.green(0.5).hex(),
169 keyword: colorScheme.ramps.blue(0.5).hex(),
170 }
171
172 // Then assign colors and use Syntax to enforce each style getting it's own color
173 const defaultSyntax: Syntax = {
174 ...syntax,
175 comment: {
176 color: color.comment,
177 },
178 "comment.doc": {
179 color: color.comment,
180 },
181 primary: {
182 color: color.primary,
183 },
184 predictive: {
185 color: color.predictive,
186 italic: true,
187 },
188 emphasis: {
189 color: color.emphasis,
190 },
191 "emphasis.strong": {
192 color: color.emphasis,
193 weight: fontWeights.bold,
194 },
195 title: {
196 color: color.primary,
197 weight: fontWeights.bold,
198 },
199 linkUri: {
200 color: colorScheme.ramps.green(0.5).hex(),
201 underline: true,
202 },
203 linkText: {
204 color: colorScheme.ramps.orange(0.5).hex(),
205 italic: true,
206 },
207 "text.literal": {
208 color: color.string,
209 },
210 punctuation: {
211 color: color.punctuation,
212 },
213 "punctuation.bracket": {
214 color: color.punctuation,
215 },
216 "punctuation.delimiter": {
217 color: color.punctuation,
218 },
219 "punctuation.special": {
220 color: colorScheme.ramps.neutral(0.86).hex(),
221 },
222 "punctuation.list_marker": {
223 color: color.punctuation,
224 },
225 string: {
226 color: color.string,
227 },
228 "string.special": {
229 color: color.string,
230 },
231 "string.special.symbol": {
232 color: color.string,
233 },
234 "string.escape": {
235 color: color.comment,
236 },
237 "string.regex": {
238 color: color.string,
239 },
240 constructor: {
241 color: colorScheme.ramps.blue(0.5).hex(),
242 },
243 variant: {
244 color: colorScheme.ramps.blue(0.5).hex(),
245 },
246 type: {
247 color: color.type,
248 },
249 variable: {
250 color: color.primary,
251 },
252 label: {
253 color: colorScheme.ramps.blue(0.5).hex(),
254 },
255 tag: {
256 color: colorScheme.ramps.blue(0.5).hex(),
257 },
258 attribute: {
259 color: colorScheme.ramps.blue(0.5).hex(),
260 },
261 property: {
262 color: colorScheme.ramps.blue(0.5).hex(),
263 },
264 constant: {
265 color: color.constant,
266 },
267 keyword: {
268 color: color.keyword,
269 },
270 enum: {
271 color: color.enum,
272 },
273 operator: {
274 color: color.operator,
275 },
276 number: {
277 color: color.number,
278 },
279 boolean: {
280 color: color.boolean,
281 },
282 function: {
283 color: color.function,
284 },
285 preproc: {
286 color: color.primary,
287 },
288 embedded: {
289 color: color.primary,
290 },
291 }
292
293 return defaultSyntax
294}
295
296function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
297 if (!colorScheme.syntax) {
298 return defaultSyntax
299 }
300
301 return deepmerge<Syntax, Partial<ThemeSyntax>>(
302 defaultSyntax,
303 colorScheme.syntax,
304 {
305 arrayMerge: (destinationArray, sourceArray) => [
306 ...destinationArray,
307 ...sourceArray,
308 ],
309 }
310 )
311}
312
313export function buildSyntax(colorScheme: ColorScheme): Syntax {
314 const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
315
316 const syntax = mergeSyntax(defaultSyntax, colorScheme)
317
318 return syntax
319}