From 5359c7a4be45adbadb3eb3e449a5f3cbb40e53d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:48:05 +0000 Subject: [PATCH] Implement syntax highlighting for markdown code blocks using Prism.js Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com> --- package-lock.json | 17 ++++ package.json | 2 + src/app/globals.css | 77 ++++++++++++++++ src/services/htmlMarkdownUtils.ts | 45 +++++++++ src/services/tests/syntaxHighlighting.test.ts | 92 +++++++++++++++++++ 5 files changed, 233 insertions(+) create mode 100644 src/services/tests/syntaxHighlighting.test.ts diff --git a/package-lock.json b/package-lock.json index 7d1b5de..480dfca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@trpc/react-query": "11.4.3", "@trpc/server": "11.4.3", "@trpc/tanstack-react-query": "^11.4.3", + "@types/prismjs": "^1.26.5", "@types/ws": "^8.18.1", "@typescript-eslint/parser": "^8.37.0", "chokidar": "^4.0.3", @@ -27,6 +28,7 @@ "marked-katex-extension": "^5.1.5", "mcp-handler": "^1.0.0", "next": "^15.3.5", + "prismjs": "^1.30.0", "react": "^19.1.0", "react-dom": "^19.1.0", "socket.io": "^4.8.1", @@ -2861,6 +2863,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -8009,6 +8017,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 77d4429..77608d7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@trpc/react-query": "11.4.3", "@trpc/server": "11.4.3", "@trpc/tanstack-react-query": "^11.4.3", + "@types/prismjs": "^1.26.5", "@types/ws": "^8.18.1", "@typescript-eslint/parser": "^8.37.0", "chokidar": "^4.0.3", @@ -32,6 +33,7 @@ "marked-katex-extension": "^5.1.5", "mcp-handler": "^1.0.0", "next": "^15.3.5", + "prismjs": "^1.30.0", "react": "^19.1.0", "react-dom": "^19.1.0", "socket.io": "^4.8.1", diff --git a/src/app/globals.css b/src/app/globals.css index 8431c64..0c5cc99 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -76,6 +76,83 @@ code { @apply text-wrap; } +/* Syntax highlighting styles for code blocks */ +pre code { + @apply block bg-transparent p-0 m-0 rounded-none; +} + +pre { + @apply bg-gray-900 p-4 rounded-lg overflow-x-auto font-mono text-sm leading-relaxed; +} + +/* Prism.js syntax highlighting styles */ +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + @apply text-gray-500; +} + +.token.punctuation { + @apply text-gray-400; +} + +.token.property, +.token.tag, +.token.constant, +.token.symbol, +.token.deleted { + @apply text-red-400; +} + +.token.boolean, +.token.number { + @apply text-orange-400; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + @apply text-green-400; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string, +.token.variable { + @apply text-yellow-400; +} + +.token.atrule, +.token.attr-value, +.token.function, +.token.class-name { + @apply text-blue-400; +} + +.token.keyword { + @apply text-purple-400; +} + +.token.regex, +.token.important { + @apply text-orange-400; +} + +.token.important, +.token.bold { + @apply font-bold; +} + +.token.italic { + @apply italic; +} + p { @apply mb-3; } diff --git a/src/services/htmlMarkdownUtils.ts b/src/services/htmlMarkdownUtils.ts index d29d7ad..95a1ec2 100644 --- a/src/services/htmlMarkdownUtils.ts +++ b/src/services/htmlMarkdownUtils.ts @@ -3,6 +3,25 @@ import { marked, MarkedExtension } from "marked"; import DOMPurify from "isomorphic-dompurify"; import { LocalCourseSettings } from "@/models/local/localCourseSettings"; import markedKatex from "marked-katex-extension"; +import Prism from "prismjs"; +import "prismjs/components/prism-javascript"; +import "prismjs/components/prism-typescript"; +import "prismjs/components/prism-jsx"; +import "prismjs/components/prism-tsx"; +import "prismjs/components/prism-python"; +import "prismjs/components/prism-java"; +import "prismjs/components/prism-c"; +import "prismjs/components/prism-cpp"; +import "prismjs/components/prism-csharp"; +import "prismjs/components/prism-go"; +import "prismjs/components/prism-rust"; +import "prismjs/components/prism-sql"; +import "prismjs/components/prism-json"; +import "prismjs/components/prism-yaml"; +import "prismjs/components/prism-bash"; +import "prismjs/components/prism-markdown"; +import "prismjs/components/prism-css"; +import "prismjs/components/prism-markup"; const mermaidExtension = { name: "mermaid", @@ -38,6 +57,32 @@ marked.use( marked.use({ extensions: [mermaidExtension] }); +// Configure syntax highlighting for code blocks +marked.use({ + renderer: { + code(token: any) { + const code = token.text; + const language = token.lang || ''; + + if (language && Prism.languages[language]) { + try { + const highlighted = Prism.highlight(code, Prism.languages[language], language); + return `
${highlighted}
`; + } catch (error) { + console.warn(`Syntax highlighting failed for language: ${language}`, error); + // Fallback to plain code block with escaped HTML + const escapedCode = code.replace(/&/g, '&').replace(//g, '>'); + return `
${escapedCode}
`; + } + } + + // Fallback to plain code block with escaped HTML + const escapedCode = code.replace(/&/g, '&').replace(//g, '>'); + return `
${escapedCode}
`; + } + } +}); + export function extractImageSources(htmlString: string) { const srcUrls = []; const regex = /]+src=["']?([^"'>]+)["']?/g; diff --git a/src/services/tests/syntaxHighlighting.test.ts b/src/services/tests/syntaxHighlighting.test.ts new file mode 100644 index 0000000..40fa820 --- /dev/null +++ b/src/services/tests/syntaxHighlighting.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from "vitest"; +import { markdownToHtmlNoImages } from "../htmlMarkdownUtils"; + +describe("Syntax Highlighting Tests", () => { + it("should highlight JavaScript code blocks", () => { + const markdown = `\`\`\`javascript +function hello() { + console.log("Hello, World!"); +} +\`\`\``; + + const html = markdownToHtmlNoImages(markdown); + + // Check that the code block has proper structure + expect(html).toContain('
');
+    expect(html).toContain('');
+    expect(html).toContain('function');
+    expect(html).toContain('console'); // Just check for console, not console.log
+    
+    // Check that syntax highlighting tokens are present
+    expect(html).toContain(' {
+    const markdown = `\`\`\`typescript
+interface User {
+  name: string;
+  age: number;
+}
+\`\`\``;
+
+    const html = markdownToHtmlNoImages(markdown);
+    
+    expect(html).toContain('
');
+    expect(html).toContain('');
+    expect(html).toContain('interface');
+    expect(html).toContain('string');
+    expect(html).toContain('number');
+    expect(html).toContain(' {
+    const markdown = `\`\`\`python
+def hello_world():
+    print("Hello, World!")
+\`\`\``;
+
+    const html = markdownToHtmlNoImages(markdown);
+    
+    expect(html).toContain('
');
+    expect(html).toContain('');
+    expect(html).toContain('def');
+    expect(html).toContain('print');
+    expect(html).toContain(' {
+    const markdown = `\`\`\`unknownlang
+some code here
+\`\`\``;
+
+    const html = markdownToHtmlNoImages(markdown);
+    
+    // Should fallback to plain code block
+    expect(html).toContain('
');
+    expect(html).toContain('some code here');
+    expect(html).not.toContain(' {
+    const markdown = `\`\`\`
+plain code block
+\`\`\``;
+
+    const html = markdownToHtmlNoImages(markdown);
+    
+    // Should fallback to plain code block
+    expect(html).toContain('
');
+    expect(html).toContain('plain code block');
+    expect(html).not.toContain(' {
+    const markdown = `Here is some \`inline code\` in a paragraph.`;
+
+    const html = markdownToHtmlNoImages(markdown);
+    
+    // Inline code should not be affected by syntax highlighting
+    expect(html).toContain('inline code');
+    expect(html).not.toContain('
');
+  });
+});
\ No newline at end of file