diff --git a/src/adapters/electron/fs.js b/src/adapters/electron/fs.js index 29813f6..639f078 100644 --- a/src/adapters/electron/fs.js +++ b/src/adapters/electron/fs.js @@ -12,100 +12,100 @@ * @type {import('../types').FileSystemAdapter} */ const electronFsAdapter = { - /** - * Read file content - * @param {string} path - File path - * @returns {Promise} File content - */ - async readFile(path) { - return await window.electronAPI.file.read(path); - }, + /** + * Read file content + * @param {string} path - File path + * @returns {Promise} File content + */ + async readFile(path) { + return await window.electronAPI.file.read(path); + }, - /** - * Write content to file - * @param {string} path - File path - * @param {string} content - File content - * @returns {Promise} - */ - async writeFile(path, content) { - return await window.electronAPI.file.write(path, content); - }, + /** + * Write content to file + * @param {string} path - File path + * @param {string} content - File content + * @returns {Promise} + */ + async writeFile(path, content) { + return await window.electronAPI.file.write(path, content); + }, - /** - * Delete file - * @param {string} path - File path - * @returns {Promise} - */ - async deleteFile(path) { - return await window.electronAPI.file.delete(path); - }, + /** + * Delete file + * @param {string} path - File path + * @returns {Promise} + */ + async deleteFile(path) { + return await window.electronAPI.file.delete(path); + }, - /** - * Ensure directory exists - * @param {string} path - Directory path - * @returns {Promise} - */ - async ensureDir(path) { - return await window.electronAPI.file.ensureDir(path); - }, + /** + * Ensure directory exists + * @param {string} path - Directory path + * @returns {Promise} + */ + async ensureDir(path) { + return await window.electronAPI.file.ensureDir(path); + }, - /** - * List directory contents - * @param {string} path - Directory path - * @returns {Promise>} - */ - async listDirectory(path) { - const result = await window.electronAPI.invoke('list-directory', path); - if (!result?.entries) { - return []; - } - - return result.entries.map((entry) => ({ - name: entry.name, - isDir: entry.isDirectory, - size: entry.size ?? 0, - modified: entry.modified ?? 0, - path: entry.path - })); - }, - - /** - * Check if path exists - * @param {string} path - Path to check - * @returns {Promise} - */ - async exists(path) { - return await window.electronAPI.file.exists(path); - }, - - /** - * Check if path is a directory - * @param {string} path - Path to check - * @returns {Promise} - */ - async isDirectory(path) { - return await window.electronAPI.file.isDirectory(path); - }, - - /** - * Copy file or directory - * @param {string} source - Source path - * @param {string} dest - Destination path - * @returns {Promise} - */ - async copy(source, dest) { - return await window.electronAPI.file.copy(source, dest); - }, - - /** - * Move file or directory - * @param {string} source - Source path - * @param {string} dest - Destination path - * @returns {Promise} - */ - async move(source, dest) { - return await window.electronAPI.file.move(source, dest); + /** + * List directory contents + * @param {string} path - Directory path + * @returns {Promise>} + */ + async listDirectory(path) { + const result = await window.electronAPI.invoke('list-directory', path); + if (!result?.entries) { + return []; } + + return result.entries.map((entry) => ({ + name: entry.name, + isDir: entry.isDirectory, + size: entry.size ?? 0, + modified: entry.modified ?? 0, + path: entry.path, + })); + }, + + /** + * Check if path exists + * @param {string} path - Path to check + * @returns {Promise} + */ + async exists(path) { + return await window.electronAPI.file.exists(path); + }, + + /** + * Check if path is a directory + * @param {string} path - Path to check + * @returns {Promise} + */ + async isDirectory(path) { + return await window.electronAPI.file.isDirectory(path); + }, + + /** + * Copy file or directory + * @param {string} source - Source path + * @param {string} dest - Destination path + * @returns {Promise} + */ + async copy(source, dest) { + return await window.electronAPI.file.copy(source, dest); + }, + + /** + * Move file or directory + * @param {string} source - Source path + * @param {string} dest - Destination path + * @returns {Promise} + */ + async move(source, dest) { + return await window.electronAPI.file.move(source, dest); + }, }; module.exports = { electronFsAdapter }; diff --git a/src/adapters/types.js b/src/adapters/types.js index 065fb2e..124bf24 100644 --- a/src/adapters/types.js +++ b/src/adapters/types.js @@ -129,5 +129,5 @@ */ module.exports = { - // Type definitions are JSDoc only, no runtime exports needed + // Type definitions are JSDoc only, no runtime exports needed }; diff --git a/src/analytics/analytics-panel.js b/src/analytics/analytics-panel.js index 180a923..0a9afa3 100644 --- a/src/analytics/analytics-panel.js +++ b/src/analytics/analytics-panel.js @@ -5,19 +5,19 @@ const { analyze } = require('./writing-analytics'); function showAnalyticsModal(tabManager) { - const existing = document.getElementById('analytics-modal'); - if (existing) existing.remove(); + const existing = document.getElementById('analytics-modal'); + if (existing) existing.remove(); - const content = tabManager.getEditorContent(); - const metrics = analyze(content); + const content = tabManager.getEditorContent(); + const metrics = analyze(content); - const overlay = document.createElement('div'); - overlay.id = 'analytics-modal'; - overlay.className = 'analytics-overlay'; + const overlay = document.createElement('div'); + overlay.id = 'analytics-modal'; + overlay.className = 'analytics-overlay'; - const maxCount = metrics.topWords.length > 0 ? metrics.topWords[0].count : 1; + const maxCount = metrics.topWords.length > 0 ? metrics.topWords[0].count : 1; - overlay.innerHTML = ` + overlay.innerHTML = `

Writing Analytics

@@ -61,11 +61,15 @@ function showAnalyticsModal(tabManager) { Avg Sentence ${metrics.avgSentenceLength} words
- ${metrics.longestSentenceLength > 0 ? ` + ${ + metrics.longestSentenceLength > 0 + ? `
Longest (${metrics.longestSentenceLength} words) ${escapeHtml(metrics.longestSentence)} -
` : ''} +
` + : '' + }
@@ -74,39 +78,45 @@ function showAnalyticsModal(tabManager) { Unique ${metrics.uniqueWordCount} / ${metrics.wordCount}${metrics.lexicalDiversity}%
- ${metrics.topWords.length > 0 ? ` + ${ + metrics.topWords.length > 0 + ? `
- ${metrics.topWords.map(w => { + ${metrics.topWords + .map((w) => { const scale = 13 + Math.round((w.count / maxCount) * 3); return `${escapeHtml(w.word)}${w.count}`; - }).join('')} -
` : ''} + }) + .join('')} + ` + : '' + } `; - const closeBtn = overlay.querySelector('.analytics-close'); - closeBtn.addEventListener('click', () => overlay.remove()); - overlay.addEventListener('click', (e) => { - if (e.target === overlay) overlay.remove(); - }); + const closeBtn = overlay.querySelector('.analytics-close'); + closeBtn.addEventListener('click', () => overlay.remove()); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); - const escHandler = (e) => { - if (e.key === 'Escape') { - overlay.remove(); - document.removeEventListener('keydown', escHandler); - } - }; - document.addEventListener('keydown', escHandler); + const escHandler = (e) => { + if (e.key === 'Escape') { + overlay.remove(); + document.removeEventListener('keydown', escHandler); + } + }; + document.addEventListener('keydown', escHandler); - document.body.appendChild(overlay); + document.body.appendChild(overlay); } function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; } module.exports = { showAnalyticsModal }; diff --git a/src/analytics/writing-analytics.js b/src/analytics/writing-analytics.js index 8b6ca08..7c83f73 100644 --- a/src/analytics/writing-analytics.js +++ b/src/analytics/writing-analytics.js @@ -4,124 +4,197 @@ */ const STOP_WORDS = new Set([ - 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', - 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', - 'could', 'should', 'to', 'of', 'in', 'for', 'on', 'with', - 'at', 'by', 'from', 'as', 'and', 'or', 'but', 'if', 'it', - 'its', 'this', 'that', 'these', 'those', 'i', 'me', 'my', - 'we', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her', - 'they', 'them', 'their', 'not', 'no', 'so', 'than', 'too', - 'very', 'also', 'just', 'about', 'up', 'out', 'what', 'which', 'who' + 'the', + 'a', + 'an', + 'is', + 'are', + 'was', + 'were', + 'be', + 'been', + 'have', + 'has', + 'had', + 'do', + 'does', + 'did', + 'will', + 'would', + 'could', + 'should', + 'to', + 'of', + 'in', + 'for', + 'on', + 'with', + 'at', + 'by', + 'from', + 'as', + 'and', + 'or', + 'but', + 'if', + 'it', + 'its', + 'this', + 'that', + 'these', + 'those', + 'i', + 'me', + 'my', + 'we', + 'our', + 'you', + 'your', + 'he', + 'him', + 'his', + 'she', + 'her', + 'they', + 'them', + 'their', + 'not', + 'no', + 'so', + 'than', + 'too', + 'very', + 'also', + 'just', + 'about', + 'up', + 'out', + 'what', + 'which', + 'who', ]); function countSyllables(word) { - word = word.toLowerCase().replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, ''); - word = word.replace(/^y/, ''); - return word.match(/[aeiouy]{1,2}/gi)?.length || 1; + word = word.toLowerCase().replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, ''); + word = word.replace(/^y/, ''); + return word.match(/[aeiouy]{1,2}/gi)?.length || 1; } function extractWords(text) { - return text.match(/[a-zA-Z]+(?:['-][a-zA-Z]+)*/g) || []; + return text.match(/[a-zA-Z]+(?:['-][a-zA-Z]+)*/g) || []; } function getReadabilityLabel(score) { - if (score >= 90) return 'Very Easy'; - if (score >= 70) return 'Easy'; - if (score >= 50) return 'Standard'; - if (score >= 30) return 'Difficult'; - return 'Very Difficult'; + if (score >= 90) return 'Very Easy'; + if (score >= 70) return 'Easy'; + if (score >= 50) return 'Standard'; + if (score >= 30) return 'Difficult'; + return 'Very Difficult'; } function analyze(text) { - if (!text || !text.trim()) { - return { - wordCount: 0, - sentenceCount: 0, - paragraphCount: 0, - fleschEase: 0, - fleschGrade: 0, - readabilityLabel: 'N/A', - readingTime: 0, - speakingTime: 0, - uniqueWordCount: 0, - lexicalDiversity: 0, - avgSentenceLength: 0, - longestSentence: '', - longestSentenceLength: 0, - topWords: [] - }; - } - - const words = extractWords(text); - const wordCount = words.length; - - const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(Boolean); - const sentenceCount = Math.max(sentences.length, 1); - - const paragraphs = text.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean); - const paragraphCount = Math.max(paragraphs.length, 1); - - let totalSyllables = 0; - for (const w of words) { - totalSyllables += countSyllables(w); - } - - const fleschEase = Math.round((206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)) * 10) / 10; - const fleschGrade = Math.round((0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59) * 10) / 10; - const readabilityLabel = getReadabilityLabel(fleschEase); - - const readingTime = Math.ceil(wordCount / 200); - const speakingTime = Math.ceil(wordCount / 130); - - const uniqueWords = new Set(words.map(w => w.toLowerCase())); - const uniqueWordCount = uniqueWords.size; - const lexicalDiversity = wordCount > 0 ? Math.round((uniqueWordCount / wordCount) * 1000) / 10 : 0; - - const avgSentenceLength = Math.round((wordCount / sentenceCount) * 10) / 10; - - let longestSentence = ''; - let longestSentenceLength = 0; - for (const s of sentences) { - const sWords = extractWords(s); - if (sWords.length > longestSentenceLength) { - longestSentenceLength = sWords.length; - longestSentence = s.trim(); - } - } - - if (longestSentence.length > 80) { - longestSentence = longestSentence.substring(0, 80) + '...'; - } - - const wordFreq = {}; - for (const w of words) { - const lower = w.toLowerCase(); - if (!STOP_WORDS.has(lower) && lower.length > 1) { - wordFreq[lower] = (wordFreq[lower] || 0) + 1; - } - } - - const topWords = Object.entries(wordFreq) - .sort((a, b) => b[1] - a[1]) - .slice(0, 10) - .map(([word, count]) => ({ word, count })); - + if (!text || !text.trim()) { return { - wordCount, - sentenceCount, - paragraphCount, - fleschEase, - fleschGrade, - readabilityLabel, - readingTime, - speakingTime, - uniqueWordCount, - lexicalDiversity, - avgSentenceLength, - longestSentence, - longestSentenceLength, - topWords + wordCount: 0, + sentenceCount: 0, + paragraphCount: 0, + fleschEase: 0, + fleschGrade: 0, + readabilityLabel: 'N/A', + readingTime: 0, + speakingTime: 0, + uniqueWordCount: 0, + lexicalDiversity: 0, + avgSentenceLength: 0, + longestSentence: '', + longestSentenceLength: 0, + topWords: [], }; + } + + const words = extractWords(text); + const wordCount = words.length; + + const sentences = text + .split(/[.!?]+/) + .map((s) => s.trim()) + .filter(Boolean); + const sentenceCount = Math.max(sentences.length, 1); + + const paragraphs = text + .split(/\n\s*\n/) + .map((p) => p.trim()) + .filter(Boolean); + const paragraphCount = Math.max(paragraphs.length, 1); + + let totalSyllables = 0; + for (const w of words) { + totalSyllables += countSyllables(w); + } + + const fleschEase = + Math.round( + (206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)) * 10 + ) / 10; + const fleschGrade = + Math.round( + (0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59) * 10 + ) / 10; + const readabilityLabel = getReadabilityLabel(fleschEase); + + const readingTime = Math.ceil(wordCount / 200); + const speakingTime = Math.ceil(wordCount / 130); + + const uniqueWords = new Set(words.map((w) => w.toLowerCase())); + const uniqueWordCount = uniqueWords.size; + const lexicalDiversity = + wordCount > 0 ? Math.round((uniqueWordCount / wordCount) * 1000) / 10 : 0; + + const avgSentenceLength = Math.round((wordCount / sentenceCount) * 10) / 10; + + let longestSentence = ''; + let longestSentenceLength = 0; + for (const s of sentences) { + const sWords = extractWords(s); + if (sWords.length > longestSentenceLength) { + longestSentenceLength = sWords.length; + longestSentence = s.trim(); + } + } + + if (longestSentence.length > 80) { + longestSentence = longestSentence.substring(0, 80) + '...'; + } + + const wordFreq = {}; + for (const w of words) { + const lower = w.toLowerCase(); + if (!STOP_WORDS.has(lower) && lower.length > 1) { + wordFreq[lower] = (wordFreq[lower] || 0) + 1; + } + } + + const topWords = Object.entries(wordFreq) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([word, count]) => ({ word, count })); + + return { + wordCount, + sentenceCount, + paragraphCount, + fleschEase, + fleschGrade, + readabilityLabel, + readingTime, + speakingTime, + uniqueWordCount, + lexicalDiversity, + avgSentenceLength, + longestSentence, + longestSentenceLength, + topWords, + }; } module.exports = { analyze }; diff --git a/src/editor/codemirror-setup.js b/src/editor/codemirror-setup.js index a16f5e7..cd44a52 100644 --- a/src/editor/codemirror-setup.js +++ b/src/editor/codemirror-setup.js @@ -12,38 +12,23 @@ const { EditorState } = require('@codemirror/state'); const { markdown, markdownLanguage } = require('@codemirror/lang-markdown'); // Language extensions loaded lazily on first use let _javascript, _html, _css, _json, _python; -const { - defaultKeymap, - history, - historyKeymap, - indentWithTab, -} = require('@codemirror/commands'); -const { - searchKeymap, - highlightSelectionMatches, -} = require('@codemirror/search'); -const { - autocompletion, - completionKeymap, -} = require('@codemirror/autocomplete'); -const { - bracketMatching, - foldGutter, - indentOnInput, -} = require('@codemirror/language'); +const { defaultKeymap, history, historyKeymap, indentWithTab } = require('@codemirror/commands'); +const { searchKeymap, highlightSelectionMatches } = require('@codemirror/search'); +const { autocompletion, completionKeymap } = require('@codemirror/autocomplete'); +const { bracketMatching, foldGutter, indentOnInput } = require('@codemirror/language'); const { oneDark } = require('@codemirror/theme-one-dark'); // Custom theme for JetBrains Mono font const jetBrainsMonoTheme = EditorView.theme({ '&': { - fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Courier New', monospace" + fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Courier New', monospace", }, '.cm-content': { - fontFamily: 'inherit' + fontFamily: 'inherit', }, '.cm-scroller': { - fontFamily: 'inherit' - } + fontFamily: 'inherit', + }, }); /** @@ -59,7 +44,14 @@ const jetBrainsMonoTheme = EditorView.theme({ * @returns {EditorView} the created editor view */ function createEditor(parentElement, options = {}) { - console.log('[createEditor] Called with parentElement:', parentElement?.id, 'dimensions:', parentElement?.clientWidth, 'x', parentElement?.clientHeight); + console.log( + '[createEditor] Called with parentElement:', + parentElement?.id, + 'dimensions:', + parentElement?.clientWidth, + 'x', + parentElement?.clientHeight + ); if (!parentElement) { console.error('[createEditor] ERROR: parentElement is null or undefined!'); return null; @@ -125,11 +117,26 @@ function createEditor(parentElement, options = {}) { */ function getLanguageExtension(lang) { const loaders = { - javascript: () => { if (!_javascript) _javascript = require('@codemirror/lang-javascript').javascript; return _javascript(); }, - html: () => { if (!_html) _html = require('@codemirror/lang-html').html; return _html(); }, - css: () => { if (!_css) _css = require('@codemirror/lang-css').css; return _css(); }, - json: () => { if (!_json) _json = require('@codemirror/lang-json').json; return _json(); }, - python: () => { if (!_python) _python = require('@codemirror/lang-python').python; return _python(); }, + javascript: () => { + if (!_javascript) _javascript = require('@codemirror/lang-javascript').javascript; + return _javascript(); + }, + html: () => { + if (!_html) _html = require('@codemirror/lang-html').html; + return _html(); + }, + css: () => { + if (!_css) _css = require('@codemirror/lang-css').css; + return _css(); + }, + json: () => { + if (!_json) _json = require('@codemirror/lang-json').json; + return _json(); + }, + python: () => { + if (!_python) _python = require('@codemirror/lang-python').python; + return _python(); + }, markdown: () => markdown({ base: markdownLanguage }), }; loaders.js = loaders.javascript; diff --git a/src/main/PDFOperations.js b/src/main/PDFOperations.js index fc9e2b6..0fe7a5f 100644 --- a/src/main/PDFOperations.js +++ b/src/main/PDFOperations.js @@ -4,11 +4,11 @@ const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib'); function parsePageRanges(rangeString, totalPages) { const pages = []; - const ranges = rangeString.split(',').map(r => r.trim()); + const ranges = rangeString.split(',').map((r) => r.trim()); for (const range of ranges) { if (range.includes('-')) { - const [start, end] = range.split('-').map(n => parseInt(n.trim())); + const [start, end] = range.split('-').map((n) => parseInt(n.trim())); for (let i = start; i <= end && i <= totalPages; i++) { if (i > 0 && !pages.includes(i - 1)) { pages.push(i - 1); @@ -27,11 +27,13 @@ function parsePageRanges(rangeString, totalPages) { 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 - } : { r: 0, g: 0, b: 0 }; + return result + ? { + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255, + } + : { r: 0, g: 0, b: 0 }; } async function pdfMerge(data) { @@ -42,7 +44,7 @@ async function pdfMerge(data) { const pdfBytes = fs.readFileSync(filePath); const pdf = await PDFDocument.load(pdfBytes); const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); - copiedPages.forEach(page => mergedPdf.addPage(page)); + copiedPages.forEach((page) => mergedPdf.addPage(page)); } const pdfBytes = await mergedPdf.save(); @@ -63,13 +65,13 @@ async function pdfSplit(data) { const splits = []; if (data.splitMode === 'pages') { - const ranges = data.pageRanges.split(',').map(r => r.trim()); + const ranges = data.pageRanges.split(',').map((r) => r.trim()); for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; const pages = []; if (range.includes('-')) { - const [start, end] = range.split('-').map(n => parseInt(n.trim())); + const [start, end] = range.split('-').map((n) => parseInt(n.trim())); for (let p = start; p <= end && p <= totalPages; p++) { pages.push(p - 1); } @@ -108,7 +110,7 @@ async function pdfSplit(data) { for (const split of splits) { const newPdf = await PDFDocument.create(); const copiedPages = await newPdf.copyPages(pdf, split.pages); - copiedPages.forEach(page => newPdf.addPage(page)); + copiedPages.forEach((page) => newPdf.addPage(page)); const outputPath = path.join(data.outputFolder, `${baseName}_${split.name}.pdf`); const newPdfBytes = await newPdf.save(); @@ -129,18 +131,18 @@ async function pdfCompress(data) { const compressedPdfBytes = await pdf.save({ useObjectStreams: true, addDefaultPage: false, - objectsPerTick: 50 + objectsPerTick: 50, }); fs.writeFileSync(data.outputPath, compressedPdfBytes); const originalSize = fs.statSync(data.inputPath).size; const compressedSize = fs.statSync(data.outputPath).size; - const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); + const savings = (((originalSize - compressedSize) / originalSize) * 100).toFixed(1); return { success: true, - message: `PDF compressed. Size reduced by ${savings}% (${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB)` + message: `PDF compressed. Size reduced by ${savings}% (${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB)`, }; } catch (error) { return { success: false, error: error.message }; @@ -160,7 +162,7 @@ async function pdfRotate(data) { pagesToRotate = Array.from({ length: totalPages }, (_, i) => i); } - pagesToRotate.forEach(pageIndex => { + pagesToRotate.forEach((pageIndex) => { const page = pdf.getPage(pageIndex); page.setRotation(degrees(data.angle)); }); @@ -170,7 +172,7 @@ async function pdfRotate(data) { return { success: true, - message: `Successfully rotated ${pagesToRotate.length} page(s) by ${data.angle}\u00B0` + message: `Successfully rotated ${pagesToRotate.length} page(s) by ${data.angle}\u00B0`, }; } catch (error) { return { success: false, error: error.message }; @@ -185,16 +187,18 @@ async function pdfDeletePages(data) { const pagesToDelete = parsePageRanges(data.pages, totalPages); - pagesToDelete.sort((a, b) => b - a).forEach(pageIndex => { - pdf.removePage(pageIndex); - }); + pagesToDelete + .sort((a, b) => b - a) + .forEach((pageIndex) => { + pdf.removePage(pageIndex); + }); const newPdfBytes = await pdf.save(); fs.writeFileSync(data.outputPath, newPdfBytes); return { success: true, - message: `Successfully deleted ${pagesToDelete.length} page(s). New PDF has ${totalPages - pagesToDelete.length} pages` + message: `Successfully deleted ${pagesToDelete.length} page(s). New PDF has ${totalPages - pagesToDelete.length} pages`, }; } catch (error) { return { success: false, error: error.message }; @@ -207,7 +211,7 @@ async function pdfReorder(data) { const pdf = await PDFDocument.load(pdfBytes); const totalPages = pdf.getPageCount(); - const newOrder = data.newOrder.split(',').map(n => parseInt(n.trim()) - 1); + const newOrder = data.newOrder.split(',').map((n) => parseInt(n.trim()) - 1); if (newOrder.length !== totalPages) { return { success: false, error: `New order must include all ${totalPages} pages` }; @@ -215,7 +219,7 @@ async function pdfReorder(data) { const newPdf = await PDFDocument.create(); const copiedPages = await newPdf.copyPages(pdf, newOrder); - copiedPages.forEach(page => newPdf.addPage(page)); + copiedPages.forEach((page) => newPdf.addPage(page)); const reorderedPdfBytes = await newPdf.save(); fs.writeFileSync(data.outputPath, reorderedPdfBytes); @@ -246,7 +250,9 @@ async function pdfWatermark(data) { const page = pdf.getPage(pageIndex); const { width, height } = page.getSize(); - let x, y, rotation = 0; + let x, + y, + rotation = 0; switch (data.position) { case 'center': @@ -294,7 +300,7 @@ async function pdfWatermark(data) { font, color: rgb(color.r, color.g, color.b), opacity: data.opacity, - rotate: degrees(rotation) + rotate: degrees(rotation), }); } @@ -303,7 +309,7 @@ async function pdfWatermark(data) { return { success: true, - message: `Successfully added watermark to ${pagesToWatermark.length} page(s)` + message: `Successfully added watermark to ${pagesToWatermark.length} page(s)`, }; } catch (error) { return { success: false, error: error.message }; @@ -325,8 +331,8 @@ async function pdfEncrypt(data) { annotating: data.permissions.annotating, fillingForms: data.permissions.fillingForms, contentAccessibility: data.permissions.contentAccessibility, - documentAssembly: data.permissions.documentAssembly - } + documentAssembly: data.permissions.documentAssembly, + }, }); fs.writeFileSync(data.outputPath, encryptedPdfBytes); @@ -336,7 +342,8 @@ async function pdfEncrypt(data) { if (error.message.includes('encrypt') || error.message.includes('password')) { return { success: false, - error: 'PDF encryption requires pdf-lib with encryption support. This feature may not be available in the current version.' + error: + 'PDF encryption requires pdf-lib with encryption support. This feature may not be available in the current version.', }; } return { success: false, error: error.message }; @@ -375,8 +382,8 @@ async function pdfSetPermissions(data) { annotating: data.permissions.annotating, fillingForms: data.permissions.fillingForms, contentAccessibility: data.permissions.contentAccessibility, - documentAssembly: data.permissions.documentAssembly - } + documentAssembly: data.permissions.documentAssembly, + }, }); fs.writeFileSync(data.outputPath, newPdfBytes); @@ -386,7 +393,8 @@ async function pdfSetPermissions(data) { if (error.message.includes('encrypt') || error.message.includes('permission')) { return { success: false, - error: 'PDF permissions require pdf-lib with encryption support. This feature may not be available in the current version.' + error: + 'PDF permissions require pdf-lib with encryption support. This feature may not be available in the current version.', }; } return { success: false, error: error.message }; @@ -395,17 +403,28 @@ async function pdfSetPermissions(data) { function executeOperation(operation, data) { switch (operation) { - case 'merge': return pdfMerge(data); - case 'split': return pdfSplit(data); - case 'compress': return pdfCompress(data); - case 'rotate': return pdfRotate(data); - case 'delete': return pdfDeletePages(data); - case 'reorder': return pdfReorder(data); - case 'watermark': return pdfWatermark(data); - case 'encrypt': return pdfEncrypt(data); - case 'decrypt': return pdfDecrypt(data); - case 'permissions': return pdfSetPermissions(data); - default: return Promise.resolve({ success: false, error: `Unknown operation: ${operation}` }); + case 'merge': + return pdfMerge(data); + case 'split': + return pdfSplit(data); + case 'compress': + return pdfCompress(data); + case 'rotate': + return pdfRotate(data); + case 'delete': + return pdfDeletePages(data); + case 'reorder': + return pdfReorder(data); + case 'watermark': + return pdfWatermark(data); + case 'encrypt': + return pdfEncrypt(data); + case 'decrypt': + return pdfDecrypt(data); + case 'permissions': + return pdfSetPermissions(data); + default: + return Promise.resolve({ success: false, error: `Unknown operation: ${operation}` }); } } @@ -429,5 +448,5 @@ module.exports = { pdfDecrypt, pdfSetPermissions, executeOperation, - getPageCount + getPageCount, }; diff --git a/src/main/files/binary.js b/src/main/files/binary.js index e653e74..2d43995 100644 --- a/src/main/files/binary.js +++ b/src/main/files/binary.js @@ -10,4 +10,4 @@ function register() { }); } -module.exports = { register }; \ No newline at end of file +module.exports = { register }; diff --git a/src/main/files/git.js b/src/main/files/git.js index b17bdd4..4d8bebc 100644 --- a/src/main/files/git.js +++ b/src/main/files/git.js @@ -5,24 +5,32 @@ const GitOperations = require('../GitOperations'); function register(currentFileRef) { ipcMain.handle('git-status', async () => { - const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd(); + const dir = currentFileRef.current + ? require('path').dirname(currentFileRef.current) + : process.cwd(); return GitOperations.getStatus(dir); }); ipcMain.handle('git-stage', async (_event, { files }) => { - const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd(); + const dir = currentFileRef.current + ? require('path').dirname(currentFileRef.current) + : process.cwd(); return GitOperations.stage(dir, files); }); ipcMain.handle('git-commit', async (_event, { message }) => { - const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd(); + const dir = currentFileRef.current + ? require('path').dirname(currentFileRef.current) + : process.cwd(); return GitOperations.commit(dir, message); }); ipcMain.handle('git-log', async () => { - const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd(); + const dir = currentFileRef.current + ? require('path').dirname(currentFileRef.current) + : process.cwd(); return GitOperations.log(dir); }); } -module.exports = { register }; \ No newline at end of file +module.exports = { register }; diff --git a/src/main/files/index.js b/src/main/files/index.js index 03ecaa5..41abb31 100644 --- a/src/main/files/index.js +++ b/src/main/files/index.js @@ -6,7 +6,13 @@ const path = require('path'); const { register: registerGit } = require('./git'); const { register: registerBinary } = require('./binary'); -function register({ validatePath, resolveWritablePath, isPathAccessible, currentFileRef, mainWindow }) { +function register({ + validatePath, + resolveWritablePath, + isPathAccessible, + currentFileRef, + mainWindow, +}) { // pick-folder ipcMain.handle('pick-folder', async () => { const result = await dialog.showOpenDialog(mainWindow, { @@ -154,4 +160,4 @@ function register({ validatePath, resolveWritablePath, isPathAccessible, current registerBinary(); } -module.exports = { register }; \ No newline at end of file +module.exports = { register }; diff --git a/src/main/index.js b/src/main/index.js index 11bee62..7a7a481 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -11,7 +11,12 @@ const { exec, execFile, spawn } = require('child_process'); const WordTemplateExporter = require('./word-template'); const PDFOperations = require('./PDFOperations'); const GitOperations = require('./GitOperations'); -const { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible } = require('./utils/paths'); +const { + getAllowedDirectories, + validatePath, + resolveWritablePath, + isPathAccessible, +} = require('./utils/paths'); const fileOps = require('./files'); const menu = require('./menu'); const { createMainWindow } = require('./window'); @@ -37,7 +42,7 @@ function getPandocPath() { '../..', 'bin', process.platform, - process.platform === 'win32' ? 'pandoc.exe' : 'pandoc', + process.platform === 'win32' ? 'pandoc.exe' : 'pandoc' ); if (fs.existsSync(devBin)) return devBin; return 'pandoc'; @@ -77,13 +82,18 @@ function checkPandocAvailable() { */ function safeExecFile(command, args, options = {}) { return new Promise((resolve, reject) => { - execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => { - if (error) { - reject({ error, stdout, stderr }); - } else { - resolve({ stdout, stderr }); + execFile( + command, + args, + { maxBuffer: 10 * 1024 * 1024, ...options }, + (error, stdout, stderr) => { + if (error) { + reject({ error, stdout, stderr }); + } else { + resolve({ stdout, stderr }); + } } - }); + ); }); } @@ -93,40 +103,40 @@ const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024; // Sanitize error messages to strip absolute file paths function sanitizeErrorMessage(message) { - if (typeof message !== 'string') return String(message); - // Strip absolute Windows paths, keeping only filename - return message - .replace(/[A-Z]:\\[^\s"']+\\([^\s"'\\]+)/gi, '$1') - .replace(/\/[^\s"']+\/([^\s"'/]+)/g, '$1'); + if (typeof message !== 'string') return String(message); + // Strip absolute Windows paths, keeping only filename + return message + .replace(/[A-Z]:\\[^\s"']+\\([^\s"'\\]+)/gi, '$1') + .replace(/\/[^\s"']+\/([^\s"'/]+)/g, '$1'); } // Rate limiter for conversions function createRateLimiter(minIntervalMs = 2000) { - let lastCall = 0; - return function canProceed() { - const now = Date.now(); - if (now - lastCall < minIntervalMs) return false; - lastCall = now; - return true; - }; + let lastCall = 0; + return function canProceed() { + const now = Date.now(); + if (now - lastCall < minIntervalMs) return false; + lastCall = now; + return true; + }; } const conversionLimiter = createRateLimiter(2000); // Convert structured data formats to markdown code blocks function convertDataToMarkdown(content, format) { - switch (format) { - case 'json': - return '```json\n' + content + '\n```'; - case 'yaml': - case 'yml': - return '```yaml\n' + content + '\n```'; - case 'xml': - return '```xml\n' + content + '\n```'; - case 'toml': - return '```toml\n' + content + '\n```'; - default: - return '```\n' + content + '\n```'; - } + switch (format) { + case 'json': + return '```json\n' + content + '\n```'; + case 'yaml': + case 'yml': + return '```yaml\n' + content + '\n```'; + case 'xml': + return '```xml\n' + content + '\n```'; + case 'toml': + return '```toml\n' + content + '\n```'; + default: + return '```\n' + content + '\n```'; + } } /** @@ -191,7 +201,7 @@ function parseCommand(cmdString) { return { command: parts[0], - args: parts.slice(1) + args: parts.slice(1), }; } @@ -214,10 +224,7 @@ ipcMain.handle('get-app-version', () => app.getVersion()); // Allow: clipboard-read, clipboard-write (user expects these) app.on('web-contents-created', (event, contents) => { contents.session.setPermissionRequestHandler((webContents, permission, callback) => { - const ALLOWED_PERMISSIONS = [ - 'clipboard-read', - 'clipboard-write' - ]; + const ALLOWED_PERMISSIONS = ['clipboard-read', 'clipboard-write']; if (ALLOWED_PERMISSIONS.includes(permission)) { callback(true); @@ -246,14 +253,14 @@ let headerFooterSettings = { left: '', center: '', right: '', - logo: null // Will store image file path + logo: null, // Will store image file path }, footer: { left: '', center: '$PAGE$ of $TOTAL$', right: '', - logo: null - } + logo: null, + }, }; // Page Size Definitions (in twentieths of a point for Word, mm/inches for Pandoc) @@ -262,50 +269,50 @@ const PAGE_SIZES = { name: 'A4', pandoc: 'a4', word: { width: 11906, height: 16838 }, // 210×297mm - dimensions: '210×297mm' + dimensions: '210×297mm', }, a3: { name: 'A3', pandoc: 'a3', word: { width: 16838, height: 23811 }, // 297×420mm - dimensions: '297×420mm' + dimensions: '297×420mm', }, a5: { name: 'A5', pandoc: 'a5', word: { width: 8391, height: 11906 }, // 148×210mm - dimensions: '148×210mm' + dimensions: '148×210mm', }, b4: { name: 'B4', pandoc: 'b4', word: { width: 14170, height: 20015 }, // 250×353mm - dimensions: '250×353mm' + dimensions: '250×353mm', }, b5: { name: 'B5', pandoc: 'b5', word: { width: 9979, height: 14170 }, // 176×250mm - dimensions: '176×250mm' + dimensions: '176×250mm', }, letter: { name: 'Letter', pandoc: 'letter', word: { width: 12240, height: 15840 }, // 8.5×11in - dimensions: '8.5×11in' + dimensions: '8.5×11in', }, legal: { name: 'Legal', pandoc: 'legal', word: { width: 12240, height: 20160 }, // 8.5×14in - dimensions: '8.5×14in' + dimensions: '8.5×14in', }, tabloid: { name: 'Tabloid', pandoc: 'tabloid', word: { width: 15840, height: 24480 }, // 11×17in - dimensions: '11×17in' - } + dimensions: '11×17in', + }, }; // Default page settings @@ -313,7 +320,7 @@ let pageSettings = { size: 'a4', orientation: 'portrait', customWidth: null, - customHeight: null + customHeight: null, }; // Handle single instance lock for Windows file association @@ -341,7 +348,7 @@ if (!gotTheLock) { const fileArgs = commandLine.slice(startIndex); console.log('[MAIN] Second instance file args:', fileArgs); for (const arg of fileArgs) { - if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { + if (arg.endsWith('.md') || arg.endsWith('.markdown')) { const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(workingDirectory, arg); console.log('[MAIN] Second instance resolved path:', resolvedPath); if (fs.existsSync(resolvedPath)) { @@ -373,8 +380,6 @@ function checkPandocAvailability() { }); } - - // Show About Dialog with logo function showAboutDialog() { const aboutWindow = new BrowserWindow({ @@ -387,9 +392,9 @@ function showAboutDialog() { maximizable: false, webPreferences: { nodeIntegration: false, - contextIsolation: true + contextIsolation: true, }, - icon: path.join(__dirname, '../assets/icon.png') + icon: path.join(__dirname, '../assets/icon.png'), }); // Convert images to base64 for data URL compatibility @@ -488,9 +493,9 @@ function showDependenciesDialog() { resizable: true, webPreferences: { nodeIntegration: false, - contextIsolation: true + contextIsolation: true, }, - icon: path.join(__dirname, '../assets/icon.png') + icon: path.join(__dirname, '../assets/icon.png'), }); const depsHTML = ` @@ -596,16 +601,14 @@ function showDependenciesDialog() { function openPDFFile() { const files = dialog.showOpenDialogSync(mainWindow, { properties: ['openFile'], - filters: [ - { name: 'PDF Files', extensions: ['pdf'] } - ] + filters: [{ name: 'PDF Files', extensions: ['pdf'] }], }); if (files && files[0]) { const stats = fs.statSync(files[0]); if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; } mainWindow.webContents.send('open-pdf-viewer', files[0]); } @@ -617,15 +620,15 @@ function openFile() { filters: [ { name: 'Markdown', extensions: ['md', 'markdown'] }, { name: 'Developer Formats', extensions: ['json', 'yaml', 'yml', 'xml', 'toml'] }, - { name: 'All Files', extensions: ['*'] } - ] + { name: 'All Files', extensions: ['*'] }, + ], }); if (files && files[0]) { const stats = fs.statSync(files[0]); if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; } currentFile = files[0]; const ext = path.extname(currentFile).toLowerCase().slice(1); @@ -646,15 +649,15 @@ function openPdfFile() { properties: ['openFile'], filters: [ { name: 'PDF Files', extensions: ['pdf'] }, - { name: 'All Files', extensions: ['*'] } - ] + { name: 'All Files', extensions: ['*'] }, + ], }); if (files && files[0]) { const stats = fs.statSync(files[0]); if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; } mainWindow.webContents.send('open-pdf-file', files[0]); } @@ -665,8 +668,8 @@ function saveAsFile() { defaultExt: '.md', filters: [ { name: 'Markdown', extensions: ['md', 'markdown'] }, - { name: 'All Files', extensions: ['*'] } - ] + { name: 'All Files', extensions: ['*'] }, + ], }); if (file) { @@ -698,7 +701,7 @@ async function selectWordTemplate() { const result = await dialog.showOpenDialog(mainWindow, { title: 'Select Word Template', filters: [{ name: 'Word Document', extensions: ['docx'] }], - properties: ['openFile'] + properties: ['openFile'], }); if (!result.canceled && result.filePaths.length > 0) { @@ -709,7 +712,7 @@ async function selectWordTemplate() { type: 'info', title: 'Template Selected', message: 'Word template has been updated', - detail: `Template: ${path.basename(wordTemplatePath)}` + detail: `Template: ${path.basename(wordTemplatePath)}`, }); } } @@ -723,13 +726,14 @@ async function showTemplateSettings() { detail: `Current template: ${wordTemplatePath ? path.basename(wordTemplatePath) : 'Default template'}\nContent starts from page: ${templateStartPage}\n\nWhich page should content start from?\n(Templates usually have cover pages, TOC, etc.)`, buttons: ['Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5', 'Custom...', 'Cancel'], defaultId: templateStartPage - 1, - cancelId: 6 + cancelId: 6, }); if (result.response === 6) return; // Cancel let newStartPage; - if (result.response === 5) { // Custom + if (result.response === 5) { + // Custom // Show input dialog for custom page number mainWindow.webContents.send('show-custom-start-page-dialog', templateStartPage); } else { @@ -741,7 +745,7 @@ async function showTemplateSettings() { type: 'info', title: 'Settings Updated', message: 'Template settings have been updated', - detail: `Content will now start from page ${templateStartPage}` + detail: `Content will now start from page ${templateStartPage}`, }); } } @@ -757,7 +761,7 @@ ipcMain.on('set-custom-start-page', (event, pageNumber) => { type: 'info', title: 'Settings Updated', message: 'Template settings have been updated', - detail: `Content will now start from page ${templateStartPage}` + detail: `Content will now start from page ${templateStartPage}`, }); } else { dialog.showErrorBox('Invalid Page Number', 'Please enter a page number between 1 and 100'); @@ -780,7 +784,7 @@ ipcMain.on('save-header-footer-settings', (event, settings) => { type: 'info', title: 'Settings Saved', message: 'Header and footer settings have been saved successfully!', - buttons: ['OK'] + buttons: ['OK'], }); }); @@ -803,9 +807,9 @@ ipcMain.on('browse-header-footer-logo', async (event, position) => { const result = await dialog.showOpenDialog(mainWindow, { title: `Select ${position.charAt(0).toUpperCase() + position.slice(1)} Logo/Image`, filters: [ - { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'] } + { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'] }, ], - properties: ['openFile'] + properties: ['openFile'], }); if (!result.canceled && result.filePaths.length > 0) { @@ -839,14 +843,20 @@ ipcMain.on('browse-header-footer-logo', async (event, position) => { } } catch (error) { console.error('Logo browse error:', error); - dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Failed to select logo: ${error.message}`)); + dialog.showErrorBox( + 'Logo Error', + sanitizeErrorMessage(`Failed to select logo: ${error.message}`) + ); } }); ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => { try { if (!filePath) { - dialog.showErrorBox('Logo Error', 'Failed to save logo: The "path" argument must be of type string. Received undefined'); + dialog.showErrorBox( + 'Logo Error', + 'Failed to save logo: The "path" argument must be of type string. Received undefined' + ); return; } @@ -883,7 +893,10 @@ ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => { event.reply('header-footer-logo-saved', { position, path: destPath }); } catch (error) { console.error('Logo save error:', error); - dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Failed to save logo: ${error.message}`)); + dialog.showErrorBox( + 'Logo Error', + sanitizeErrorMessage(`Failed to save logo: ${error.message}`) + ); } }); @@ -973,7 +986,6 @@ async function setDocxPageSize(docxPath) { // Write modified DOCX const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); fs.writeFileSync(docxPath, newDocxBuffer); - } catch (error) { console.error('Failed to set DOCX page size:', error); } @@ -1024,15 +1036,17 @@ async function addHeaderFooterToDocx(docxPath, metadata = {}) { // Handle $PAGE$ and $TOTAL$ in footer if (footerCenter.includes('$PAGE$') || footerCenter.includes('$TOTAL$')) { const parts = footerCenter.split(/(\$PAGE\$|\$TOTAL\$)/); - footerCenterXml = parts.map(part => { - if (part === '$PAGE$') { - return ''; - } else if (part === '$TOTAL$') { - return ''; - } else { - return `${part}`; - } - }).join(''); + footerCenterXml = parts + .map((part) => { + if (part === '$PAGE$') { + return ''; + } else if (part === '$TOTAL$') { + return ''; + } else { + return `${part}`; + } + }) + .join(''); } else { footerCenterXml = `${footerCenter}`; } @@ -1077,16 +1091,22 @@ async function addHeaderFooterToDocx(docxPath, metadata = {}) { // Update document.xml to use header/footer in sections let documentXml = zip.file('word/document.xml').asText(); - if ((headerLeft || headerCenter || headerRight || footerLeft || footerCenter || footerRight)) { + if (headerLeft || headerCenter || headerRight || footerLeft || footerCenter || footerRight) { // Find all section properties and add header/footer references const sectPrRegex = /]*>[\s\S]*?<\/w:sectPr>/g; documentXml = documentXml.replace(sectPrRegex, (match) => { let updated = match; if ((headerLeft || headerCenter || headerRight) && !match.includes('headerReference')) { - updated = updated.replace('', ''); + updated = updated.replace( + '', + '' + ); } if ((footerLeft || footerCenter || footerRight) && !match.includes('footerReference')) { - updated = updated.replace('', ''); + updated = updated.replace( + '', + '' + ); } return updated; }); @@ -1097,7 +1117,6 @@ async function addHeaderFooterToDocx(docxPath, metadata = {}) { // Write modified DOCX const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); fs.writeFileSync(docxPath, newDocxBuffer); - } catch (error) { console.error('Failed to add headers/footers to DOCX:', error); // Don't fail the export, just log the error @@ -1119,7 +1138,7 @@ async function exportWordWithTemplate() { const result = await dialog.showSaveDialog(mainWindow, { title: 'Export to Word (Enhanced)', defaultPath: currentFile.replace(/\.md$/, '.docx'), - filters: [{ name: 'Word Document', extensions: ['docx'] }] + filters: [{ name: 'Word Document', extensions: ['docx'] }], }); if (result.canceled) return; @@ -1134,11 +1153,13 @@ async function exportWordWithTemplate() { type: 'info', title: 'Export Successful', message: 'Document exported successfully!', - detail: `Saved to: ${result.filePath}` + detail: `Saved to: ${result.filePath}`, }); - } catch (error) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export document: ${error.message}`)); + dialog.showErrorBox( + 'Export Error', + sanitizeErrorMessage(`Failed to export document: ${error.message}`) + ); } } @@ -1157,7 +1178,7 @@ async function exportPDFViaWordTemplate() { const result = await dialog.showSaveDialog(mainWindow, { title: 'Export to PDF (Enhanced)', defaultPath: currentFile.replace(/\.md$/, '.pdf'), - filters: [{ name: 'PDF Document', extensions: ['pdf'] }] + filters: [{ name: 'PDF Document', extensions: ['pdf'] }], }); if (result.canceled) return; @@ -1169,9 +1190,10 @@ async function exportPDFViaWordTemplate() { await exporter.convert(content, tempDocxPath); // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety) - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; + const soffice = + process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; const outputDir = path.dirname(result.filePath); const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; @@ -1185,8 +1207,12 @@ async function exportPDFViaWordTemplate() { } if (error) { - dialog.showErrorBox('PDF Conversion Error', - sanitizeErrorMessage(`Failed to convert to PDF. Please ensure LibreOffice is installed.\n\nError: ${error.message}`)); + dialog.showErrorBox( + 'PDF Conversion Error', + sanitizeErrorMessage( + `Failed to convert to PDF. Please ensure LibreOffice is installed.\n\nError: ${error.message}` + ) + ); return; } @@ -1206,12 +1232,14 @@ async function exportPDFViaWordTemplate() { type: 'info', title: 'Export Successful', message: 'PDF exported successfully using Word template!', - detail: `Saved to: ${result.filePath}` + detail: `Saved to: ${result.filePath}`, }); }); - } catch (error) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export PDF: ${error.message}`)); + dialog.showErrorBox( + 'Export Error', + sanitizeErrorMessage(`Failed to export PDF: ${error.message}`) + ); } } @@ -1304,7 +1332,7 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file if (error) { mainWindow.webContents.send('conversion-complete', { success: false, - error: error.message + error: error.message, }); dialog.showMessageBox(mainWindow, { @@ -1312,12 +1340,12 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file title: 'Conversion Failed', message: `${tool} conversion failed`, detail: stderr || error.message, - buttons: ['OK'] + buttons: ['OK'], }); } else { mainWindow.webContents.send('conversion-complete', { success: true, - outputPath: outputPath + outputPath: outputPath, }); dialog.showMessageBox(mainWindow, { @@ -1325,14 +1353,14 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file title: 'Conversion Complete', message: 'File converted successfully!', detail: `Saved to: ${outputPath}`, - buttons: ['OK'] + buttons: ['OK'], }); } }); } catch (error) { mainWindow.webContents.send('conversion-complete', { success: false, - error: error.message + error: error.message, }); dialog.showMessageBox(mainWindow, { @@ -1340,119 +1368,148 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file title: 'Conversion Failed', message: 'Universal conversion failed', detail: error.message, - buttons: ['OK'] + buttons: ['OK'], }); } }); // Handle universal batch file conversion -ipcMain.on('universal-convert-batch', async (event, { tool, fromFormat, toFormat, inputFolder, outputFolder, includeSubfolders, advancedOptions }) => { - if (!conversionLimiter()) { - mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); - return; - } - try { - const toolAvailable = await checkConverterAvailable(tool); - if (!toolAvailable) { - throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); - } - - // Collect matching files - const files = []; - function collectFiles(dir) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory() && includeSubfolders) { - collectFiles(fullPath); - } else if (entry.isFile() && entry.name.toLowerCase().endsWith(`.${fromFormat}`)) { - files.push(fullPath); - } - } - } - collectFiles(inputFolder); - - if (files.length === 0) { - mainWindow.webContents.send('conversion-complete', { success: false, error: `No .${fromFormat} files found in the selected folder.` }); +ipcMain.on( + 'universal-convert-batch', + async ( + event, + { tool, fromFormat, toFormat, inputFolder, outputFolder, includeSubfolders, advancedOptions } + ) => { + if (!conversionLimiter()) { + mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); return; } - - let completed = 0; - let failed = 0; - - for (const filePath of files) { - const relativePath = path.relative(inputFolder, filePath); - const outputPath = path.join(outputFolder, relativePath.replace(/\.[^/.]+$/, `.${toFormat}`)); - - // Ensure output subdirectory exists - fs.mkdirSync(path.dirname(outputPath), { recursive: true }); - - mainWindow.webContents.send('conversion-status', `Converting ${completed + 1}/${files.length}: ${path.basename(filePath)}`); - - let conversionInfo; - switch (tool) { - case 'libreoffice': conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); break; - case 'imagemagick': conversionInfo = convertWithImageMagick(filePath, outputPath); break; - case 'ffmpeg': conversionInfo = convertWithFFmpeg(filePath, outputPath); break; - case 'pandoc': conversionInfo = convertWithPandoc(filePath, outputPath); break; - default: throw new Error(`Unknown conversion tool: ${tool}`); + try { + const toolAvailable = await checkConverterAvailable(tool); + if (!toolAvailable) { + throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); } - await new Promise((resolve) => { - execFile(conversionInfo.command, conversionInfo.args, (error) => { - if (error) { failed++; } else { completed++; } - resolve(); + // Collect matching files + const files = []; + function collectFiles(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory() && includeSubfolders) { + collectFiles(fullPath); + } else if (entry.isFile() && entry.name.toLowerCase().endsWith(`.${fromFormat}`)) { + files.push(fullPath); + } + } + } + collectFiles(inputFolder); + + if (files.length === 0) { + mainWindow.webContents.send('conversion-complete', { + success: false, + error: `No .${fromFormat} files found in the selected folder.`, }); + return; + } + + let completed = 0; + let failed = 0; + + for (const filePath of files) { + const relativePath = path.relative(inputFolder, filePath); + const outputPath = path.join( + outputFolder, + relativePath.replace(/\.[^/.]+$/, `.${toFormat}`) + ); + + // Ensure output subdirectory exists + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + mainWindow.webContents.send( + 'conversion-status', + `Converting ${completed + 1}/${files.length}: ${path.basename(filePath)}` + ); + + let conversionInfo; + switch (tool) { + case 'libreoffice': + conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); + break; + case 'imagemagick': + conversionInfo = convertWithImageMagick(filePath, outputPath); + break; + case 'ffmpeg': + conversionInfo = convertWithFFmpeg(filePath, outputPath); + break; + case 'pandoc': + conversionInfo = convertWithPandoc(filePath, outputPath); + break; + default: + throw new Error(`Unknown conversion tool: ${tool}`); + } + + await new Promise((resolve) => { + execFile(conversionInfo.command, conversionInfo.args, (error) => { + if (error) { + failed++; + } else { + completed++; + } + resolve(); + }); + }); + } + + mainWindow.webContents.send('conversion-complete', { + success: true, + outputPath: outputFolder, + }); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Batch Conversion Complete', + message: `Batch conversion finished!`, + detail: `Converted: ${completed}/${files.length} files${failed > 0 ? ` (${failed} failed)` : ''}\nOutput: ${outputFolder}`, + buttons: ['OK'], + }); + } catch (error) { + mainWindow.webContents.send('conversion-complete', { success: false, error: error.message }); + dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Batch Conversion Failed', + message: 'Batch conversion failed', + detail: error.message, + buttons: ['OK'], }); } - - mainWindow.webContents.send('conversion-complete', { - success: true, - outputPath: outputFolder - }); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Batch Conversion Complete', - message: `Batch conversion finished!`, - detail: `Converted: ${completed}/${files.length} files${failed > 0 ? ` (${failed} failed)` : ''}\nOutput: ${outputFolder}`, - buttons: ['OK'] - }); - } catch (error) { - mainWindow.webContents.send('conversion-complete', { success: false, error: error.message }); - dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Batch Conversion Failed', - message: 'Batch conversion failed', - detail: error.message, - buttons: ['OK'] - }); } -}); +); // LibreOffice conversion - returns {command, args} for execFile (safer than exec) function convertWithLibreOffice(inputFile, outputFormat, outputPath) { const outputDir = path.dirname(outputPath); - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; + const soffice = + process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; // LibreOffice conversion format mapping const formatMap = { - 'pdf': 'pdf', - 'docx': 'docx', - 'doc': 'doc', - 'odt': 'odt', - 'rtf': 'rtf', - 'txt': 'txt', - 'html': 'html', - 'xlsx': 'xlsx', - 'xls': 'xls', - 'ods': 'ods', - 'csv': 'csv', - 'pptx': 'pptx', - 'ppt': 'ppt', - 'odp': 'odp' + pdf: 'pdf', + docx: 'docx', + doc: 'doc', + odt: 'odt', + rtf: 'rtf', + txt: 'txt', + html: 'html', + xlsx: 'xlsx', + xls: 'xls', + ods: 'ods', + csv: 'csv', + pptx: 'pptx', + ppt: 'ppt', + odp: 'odp', }; const format = formatMap[outputFormat] || outputFormat; @@ -1460,7 +1517,7 @@ function convertWithLibreOffice(inputFile, outputFormat, outputPath) { // Return command and args for execFile return { command: soffice, - args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile] + args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile], }; } @@ -1469,7 +1526,7 @@ function convertWithImageMagick(inputFile, outputPath) { const magick = process.platform === 'win32' ? 'magick' : 'convert'; return { command: magick, - args: [inputFile, outputPath] + args: [inputFile, outputPath], }; } @@ -1485,7 +1542,7 @@ function convertWithFFmpeg(inputFile, outputPath) { function convertWithPandoc(inputFile, outputPath) { return { command: 'pandoc', - args: [inputFile, '-o', outputPath] + args: [inputFile, '-o', outputPath], }; } @@ -1501,9 +1558,7 @@ function performExportWithOptions(format, options) { const outputFile = dialog.showSaveDialogSync(mainWindow, { defaultPath: currentFile.replace(/\.[^/.]+$/, `.${fileExt}`), - filters: [ - { name: format.toUpperCase(), extensions: [fileExt] } - ] + filters: [{ name: format.toUpperCase(), extensions: [fileExt] }], }); if (!outputFile) return; // User cancelled @@ -1511,88 +1566,92 @@ function performExportWithOptions(format, options) { console.log(`Attempting to export ${format} to:`, outputFile); // Check pandoc availability first - checkPandocAvailability().then((hasPandoc) => { - console.log('Pandoc available:', hasPandoc); - - if (!hasPandoc) { - // Handle formats that don't require pandoc - if (format === 'html') { - console.log('Using built-in HTML export'); - exportToHTML(outputFile); - return; - } else if (format === 'pdf') { - console.log('Using built-in PDF export'); - exportToPDFElectron(outputFile); - return; - } else { - dialog.showErrorBox('Export Error', - `Pandoc is required for ${format.toUpperCase()} export but is not installed or not found in PATH.\n\n` + - `Please install Pandoc from: https://pandoc.org/installing.html\n\n` + - `Alternatively, you can export to HTML or PDF using the built-in converters.` - ); - return; - } - } + checkPandocAvailability() + .then((hasPandoc) => { + console.log('Pandoc available:', hasPandoc); - // Use pandoc for export with advanced options - console.log('Using Pandoc for export'); - let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`; - - // Add template if specified - if (options.template && options.template !== 'default') { - pandocCmd += ` --template="${options.template}"`; - } - - // Add metadata - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + if (!hasPandoc) { + // Handle formats that don't require pandoc + if (format === 'html') { + console.log('Using built-in HTML export'); + exportToHTML(outputFile); + return; + } else if (format === 'pdf') { + console.log('Using built-in PDF export'); + exportToPDFElectron(outputFile); + return; + } else { + dialog.showErrorBox( + 'Export Error', + `Pandoc is required for ${format.toUpperCase()} export but is not installed or not found in PATH.\n\n` + + `Please install Pandoc from: https://pandoc.org/installing.html\n\n` + + `Alternatively, you can export to HTML or PDF using the built-in converters.` + ); + return; } } - } - // Add variables - if (options.variables) { - for (const [key, value] of Object.entries(options.variables)) { - if (value.trim()) { - pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; + // Use pandoc for export with advanced options + console.log('Using Pandoc for export'); + let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`; + + // Add template if specified + if (options.template && options.template !== 'default') { + pandocCmd += ` --template="${options.template}"`; + } + + // Add metadata + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } } } - } - // Add other options - if (options.toc) pandocCmd += ' --toc'; - if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; - if (options.numberSections) pandocCmd += ' --number-sections'; - if (options.citeproc) pandocCmd += ' --citeproc'; - if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; - if (options.csl) pandocCmd += ` --csl="${options.csl}"`; + // Add variables + if (options.variables) { + for (const [key, value] of Object.entries(options.variables)) { + if (value.trim()) { + pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } - // Add specific options for PDF export to ensure proper generation - if (format === 'pdf') { - const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex - pandocCmd += ` --pdf-engine="${pdfEngine}"`; - if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; + // Add other options + if (options.toc) pandocCmd += ' --toc'; + if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; + if (options.numberSections) pandocCmd += ' --number-sections'; + if (options.citeproc) pandocCmd += ' --citeproc'; + if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; + if (options.csl) pandocCmd += ` --csl="${options.csl}"`; - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; - pandocCmd += ' --highlight-style=tango'; + // Add specific options for PDF export to ensure proper generation + if (format === 'pdf') { + const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex + pandocCmd += ` --pdf-engine="${pdfEngine}"`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { filename, title: filename, author: '' }; + // Add monospace font settings for code blocks (ASCII art preservation) + pandocCmd += ' -V monofont="Consolas"'; + pandocCmd += ' --highlight-style=tango'; - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + const filename = currentFile + ? path.basename(currentFile, path.extname(currentFile)) + : 'document'; + const metadata = { filename, title: filename, author: '' }; - // Create LaTeX header - const latexHeader = ` + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create LaTeX header + const latexHeader = ` \\usepackage{fancyhdr} \\pagestyle{fancy} \\fancyhf{} @@ -1600,117 +1659,142 @@ function performExportWithOptions(format, options) { \\chead{${headerCenter.replace(/\\/g, '\\\\')}} \\rhead{${headerRight.replace(/\\/g, '\\\\')}} \\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter + .replace(/[$]PAGE[$]/g, '\\\\thepage') + .replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}') + .replace(/\\/g, '\\\\')}} \\rfoot{${footerRight.replace(/\\/g, '\\\\')}} \\renewcommand{\\headrulewidth}{0.4pt} \\renewcommand{\\footrulewidth}{0.4pt} `; - const headerFile = path.join(require('os').tmpdir(), `header_export_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - pandocCmd += ` --include-in-header="${headerFile}"`; - pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; - } + const headerFile = path.join(require('os').tmpdir(), `header_export_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + pandocCmd += ` --include-in-header="${headerFile}"`; + pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; + } - // Try with specified PDF engine (using runPandocCmd for safety) - runPandocCmd(pandocCmd, (error) => { - if (error) { - // Try fallback engines if the specified one fails - const fallbackEngines = ['pdflatex', 'lualatex']; - tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); - } else { - showExportSuccess(outputFile); - } - }); - } else if (format === 'docx') { - pandocCmd += ' -t docx'; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'pptx') { - // Add PowerPoint footer if enabled - if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { filename, title: filename, author: '' }; - const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); - pandocCmd += ` --variable footer="${footerText}"`; - } - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'json') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t json -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'yaml' || format === 'xml' || format === 'toml') { - // For YAML/XML/TOML, save the raw markdown content with the new extension - try { - const content = fs.readFileSync(currentFile, 'utf-8'); - fs.writeFileSync(outputFile, content, 'utf-8'); - showExportSuccess(outputFile); - } catch (err) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export: ${err.message}`)); - } - } else if (format === 'revealjs') { - let revealCmd = `${getPandocPath()} "${currentFile}" -t revealjs -s -o "${outputFile}" --slide-level=2`; - if (options) { - if (options.revealTheme) revealCmd += ` -V theme="${options.revealTheme}"`; - if (options.revealTransition) revealCmd += ` -V transition="${options.revealTransition}"`; - if (options.revealTransitionSpeed) revealCmd += ` -V transitionSpeed="${options.revealTransitionSpeed}"`; - if (options.revealControls !== undefined) revealCmd += ` -V controls="${options.revealControls}"`; - if (options.revealSlideNumber !== undefined) revealCmd += ` -V slideNumber="${options.revealSlideNumber}"`; - if (options.revealProgress !== undefined) revealCmd += ` -V progress="${options.revealProgress}"`; - if (options.revealHistory !== undefined) revealCmd += ` -V history="${options.revealHistory}"`; - if (options.revealCenter !== undefined) revealCmd += ` -V center="${options.revealCenter}"`; - - // Support for templates, metadata, bibliography - if (options.template && options.template !== 'default') { - revealCmd += ` --template="${options.template}"`; - } - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - revealCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - if (options.bibliography) revealCmd += ` --bibliography="${options.bibliography}"`; - if (options.csl) revealCmd += ` --csl="${options.csl}"`; - } - exportWithPandoc(revealCmd, outputFile, format); - } else if (format === 'beamer') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t beamer -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'confluence' || format === 'jira') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t jira -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'mobi') { - // First export to EPUB, then try ebook-convert if available - const epubFile = outputFile.replace(/\.mobi$/i, '.epub'); - pandocCmd = `${getPandocPath()} "${currentFile}" -o "${epubFile}"`; - runPandocCmd(pandocCmd, (error) => { - if (error) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export EPUB intermediate: ${error.message}`)); - return; - } - // Try ebook-convert (Calibre) for MOBI - execFile('ebook-convert', [epubFile, outputFile], (ebookError) => { - if (ebookError) { - dialog.showMessageBox(mainWindow, { - type: 'warning', - title: 'MOBI Export - Partial', - message: `Calibre's ebook-convert was not found. The file has been exported as EPUB instead.\n\nEPUB saved to: ${epubFile}\n\nTo get MOBI output, install Calibre from: https://calibre-ebook.com/`, - buttons: ['OK'] - }); + // Try with specified PDF engine (using runPandocCmd for safety) + runPandocCmd(pandocCmd, (error) => { + if (error) { + // Try fallback engines if the specified one fails + const fallbackEngines = ['pdflatex', 'lualatex']; + tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); } else { - // Clean up intermediate EPUB - try { fs.unlinkSync(epubFile); } catch (e) { /* ignore */ } showExportSuccess(outputFile); } }); - }); - } else { - // Generic export for other formats - exportWithPandoc(pandocCmd, outputFile, format); - } - }).catch((error) => { - console.error('Error checking pandoc availability:', error); - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Error checking system requirements: ${error.message}`)); - }); + } else if (format === 'docx') { + pandocCmd += ' -t docx'; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'pptx') { + // Add PowerPoint footer if enabled + if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { + const filename = currentFile + ? path.basename(currentFile, path.extname(currentFile)) + : 'document'; + const metadata = { filename, title: filename, author: '' }; + const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); + pandocCmd += ` --variable footer="${footerText}"`; + } + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'json') { + pandocCmd = `${getPandocPath()} "${currentFile}" -t json -o "${outputFile}"`; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'yaml' || format === 'xml' || format === 'toml') { + // For YAML/XML/TOML, save the raw markdown content with the new extension + try { + const content = fs.readFileSync(currentFile, 'utf-8'); + fs.writeFileSync(outputFile, content, 'utf-8'); + showExportSuccess(outputFile); + } catch (err) { + dialog.showErrorBox( + 'Export Error', + sanitizeErrorMessage(`Failed to export: ${err.message}`) + ); + } + } else if (format === 'revealjs') { + let revealCmd = `${getPandocPath()} "${currentFile}" -t revealjs -s -o "${outputFile}" --slide-level=2`; + if (options) { + if (options.revealTheme) revealCmd += ` -V theme="${options.revealTheme}"`; + if (options.revealTransition) revealCmd += ` -V transition="${options.revealTransition}"`; + if (options.revealTransitionSpeed) + revealCmd += ` -V transitionSpeed="${options.revealTransitionSpeed}"`; + if (options.revealControls !== undefined) + revealCmd += ` -V controls="${options.revealControls}"`; + if (options.revealSlideNumber !== undefined) + revealCmd += ` -V slideNumber="${options.revealSlideNumber}"`; + if (options.revealProgress !== undefined) + revealCmd += ` -V progress="${options.revealProgress}"`; + if (options.revealHistory !== undefined) + revealCmd += ` -V history="${options.revealHistory}"`; + if (options.revealCenter !== undefined) + revealCmd += ` -V center="${options.revealCenter}"`; + + // Support for templates, metadata, bibliography + if (options.template && options.template !== 'default') { + revealCmd += ` --template="${options.template}"`; + } + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + revealCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + if (options.bibliography) revealCmd += ` --bibliography="${options.bibliography}"`; + if (options.csl) revealCmd += ` --csl="${options.csl}"`; + } + exportWithPandoc(revealCmd, outputFile, format); + } else if (format === 'beamer') { + pandocCmd = `${getPandocPath()} "${currentFile}" -t beamer -o "${outputFile}"`; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'confluence' || format === 'jira') { + pandocCmd = `${getPandocPath()} "${currentFile}" -t jira -o "${outputFile}"`; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'mobi') { + // First export to EPUB, then try ebook-convert if available + const epubFile = outputFile.replace(/\.mobi$/i, '.epub'); + pandocCmd = `${getPandocPath()} "${currentFile}" -o "${epubFile}"`; + runPandocCmd(pandocCmd, (error) => { + if (error) { + dialog.showErrorBox( + 'Export Error', + sanitizeErrorMessage(`Failed to export EPUB intermediate: ${error.message}`) + ); + return; + } + // Try ebook-convert (Calibre) for MOBI + execFile('ebook-convert', [epubFile, outputFile], (ebookError) => { + if (ebookError) { + dialog.showMessageBox(mainWindow, { + type: 'warning', + title: 'MOBI Export - Partial', + message: `Calibre's ebook-convert was not found. The file has been exported as EPUB instead.\n\nEPUB saved to: ${epubFile}\n\nTo get MOBI output, install Calibre from: https://calibre-ebook.com/`, + buttons: ['OK'], + }); + } else { + // Clean up intermediate EPUB + try { + fs.unlinkSync(epubFile); + } catch (e) { + /* ignore */ + } + showExportSuccess(outputFile); + } + }); + }); + } else { + // Generic export for other formats + exportWithPandoc(pandocCmd, outputFile, format); + } + }) + .catch((error) => { + console.error('Error checking pandoc availability:', error); + dialog.showErrorBox( + 'Export Error', + sanitizeErrorMessage(`Error checking system requirements: ${error.message}`) + ); + }); } function tryPdfFallback(inputFile, outputFile, engines, index, options, lastError) { @@ -1729,7 +1813,8 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro pandocCmd += ' --highlight-style=tango'; // Add geometry if specified - if (options.geometry) pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `); + if (options.geometry) + pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `); // Add header/footer if enabled if (headerFooterSettings.enabled) { @@ -1753,7 +1838,10 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro \\chead{${headerCenter.replace(/\\/g, '\\\\')}} \\rhead{${headerRight.replace(/\\/g, '\\\\')}} \\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/\$PAGE\$/g, '\\\\thepage').replace(/\$TOTAL\$/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter + .replace(/\$PAGE\$/g, '\\\\thepage') + .replace(/\$TOTAL\$/g, '\\\\pageref{LastPage}') + .replace(/\\/g, '\\\\')}} \\rfoot{${footerRight.replace(/\\/g, '\\\\')}} \\renewcommand{\\headrulewidth}{0.4pt} \\renewcommand{\\footrulewidth}{0.4pt} @@ -1791,7 +1879,7 @@ function showExportSuccess(outputFile) { type: 'info', title: 'Export Complete', message: `File exported successfully to ${outputFile}`, - buttons: ['OK'] + buttons: ['OK'], }); } @@ -1840,11 +1928,13 @@ function exportWithPandoc(pandocCmd, outputFile, format) { // Add headers/footers to DOCX if enabled if (format === 'docx' && headerFooterSettings.enabled) { try { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const filename = currentFile + ? path.basename(currentFile, path.extname(currentFile)) + : 'document'; const metadata = { filename: filename, title: filename, - author: '' + author: '', }; await addHeaderFooterToDocx(outputFile, metadata); console.log('Headers/footers added to DOCX'); @@ -2011,7 +2101,10 @@ function exportToHTML(outputFile) { showExportSuccess(outputFile); } catch (error) { console.error('HTML export error:', error); - dialog.showErrorBox('HTML Export Error', sanitizeErrorMessage(`Failed to export HTML: ${error.message}`)); + dialog.showErrorBox( + 'HTML Export Error', + sanitizeErrorMessage(`Failed to export HTML: ${error.message}`) + ); } } @@ -2122,34 +2215,44 @@ function exportToPDFElectron(outputFile) { show: false, webPreferences: { nodeIntegration: true, - contextIsolation: false - } + contextIsolation: false, + }, }); - - pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(fullHtml)}`).then(() => { - return pdfWindow.webContents.printToPDF({ - marginsType: 1, // Use default margins - pageSize: 'A4', - printBackground: true, - printSelectionOnly: false, - landscape: false + + pdfWindow + .loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(fullHtml)}`) + .then(() => { + return pdfWindow.webContents.printToPDF({ + marginsType: 1, // Use default margins + pageSize: 'A4', + printBackground: true, + printSelectionOnly: false, + landscape: false, + }); + }) + .then((pdfData) => { + fs.writeFileSync(outputFile, pdfData); + pdfWindow.close(); + console.log('Successfully exported PDF with Electron'); + showExportSuccess(outputFile); + }) + .catch((error) => { + pdfWindow.close(); + console.error('Electron PDF export error:', error); + dialog.showErrorBox( + 'PDF Export Error', + sanitizeErrorMessage( + `Failed to export PDF using built-in engine: ${error.message}\n\n` + + `For better PDF export, please install Pandoc with LaTeX support.` + ) + ); }); - }).then((pdfData) => { - fs.writeFileSync(outputFile, pdfData); - pdfWindow.close(); - console.log('Successfully exported PDF with Electron'); - showExportSuccess(outputFile); - }).catch((error) => { - pdfWindow.close(); - console.error('Electron PDF export error:', error); - dialog.showErrorBox('PDF Export Error', - sanitizeErrorMessage(`Failed to export PDF using built-in engine: ${error.message}\n\n` + - `For better PDF export, please install Pandoc with LaTeX support.`) - ); - }); } catch (error) { console.error('PDF export setup error:', error); - dialog.showErrorBox('PDF Export Error', sanitizeErrorMessage(`Failed to setup PDF export: ${error.message}`)); + dialog.showErrorBox( + 'PDF Export Error', + sanitizeErrorMessage(`Failed to setup PDF export: ${error.message}`) + ); } } @@ -2167,25 +2270,31 @@ function importDocument() { const files = dialog.showOpenDialogSync(mainWindow, { properties: ['openFile'], filters: [ - { name: 'Documents', extensions: ['docx', 'odt', 'rtf', 'html', 'htm', 'tex', 'epub', 'pdf', 'txt'] }, + { + name: 'Documents', + extensions: ['docx', 'odt', 'rtf', 'html', 'htm', 'tex', 'epub', 'pdf', 'txt'], + }, { name: 'Presentations', extensions: ['pptx', 'odp'] }, - { name: 'Markup Languages', extensions: ['rst', 'textile', 'mediawiki', 'org', 'asciidoc', 'twiki', 'opml'] }, + { + name: 'Markup Languages', + extensions: ['rst', 'textile', 'mediawiki', 'org', 'asciidoc', 'twiki', 'opml'], + }, { name: 'E-book Formats', extensions: ['epub', 'fb2'] }, { name: 'LaTeX Formats', extensions: ['tex', 'latex', 'ltx'] }, { name: 'Web Formats', extensions: ['html', 'htm', 'xhtml'] }, { name: 'Wiki Formats', extensions: ['mediawiki', 'dokuwiki', 'tikiwiki', 'twiki'] }, { name: 'CSV/TSV', extensions: ['csv', 'tsv'] }, { name: 'Developer Formats', extensions: ['json', 'yaml', 'yml', 'xml', 'toml'] }, - { name: 'All Files', extensions: ['*'] } - ] + { name: 'All Files', extensions: ['*'] }, + ], }); if (files && files[0]) { const inputFile = files[0]; const stats = fs.statSync(inputFile); if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; } const ext = path.extname(inputFile).toLowerCase().slice(1); const outputFile = inputFile.replace(/\.[^/.]+$/, '.md'); @@ -2220,7 +2329,7 @@ function importDocument() { type: 'info', title: 'Import Complete', message: `Document imported successfully as ${path.basename(mdOutputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, - buttons: ['OK'] + buttons: ['OK'], }); return; } @@ -2230,7 +2339,12 @@ function importDocument() { runPandocCmd(pandocCmd, (error, stdout, stderr) => { if (error) { - dialog.showErrorBox('Import Error', sanitizeErrorMessage(`Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`)); + dialog.showErrorBox( + 'Import Error', + sanitizeErrorMessage( + `Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.` + ) + ); } else { // Open the converted markdown file currentFile = outputFile; @@ -2241,14 +2355,13 @@ function importDocument() { type: 'info', title: 'Import Complete', message: `Document imported successfully as ${path.basename(outputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, - buttons: ['OK'] + buttons: ['OK'], }); } }); } } - function setTheme(theme) { store.set('theme', theme); mainWindow.webContents.send('theme-changed', theme); @@ -2312,7 +2425,7 @@ ipcMain.on('do-print', (event, { withStyles }) => { silent: false, printBackground: withStyles, color: true, - margin: { marginType: 'default' } + margin: { marginType: 'default' }, }); } }); @@ -2322,10 +2435,10 @@ ipcMain.on('do-print-with-options', (event, options) => { if (!mainWindow) return; const marginsMap = { - 'default': { marginType: 'default' }, - 'narrow': { top: 0.4, bottom: 0.4, left: 0.4, right: 0.4 }, - 'wide': { top: 1.0, bottom: 1.0, left: 1.0, right: 1.0 }, - 'none': { top: 0, bottom: 0, left: 0, right: 0 }, + default: { marginType: 'default' }, + narrow: { top: 0.4, bottom: 0.4, left: 0.4, right: 0.4 }, + wide: { top: 1.0, bottom: 1.0, left: 1.0, right: 1.0 }, + none: { top: 0, bottom: 0, left: 0, right: 0 }, }; const printOptions = { @@ -2347,17 +2460,23 @@ ipcMain.on('do-print-with-options', (event, options) => { ipcMain.on('renderer-ready', (event) => { console.log('[MAIN] renderer-ready received, rendererReady was:', rendererReady); if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] renderer-ready received, rendererReady was: ${rendererReady}')`); + mainWindow.webContents.executeJavaScript( + `console.log('[MAIN->RENDERER] renderer-ready received, rendererReady was: ${rendererReady}')` + ); } rendererReady = true; console.log('[MAIN] app.pendingFile:', app.pendingFile); if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] app.pendingFile: ${app.pendingFile}')`); + mainWindow.webContents.executeJavaScript( + `console.log('[MAIN->RENDERER] app.pendingFile: ${app.pendingFile}')` + ); } if (app.pendingFile) { console.log('[MAIN] Opening pending file:', app.pendingFile); if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] Opening pending file: ${app.pendingFile}')`); + mainWindow.webContents.executeJavaScript( + `console.log('[MAIN->RENDERER] Opening pending file: ${app.pendingFile}')` + ); } openFileFromPath(app.pendingFile); app.pendingFile = null; @@ -2385,7 +2504,7 @@ ipcMain.on('batch-convert', (event, { inputFolder, outputFolder, format, options // Handle folder selection for batch conversion ipcMain.on('select-folder', (event, type) => { const folder = dialog.showOpenDialogSync(mainWindow, { - properties: ['openDirectory'] + properties: ['openDirectory'], }); if (folder && folder[0]) { @@ -2396,9 +2515,7 @@ ipcMain.on('select-folder', (event, type) => { ipcMain.on('export-spreadsheet', (event, { content, format }) => { const outputFile = dialog.showSaveDialogSync(mainWindow, { defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), - filters: [ - { name: format.toUpperCase(), extensions: [format] } - ] + filters: [{ name: format.toUpperCase(), extensions: [format] }], }); if (outputFile) { @@ -2418,13 +2535,18 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => { if (index > 0) csvContent += '\n\n'; // Separate multiple tables if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`; - table.forEach(row => { - const csvRow = row.map(cell => { - // Escape quotes and wrap in quotes if necessary - const cleanCell = cell.replace(/"/g, '""'); - return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n') - ? `"${cleanCell}"` : cleanCell; - }).join(','); + table.forEach((row) => { + const csvRow = row + .map((cell) => { + // Escape quotes and wrap in quotes if necessary + const cleanCell = cell.replace(/"/g, '""'); + return cleanCell.includes(',') || + cleanCell.includes('"') || + cleanCell.includes('\n') + ? `"${cleanCell}"` + : cleanCell; + }) + .join(','); csvContent += csvRow + '\n'; }); }); @@ -2436,10 +2558,13 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => { type: 'info', title: 'Export Complete', message: `${format.toUpperCase()} exported successfully to ${outputFile}`, - buttons: ['OK'] + buttons: ['OK'], }); } catch (error) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export: ${error.message}`)); + dialog.showErrorBox( + 'Export Error', + sanitizeErrorMessage(`Failed to export: ${error.message}`) + ); } } }); @@ -2450,20 +2575,21 @@ function extractTablesFromMarkdown(markdown) { const lines = markdown.split('\n'); let currentTable = []; let inTable = false; - + for (const line of lines) { if (line.includes('|')) { if (!inTable) { inTable = true; currentTable = []; } - + // Skip separator lines (|---|---|) if (!line.match(/^\s*\|?\s*:?-+:?\s*\|/)) { - const cells = line.split('|') - .map(cell => cell.trim()) - .filter(cell => cell !== ''); - + const cells = line + .split('|') + .map((cell) => cell.trim()) + .filter((cell) => cell !== ''); + if (cells.length > 0) { currentTable.push(cells); } @@ -2477,12 +2603,12 @@ function extractTablesFromMarkdown(markdown) { inTable = false; } } - + // Add last table if exists if (currentTable.length > 0) { tables.push(currentTable); } - + return tables; } @@ -2497,7 +2623,10 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { try { fs.mkdirSync(outputFolder, { recursive: true }); } catch (error) { - dialog.showErrorBox('Error', sanitizeErrorMessage(`Failed to create output folder: ${error.message}`)); + dialog.showErrorBox( + 'Error', + sanitizeErrorMessage(`Failed to create output folder: ${error.message}`) + ); return; } } @@ -2539,7 +2668,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { type: 'info', title: 'Batch Conversion Complete', message: `Successfully converted ${completedCount} out of ${totalCount} files to ${format.toUpperCase()}.`, - buttons: ['OK'] + buttons: ['OK'], }); return; } @@ -2550,7 +2679,10 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { let outputExtension = format; if (format === 'docx-enhanced') outputExtension = 'docx'; if (format === 'pdf-enhanced') outputExtension = 'pdf'; - const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`)); + const outputFile = path.join( + outputFolder, + relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`) + ); // Create subdirectories in output folder if needed const outputDir = path.dirname(outputFile); @@ -2572,7 +2704,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { completed: index + 1, total: totalCount, currentFile: path.basename(inputFile), - success: true + success: true, }); // Process next file @@ -2583,7 +2715,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { completed: index + 1, total: totalCount, currentFile: path.basename(inputFile), - success: false + success: false, }); // Process next file even if this one failed @@ -2603,12 +2735,20 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { await exporter.convert(content, tempDocxPath); // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety) - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; + const soffice = + process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; const outputDir = path.dirname(outputFile); - const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; + const sofficeArgs = [ + '--headless', + '--convert-to', + 'pdf', + '--outdir', + outputDir, + tempDocxPath, + ]; execFile(soffice, sofficeArgs, (error, stdout, stderr) => { // Clean up temporary DOCX file @@ -2624,7 +2764,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { completed: index + 1, total: totalCount, currentFile: path.basename(inputFile), - success: false + success: false, }); processNextFile(index + 1); return; @@ -2649,20 +2789,19 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { completed: index + 1, total: totalCount, currentFile: path.basename(inputFile), - success: true + success: true, }); // Process next file processNextFile(index + 1); }); - } catch (error) { // Update progress with error mainWindow.webContents.send('batch-progress', { completed: index + 1, total: totalCount, currentFile: path.basename(inputFile), - success: false + success: false, }); // Process next file even if this one failed @@ -2736,7 +2875,10 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { \\chead{${headerCenter.replace(/\\/g, '\\\\')}} \\rhead{${headerRight.replace(/\\/g, '\\\\')}} \\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter + .replace(/[$]PAGE[$]/g, '\\\\thepage') + .replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}') + .replace(/\\/g, '\\\\')}} \\rfoot{${footerRight.replace(/\\/g, '\\\\')}} \\renewcommand{\\headrulewidth}{0.4pt} \\renewcommand{\\footrulewidth}{0.4pt} @@ -2782,7 +2924,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { completed: index + 1, total: totalCount, currentFile: path.basename(inputFile), - success: !error + success: !error, }); // Process next file @@ -2798,7 +2940,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { function handleCLIConversion(args) { const command = args[0]; const filePath = args[args.length - 1]; // File path is always last argument - + if (!fs.existsSync(filePath)) { console.error(`Error: File not found: ${filePath}`); app.quit(); @@ -2825,14 +2967,14 @@ function handleCLIConversion(args) { // Show conversion dialog for CLI function showConversionDialog(filePath) { const { dialog } = require('electron'); - + // Create a hidden window for dialog operations const hiddenWindow = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, - contextIsolation: false - } + contextIsolation: false, + }, }); const formats = [ @@ -2842,31 +2984,33 @@ function showConversionDialog(filePath) { { name: 'LaTeX', value: 'latex' }, { name: 'RTF', value: 'rtf' }, { name: 'ODT', value: 'odt' }, - { name: 'PowerPoint', value: 'pptx' } + { name: 'PowerPoint', value: 'pptx' }, ]; // Create format selection dialog using message box - const formatButtons = formats.map(f => f.name); + const formatButtons = formats.map((f) => f.name); formatButtons.push('Cancel'); - dialog.showMessageBox(hiddenWindow, { - type: 'question', - title: 'PanConverter - Choose Format', - message: `Convert "${path.basename(filePath)}" to:`, - detail: 'Select the output format for conversion', - buttons: formatButtons, - defaultId: 0, - cancelId: formatButtons.length - 1 - }).then(result => { - if (result.response < formats.length) { - const selectedFormat = formats[result.response].value; - performCLIConversion(filePath, selectedFormat); - } else { - console.log('Conversion cancelled'); - app.quit(); - } - hiddenWindow.destroy(); - }); + dialog + .showMessageBox(hiddenWindow, { + type: 'question', + title: 'PanConverter - Choose Format', + message: `Convert "${path.basename(filePath)}" to:`, + detail: 'Select the output format for conversion', + buttons: formatButtons, + defaultId: 0, + cancelId: formatButtons.length - 1, + }) + .then((result) => { + if (result.response < formats.length) { + const selectedFormat = formats[result.response].value; + performCLIConversion(filePath, selectedFormat); + } else { + console.log('Conversion cancelled'); + app.quit(); + } + hiddenWindow.destroy(); + }); } // Perform CLI conversion @@ -2874,9 +3018,9 @@ function performCLIConversion(inputPath, format) { try { const content = fs.readFileSync(inputPath, 'utf-8'); const outputPath = inputPath.replace(/\.[^/.]+$/, `.${format}`); - + console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`); - + // Use existing export functions but with CLI output (using runPandocCmd for safety) const pandocCommand = buildPandocCommand(content, format, outputPath); @@ -2893,7 +3037,14 @@ function performCLIConversion(inputPath, format) { // Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command) if (process.platform === 'win32') { const iconPath = path.join(__dirname, '../assets/icon.png'); - execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {}); + execFile( + 'powershell', + [ + '-Command', + `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`, + ], + () => {} + ); } app.quit(); @@ -2969,7 +3120,10 @@ function buildPandocCommand(content, format, outputPath) { \\chead{${headerCenter.replace(/\\/g, '\\\\')}} \\rhead{${headerRight.replace(/\\/g, '\\\\')}} \\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter + .replace(/[$]PAGE[$]/g, '\\\\thepage') + .replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}') + .replace(/\\/g, '\\\\')}} \\rfoot{${footerRight.replace(/\\/g, '\\\\')}} \\renewcommand{\\headrulewidth}{0.4pt} \\renewcommand{\\footrulewidth}{0.4pt} @@ -3070,7 +3224,9 @@ app.whenReady().then(() => { } mainWindow = createMainWindow(); - mainWindow.on('closed', () => { mainWindow = null; }); + mainWindow.on('closed', () => { + mainWindow = null; + }); // --- Updater, crash writer, migration (must run before any settings read) --- const userData = app.getPath('userData'); @@ -3108,10 +3264,14 @@ app.whenReady().then(() => { validatePath, resolveWritablePath, isPathAccessible, - currentFileRef: { get current() { return currentFile; } }, + currentFileRef: { + get current() { + return currentFile; + }, + }, mainWindow, }); - + // Handle file association on app startup // In packaged apps, process.argv structure is different: // Development: ['electron', 'app.js', 'file.md'] - need slice(2) @@ -3127,7 +3287,7 @@ app.whenReady().then(() => { for (const arg of fileArgs) { console.log('[MAIN] Checking arg:', arg); - if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { + if (arg.endsWith('.md') || arg.endsWith('.markdown')) { // Try to resolve the path (might be relative) const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(process.cwd(), arg); console.log('[MAIN] Resolved path:', resolvedPath); @@ -3153,7 +3313,9 @@ app.on('window-all-closed', () => { app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createMainWindow(); - mainWindow.on('closed', () => { mainWindow = null; }); + mainWindow.on('closed', () => { + mainWindow = null; + }); } }); @@ -3195,17 +3357,31 @@ app.on('open-file', (event, filePath) => { // Handle file opening from command line or file association function openFileFromPath(filePath) { console.log('[MAIN] openFileFromPath called with:', filePath); - console.log('[MAIN] rendererReady:', rendererReady, 'mainWindow exists:', !!mainWindow, 'webContents exists:', !!(mainWindow?.webContents)); + console.log( + '[MAIN] rendererReady:', + rendererReady, + 'mainWindow exists:', + !!mainWindow, + 'webContents exists:', + !!mainWindow?.webContents + ); if (fs.existsSync(filePath)) { const stats = fs.statSync(filePath); if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; } currentFile = filePath; const content = fs.readFileSync(filePath, 'utf-8'); console.log('[MAIN] File read successfully, content length:', content.length); - console.log('[MAIN] About to send file-opened. mainWindow:', !!mainWindow, 'webContents:', !!(mainWindow?.webContents), 'rendererReady:', rendererReady); + console.log( + '[MAIN] About to send file-opened. mainWindow:', + !!mainWindow, + 'webContents:', + !!mainWindow?.webContents, + 'rendererReady:', + rendererReady + ); if (mainWindow && mainWindow.webContents && rendererReady) { // Send file immediately - renderer-ready means UI is initialized console.log('[MAIN] Sending file-opened to renderer'); @@ -3228,7 +3404,7 @@ ipcMain.on('process-pdf-operation', async (event, data) => { try { mainWindow.webContents.send('pdf-operation-progress', { message: `Processing ${data.operation}...`, - progress: 10 + progress: 10, }); const result = await PDFOperations.executeOperation(data.operation, data); @@ -3236,7 +3412,7 @@ ipcMain.on('process-pdf-operation', async (event, data) => { } catch (error) { mainWindow.webContents.send('pdf-operation-complete', { success: false, - error: error.message + error: error.message, }); } }); @@ -3253,7 +3429,7 @@ ipcMain.on('get-pdf-page-count', async (event, filePath) => { // IPC Handler for folder selection (for PDF operations) ipcMain.on('select-pdf-folder', (event, inputId) => { const folder = dialog.showOpenDialogSync(mainWindow, { - properties: ['openDirectory'] + properties: ['openDirectory'], }); if (folder && folder[0]) { @@ -3315,7 +3491,7 @@ ipcMain.handle('list-directory', async (event, dirPath) => { try { if (!dirPath) { const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openDirectory'] + properties: ['openDirectory'], }); if (result.canceled || !result.filePaths[0]) return null; dirPath = result.filePaths[0]; @@ -3332,19 +3508,20 @@ ipcMain.handle('list-directory', async (event, dirPath) => { return null; } - const entries = fs.readdirSync(validation.resolved, { withFileTypes: true }) - .filter(e => !e.name.startsWith('.')) + const entries = fs + .readdirSync(validation.resolved, { withFileTypes: true }) + .filter((e) => !e.name.startsWith('.')) .sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }) - .map(e => ({ + .map((e) => ({ name: e.name, isDirectory: e.isDirectory(), size: e.isDirectory() ? 0 : fs.statSync(path.join(validation.resolved, e.name)).size, modified: fs.statSync(path.join(validation.resolved, e.name)).mtimeMs, - path: path.join(validation.resolved, e.name) + path: path.join(validation.resolved, e.name), })); return { path: validation.resolved, entries }; } catch (err) { @@ -3357,9 +3534,7 @@ ipcMain.handle('select-custom-css', async (event) => { const result = dialog.showOpenDialogSync(mainWindow, { title: 'Select Custom Preview CSS', properties: ['openFile'], - filters: [ - { name: 'CSS Stylesheets', extensions: ['css'] } - ] + filters: [{ name: 'CSS Stylesheets', extensions: ['css'] }], }); if (result && result[0]) { @@ -3380,7 +3555,9 @@ function loadSnippets() { if (fs.existsSync(snippetsPath)) { return JSON.parse(fs.readFileSync(snippetsPath, 'utf-8')); } - } catch (err) { console.error('Failed to load snippets:', err); } + } catch (err) { + console.error('Failed to load snippets:', err); + } return []; } @@ -3392,7 +3569,7 @@ ipcMain.handle('get-snippets', async () => loadSnippets()); ipcMain.handle('save-snippet', async (event, snippet) => { const snippets = loadSnippets(); - const existing = snippets.findIndex(s => s.id === snippet.id); + const existing = snippets.findIndex((s) => s.id === snippet.id); if (existing >= 0) snippets[existing] = snippet; else snippets.push(snippet); saveSnippetsFile(snippets); @@ -3400,7 +3577,7 @@ ipcMain.handle('save-snippet', async (event, snippet) => { }); ipcMain.handle('delete-snippet', async (event, id) => { - const snippets = loadSnippets().filter(s => s.id !== id); + const snippets = loadSnippets().filter((s) => s.id !== id); saveSnippetsFile(snippets); return snippets; }); @@ -3431,7 +3608,7 @@ ipcMain.handle('execute-code', async (event, { code, language }) => { resolve({ stdout: stdout || '', stderr: stderr || '', - error: err?.killed ? 'Execution timed out (10s limit)' : (err?.message || null) + error: err?.killed ? 'Execution timed out (10s limit)' : err?.message || null, }); }); }); diff --git a/src/main/ipc/crash-handlers.js b/src/main/ipc/crash-handlers.js index 9de58b5..0b3c5aa 100644 --- a/src/main/ipc/crash-handlers.js +++ b/src/main/ipc/crash-handlers.js @@ -10,7 +10,10 @@ function register({ crash, getMainWindow }) { }); ipcMain.handle('crash:delete', (_event, filename) => { - if (typeof filename === 'string' && /^\d+(-\d+)?-(uncaughtException|unhandledRejection)\.json$/.test(filename)) { + if ( + typeof filename === 'string' && + /^\d+(-\d+)?-(uncaughtException|unhandledRejection)\.json$/.test(filename) + ) { crash.delete(filename); return true; } diff --git a/src/main/menu/index.js b/src/main/menu/index.js index 4aa762b..8560f00 100644 --- a/src/main/menu/index.js +++ b/src/main/menu/index.js @@ -10,7 +10,7 @@ const { convertItems, pdfEditorItems, toolsItems, - helpItems + helpItems, } = require('./items'); function buildMenu(mainWindow) { @@ -22,7 +22,7 @@ function buildMenu(mainWindow) { { label: '&Convert', submenu: convertItems(mainWindow) }, { label: 'PDF Editor', submenu: pdfEditorItems(mainWindow) }, { label: '&Tools', submenu: toolsItems(mainWindow) }, - { label: '&Help', submenu: helpItems(mainWindow) } + { label: '&Help', submenu: helpItems(mainWindow) }, ]; return Menu.buildFromTemplate(template); } @@ -31,4 +31,4 @@ function register(mainWindow) { Menu.setApplicationMenu(buildMenu(mainWindow)); } -module.exports = { register, buildMenu }; \ No newline at end of file +module.exports = { register, buildMenu }; diff --git a/src/main/menu/items.js b/src/main/menu/items.js index d61eb55..94f8c4c 100644 --- a/src/main/menu/items.js +++ b/src/main/menu/items.js @@ -11,14 +11,14 @@ function buildRecentFilesMenu(mainWindow) { const recentFilesPath = path.join(app.getPath('userData'), 'recent-files.json'); if (!fs.existsSync(recentFilesPath)) return [{ label: 'No Recent Files', enabled: false }]; const recentFiles = JSON.parse(fs.readFileSync(recentFilesPath, 'utf-8')); - const existing = recentFiles.filter(file => fs.existsSync(file)); + const existing = recentFiles.filter((file) => fs.existsSync(file)); if (existing.length === 0) return [{ label: 'No Recent Files', enabled: false }]; - const items = existing.map(file => ({ + const items = existing.map((file) => ({ label: path.basename(file), click: () => { const { openFileFromPath } = require('../index'); openFileFromPath(file); - } + }, })); items.push( { type: 'separator' }, @@ -28,7 +28,7 @@ function buildRecentFilesMenu(mainWindow) { if (mainWindow) { mainWindow.webContents.send('clear-recent-files'); } - } + }, } ); return items; @@ -42,7 +42,7 @@ function fileItems(mainWindow) { { label: 'New', accelerator: 'CmdOrCtrl+N', - click: () => mainWindow.webContents.send('file-new') + click: () => mainWindow.webContents.send('file-new'), }, { label: 'Open', @@ -50,7 +50,7 @@ function fileItems(mainWindow) { click: () => { const { openFile } = require('../index'); openFile(); - } + }, }, { label: 'Open PDF', @@ -58,12 +58,12 @@ function fileItems(mainWindow) { click: () => { const { openPdfFile } = require('../index'); openPdfFile(); - } + }, }, { label: 'Save', accelerator: 'CmdOrCtrl+S', - click: () => mainWindow.webContents.send('file-save') + click: () => mainWindow.webContents.send('file-save'), }, { label: 'Save As', @@ -71,30 +71,60 @@ function fileItems(mainWindow) { click: () => { const { saveAsFile } = require('../index'); saveAsFile(); - } + }, }, { type: 'separator' }, // NOTE: Print Preview submenu removed — handled by React overlay { type: 'separator' }, { label: 'Recent Files', - submenu: buildRecentFilesMenu(mainWindow) + submenu: buildRecentFilesMenu(mainWindow), }, { type: 'separator' }, { label: 'New from Template', submenu: [ - { label: 'Blog Post', click: () => mainWindow.webContents.send('load-template-menu', 'blog-post.md') }, - { label: 'Meeting Notes', click: () => mainWindow.webContents.send('load-template-menu', 'meeting-notes.md') }, - { label: 'Technical Spec', click: () => mainWindow.webContents.send('load-template-menu', 'technical-spec.md') }, - { label: 'Changelog', click: () => mainWindow.webContents.send('load-template-menu', 'changelog.md') }, - { label: 'README', click: () => mainWindow.webContents.send('load-template-menu', 'readme.md') }, - { label: 'Project Plan', click: () => mainWindow.webContents.send('load-template-menu', 'project-plan.md') }, - { label: 'API Documentation', click: () => mainWindow.webContents.send('load-template-menu', 'api-docs.md') }, - { label: 'Tutorial', click: () => mainWindow.webContents.send('load-template-menu', 'tutorial.md') }, - { label: 'Release Notes', click: () => mainWindow.webContents.send('load-template-menu', 'release-notes.md') }, - { label: 'Comparison', click: () => mainWindow.webContents.send('load-template-menu', 'comparison.md') } - ] + { + label: 'Blog Post', + click: () => mainWindow.webContents.send('load-template-menu', 'blog-post.md'), + }, + { + label: 'Meeting Notes', + click: () => mainWindow.webContents.send('load-template-menu', 'meeting-notes.md'), + }, + { + label: 'Technical Spec', + click: () => mainWindow.webContents.send('load-template-menu', 'technical-spec.md'), + }, + { + label: 'Changelog', + click: () => mainWindow.webContents.send('load-template-menu', 'changelog.md'), + }, + { + label: 'README', + click: () => mainWindow.webContents.send('load-template-menu', 'readme.md'), + }, + { + label: 'Project Plan', + click: () => mainWindow.webContents.send('load-template-menu', 'project-plan.md'), + }, + { + label: 'API Documentation', + click: () => mainWindow.webContents.send('load-template-menu', 'api-docs.md'), + }, + { + label: 'Tutorial', + click: () => mainWindow.webContents.send('load-template-menu', 'tutorial.md'), + }, + { + label: 'Release Notes', + click: () => mainWindow.webContents.send('load-template-menu', 'release-notes.md'), + }, + { + label: 'Comparison', + click: () => mainWindow.webContents.send('load-template-menu', 'comparison.md'), + }, + ], }, { type: 'separator' }, { @@ -103,58 +133,137 @@ function fileItems(mainWindow) { click: () => { const { importDocument } = require('../index'); importDocument(); - } + }, }, { label: 'Export', submenu: [ { - label: 'HTML', click: () => { + label: 'HTML', + click: () => { const { exportFile } = require('../index'); exportFile('html'); - } + }, }, { - label: 'PDF', click: () => { + label: 'PDF', + click: () => { const { exportFile } = require('../index'); exportFile('pdf'); - } + }, }, { - label: 'PDF (Enhanced)', click: () => { + label: 'PDF (Enhanced)', + click: () => { const { exportPDFViaWordTemplate } = require('../index'); exportPDFViaWordTemplate(); - }, accelerator: 'Ctrl+Shift+P' + }, + accelerator: 'Ctrl+Shift+P', }, { - label: 'DOCX', click: () => { + label: 'DOCX', + click: () => { const { exportFile } = require('../index'); exportFile('docx'); - } + }, }, { - label: 'DOCX (Enhanced)', click: () => { + label: 'DOCX (Enhanced)', + click: () => { const { exportWordWithTemplate } = require('../index'); exportWordWithTemplate(); - }, accelerator: 'Ctrl+Shift+W' + }, + accelerator: 'Ctrl+Shift+W', + }, + { + label: 'LaTeX', + click: () => { + const { exportFile } = require('../index'); + exportFile('latex'); + }, + }, + { + label: 'RTF', + click: () => { + const { exportFile } = require('../index'); + exportFile('rtf'); + }, + }, + { + label: 'ODT', + click: () => { + const { exportFile } = require('../index'); + exportFile('odt'); + }, + }, + { + label: 'EPUB', + click: () => { + const { exportFile } = require('../index'); + exportFile('epub'); + }, }, - { label: 'LaTeX', click: () => { const { exportFile } = require('../index'); exportFile('latex'); } }, - { label: 'RTF', click: () => { const { exportFile } = require('../index'); exportFile('rtf'); } }, - { label: 'ODT', click: () => { const { exportFile } = require('../index'); exportFile('odt'); } }, - { label: 'EPUB', click: () => { const { exportFile } = require('../index'); exportFile('epub'); } }, { type: 'separator' }, - { label: 'PowerPoint (PPTX)', click: () => { const { exportFile } = require('../index'); exportFile('pptx'); } }, - { label: 'OpenDocument Presentation (ODP)', click: () => { const { exportFile } = require('../index'); exportFile('odp'); } }, + { + label: 'PowerPoint (PPTX)', + click: () => { + const { exportFile } = require('../index'); + exportFile('pptx'); + }, + }, + { + label: 'OpenDocument Presentation (ODP)', + click: () => { + const { exportFile } = require('../index'); + exportFile('odp'); + }, + }, { type: 'separator' }, - { label: 'CSV (Tables)', click: () => { const { exportSpreadsheet } = require('../index'); exportSpreadsheet('csv'); } }, + { + label: 'CSV (Tables)', + click: () => { + const { exportSpreadsheet } = require('../index'); + exportSpreadsheet('csv'); + }, + }, { type: 'separator' }, - { label: 'JSON (.json)', click: () => { const { exportFile } = require('../index'); exportFile('json'); } }, - { label: 'YAML (.yaml)', click: () => { const { exportFile } = require('../index'); exportFile('yaml'); } }, - { label: 'XML (.xml)', click: () => { const { exportFile } = require('../index'); exportFile('xml'); } }, + { + label: 'JSON (.json)', + click: () => { + const { exportFile } = require('../index'); + exportFile('json'); + }, + }, + { + label: 'YAML (.yaml)', + click: () => { + const { exportFile } = require('../index'); + exportFile('yaml'); + }, + }, + { + label: 'XML (.xml)', + click: () => { + const { exportFile } = require('../index'); + exportFile('xml'); + }, + }, { type: 'separator' }, - { label: 'Confluence Wiki (.txt)', click: () => { const { exportFile } = require('../index'); exportFile('confluence'); } }, - { label: 'MOBI E-book (.mobi)', click: () => { const { exportFile } = require('../index'); exportFile('mobi'); } } - ] + { + label: 'Confluence Wiki (.txt)', + click: () => { + const { exportFile } = require('../index'); + exportFile('confluence'); + }, + }, + { + label: 'MOBI E-book (.mobi)', + click: () => { + const { exportFile } = require('../index'); + exportFile('mobi'); + }, + }, + ], }, { type: 'separator' }, { @@ -162,14 +271,14 @@ function fileItems(mainWindow) { click: () => { const { selectWordTemplate } = require('../index'); selectWordTemplate(); - } + }, }, { label: 'Template Settings...', click: () => { const { showTemplateSettings } = require('../index'); showTemplateSettings(); - } + }, }, { label: 'Header & Footer Settings...', @@ -177,14 +286,14 @@ function fileItems(mainWindow) { if (mainWindow) { mainWindow.webContents.send('open-header-footer-dialog'); } - } + }, }, { type: 'separator' }, { label: 'Quit', accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', - click: () => app.quit() - } + click: () => app.quit(), + }, ]; } @@ -193,12 +302,12 @@ function editItems(mainWindow) { { label: 'Undo', accelerator: 'CmdOrCtrl+Z', - click: () => mainWindow.webContents.send('undo') + click: () => mainWindow.webContents.send('undo'), }, { label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', - click: () => mainWindow.webContents.send('redo') + click: () => mainWindow.webContents.send('redo'), }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, @@ -209,8 +318,8 @@ function editItems(mainWindow) { { label: 'Find & Replace', accelerator: 'CmdOrCtrl+F', - click: () => mainWindow.webContents.send('toggle-find') - } + click: () => mainWindow.webContents.send('toggle-find'), + }, ]; } @@ -219,60 +328,222 @@ function viewItems(mainWindow) { { label: 'Toggle Preview', accelerator: 'CmdOrCtrl+Shift+V', - click: () => mainWindow.webContents.send('toggle-preview') + click: () => mainWindow.webContents.send('toggle-preview'), }, { label: 'Writing Analytics', accelerator: 'CmdOrCtrl+Shift+A', - click: () => mainWindow.webContents.send('show-analytics-dialog') + click: () => mainWindow.webContents.send('show-analytics-dialog'), }, // NOTE: Command Palette removed — handled by useCommandStore { type: 'separator' }, { label: 'Sidebar', submenu: [ - { label: 'Files', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'explorer') }, - { label: 'Outline', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'outline') }, - { label: 'Snippets', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'snippets') }, - { label: 'Templates', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'templates') }, - { label: 'Git', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'git') } - ] + { + label: 'Files', + click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'explorer'), + }, + { + label: 'Outline', + click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'outline'), + }, + { + label: 'Snippets', + click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'snippets'), + }, + { + label: 'Templates', + click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'templates'), + }, + { label: 'Git', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'git') }, + ], }, { label: 'Bottom Panel (REPL)', - click: () => mainWindow.webContents.send('toggle-bottom-panel') + click: () => mainWindow.webContents.send('toggle-bottom-panel'), }, { type: 'separator' }, { label: 'Theme', submenu: [ - { label: 'Atom One Light (Default)', click: () => { const { setTheme } = require('../index'); setTheme('atomonelight'); } }, - { label: 'GitHub Light', click: () => { const { setTheme } = require('../index'); setTheme('github'); } }, - { label: 'Light', click: () => { const { setTheme } = require('../index'); setTheme('light'); } }, - { label: 'Solarized Light', click: () => { const { setTheme } = require('../index'); setTheme('solarized'); } }, - { label: 'Gruvbox Light', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-light'); } }, - { label: 'Ayu Light', click: () => { const { setTheme } = require('../index'); setTheme('ayu-light'); } }, - { label: 'Sepia', click: () => { const { setTheme } = require('../index'); setTheme('sepia'); } }, - { label: 'Paper', click: () => { const { setTheme } = require('../index'); setTheme('paper'); } }, - { label: 'Rose Pine Dawn', click: () => { const { setTheme } = require('../index'); setTheme('rosepine-dawn'); } }, - { label: 'Concrete Light', click: () => { const { setTheme } = require('../index'); setTheme('concrete-light'); } }, + { + label: 'Atom One Light (Default)', + click: () => { + const { setTheme } = require('../index'); + setTheme('atomonelight'); + }, + }, + { + label: 'GitHub Light', + click: () => { + const { setTheme } = require('../index'); + setTheme('github'); + }, + }, + { + label: 'Light', + click: () => { + const { setTheme } = require('../index'); + setTheme('light'); + }, + }, + { + label: 'Solarized Light', + click: () => { + const { setTheme } = require('../index'); + setTheme('solarized'); + }, + }, + { + label: 'Gruvbox Light', + click: () => { + const { setTheme } = require('../index'); + setTheme('gruvbox-light'); + }, + }, + { + label: 'Ayu Light', + click: () => { + const { setTheme } = require('../index'); + setTheme('ayu-light'); + }, + }, + { + label: 'Sepia', + click: () => { + const { setTheme } = require('../index'); + setTheme('sepia'); + }, + }, + { + label: 'Paper', + click: () => { + const { setTheme } = require('../index'); + setTheme('paper'); + }, + }, + { + label: 'Rose Pine Dawn', + click: () => { + const { setTheme } = require('../index'); + setTheme('rosepine-dawn'); + }, + }, + { + label: 'Concrete Light', + click: () => { + const { setTheme } = require('../index'); + setTheme('concrete-light'); + }, + }, { type: 'separator' }, - { label: 'Dark', click: () => { const { setTheme } = require('../index'); setTheme('dark'); } }, - { label: 'One Dark', click: () => { const { setTheme } = require('../index'); setTheme('onedark'); } }, - { label: 'Dracula', click: () => { const { setTheme } = require('../index'); setTheme('dracula'); } }, - { label: 'Nord', click: () => { const { setTheme } = require('../index'); setTheme('nord'); } }, - { label: 'Monokai', click: () => { const { setTheme } = require('../index'); setTheme('monokai'); } }, - { label: 'Material', click: () => { const { setTheme } = require('../index'); setTheme('material'); } }, - { label: 'Gruvbox Dark', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-dark'); } }, - { label: 'Tokyo Night', click: () => { const { setTheme } = require('../index'); setTheme('tokyonight'); } }, - { label: 'Palenight', click: () => { const { setTheme } = require('../index'); setTheme('palenight'); } }, - { label: 'Ayu Dark', click: () => { const { setTheme } = require('../index'); setTheme('ayu-dark'); } }, - { label: 'Ayu Mirage', click: () => { const { setTheme } = require('../index'); setTheme('ayu-mirage'); } }, - { label: 'Oceanic Next', click: () => { const { setTheme } = require('../index'); setTheme('oceanic-next'); } }, - { label: 'Cobalt2', click: () => { const { setTheme } = require('../index'); setTheme('cobalt2'); } }, - { label: 'Concrete Dark', click: () => { const { setTheme } = require('../index'); setTheme('concrete-dark'); } }, - { label: 'Concrete Warm', click: () => { const { setTheme } = require('../index'); setTheme('concrete-warm'); } } - ] + { + label: 'Dark', + click: () => { + const { setTheme } = require('../index'); + setTheme('dark'); + }, + }, + { + label: 'One Dark', + click: () => { + const { setTheme } = require('../index'); + setTheme('onedark'); + }, + }, + { + label: 'Dracula', + click: () => { + const { setTheme } = require('../index'); + setTheme('dracula'); + }, + }, + { + label: 'Nord', + click: () => { + const { setTheme } = require('../index'); + setTheme('nord'); + }, + }, + { + label: 'Monokai', + click: () => { + const { setTheme } = require('../index'); + setTheme('monokai'); + }, + }, + { + label: 'Material', + click: () => { + const { setTheme } = require('../index'); + setTheme('material'); + }, + }, + { + label: 'Gruvbox Dark', + click: () => { + const { setTheme } = require('../index'); + setTheme('gruvbox-dark'); + }, + }, + { + label: 'Tokyo Night', + click: () => { + const { setTheme } = require('../index'); + setTheme('tokyonight'); + }, + }, + { + label: 'Palenight', + click: () => { + const { setTheme } = require('../index'); + setTheme('palenight'); + }, + }, + { + label: 'Ayu Dark', + click: () => { + const { setTheme } = require('../index'); + setTheme('ayu-dark'); + }, + }, + { + label: 'Ayu Mirage', + click: () => { + const { setTheme } = require('../index'); + setTheme('ayu-mirage'); + }, + }, + { + label: 'Oceanic Next', + click: () => { + const { setTheme } = require('../index'); + setTheme('oceanic-next'); + }, + }, + { + label: 'Cobalt2', + click: () => { + const { setTheme } = require('../index'); + setTheme('cobalt2'); + }, + }, + { + label: 'Concrete Dark', + click: () => { + const { setTheme } = require('../index'); + setTheme('concrete-dark'); + }, + }, + { + label: 'Concrete Warm', + click: () => { + const { setTheme } = require('../index'); + setTheme('concrete-warm'); + }, + }, + ], }, { type: 'separator' }, { @@ -281,19 +552,19 @@ function viewItems(mainWindow) { { label: 'Increase Font Size', accelerator: 'CmdOrCtrl+Shift+Plus', - click: () => mainWindow.webContents.send('adjust-font-size', 'increase') + click: () => mainWindow.webContents.send('adjust-font-size', 'increase'), }, { label: 'Decrease Font Size', accelerator: 'CmdOrCtrl+Shift+-', - click: () => mainWindow.webContents.send('adjust-font-size', 'decrease') + click: () => mainWindow.webContents.send('adjust-font-size', 'decrease'), }, { label: 'Reset Font Size', accelerator: 'CmdOrCtrl+Shift+0', - click: () => mainWindow.webContents.send('adjust-font-size', 'reset') - } - ] + click: () => mainWindow.webContents.send('adjust-font-size', 'reset'), + }, + ], }, { type: 'separator' }, { @@ -302,7 +573,7 @@ function viewItems(mainWindow) { checked: true, click: (menuItem) => { mainWindow.webContents.session.setSpellCheckerEnabled(menuItem.checked); - } + }, }, { type: 'separator' }, { @@ -310,13 +581,13 @@ function viewItems(mainWindow) { submenu: [ { label: 'Load Custom Preview CSS...', - click: () => mainWindow.webContents.send('load-custom-css') + click: () => mainWindow.webContents.send('load-custom-css'), }, { label: 'Clear Custom Preview CSS', - click: () => mainWindow.webContents.send('clear-custom-css') - } - ] + click: () => mainWindow.webContents.send('clear-custom-css'), + }, + ], }, { type: 'separator' }, { label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' }, @@ -324,7 +595,7 @@ function viewItems(mainWindow) { { type: 'separator' }, { label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' }, { label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }, - { label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' } + { label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }, ]; } @@ -335,25 +606,25 @@ function batchItems(mainWindow) { click: () => { const { showBatchConversionDialog } = require('../index'); showBatchConversionDialog(); - } + }, }, { type: 'separator' }, { label: 'Batch Image Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'image') + click: () => mainWindow.webContents.send('show-batch-converter', 'image'), }, { label: 'Batch Audio Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'audio') + click: () => mainWindow.webContents.send('show-batch-converter', 'audio'), }, { label: 'Batch Video Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'video') + click: () => mainWindow.webContents.send('show-batch-converter', 'video'), }, { label: 'Batch PDF Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'pdf') - } + click: () => mainWindow.webContents.send('show-batch-converter', 'pdf'), + }, ]; } @@ -365,8 +636,8 @@ function convertItems(mainWindow) { click: () => { const { showUniversalConverterDialog } = require('../index'); showUniversalConverterDialog(); - } - } + }, + }, ]; } @@ -378,7 +649,7 @@ function pdfEditorItems(mainWindow) { click: () => { const { openPdfFile } = require('../index'); openPdfFile(); - } + }, }, { type: 'separator' }, { @@ -386,21 +657,21 @@ function pdfEditorItems(mainWindow) { click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('merge'); - } + }, }, { label: 'Split PDF...', click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('split'); - } + }, }, { label: 'Compress PDF...', click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('compress'); - } + }, }, { type: 'separator' }, { @@ -408,21 +679,21 @@ function pdfEditorItems(mainWindow) { click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('rotate'); - } + }, }, { label: 'Delete Pages...', click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('delete'); - } + }, }, { label: 'Reorder Pages...', click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('reorder'); - } + }, }, { type: 'separator' }, { @@ -430,7 +701,7 @@ function pdfEditorItems(mainWindow) { click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('watermark'); - } + }, }, { type: 'separator' }, { @@ -441,23 +712,23 @@ function pdfEditorItems(mainWindow) { click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('encrypt'); - } + }, }, { label: 'Remove Password...', click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('decrypt'); - } + }, }, { label: 'Set Permissions...', click: () => { const { showPDFEditorDialog } = require('../index'); showPDFEditorDialog('permissions'); - } - } - ] + }, + }, + ], }, { type: 'separator' }, { @@ -465,8 +736,8 @@ function pdfEditorItems(mainWindow) { click: () => { const { showAboutDialog } = require('../index'); showAboutDialog(); - } - } + }, + }, ]; } @@ -478,8 +749,8 @@ function toolsItems(mainWindow) { { type: 'separator' }, { label: 'Document Compare', - click: () => mainWindow.webContents.send('show-document-compare') - } + click: () => mainWindow.webContents.send('show-document-compare'), + }, ]; } @@ -490,7 +761,7 @@ function helpItems(mainWindow) { click: () => { const { showAboutDialog } = require('../index'); showAboutDialog(); - } + }, }, { type: 'separator' }, { @@ -498,21 +769,21 @@ function helpItems(mainWindow) { click: () => { const { showDependenciesDialog } = require('../index'); showDependenciesDialog(); - } + }, }, { type: 'separator' }, { label: 'Documentation', - click: () => shell.openExternal('https://github.com/amitwh/markdown-converter') + click: () => shell.openExternal('https://github.com/amitwh/markdown-converter'), }, { label: 'Report Issue', - click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues') + click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues'), }, { label: 'Check for Updates', - click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases') - } + click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases'), + }, ]; } @@ -524,5 +795,5 @@ module.exports = { convertItems, pdfEditorItems, toolsItems, - helpItems -}; \ No newline at end of file + helpItems, +}; diff --git a/src/main/store.js b/src/main/store.js index 32664e6..583e0cf 100644 --- a/src/main/store.js +++ b/src/main/store.js @@ -25,7 +25,7 @@ const store = { } catch {} settings[key] = value; fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); - } + }, }; module.exports = store; diff --git a/src/main/updater/crash-writer.js b/src/main/updater/crash-writer.js index b52d029..26939b9 100644 --- a/src/main/updater/crash-writer.js +++ b/src/main/updater/crash-writer.js @@ -29,13 +29,18 @@ class CrashWriter { const files = fs.readdirSync(this.dir).sort(); while (files.length > MAX_DUMPS) { const oldest = files.shift(); - try { fs.unlinkSync(path.join(this.dir, oldest)); } catch (_) { /* ignore */ } + try { + fs.unlinkSync(path.join(this.dir, oldest)); + } catch (_) { + /* ignore */ + } } } list() { if (!fs.existsSync(this.dir)) return []; - return fs.readdirSync(this.dir) + return fs + .readdirSync(this.dir) .filter((f) => f.endsWith('.json')) .sort() .reverse() @@ -56,4 +61,4 @@ class CrashWriter { } } -module.exports = { CrashWriter }; \ No newline at end of file +module.exports = { CrashWriter }; diff --git a/src/main/updater/migration-transform.js b/src/main/updater/migration-transform.js index 2fbaada..6ef2864 100644 --- a/src/main/updater/migration-transform.js +++ b/src/main/updater/migration-transform.js @@ -2,42 +2,46 @@ // Kept in sync manually; if the renderer transform changes, update this too. const { z } = require('zod'); -const v4SettingsSchema = z.object({ - theme: z.enum(['light', 'dark', 'auto']).default('auto'), - customCss: z.string().optional().nullable(), - recentFiles: z.array(z.string()).default([]), - editorFontSize: z.number().min(10).max(28).default(14), - keyBindings: z.record(z.string(), z.string()).optional(), - snippets: z.array(z.unknown()).default([]), -}).passthrough(); +const v4SettingsSchema = z + .object({ + theme: z.enum(['light', 'dark', 'auto']).default('auto'), + customCss: z.string().optional().nullable(), + recentFiles: z.array(z.string()).default([]), + editorFontSize: z.number().min(10).max(28).default(14), + keyBindings: z.record(z.string(), z.string()).optional(), + snippets: z.array(z.unknown()).default([]), + }) + .passthrough(); -const v5SettingsSchema = z.object({ - fontSize: z.number().default(14), - tabSize: z.number().default(4), - lineNumbers: z.boolean().default(true), - wordWrap: z.boolean().default(true), - minimap: z.boolean().default(true), - theme: z.enum(['light', 'dark', 'system']).default('system'), - accentColor: z.string().default('brand'), - fontFamily: z.string().default('system'), - pdfFormat: z.string().default('a4'), - pdfMargins: z.string().default('normal'), - pdfEmbedFonts: z.boolean().default(true), - docxTemplate: z.string().default('standard'), - docxCustomTemplatePath: z.string().nullable().default(null), - replOpen: z.boolean().default(false), - breadcrumbSymbols: z.boolean().default(true), - htmlHighlightStyle: z.string().default('github'), - renderTablesAsAscii: z.boolean().default(false), - welcomeDismissed: z.boolean().default(false), - editorFontSize: z.number().default(14), - customCssPath: z.string().nullable().default(null), - userBindings: z.record(z.string(), z.string()).default({}), - updateChannel: z.enum(['github', 'concreteinfo']).default('github'), - autoCheckUpdates: z.boolean().default(true), - firstRun: z.boolean().default(true), - 'migration.version': z.literal(5).optional(), -}).passthrough(); +const v5SettingsSchema = z + .object({ + fontSize: z.number().default(14), + tabSize: z.number().default(4), + lineNumbers: z.boolean().default(true), + wordWrap: z.boolean().default(true), + minimap: z.boolean().default(true), + theme: z.enum(['light', 'dark', 'system']).default('system'), + accentColor: z.string().default('brand'), + fontFamily: z.string().default('system'), + pdfFormat: z.string().default('a4'), + pdfMargins: z.string().default('normal'), + pdfEmbedFonts: z.boolean().default(true), + docxTemplate: z.string().default('standard'), + docxCustomTemplatePath: z.string().nullable().default(null), + replOpen: z.boolean().default(false), + breadcrumbSymbols: z.boolean().default(true), + htmlHighlightStyle: z.string().default('github'), + renderTablesAsAscii: z.boolean().default(false), + welcomeDismissed: z.boolean().default(false), + editorFontSize: z.number().default(14), + customCssPath: z.string().nullable().default(null), + userBindings: z.record(z.string(), z.string()).default({}), + updateChannel: z.enum(['github', 'concreteinfo']).default('github'), + autoCheckUpdates: z.boolean().default(true), + firstRun: z.boolean().default(true), + 'migration.version': z.literal(5).optional(), + }) + .passthrough(); const v5OnlyFields = ['updateChannel', 'autoCheckUpdates', 'firstRun']; const v5ThemeValues = ['light', 'dark', 'system']; diff --git a/src/main/updater/updater-service.js b/src/main/updater/updater-service.js index 1bc1bad..33b8320 100644 --- a/src/main/updater/updater-service.js +++ b/src/main/updater/updater-service.js @@ -18,7 +18,8 @@ class UpdaterService extends EventEmitter { au.on('update-downloaded', (info) => this._emit({ state: 'ready', version: info.version })); au.on('update-not-available', () => this._emit({ state: 'idle' })); au.on('error', (err) => { - const code = err && /ENOTFOUND|ETIMEDOUT|ECONNREFUSED/.test(err.message) ? 'NETWORK' : 'UNKNOWN'; + const code = + err && /ENOTFOUND|ETIMEDOUT|ECONNREFUSED/.test(err.message) ? 'NETWORK' : 'UNKNOWN'; this._emit({ state: 'error', code }); }); } @@ -39,4 +40,4 @@ class UpdaterService extends EventEmitter { } } -module.exports = { UpdaterService }; \ No newline at end of file +module.exports = { UpdaterService }; diff --git a/src/main/utils/paths.js b/src/main/utils/paths.js index da72dcd..362a4a7 100644 --- a/src/main/utils/paths.js +++ b/src/main/utils/paths.js @@ -10,7 +10,7 @@ function getAllowedDirectories() { app.getPath('desktop'), app.getPath('downloads'), app.getPath('home'), - process.cwd() // Current working directory + process.cwd(), // Current working directory ].filter(Boolean); // Remove any undefined paths return dirs; } @@ -88,9 +88,13 @@ function resolveWritablePath(filePath) { function isPathAccessible(resolvedPath) { // Block access to sensitive system directories const blockedPaths = [ - '/etc/passwd', '/etc/shadow', '/root', - 'C:\\Windows\\System32', 'C:\\Windows\\System', - '/System', '/private/etc' + '/etc/passwd', + '/etc/shadow', + '/root', + 'C:\\Windows\\System32', + 'C:\\Windows\\System', + '/System', + '/private/etc', ]; const normalizedPath = resolvedPath.toLowerCase(); diff --git a/src/main/window/index.js b/src/main/window/index.js index 24b45c2..657311e 100644 --- a/src/main/window/index.js +++ b/src/main/window/index.js @@ -25,9 +25,9 @@ function createMainWindow() { // on load and the renderer never gets the IPC bridge. contextIsolation: true, nodeIntegration: false, - spellcheck: true + spellcheck: true, }, - icon: path.join(__dirname, '../../../assets/icon.png') + icon: path.join(__dirname, '../../../assets/icon.png'), }); // Dev (Vite): load the running dev server so .tsx is transformed on the fly. @@ -54,7 +54,7 @@ function createMainWindow() { console.error( '[WINDOW] Renderer not found at', rendererIndex, - '— did you run `npm run build:renderer` before packaging?', + '— did you run `npm run build:renderer` before packaging?' ); } } @@ -84,18 +84,23 @@ function createMainWindow() { // Add spell check suggestions if (params.misspelledWord) { for (const suggestion of params.dictionarySuggestions) { - ctxMenu.append(new MenuItem({ - label: suggestion, - click: () => win.webContents.replaceMisspelling(suggestion) - })); + ctxMenu.append( + new MenuItem({ + label: suggestion, + click: () => win.webContents.replaceMisspelling(suggestion), + }) + ); } if (params.dictionarySuggestions.length > 0) { ctxMenu.append(new MenuItem({ type: 'separator' })); } - ctxMenu.append(new MenuItem({ - label: 'Add to Dictionary', - click: () => win.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) - })); + ctxMenu.append( + new MenuItem({ + label: 'Add to Dictionary', + click: () => + win.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord), + }) + ); ctxMenu.append(new MenuItem({ type: 'separator' })); } diff --git a/src/main/word-template/index.js b/src/main/word-template/index.js index bad6fe2..267000e 100644 --- a/src/main/word-template/index.js +++ b/src/main/word-template/index.js @@ -10,293 +10,293 @@ const PizZip = require('pizzip'); const Docx = require('docx4js').default; class WordTemplateExporter { - constructor(templatePath, startPage = 3, pageSettings = null) { - this.templatePath = templatePath || path.join(__dirname, '../word_template.docx'); - this.startPage = startPage; // Which page to start inserting content - this.pageSettings = pageSettings; // Page size and orientation settings + constructor(templatePath, startPage = 3, pageSettings = null) { + this.templatePath = templatePath || path.join(__dirname, '../word_template.docx'); + this.startPage = startPage; // Which page to start inserting content + this.pageSettings = pageSettings; // Page size and orientation settings + } + + /** + * Convert markdown to Word document using template + */ + async convert(markdownContent, outputPath) { + try { + // Load template + const templateBuffer = fs.readFileSync(this.templatePath); + const zip = new PizZip(templateBuffer); + + // Extract document.xml + let documentXml = zip.file('word/document.xml').asText(); + + // Set page size if settings provided + if (this.pageSettings) { + documentXml = this.setPageSize(documentXml); + } + + // Parse markdown and generate Word XML + const newContentXml = this.markdownToWordXml(markdownContent); + + // Insert new content after the specified start page + const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage); + + // Update the zip with modified XML + zip.file('word/document.xml', modifiedXml); + + // Generate and save the new document + const newDocBuffer = zip.generate({ type: 'nodebuffer' }); + fs.writeFileSync(outputPath, newDocBuffer); + + return outputPath; + } catch (error) { + console.error('Error in Word export:', error); + throw error; + } + } + + /** + * Set page size in document XML + */ + setPageSize(documentXml) { + // Import PAGE_SIZES from main process (need to pass as parameter) + const PAGE_SIZES = { + a4: { width: 11906, height: 16838 }, + a3: { width: 16838, height: 23811 }, + a5: { width: 8391, height: 11906 }, + b4: { width: 14170, height: 20015 }, + b5: { width: 9979, height: 14170 }, + letter: { width: 12240, height: 15840 }, + legal: { width: 12240, height: 20160 }, + tabloid: { width: 15840, height: 24480 }, + }; + + let width, height; + const pageSize = PAGE_SIZES[this.pageSettings.size]; + + if (pageSize) { + width = pageSize.width; + height = pageSize.height; + } else { + // Default to A4 + width = 11906; + height = 16838; } - /** - * Convert markdown to Word document using template - */ - async convert(markdownContent, outputPath) { - try { - // Load template - const templateBuffer = fs.readFileSync(this.templatePath); - const zip = new PizZip(templateBuffer); - - // Extract document.xml - let documentXml = zip.file('word/document.xml').asText(); - - // Set page size if settings provided - if (this.pageSettings) { - documentXml = this.setPageSize(documentXml); - } - - // Parse markdown and generate Word XML - const newContentXml = this.markdownToWordXml(markdownContent); - - // Insert new content after the specified start page - const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage); - - // Update the zip with modified XML - zip.file('word/document.xml', modifiedXml); - - // Generate and save the new document - const newDocBuffer = zip.generate({ type: 'nodebuffer' }); - fs.writeFileSync(outputPath, newDocBuffer); - - return outputPath; - } catch (error) { - console.error('Error in Word export:', error); - throw error; - } + // Swap dimensions for landscape + if (this.pageSettings.orientation === 'landscape') { + [width, height] = [height, width]; } - /** - * Set page size in document XML - */ - setPageSize(documentXml) { - // Import PAGE_SIZES from main process (need to pass as parameter) - const PAGE_SIZES = { - a4: { width: 11906, height: 16838 }, - a3: { width: 16838, height: 23811 }, - a5: { width: 8391, height: 11906 }, - b4: { width: 14170, height: 20015 }, - b5: { width: 9979, height: 14170 }, - letter: { width: 12240, height: 15840 }, - legal: { width: 12240, height: 20160 }, - tabloid: { width: 15840, height: 24480 } - }; + // Update all elements in section properties + const pgSzRegex = /]*\/>/g; + let modifiedXml = documentXml.replace(pgSzRegex, () => { + return ``; + }); - let width, height; - const pageSize = PAGE_SIZES[this.pageSettings.size]; + // If no pgSz found, add it to all sectPr elements + if (!pgSzRegex.test(documentXml)) { + const sectPrRegex = /]*>/g; + modifiedXml = modifiedXml.replace(sectPrRegex, (match) => { + return `${match}`; + }); + } - if (pageSize) { - width = pageSize.width; - height = pageSize.height; + return modifiedXml; + } + + /** + * Insert markdown content after the specified page in the document + * @param {string} documentXml - The document XML + * @param {string} newContentXml - The new content to insert + * @param {number} afterPage - Insert content after this page number (1-based) + */ + insertContentAfterPage(documentXml, newContentXml, afterPage) { + // Find section breaks that mark page boundaries + // Look for the section properties tag that marks page breaks + const sectionBreakRegex = /]*>[\s\S]*?<\/w:sectPr>/g; + const matches = [...documentXml.matchAll(sectionBreakRegex)]; + + // Calculate which section break to insert after + // Page 1 = before 1st section break + // Page 2 = after 1st section break + // Page 3 = after 2nd section break, etc. + const sectionIndex = afterPage - 1; + + if (matches.length >= sectionIndex && sectionIndex > 0) { + // Insert after the specified section break + const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length; + return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint); + } else if (afterPage === 1 || matches.length === 0) { + // Insert at the beginning (after ) or if no section breaks found + const bodyStart = documentXml.indexOf('') + 8; + return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart); + } else { + // If not enough section breaks, insert before closing body tag + return documentXml.replace('', newContentXml + ''); + } + } + + /** + * Convert markdown to Word XML format + */ + markdownToWordXml(markdown) { + const lines = markdown.split('\n'); + let xml = ''; + let inCodeBlock = false; + let codeLines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Handle code blocks + if (line.trim().startsWith('```')) { + if (inCodeBlock) { + // End code block + xml += this.createCodeBlockXml(codeLines.join('\n')); + codeLines = []; + inCodeBlock = false; } else { - // Default to A4 - width = 11906; - height = 16838; + inCodeBlock = true; } + continue; + } - // Swap dimensions for landscape - if (this.pageSettings.orientation === 'landscape') { - [width, height] = [height, width]; + if (inCodeBlock) { + codeLines.push(line); + continue; + } + + // Empty lines + if (!line.trim()) { + xml += ''; + continue; + } + + // Tables - detect table lines + if (line.includes('|') && line.trim().startsWith('|')) { + const tableLines = [line]; + i++; + // Collect all consecutive table lines + while (i < lines.length && lines[i].includes('|')) { + tableLines.push(lines[i]); + i++; } + i--; // Back up one line + xml += this.createTableXml(tableLines); + continue; + } - // Update all elements in section properties - const pgSzRegex = /]*\/>/g; - let modifiedXml = documentXml.replace(pgSzRegex, () => { - return ``; - }); - - // If no pgSz found, add it to all sectPr elements - if (!pgSzRegex.test(documentXml)) { - const sectPrRegex = /]*>/g; - modifiedXml = modifiedXml.replace(sectPrRegex, (match) => { - return `${match}`; - }); + // ASCII flowcharts/diagrams - detect box drawing characters + if (this.isAsciiArt(line)) { + const asciiLines = [line]; + i++; + // Collect all consecutive ASCII art lines + while (i < lines.length && (this.isAsciiArt(lines[i]) || !lines[i].trim())) { + asciiLines.push(lines[i]); + i++; + if (i < lines.length && lines[i].trim() && !this.isAsciiArt(lines[i])) { + break; + } } + i--; // Back up one line + xml += this.createAsciiArtXml(asciiLines.join('\n')); + continue; + } - return modifiedXml; + // Headings - strip markdown numbering + if (line.trim().startsWith('#')) { + const level = (line.match(/^#+/) || [''])[0].length; + let text = line.replace(/^#+\s*/, '').trim(); + // Remove markdown numbering like "1.1 Title" -> "Title" + text = text.replace(/^\d+(\.\d+)*\.?\s+/, ''); + xml += this.createHeadingXml(text, level); + continue; + } + + // Blockquotes + if (line.trim().startsWith('>')) { + const text = line.replace(/^>\s*/, '').trim(); + xml += this.createQuoteXml(text); + continue; + } + + // Ordered lists - strip markdown numbering, use template numbering + if (/^\s*\d+\.\s+/.test(line)) { + const text = line.replace(/^\s*\d+\.\s+/, ''); + xml += this.createListItemXml(text, true); + continue; + } + + // Unordered lists + if (/^\s*[-*+]\s+/.test(line)) { + const text = line.replace(/^\s*[-*+]\s+/, ''); + xml += this.createListItemXml(text, false); + continue; + } + + // Horizontal rule + if (/^[-*_]{3,}$/.test(line.trim())) { + xml += this.createHorizontalRuleXml(); + continue; + } + + // Normal paragraph + xml += this.createParagraphXml(line); } - /** - * Insert markdown content after the specified page in the document - * @param {string} documentXml - The document XML - * @param {string} newContentXml - The new content to insert - * @param {number} afterPage - Insert content after this page number (1-based) - */ - insertContentAfterPage(documentXml, newContentXml, afterPage) { - // Find section breaks that mark page boundaries - // Look for the section properties tag that marks page breaks - const sectionBreakRegex = /]*>[\s\S]*?<\/w:sectPr>/g; - const matches = [...documentXml.matchAll(sectionBreakRegex)]; + return xml; + } - // Calculate which section break to insert after - // Page 1 = before 1st section break - // Page 2 = after 1st section break - // Page 3 = after 2nd section break, etc. - const sectionIndex = afterPage - 1; + /** + * Create heading XML using template styles + */ + createHeadingXml(text, level) { + const styleName = `Heading${level}`; + const runs = this.parseInlineFormatting(text); - if (matches.length >= sectionIndex && sectionIndex > 0) { - // Insert after the specified section break - const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length; - return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint); - } else if (afterPage === 1 || matches.length === 0) { - // Insert at the beginning (after ) or if no section breaks found - const bodyStart = documentXml.indexOf('') + 8; - return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart); - } else { - // If not enough section breaks, insert before closing body tag - return documentXml.replace('', newContentXml + ''); - } - } - - /** - * Convert markdown to Word XML format - */ - markdownToWordXml(markdown) { - const lines = markdown.split('\n'); - let xml = ''; - let inCodeBlock = false; - let codeLines = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Handle code blocks - if (line.trim().startsWith('```')) { - if (inCodeBlock) { - // End code block - xml += this.createCodeBlockXml(codeLines.join('\n')); - codeLines = []; - inCodeBlock = false; - } else { - inCodeBlock = true; - } - continue; - } - - if (inCodeBlock) { - codeLines.push(line); - continue; - } - - // Empty lines - if (!line.trim()) { - xml += ''; - continue; - } - - // Tables - detect table lines - if (line.includes('|') && line.trim().startsWith('|')) { - const tableLines = [line]; - i++; - // Collect all consecutive table lines - while (i < lines.length && lines[i].includes('|')) { - tableLines.push(lines[i]); - i++; - } - i--; // Back up one line - xml += this.createTableXml(tableLines); - continue; - } - - // ASCII flowcharts/diagrams - detect box drawing characters - if (this.isAsciiArt(line)) { - const asciiLines = [line]; - i++; - // Collect all consecutive ASCII art lines - while (i < lines.length && (this.isAsciiArt(lines[i]) || !lines[i].trim())) { - asciiLines.push(lines[i]); - i++; - if (i < lines.length && lines[i].trim() && !this.isAsciiArt(lines[i])) { - break; - } - } - i--; // Back up one line - xml += this.createAsciiArtXml(asciiLines.join('\n')); - continue; - } - - // Headings - strip markdown numbering - if (line.trim().startsWith('#')) { - const level = (line.match(/^#+/) || [''])[0].length; - let text = line.replace(/^#+\s*/, '').trim(); - // Remove markdown numbering like "1.1 Title" -> "Title" - text = text.replace(/^\d+(\.\d+)*\.?\s+/, ''); - xml += this.createHeadingXml(text, level); - continue; - } - - // Blockquotes - if (line.trim().startsWith('>')) { - const text = line.replace(/^>\s*/, '').trim(); - xml += this.createQuoteXml(text); - continue; - } - - // Ordered lists - strip markdown numbering, use template numbering - if (/^\s*\d+\.\s+/.test(line)) { - const text = line.replace(/^\s*\d+\.\s+/, ''); - xml += this.createListItemXml(text, true); - continue; - } - - // Unordered lists - if (/^\s*[-*+]\s+/.test(line)) { - const text = line.replace(/^\s*[-*+]\s+/, ''); - xml += this.createListItemXml(text, false); - continue; - } - - // Horizontal rule - if (/^[-*_]{3,}$/.test(line.trim())) { - xml += this.createHorizontalRuleXml(); - continue; - } - - // Normal paragraph - xml += this.createParagraphXml(line); - } - - return xml; - } - - /** - * Create heading XML using template styles - */ - createHeadingXml(text, level) { - const styleName = `Heading${level}`; - const runs = this.parseInlineFormatting(text); - - return ` + return ` ${runs} `; - } + } - /** - * Create paragraph XML with Normal style - */ - createParagraphXml(text) { - const runs = this.parseInlineFormatting(text); + /** + * Create paragraph XML with Normal style + */ + createParagraphXml(text) { + const runs = this.parseInlineFormatting(text); - return ` + return ` ${runs} `; - } + } - /** - * Create quote XML - */ - createQuoteXml(text) { - const runs = this.parseInlineFormatting(text); + /** + * Create quote XML + */ + createQuoteXml(text) { + const runs = this.parseInlineFormatting(text); - return ` + return ` ${runs} `; - } + } - /** - * Create list item XML using template numbering - */ - createListItemXml(text, numbered) { - const runs = this.parseInlineFormatting(text); - const numId = numbered ? '1' : '2'; // Template numbering IDs + /** + * Create list item XML using template numbering + */ + createListItemXml(text, numbered) { + const runs = this.parseInlineFormatting(text); + const numId = numbered ? '1' : '2'; // Template numbering IDs - return ` + return ` @@ -306,23 +306,23 @@ class WordTemplateExporter { ${runs} `; - } + } - /** - * Create code block XML - renders each line as separate paragraph - * to preserve exact formatting like in preview (monospace, no wrapping) - * Background shading applied at paragraph level to extend full width - */ - createCodeBlockXml(code) { - const lines = code.split('\n'); - let xml = ''; + /** + * Create code block XML - renders each line as separate paragraph + * to preserve exact formatting like in preview (monospace, no wrapping) + * Background shading applied at paragraph level to extend full width + */ + createCodeBlockXml(code) { + const lines = code.split('\n'); + let xml = ''; - lines.forEach((line, index) => { - const escapedLine = this.escapeXml(line); + lines.forEach((line, index) => { + const escapedLine = this.escapeXml(line); - // Each line gets its own paragraph with exact spacing and no wrapping - // Shading (shd) is at paragraph level so background extends full width - xml += ` + // Each line gets its own paragraph with exact spacing and no wrapping + // Shading (shd) is at paragraph level so background extends full width + xml += ` @@ -342,58 +342,61 @@ class WordTemplateExporter { ${escapedLine} `; - }); + }); - return xml; - } + return xml; + } - /** - * Create horizontal rule XML - */ - createHorizontalRuleXml() { - return ` + /** + * Create horizontal rule XML + */ + createHorizontalRuleXml() { + return ` `; + } + + /** + * Create table XML from markdown table lines with template styling + * Uses full-width tables with equal column distribution matching template style + */ + createTableXml(tableLines) { + // Parse table + const rows = []; + for (const line of tableLines) { + // Skip separator lines (e.g., |---|---|) + if (/^\s*\|[\s\-:]+\|\s*$/.test(line)) { + continue; + } + // Split by | and trim + const cells = line + .split('|') + .filter((cell) => cell.trim()) + .map((cell) => cell.trim()); + if (cells.length > 0) { + rows.push(cells); + } } - /** - * Create table XML from markdown table lines with template styling - * Uses full-width tables with equal column distribution matching template style - */ - createTableXml(tableLines) { - // Parse table - const rows = []; - for (const line of tableLines) { - // Skip separator lines (e.g., |---|---|) - if (/^\s*\|[\s\-:]+\|\s*$/.test(line)) { - continue; - } - // Split by | and trim - const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim()); - if (cells.length > 0) { - rows.push(cells); - } - } + if (rows.length === 0) return ''; - if (rows.length === 0) return ''; + // Calculate number of columns + const numCols = Math.max(...rows.map((row) => row.length)); - // Calculate number of columns - const numCols = Math.max(...rows.map(row => row.length)); + // Calculate column width in twips (1440 twips = 1 inch) + // Assume standard page width of 9360 twips (6.5 inches usable) + const totalTableWidth = 9360; + const colWidth = Math.floor(totalTableWidth / numCols); - // Calculate column width in twips (1440 twips = 1 inch) - // Assume standard page width of 9360 twips (6.5 inches usable) - const totalTableWidth = 9360; - const colWidth = Math.floor(totalTableWidth / numCols); + // Build table XML + let tableXml = ''; - // Build table XML - let tableXml = ''; - - // Table properties - use template table style with full width - tableXml += ` + // Table properties - use template table style with full width + tableXml += ` @@ -408,150 +411,207 @@ class WordTemplateExporter { `; - // Table grid with explicit column widths - tableXml += ''; - for (let i = 0; i < numCols; i++) { - tableXml += ``; + // Table grid with explicit column widths + tableXml += ''; + for (let i = 0; i < numCols; i++) { + tableXml += ``; + } + tableXml += ''; + + // Table rows + rows.forEach((rowCells, rowIndex) => { + const isHeader = rowIndex === 0; + + tableXml += ''; + + // Row properties for consistent height + tableXml += ''; + + // Pad row to have same number of columns + while (rowCells.length < numCols) { + rowCells.push(''); + } + + rowCells.forEach((cellText, colIndex) => { + tableXml += ''; + tableXml += ''; + + // Cell width + tableXml += ``; + + // Cell shading - orange for header, white for data rows + if (isHeader) { + tableXml += ''; + } else { + tableXml += ''; } - tableXml += ''; - // Table rows - rows.forEach((rowCells, rowIndex) => { - const isHeader = rowIndex === 0; + // Cell borders + tableXml += + '' + + '' + + '' + + '' + + '' + + ''; - tableXml += ''; + // Cell margins for proper padding + tableXml += + '' + + '' + + '' + + '' + + '' + + ''; - // Row properties for consistent height - tableXml += ''; + tableXml += ''; - // Pad row to have same number of columns - while (rowCells.length < numCols) { - rowCells.push(''); - } + // Cell content + tableXml += ''; + tableXml += ''; + tableXml += ''; + tableXml += ''; - rowCells.forEach((cellText, colIndex) => { - tableXml += ''; - tableXml += ''; + const runs = this.parseInlineFormatting(cellText); - // Cell width - tableXml += ``; + if (isHeader) { + // Header: bold white text + tableXml += runs.replace(//g, ''); + } else { + // Data rows: normal black text + tableXml += runs; + } - // Cell shading - orange for header, white for data rows - if (isHeader) { - tableXml += ''; - } else { - tableXml += ''; - } + tableXml += ''; + tableXml += ''; + }); - // Cell borders - tableXml += '' + - '' + - '' + - '' + - '' + - ''; + tableXml += ''; + }); - // Cell margins for proper padding - tableXml += '' + - '' + - '' + - '' + - '' + - ''; + tableXml += ''; - tableXml += ''; + // Add spacing after table + tableXml += ''; - // Cell content - tableXml += ''; - tableXml += ''; - tableXml += ''; - tableXml += ''; + return tableXml; + } - const runs = this.parseInlineFormatting(cellText); - - if (isHeader) { - // Header: bold white text - tableXml += runs.replace(//g, ''); - } else { - // Data rows: normal black text - tableXml += runs; - } - - tableXml += ''; - tableXml += ''; - }); - - tableXml += ''; - }); - - tableXml += ''; - - // Add spacing after table - tableXml += ''; - - return tableXml; + /** + * Detect if line contains ASCII art/flowchart characters + */ + isAsciiArt(line) { + // Don't treat markdown tables as ASCII art + if (line.trim().startsWith('|') && line.trim().endsWith('|')) { + // Check if it's a proper markdown table (has multiple cells) + const cells = line.split('|').filter((c) => c.trim()); + if (cells.length >= 2) { + return false; + } } - /** - * Detect if line contains ASCII art/flowchart characters - */ - isAsciiArt(line) { - // Don't treat markdown tables as ASCII art - if (line.trim().startsWith('|') && line.trim().endsWith('|')) { - // Check if it's a proper markdown table (has multiple cells) - const cells = line.split('|').filter(c => c.trim()); - if (cells.length >= 2) { - return false; - } - } + // Common ASCII art characters (Unicode box drawing and symbols) + const asciiArtChars = [ + '─', + '│', + '┌', + '┐', + '└', + '┘', + '├', + '┤', + '┬', + '┴', + '┼', // Box drawing + '═', + '║', + '╔', + '╗', + '╚', + '╝', + '╠', + '╣', + '╦', + '╩', + '╬', // Double box + '╭', + '╮', + '╯', + '╰', // Rounded corners + '▲', + '▼', + '◄', + '►', + '♦', + '●', + '○', + '■', + '□', + '◆', + '◇', // Shapes + '↓', + '→', + '←', + '↑', + '↔', + '↕', + '⇒', + '⇐', + '⇓', + '⇑', // Arrows + '┃', + '━', + '┏', + '┓', + '┗', + '┛', + '┣', + '┫', + '┳', + '┻', + '╋', // Heavy box + '░', + '▒', + '▓', + '█', // Shading + ]; - // Common ASCII art characters (Unicode box drawing and symbols) - const asciiArtChars = [ - '─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing - '═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box - '╭', '╮', '╯', '╰', // Rounded corners - '▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes - '↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows - '┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box - '░', '▒', '▓', '█', // Shading - ]; - - // Check for box drawing characters - if (asciiArtChars.some(char => line.includes(char))) { - return true; - } - - // Check for ASCII box patterns with regular characters - const asciiPatterns = [ - /^\s*\+[-=_+]+\+/, // +-----+ or +=====+ - /^\s*\|[-=_]{3,}\|/, // |-----| - /^\s*[-=_]{5,}$/, // ----- - /^\s*\+[-]+\+[-]+\+/, // +---+---+ - /^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) || - /^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE - /^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark] - /^\s*\|\s{2,}\w+.*\|\|/, // || text || - /^\s*\[[^\]]+\]\s*$/, // [Step in brackets] - /^\s*<[-=]+>/, // <----> or <====> - /^\s*\/[-_\\\/]+\//, // /----/ - /^\s*\*[-=\*]+\*/, // *----* - ]; - - return asciiPatterns.some(pattern => pattern.test(line)); + // Check for box drawing characters + if (asciiArtChars.some((char) => line.includes(char))) { + return true; } - /** - * Create ASCII art XML with monospace font - * Background shading applied at paragraph level to extend full width - */ - createAsciiArtXml(asciiContent) { - // Split ASCII art into individual lines and create a paragraph for each - const lines = asciiContent.split('\n'); - let xml = ''; + // Check for ASCII box patterns with regular characters + const asciiPatterns = [ + /^\s*\+[-=_+]+\+/, // +-----+ or +=====+ + /^\s*\|[-=_]{3,}\|/, // |-----| + /^\s*[-=_]{5,}$/, // ----- + /^\s*\+[-]+\+[-]+\+/, // +---+---+ + /^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) || + /^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE + /^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark] + /^\s*\|\s{2,}\w+.*\|\|/, // || text || + /^\s*\[[^\]]+\]\s*$/, // [Step in brackets] + /^\s*<[-=]+>/, // <----> or <====> + /^\s*\/[-_\\\/]+\//, // /----/ + /^\s*\*[-=\*]+\*/, // *----* + ]; - lines.forEach((line, index) => { - // Shading (shd) is at paragraph level so background extends full width - xml += ` + return asciiPatterns.some((pattern) => pattern.test(line)); + } + + /** + * Create ASCII art XML with monospace font + * Background shading applied at paragraph level to extend full width + */ + createAsciiArtXml(asciiContent) { + // Split ASCII art into individual lines and create a paragraph for each + const lines = asciiContent.split('\n'); + let xml = ''; + + lines.forEach((line, index) => { + // Shading (shd) is at paragraph level so background extends full width + xml += ` @@ -562,34 +622,34 @@ class WordTemplateExporter { `; - // Check if line contains arrow characters and color them red - const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲']; - const hasArrow = arrowChars.some(arrow => line.includes(arrow)); + // Check if line contains arrow characters and color them red + const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲']; + const hasArrow = arrowChars.some((arrow) => line.includes(arrow)); - if (hasArrow) { - // Split line into parts and color arrows red - let remainingLine = line; + if (hasArrow) { + // Split line into parts and color arrows red + let remainingLine = line; - while (remainingLine.length > 0) { - let foundArrow = false; - let arrowIndex = -1; - let foundArrowChar = ''; + while (remainingLine.length > 0) { + let foundArrow = false; + let arrowIndex = -1; + let foundArrowChar = ''; - // Find the first arrow in the remaining text - for (const arrow of arrowChars) { - const idx = remainingLine.indexOf(arrow); - if (idx !== -1 && (arrowIndex === -1 || idx < arrowIndex)) { - arrowIndex = idx; - foundArrowChar = arrow; - foundArrow = true; - } - } + // Find the first arrow in the remaining text + for (const arrow of arrowChars) { + const idx = remainingLine.indexOf(arrow); + if (idx !== -1 && (arrowIndex === -1 || idx < arrowIndex)) { + arrowIndex = idx; + foundArrowChar = arrow; + foundArrow = true; + } + } - if (foundArrow) { - // Add text before arrow (if any) - if (arrowIndex > 0) { - const beforeArrow = this.escapeXml(remainingLine.substring(0, arrowIndex)); - xml += ` + if (foundArrow) { + // Add text before arrow (if any) + if (arrowIndex > 0) { + const beforeArrow = this.escapeXml(remainingLine.substring(0, arrowIndex)); + xml += ` @@ -598,11 +658,11 @@ class WordTemplateExporter { ${beforeArrow} `; - } + } - // Add arrow in red - const escapedArrow = this.escapeXml(foundArrowChar); - xml += ` + // Add arrow in red + const escapedArrow = this.escapeXml(foundArrowChar); + xml += ` @@ -613,12 +673,12 @@ class WordTemplateExporter { ${escapedArrow} `; - // Continue with remaining text - remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length); - } else { - // No more arrows, add remaining text - const escapedRemaining = this.escapeXml(remainingLine); - xml += ` + // Continue with remaining text + remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length); + } else { + // No more arrows, add remaining text + const escapedRemaining = this.escapeXml(remainingLine); + xml += ` @@ -627,13 +687,13 @@ class WordTemplateExporter { ${escapedRemaining} `; - remainingLine = ''; - } - } - } else { - // No arrows, just add the line normally - const escapedLine = this.escapeXml(line); - xml += ` + remainingLine = ''; + } + } + } else { + // No arrows, just add the line normally + const escapedLine = this.escapeXml(line); + xml += ` @@ -642,102 +702,107 @@ class WordTemplateExporter { ${escapedLine} `; - } + } - xml += ``; - }); + xml += ``; + }); - return xml; - } + return xml; + } - /** - * Parse inline formatting (bold, italic, code) - */ - parseInlineFormatting(text) { - let xml = ''; - const pos = 0; + /** + * Parse inline formatting (bold, italic, code) + */ + parseInlineFormatting(text) { + let xml = ''; + const pos = 0; - // Patterns for inline formatting - const patterns = [ - { regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true }, - { regex: /\*\*(.+?)\*\*/g, bold: true }, - { regex: /\*(.+?)\*/g, italic: true }, - { regex: /`(.+?)`/g, code: true } - ]; + // Patterns for inline formatting + const patterns = [ + { regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true }, + { regex: /\*\*(.+?)\*\*/g, bold: true }, + { regex: /\*(.+?)\*/g, italic: true }, + { regex: /`(.+?)`/g, code: true }, + ]; - // Simple approach: process text sequentially - let remaining = text; + // Simple approach: process text sequentially + let remaining = text; - while (remaining.length > 0) { - let foundMatch = false; - let earliestPos = remaining.length; - let matchedPattern = null; - let match = null; + while (remaining.length > 0) { + let foundMatch = false; + let earliestPos = remaining.length; + let matchedPattern = null; + let match = null; - // Find earliest match - for (const pattern of patterns) { - pattern.regex.lastIndex = 0; - const m = pattern.regex.exec(remaining); - if (m && m.index < earliestPos) { - earliestPos = m.index; - matchedPattern = pattern; - match = m; - foundMatch = true; - } - } + // Find earliest match + for (const pattern of patterns) { + pattern.regex.lastIndex = 0; + const m = pattern.regex.exec(remaining); + if (m && m.index < earliestPos) { + earliestPos = m.index; + matchedPattern = pattern; + match = m; + foundMatch = true; + } + } - if (foundMatch) { - // Add text before match - if (earliestPos > 0) { - xml += this.createRunXml(remaining.substring(0, earliestPos)); - } - - // Add formatted text - xml += this.createRunXml(match[1], matchedPattern.bold, matchedPattern.italic, matchedPattern.code); - - remaining = remaining.substring(earliestPos + match[0].length); - } else { - // No more matches, add remaining text - xml += this.createRunXml(remaining); - break; - } + if (foundMatch) { + // Add text before match + if (earliestPos > 0) { + xml += this.createRunXml(remaining.substring(0, earliestPos)); } - return xml; + // Add formatted text + xml += this.createRunXml( + match[1], + matchedPattern.bold, + matchedPattern.italic, + matchedPattern.code + ); + + remaining = remaining.substring(earliestPos + match[0].length); + } else { + // No more matches, add remaining text + xml += this.createRunXml(remaining); + break; + } } - /** - * Create a run (text segment) XML - */ - createRunXml(text, bold = false, italic = false, code = false) { - if (!text) return ''; + return xml; + } - const escapedText = this.escapeXml(text); - let propsXml = ''; + /** + * Create a run (text segment) XML + */ + createRunXml(text, bold = false, italic = false, code = false) { + if (!text) return ''; - if (bold) propsXml += ''; - if (italic) propsXml += ''; - if (code) { - propsXml += ''; - propsXml += ''; - } + const escapedText = this.escapeXml(text); + let propsXml = ''; - propsXml += ''; - - return `${propsXml}${escapedText}`; + if (bold) propsXml += ''; + if (italic) propsXml += ''; + if (code) { + propsXml += ''; + propsXml += ''; } - /** - * Escape XML special characters - */ - escapeXml(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } + propsXml += ''; + + return `${propsXml}${escapedText}`; + } + + /** + * Escape XML special characters + */ + escapeXml(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } } module.exports = WordTemplateExporter; diff --git a/src/plugins/built-in/_sample/manifest.json b/src/plugins/built-in/_sample/manifest.json index 48865a6..6cacca7 100644 --- a/src/plugins/built-in/_sample/manifest.json +++ b/src/plugins/built-in/_sample/manifest.json @@ -5,9 +5,7 @@ "description": "Demonstrates the plugin system. Safe to delete.", "icon": "puzzle", "extensionPoints": { - "commands": [ - { "id": "hello", "label": "Sample: Hello World", "shortcut": "" } - ] + "commands": [{ "id": "hello", "label": "Sample: Hello World", "shortcut": "" }] }, "settings": [] } diff --git a/src/plugins/built-in/writing-studio/index.js b/src/plugins/built-in/writing-studio/index.js index 8a4a1d4..1fefa50 100644 --- a/src/plugins/built-in/writing-studio/index.js +++ b/src/plugins/built-in/writing-studio/index.js @@ -9,7 +9,7 @@ class WritingStudioPlugin extends PluginAPI { this.context = context; this.sprintEngine = new SprintEngine({ - onEvent: (name, data) => context.events.emit(name, data) + onEvent: (name, data) => context.events.emit(name, data), }); this.goalTracker = new GoalTracker(context.settings); this.snapshotManager = new SnapshotManager(context.settings); @@ -17,14 +17,14 @@ class WritingStudioPlugin extends PluginAPI { readFile: (p) => context.ipc.invoke('read-file', p), writeFile: (p, c) => context.ipc.invoke('write-file', p, c), fileExists: (p) => context.ipc.invoke('path-exists', p), - listDir: (p) => context.ipc.invoke('list-directory', p) + listDir: (p) => context.ipc.invoke('list-directory', p), }); this._engines = { sprint: this.sprintEngine, goals: this.goalTracker, snapshots: this.snapshotManager, - projects: this.projectManager + projects: this.projectManager, }; this._registerCommands(context); @@ -34,59 +34,89 @@ class WritingStudioPlugin extends PluginAPI { _registerCommands(context) { const { sprintEngine, snapshotManager, goalTracker } = this; - context.commands.register('start-sprint', 'Studio: Start Sprint', () => { - const duration = context.settings.get('sprintDuration') || 25; - const content = context.editor.getContent() || ''; - const words = content.split(/\s+/).filter(Boolean).length; - sprintEngine.start(duration, words); - }, 'Ctrl+Alt+S'); + context.commands.register( + 'start-sprint', + 'Studio: Start Sprint', + () => { + const duration = context.settings.get('sprintDuration') || 25; + const content = context.editor.getContent() || ''; + const words = content.split(/\s+/).filter(Boolean).length; + sprintEngine.start(duration, words); + }, + 'Ctrl+Alt+S' + ); - context.commands.register('stop-sprint', 'Studio: Stop Sprint', () => { - if (!sprintEngine.isActive()) return; - const content = context.editor.getContent() || ''; - const words = content.split(/\s+/).filter(Boolean).length; - const result = sprintEngine.stop(words); - goalTracker.addWords(result.wordDelta); - context.events.emit('sprint:stopped', result); - }, 'Ctrl+Alt+Shift+S'); + context.commands.register( + 'stop-sprint', + 'Studio: Stop Sprint', + () => { + if (!sprintEngine.isActive()) return; + const content = context.editor.getContent() || ''; + const words = content.split(/\s+/).filter(Boolean).length; + const result = sprintEngine.stop(words); + goalTracker.addWords(result.wordDelta); + context.events.emit('sprint:stopped', result); + }, + 'Ctrl+Alt+Shift+S' + ); - context.commands.register('take-snapshot', 'Studio: Take Snapshot', () => { - const content = context.editor.getContent() || ''; - snapshotManager.create(content, 'manual'); - context.events.emit('snapshot:created', {}); - }, 'Ctrl+Alt+N'); + context.commands.register( + 'take-snapshot', + 'Studio: Take Snapshot', + () => { + const content = context.editor.getContent() || ''; + snapshotManager.create(content, 'manual'); + context.events.emit('snapshot:created', {}); + }, + 'Ctrl+Alt+N' + ); - context.commands.register('restore-last-snapshot', 'Studio: Restore Last Snapshot', () => { - const snaps = snapshotManager.list(); - if (snaps.length === 0) return; - const content = snapshotManager.restore(snaps[0].id); - context.editor.insertAtCursor(content); - }, 'Ctrl+Alt+Z'); + context.commands.register( + 'restore-last-snapshot', + 'Studio: Restore Last Snapshot', + () => { + const snaps = snapshotManager.list(); + if (snaps.length === 0) return; + const content = snapshotManager.restore(snaps[0].id); + context.editor.insertAtCursor(content); + }, + 'Ctrl+Alt+Z' + ); context.commands.register('new-project', 'Studio: New Project', () => { context.events.emit('studio:new-project', {}); }); - context.commands.register('compile-manuscript', 'Studio: Compile Manuscript', () => { - context.events.emit('studio:compile', {}); - }, 'Ctrl+Alt+E'); + context.commands.register( + 'compile-manuscript', + 'Studio: Compile Manuscript', + () => { + context.events.emit('studio:compile', {}); + }, + 'Ctrl+Alt+E' + ); - context.commands.register('proofread-document', 'Studio: Proofread Document', () => { - if (context.events.hasHandler('ai:analyze')) { - const content = context.editor.getContent() || ''; - context.events.emit('ai:analyze', { text: content, type: 'grammar' }); - } - }, 'Ctrl+Alt+G'); + context.commands.register( + 'proofread-document', + 'Studio: Proofread Document', + () => { + if (context.events.hasHandler('ai:analyze')) { + const content = context.editor.getContent() || ''; + context.events.emit('ai:analyze', { text: content, type: 'grammar' }); + } + }, + 'Ctrl+Alt+G' + ); } _registerStatusBar(context) { context.statusBar.registerIndicator('word-goal', { text: '0/1000', - tooltip: 'Daily word goal progress' + tooltip: 'Daily word goal progress', }); context.statusBar.registerIndicator('sprint-timer', { text: '', - tooltip: 'Writing sprint timer' + tooltip: 'Writing sprint timer', }); } diff --git a/src/plugins/built-in/writing-studio/manifest.json b/src/plugins/built-in/writing-studio/manifest.json index 509285a..129ff97 100644 --- a/src/plugins/built-in/writing-studio/manifest.json +++ b/src/plugins/built-in/writing-studio/manifest.json @@ -15,17 +15,34 @@ { "id": "start-sprint", "label": "Studio: Start Sprint", "shortcut": "Ctrl+Alt+S" }, { "id": "stop-sprint", "label": "Studio: Stop Sprint", "shortcut": "Ctrl+Alt+Shift+S" }, { "id": "take-snapshot", "label": "Studio: Take Snapshot", "shortcut": "Ctrl+Alt+N" }, - { "id": "restore-last-snapshot", "label": "Studio: Restore Last Snapshot", "shortcut": "Ctrl+Alt+Z" }, + { + "id": "restore-last-snapshot", + "label": "Studio: Restore Last Snapshot", + "shortcut": "Ctrl+Alt+Z" + }, { "id": "new-project", "label": "Studio: New Project", "shortcut": "" }, - { "id": "compile-manuscript", "label": "Studio: Compile Manuscript", "shortcut": "Ctrl+Alt+E" }, - { "id": "proofread-document", "label": "Studio: Proofread Document", "shortcut": "Ctrl+Alt+G" } + { + "id": "compile-manuscript", + "label": "Studio: Compile Manuscript", + "shortcut": "Ctrl+Alt+E" + }, + { + "id": "proofread-document", + "label": "Studio: Proofread Document", + "shortcut": "Ctrl+Alt+G" + } ], "statusBar": { "indicators": ["sprint-timer", "word-goal"] } }, "settings": [ { "key": "dailyGoal", "type": "number", "default": 1000, "label": "Daily word goal" }, { "key": "sprintDuration", "type": "number", "default": 25, "label": "Sprint duration (min)" }, - { "key": "autoSnapshotInterval", "type": "number", "default": 0, "label": "Auto-snapshot interval (min, 0=off)" }, + { + "key": "autoSnapshotInterval", + "type": "number", + "default": 0, + "label": "Auto-snapshot interval (min, 0=off)" + }, { "key": "maxSnapshots", "type": "number", "default": 50, "label": "Max snapshots to keep" } ] } diff --git a/src/plugins/built-in/writing-studio/panels/goals-panel.js b/src/plugins/built-in/writing-studio/panels/goals-panel.js index 7a75beb..dc57d45 100644 --- a/src/plugins/built-in/writing-studio/panels/goals-panel.js +++ b/src/plugins/built-in/writing-studio/panels/goals-panel.js @@ -29,7 +29,8 @@ function renderGoalsPanel(container, { engines, settings }) { const row = document.createElement('div'); row.className = 'ws-stat-row'; const label = document.createElement('span'); - label.textContent = progress.written.toLocaleString() + ' / ' + dailyGoal.toLocaleString() + ' words'; + label.textContent = + progress.written.toLocaleString() + ' / ' + dailyGoal.toLocaleString() + ' words'; const pct = document.createElement('span'); pct.className = 'ws-pct'; pct.textContent = progress.pct + '%'; @@ -81,7 +82,7 @@ function renderGoalsPanel(container, { engines, settings }) { const chart = document.createElement('div'); chart.className = 'ws-chart'; - const maxWords = Math.max(...last30.map(d => d.words), 1); + const maxWords = Math.max(...last30.map((d) => d.words), 1); for (const day of last30) { const barEl = document.createElement('div'); const height = Math.max(2, (day.words / maxWords) * 60); diff --git a/src/plugins/built-in/writing-studio/panels/manuscript-panel.js b/src/plugins/built-in/writing-studio/panels/manuscript-panel.js index 469b463..d11f30f 100644 --- a/src/plugins/built-in/writing-studio/panels/manuscript-panel.js +++ b/src/plugins/built-in/writing-studio/panels/manuscript-panel.js @@ -69,7 +69,8 @@ function renderManuscriptPanel(container, { engines, editor, settings }) { const row = document.createElement('div'); row.className = 'ws-stat-row'; const label = document.createElement('span'); - label.textContent = stats.totalWords.toLocaleString() + ' / ' + stats.targetWords.toLocaleString() + ' words'; + label.textContent = + stats.totalWords.toLocaleString() + ' / ' + stats.targetWords.toLocaleString() + ' words'; const pct = document.createElement('span'); pct.className = 'ws-pct'; pct.textContent = stats.pctComplete + '%'; diff --git a/src/plugins/built-in/writing-studio/panels/proofread-panel.js b/src/plugins/built-in/writing-studio/panels/proofread-panel.js index 4314970..f8619fc 100644 --- a/src/plugins/built-in/writing-studio/panels/proofread-panel.js +++ b/src/plugins/built-in/writing-studio/panels/proofread-panel.js @@ -42,7 +42,7 @@ function renderProofreadPanel(container, { events, editor }) { if (result && result.issues) { renderIssues(issuesList, result.issues); } - } + }, }); }); } @@ -80,7 +80,10 @@ function renderIssues(container, issues) { const actions = document.createElement('div'); actions.className = 'ws-issue-actions'; - for (const [action, label] of [['accept', 'Accept'], ['dismiss', 'Dismiss']]) { + for (const [action, label] of [ + ['accept', 'Accept'], + ['dismiss', 'Dismiss'], + ]) { const actionBtn = document.createElement('button'); actionBtn.className = 'ws-btn ws-btn-sm'; actionBtn.textContent = label; diff --git a/src/plugins/built-in/writing-studio/panels/snapshots-panel.js b/src/plugins/built-in/writing-studio/panels/snapshots-panel.js index da65642..9c865d1 100644 --- a/src/plugins/built-in/writing-studio/panels/snapshots-panel.js +++ b/src/plugins/built-in/writing-studio/panels/snapshots-panel.js @@ -45,7 +45,11 @@ function renderSnapshotsPanel(container, { engines, editor }) { const actions = document.createElement('div'); actions.className = 'ws-snapshot-actions'; - for (const [action, text, cls] of [['restore', 'Restore', ''], ['diff', 'Diff', ''], ['delete', 'Delete', 'ws-btn-danger']]) { + for (const [action, text, cls] of [ + ['restore', 'Restore', ''], + ['diff', 'Diff', ''], + ['delete', 'Delete', 'ws-btn-danger'], + ]) { const actionBtn = document.createElement('button'); actionBtn.className = 'ws-btn ws-btn-sm' + (cls ? ' ' + cls : ''); actionBtn.textContent = text; diff --git a/src/plugins/built-in/writing-studio/project-manager.js b/src/plugins/built-in/writing-studio/project-manager.js index d337c74..dc42c29 100644 --- a/src/plugins/built-in/writing-studio/project-manager.js +++ b/src/plugins/built-in/writing-studio/project-manager.js @@ -12,7 +12,7 @@ class ProjectManager { type: opts.type || 'manuscript', target: { words: opts.targetWords || 0, deadline: opts.deadline || null }, chapters: [], - metadata: opts.metadata || {} + metadata: opts.metadata || {}, }; this.fs.writeFile(dir + '/.project.json', JSON.stringify(project, null, 2)); return project; @@ -66,7 +66,7 @@ class ProjectManager { totalWords, chapterCount: project.chapters.length, targetWords: target, - pctComplete: target > 0 ? Math.min(100, Math.round((totalWords / target) * 100)) : 0 + pctComplete: target > 0 ? Math.min(100, Math.round((totalWords / target) * 100)) : 0, }; } } diff --git a/src/plugins/built-in/writing-studio/snapshot-manager.js b/src/plugins/built-in/writing-studio/snapshot-manager.js index 7b174d7..3cb7bf6 100644 --- a/src/plugins/built-in/writing-studio/snapshot-manager.js +++ b/src/plugins/built-in/writing-studio/snapshot-manager.js @@ -24,7 +24,7 @@ class SnapshotManager { timestamp: new Date().toISOString(), content, wordCount: content.split(/\s+/).filter(Boolean).length, - label + label, }; snaps.unshift(snap); this._saveAll(snaps); @@ -36,7 +36,7 @@ class SnapshotManager { } getById(id) { - return this._getAll().find(s => s.id === id) || null; + return this._getAll().find((s) => s.id === id) || null; } restore(id) { @@ -46,7 +46,7 @@ class SnapshotManager { } delete(id) { - const snaps = this._getAll().filter(s => s.id !== id); + const snaps = this._getAll().filter((s) => s.id !== id); this._saveAll(snaps); } @@ -59,8 +59,12 @@ class SnapshotManager { const newSet = new Set(newLines); let added = 0; let removed = 0; - for (const line of newLines) { if (!oldSet.has(line)) added++; } - for (const line of oldLines) { if (!newSet.has(line)) removed++; } + for (const line of newLines) { + if (!oldSet.has(line)) added++; + } + for (const line of oldLines) { + if (!newSet.has(line)) removed++; + } return { added, removed }; } diff --git a/src/plugins/built-in/writing-studio/sprint-engine.js b/src/plugins/built-in/writing-studio/sprint-engine.js index 8d67023..fd01b2e 100644 --- a/src/plugins/built-in/writing-studio/sprint-engine.js +++ b/src/plugins/built-in/writing-studio/sprint-engine.js @@ -40,7 +40,9 @@ class SprintEngine { } } - isActive() { return this._active; } + isActive() { + return this._active; + } getRemaining() { if (!this._active) return 0; diff --git a/src/plugins/plugin-context.js b/src/plugins/plugin-context.js index 2d0e0d1..2121339 100644 --- a/src/plugins/plugin-context.js +++ b/src/plugins/plugin-context.js @@ -12,10 +12,11 @@ class PluginContext { * @param {object} deps.exportHooks - { preHooks: [], postHooks: [] } */ constructor(deps) { - const { pluginId, sidebar, commands, statusBar, eventBus, settings, editor, ipc, exportHooks } = deps; + const { pluginId, sidebar, commands, statusBar, eventBus, settings, editor, ipc, exportHooks } = + deps; this.sidebar = { - registerPanel: (id, opts) => sidebar.registerPanel(`${pluginId}:${id}`, opts) + registerPanel: (id, opts) => sidebar.registerPanel(`${pluginId}:${id}`, opts), }; this.commands = { @@ -28,41 +29,45 @@ class PluginContext { } }; commands.register(`${pluginId}:${id}`, label, safeHandler, shortcut); - } + }, }; this.statusBar = { - registerIndicator: (id, opts) => statusBar.registerIndicator(`${pluginId}:${id}`, opts) + registerIndicator: (id, opts) => statusBar.registerIndicator(`${pluginId}:${id}`, opts), }; this.settings = { get: (key) => settings.get(`plugins.${pluginId}.${key}`), set: (key, value) => settings.set(`plugins.${pluginId}.${key}`, value), - onChanged: (key, cb) => settings.onChanged(`plugins.${pluginId}.${key}`, cb) + onChanged: (key, cb) => settings.onChanged(`plugins.${pluginId}.${key}`, cb), }; this.editor = { getContent: () => editor.getContent(), getSelection: () => editor.getSelection(), insertAtCursor: (text) => editor.insertAtCursor(text), - onContentChanged: (cb) => editor.onContentChanged(cb) + onContentChanged: (cb) => editor.onContentChanged(cb), }; this.events = { on: (event, handler) => eventBus.on(event, handler), off: (event, handler) => eventBus.off(event, handler), emit: (event, payload) => eventBus.emit(event, payload), - hasHandler: (event) => eventBus.hasHandler(event) + hasHandler: (event) => eventBus.hasHandler(event), }; this.ipc = { invoke: (channel, ...args) => ipc.invoke(channel, ...args), - on: (channel, handler) => ipc.on(channel, handler) + on: (channel, handler) => ipc.on(channel, handler), }; this.exports = { - registerPreHook: (handler) => { if (exportHooks) exportHooks.preHooks.push(handler); }, - registerPostHook: (handler) => { if (exportHooks) exportHooks.postHooks.push(handler); } + registerPreHook: (handler) => { + if (exportHooks) exportHooks.preHooks.push(handler); + }, + registerPostHook: (handler) => { + if (exportHooks) exportHooks.postHooks.push(handler); + }, }; } } diff --git a/src/plugins/plugin-loader.js b/src/plugins/plugin-loader.js index 4334b05..697a6fb 100644 --- a/src/plugins/plugin-loader.js +++ b/src/plugins/plugin-loader.js @@ -35,7 +35,10 @@ class PluginLoader { const loaded = require(indexPath); PluginClass = loaded.Plugin || loaded.default || null; } catch (err) { - console.error(`[PluginLoader] Failed to load index.js for "${manifest.id}":`, err.message); + console.error( + `[PluginLoader] Failed to load index.js for "${manifest.id}":`, + err.message + ); continue; } } @@ -46,7 +49,7 @@ class PluginLoader { description: manifest.description, manifest, PluginClass, - dir: pluginDir + dir: pluginDir, }); this.loadedIds.add(manifest.id); } catch (err) { diff --git a/src/plugins/plugin-registry.js b/src/plugins/plugin-registry.js index 5482478..be6e2fb 100644 --- a/src/plugins/plugin-registry.js +++ b/src/plugins/plugin-registry.js @@ -32,7 +32,7 @@ class PluginRegistry { settings: this.deps.settings, editor: this.deps.editor, ipc: this.deps.ipc, - exportHooks: this.exportHooks + exportHooks: this.exportHooks, }); try { diff --git a/src/preload.js b/src/preload.js index 1a12b3c..5ba7628 100644 --- a/src/preload.js +++ b/src/preload.js @@ -150,7 +150,7 @@ const ALLOWED_SEND_CHANNELS = [ // Plugin settings 'plugin-settings:get', - 'plugin-settings:set' + 'plugin-settings:set', ]; const ALLOWED_RECEIVE_CHANNELS = [ @@ -251,7 +251,7 @@ const ALLOWED_RECEIVE_CHANNELS = [ // File dialog / directory listing 'list-directory', 'pick-folder', - 'pick-file' + 'pick-file', ]; /** @@ -361,12 +361,12 @@ contextBridge.exposeInMainWorld('electronAPI', { move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination }), list: (dirPath) => ipcRenderer.invoke('list-directory', dirPath), pickFolder: () => ipcRenderer.invoke('pick-folder'), - pickFile: () => ipcRenderer.invoke('pick-file') + pickFile: () => ipcRenderer.invoke('pick-file'), }, // Theme Operations theme: { - get: () => ipcRenderer.send('get-theme') + get: () => ipcRenderer.send('get-theme'), }, // Print Operations @@ -378,7 +378,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // Export Operations export: { withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }), - spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format }) + spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format }), }, // Batch Conversion @@ -386,7 +386,7 @@ contextBridge.exposeInMainWorld('electronAPI', { convert: (inputFolder, outputFolder, format, options) => { ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options }); }, - selectFolder: (type) => ipcRenderer.send('select-folder', type) + selectFolder: (type) => ipcRenderer.send('select-folder', type), }, // Universal Converter @@ -395,8 +395,14 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath }); }, convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => { - ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder }); - } + ipcRenderer.send('universal-convert-batch', { + tool, + fromFormat, + toFormat, + inputFolder, + outputFolder, + }); + }, }, // Header/Footer Operations @@ -404,22 +410,23 @@ contextBridge.exposeInMainWorld('electronAPI', { getSettings: () => ipcRenderer.send('get-header-footer-settings'), saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings), browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position), - saveLogo: (position, filePath) => ipcRenderer.send('save-header-footer-logo', { position, filePath }), - clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position) + saveLogo: (position, filePath) => + ipcRenderer.send('save-header-footer-logo', { position, filePath }), + clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position), }, // Page Settings page: { getSettings: () => ipcRenderer.send('get-page-settings'), updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings), - setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber) + setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber), }, // PDF Operations pdf: { processOperation: (data) => ipcRenderer.send('process-pdf-operation', data), getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath), - selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId) + selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId), }, // Image Converter Operations @@ -428,7 +435,7 @@ contextBridge.exposeInMainWorld('electronAPI', { batchConvert: (data) => ipcRenderer.send('image-batch-convert', data), resize: (data) => ipcRenderer.send('image-resize', data), compress: (data) => ipcRenderer.send('image-compress', data), - rotate: (data) => ipcRenderer.send('image-rotate', data) + rotate: (data) => ipcRenderer.send('image-rotate', data), }, // Audio Converter Operations @@ -437,7 +444,7 @@ contextBridge.exposeInMainWorld('electronAPI', { batchConvert: (data) => ipcRenderer.send('audio-batch-convert', data), extract: (data) => ipcRenderer.send('audio-extract', data), trim: (data) => ipcRenderer.send('audio-trim', data), - merge: (data) => ipcRenderer.send('audio-merge', data) + merge: (data) => ipcRenderer.send('audio-merge', data), }, // Video Converter Operations @@ -447,7 +454,7 @@ contextBridge.exposeInMainWorld('electronAPI', { compress: (data) => ipcRenderer.send('video-compress', data), trim: (data) => ipcRenderer.send('video-trim', data), extractFrames: (data) => ipcRenderer.send('video-frames', data), - toGif: (data) => ipcRenderer.send('video-gif', data) + toGif: (data) => ipcRenderer.send('video-gif', data), }, getAppVersion: () => ipcRenderer.invoke('get-app-version'), diff --git a/src/renderer/components/FirstRunWizard.tsx b/src/renderer/components/FirstRunWizard.tsx index 6d17b6e..700bd9a 100644 --- a/src/renderer/components/FirstRunWizard.tsx +++ b/src/renderer/components/FirstRunWizard.tsx @@ -5,7 +5,8 @@ import { useSettingsStore } from '@/stores/settings-store'; const TEMPLATES = { blank: '', readme: '# Project\n\nDescription.\n\n## Usage\n\n```\nnpm install\n```\n', - meeting: '# Meeting Notes — YYYY-MM-DD\n\n## Attendees\n\n- \n## Agenda\n\n1. \n\n## Action items\n\n- [ ] \n', + meeting: + '# Meeting Notes — YYYY-MM-DD\n\n## Attendees\n\n- \n## Agenda\n\n1. \n\n## Action items\n\n- [ ] \n', blog: '# Title\n\n*Subtitle*\n\nLorem ipsum.\n\n---\n\n## Section 1\n', }; @@ -23,7 +24,12 @@ export function FirstRunWizard() { const close = () => setFirstRun(false); return ( -
+
{step === 0 && (
@@ -31,7 +37,12 @@ export function FirstRunWizard() {
{(['light', 'dark', 'system'] as const).map((t) => ( ))} @@ -43,11 +54,21 @@ export function FirstRunWizard() {

Update channel

@@ -56,7 +77,11 @@ export function FirstRunWizard() { {step === 2 && (

Starter template

- setTemplate(e.target.value as any)} + className="border rounded px-2 py-1 w-full mb-4" + > @@ -65,13 +90,28 @@ export function FirstRunWizard() {
)}
- +
{step > 0 && } {step < 2 ? ( - + ) : ( - + )}
diff --git a/src/renderer/components/UpdateBanner.tsx b/src/renderer/components/UpdateBanner.tsx index 5b4206d..9b9f93d 100644 --- a/src/renderer/components/UpdateBanner.tsx +++ b/src/renderer/components/UpdateBanner.tsx @@ -9,9 +9,22 @@ export function UpdateBanner() { if (state === 'error') { return ( -
+
Couldn't check for updates.{' '} -
@@ -20,7 +33,10 @@ export function UpdateBanner() { if (state === 'available') { return ( -
+
A new version (v{version}) is available.{' '}
); -} \ No newline at end of file +} diff --git a/src/renderer/components/layout/Breadcrumb.tsx b/src/renderer/components/layout/Breadcrumb.tsx index bc2fd00..0596fa1 100644 --- a/src/renderer/components/layout/Breadcrumb.tsx +++ b/src/renderer/components/layout/Breadcrumb.tsx @@ -27,7 +27,9 @@ export function Breadcrumb() { className="flex items-center gap-1 hover:text-foreground" > - {'#'.repeat(h.level)} {h.text} + + {'#'.repeat(h.level)} {h.text} + ))} diff --git a/src/renderer/components/layout/StatusBar.tsx b/src/renderer/components/layout/StatusBar.tsx index 7fa63da..10279da 100644 --- a/src/renderer/components/layout/StatusBar.tsx +++ b/src/renderer/components/layout/StatusBar.tsx @@ -17,7 +17,9 @@ export function StatusBar() { UTF-8
- Ln {cursor.line}, Col {cursor.column} + + Ln {cursor.line}, Col {cursor.column} + Markdown
diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx index 989414f..0ad76a4 100644 --- a/src/renderer/components/layout/TabBar.tsx +++ b/src/renderer/components/layout/TabBar.tsx @@ -7,11 +7,7 @@ import { closestCenter, type DragEndEvent, } from '@dnd-kit/core'; -import { - SortableContext, - horizontalListSortingStrategy, - useSortable, -} from '@dnd-kit/sortable'; +import { SortableContext, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useFileStore, type OpenTab } from '@/stores/file-store'; import { cn } from '@/lib/utils'; diff --git a/src/renderer/components/modals/AboutDialog.tsx b/src/renderer/components/modals/AboutDialog.tsx index f8679bd..ef60665 100644 --- a/src/renderer/components/modals/AboutDialog.tsx +++ b/src/renderer/components/modals/AboutDialog.tsx @@ -1,5 +1,12 @@ import { useEffect, useState } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { ipc } from '@/lib/ipc'; import { useAppStore } from '@/stores/app-store'; @@ -46,4 +53,4 @@ export function AboutDialog() { ); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/AboutSettings.tsx b/src/renderer/components/modals/AboutSettings.tsx index 21eb054..8f2ca49 100644 --- a/src/renderer/components/modals/AboutSettings.tsx +++ b/src/renderer/components/modals/AboutSettings.tsx @@ -18,7 +18,10 @@ export function AboutSettings() { { e.preventDefault(); ipc.app.openExternal('https://github.com/amitwh/markdown-converter'); }} + onClick={(e) => { + e.preventDefault(); + ipc.app.openExternal('https://github.com/amitwh/markdown-converter'); + }} > GitHub repository @@ -27,7 +30,10 @@ export function AboutSettings() { { e.preventDefault(); ipc.app.openExternal('https://concreteinfo.co.in'); }} + onClick={(e) => { + e.preventDefault(); + ipc.app.openExternal('https://concreteinfo.co.in'); + }} > ConcreteInfo diff --git a/src/renderer/components/modals/AsciiGeneratorDialog.tsx b/src/renderer/components/modals/AsciiGeneratorDialog.tsx index 36a7921..f225d8a 100644 --- a/src/renderer/components/modals/AsciiGeneratorDialog.tsx +++ b/src/renderer/components/modals/AsciiGeneratorDialog.tsx @@ -1,8 +1,21 @@ import { useState, useEffect } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { useAppStore } from '@/stores/app-store'; import { toast } from '@/lib/toast'; @@ -17,9 +30,15 @@ export function AsciiGeneratorDialog() { useEffect(() => { let cancelled = false; figletText(text || ' ', font) - .then((result) => { if (!cancelled) setOutput(result); }) - .catch(() => { if (!cancelled) setOutput('(render error)'); }); - return () => { cancelled = true; }; + .then((result) => { + if (!cancelled) setOutput(result); + }) + .catch(() => { + if (!cancelled) setOutput('(render error)'); + }); + return () => { + cancelled = true; + }; }, [text, font]); const handleCopy = async () => { @@ -52,26 +71,35 @@ export function AsciiGeneratorDialog() {
-
+            
               {output}
             
- + ); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/ConfirmDialog.tsx b/src/renderer/components/modals/ConfirmDialog.tsx index 4acbbd5..a4c8dfb 100644 --- a/src/renderer/components/modals/ConfirmDialog.tsx +++ b/src/renderer/components/modals/ConfirmDialog.tsx @@ -1,10 +1,25 @@ -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { useAppStore, type ConfirmProps } from '@/stores/app-store'; export function ConfirmDialog(props: ConfirmProps) { const closeModal = useAppStore((s) => s.closeModal); - const { title, body, confirmLabel = 'Confirm', cancelLabel = 'Cancel', destructive, onConfirm, onCancel } = props; + const { + title, + body, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + destructive, + onConfirm, + onCancel, + } = props; const handleConfirm = async () => { await onConfirm(); @@ -33,4 +48,4 @@ export function ConfirmDialog(props: ConfirmProps) { ); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/CrashReportModal.tsx b/src/renderer/components/modals/CrashReportModal.tsx index eae895a..1a02106 100644 --- a/src/renderer/components/modals/CrashReportModal.tsx +++ b/src/renderer/components/modals/CrashReportModal.tsx @@ -12,29 +12,52 @@ export function CrashReportModal({ onClose }: { onClose: () => void }) { const [dumps, setDumps] = useState([]); const refresh = async () => setDumps(await ipc.crash.read()); - useEffect(() => { refresh(); }, []); + useEffect(() => { + refresh(); + }, []); return ( -
+

Crash reports

- +
- {dumps.length === 0 ? ( -

No crashes recorded — nice work!

+

+ No crashes recorded — nice work! +

) : (
    {dumps.map((d) => ( -
  • +
  • {d.timestamp}
    {d.message ?? '(no message)'}
    -
  • @@ -44,4 +67,4 @@ export function CrashReportModal({ onClose }: { onClose: () => void }) {
); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/EditorSettings.tsx b/src/renderer/components/modals/EditorSettings.tsx index c14010b..f4e9ef6 100644 --- a/src/renderer/components/modals/EditorSettings.tsx +++ b/src/renderer/components/modals/EditorSettings.tsx @@ -1,7 +1,13 @@ import { useSettingsStore } from '@/stores/settings-store'; import { Label } from '@/components/ui/label'; import { Slider } from '@/components/ui/slider'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; export function EditorSettings() { @@ -11,12 +17,24 @@ export function EditorSettings() {
- setSetting('fontSize', v)} /> + setSetting('fontSize', v)} + />
- setSetting('tabSize', Number(v) as 2 | 4 | 8)} + > + + + 2 spaces 4 spaces @@ -26,15 +44,27 @@ export function EditorSettings() {
); diff --git a/src/renderer/components/modals/ExportBatchDialog.tsx b/src/renderer/components/modals/ExportBatchDialog.tsx index 1571d9d..ff3988b 100644 --- a/src/renderer/components/modals/ExportBatchDialog.tsx +++ b/src/renderer/components/modals/ExportBatchDialog.tsx @@ -1,7 +1,19 @@ import { useState } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { useAppStore } from '@/stores/app-store'; import { ipc } from '@/lib/ipc'; import { toast } from '@/lib/toast'; @@ -46,7 +58,9 @@ export function ExportBatchDialog({ sourcePaths }: { sourcePaths: string[] }) {
setConcurrency(Number(v))}> - + + + {[1, 2, 4, 8, 16].map((n) => ( - {n} + + {n} + ))}
- {sourcePaths.map((p) =>
{p}
)} + {sourcePaths.map((p) => ( +
+ {p} +
+ ))}
{error && ( -
+
{error}
)}
- + ); diff --git a/src/renderer/components/modals/ExportDialogFooter.tsx b/src/renderer/components/modals/ExportDialogFooter.tsx index dca8015..a593d2e 100644 --- a/src/renderer/components/modals/ExportDialogFooter.tsx +++ b/src/renderer/components/modals/ExportDialogFooter.tsx @@ -9,7 +9,13 @@ interface Props { submitDisabled?: boolean; } -export function ExportDialogFooter({ onCancel, onSubmit, submitting, submitLabel, submitDisabled }: Props) { +export function ExportDialogFooter({ + onCancel, + onSubmit, + submitting, + submitLabel, + submitDisabled, +}: Props) { return ( ); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/ExportDocxDialog.tsx b/src/renderer/components/modals/ExportDocxDialog.tsx index 0347452..d6eca12 100644 --- a/src/renderer/components/modals/ExportDocxDialog.tsx +++ b/src/renderer/components/modals/ExportDocxDialog.tsx @@ -1,8 +1,20 @@ import { useState } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { useAppStore } from '@/stores/app-store'; import { useSettingsStore } from '@/stores/settings-store'; import { useExportSource } from '@/hooks/use-export-source'; @@ -21,12 +33,18 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) { const [error, setError] = useState(null); const handleSubmit = async () => { - if (!source) { setError('No file open.'); return; } + if (!source) { + setError('No file open.'); + return; + } setSubmitting(true); setError(null); try { const blob = await generateDocx({ source: source.source, title: source.title }); - const saveResult = await ipc.app.showSaveDialog?.({ title: 'Save as Word document', defaultPath: source.path.replace(/\.md$/, '.docx') }); + const saveResult = await ipc.app.showSaveDialog?.({ + title: 'Save as Word document', + defaultPath: source.path.replace(/\.md$/, '.docx'), + }); if (!saveResult?.ok || !saveResult.data) { setSubmitting(false); return; @@ -59,7 +77,9 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) {

- The renderer-side export produces the same document for all three - template choices; the option is preserved for future stylesheets. + The renderer-side export produces the same document for all three template choices; + the option is preserved for future stylesheets.

{error && ( -
+
{error}
)}
- + ); diff --git a/src/renderer/components/modals/ExportHtmlDialog.tsx b/src/renderer/components/modals/ExportHtmlDialog.tsx index c633698..7ba8216 100644 --- a/src/renderer/components/modals/ExportHtmlDialog.tsx +++ b/src/renderer/components/modals/ExportHtmlDialog.tsx @@ -1,8 +1,20 @@ import { useState } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { useAppStore } from '@/stores/app-store'; import { useSettingsStore } from '@/stores/settings-store'; import { useExportSource } from '@/hooks/use-export-source'; @@ -22,7 +34,10 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) { const [error, setError] = useState(null); const handleSubmit = async () => { - if (!source) { setError('No file open.'); return; } + if (!source) { + setError('No file open.'); + return; + } setSubmitting(true); setError(null); try { @@ -33,7 +48,10 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) { highlightStyle: highlight, renderTablesAsAscii: ascii, }); - const saveResult = await ipc.app.showSaveDialog?.({ title: 'Save as HTML', defaultPath: source.path.replace(/\.md$/, '.html') }); + const saveResult = await ipc.app.showSaveDialog?.({ + title: 'Save as HTML', + defaultPath: source.path.replace(/\.md$/, '.html'), + }); if (!saveResult?.ok || !saveResult.data) { setSubmitting(false); return; @@ -64,13 +82,19 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
{error && ( -
+
{error}
)}
- + ); diff --git a/src/renderer/components/modals/ExportPdfDialog.tsx b/src/renderer/components/modals/ExportPdfDialog.tsx index 7a6a65c..ded40dd 100644 --- a/src/renderer/components/modals/ExportPdfDialog.tsx +++ b/src/renderer/components/modals/ExportPdfDialog.tsx @@ -1,8 +1,20 @@ import { useState } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { useAppStore } from '@/stores/app-store'; import { useSettingsStore } from '@/stores/settings-store'; import { useExportSource } from '@/hooks/use-export-source'; @@ -19,7 +31,8 @@ const MARGIN_MAP = { export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) { const closeModal = useAppStore((s) => s.closeModal); - const { fontSize, pdfFormat, pdfMargins, pdfEmbedFonts, renderTablesAsAscii } = useSettingsStore(); + const { fontSize, pdfFormat, pdfMargins, pdfEmbedFonts, renderTablesAsAscii } = + useSettingsStore(); const [format, setFormat] = useState<'letter' | 'a4' | 'legal'>(pdfFormat); const [margins, setMargins] = useState<'normal' | 'narrow' | 'wide'>(pdfMargins); const [embed, setEmbed] = useState(pdfEmbedFonts); @@ -45,9 +58,12 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) { highlightStyle: 'github', renderTablesAsAscii: ascii, }); - const fmt = format === 'a4' ? { width: '210mm', height: '297mm' } - : format === 'legal' ? { width: '8.5in', height: '14in' } - : { width: '8.5in', height: '11in' }; + const fmt = + format === 'a4' + ? { width: '210mm', height: '297mm' } + : format === 'legal' + ? { width: '8.5in', height: '14in' } + : { width: '8.5in', height: '11in' }; const m = MARGIN_MAP[margins]; const pageCss = `@page { size: ${fmt.width} ${fmt.height}; margin: ${m.top}mm ${m.right}mm ${m.bottom}mm ${m.left}mm; }`; const finalHtml = html.replace('', `${pageCss}`); @@ -80,7 +96,9 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
setMargins(v as typeof margins)}> - + + + Narrow Normal @@ -100,20 +120,36 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
{error && ( -
+
{error}
)}
- + ); diff --git a/src/renderer/components/modals/ExportSettings.tsx b/src/renderer/components/modals/ExportSettings.tsx index d2d9e8d..df6e4a9 100644 --- a/src/renderer/components/modals/ExportSettings.tsx +++ b/src/renderer/components/modals/ExportSettings.tsx @@ -1,17 +1,33 @@ import { useSettingsStore } from '@/stores/settings-store'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; export function ExportSettings() { - const { pdfFormat, pdfMargins, pdfEmbedFonts, docxTemplate, htmlHighlightStyle, renderTablesAsAscii, setSetting } = useSettingsStore(); + const { + pdfFormat, + pdfMargins, + pdfEmbedFonts, + docxTemplate, + htmlHighlightStyle, + renderTablesAsAscii, + setSetting, + } = useSettingsStore(); return (
setSetting('pdfMargins', v as any)}> - + + + Narrow Normal @@ -32,12 +50,18 @@ export function ExportSettings() {
setSetting('htmlHighlightStyle', v as any)}> - + -
+            
               {output}
             
- + ); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/ThemeSettings.tsx b/src/renderer/components/modals/ThemeSettings.tsx index d677b20..2854d1d 100644 --- a/src/renderer/components/modals/ThemeSettings.tsx +++ b/src/renderer/components/modals/ThemeSettings.tsx @@ -1,7 +1,13 @@ import { useSettingsStore } from '@/stores/settings-store'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; export function ThemeSettings() { const { theme, accentColor, fontFamily, setSetting } = useSettingsStore(); @@ -10,16 +16,30 @@ export function ThemeSettings() {
- setSetting('theme', v as 'light' | 'dark' | 'system')}> -
-
-
+ setSetting('theme', v as 'light' | 'dark' | 'system')} + > +
+ + +
+
+ + +
+
+ + +
setSetting('fontFamily', v as any)}> - + + + System (Plus Jakarta Sans) JetBrains Mono @@ -42,4 +64,4 @@ export function ThemeSettings() {
); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/UpdatesSettings.tsx b/src/renderer/components/modals/UpdatesSettings.tsx index 15d6a57..6722673 100644 --- a/src/renderer/components/modals/UpdatesSettings.tsx +++ b/src/renderer/components/modals/UpdatesSettings.tsx @@ -58,4 +58,4 @@ export function UpdatesSettings() {
); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/WelcomeDialog.tsx b/src/renderer/components/modals/WelcomeDialog.tsx index defbcda..88051b9 100644 --- a/src/renderer/components/modals/WelcomeDialog.tsx +++ b/src/renderer/components/modals/WelcomeDialog.tsx @@ -1,5 +1,12 @@ import { useState } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { useAppStore } from '@/stores/app-store'; @@ -27,20 +34,30 @@ export function WelcomeDialog() {

1. Open a folder

-

File → Open Folder, or ⌘O. Your tree appears on the left.

+

+ File → Open Folder, or ⌘O. Your tree appears on the left. +

2. Edit & preview

-

Type on the left, see the rendered preview on the right. Toggle with ⌘\.

+

+ Type on the left, see the rendered preview on the right. Toggle with ⌘\. +

3. Export anywhere

-

File → Export to PDF / DOCX / HTML, or batch convert a folder.

+

+ File → Export to PDF / DOCX / HTML, or batch convert a folder. +

@@ -48,4 +65,4 @@ export function WelcomeDialog() { ); -} \ No newline at end of file +} diff --git a/src/renderer/components/modals/WordExportDialog.tsx b/src/renderer/components/modals/WordExportDialog.tsx index 654f151..ad3f075 100644 --- a/src/renderer/components/modals/WordExportDialog.tsx +++ b/src/renderer/components/modals/WordExportDialog.tsx @@ -1,5 +1,12 @@ import { useState, useEffect } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; @@ -16,7 +23,9 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) { const docxCustomTemplatePath = useSettingsStore((s) => s.docxCustomTemplatePath); const source = useExportSource(); - const [templateMode, setTemplateMode] = useState<'standard' | 'custom'>(docxCustomTemplatePath ? 'custom' : 'standard'); + const [templateMode, setTemplateMode] = useState<'standard' | 'custom'>( + docxCustomTemplatePath ? 'custom' : 'standard' + ); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -32,7 +41,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) { }; const handleSubmit = async () => { - if (!source) { setError('No file open.'); return; } + if (!source) { + setError('No file open.'); + return; + } setSubmitting(true); setError(null); try { @@ -41,7 +53,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) { title: source.title, customTemplatePath: templateMode === 'custom' ? docxCustomTemplatePath : null, }); - const saveResult = await ipc.app.showSaveDialog?.({ title: 'Save as Word document', defaultPath: source.path.replace(/\.md$/, '.docx') }); + const saveResult = await ipc.app.showSaveDialog?.({ + title: 'Save as Word document', + defaultPath: source.path.replace(/\.md$/, '.docx'), + }); if (!saveResult?.ok || !saveResult.data) { setSubmitting(false); return; @@ -73,7 +88,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
- setTemplateMode(v as 'standard' | 'custom')}> + setTemplateMode(v as 'standard' | 'custom')} + >
@@ -87,9 +105,13 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) { {templateMode === 'custom' && (
{docxCustomTemplatePath ? ( - Template path: {docxCustomTemplatePath} + + Template path: {docxCustomTemplatePath} + ) : ( - No template selected. Click "Choose template..." to pick a .dotx file. + + No template selected. Click "Choose template..." to pick a .dotx file. + )}
)} {error && ( -
+
{error}
)}
- + diff --git a/src/renderer/components/modals/WritingAnalyticsDialog.tsx b/src/renderer/components/modals/WritingAnalyticsDialog.tsx index 4f3bc3d..ba7459c 100644 --- a/src/renderer/components/modals/WritingAnalyticsDialog.tsx +++ b/src/renderer/components/modals/WritingAnalyticsDialog.tsx @@ -1,5 +1,11 @@ import { useState, useEffect } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useAppStore } from '@/stores/app-store'; @@ -32,7 +38,8 @@ export function WritingAnalyticsDialog() { }, [isGoalReached, celebrated]); // Find max count for word cloud scaling - const maxWordCount = metrics.topWords.length > 0 ? Math.max(...metrics.topWords.map((w) => w.count)) : 1; + const maxWordCount = + metrics.topWords.length > 0 ? Math.max(...metrics.topWords.map((w) => w.count)) : 1; // Helper to determine color coding for Flesch Readability Ease const getReadabilityColor = (score: number) => { @@ -50,7 +57,8 @@ export function WritingAnalyticsDialog() { Writing Analytics
- Real-time readability metrics, vocabulary analysis, and progress towards your daily word goal. + Real-time readability metrics, vocabulary analysis, and progress towards your daily word + goal. @@ -75,9 +83,7 @@ export function WritingAnalyticsDialog() {

Grade Level

-

- {metrics.fleschGrade} -

+

{metrics.fleschGrade}

US school grade

@@ -225,12 +231,19 @@ export function WritingAnalyticsDialog() {
- Unique words: {metrics.uniqueWordCount} /{' '} - {metrics.wordCount} ({metrics.wordCount > 0 ? Math.round((metrics.uniqueWordCount / metrics.wordCount) * 100) : 0}%) + Unique words:{' '} + {metrics.uniqueWordCount} /{' '} + {metrics.wordCount} ( + {metrics.wordCount > 0 + ? Math.round((metrics.uniqueWordCount / metrics.wordCount) * 100) + : 0} + %)
-

Word Frequency Cloud

+

+ Word Frequency Cloud +

{metrics.topWords.length === 0 ? (
Add more words to see vocabulary statistics diff --git a/src/renderer/components/preview/MermaidLazy.tsx b/src/renderer/components/preview/MermaidLazy.tsx index 6880b3d..ccc537e 100644 --- a/src/renderer/components/preview/MermaidLazy.tsx +++ b/src/renderer/components/preview/MermaidLazy.tsx @@ -39,7 +39,12 @@ export function MermaidLazy({ code }: Props) { }; }, [code]); - if (error) return
{error}
; + if (error) + return ( +
+ {error} +
+ ); if (!svg) return
Loading diagram…
; return
; } diff --git a/src/renderer/components/sidebar/FileTree.tsx b/src/renderer/components/sidebar/FileTree.tsx index 67863f0..2b1e2a1 100644 --- a/src/renderer/components/sidebar/FileTree.tsx +++ b/src/renderer/components/sidebar/FileTree.tsx @@ -2,7 +2,13 @@ import { ChevronRight, Folder, FolderOpen, File, FileText } from 'lucide-react'; import { useFileStore } from '@/stores/file-store'; import { cn } from '@/lib/utils'; -function FileTreeNode({ node, depth }: { node: import('@/stores/file-store').FileNode; depth: number }) { +function FileTreeNode({ + node, + depth, +}: { + node: import('@/stores/file-store').FileNode; + depth: number; +}) { const { expanded, activeTabId, loadChildren, toggleExpanded, openFile } = useFileStore(); const isExpanded = expanded.has(node.path); const isActive = activeTabId === node.path; diff --git a/src/renderer/components/sidebar/GitStatusPanel.tsx b/src/renderer/components/sidebar/GitStatusPanel.tsx index 8a38e62..1e59339 100644 --- a/src/renderer/components/sidebar/GitStatusPanel.tsx +++ b/src/renderer/components/sidebar/GitStatusPanel.tsx @@ -65,7 +65,9 @@ export function GitStatusPanel() {
Error: {error}

Not a git repository, or git not installed.

- +
); } @@ -77,7 +79,9 @@ export function GitStatusPanel() { return (
- {status.length} changed file{status.length === 1 ? '' : 's'} + + {status.length} changed file{status.length === 1 ? '' : 's'} + @@ -98,4 +102,4 @@ export function GitStatusPanel() {
); -} \ No newline at end of file +} diff --git a/src/renderer/components/sidebar/Sidebar.tsx b/src/renderer/components/sidebar/Sidebar.tsx index d328eb9..e3eb05d 100644 --- a/src/renderer/components/sidebar/Sidebar.tsx +++ b/src/renderer/components/sidebar/Sidebar.tsx @@ -35,7 +35,11 @@ export function Sidebar() { {!tree ? (
No folder opened -
@@ -114,11 +118,21 @@ export function Sidebar() { matching section into view. The hidden elements expose a hook for Playwright tests and the menu handler. */} ); diff --git a/src/renderer/components/sidebar/Templates.tsx b/src/renderer/components/sidebar/Templates.tsx index 5d3cf39..ec429c1 100644 --- a/src/renderer/components/sidebar/Templates.tsx +++ b/src/renderer/components/sidebar/Templates.tsx @@ -5,7 +5,11 @@ import { insertSnippet } from '@/lib/editor-commands'; const TEMPLATES = [ { name: 'Blog Post', file: 'blog-post.md', description: 'Article with frontmatter' }, { name: 'Meeting Notes', file: 'meeting-notes.md', description: 'Agenda, notes, action items' }, - { name: 'Technical Spec', file: 'technical-spec.md', description: 'Requirements and architecture' }, + { + name: 'Technical Spec', + file: 'technical-spec.md', + description: 'Requirements and architecture', + }, { name: 'Changelog', file: 'changelog.md', description: 'Keep a Changelog format' }, { name: 'README', file: 'readme.md', description: 'Project documentation' }, { name: 'Project Plan', file: 'project-plan.md', description: 'Goals, milestones, timeline' }, @@ -50,7 +54,9 @@ export function Templates() { {t.name}
-

{t.description}

+

+ {t.description} +

))} diff --git a/src/renderer/components/theme-provider.tsx b/src/renderer/components/theme-provider.tsx index faf4cc9..30519a2 100644 --- a/src/renderer/components/theme-provider.tsx +++ b/src/renderer/components/theme-provider.tsx @@ -3,4 +3,4 @@ import type { ComponentProps } from 'react'; export function ThemeProvider({ children, ...props }: ComponentProps) { return {children}; -} \ No newline at end of file +} diff --git a/src/renderer/components/theme-toggle.tsx b/src/renderer/components/theme-toggle.tsx index fc16a74..5112f32 100644 --- a/src/renderer/components/theme-toggle.tsx +++ b/src/renderer/components/theme-toggle.tsx @@ -27,4 +27,4 @@ export function ThemeToggle() { {isDark ? : } ); -} \ No newline at end of file +} diff --git a/src/renderer/components/tools/ReplPanel.tsx b/src/renderer/components/tools/ReplPanel.tsx index e474e7f..cad950b 100644 --- a/src/renderer/components/tools/ReplPanel.tsx +++ b/src/renderer/components/tools/ReplPanel.tsx @@ -54,4 +54,4 @@ export function ReplPanel() {
); -} \ No newline at end of file +} diff --git a/src/renderer/components/ui/button.tsx b/src/renderer/components/ui/button.tsx index 208a599..91f7174 100644 --- a/src/renderer/components/ui/button.tsx +++ b/src/renderer/components/ui/button.tsx @@ -1,57 +1,49 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, } -) +); export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean + extends React.ButtonHTMLAttributes, VariantProps { + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button'; return ( - - ) + + ); } -) -Button.displayName = "Button" +); +Button.displayName = 'Button'; -export { Button, buttonVariants } \ No newline at end of file +export { Button, buttonVariants }; diff --git a/src/renderer/components/ui/checkbox.tsx b/src/renderer/components/ui/checkbox.tsx index 97819e0..26db6a7 100644 --- a/src/renderer/components/ui/checkbox.tsx +++ b/src/renderer/components/ui/checkbox.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { Check } from "lucide-react" -import { cn } from "@/lib/utils" +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { Check } from 'lucide-react'; +import { cn } from '@/lib/utils'; const Checkbox = React.forwardRef< React.ElementRef, @@ -10,18 +10,16 @@ const Checkbox = React.forwardRef< - + -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; -export { Checkbox } \ No newline at end of file +export { Checkbox }; diff --git a/src/renderer/components/ui/collapsible.tsx b/src/renderer/components/ui/collapsible.tsx index 9fa4894..86ab87d 100644 --- a/src/renderer/components/ui/collapsible.tsx +++ b/src/renderer/components/ui/collapsible.tsx @@ -1,11 +1,11 @@ -"use client" +'use client'; -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; -const Collapsible = CollapsiblePrimitive.Root +const Collapsible = CollapsiblePrimitive.Root; -const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; -const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; -export { Collapsible, CollapsibleTrigger, CollapsibleContent } +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/src/renderer/components/ui/context-menu.tsx b/src/renderer/components/ui/context-menu.tsx index 1306dd2..72f7e03 100644 --- a/src/renderer/components/ui/context-menu.tsx +++ b/src/renderer/components/ui/context-menu.tsx @@ -1,34 +1,34 @@ -"use client" +'use client'; -import * as React from "react" -import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from 'react'; +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; +import { Check, ChevronRight, Circle } from 'lucide-react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -const ContextMenu = ContextMenuPrimitive.Root +const ContextMenu = ContextMenuPrimitive.Root; -const ContextMenuTrigger = ContextMenuPrimitive.Trigger +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; -const ContextMenuGroup = ContextMenuPrimitive.Group +const ContextMenuGroup = ContextMenuPrimitive.Group; -const ContextMenuPortal = ContextMenuPrimitive.Portal +const ContextMenuPortal = ContextMenuPrimitive.Portal; -const ContextMenuSub = ContextMenuPrimitive.Sub +const ContextMenuSub = ContextMenuPrimitive.Sub; -const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; const ContextMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( -)) -ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; const ContextMenuSubContent = React.forwardRef< React.ElementRef, @@ -46,13 +46,13 @@ const ContextMenuSubContent = React.forwardRef< -)) -ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; const ContextMenuContent = React.forwardRef< React.ElementRef, @@ -62,32 +62,32 @@ const ContextMenuContent = React.forwardRef< -)) -ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; const ContextMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; const ContextMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -96,7 +96,7 @@ const ContextMenuCheckboxItem = React.forwardRef< {children} -)) -ContextMenuCheckboxItem.displayName = - ContextMenuPrimitive.CheckboxItem.displayName +)); +ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; const ContextMenuRadioItem = React.forwardRef< React.ElementRef, @@ -120,7 +119,7 @@ const ContextMenuRadioItem = React.forwardRef< {children} -)) -ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; const ContextMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; const ContextMenuSeparator = React.forwardRef< React.ElementRef, @@ -159,27 +154,21 @@ const ContextMenuSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; -const ContextMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { +const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( - ) -} -ContextMenuShortcut.displayName = "ContextMenuShortcut" + ); +}; +ContextMenuShortcut.displayName = 'ContextMenuShortcut'; export { ContextMenu, @@ -197,4 +186,4 @@ export { ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup, -} +}; diff --git a/src/renderer/components/ui/dialog.tsx b/src/renderer/components/ui/dialog.tsx index 5160fa7..1775930 100644 --- a/src/renderer/components/ui/dialog.tsx +++ b/src/renderer/components/ui/dialog.tsx @@ -1,34 +1,26 @@ -"use client" +'use client'; -import * as React from "react" -import { XIcon } from "lucide-react" -import * as DialogPrimitive from "@radix-ui/react-dialog" +import * as React from 'react'; +import { XIcon } from 'lucide-react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; -function Dialog({ - ...props -}: React.ComponentProps) { - return +function Dialog({ ...props }: React.ComponentProps) { + return ; } -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return +function DialogTrigger({ ...props }: React.ComponentProps) { + return ; } -function DialogPortal({ - ...props -}: React.ComponentProps) { - return +function DialogPortal({ ...props }: React.ComponentProps) { + return ; } -function DialogClose({ - ...props -}: React.ComponentProps) { - return +function DialogClose({ ...props }: React.ComponentProps) { + return ; } function DialogOverlay({ @@ -39,12 +31,12 @@ function DialogOverlay({ - ) + ); } function DialogContent({ @@ -53,7 +45,7 @@ function DialogContent({ showCloseButton = true, ...props }: React.ComponentProps & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { return ( @@ -61,7 +53,7 @@ function DialogContent({ - ) + ); } -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
- ) + ); } function DialogFooter({ @@ -96,16 +88,13 @@ function DialogFooter({ showCloseButton = false, children, ...props -}: React.ComponentProps<"div"> & { - showCloseButton?: boolean +}: React.ComponentProps<'div'> & { + showCloseButton?: boolean; }) { return (
{children} @@ -115,20 +104,17 @@ function DialogFooter({ )}
- ) + ); } -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { +function DialogTitle({ className, ...props }: React.ComponentProps) { return ( - ) + ); } function DialogDescription({ @@ -138,10 +124,10 @@ function DialogDescription({ return ( - ) + ); } export { @@ -155,4 +141,4 @@ export { DialogPortal, DialogTitle, DialogTrigger, -} \ No newline at end of file +}; diff --git a/src/renderer/components/ui/form.tsx b/src/renderer/components/ui/form.tsx index ca5791b..96d1210 100644 --- a/src/renderer/components/ui/form.tsx +++ b/src/renderer/components/ui/form.tsx @@ -1,75 +1,71 @@ -"use client" +'use client'; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; import { Controller, type ControllerProps, type FieldPath, type FieldValues, useFormContext, -} from "react-hook-form" +} from 'react-hook-form'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -const Form = React.forwardRef< - HTMLFormElement, - React.ComponentProps<"form"> ->(({ className, ...props }, ref) => { - const methods = useFormContext() - return ( -
{})} {...props} /> - ) -}) -Form.displayName = "Form" +const Form = React.forwardRef>( + ({ className, ...props }, ref) => { + const methods = useFormContext(); + return ( + {})} {...props} /> + ); + } +); +Form.displayName = 'Form'; // ============================================================ // FormFieldContext - provides field-level context // ============================================================ -const FormFieldContext = React.createContext< - ControllerProps ->({} as never) +const FormFieldContext = React.createContext>({} as never); const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { - const { name } = props + const { name } = props; return ( }> - ) -} + ); +}; // ============================================================ // useFormField - must be used inside a FormField // ============================================================ function useFormField(): ControllerProps { - const fieldContext = React.useContext(FormFieldContext) + const fieldContext = React.useContext(FormFieldContext); if (!fieldContext) { - throw new Error("useFormField must be used within a FormField") + throw new Error('useFormField must be used within a FormField'); } - return fieldContext + return fieldContext; } // ============================================================ // FormItem - wraps a labeled form control // ============================================================ -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -FormItem.displayName = "FormItem" +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +FormItem.displayName = 'FormItem'; // ============================================================ // FormLabel @@ -82,13 +78,13 @@ const FormLabel = React.forwardRef< -)) -FormLabel.displayName = LabelPrimitive.Root.displayName +)); +FormLabel.displayName = LabelPrimitive.Root.displayName; // ============================================================ // FormControl - Slot bridge @@ -97,10 +93,8 @@ FormLabel.displayName = LabelPrimitive.Root.displayName const FormControl = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ ...props }, ref) => ( - -)) -FormControl.displayName = "FormControl" +>(({ ...props }, ref) => ); +FormControl.displayName = 'FormControl'; // ============================================================ // FormDescription @@ -110,13 +104,9 @@ const FormDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

-)) -FormDescription.displayName = "FormDescription" +

+)); +FormDescription.displayName = 'FormDescription'; // ============================================================ // FormMessage @@ -126,20 +116,16 @@ const FormMessage = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, children, ...props }, ref) => { - const { error } = useFormField() - const body = error ? String(error.message ?? "") : children - if (!body) return null + const { error } = useFormField(); + const body = error ? String(error.message ?? '') : children; + if (!body) return null; return ( -

+

{body}

- ) -}) -FormMessage.displayName = "FormMessage" + ); +}); +FormMessage.displayName = 'FormMessage'; export { Form, @@ -150,4 +136,4 @@ export { FormDescription, FormMessage, useFormField, -} +}; diff --git a/src/renderer/components/ui/input.tsx b/src/renderer/components/ui/input.tsx index e66133c..e58b68d 100644 --- a/src/renderer/components/ui/input.tsx +++ b/src/renderer/components/ui/input.tsx @@ -1,9 +1,8 @@ -import * as React from "react" +import * as React from 'react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -export interface InputProps - extends React.InputHTMLAttributes {} +export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef( ({ className, type, ...props }, ref) => { @@ -11,15 +10,15 @@ const Input = React.forwardRef( - ) + ); } -) -Input.displayName = "Input" +); +Input.displayName = 'Input'; -export { Input } \ No newline at end of file +export { Input }; diff --git a/src/renderer/components/ui/label.tsx b/src/renderer/components/ui/label.tsx index 97dea45..8fdf078 100644 --- a/src/renderer/components/ui/label.tsx +++ b/src/renderer/components/ui/label.tsx @@ -1,6 +1,6 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cn } from "@/lib/utils" +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cn } from '@/lib/utils'; const Label = React.forwardRef< React.ElementRef, @@ -9,12 +9,12 @@ const Label = React.forwardRef< -)) -Label.displayName = LabelPrimitive.Root.displayName +)); +Label.displayName = LabelPrimitive.Root.displayName; -export { Label } +export { Label }; diff --git a/src/renderer/components/ui/radio-group.tsx b/src/renderer/components/ui/radio-group.tsx index d34dfdf..7264c0b 100644 --- a/src/renderer/components/ui/radio-group.tsx +++ b/src/renderer/components/ui/radio-group.tsx @@ -1,21 +1,15 @@ -import * as React from "react" -import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" -import { Circle } from "lucide-react" -import { cn } from "@/lib/utils" +import * as React from 'react'; +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; +import { Circle } from 'lucide-react'; +import { cn } from '@/lib/utils'; const RadioGroup = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { - return ( - - ) -}) -RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + return ; +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; const RadioGroupItem = React.forwardRef< React.ElementRef, @@ -25,7 +19,7 @@ const RadioGroupItem = React.forwardRef< - ) -}) -RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; -export { RadioGroup, RadioGroupItem } +export { RadioGroup, RadioGroupItem }; diff --git a/src/renderer/components/ui/resizable.tsx b/src/renderer/components/ui/resizable.tsx index a312351..6438a59 100644 --- a/src/renderer/components/ui/resizable.tsx +++ b/src/renderer/components/ui/resizable.tsx @@ -1,10 +1,10 @@ -"use client" +'use client'; -import * as React from "react" -import { GripVertical } from "lucide-react" -import * as ResizablePrimitive from "react-resizable-panels" +import * as React from 'react'; +import { GripVertical } from 'lucide-react'; +import * as ResizablePrimitive from 'react-resizable-panels'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const ResizablePanelGroup = React.forwardRef< React.ElementRef, @@ -12,14 +12,11 @@ const ResizablePanelGroup = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -ResizablePanelGroup.displayName = "ResizablePanelGroup" +)); +ResizablePanelGroup.displayName = 'ResizablePanelGroup'; const ResizablePanel = React.forwardRef< React.ElementRef, @@ -27,25 +24,22 @@ const ResizablePanel = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -ResizablePanel.displayName = "ResizablePanel" +)); +ResizablePanel.displayName = 'ResizablePanel'; const ResizableHandle = ({ withHandle, className, ...props }: React.ComponentPropsWithoutRef & { - withHandle?: boolean + withHandle?: boolean; }) => ( div]:rotate-90", + 'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90', className )} {...props} @@ -56,7 +50,7 @@ const ResizableHandle = ({
)} -) -ResizableHandle.displayName = "ResizableHandle" +); +ResizableHandle.displayName = 'ResizableHandle'; -export { ResizablePanelGroup, ResizablePanel, ResizableHandle } \ No newline at end of file +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/src/renderer/components/ui/scroll-area.tsx b/src/renderer/components/ui/scroll-area.tsx index 3c9d887..80e03e0 100644 --- a/src/renderer/components/ui/scroll-area.tsx +++ b/src/renderer/components/ui/scroll-area.tsx @@ -1,9 +1,9 @@ -"use client" +'use client'; -import * as React from "react" -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" +import * as React from 'react'; +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const ScrollArea = React.forwardRef< React.ElementRef, @@ -11,7 +11,7 @@ const ScrollArea = React.forwardRef< >(({ className, children, ...props }, ref) => ( @@ -20,28 +20,26 @@ const ScrollArea = React.forwardRef< -)) -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; const ScrollBar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, orientation = "vertical", ...props }, ref) => ( +>(({ className, orientation = 'vertical', ...props }, ref) => ( -)) -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName -export { ScrollArea, ScrollBar } +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; +export { ScrollArea, ScrollBar }; diff --git a/src/renderer/components/ui/select.tsx b/src/renderer/components/ui/select.tsx index 3de94d2..2de68c5 100644 --- a/src/renderer/components/ui/select.tsx +++ b/src/renderer/components/ui/select.tsx @@ -1,15 +1,15 @@ -"use client" +'use client'; -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { Check, ChevronDown, ChevronUp } from "lucide-react" -import { cn } from "@/lib/utils" +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { Check, ChevronDown, ChevronUp } from 'lucide-react'; +import { cn } from '@/lib/utils'; -const Select = SelectPrimitive.Root +const Select = SelectPrimitive.Root; -const SelectGroup = SelectPrimitive.Group +const SelectGroup = SelectPrimitive.Group; -const SelectValue = SelectPrimitive.Value +const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef, @@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', className )} {...props} @@ -28,8 +28,8 @@ const SelectTrigger = React.forwardRef< -)) -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectScrollUpButton = React.forwardRef< React.ElementRef, @@ -37,16 +37,13 @@ const SelectScrollUpButton = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; const SelectScrollDownButton = React.forwardRef< React.ElementRef, @@ -54,29 +51,25 @@ const SelectScrollDownButton = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName +)); +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; const SelectContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, position = "popper", ...props }, ref) => ( +>(({ className, children, position = 'popper', ...props }, ref) => ( {children} @@ -95,8 +88,8 @@ const SelectContent = React.forwardRef< -)) -SelectContent.displayName = SelectPrimitive.Content.displayName +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< React.ElementRef, @@ -104,11 +97,11 @@ const SelectLabel = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SelectLabel.displayName = SelectPrimitive.Label.displayName +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< React.ElementRef, @@ -117,7 +110,7 @@ const SelectItem = React.forwardRef< {children} -)) -SelectItem.displayName = SelectPrimitive.Item.displayName +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< React.ElementRef, @@ -139,11 +132,11 @@ const SelectSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SelectSeparator.displayName = SelectPrimitive.Separator.displayName +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, @@ -156,4 +149,4 @@ export { SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, -} \ No newline at end of file +}; diff --git a/src/renderer/components/ui/sheet.tsx b/src/renderer/components/ui/sheet.tsx index 885c295..4484955 100644 --- a/src/renderer/components/ui/sheet.tsx +++ b/src/renderer/components/ui/sheet.tsx @@ -1,25 +1,22 @@ -"use client" +'use client'; -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +import * as React from 'react'; +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { X } from 'lucide-react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -const Sheet = SheetPrimitive.Root +const Sheet = SheetPrimitive.Root; -const SheetTrigger = SheetPrimitive.Trigger +const SheetTrigger = SheetPrimitive.Trigger; -const SheetClose = SheetPrimitive.Close +const SheetClose = SheetPrimitive.Close; -const SheetPortal = ({ - className, - ...props -}: SheetPrimitive.DialogPortalProps) => ( +const SheetPortal = ({ className, ...props }: SheetPrimitive.DialogPortalProps) => ( -) -SheetPortal.displayName = SheetPrimitive.Portal.displayName +); +SheetPortal.displayName = SheetPrimitive.Portal.displayName; const SheetOverlay = React.forwardRef< React.ElementRef, @@ -27,49 +24,46 @@ const SheetOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( - "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500", + 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500', { variants: { side: { - top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', bottom: - "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", - left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', right: - "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm" - } + 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + }, }, defaultVariants: { - side: "right" - } + side: 'right', + }, } -) +); interface SheetContentProps - extends React.ComponentPropsWithoutRef, + extends + React.ComponentPropsWithoutRef, VariantProps {} const SheetContent = React.forwardRef< React.ElementRef, SheetContentProps ->(({ side = "right", className, children, ...props }, ref) => ( +>(({ side = 'right', className, children, ...props }, ref) => ( - + {children} @@ -77,36 +71,21 @@ const SheetContent = React.forwardRef< -)) -SheetContent.displayName = SheetPrimitive.Content.displayName +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; -const SheetHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( +const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = 'SheetHeader'; + +const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => (
-) -SheetHeader.displayName = "SheetHeader" - -const SheetFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -SheetFooter.displayName = "SheetFooter" +); +SheetFooter.displayName = 'SheetFooter'; const SheetTitle = React.forwardRef< React.ElementRef, @@ -114,11 +93,11 @@ const SheetTitle = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SheetTitle.displayName = SheetPrimitive.Title.displayName +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; const SheetDescription = React.forwardRef< React.ElementRef, @@ -126,11 +105,11 @@ const SheetDescription = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SheetDescription.displayName = SheetPrimitive.Description.displayName +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; export { Sheet, @@ -142,5 +121,5 @@ export { SheetHeader, SheetFooter, SheetTitle, - SheetDescription -} \ No newline at end of file + SheetDescription, +}; diff --git a/src/renderer/components/ui/slider.tsx b/src/renderer/components/ui/slider.tsx index 81877b2..e707664 100644 --- a/src/renderer/components/ui/slider.tsx +++ b/src/renderer/components/ui/slider.tsx @@ -1,8 +1,8 @@ -"use client" +'use client'; -import * as React from "react" -import * as SliderPrimitive from "@radix-ui/react-slider" -import { cn } from "@/lib/utils" +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; +import { cn } from '@/lib/utils'; const Slider = React.forwardRef< React.ElementRef, @@ -12,10 +12,7 @@ const Slider = React.forwardRef< return ( @@ -27,7 +24,7 @@ const Slider = React.forwardRef< /> ); -}) -Slider.displayName = SliderPrimitive.Root.displayName +}); +Slider.displayName = SliderPrimitive.Root.displayName; -export { Slider } +export { Slider }; diff --git a/src/renderer/components/ui/sonner.tsx b/src/renderer/components/ui/sonner.tsx index b24d6c3..6317bca 100644 --- a/src/renderer/components/ui/sonner.tsx +++ b/src/renderer/components/ui/sonner.tsx @@ -1,26 +1,24 @@ -"use client" +'use client'; -import { useTheme } from "next-themes" -import { Toaster as Sonner } from "sonner" +import { useTheme } from 'next-themes'; +import { Toaster as Sonner } from 'sonner'; -type ToasterProps = React.ComponentProps +type ToasterProps = React.ComponentProps; const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() + const { theme = 'system' } = useTheme(); return ( { position="bottom-right" {...props} /> - ) -} + ); +}; -export { Toaster } \ No newline at end of file +export { Toaster }; diff --git a/src/renderer/components/ui/switch.tsx b/src/renderer/components/ui/switch.tsx index 2783b63..d0d3daa 100644 --- a/src/renderer/components/ui/switch.tsx +++ b/src/renderer/components/ui/switch.tsx @@ -1,6 +1,6 @@ -import * as React from "react" -import * as SwitchPrimitive from "@radix-ui/react-switch" -import { cn } from "@/lib/utils" +import * as React from 'react'; +import * as SwitchPrimitive from '@radix-ui/react-switch'; +import { cn } from '@/lib/utils'; const Switch = React.forwardRef< React.ElementRef, @@ -9,18 +9,18 @@ const Switch = React.forwardRef< -)) -Switch.displayName = SwitchPrimitive.Root.displayName +)); +Switch.displayName = SwitchPrimitive.Root.displayName; -export { Switch } \ No newline at end of file +export { Switch }; diff --git a/src/renderer/components/ui/tabs.tsx b/src/renderer/components/ui/tabs.tsx index eb014e6..d787119 100644 --- a/src/renderer/components/ui/tabs.tsx +++ b/src/renderer/components/ui/tabs.tsx @@ -1,14 +1,14 @@ -"use client" +'use client'; -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; function Tabs({ className, - orientation = "horizontal", + orientation = 'horizontal', ...props }: React.ComponentProps) { return ( @@ -16,36 +16,32 @@ function Tabs({ data-slot="tabs" data-orientation={orientation} orientation={orientation} - className={cn( - "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", - className - )} + className={cn('group/tabs flex gap-2 data-[orientation=horizontal]:flex-col', className)} {...props} /> - ) + ); } const tabsListVariants = cva( - "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", + 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none', { variants: { variant: { - default: "bg-muted", - line: "gap-1 bg-transparent", + default: 'bg-muted', + line: 'gap-1 bg-transparent', }, }, defaultVariants: { - variant: "default", + variant: 'default', }, } -) +); function TabsList({ className, - variant = "default", + variant = 'default', ...props -}: React.ComponentProps & - VariantProps) { +}: React.ComponentProps & VariantProps) { return ( - ) + ); } -function TabsTrigger({ - className, - ...props -}: React.ComponentProps) { +function TabsTrigger({ className, ...props }: React.ComponentProps) { return ( - ) + ); } -function TabsContent({ - className, - ...props -}: React.ComponentProps) { +function TabsContent({ className, ...props }: React.ComponentProps) { return ( - ) + ); } -export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } \ No newline at end of file +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; diff --git a/src/renderer/components/ui/textarea.tsx b/src/renderer/components/ui/textarea.tsx index 83caa9b..b6d2be1 100644 --- a/src/renderer/components/ui/textarea.tsx +++ b/src/renderer/components/ui/textarea.tsx @@ -1,24 +1,23 @@ -import * as React from "react" +import * as React from 'react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -export interface TextareaProps - extends React.TextareaHTMLAttributes {} +export interface TextareaProps extends React.TextareaHTMLAttributes {} const Textarea = React.forwardRef( ({ className, ...props }, ref) => { return (