| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | /** |
| 9 | * This is the prefix used by Monaco for the CSS classes it uses for syntax |
| 10 | * highlighting. Each token returned by `IGrammar.tokenizeLine2()` has a |
| 11 | * `number` associated with it. To construct the appropriate CSS class name for |
| 12 | * color number `n`, do: `${CSS_CLASS_PREFIX}${n}`. |
| 13 | */ |
| 14 | export const CSS_CLASS_PREFIX = 'mtk'; |
| 15 | |
| 16 | /** |
| 17 | * Updates the <style> element on the page to define the CSS rules necessary to |
| 18 | * honor the user's selected theme. |
| 19 | * @param colorMap as returned by `Registry.getColorMap()` where each value in |
| 20 | * the array is a CSS hex value, such as "#AA0000". |
| 21 | */ |
| 22 | export function updateTextMateGrammarCSS(colorMap: string[]): void { |
| 23 | // Note that if the Monaco editor is used on the page, then we also need to do |
| 24 | // something like: |
| 25 | // |
| 26 | // const colorMap = cssColors.map(Color.Format.CSS.parseHex); |
| 27 | // TokenizationRegistry.setColorMap(colorMap); |
| 28 | // |
| 29 | // though that will require loading code from the monaco-editor npm module. |
| 30 | |
| 31 | const css = generateTokensCSSForColorMap(colorMap); |
| 32 | const style = getOrCreateStyleElementForColorsCSS(); |
| 33 | style.innerHTML = css; |
| 34 | } |
| 35 | |
| 36 | let styleElementForTextMateCSS: HTMLStyleElement | null = null; |
| 37 | |
| 38 | function getOrCreateStyleElementForColorsCSS(): HTMLStyleElement { |
| 39 | // If there is an existing <style> element, then overwrite its contents |
| 40 | // rather than create a new one. (Yes, this means that we support only one |
| 41 | // theme globally on the page at a time, at least for now.) |
| 42 | if (styleElementForTextMateCSS != null) { |
| 43 | return styleElementForTextMateCSS; |
| 44 | } |
| 45 | |
| 46 | // We want to ensure that our <style> element appears after Monaco's so that |
| 47 | // we can override some styles it inserted for the default theme. |
| 48 | styleElementForTextMateCSS = document.createElement('style'); |
| 49 | |
| 50 | // If an instance of the Monaco editor is being used on the page, then it will |
| 51 | // have injected a stylesheet that we need to override. We expect these styles |
| 52 | // to be in an element with the class name 'monaco-colors' based on: |
| 53 | // https://github.com/microsoft/vscode/blob/f78d84606cd16d75549c82c68888de91d8bdec9f/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts#L206-L214 |
| 54 | // |
| 55 | // However, .monaco-colors may not have been inserted yet (this could be the |
| 56 | // case depending on where Monaco is in its own initialization process), so we |
| 57 | // just append the <style> tag to <body> so that when .monaco-colors is |
| 58 | // inserted to the <head> tag, our <style> tag will be guaranteed to appear |
| 59 | // later in the DOM and therefore its styles will take precedence. |
| 60 | document.body.appendChild(styleElementForTextMateCSS); |
| 61 | |
| 62 | return styleElementForTextMateCSS; |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * Adapted from the `generateTokensCSSForColorMap()` implementation in |
| 67 | * `monaco-editor/esm/vs/editor/common/modes/supports/tokenization.js`. |
| 68 | * Note that the original takes an Array<Color>, but while the Color class has |
| 69 | * all sorts of fancy methods like `getRelativeLuminance()` and `blend()`, the |
| 70 | * only thing this function needs is its `toString()` method, which formats it |
| 71 | * as a CSS hex value, which is what `registry.getColorMap()` returned in the |
| 72 | * first place! |
| 73 | */ |
| 74 | function generateTokensCSSForColorMap(cssColors: readonly string[]): string { |
| 75 | const rules: string[] = []; |
| 76 | for (let i = 1, len = cssColors.length; i < len; i++) { |
| 77 | const color = cssColors[i]; |
| 78 | rules[i] = `.${CSS_CLASS_PREFIX}${i} { color: ${color}; }`; |
| 79 | } |
| 80 | rules.push(`.${CSS_CLASS_PREFIX}i { font-style: italic; }`); |
| 81 | rules.push(`.${CSS_CLASS_PREFIX}b { font-weight: bold; }`); |
| 82 | rules.push( |
| 83 | `.${CSS_CLASS_PREFIX}u { text-decoration: underline; text-underline-position: under; }`, |
| 84 | ); |
| 85 | rules.push(`.${CSS_CLASS_PREFIX}s { text-decoration: line-through; }`); |
| 86 | rules.push( |
| 87 | `.${CSS_CLASS_PREFIX}s.${CSS_CLASS_PREFIX}u { text-decoration: underline line-through; text-underline-position: under; }`, |
| 88 | ); |
| 89 | return rules.join('\n'); |
| 90 | } |
| 91 | |