diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e2a565f72f..d807723a1d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1736,6 +1736,9 @@ importers: dompurify: specifier: 3.4.3 version: 3.4.3 + highlight.js: + specifier: 10.7.3 + version: 10.7.3 json5: specifier: 2.2.3 version: 2.2.3 diff --git a/ui/package.json b/ui/package.json index fbc3054c10d..3fbe4e19a8f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "@create-markdown/preview": "2.0.3", "@noble/ed25519": "3.1.0", "dompurify": "3.4.3", + "highlight.js": "10.7.3", "json5": "2.2.3", "lit": "3.3.3", "markdown-it": "14.1.1", diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 12bc0a4cc6e..6714a9cfc9e 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -2004,6 +2004,159 @@ background: var(--bg); } +:is(.code-block .hljs, .code-block-wrapper pre code.hljs) { + color: var(--text); +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-comment, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-quote { + color: var(--muted); + font-style: italic; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-keyword, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-selector-tag, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-subst { + color: #ff8a80; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-number, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-literal, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-variable, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-template-variable, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-tag .hljs-attr { + color: #79c0ff; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-string, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-doctag { + color: #a5d6ff; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-title, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-section, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-selector-id, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-function .hljs-title { + color: #d2a8ff; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-type, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-class .hljs-title, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-built_in, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-builtin-name { + color: #ffa657; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-attr, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-attribute, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-name, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-selector-class, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-selector-attr, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-selector-pseudo { + color: #7ee787; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-symbol, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-bullet, +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-link { + color: var(--accent-2); +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-meta { + color: #c9d1d9; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-deletion { + color: #ffa198; + background: rgba(248, 81, 73, 0.12); +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-addition { + color: #7ee787; + background: rgba(46, 160, 67, 0.12); +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-emphasis { + font-style: italic; +} + +:is(.code-block, .code-block-wrapper pre code.hljs) .hljs-strong { + font-weight: 600; +} + +:root[data-theme-mode="light"] :is(.code-block .hljs, .code-block-wrapper pre code.hljs) { + color: var(--text); +} + +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-keyword, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-selector-tag, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-subst { + color: #cf222e; +} + +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-number, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-literal, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-variable, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-template-variable, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-tag + .hljs-attr { + color: #0550ae; +} + +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-string, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-doctag { + color: #0a3069; +} + +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-title, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-section, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-selector-id, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-function + .hljs-title { + color: #8250df; +} + +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-type, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-class + .hljs-title, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-built_in, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-builtin-name { + color: #953800; +} + +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-attr, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-attribute, +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-name, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-selector-class, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-selector-attr, +:root[data-theme-mode="light"] + :is(.code-block, .code-block-wrapper pre code.hljs) + .hljs-selector-pseudo { + color: #116329; +} + +:root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-meta { + color: #57606a; +} + .markdown-plain-text-fallback { display: block; white-space: pre-wrap; diff --git a/ui/src/styles/components.test.ts b/ui/src/styles/components.test.ts index 49df92d12a3..4037480d132 100644 --- a/ui/src/styles/components.test.ts +++ b/ui/src/styles/components.test.ts @@ -5,6 +5,18 @@ function readComponentsCss(): string { return readStyleSheet("ui/src/styles/components.css"); } +describe("code block highlight styles", () => { + it("targets the markdown renderer's generated code block wrapper", () => { + const css = readComponentsCss(); + + expect(css).toContain(":is(.code-block .hljs, .code-block-wrapper pre code.hljs)"); + expect(css).toContain(":is(.code-block, .code-block-wrapper pre code.hljs) .hljs-keyword"); + expect(css).toContain( + ':root[data-theme-mode="light"] :is(.code-block, .code-block-wrapper pre code.hljs) .hljs-string', + ); + }); +}); + describe("agent fallback chip styles", () => { it("styles the chip remove control inside the agent model input", () => { const css = readComponentsCss(); diff --git a/ui/src/types/highlight-js-subpaths.d.ts b/ui/src/types/highlight-js-subpaths.d.ts new file mode 100644 index 00000000000..535d78b37fd --- /dev/null +++ b/ui/src/types/highlight-js-subpaths.d.ts @@ -0,0 +1,9 @@ +declare module "highlight.js/lib/core.js" { + import hljs = require("highlight.js"); + + export default hljs; +} + +declare module "highlight.js/lib/languages/*.js" { + export default function language(hljs?: HLJSApi): LanguageDetail; +} diff --git a/ui/src/ui/markdown.test.ts b/ui/src/ui/markdown.test.ts index 0549558f6cf..c2645ed978d 100644 --- a/ui/src/ui/markdown.test.ts +++ b/ui/src/ui/markdown.test.ts @@ -332,9 +332,14 @@ describe("toSanitizedMarkdownHtml", () => { describe("code blocks", () => { it("renders fenced code blocks", () => { const html = toSanitizedMarkdownHtml("```ts\nconsole.log(1)\n```"); - expect(html).toBe( - '
console.log(1)\n