From 84b2d233a777c1362cc6234d3153fa2b9856e8d6 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Thu, 21 Aug 2025 21:18:11 -0500 Subject: [PATCH] Add JavaScript syntax highlighting with Highlight.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement code highlighting utility using Highlight.js library - Add dedicated code-slide layout with proper pre/code structure - Update HTML sanitizer to allow pre and code elements - Add comprehensive VS Code dark theme syntax colors - Fix whitespace preservation in highlighted code blocks - Support code slot type in template rendering system - Add code-specific styling and editor improvements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package-lock.json | 10 + package.json | 1 + public/themes-manifest.json | 2 +- public/themes/default/layouts/code-slide.html | 30 +- public/themes/default/style.css | 301 +++++++++++++++++- .../presentations/PresentationMode.tsx | 18 +- src/components/slide-editor/ContentEditor.tsx | 27 +- src/components/slide-editor/utils.ts | 18 +- src/utils/codeHighlighter.ts | 64 ++++ src/utils/htmlSanitizer.ts | 4 +- src/utils/markdownProcessor.ts | 5 +- 11 files changed, 441 insertions(+), 39 deletions(-) create mode 100644 src/utils/codeHighlighter.ts diff --git a/package-lock.json b/package-lock.json index 2a2a194..255a41d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/prismjs": "^1.26.5", "dompurify": "^3.2.6", + "highlight.js": "^11.11.1", "loglevel": "^1.9.2", "marked": "^16.2.0", "mermaid": "^11.10.0", @@ -3421,6 +3422,15 @@ "node": ">=8" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/package.json b/package.json index ce2e6e2..2a9d14b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@types/prismjs": "^1.26.5", "dompurify": "^3.2.6", + "highlight.js": "^11.11.1", "loglevel": "^1.9.2", "marked": "^16.2.0", "mermaid": "^11.10.0", diff --git a/public/themes-manifest.json b/public/themes-manifest.json index 9535be5..c9b9ae4 100644 --- a/public/themes-manifest.json +++ b/public/themes-manifest.json @@ -14,5 +14,5 @@ "hasMasterSlide": true } }, - "generated": "2025-08-22T01:40:33.956Z" + "generated": "2025-08-22T02:15:00.761Z" } \ No newline at end of file diff --git a/public/themes/default/layouts/code-slide.html b/public/themes/default/layouts/code-slide.html index d4945d9..fb71d0f 100644 --- a/public/themes/default/layouts/code-slide.html +++ b/public/themes/default/layouts/code-slide.html @@ -1,8 +1,24 @@ -
-

- {{title}} -

-
- -
+
+

+ {{title}} +

+ +
{{code}}
+ +
+ {{notes}} +
\ No newline at end of file diff --git a/public/themes/default/style.css b/public/themes/default/style.css index 3f017e9..48a3619 100644 --- a/public/themes/default/style.css +++ b/public/themes/default/style.css @@ -439,25 +439,302 @@ background: rgba(149, 116, 235, 0.1); } +/* JavaScript syntax highlighting with Prism.js */ +.language-javascript, +.code-block, +.markdown-content pre, +.markdown-content code { + background: #1e1e1e; + border: 1px solid var(--theme-secondary); + border-radius: 8px; + padding: 1.5rem; + margin: 1.5em 0; + overflow-x: auto; + font-family: var(--theme-font-code); + font-size: clamp(0.75rem, 1.5vw, 0.9rem); + line-height: 1.4; + position: relative; + white-space: pre-wrap; + display: block; +} + +.language-javascript code, +.code-block code { + background: none; + padding: 0; + color: #e6e6e6; + font-family: inherit; +} + +/* JavaScript-specific Prism tokens */ +.language-javascript .token.comment, +.language-javascript .token.prolog, +.language-javascript .token.doctype, +.language-javascript .token.cdata, +.markdown-content .token.comment, +.markdown-content .token.prolog, +.markdown-content .token.doctype, +.markdown-content .token.cdata { + color: #6a9955; + font-style: italic; +} + +.language-javascript .token.punctuation { + color: #d4d4d4; +} + +.language-javascript .token.property, +.language-javascript .token.tag, +.language-javascript .token.boolean, +.language-javascript .token.number, +.language-javascript .token.constant, +.language-javascript .token.symbol, +.language-javascript .token.deleted { + color: #b5cea8; +} + +.language-javascript .token.selector, +.language-javascript .token.attr-name, +.language-javascript .token.string, +.language-javascript .token.char, +.language-javascript .token.builtin, +.language-javascript .token.inserted { + color: #ce9178; +} + +.language-javascript .token.operator, +.language-javascript .token.entity, +.language-javascript .token.url { + color: #d4d4d4; +} + +.language-javascript .token.atrule, +.language-javascript .token.attr-value, +.language-javascript .token.keyword { + color: #569cd6; +} + +.language-javascript .token.function, +.language-javascript .token.class-name { + color: #dcdcaa; +} + +.language-javascript .token.regex, +.language-javascript .token.important, +.language-javascript .token.variable { + color: #d16969; +} + +.language-javascript .token.important, +.language-javascript .token.bold { + font-weight: bold; +} + +.language-javascript .token.italic { + font-style: italic; +} + +/* Universal token styles for markdown content */ +.markdown-content .token.punctuation { color: #d4d4d4; } + +.markdown-content .token.property, +.markdown-content .token.tag, +.markdown-content .token.boolean, +.markdown-content .token.number, +.markdown-content .token.constant, +.markdown-content .token.symbol, +.markdown-content .token.deleted { color: #b5cea8; } + +.markdown-content .token.selector, +.markdown-content .token.attr-name, +.markdown-content .token.string, +.markdown-content .token.char, +.markdown-content .token.builtin, +.markdown-content .token.inserted { color: #ce9178; } + +.markdown-content .token.operator, +.markdown-content .token.entity, +.markdown-content .token.url { color: #d4d4d4; } + +.markdown-content .token.atrule, +.markdown-content .token.attr-value, +.markdown-content .token.keyword { color: #569cd6; } + +.markdown-content .token.function, +.markdown-content .token.class-name { color: #dcdcaa; } + +.markdown-content .token.regex, +.markdown-content .token.important, +.markdown-content .token.variable { color: #d16969; } + +.markdown-content .token.parameter { color: #9cdcfe; } + +.markdown-content .token.important, +.markdown-content .token.bold { font-weight: bold; } + +.markdown-content .token.italic { font-style: italic; } + +/* Language label */ +.language-javascript::before { + content: 'JavaScript'; + position: absolute; + top: 0.5rem; + right: 1rem; + font-size: 0.75rem; + color: var(--theme-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + opacity: 0.7; +} + +/* Code slide layout */ +.layout-code-slide, +.slide-container .layout-code-slide { + justify-content: flex-start; + align-items: stretch; + text-align: left; + padding: 2rem; +} + +.layout-code-slide .slot[data-slot="title"] { + font-size: clamp(1.5rem, 4vw, 2rem); + margin-bottom: 1.5rem; + text-align: center; + color: var(--theme-text); +} + +.layout-code-slide .slot[data-slot="code"] { + flex: 1; + margin-bottom: 1rem; +} + +.layout-code-slide .slot[data-slot="notes"] { + font-size: clamp(0.9rem, 2vw, 1rem); + color: var(--theme-text-secondary); + line-height: 1.5; + text-align: left; +} + +/* Code slot styling - now targets the pre element */ +pre.slot[data-type="code"], +pre.code-content { + background: #1e1e1e !important; + border: 1px solid var(--theme-secondary); + border-radius: 8px; + padding: 1.5rem; + margin: 0; + overflow-x: auto; + font-family: var(--theme-font-code); + font-size: clamp(0.75rem, 1.5vw, 0.9rem); + line-height: 1.4; + color: #e6e6e6; + position: relative; + white-space: pre !important; + display: block !important; + text-align: left !important; +} + +/* Override any inherited slot styles for code elements */ +pre.slot[data-type="code"] * { + white-space: inherit; + display: inline; +} + +/* Ensure spans from highlighting don't break lines */ +pre.slot[data-type="code"] .hljs-keyword, +pre.slot[data-type="code"] .hljs-string, +pre.slot[data-type="code"] .hljs-number, +pre.slot[data-type="code"] .hljs-comment, +pre.slot[data-type="code"] .hljs-function, +pre.slot[data-type="code"] .hljs-variable, +pre.slot[data-type="code"] .hljs-punctuation, +pre.slot[data-type="code"] .hljs-operator, +pre.slot[data-type="code"] span { + display: inline !important; + white-space: inherit !important; +} + +pre.slot[data-type="code"]::before, +pre.code-content::before { + content: 'JavaScript'; + position: absolute; + top: 0.5rem; + right: 1rem; + font-size: 0.75rem; + color: var(--theme-text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + opacity: 0.7; + font-family: var(--theme-font-body); +} + +/* Highlight.js token colors (VS Code Dark theme) */ +.hljs-keyword, +.hljs-built_in, +.hljs-type, +.hljs-literal { color: #569cd6; } + +.hljs-string, +.hljs-regexp { color: #ce9178; } + +.hljs-number { color: #b5cea8; } + +.hljs-comment { + color: #6a9955; + font-style: italic; +} + +.hljs-function .hljs-title, +.hljs-title.function_ { color: #dcdcaa; } + +.hljs-variable, +.hljs-params { color: #9cdcfe; } + +.hljs-punctuation, +.hljs-operator { color: #d4d4d4; } + +.hljs-attr, +.hljs-property { color: #92c5f7; } + +.hljs-tag, +.hljs-name { color: #569cd6; } + +.hljs-attribute { color: #9cdcfe; } + +/* Code editor textarea styling */ +.field-textarea.code-field { + font-family: var(--theme-font-code); + font-size: 0.9rem; + line-height: 1.4; + background: #1a1a1a; + color: #e6e6e6; + border: 1px solid var(--theme-secondary); + border-radius: 4px; + padding: 0.75rem; + white-space: pre; + tab-size: 2; +} + +.field-textarea.code-field::placeholder { + color: #6a6a6a; + font-style: italic; +} + /* Responsive adjustments for enhanced features */ @media (max-width: 768px) { - .slide-code { + .layout-code-slide { + padding: 1rem; + } + + .hljs-code, + .hljs-code pre, + .hljs-code code { padding: 1rem; font-size: 0.8rem; } - .slide-code::before { + .hljs-code::before { font-size: 0.7rem; } - - .mermaid-container { - padding: 0.5rem; - margin: 1rem 0; - } - - .slide-callout { - padding: 0.75rem 1rem; - margin: 1rem 0; - } } diff --git a/src/components/presentations/PresentationMode.tsx b/src/components/presentations/PresentationMode.tsx index 4abfd96..fcf7d7a 100644 --- a/src/components/presentations/PresentationMode.tsx +++ b/src/components/presentations/PresentationMode.tsx @@ -4,6 +4,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts'; +import { highlightCode } from '../../utils/codeHighlighter.ts'; import { loggers } from '../../utils/logger.ts'; import './PresentationMode.css'; import {usePresentationLoader} from "./hooks/usePresentationLoader.ts"; @@ -66,13 +67,20 @@ export const PresentationMode: React.FC = () => { Object.entries(slide.content).forEach(([slotId, content]) => { const regex = new RegExp(`\\{\\{${slotId}\\}\\}`, 'g'); - // Find the corresponding slot to determine if it should be processed as markdown + // Find the corresponding slot to determine processing type const slot = layout.slots.find(s => s.id === slotId); - const shouldProcessAsMarkdown = slot?.type === 'markdown' || - (slot?.type === 'text' && isMarkdownContent(content)); - const processedContent = shouldProcessAsMarkdown ? - renderSlideMarkdown(content, slot?.type) : content; + let processedContent = content; + + // Process based on slot type + if (slot?.type === 'code') { + // Handle code highlighting + const language = slot.attributes?.['data-language'] || 'javascript'; + processedContent = highlightCode(content, language); + } else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(content))) { + // Handle markdown processing + processedContent = renderSlideMarkdown(content, slot?.type); + } renderedTemplate = renderedTemplate.replace(regex, processedContent); }); diff --git a/src/components/slide-editor/ContentEditor.tsx b/src/components/slide-editor/ContentEditor.tsx index dc26602..7ac6eb6 100644 --- a/src/components/slide-editor/ContentEditor.tsx +++ b/src/components/slide-editor/ContentEditor.tsx @@ -45,17 +45,32 @@ export const ContentEditor: React.FC = ({ ); } - if (slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) { + if (slot.type === 'code' || slot.type === 'markdown' || (slot.type === 'text' && slot.id.includes('content'))) { + const getPlaceholder = () => { + if (slot.type === 'code') { + return slot.placeholder || 'Enter your JavaScript code here...'; + } + if (slot.type === 'markdown') { + return slot.placeholder || 'Enter markdown content (e.g., **bold**, *italic*, - list items)'; + } + return slot.placeholder || `Enter ${slot.id}`; + }; + + const getRows = () => { + if (slot.type === 'code') return 8; + if (slot.type === 'markdown') return 6; + return 4; + }; + return (