3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
5359c7a4be Implement syntax highlighting for markdown code blocks using Prism.js
Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com>
2025-07-18 15:48:05 +00:00
copilot-swe-agent[bot]
6db2c5b66b Initial exploration and planning for syntax highlighting
Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com>
2025-07-18 15:40:53 +00:00
copilot-swe-agent[bot]
4cdafb5ffc Initial plan 2025-07-18 15:32:08 +00:00
5 changed files with 10428 additions and 0 deletions

10212
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@
"@trpc/react-query": "11.4.3", "@trpc/react-query": "11.4.3",
"@trpc/server": "11.4.3", "@trpc/server": "11.4.3",
"@trpc/tanstack-react-query": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3",
"@types/prismjs": "^1.26.5",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@typescript-eslint/parser": "^8.37.0", "@typescript-eslint/parser": "^8.37.0",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
@@ -32,6 +33,7 @@
"marked-katex-extension": "^5.1.5", "marked-katex-extension": "^5.1.5",
"mcp-handler": "^1.0.0", "mcp-handler": "^1.0.0",
"next": "^15.3.5", "next": "^15.3.5",
"prismjs": "^1.30.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",

View File

@@ -76,6 +76,83 @@ code {
@apply text-wrap; @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 { p {
@apply mb-3; @apply mb-3;
} }

View File

@@ -3,6 +3,25 @@ import { marked, MarkedExtension } from "marked";
import DOMPurify from "isomorphic-dompurify"; import DOMPurify from "isomorphic-dompurify";
import { LocalCourseSettings } from "@/models/local/localCourseSettings"; import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import markedKatex from "marked-katex-extension"; 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 = { const mermaidExtension = {
name: "mermaid", name: "mermaid",
@@ -38,6 +57,32 @@ marked.use(
marked.use({ extensions: [mermaidExtension] }); 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 `<pre class="language-${language}"><code class="language-${language}">${highlighted}</code></pre>`;
} catch (error) {
console.warn(`Syntax highlighting failed for language: ${language}`, error);
// Fallback to plain code block with escaped HTML
const escapedCode = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return `<pre><code>${escapedCode}</code></pre>`;
}
}
// Fallback to plain code block with escaped HTML
const escapedCode = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return `<pre><code>${escapedCode}</code></pre>`;
}
}
});
export function extractImageSources(htmlString: string) { export function extractImageSources(htmlString: string) {
const srcUrls = []; const srcUrls = [];
const regex = /<img[^>]+src=["']?([^"'>]+)["']?/g; const regex = /<img[^>]+src=["']?([^"'>]+)["']?/g;

View File

@@ -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('<pre class="language-javascript">');
expect(html).toContain('<code class="language-javascript">');
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('<span class="token');
});
it("should highlight TypeScript code blocks", () => {
const markdown = `\`\`\`typescript
interface User {
name: string;
age: number;
}
\`\`\``;
const html = markdownToHtmlNoImages(markdown);
expect(html).toContain('<pre class="language-typescript">');
expect(html).toContain('<code class="language-typescript">');
expect(html).toContain('interface');
expect(html).toContain('string');
expect(html).toContain('number');
expect(html).toContain('<span class="token');
});
it("should highlight Python code blocks", () => {
const markdown = `\`\`\`python
def hello_world():
print("Hello, World!")
\`\`\``;
const html = markdownToHtmlNoImages(markdown);
expect(html).toContain('<pre class="language-python">');
expect(html).toContain('<code class="language-python">');
expect(html).toContain('def');
expect(html).toContain('print');
expect(html).toContain('<span class="token');
});
it("should handle unknown languages gracefully", () => {
const markdown = `\`\`\`unknownlang
some code here
\`\`\``;
const html = markdownToHtmlNoImages(markdown);
// Should fallback to plain code block
expect(html).toContain('<pre><code>');
expect(html).toContain('some code here');
expect(html).not.toContain('<span class="token');
});
it("should handle code blocks without language specification", () => {
const markdown = `\`\`\`
plain code block
\`\`\``;
const html = markdownToHtmlNoImages(markdown);
// Should fallback to plain code block
expect(html).toContain('<pre><code>');
expect(html).toContain('plain code block');
expect(html).not.toContain('<span class="token');
});
it("should preserve inline code formatting", () => {
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('<code>inline code</code>');
expect(html).not.toContain('<pre>');
});
});