syntax.ts

  1import deepmerge from "deepmerge"
  2import { FontWeight, fontWeights } from "../../common"
  3import { ColorScheme } from "./colorScheme"
  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
 20    // === Formatted Text ====== /
 21    emphasis: SyntaxHighlightStyle
 22    "emphasis.strong": SyntaxHighlightStyle
 23    title: SyntaxHighlightStyle
 24    linkUri: SyntaxHighlightStyle
 25    linkText: SyntaxHighlightStyle
 26    /** md: indented_code_block, fenced_code_block, code_span */
 27    "text.literal": SyntaxHighlightStyle
 28
 29    // == Punctuation ====== /
 30    punctuation: SyntaxHighlightStyle
 31    /** Example: `(`, `[`, `{`...*/
 32    "punctuation.bracket": SyntaxHighlightStyle
 33    /**., ;*/
 34    "punctuation.delimiter": SyntaxHighlightStyle
 35    // js, ts: ${, } in a template literal
 36    // yaml: *, &, ---, ...
 37    "punctuation.special": SyntaxHighlightStyle
 38    // md: list_marker_plus, list_marker_dot, etc
 39    "punctuation.list_marker": SyntaxHighlightStyle
 40
 41    // == Strings ====== /
 42
 43    string: SyntaxHighlightStyle
 44    // css: color_value
 45    // js: this, super
 46    // toml: offset_date_time, local_date_time...
 47    "string.special": SyntaxHighlightStyle
 48    // elixir: atom, quoted_atom, keyword, quoted_keyword
 49    // ruby: simple_symbol, delimited_symbol...
 50    "string.special.symbol"?: SyntaxHighlightStyle
 51    // elixir, python, yaml...: escape_sequence
 52    "string.escape"?: SyntaxHighlightStyle
 53    // Regular expressions
 54    "string.regex"?: SyntaxHighlightStyle
 55
 56    // == Types ====== /
 57    // We allow Function here because all JS objects literals have this property
 58    constructor: SyntaxHighlightStyle | Function
 59    variant: SyntaxHighlightStyle
 60    type: SyntaxHighlightStyle
 61    // js: predefined_type
 62    "type.builtin"?: SyntaxHighlightStyle
 63
 64    // == Values
 65    variable: SyntaxHighlightStyle
 66    // this, ...
 67    // css: -- (var(--foo))
 68    // lua: self
 69    "variable.special"?: SyntaxHighlightStyle
 70    // c: statement_identifier,
 71    label: SyntaxHighlightStyle
 72    // css: tag_name, nesting_selector, universal_selector...
 73    tag: SyntaxHighlightStyle
 74    // css: attribute, pseudo_element_selector (tag_name),
 75    attribute: SyntaxHighlightStyle
 76    // css: class_name, property_name, namespace_name...
 77    property: SyntaxHighlightStyle
 78    // true, false, null, nullptr
 79    constant: SyntaxHighlightStyle
 80    // css: @media, @import, @supports...
 81    // js: declare, implements, interface, keyof, public...
 82    keyword: SyntaxHighlightStyle
 83    // note: js enum is currently defined as a keyword
 84    enum: SyntaxHighlightStyle
 85    // -, --, ->, !=, &&, ||, <=...
 86    operator: SyntaxHighlightStyle
 87    number: SyntaxHighlightStyle
 88    boolean: SyntaxHighlightStyle
 89    // elixir: __MODULE__, __DIR__, __ENV__, etc
 90    // go: nil, iota
 91    "constant.builtin"?: SyntaxHighlightStyle
 92
 93    // == Functions ====== /
 94
 95    function: SyntaxHighlightStyle
 96    // lua: assert, error, loadfile, tostring, unpack...
 97    "function.builtin"?: SyntaxHighlightStyle
 98    // go: call_expression, method_declaration
 99    // js: call_expression, method_definition, pair (key, arrow function)
100    // rust: function_item name: (identifier)
101    "function.definition"?: SyntaxHighlightStyle
102    // rust: macro_definition name: (identifier)
103    "function.special.definition"?: SyntaxHighlightStyle
104    "function.method"?: SyntaxHighlightStyle
105    // ruby: identifier/"defined?" // Nate note: I don't fully understand this one.
106    "function.method.builtin"?: SyntaxHighlightStyle
107
108    // == Unsorted ====== /
109    // lua: hash_bang_line
110    preproc: SyntaxHighlightStyle
111    // elixir, python: interpolation (ex: foo in ${foo})
112    // js: template_substitution
113    embedded: SyntaxHighlightStyle
114}
115
116export type ThemeSyntax = Partial<Syntax>
117
118const defaultSyntaxHighlightStyle: Omit<SyntaxHighlightStyle, "color"> = {
119    weight: fontWeights.normal,
120    underline: false,
121    italic: false,
122}
123
124function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
125    // Make a temporary object that is allowed to be missing
126    // the "color" property for each style
127    const syntax: {
128        [key: string]: Omit<SyntaxHighlightStyle, "color">
129    } = {}
130
131    // then spread the default to each style
132    for (const key of Object.keys({} as Syntax)) {
133        syntax[key as keyof Syntax] = {
134            ...defaultSyntaxHighlightStyle,
135        }
136    }
137
138    const color = {
139        primary: colorScheme.ramps.neutral(1).hex(),
140        comment: colorScheme.ramps.neutral(0.71).hex(),
141        punctuation: colorScheme.ramps.neutral(0.86).hex(),
142        predictive: colorScheme.ramps.neutral(0.57).hex(),
143        emphasis: colorScheme.ramps.blue(0.5).hex(),
144        string: colorScheme.ramps.orange(0.5).hex(),
145        function: colorScheme.ramps.yellow(0.5).hex(),
146        type: colorScheme.ramps.cyan(0.5).hex(),
147        constructor: colorScheme.ramps.blue(0.5).hex(),
148        variant: colorScheme.ramps.blue(0.5).hex(),
149        property: colorScheme.ramps.blue(0.5).hex(),
150        enum: colorScheme.ramps.orange(0.5).hex(),
151        operator: colorScheme.ramps.orange(0.5).hex(),
152        number: colorScheme.ramps.green(0.5).hex(),
153        boolean: colorScheme.ramps.green(0.5).hex(),
154        constant: colorScheme.ramps.green(0.5).hex(),
155        keyword: colorScheme.ramps.blue(0.5).hex(),
156    }
157
158    // Then assign colors and use Syntax to enforce each style getting it's own color
159    const defaultSyntax: Syntax = {
160        ...syntax,
161        comment: {
162            color: color.comment,
163        },
164        "comment.doc": {
165            color: color.comment,
166        },
167        primary: {
168            color: color.primary,
169        },
170        predictive: {
171            color: color.predictive,
172        },
173        emphasis: {
174            color: color.emphasis,
175        },
176        "emphasis.strong": {
177            color: color.emphasis,
178            weight: fontWeights.bold,
179        },
180        title: {
181            color: color.primary,
182            weight: fontWeights.bold,
183        },
184        linkUri: {
185            color: colorScheme.ramps.green(0.5).hex(),
186            underline: true,
187        },
188        linkText: {
189            color: colorScheme.ramps.orange(0.5).hex(),
190            italic: true,
191        },
192        "text.literal": {
193            color: color.string,
194        },
195        punctuation: {
196            color: color.punctuation,
197        },
198        "punctuation.bracket": {
199            color: color.punctuation,
200        },
201        "punctuation.delimiter": {
202            color: color.punctuation,
203        },
204        "punctuation.special": {
205            color: colorScheme.ramps.neutral(0.86).hex(),
206        },
207        "punctuation.list_marker": {
208            color: color.punctuation,
209        },
210        string: {
211            color: color.string,
212        },
213        "string.special": {
214            color: color.string,
215        },
216        "string.special.symbol": {
217            color: color.string,
218        },
219        "string.escape": {
220            color: color.comment,
221        },
222        "string.regex": {
223            color: color.string,
224        },
225        constructor: {
226            color: colorScheme.ramps.blue(0.5).hex(),
227        },
228        variant: {
229            color: colorScheme.ramps.blue(0.5).hex(),
230        },
231        type: {
232            color: color.type,
233        },
234        variable: {
235            color: color.primary,
236        },
237        label: {
238            color: colorScheme.ramps.blue(0.5).hex(),
239        },
240        tag: {
241            color: colorScheme.ramps.blue(0.5).hex(),
242        },
243        attribute: {
244            color: colorScheme.ramps.blue(0.5).hex(),
245        },
246        property: {
247            color: colorScheme.ramps.blue(0.5).hex(),
248        },
249        constant: {
250            color: color.constant,
251        },
252        keyword: {
253            color: color.keyword,
254        },
255        enum: {
256            color: color.enum,
257        },
258        operator: {
259            color: color.operator,
260        },
261        number: {
262            color: color.number,
263        },
264        boolean: {
265            color: color.boolean,
266        },
267        function: {
268            color: color.function,
269        },
270        preproc: {
271            color: color.primary,
272        },
273        embedded: {
274            color: color.primary,
275        },
276    }
277
278    return defaultSyntax
279}
280
281function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
282    if (!colorScheme.syntax) {
283        return defaultSyntax
284    }
285
286    return deepmerge<Syntax, Partial<ThemeSyntax>>(
287        defaultSyntax,
288        colorScheme.syntax,
289        {
290            arrayMerge: (destinationArray, sourceArray) => [
291                ...destinationArray,
292                ...sourceArray,
293            ],
294        }
295    )
296}
297
298export function buildSyntax(colorScheme: ColorScheme): Syntax {
299    const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
300
301    const syntax = mergeSyntax(defaultSyntax, colorScheme)
302
303    return syntax
304}