syntax.ts

  1import deepmerge from "deepmerge"
  2import { FontWeight, fontWeights } from "../../common"
  3import { ColorScheme } from "./colorScheme"
  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
 21    // === Formatted Text ====== /
 22    emphasis: SyntaxHighlightStyle
 23    "emphasis.strong": SyntaxHighlightStyle
 24    title: SyntaxHighlightStyle
 25    linkUri: SyntaxHighlightStyle
 26    linkText: 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
 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 defaultSyntaxHighlightStyle: Omit<SyntaxHighlightStyle, "color"> = {
120    weight: fontWeights.normal,
121    underline: false,
122    italic: false,
123}
124
125function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
126    // Make a temporary object that is allowed to be missing
127    // the "color" property for each style
128    const syntax: {
129        [key: string]: Omit<SyntaxHighlightStyle, "color">
130    } = {}
131
132    const light = colorScheme.isLight
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            ...defaultSyntaxHighlightStyle,
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.mix(
144        colorScheme.ramps.neutral(0.4).hex(),
145        colorScheme.ramps.blue(0.4).hex(),
146        0.45,
147        "lch"
148    ).hex()
149
150    const color = {
151        primary: colorScheme.ramps.neutral(1).hex(),
152        comment: colorScheme.ramps.neutral(0.71).hex(),
153        punctuation: colorScheme.ramps.neutral(0.86).hex(),
154        predictive: predictive,
155        emphasis: colorScheme.ramps.blue(0.5).hex(),
156        string: colorScheme.ramps.orange(0.5).hex(),
157        function: colorScheme.ramps.yellow(0.5).hex(),
158        type: colorScheme.ramps.cyan(0.5).hex(),
159        constructor: colorScheme.ramps.blue(0.5).hex(),
160        variant: colorScheme.ramps.blue(0.5).hex(),
161        property: colorScheme.ramps.blue(0.5).hex(),
162        enum: colorScheme.ramps.orange(0.5).hex(),
163        operator: colorScheme.ramps.orange(0.5).hex(),
164        number: colorScheme.ramps.green(0.5).hex(),
165        boolean: colorScheme.ramps.green(0.5).hex(),
166        constant: colorScheme.ramps.green(0.5).hex(),
167        keyword: colorScheme.ramps.blue(0.5).hex(),
168    }
169
170    // Then assign colors and use Syntax to enforce each style getting it's own color
171    const defaultSyntax: Syntax = {
172        ...syntax,
173        comment: {
174            color: color.comment,
175        },
176        "comment.doc": {
177            color: color.comment,
178        },
179        primary: {
180            color: color.primary,
181        },
182        predictive: {
183            color: color.predictive,
184            italic: true,
185        },
186        emphasis: {
187            color: color.emphasis,
188        },
189        "emphasis.strong": {
190            color: color.emphasis,
191            weight: fontWeights.bold,
192        },
193        title: {
194            color: color.primary,
195            weight: fontWeights.bold,
196        },
197        linkUri: {
198            color: colorScheme.ramps.green(0.5).hex(),
199            underline: true,
200        },
201        linkText: {
202            color: colorScheme.ramps.orange(0.5).hex(),
203            italic: true,
204        },
205        "text.literal": {
206            color: color.string,
207        },
208        punctuation: {
209            color: color.punctuation,
210        },
211        "punctuation.bracket": {
212            color: color.punctuation,
213        },
214        "punctuation.delimiter": {
215            color: color.punctuation,
216        },
217        "punctuation.special": {
218            color: colorScheme.ramps.neutral(0.86).hex(),
219        },
220        "punctuation.list_marker": {
221            color: color.punctuation,
222        },
223        string: {
224            color: color.string,
225        },
226        "string.special": {
227            color: color.string,
228        },
229        "string.special.symbol": {
230            color: color.string,
231        },
232        "string.escape": {
233            color: color.comment,
234        },
235        "string.regex": {
236            color: color.string,
237        },
238        constructor: {
239            color: colorScheme.ramps.blue(0.5).hex(),
240        },
241        variant: {
242            color: colorScheme.ramps.blue(0.5).hex(),
243        },
244        type: {
245            color: color.type,
246        },
247        variable: {
248            color: color.primary,
249        },
250        label: {
251            color: colorScheme.ramps.blue(0.5).hex(),
252        },
253        tag: {
254            color: colorScheme.ramps.blue(0.5).hex(),
255        },
256        attribute: {
257            color: colorScheme.ramps.blue(0.5).hex(),
258        },
259        property: {
260            color: colorScheme.ramps.blue(0.5).hex(),
261        },
262        constant: {
263            color: color.constant,
264        },
265        keyword: {
266            color: color.keyword,
267        },
268        enum: {
269            color: color.enum,
270        },
271        operator: {
272            color: color.operator,
273        },
274        number: {
275            color: color.number,
276        },
277        boolean: {
278            color: color.boolean,
279        },
280        function: {
281            color: color.function,
282        },
283        preproc: {
284            color: color.primary,
285        },
286        embedded: {
287            color: color.primary,
288        },
289    }
290
291    return defaultSyntax
292}
293
294function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
295    if (!colorScheme.syntax) {
296        return defaultSyntax
297    }
298
299    return deepmerge<Syntax, Partial<ThemeSyntax>>(
300        defaultSyntax,
301        colorScheme.syntax,
302        {
303            arrayMerge: (destinationArray, sourceArray) => [
304                ...destinationArray,
305                ...sourceArray,
306            ],
307        }
308    )
309}
310
311export function buildSyntax(colorScheme: ColorScheme): Syntax {
312    const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
313
314    const syntax = mergeSyntax(defaultSyntax, colorScheme)
315
316    return syntax
317}