syntax.ts

  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}