Add the ability to override the system syntax config

Nate Butler created

Change summary

styles/package-lock.json                | 14 +++++++++
styles/package.json                     |  1 
styles/src/styleTree/editor.ts          | 24 ++++++++++++++--
styles/src/themes/common/colorScheme.ts | 38 +++++++++++++++++++++++++++
styles/src/themes/common/ramps.ts       |  9 ++++--
5 files changed, 80 insertions(+), 6 deletions(-)

Detailed changes

styles/package-lock.json 🔗

@@ -14,6 +14,7 @@
                 "bezier-easing": "^2.1.0",
                 "case-anything": "^2.1.10",
                 "chroma-js": "^2.4.2",
+                "deepmerge": "^4.3.0",
                 "toml": "^3.0.0",
                 "ts-node": "^10.9.1"
             }
@@ -131,6 +132,14 @@
             "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
             "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
         },
+        "node_modules/deepmerge": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
+            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/diff": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -311,6 +320,11 @@
             "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
             "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
         },
+        "deepmerge": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
+            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og=="
+        },
         "diff": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",

styles/package.json 🔗

@@ -15,6 +15,7 @@
         "bezier-easing": "^2.1.0",
         "case-anything": "^2.1.10",
         "chroma-js": "^2.4.2",
+        "deepmerge": "^4.3.0",
         "toml": "^3.0.0",
         "ts-node": "^10.9.1"
     },

styles/src/styleTree/editor.ts 🔗

@@ -1,9 +1,12 @@
 import { fontWeights } from "../common"
 import { withOpacity } from "../utils/color"
-import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme"
+import { ColorScheme, Layer, StyleSets, Syntax, ThemeSyntax } from "../themes/common/colorScheme"
 import { background, border, borderColor, foreground, text } from "./components"
 import hoverPopover from "./hoverPopover"
 
+import deepmerge from 'deepmerge';
+
+
 export default function editor(colorScheme: ColorScheme) {
     let layer = colorScheme.highest
 
@@ -35,7 +38,7 @@ export default function editor(colorScheme: ColorScheme) {
         }
     }
 
-    const syntax = {
+    const syntax: Syntax = {
         primary: {
             color: colorScheme.ramps.neutral(1).hex(),
             weight: fontWeights.normal,
@@ -129,6 +132,21 @@ export default function editor(colorScheme: ColorScheme) {
         },
     }
 
+    function createSyntax(colorScheme: ColorScheme): Syntax {
+        if (!colorScheme.syntax) {
+            return syntax
+        }
+
+        return deepmerge<Syntax, Partial<ThemeSyntax>>(syntax, colorScheme.syntax, {
+            arrayMerge: (destinationArray, sourceArray) => [
+                ...destinationArray,
+                ...sourceArray,
+            ],
+        });
+    }
+
+    const mergedSyntax = createSyntax(colorScheme)
+
     return {
         textColor: syntax.primary.color,
         background: background(layer),
@@ -285,6 +303,6 @@ export default function editor(colorScheme: ColorScheme) {
                 color: borderColor(layer),
             },
         },
-        syntax,
+        syntax: mergedSyntax,
     }
 }

styles/src/themes/common/colorScheme.ts 🔗

@@ -1,4 +1,5 @@
 import { Scale } from "chroma-js"
+import { FontWeight } from "../../common"
 
 export interface ColorScheme {
     name: string
@@ -14,6 +15,7 @@ export interface ColorScheme {
     modalShadow: Shadow
 
     players: Players
+    syntax?: Partial<ThemeSyntax>
 }
 
 export interface Meta {
@@ -98,3 +100,39 @@ export interface Style {
     border: string
     foreground: string
 }
+
+export interface SyntaxHighlightStyle {
+    color: string,
+    weight?: FontWeight
+    underline?: boolean
+    italic?: boolean
+}
+
+export interface Syntax {
+    primary: SyntaxHighlightStyle,
+    "variable.special": SyntaxHighlightStyle,
+    comment: SyntaxHighlightStyle,
+    punctuation: SyntaxHighlightStyle,
+    constant: SyntaxHighlightStyle,
+    keyword: SyntaxHighlightStyle,
+    function: SyntaxHighlightStyle,
+    type: SyntaxHighlightStyle,
+    constructor: SyntaxHighlightStyle,
+    variant: SyntaxHighlightStyle,
+    property: SyntaxHighlightStyle,
+    enum: SyntaxHighlightStyle,
+    operator: SyntaxHighlightStyle,
+    string: SyntaxHighlightStyle,
+    number: SyntaxHighlightStyle,
+    boolean: SyntaxHighlightStyle,
+    predictive: SyntaxHighlightStyle,
+    title: SyntaxHighlightStyle,
+    emphasis: SyntaxHighlightStyle,
+    "emphasis.strong": SyntaxHighlightStyle,
+    linkUri: SyntaxHighlightStyle,
+    linkText: SyntaxHighlightStyle
+}
+
+// HACK: "constructor" as a key in the syntax interface returns an error when a theme tries to use it.
+// For now hack around it by omiting constructor as a valid key for overrides.
+export type ThemeSyntax = Partial<Omit<Syntax, "constructor">>

styles/src/themes/common/ramps.ts 🔗

@@ -7,6 +7,7 @@ import {
     Style,
     Styles,
     StyleSet,
+    ThemeSyntax,
 } from "./colorScheme"
 
 export function colorRamp(color: Color): Scale {
@@ -18,7 +19,8 @@ export function colorRamp(color: Color): Scale {
 export function createColorScheme(
     name: string,
     isLight: boolean,
-    colorRamps: { [rampName: string]: Scale }
+    colorRamps: { [rampName: string]: Scale },
+    syntax?: ThemeSyntax
 ): ColorScheme {
     // Chromajs scales from 0 to 1 flipped if isLight is true
     let ramps: RampSet = {} as any
@@ -31,14 +33,14 @@ export function createColorScheme(
     // function to any in order to get the colors back out from the original ramps.
     if (isLight) {
         for (var rampName in colorRamps) {
-            ;(ramps as any)[rampName] = chroma.scale(
+            ; (ramps as any)[rampName] = chroma.scale(
                 colorRamps[rampName].colors(100).reverse()
             )
         }
         ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse())
     } else {
         for (var rampName in colorRamps) {
-            ;(ramps as any)[rampName] = chroma.scale(
+            ; (ramps as any)[rampName] = chroma.scale(
                 colorRamps[rampName].colors(100)
             )
         }
@@ -94,6 +96,7 @@ export function createColorScheme(
         modalShadow,
 
         players,
+        syntax
     }
 }