feat(editor): add catppuccin theme to highlightjs (#9075)
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run

This commit is contained in:
Elian Doran 2026-03-16 18:39:40 +02:00 committed by GitHub
commit ca349e03f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 125 additions and 6 deletions

View File

@ -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",

View File

@ -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\s+\.hljs-/.test(themeCss);
if (themeSelectorScopedToCodeTag) {
themeCss = themeCss.replace(/\bcode\.hljs/g, ".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.
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;

View File

@ -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");
});
});
});

View File

@ -56,6 +56,22 @@ const themeDefinitions: Record<string, Theme> = {
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")

8
pnpm-lock.yaml generated
View File

@ -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':