1import deepmerge from "deepmerge"
2import { FontWeight, font_weights, useTheme } from "../common"
3import chroma from "chroma-js"
4
5export interface SyntaxHighlightStyle {
6 color?: string
7 weight?: FontWeight
8 underline?: boolean
9 italic?: boolean
10}
11
12export interface Syntax {
13 // == Text Styles ====== /
14 comment: SyntaxHighlightStyle
15 // elixir: doc comment
16 "comment.doc": SyntaxHighlightStyle
17 primary: SyntaxHighlightStyle
18 predictive: SyntaxHighlightStyle
19 hint: SyntaxHighlightStyle
20
21 // === Formatted Text ====== /
22 emphasis: SyntaxHighlightStyle
23 "emphasis.strong": SyntaxHighlightStyle
24 title: SyntaxHighlightStyle
25 link_uri: SyntaxHighlightStyle
26 link_text: 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 // eslint-disable-line @typescript-eslint/ban-types
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 default_syntax_highlight_style: Omit<SyntaxHighlightStyle, "color"> = {
120 weight: "normal",
121 underline: false,
122 italic: false,
123}
124
125function build_default_syntax(): Syntax {
126 const theme = useTheme()
127
128 // Make a temporary object that is allowed to be missing
129 // the "color" property for each style
130 const syntax: {
131 [key: string]: Omit<SyntaxHighlightStyle, "color">
132 } = {}
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 ...default_syntax_highlight_style,
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 theme.ramps.neutral(0.4).hex(),
146 theme.ramps.blue(0.4).hex(),
147 0.45,
148 "lch"
149 )
150 .hex()
151 // Mix the neutral and green colors to get a
152 // hint color distinct from any other color in the theme
153 const hint = chroma
154 .mix(
155 theme.ramps.neutral(0.6).hex(),
156 theme.ramps.blue(0.4).hex(),
157 0.45,
158 "lch"
159 )
160 .hex()
161
162 const color = {
163 primary: theme.ramps.neutral(1).hex(),
164 comment: theme.ramps.neutral(0.71).hex(),
165 punctuation: theme.ramps.neutral(0.86).hex(),
166 predictive: predictive,
167 hint: hint,
168 emphasis: theme.ramps.blue(0.5).hex(),
169 string: theme.ramps.orange(0.5).hex(),
170 function: theme.ramps.yellow(0.5).hex(),
171 type: theme.ramps.cyan(0.5).hex(),
172 constructor: theme.ramps.blue(0.5).hex(),
173 variant: theme.ramps.blue(0.5).hex(),
174 property: theme.ramps.blue(0.5).hex(),
175 enum: theme.ramps.orange(0.5).hex(),
176 operator: theme.ramps.orange(0.5).hex(),
177 number: theme.ramps.green(0.5).hex(),
178 boolean: theme.ramps.green(0.5).hex(),
179 constant: theme.ramps.green(0.5).hex(),
180 keyword: theme.ramps.blue(0.5).hex(),
181 }
182
183 // Then assign colors and use Syntax to enforce each style getting it's own color
184 const default_syntax: Syntax = {
185 ...syntax,
186 comment: {
187 color: color.comment,
188 },
189 "comment.doc": {
190 color: color.comment,
191 },
192 primary: {
193 color: color.primary,
194 },
195 predictive: {
196 color: color.predictive,
197 italic: true,
198 },
199 hint: {
200 color: color.hint,
201 weight: font_weights.bold,
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: theme.ramps.green(0.5).hex(),
216 underline: true,
217 },
218 link_text: {
219 color: theme.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: theme.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: theme.ramps.blue(0.5).hex(),
257 },
258 variant: {
259 color: theme.ramps.blue(0.5).hex(),
260 },
261 type: {
262 color: color.type,
263 },
264 variable: {
265 color: color.primary,
266 },
267 label: {
268 color: theme.ramps.blue(0.5).hex(),
269 },
270 tag: {
271 color: theme.ramps.blue(0.5).hex(),
272 },
273 attribute: {
274 color: theme.ramps.blue(0.5).hex(),
275 },
276 property: {
277 color: theme.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
311export function build_syntax(): Syntax {
312 const theme = useTheme()
313
314 const default_syntax: Syntax = build_default_syntax()
315
316 if (!theme.syntax) {
317 return default_syntax
318 }
319
320 const syntax = deepmerge<Syntax, Partial<ThemeSyntax>>(
321 default_syntax,
322 theme.syntax,
323 {
324 arrayMerge: (destinationArray, sourceArray) => [
325 ...destinationArray,
326 ...sourceArray,
327 ],
328 }
329 )
330
331 return syntax
332}