Compare commits

...

2 Commits

Author SHA1 Message Date
69be84e5ab Fix TypeScript compilation errors
- Remove deprecated marked options (sanitize, headerIds, mangle)
- Fix undefined codeExample and diagramExample references in templateRenderer
- Replace with additional markdown sample content
- Ensure clean TypeScript build with no errors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 21:20:09 -05:00
84b2d233a7 Add JavaScript syntax highlighting with Highlight.js
- 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 <noreply@anthropic.com>
2025-08-21 21:18:11 -05:00
12 changed files with 444 additions and 45 deletions

10
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -14,5 +14,5 @@
"hasMasterSlide": true
}
},
"generated": "2025-08-22T01:40:33.956Z"
"generated": "2025-08-22T02:19:52.970Z"
}

View File

@ -1,8 +1,24 @@
<div class="fade-in layout-content-slide">
<h1 class="slot title-slot" data-slot="title" data-placeholder="Slide Title" data-required>
{{title}}
</h1>
<div id="code" class="slot content-area" data-slot="content" data-placeholder="Your content here..." data-multiline="true">
</div>
<div class="slide layout-code-slide">
<h1 class="slot title-slot"
data-slot="title"
data-type="title"
data-placeholder="Code Example Title"
data-required>
{{title}}
</h1>
<pre class="slot code-content"
data-slot="code"
data-type="code"
data-language="javascript"
data-placeholder="Enter your JavaScript code here..."
data-multiline="true">{{code}}</pre>
<div class="slot notes-content"
data-slot="notes"
data-type="text"
data-placeholder="Optional code explanation..."
data-multiline="true">
{{notes}}
</div>
</div>

View File

@ -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;
}
}

View File

@ -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);
});

View File

@ -45,17 +45,32 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
);
}
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 (
<textarea
id={slot.id}
value={slideContent[slot.id] || ''}
onChange={(e) => onSlotContentChange(slot.id, e.target.value)}
placeholder={slot.type === 'markdown'
? slot.placeholder || 'Enter markdown content (e.g., **bold**, *italic*, - list items)'
: slot.placeholder || `Enter ${slot.id}`}
className={`field-textarea ${slot.type === 'markdown' ? 'markdown-field' : ''}`}
rows={slot.type === 'markdown' ? 6 : 4}
placeholder={getPlaceholder()}
className={`field-textarea ${slot.type === 'code' ? 'code-field' : slot.type === 'markdown' ? 'markdown-field' : ''}`}
rows={getRows()}
style={slot.type === 'code' ? { fontFamily: 'var(--theme-font-code)' } : undefined}
/>
);
}

View File

@ -1,5 +1,6 @@
import type { SlideLayout } from '../../types/theme.ts';
import { renderSlideMarkdown, isMarkdownContent } from '../../utils/markdownProcessor.ts';
import { highlightCode } from '../../utils/codeHighlighter.ts';
// Helper function to render template with actual content
export const renderTemplateWithContent = (layout: SlideLayout, content: Record<string, string>): string => {
@ -9,13 +10,20 @@ export const renderTemplateWithContent = (layout: SlideLayout, content: Record<s
Object.entries(content).forEach(([slotId, value]) => {
const placeholder = 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(value || ''));
const processedValue = shouldProcessAsMarkdown ?
renderSlideMarkdown(value || '', slot?.type) : (value || '');
let processedValue = value || '';
// Process based on slot type
if (slot?.type === 'code') {
// Handle code highlighting
const language = slot.attributes?.['data-language'] || 'javascript';
processedValue = highlightCode(value || '', language);
} else if (slot?.type === 'markdown' || (slot?.type === 'text' && isMarkdownContent(value || ''))) {
// Handle markdown processing
processedValue = renderSlideMarkdown(value || '', slot?.type);
}
rendered = rendered.replace(placeholder, processedValue);
});

View File

@ -0,0 +1,64 @@
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
/**
* Code syntax highlighter for slide content
* Uses Highlight.js for reliable browser-based highlighting
*/
// Register supported languages
hljs.registerLanguage('javascript', javascript);
// Track which languages are available
const SUPPORTED_LANGUAGES = ['javascript', 'js'];
/**
* Highlights code with syntax highlighting
* @param code - The code string to highlight
* @param language - The programming language (optional)
* @returns HTML string with syntax highlighting
*/
export const highlightCode = (code: string, language?: string): string => {
if (!code || typeof code !== 'string') {
return '';
}
// Normalize language names
const normalizedLang = language?.toLowerCase();
const targetLang = normalizedLang === 'js' ? 'javascript' : normalizedLang;
// Try to highlight if language is supported
if (targetLang && SUPPORTED_LANGUAGES.includes(targetLang)) {
try {
const result = hljs.highlight(code, { language: targetLang });
return result.value;
} catch (error) {
console.warn(`Syntax highlighting failed for ${targetLang}:`, error);
}
}
// Fallback to escaped plain text
const escapedCode = code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return escapedCode;
};
/**
* Gets list of supported languages
*/
export const getSupportedLanguages = (): string[] => {
return [...SUPPORTED_LANGUAGES];
};
/**
* Checks if a language is supported for highlighting
*/
export const isLanguageSupported = (language?: string): boolean => {
if (!language) return false;
const normalizedLang = language.toLowerCase();
const targetLang = normalizedLang === 'js' ? 'javascript' : normalizedLang;
return SUPPORTED_LANGUAGES.includes(targetLang);
};

View File

@ -47,7 +47,9 @@ const DEFAULT_SLIDE_CONFIG: Required<SanitizeConfig> = {
// Headings
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
// Quotes
'blockquote', 'cite'
'blockquote', 'cite',
// Code elements
'pre', 'code'
],
allowedAttributes: [
'class', 'id', 'style', 'data-*'

View File

@ -8,12 +8,8 @@ import DOMPurify from 'dompurify';
// Configure marked for slide-safe markdown
marked.setOptions({
// Disable HTML rendering for security
sanitize: false, // We'll handle sanitization with DOMPurify
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert line breaks to <br>
headerIds: false, // Don't generate header IDs
mangle: false, // Don't mangle email addresses
});
/**
@ -44,7 +40,9 @@ const PURIFY_CONFIG = {
// Table attributes
'colspan', 'rowspan',
// Link attributes (but we'll filter URLs)
'target', 'rel'
'target', 'rel',
// Code highlighting attributes
'style' // Allow style attribute for syntax highlighting
],
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'button'],

View File

@ -64,9 +64,9 @@ const SAMPLE_CONTENT = {
markdown: [
SAMPLE_MARKDOWN_CONTENT.content,
SAMPLE_MARKDOWN_CONTENT.list,
SAMPLE_MARKDOWN_CONTENT.codeExample,
SAMPLE_MARKDOWN_CONTENT.diagramExample,
'## Key Points\n\n- **Important**: Focus on *customer needs*\n- Use `data-driven` decisions\n- > Success comes from teamwork'
'## Key Points\n\n- **Important**: Focus on *customer needs*\n- Use `data-driven` decisions\n- > Success comes from teamwork',
'### Implementation Steps\n\n1. **Analysis** - Review current metrics\n2. **Strategy** - Define clear objectives\n3. **Execution** - Deploy with *precision*\n\n> Remember: `quality` over quantity',
'## Technical Overview\n\n- Modern **JavaScript** frameworks\n- *Responsive* design principles\n- `API-first` architecture\n\n**Next Steps**: Begin development phase'
]
};