mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
Implement syntax highlighting for markdown code blocks using Prism.js
Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com>
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 `<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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return `<pre><code>${escapedCode}</code></pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to plain code block with escaped HTML
|
||||
const escapedCode = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return `<pre><code>${escapedCode}</code></pre>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function extractImageSources(htmlString: string) {
|
||||
const srcUrls = [];
|
||||
const regex = /<img[^>]+src=["']?([^"'>]+)["']?/g;
|
||||
|
||||
92
src/services/tests/syntaxHighlighting.test.ts
Normal file
92
src/services/tests/syntaxHighlighting.test.ts
Normal 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>');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user