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 = /');
+ 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