/** * Converts an IntelliJ theme (`.theme.json`, optionally with its editor scheme * `.icls`) into an OpenIDEA theme + editor scheme. The mapping covers the * main keys only; anything unmapped keeps the built-in value of the base * (`kind`). Pure functions, no dependencies: see import-intellij-theme.ts * for the command line. */ import { stripJsonComments } from "./tokens.ts"; /** OpenIDEA token ← IntelliJ `ui` keys, first present wins. `*.prop` wildcards apply too. */ export const UI_KEYS: Record = { "--bg-app": ["Panel.background"], "--bg-frame": ["MainWindow.background", "MainToolbar.background", "Panel.background"], "--bg-island": ["ToolWindow.background", "Panel.background"], "--bg-elev": ["ToolWindow.background", "SidePanel.background"], "--bg-popup": ["Popup.background", "PopupMenu.background"], "--bg-floating-toolbar": ["Toolbar.Floating.background"], "--bg-backdrop": ["Popup.background"], "--bg-hover": ["List.hoverBackground", "Tree.hoverBackground"], "--bg-selected": ["List.selectionBackground", "Tree.selectionBackground"], "--bg-selected-strong": ["List.selectionBackground"], "--bg-tree-selected": ["Tree.selectionBackground", "List.selectionBackground"], "--bg-tree-selected-inactive": ["Tree.selectionInactiveBackground", "List.selectionInactiveBackground"], "--bg-input": ["TextField.background"], "--bg-field": ["TextField.background"], "--bg-dialog": ["Dialog.background", "Panel.background"], "--border": ["Borders.color"], "--border-popup": ["Popup.borderColor", "Popup.Border.color"], "--border-strong": ["Component.borderColor", "Borders.ContrastBorderColor"], "--border-window": ["Borders.ContrastBorderColor", "Component.borderColor"], "--fg": ["Label.foreground"], "--fg-emphasis": ["Label.foreground"], "--fg-strong": ["Label.selectedForeground", "Label.foreground"], "--fg-secondary": ["Label.infoForeground", "Component.infoForeground"], "--fg-muted": ["Label.infoForeground", "Component.infoForeground"], "--fg-subtle": ["Label.disabledForeground"], "--fg-faint": ["Label.disabledForeground"], "--accent": ["Button.default.startBackground", "Button.default.background", "Component.focusColor"], "--fg-on-accent": ["Button.default.foreground"], "--fg-button-primary": ["Button.default.foreground"], "--focus-ring-soft": ["Component.focusColor"], "--link": ["Link.activeForeground", "Hyperlink.linkColor"], "--link-hover": ["Link.hoverForeground", "Link.activeForeground"], "--danger": ["Component.errorFocusColor"], "--warning": ["Component.warningFocusColor"], "--bg-tooltip": ["ToolTip.background"], "--fg-tooltip": ["ToolTip.foreground"], "--border-tooltip": ["ToolTip.borderColor"], "--bg-button": ["Button.startBackground", "Button.background"], "--border-button": ["Button.startBorderColor", "Button.borderColor"], "--bg-checkbox": ["Checkbox.Background.Default"], "--border-checkbox": ["Checkbox.Border.Default"], "--bg-tab-selected": ["EditorTabs.underlinedTabBackground", "EditorTabs.selectedBackground"], "--border-tab-selected": ["EditorTabs.underlineColor"], "--bg-tab-hover": ["EditorTabs.hoverBackground"], "--border-editor-tabs": ["EditorTabs.borderColor"], "--bg-toolbar-hover": ["ActionButton.hoverBackground"], "--bg-toolbar-pressed": ["ActionButton.pressedBackground"], "--bg-stripe-hover": ["ToolWindow.Button.hoverBackground"], "--bg-stripe-selected": ["ToolWindow.Button.selectedBackground"], "--scrollbar-thumb": ["ScrollBar.Mac.thumbColor", "ScrollBar.thumbColor"], "--scrollbar-thumb-hover": ["ScrollBar.Mac.hoverThumbColor", "ScrollBar.hoverThumbColor"], "--bg-progress-track": ["ProgressBar.trackColor"], "--bg-counter": ["Counter.background"], "--fg-counter": ["Counter.foreground"], "--bg-search-match": ["SearchMatch.startBackground", "SearchMatch.endBackground"], "--bg-toggle-on": ["ToggleButton.onBackground"], "--fg-toggle-on": ["ToggleButton.onForeground"], "--bg-toggle-off": ["ToggleButton.offBackground"], "--fg-toggle-off": ["ToggleButton.offForeground"], "--border-toggle": ["ToggleButton.borderColor"], "--bg-toggle-knob": ["ToggleButton.buttonColor"], "--bg-feedback-info": ["Banner.infoBackground"], "--border-feedback-info": ["Banner.infoBorderColor"], "--bg-feedback-error": ["Banner.errorBackground"], "--border-feedback-error": ["Banner.errorBorderColor"], "--bg-feedback-warning": ["Banner.warningBackground"], "--border-feedback-warning": ["Banner.warningBorderColor"], "--bg-feedback-success": ["Banner.successBackground"], "--border-feedback-success": ["Banner.successBorderColor"], }; /** `.icls` `` options → tokens. */ export const ICLS_COLORS: Record = { CARET_COLOR: ["--caret"], CARET_ROW_COLOR: ["--bg-current-line"], SELECTION_BACKGROUND: ["--bg-editor-selection"], LINE_NUMBERS_COLOR: ["--fg-line-number"], LINE_NUMBER_ON_CARET_ROW_COLOR: ["--fg-line-number-current"], GUTTER_BACKGROUND: ["--bg-editor-gutter"], INDENT_GUIDE: ["--border-indent-guide"], SELECTED_INDENT_GUIDE: ["--border-indent-guide-active"], RIGHT_MARGIN_COLOR: ["--border-right-margin"], WHITESPACES: ["--fg-whitespace"], METHOD_SEPARATORS_COLOR: ["--border-method-separator"], FILESTATUS_MODIFIED: ["--vcs-modified"], FILESTATUS_ADDED: ["--vcs-added"], FILESTATUS_DELETED: ["--vcs-deleted"], FILESTATUS_UNKNOWN: ["--vcs-untracked"], FILESTATUS_IDEA_FILESTATUS_IGNORED: ["--vcs-ignored"], FILESTATUS_IDEA_FILESTATUS_MERGED_WITH_CONFLICTS: ["--vcs-conflict"], MODIFIED_LINES_COLOR: ["--gutter-modified"], ADDED_LINES_COLOR: ["--gutter-added"], DELETED_LINES_COLOR: ["--gutter-deleted"], }; type Part = "FOREGROUND" | "BACKGROUND" | "EFFECT_COLOR"; /** `.icls` `` options → [part, token] pairs. */ export const ICLS_ATTRIBUTES: Record> = { TEXT: [ ["FOREGROUND", "--fg-editor"], ["BACKGROUND", "--bg-editor"], ], DIFF_MODIFIED: [["BACKGROUND", "--diff-modified"]], DIFF_INSERTED: [["BACKGROUND", "--diff-inserted"]], DIFF_DELETED: [["BACKGROUND", "--diff-deleted"]], DEFAULT_KEYWORD: [["FOREGROUND", "--syn-keyword"]], DEFAULT_STRING: [["FOREGROUND", "--syn-string"]], DEFAULT_NUMBER: [["FOREGROUND", "--syn-number"]], DEFAULT_LINE_COMMENT: [["FOREGROUND", "--syn-comment"]], DEFAULT_DOC_COMMENT: [["FOREGROUND", "--syn-doc-comment"]], DEFAULT_FUNCTION_DECLARATION: [["FOREGROUND", "--syn-function"]], DEFAULT_FUNCTION_CALL: [["FOREGROUND", "--syn-function-call"]], DEFAULT_CLASS_NAME: [["FOREGROUND", "--syn-type"]], DEFAULT_CONSTANT: [["FOREGROUND", "--syn-constant"]], DEFAULT_INSTANCE_FIELD: [["FOREGROUND", "--syn-property"]], DEFAULT_IDENTIFIER: [["FOREGROUND", "--syn-variable"]], DEFAULT_LOCAL_VARIABLE: [["FOREGROUND", "--syn-variable"]], DEFAULT_PARAMETER: [["FOREGROUND", "--syn-param"]], DEFAULT_PREDEFINED_SYMBOL: [["FOREGROUND", "--syn-builtin"]], DEFAULT_METADATA: [ ["FOREGROUND", "--syn-annotation"], ["FOREGROUND", "--syn-decorator"], ], DEFAULT_OPERATION_SIGN: [["FOREGROUND", "--syn-punct"]], DEFAULT_VALID_STRING_ESCAPE: [["FOREGROUND", "--syn-escape"]], DEFAULT_TAG: [["FOREGROUND", "--syn-tag"]], DEFAULT_ATTRIBUTE: [["FOREGROUND", "--syn-attribute"]], TODO_DEFAULT_ATTRIBUTES: [["FOREGROUND", "--syn-todo"]], BAD_CHARACTER: [["FOREGROUND", "--syn-invalid"]], RAINBOW_COLOR1: [["FOREGROUND", "--syn-rainbow-1"]], RAINBOW_COLOR2: [["FOREGROUND", "--syn-rainbow-2"]], RAINBOW_COLOR3: [["FOREGROUND", "--syn-rainbow-3"]], RAINBOW_COLOR4: [["FOREGROUND", "--syn-rainbow-4"]], RAINBOW_COLOR5: [["FOREGROUND", "--syn-rainbow-5"]], ERRORS_ATTRIBUTES: [["EFFECT_COLOR", "--wave-error"]], WARNING_ATTRIBUTES: [["EFFECT_COLOR", "--wave-warning"]], NOT_USED_ELEMENT_ATTRIBUTES: [["FOREGROUND", "--fg-unused"]], MATCHED_BRACE_ATTRIBUTES: [["BACKGROUND", "--bg-matched-brace"]], SEARCH_RESULT_ATTRIBUTES: [["BACKGROUND", "--bg-search-match"]], TEXT_SEARCH_RESULT_ATTRIBUTES: [["BACKGROUND", "--bg-text-search-result"]], IDENTIFIER_UNDER_CARET_ATTRIBUTES: [["BACKGROUND", "--bg-identifier-under-caret"]], WRITE_IDENTIFIER_UNDER_CARET_ATTRIBUTES: [["BACKGROUND", "--bg-identifier-under-caret-write"]], FOLDED_TEXT_ATTRIBUTES: [ ["FOREGROUND", "--fg-fold-placeholder"], ["BACKGROUND", "--bg-fold-placeholder"], ], INLINE_PARAMETER_HINT: [ ["FOREGROUND", "--fg-inlay"], ["BACKGROUND", "--bg-inlay"], ], BREAKPOINT_ATTRIBUTES: [["BACKGROUND", "--bg-breakpoint-line"]], DEBUGGER_INLINED_VALUES: [["FOREGROUND", "--syn-inline-value"]], "PY.SELF_PARAMETER": [["FOREGROUND", "--syn-self"]], "PY.KEYWORD_ARGUMENT": [["FOREGROUND", "--syn-keyword-arg"]], "PY.DOC_COMMENT": [["FOREGROUND", "--syn-docstring"]], "PY.BUILTIN_NAME": [["FOREGROUND", "--syn-predefined"]], MARKDOWN_HEADER_LEVEL_1: [["FOREGROUND", "--syn-markup-heading"]], MARKDOWN_CODE_SPAN: [["FOREGROUND", "--syn-markup-code"]], MARKDOWN_LINK_TEXT: [["FOREGROUND", "--syn-link"]], CONSOLE_BLACK_OUTPUT: [["FOREGROUND", "--ansi-black"]], CONSOLE_RED_OUTPUT: [["FOREGROUND", "--ansi-red"]], CONSOLE_GREEN_OUTPUT: [["FOREGROUND", "--ansi-green"]], CONSOLE_YELLOW_OUTPUT: [["FOREGROUND", "--ansi-yellow"]], CONSOLE_BLUE_OUTPUT: [["FOREGROUND", "--ansi-blue"]], CONSOLE_MAGENTA_OUTPUT: [["FOREGROUND", "--ansi-magenta"]], CONSOLE_CYAN_OUTPUT: [["FOREGROUND", "--ansi-cyan"]], CONSOLE_GRAY_OUTPUT: [["FOREGROUND", "--ansi-white"]], CONSOLE_DARKGRAY_OUTPUT: [["FOREGROUND", "--ansi-bright-black"]], CONSOLE_RED_BRIGHT_OUTPUT: [["FOREGROUND", "--ansi-bright-red"]], CONSOLE_GREEN_BRIGHT_OUTPUT: [["FOREGROUND", "--ansi-bright-green"]], CONSOLE_YELLOW_BRIGHT_OUTPUT: [["FOREGROUND", "--ansi-bright-yellow"]], CONSOLE_BLUE_BRIGHT_OUTPUT: [["FOREGROUND", "--ansi-bright-blue"]], CONSOLE_MAGENTA_BRIGHT_OUTPUT: [["FOREGROUND", "--ansi-bright-magenta"]], CONSOLE_CYAN_BRIGHT_OUTPUT: [["FOREGROUND", "--ansi-bright-cyan"]], CONSOLE_WHITE_OUTPUT: [["FOREGROUND", "--ansi-bright-white"]], }; export interface ConvertedTheme { theme: { id: string; name: string; kind: "dark" | "light"; tokens: Record }; scheme: { name: string; tokens: Record } | null; } type Json = string | number | boolean | null | Json[] | { [k: string]: Json }; /** `"Button": {"default": {"startBackground": x}}` → `Button.default.startBackground`; `"*": {...}` → `*.prop`. */ function flatten(obj: { [k: string]: Json }, prefix = "", out = new Map()): Map { for (const [k, v] of Object.entries(obj)) { const key = prefix ? `${prefix}.${k}` : k; if (typeof v === "string") out.set(key, v); else if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out); } return out; } const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i; /** IntelliJ colour (`#rrggbb`, `#rrggbbaa`, or a named colour from `colors`) → CSS hex. */ function resolveColor(value: string, named: Map, depth = 0): string | null { const m = HEX.exec(value.trim()); if (m) return `#${m[1]!.toLowerCase()}`; const ref = named.get(value); return ref !== undefined && depth < 8 ? resolveColor(ref, named, depth + 1) : null; } /** `.icls` hex: RGB without `#`, leading zeros dropped (`"0"` is black, `"ff"` is #0000ff). */ function iclsColor(value: string): string | null { const v = value.trim().toLowerCase(); if (!/^[0-9a-f]{1,8}$/.test(v)) return null; return `#${v.length <= 6 ? v.padStart(6, "0") : v.padStart(8, "0")}`; } const attr = (tag: string, name: string) => new RegExp(`${name}="([^"]*)"`).exec(tag)?.[1]; /** Parses the colours and attributes of an `.icls` file into tokens. */ export function convertIcls(xml: string): { name: string; dark: boolean | null; tokens: Record } { const tokens: Record = {}; const set = (names: string[], color: string | null) => { if (color) for (const n of names) tokens[n] ??= color; }; const schemeTag = /]*>/.exec(xml)?.[0] ?? ""; const name = attr(schemeTag, "name") ?? "Imported"; const parent = attr(schemeTag, "parent_scheme") ?? ""; const dark = parent ? /darcula|dark/i.test(parent) : null; const colors = /([\s\S]*?)<\/colors>/.exec(xml)?.[1] ?? ""; for (const m of colors.matchAll(/]*\/>/g)) { const key = attr(m[0], "name"); const value = attr(m[0], "value"); if (key && value !== undefined && ICLS_COLORS[key]) set(ICLS_COLORS[key], iclsColor(value)); } const attributes = /([\s\S]*?)<\/attributes>/.exec(xml)?.[1] ?? ""; for (const m of attributes.matchAll(/