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 },
202 emphasis: {
203 color: color.emphasis,
204 },
205 "emphasis.strong": {
206 color: color.emphasis,
207 weight: font_weights.bold,
208 },
209 title: {
210 color: color.primary,
211 weight: font_weights.bold,
212 },
213 link_uri: {
214 color: color_scheme.ramps.green(0.5).hex(),
215 underline: true,
216 },
217 link_text: {
218 color: color_scheme.ramps.orange(0.5).hex(),
219 italic: true,
220 },
221 "text.literal": {
222 color: color.string,
223 },
224 punctuation: {
225 color: color.punctuation,
226 },
227 "punctuation.bracket": {
228 color: color.punctuation,
229 },
230 "punctuation.delimiter": {
231 color: color.punctuation,
232 },
233 "punctuation.special": {
234 color: color_scheme.ramps.neutral(0.86).hex(),
235 },
236 "punctuation.list_marker": {
237 color: color.punctuation,
238 },
239 string: {
240 color: color.string,
241 },
242 "string.special": {
243 color: color.string,
244 },
245 "string.special.symbol": {
246 color: color.string,
247 },
248 "string.escape": {
249 color: color.comment,
250 },
251 "string.regex": {
252 color: color.string,
253 },
254 constructor: {
255 color: color_scheme.ramps.blue(0.5).hex(),
256 },
257 variant: {
258 color: color_scheme.ramps.blue(0.5).hex(),
259 },
260 type: {
261 color: color.type,
262 },
263 variable: {
264 color: color.primary,
265 },
266 label: {
267 color: color_scheme.ramps.blue(0.5).hex(),
268 },
269 tag: {
270 color: color_scheme.ramps.blue(0.5).hex(),
271 },
272 attribute: {
273 color: color_scheme.ramps.blue(0.5).hex(),
274 },
275 property: {
276 color: color_scheme.ramps.blue(0.5).hex(),
277 },
278 constant: {
279 color: color.constant,
280 },
281 keyword: {
282 color: color.keyword,
283 },
284 enum: {
285 color: color.enum,
286 },
287 operator: {
288 color: color.operator,
289 },
290 number: {
291 color: color.number,
292 },
293 boolean: {
294 color: color.boolean,
295 },
296 function: {
297 color: color.function,
298 },
299 preproc: {
300 color: color.primary,
301 },
302 embedded: {
303 color: color.primary,
304 },
305 }
306
307 return default_syntax
308}
309
310function merge_syntax(
311 default_syntax: Syntax,
312 color_scheme: ColorScheme
313): Syntax {
314 if (!color_scheme.syntax) {
315 return default_syntax
316 }
317
318 return deepmerge<Syntax, Partial<ThemeSyntax>>(
319 default_syntax,
320 color_scheme.syntax,
321 {
322 arrayMerge: (destinationArray, sourceArray) => [
323 ...destinationArray,
324 ...sourceArray,
325 ],
326 }
327 )
328}
329
330export function build_syntax(color_scheme: ColorScheme): Syntax {
331 const default_syntax: Syntax = build_default_syntax(color_scheme)
332
333 const syntax = merge_syntax(default_syntax, color_scheme)
334
335 return syntax
336}