diff --git a/scripts/generate_prism_css.ts b/scripts/generate_prism_css.ts new file mode 100755 index 000000000..d95ea9366 --- /dev/null +++ b/scripts/generate_prism_css.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env bun + +/** + * Generates Prism CSS stylesheet from vscDarkPlus theme + * Used for syntax highlighting when react-syntax-highlighter has useInlineStyles={false} + * + * Strips backgrounds to preserve diff backgrounds in Review tab + * Omits font-family and font-size to inherit from parent components + */ + +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; +import type { CSSProperties } from "react"; + +const OUTPUT_PATH = "src/styles/prism-syntax.css"; + +// Strip backgrounds like we do in syntaxHighlighting.ts +const syntaxStyleNoBackgrounds: Record = {}; +for (const [key, value] of Object.entries(vscDarkPlus as Record)) { + if (typeof value === "object" && value !== null) { + const { background, backgroundColor, ...rest } = value as Record; + if (Object.keys(rest).length > 0) { + syntaxStyleNoBackgrounds[key] = rest as CSSProperties; + } + } +} + +// Convert CSS properties object to CSS string +function cssPropertiesToString(props: CSSProperties): string { + return Object.entries(props) + .filter(([key]) => { + // Skip font-family and font-size - we want to inherit these + return key !== "fontFamily" && key !== "fontSize"; + }) + .map(([key, value]) => { + // Convert camelCase to kebab-case + const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase(); + return ` ${cssKey}: ${value};`; + }) + .join("\n"); +} + +// Generate CSS content +function generateCSS(): string { + const lines: string[] = [ + "/**", + " * Auto-generated Prism syntax highlighting styles", + " * Based on VS Code Dark+ theme with backgrounds removed", + " * Used when react-syntax-highlighter has useInlineStyles={false}", + " *", + " * Font family and size are intentionally omitted to inherit from parent.", + " * ", + " * To regenerate: bun run scripts/generate_prism_css.ts", + " */", + "", + ]; + + for (const [selector, props] of Object.entries(syntaxStyleNoBackgrounds)) { + const cssRules = cssPropertiesToString(props); + if (cssRules.trim().length > 0) { + // Handle selectors that need .token prefix + let cssSelector = selector; + + // Add .token prefix for single-word selectors (token types) + if (!/[ >[\]:.]/.test(selector) && !selector.startsWith("pre") && !selector.startsWith("code")) { + cssSelector = `.token.${selector}`; + } + + lines.push(`${cssSelector} {`); + lines.push(cssRules); + lines.push("}"); + lines.push(""); + } + } + + return lines.join("\n"); +} + +async function main() { + console.log("Generating Prism CSS stylesheet..."); + + const css = generateCSS(); + + console.log(`Writing CSS to ${OUTPUT_PATH}...`); + await Bun.write(OUTPUT_PATH, css); + + console.log("✓ Prism CSS generated successfully"); +} + +main().catch((error) => { + console.error("Error generating Prism CSS:", error); + process.exit(1); +}); + diff --git a/src/components/shared/DiffRenderer.tsx b/src/components/shared/DiffRenderer.tsx index abdfa536f..db5e89a1f 100644 --- a/src/components/shared/DiffRenderer.tsx +++ b/src/components/shared/DiffRenderer.tsx @@ -10,6 +10,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { syntaxStyleNoBackgrounds } from "@/styles/syntaxHighlighting"; import { getLanguageFromPath } from "@/utils/git/languageDetector"; import { Tooltip, TooltipWrapper } from "../Tooltip"; +import "@/styles/prism-syntax.css"; // Shared type for diff line types export type DiffLineType = "add" | "remove" | "context" | "header"; @@ -159,6 +160,7 @@ const HighlightedContent = React.memo<{ code: string; language: string }>(({ cod code[class*="language-"] { + padding: .1em .3em; + border-radius: .3em; + color: #db4c69; +} + +.namespace { + -opacity: .7; +} + +doctype.doctype-tag { + color: #569CD6; +} + +doctype.name { + color: #9cdcfe; +} + +.token.comment { + color: #6a9955; +} + +.token.prolog { + color: #6a9955; +} + +.token.punctuation { + color: #d4d4d4; +} + +.language-html .language-css .token.punctuation { + color: #d4d4d4; +} + +.language-html .language-javascript .token.punctuation { + color: #d4d4d4; +} + +.token.property { + color: #9cdcfe; +} + +.token.tag { + color: #569cd6; +} + +.token.boolean { + color: #569cd6; +} + +.token.number { + color: #b5cea8; +} + +.token.constant { + color: #9cdcfe; +} + +.token.symbol { + color: #b5cea8; +} + +.token.inserted { + color: #b5cea8; +} + +.token.unit { + color: #b5cea8; +} + +.token.selector { + color: #d7ba7d; +} + +.token.attr-name { + color: #9cdcfe; +} + +.token.string { + color: #ce9178; +} + +.token.char { + color: #ce9178; +} + +.token.builtin { + color: #ce9178; +} + +.token.deleted { + color: #ce9178; +} + +.language-css .token.string.url { + text-decoration: underline; +} + +.token.operator { + color: #d4d4d4; +} + +.token.entity { + color: #569cd6; +} + +operator.arrow { + color: #569CD6; +} + +.token.atrule { + color: #ce9178; +} + +atrule.rule { + color: #c586c0; +} + +atrule.url { + color: #9cdcfe; +} + +atrule.url.function { + color: #dcdcaa; +} + +atrule.url.punctuation { + color: #d4d4d4; +} + +.token.keyword { + color: #569CD6; +} + +keyword.module { + color: #c586c0; +} + +keyword.control-flow { + color: #c586c0; +} + +.token.function { + color: #dcdcaa; +} + +function.maybe-class-name { + color: #dcdcaa; +} + +.token.regex { + color: #d16969; +} + +.token.important { + color: #569cd6; +} + +.token.italic { + font-style: italic; +} + +.token.class-name { + color: #4ec9b0; +} + +.token.maybe-class-name { + color: #4ec9b0; +} + +.token.console { + color: #9cdcfe; +} + +.token.parameter { + color: #9cdcfe; +} + +.token.interpolation { + color: #9cdcfe; +} + +punctuation.interpolation-punctuation { + color: #569cd6; +} + +.token.variable { + color: #9cdcfe; +} + +imports.maybe-class-name { + color: #9cdcfe; +} + +exports.maybe-class-name { + color: #9cdcfe; +} + +.token.escape { + color: #d7ba7d; +} + +tag.punctuation { + color: #808080; +} + +.token.cdata { + color: #808080; +} + +.token.attr-value { + color: #ce9178; +} + +attr-value.punctuation { + color: #ce9178; +} + +attr-value.punctuation.attr-equals { + color: #d4d4d4; +} + +.token.namespace { + color: #4ec9b0; +} + +pre[class*="language-javascript"] { + color: #9cdcfe; +} + +code[class*="language-javascript"] { + color: #9cdcfe; +} + +pre[class*="language-jsx"] { + color: #9cdcfe; +} + +code[class*="language-jsx"] { + color: #9cdcfe; +} + +pre[class*="language-typescript"] { + color: #9cdcfe; +} + +code[class*="language-typescript"] { + color: #9cdcfe; +} + +pre[class*="language-tsx"] { + color: #9cdcfe; +} + +code[class*="language-tsx"] { + color: #9cdcfe; +} + +pre[class*="language-css"] { + color: #ce9178; +} + +code[class*="language-css"] { + color: #ce9178; +} + +pre[class*="language-html"] { + color: #d4d4d4; +} + +code[class*="language-html"] { + color: #d4d4d4; +} + +.language-regex .token.anchor { + color: #dcdcaa; +} + +.language-html .token.punctuation { + color: #808080; +} + +pre[class*="language-"] > code[class*="language-"] { + position: relative; + z-index: 1; +} + +.line-highlight.line-highlight { + box-shadow: inset 5px 0 0 #f7d87c; + z-index: 0; +}