From 603b47f9b0ea439bfb49293f0d5d1eb041c2e97f Mon Sep 17 00:00:00 2001 From: giuxtaposition Date: Sat, 14 Mar 2026 15:45:37 +0100 Subject: [PATCH 1/3] feat(editor): add catppuccin theme to highlightjs --- packages/highlightjs/package.json | 1 + packages/highlightjs/src/index.ts | 24 ++++-- .../src/normalize_theme_css.spec.ts | 82 +++++++++++++++++++ packages/highlightjs/src/themes.ts | 16 ++++ pnpm-lock.yaml | 8 ++ 5 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 packages/highlightjs/src/normalize_theme_css.spec.ts diff --git a/packages/highlightjs/package.json b/packages/highlightjs/package.json index 249e2373ff..17af4c0e34 100644 --- a/packages/highlightjs/package.json +++ b/packages/highlightjs/package.json @@ -5,6 +5,7 @@ "type": "module", "main": "./src/index.ts", "dependencies": { + "@catppuccin/highlightjs": "1.0.1", "@exercism/highlightjs-gdscript": "0.0.1", "@triliumnext/commons": "workspace:*", "highlight.js": "11.11.1", diff --git a/packages/highlightjs/src/index.ts b/packages/highlightjs/src/index.ts index 3016869b73..d9fa6c260b 100644 --- a/packages/highlightjs/src/index.ts +++ b/packages/highlightjs/src/index.ts @@ -47,6 +47,22 @@ export function highlight(code: string, options: HighlightOptions) { return hljs.highlight(code, options); } +export function normalizeThemeCss(themeCss: string): string { + const themeSelectorScopedToCodeTag = /\bcode \.hljs-/.test(themeCss); + if (themeSelectorScopedToCodeTag) { + themeCss = themeCss.replace(/\bcode\.hljs/g, ".hljs"); + themeCss = themeCss.replace(/\bcode \.hljs-/g, ".hljs .hljs-"); + } + + // Increase the specificity of the HLJS selector to render properly within CKEditor without the need of patching the library. + themeCss = themeCss.replace( + /^\.hljs\s*\{/m, + ".hljs, .ck-content pre.hljs {", + ); + + return themeCss; +} + export async function loadTheme(theme: "none" | Theme) { if (theme === "none") { if (highlightingThemeEl) { @@ -61,12 +77,8 @@ export async function loadTheme(theme: "none" | Theme) { document.querySelector("head")?.append(highlightingThemeEl); } - let themeCss = (await theme.load()).default as string; - - // Increase the specificity of the HLJS selector to render properly within CKEditor without the need of patching the library. - themeCss = themeCss.replace(/^.hljs {/m, ".hljs, .ck-content pre.hljs {"); - - highlightingThemeEl.textContent = themeCss; + const themeCss = (await theme.load()).default as string; + highlightingThemeEl.textContent = normalizeThemeCss(themeCss); } export const { highlightAuto } = hljs; diff --git a/packages/highlightjs/src/normalize_theme_css.spec.ts b/packages/highlightjs/src/normalize_theme_css.spec.ts new file mode 100644 index 0000000000..38d8771872 --- /dev/null +++ b/packages/highlightjs/src/normalize_theme_css.spec.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import { normalizeThemeCss } from "./index.js"; + +describe("normalizeThemeCss", () => { + describe("standard highlight.js themes", () => { + const standardThemeCss = [ + "pre code.hljs {", + " display: block;", + " overflow-x: auto;", + " padding: 1em", + "}", + "code.hljs {", + " padding: 3px 5px", + "}", + ".hljs {", + " color: #ffffff;", + " background: #1c1b1b", + "}", + ".hljs-keyword {", + " color: #88aece", + "}", + ".hljs-string {", + " color: #b5bd68", + "}", + ].join("\n"); + + it("preserves 'pre code.hljs' layout rule", () => { + const result = normalizeThemeCss(standardThemeCss); + expect(result).toContain("pre code.hljs {"); + }); + + it("preserves 'code.hljs' inline layout rule", () => { + const result = normalizeThemeCss(standardThemeCss); + expect(result).toContain("code.hljs {"); + }); + + it("preserves .hljs-* token selectors unchanged", () => { + const result = normalizeThemeCss(standardThemeCss); + expect(result).toContain(".hljs-keyword {"); + expect(result).toContain(".hljs-string {"); + }); + + it("adds CKEditor specificity to .hljs container rule", () => { + const result = normalizeThemeCss(standardThemeCss); + expect(result).toContain(".hljs, .ck-content pre.hljs {"); + }); + }); + + describe("catppuccin-style themes (code-scoped selectors)", () => { + const catppuccinCss = + "code.hljs{color:#cdd6f4;background:#1e1e2e}" + + "code .hljs-keyword{color:#cba6f7}" + + "code .hljs-string{color:#a6e3a1}" + + "code .hljs-comment{color:#9399b2}"; + + it("rewrites 'code.hljs' container to '.hljs'", () => { + const result = normalizeThemeCss(catppuccinCss); + expect(result).not.toContain("code.hljs"); + }); + + it("rewrites 'code .hljs-*' token selectors to '.hljs .hljs-*'", () => { + const result = normalizeThemeCss(catppuccinCss); + expect(result).not.toContain("code .hljs-"); + expect(result).toContain(".hljs .hljs-keyword"); + expect(result).toContain(".hljs .hljs-string"); + expect(result).toContain(".hljs .hljs-comment"); + }); + + it("adds CKEditor specificity to .hljs container rule", () => { + const result = normalizeThemeCss(catppuccinCss); + expect(result).toContain(".hljs, .ck-content pre.hljs {"); + }); + + it("preserves color values", () => { + const result = normalizeThemeCss(catppuccinCss); + expect(result).toContain("#cdd6f4"); + expect(result).toContain("#1e1e2e"); + expect(result).toContain("#cba6f7"); + expect(result).toContain("#a6e3a1"); + }); + }); +}); diff --git a/packages/highlightjs/src/themes.ts b/packages/highlightjs/src/themes.ts index 568ace5f5b..f6d9ac00db 100644 --- a/packages/highlightjs/src/themes.ts +++ b/packages/highlightjs/src/themes.ts @@ -56,6 +56,22 @@ const themeDefinitions: Record = { name: "Brown Paper (Light)", load: () => import("highlight.js/styles/brown-paper.css?raw") }, + "catppuccin-latte": { + name: "Catppuccin Latte (Light)", + load: () => import("@catppuccin/highlightjs/css/catppuccin-latte.css?raw") + }, + "catppuccin-frappe": { + name: "Catppuccin Frappé (Dark)", + load: () => import("@catppuccin/highlightjs/css/catppuccin-frappe.css?raw") + }, + "catppuccin-macchiato": { + name: "Catppuccin Macchiato (Dark)", + load: () => import("@catppuccin/highlightjs/css/catppuccin-macchiato.css?raw") + }, + "catppuccin-mocha": { + name: "Catppuccin Mocha (Dark)", + load: () => import("@catppuccin/highlightjs/css/catppuccin-mocha.css?raw") + }, "codepen-embed": { name: "CodePen Embed (Dark)", load: () => import("highlight.js/styles/codepen-embed.css?raw") diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52e83e911d..e5305a8d7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1425,6 +1425,9 @@ importers: packages/highlightjs: dependencies: + '@catppuccin/highlightjs': + specifier: 1.0.1 + version: 1.0.1 '@exercism/highlightjs-gdscript': specifier: 0.0.1 version: 0.0.1 @@ -1891,6 +1894,9 @@ packages: '@catppuccin/codemirror@1.0.3': resolution: {integrity: sha512-P1ZCj4DZVLqr/TNz28m3aaF2Elrikpb8OOnzN0Vyf95Un4pTWTkCSvhbskbnJbnNJ87Rfvt3fXoaUj4o55X30Q==} + '@catppuccin/highlightjs@1.0.1': + resolution: {integrity: sha512-wnagsNQbJroHQMalkprwRoapfGV1hHRx46d7GXp4kf6rlShImBlgpqPCt9OD471Gq4qpHdfFH/GJFIvY1CLqHA==} + '@catppuccin/palette@1.7.1': resolution: {integrity: sha512-aRc1tbzrevOTV7nFTT9SRdF26w/MIwT4Jwt4fDMc9itRZUDXCuEDBLyz4TQMlqO9ZP8mf5Hu4Jr6D03NLFc6Gw==} @@ -16836,6 +16842,8 @@ snapshots: '@codemirror/view': 6.40.0 '@lezer/highlight': 1.2.3 + '@catppuccin/highlightjs@1.0.1': {} + '@catppuccin/palette@1.7.1': {} '@chevrotain/cst-dts-gen@11.1.1': From 50e5f89e9a001eb2ee3120f005e7fa3b8fc1b4b1 Mon Sep 17 00:00:00 2001 From: Giulia Ye Date: Mon, 16 Mar 2026 11:54:14 +0100 Subject: [PATCH 2/3] feat(editor): make theme selector scoped to code tag regex more robust Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/highlightjs/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/highlightjs/src/index.ts b/packages/highlightjs/src/index.ts index d9fa6c260b..053b7f5662 100644 --- a/packages/highlightjs/src/index.ts +++ b/packages/highlightjs/src/index.ts @@ -48,7 +48,7 @@ export function highlight(code: string, options: HighlightOptions) { } export function normalizeThemeCss(themeCss: string): string { - const themeSelectorScopedToCodeTag = /\bcode \.hljs-/.test(themeCss); + const themeSelectorScopedToCodeTag = /\bcode\s+\.hljs-/.test(themeCss); if (themeSelectorScopedToCodeTag) { themeCss = themeCss.replace(/\bcode\.hljs/g, ".hljs"); themeCss = themeCss.replace(/\bcode \.hljs-/g, ".hljs .hljs-"); From 850f8ad93978b66ed3785f83665245310a7b89c0 Mon Sep 17 00:00:00 2001 From: Giulia Ye Date: Mon, 16 Mar 2026 11:55:06 +0100 Subject: [PATCH 3/3] feat(editor): make theme selector scoped to code tag replace regex more robust Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/highlightjs/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/highlightjs/src/index.ts b/packages/highlightjs/src/index.ts index 053b7f5662..617aaf7ac5 100644 --- a/packages/highlightjs/src/index.ts +++ b/packages/highlightjs/src/index.ts @@ -51,7 +51,7 @@ export function normalizeThemeCss(themeCss: string): string { const themeSelectorScopedToCodeTag = /\bcode\s+\.hljs-/.test(themeCss); if (themeSelectorScopedToCodeTag) { themeCss = themeCss.replace(/\bcode\.hljs/g, ".hljs"); - themeCss = themeCss.replace(/\bcode \.hljs-/g, ".hljs .hljs-"); + themeCss = themeCss.replace(/\bcode\s+\.hljs-/g, ".hljs .hljs-"); } // Increase the specificity of the HLJS selector to render properly within CKEditor without the need of patching the library.