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