- MarkdownConverter
-
+ MarkdownConverter
+ Error: Required libraries (marked/DOMPurify) not loaded. Check internet connection.
'; - return; - } - const html = marked.parse(tab.content); - const sanitizedHtml = DOMPurify.sanitize(html); - preview.innerHTML = sanitizedHtml; - - // Render math expressions if KaTeX is available - if (window.katex && window.renderMathInElement) { - try { - window.renderMathInElement(preview, { - delimiters: [ - {left: '$$', right: '$$', display: true}, - {left: '$', right: '$', display: false}, - {left: '\\[', right: '\\]', display: true}, - {left: '\\(', right: '\\)', display: false} - ] - }); - } catch (mathError) { - console.warn('Math rendering error:', mathError); - } - } - - // Render Mermaid diagrams if Mermaid is available - if (window.mermaid) { - try { - // Find all code blocks with language-mermaid class - const mermaidBlocks = preview.querySelectorAll('pre code.language-mermaid'); - mermaidBlocks.forEach((block, index) => { - const code = block.textContent; - const pre = block.parentElement; - - // Create a div for mermaid rendering - const mermaidDiv = document.createElement('div'); - mermaidDiv.className = 'mermaid'; - mermaidDiv.setAttribute('data-processed', 'true'); - mermaidDiv.textContent = code; - - // Replace the pre element with the mermaid div - pre.parentElement.replaceChild(mermaidDiv, pre); - }); - - // Initialize Mermaid with dark theme support - const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default'; - mermaid.initialize({ - startOnLoad: false, - theme: theme, - securityLevel: 'loose' - }); - - // Render all mermaid diagrams - mermaid.run({ - querySelector: '.mermaid:not([data-rendered])' - }).then(() => { - // Mark as rendered - preview.querySelectorAll('.mermaid').forEach(el => { - el.setAttribute('data-rendered', 'true'); - }); - }); - } catch (mermaidError) { - console.warn('Mermaid rendering error:', mermaidError); - } - } - } catch (error) { - console.error('Error rendering preview:', error); - preview.innerHTML = 'Error rendering preview. Please check your markdown syntax.
'; - } - } - - updatePreviewVisibility() { - document.querySelectorAll('.tab-content').forEach(content => { - const previewPane = content.querySelector('.pane:last-child'); - const editorPane = content.querySelector('.pane:first-child'); - - if (this.isPreviewVisible) { - previewPane.classList.remove('hidden'); - editorPane.classList.remove('full-width'); - } else { - previewPane.classList.add('hidden'); - editorPane.classList.add('full-width'); - } - }); - } - - updateLineNumbers() { - const editor = document.getElementById(`editor-${this.activeTabId}`); - const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`); - - if (!editor || !lineNumbers) return; - - if (this.showLineNumbers) { - const lines = editor.value.split('\n'); - lineNumbers.innerHTML = lines.map((_, i) => - `Error: Required libraries (marked/DOMPurify) not loaded. Check internet connection.
'; + return; + } + const html = marked.parse(tab.content); + const sanitizedHtml = DOMPurify.sanitize(html); + preview.innerHTML = sanitizedHtml; + + // Render math expressions if KaTeX is available + if (window.katex && window.renderMathInElement) { + try { + window.renderMathInElement(preview, { + delimiters: [ + {left: '$$', right: '$$', display: true}, + {left: '$', right: '$', display: false}, + {left: '\\[', right: '\\]', display: true}, + {left: '\\(', right: '\\)', display: false} + ] + }); + } catch (mathError) { + console.warn('Math rendering error:', mathError); + } + } + + // Render Mermaid diagrams if Mermaid is available + if (window.mermaid) { + try { + // Find all code blocks with language-mermaid class + const mermaidBlocks = preview.querySelectorAll('pre code.language-mermaid'); + mermaidBlocks.forEach((block, index) => { + const code = block.textContent; + const pre = block.parentElement; + + // Create a div for mermaid rendering + const mermaidDiv = document.createElement('div'); + mermaidDiv.className = 'mermaid'; + mermaidDiv.setAttribute('data-processed', 'true'); + mermaidDiv.textContent = code; + + // Replace the pre element with the mermaid div + pre.parentElement.replaceChild(mermaidDiv, pre); + }); + + // Initialize Mermaid with dark theme support + const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default'; + mermaid.initialize({ + startOnLoad: false, + theme: theme, + securityLevel: 'loose' + }); + + // Render all mermaid diagrams + mermaid.run({ + querySelector: '.mermaid:not([data-rendered])' + }).then(() => { + // Mark as rendered + preview.querySelectorAll('.mermaid').forEach(el => { + el.setAttribute('data-rendered', 'true'); + }); + }); + } catch (mermaidError) { + console.warn('Mermaid rendering error:', mermaidError); + } + } + } catch (error) { + console.error('Error rendering preview:', error); + preview.innerHTML = 'Error rendering preview. Please check your markdown syntax.
'; + } + } + + updatePreviewVisibility() { + document.querySelectorAll('.tab-content').forEach(content => { + const previewPane = content.querySelector('.pane:last-child'); + const editorPane = content.querySelector('.pane:first-child'); + + if (this.isPreviewVisible) { + previewPane.classList.remove('hidden'); + editorPane.classList.remove('full-width'); + } else { + previewPane.classList.add('hidden'); + editorPane.classList.add('full-width'); + } + }); + } + + updateLineNumbers() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`); + + if (!editor || !lineNumbers) return; + + if (this.showLineNumbers) { + const lines = editor.value.split('\n'); + lineNumbers.innerHTML = lines.map((_, i) => + `${text}
`), - setOptions: jest.fn() -}; - -// Mock DOMPurify -global.window.DOMPurify = { - sanitize: jest.fn((html) => html) -}; - -// Mock highlight.js -global.window.hljs = { - highlight: jest.fn((code, options) => ({ value: code })), - highlightAuto: jest.fn((code) => ({ value: code })), - getLanguage: jest.fn(() => true) -}; - -// Mock localStorage -const localStorageMock = { - getItem: jest.fn(), - setItem: jest.fn(), - removeItem: jest.fn(), - clear: jest.fn() -}; -global.localStorage = localStorageMock; - -// Console spy to catch unintended console logs in tests -const originalConsoleError = console.error; -console.error = (...args) => { - // Allow certain expected errors - const message = args[0]?.toString() || ''; - if (message.includes('Warning:') || message.includes('React')) { - return; - } - originalConsoleError.apply(console, args); -}; - -// Cleanup after each test -afterEach(() => { - jest.clearAllMocks(); -}); +/** + * Jest Test Setup + * Provides mocks and utilities for PanConverter tests + */ + +// Mock window.electronAPI for renderer tests +global.window = global.window || {}; +global.window.electronAPI = { + send: jest.fn(), + on: jest.fn(() => jest.fn()), // Returns cleanup function + once: jest.fn(), + invoke: jest.fn(() => Promise.resolve(null)), + removeAllListeners: jest.fn(), + file: { + save: jest.fn(), + saveCurrent: jest.fn(), + setCurrent: jest.fn(), + saveRecent: jest.fn(), + clearRecent: jest.fn(), + rendererReady: jest.fn() + }, + theme: { + get: jest.fn() + }, + print: { + doPrint: jest.fn() + }, + export: { + withOptions: jest.fn(), + spreadsheet: jest.fn() + }, + batch: { + convert: jest.fn(), + selectFolder: jest.fn() + }, + converter: { + convert: jest.fn(), + convertBatch: jest.fn() + }, + headerFooter: { + getSettings: jest.fn(), + saveSettings: jest.fn(), + browseLogo: jest.fn(), + saveLogo: jest.fn(), + clearLogo: jest.fn() + }, + page: { + getSettings: jest.fn(), + updateSettings: jest.fn(), + setCustomStartPage: jest.fn() + }, + pdf: { + processOperation: jest.fn(), + getPageCount: jest.fn(), + selectFolder: jest.fn() + } +}; + +// Mock marked library +global.window.marked = { + parse: jest.fn((text) => `${text}
`), + setOptions: jest.fn() +}; + +// Mock DOMPurify +global.window.DOMPurify = { + sanitize: jest.fn((html) => html) +}; + +// Mock highlight.js +global.window.hljs = { + highlight: jest.fn((code, options) => ({ value: code })), + highlightAuto: jest.fn((code) => ({ value: code })), + getLanguage: jest.fn(() => true) +}; + +// Mock localStorage +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn() +}; +global.localStorage = localStorageMock; + +// Console spy to catch unintended console logs in tests +const originalConsoleError = console.error; +console.error = (...args) => { + // Allow certain expected errors + const message = args[0]?.toString() || ''; + if (message.includes('Warning:') || message.includes('React')) { + return; + } + originalConsoleError.apply(console, args); +}; + +// Cleanup after each test +afterEach(() => { + jest.clearAllMocks(); +}); diff --git a/tests/utils.test.js b/tests/utils.test.js index 1bdc7df..c0c53a7 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -1,137 +1,137 @@ -/** - * Tests for Utility Functions - * Tests helper functions that can be extracted and tested - */ - -describe('Utility Functions', () => { - describe('parseCommand', () => { - // This function parses command strings into command and args - function parseCommand(cmdString) { - const parts = []; - let current = ''; - let inQuotes = false; - let quoteChar = ''; - - for (let i = 0; i < cmdString.length; i++) { - const char = cmdString[i]; - if ((char === '"' || char === "'") && !inQuotes) { - inQuotes = true; - quoteChar = char; - } else if (char === quoteChar && inQuotes) { - inQuotes = false; - quoteChar = ''; - } else if (char === ' ' && !inQuotes) { - if (current) { - parts.push(current); - current = ''; - } - } else { - current += char; - } - } - if (current) { - parts.push(current); - } - - return { - command: parts[0], - args: parts.slice(1) - }; - } - - test('should parse simple command', () => { - const result = parseCommand('pandoc input.md -o output.pdf'); - expect(result.command).toBe('pandoc'); - expect(result.args).toEqual(['input.md', '-o', 'output.pdf']); - }); - - test('should handle double-quoted paths', () => { - const result = parseCommand('pandoc "C:/path with spaces/file.md" -o output.pdf'); - expect(result.command).toBe('pandoc'); - expect(result.args).toEqual(['C:/path with spaces/file.md', '-o', 'output.pdf']); - }); - - test('should handle single-quoted paths', () => { - const result = parseCommand("pandoc 'file name.md' -o output.pdf"); - expect(result.command).toBe('pandoc'); - expect(result.args).toEqual(['file name.md', '-o', 'output.pdf']); - }); - - test('should handle multiple options', () => { - const result = parseCommand('pandoc input.md --pdf-engine=xelatex -V geometry:margin=1in -o output.pdf'); - expect(result.command).toBe('pandoc'); - expect(result.args).toContain('--pdf-engine=xelatex'); - expect(result.args).toContain('-V'); - }); - - test('should handle empty command', () => { - const result = parseCommand(''); - expect(result.command).toBeUndefined(); - expect(result.args).toEqual([]); - }); - }); - - describe('hexToRgb', () => { - // This function converts hex colors to RGB - function hexToRgb(hex) { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16) / 255, - g: parseInt(result[2], 16) / 255, - b: parseInt(result[3], 16) / 255 - } : null; - } - - test('should convert black hex to RGB', () => { - const result = hexToRgb('#000000'); - expect(result).toEqual({ r: 0, g: 0, b: 0 }); - }); - - test('should convert white hex to RGB', () => { - const result = hexToRgb('#ffffff'); - expect(result).toEqual({ r: 1, g: 1, b: 1 }); - }); - - test('should convert red hex to RGB', () => { - const result = hexToRgb('#ff0000'); - expect(result.r).toBeCloseTo(1); - expect(result.g).toBeCloseTo(0); - expect(result.b).toBeCloseTo(0); - }); - - test('should handle hex without hash', () => { - const result = hexToRgb('00ff00'); - expect(result.r).toBeCloseTo(0); - expect(result.g).toBeCloseTo(1); - expect(result.b).toBeCloseTo(0); - }); - - test('should return null for invalid hex', () => { - expect(hexToRgb('invalid')).toBeNull(); - expect(hexToRgb('#xyz')).toBeNull(); - }); - }); - - describe('File Path Utilities', () => { - test('should extract file extension', () => { - const getExtension = (filepath) => { - const match = filepath.match(/\.([^/.]+)$/); - return match ? match[1].toLowerCase() : ''; - }; - - expect(getExtension('file.md')).toBe('md'); - expect(getExtension('document.PDF')).toBe('pdf'); - expect(getExtension('path/to/file.docx')).toBe('docx'); - expect(getExtension('noextension')).toBe(''); - }); - - test('should replace file extension', () => { - const replaceExtension = (filepath, newExt) => { - return filepath.replace(/\.[^/.]+$/, `.${newExt}`); - }; - - expect(replaceExtension('file.md', 'pdf')).toBe('file.pdf'); - expect(replaceExtension('path/to/doc.docx', 'html')).toBe('path/to/doc.html'); - }); - }); -}); +/** + * Tests for Utility Functions + * Tests helper functions that can be extracted and tested + */ + +describe('Utility Functions', () => { + describe('parseCommand', () => { + // This function parses command strings into command and args + function parseCommand(cmdString) { + const parts = []; + let current = ''; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < cmdString.length; i++) { + const char = cmdString[i]; + if ((char === '"' || char === "'") && !inQuotes) { + inQuotes = true; + quoteChar = char; + } else if (char === quoteChar && inQuotes) { + inQuotes = false; + quoteChar = ''; + } else if (char === ' ' && !inQuotes) { + if (current) { + parts.push(current); + current = ''; + } + } else { + current += char; + } + } + if (current) { + parts.push(current); + } + + return { + command: parts[0], + args: parts.slice(1) + }; + } + + test('should parse simple command', () => { + const result = parseCommand('pandoc input.md -o output.pdf'); + expect(result.command).toBe('pandoc'); + expect(result.args).toEqual(['input.md', '-o', 'output.pdf']); + }); + + test('should handle double-quoted paths', () => { + const result = parseCommand('pandoc "C:/path with spaces/file.md" -o output.pdf'); + expect(result.command).toBe('pandoc'); + expect(result.args).toEqual(['C:/path with spaces/file.md', '-o', 'output.pdf']); + }); + + test('should handle single-quoted paths', () => { + const result = parseCommand("pandoc 'file name.md' -o output.pdf"); + expect(result.command).toBe('pandoc'); + expect(result.args).toEqual(['file name.md', '-o', 'output.pdf']); + }); + + test('should handle multiple options', () => { + const result = parseCommand('pandoc input.md --pdf-engine=xelatex -V geometry:margin=1in -o output.pdf'); + expect(result.command).toBe('pandoc'); + expect(result.args).toContain('--pdf-engine=xelatex'); + expect(result.args).toContain('-V'); + }); + + test('should handle empty command', () => { + const result = parseCommand(''); + expect(result.command).toBeUndefined(); + expect(result.args).toEqual([]); + }); + }); + + describe('hexToRgb', () => { + // This function converts hex colors to RGB + function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255 + } : null; + } + + test('should convert black hex to RGB', () => { + const result = hexToRgb('#000000'); + expect(result).toEqual({ r: 0, g: 0, b: 0 }); + }); + + test('should convert white hex to RGB', () => { + const result = hexToRgb('#ffffff'); + expect(result).toEqual({ r: 1, g: 1, b: 1 }); + }); + + test('should convert red hex to RGB', () => { + const result = hexToRgb('#ff0000'); + expect(result.r).toBeCloseTo(1); + expect(result.g).toBeCloseTo(0); + expect(result.b).toBeCloseTo(0); + }); + + test('should handle hex without hash', () => { + const result = hexToRgb('00ff00'); + expect(result.r).toBeCloseTo(0); + expect(result.g).toBeCloseTo(1); + expect(result.b).toBeCloseTo(0); + }); + + test('should return null for invalid hex', () => { + expect(hexToRgb('invalid')).toBeNull(); + expect(hexToRgb('#xyz')).toBeNull(); + }); + }); + + describe('File Path Utilities', () => { + test('should extract file extension', () => { + const getExtension = (filepath) => { + const match = filepath.match(/\.([^/.]+)$/); + return match ? match[1].toLowerCase() : ''; + }; + + expect(getExtension('file.md')).toBe('md'); + expect(getExtension('document.PDF')).toBe('pdf'); + expect(getExtension('path/to/file.docx')).toBe('docx'); + expect(getExtension('noextension')).toBe(''); + }); + + test('should replace file extension', () => { + const replaceExtension = (filepath, newExt) => { + return filepath.replace(/\.[^/.]+$/, `.${newExt}`); + }; + + expect(replaceExtension('file.md', 'pdf')).toBe('file.pdf'); + expect(replaceExtension('path/to/doc.docx', 'html')).toBe('path/to/doc.html'); + }); + }); +});