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 // then spread the default to each style
133 for (const key of Object.keys({} as Syntax)) {
134 syntax[key as keyof Syntax] = {
135 ...defaultSyntaxHighlightStyle,
136 }
137 }
138
139 // Mix the neutral and blue colors to get a
140 // predictive color distinct from any other color in the theme
141 const predictive = chroma
142 .mix(
143 colorScheme.ramps.neutral(0.4).hex(),
144 colorScheme.ramps.blue(0.4).hex(),
145 0.45,
146 "lch"
147 )
148 .hex()
149
150 const color = {
151 primary: colorScheme.ramps.neutral(1).hex(),
152 comment: colorScheme.ramps.neutral(0.71).hex(),
153 punctuation: colorScheme.ramps.neutral(0.86).hex(),
154 predictive: predictive,
155 emphasis: colorScheme.ramps.blue(0.5).hex(),
156 string: colorScheme.ramps.orange(0.5).hex(),
157 function: colorScheme.ramps.yellow(0.5).hex(),
158 type: colorScheme.ramps.cyan(0.5).hex(),
159 constructor: colorScheme.ramps.blue(0.5).hex(),
160 variant: colorScheme.ramps.blue(0.5).hex(),
161 property: colorScheme.ramps.blue(0.5).hex(),
162 enum: colorScheme.ramps.orange(0.5).hex(),
163 operator: colorScheme.ramps.orange(0.5).hex(),
164 number: colorScheme.ramps.green(0.5).hex(),
165 boolean: colorScheme.ramps.green(0.5).hex(),
166 constant: colorScheme.ramps.green(0.5).hex(),
167 keyword: colorScheme.ramps.blue(0.5).hex(),
168 }
169
170 // Then assign colors and use Syntax to enforce each style getting it's own color
171 const defaultSyntax: Syntax = {
172 ...syntax,
173 comment: {
174 color: color.comment,
175 },
176 "comment.doc": {
177 color: color.comment,
178 },
179 primary: {
180 color: color.primary,
181 },
182 predictive: {
183 color: color.predictive,
184 italic: true,
185 },
186 emphasis: {
187 color: color.emphasis,
188 },
189 "emphasis.strong": {
190 color: color.emphasis,
191 weight: fontWeights.bold,
192 },
193 title: {
194 color: color.primary,
195 weight: fontWeights.bold,
196 },
197 linkUri: {
198 color: colorScheme.ramps.green(0.5).hex(),
199 underline: true,
200 },
201 linkText: {
202 color: colorScheme.ramps.orange(0.5).hex(),
203 italic: true,
204 },
205 "text.literal": {
206 color: color.string,
207 },
208 punctuation: {
209 color: color.punctuation,
210 },
211 "punctuation.bracket": {
212 color: color.punctuation,
213 },
214 "punctuation.delimiter": {
215 color: color.punctuation,
216 },
217 "punctuation.special": {
218 color: colorScheme.ramps.neutral(0.86).hex(),
219 },
220 "punctuation.list_marker": {
221 color: color.punctuation,
222 },
223 string: {
224 color: color.string,
225 },
226 "string.special": {
227 color: color.string,
228 },
229 "string.special.symbol": {
230 color: color.string,
231 },
232 "string.escape": {
233 color: color.comment,
234 },
235 "string.regex": {
236 color: color.string,
237 },
238 constructor: {
239 color: colorScheme.ramps.blue(0.5).hex(),
240 },
241 variant: {
242 color: colorScheme.ramps.blue(0.5).hex(),
243 },
244 type: {
245 color: color.type,
246 },
247 variable: {
248 color: color.primary,
249 },
250 label: {
251 color: colorScheme.ramps.blue(0.5).hex(),
252 },
253 tag: {
254 color: colorScheme.ramps.blue(0.5).hex(),
255 },
256 attribute: {
257 color: colorScheme.ramps.blue(0.5).hex(),
258 },
259 property: {
260 color: colorScheme.ramps.blue(0.5).hex(),
261 },
262 constant: {
263 color: color.constant,
264 },
265 keyword: {
266 color: color.keyword,
267 },
268 enum: {
269 color: color.enum,
270 },
271 operator: {
272 color: color.operator,
273 },
274 number: {
275 color: color.number,
276 },
277 boolean: {
278 color: color.boolean,
279 },
280 function: {
281 color: color.function,
282 },
283 preproc: {
284 color: color.primary,
285 },
286 embedded: {
287 color: color.primary,
288 },
289 }
290
291 return defaultSyntax
292}
293
294function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
295 if (!colorScheme.syntax) {
296 return defaultSyntax
297 }
298
299 return deepmerge<Syntax, Partial<ThemeSyntax>>(
300 defaultSyntax,
301 colorScheme.syntax,
302 {
303 arrayMerge: (destinationArray, sourceArray) => [
304 ...destinationArray,
305 ...sourceArray,
306 ],
307 }
308 )
309}
310
311export function buildSyntax(colorScheme: ColorScheme): Syntax {
312 const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
313
314 const syntax = mergeSyntax(defaultSyntax, colorScheme)
315
316 return syntax
317}