syntax.ts

  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}