mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
feat: implement Custom Preview CSS, Reveal.js options, Large File Mode, and Interactive PDF Thumbnail Sidebar
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "markdown-converter",
|
||||
"version": "4.4.1",
|
||||
"version": "4.4.2",
|
||||
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -288,6 +288,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="export-section revealjs-only" style="display: none;">
|
||||
<label>Reveal.js Options:</label>
|
||||
<div class="form-row">
|
||||
<label for="reveal-theme">Slide Theme:</label>
|
||||
<select id="reveal-theme">
|
||||
<option value="black">Black</option>
|
||||
<option value="white">White</option>
|
||||
<option value="league">League</option>
|
||||
<option value="beige">Beige</option>
|
||||
<option value="sky">Sky</option>
|
||||
<option value="night">Night</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="simple">Simple</option>
|
||||
<option value="solarized">Solarized</option>
|
||||
<option value="blood">Blood</option>
|
||||
<option value="moon">Moon</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="reveal-transition">Transition:</label>
|
||||
<select id="reveal-transition">
|
||||
<option value="slide">Slide</option>
|
||||
<option value="none">None</option>
|
||||
<option value="fade">Fade</option>
|
||||
<option value="convex">Convex</option>
|
||||
<option value="concave">Concave</option>
|
||||
<option value="zoom">Zoom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="reveal-speed">Transition Speed:</label>
|
||||
<select id="reveal-speed">
|
||||
<option value="default">Default</option>
|
||||
<option value="fast">Fast</option>
|
||||
<option value="slow">Slow</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<label><input type="checkbox" id="reveal-slide-number"> Slide Numbers</label>
|
||||
<label><input type="checkbox" id="reveal-controls" checked> Controls</label>
|
||||
<label><input type="checkbox" id="reveal-progress" checked> Progress Bar</label>
|
||||
<label><input type="checkbox" id="reveal-history" checked> Slide History</label>
|
||||
<label><input type="checkbox" id="reveal-center" checked> Center Content</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="export-section">
|
||||
<label>Bibliography:</label>
|
||||
<div class="form-row">
|
||||
@@ -905,6 +951,7 @@
|
||||
<button class="modal-close" id="pdf-editor-dialog-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body pdf-editor-body">
|
||||
<div class="pdf-editor-left-panel">
|
||||
<!-- Merge PDFs Section -->
|
||||
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
||||
<div class="export-section">
|
||||
@@ -1287,6 +1334,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Scrollable Visual Page Sidebar -->
|
||||
<div id="pdf-thumbnail-sidebar" class="pdf-editor-right-panel hidden">
|
||||
<h4>Page Preview & Editor</h4>
|
||||
<p class="sidebar-help">Select a PDF to view pages, check to delete, click ↻ to rotate, and use ◀ / ▶ to reorder.</p>
|
||||
<div id="pdf-thumbnail-grid" class="pdf-thumbnail-grid">
|
||||
<!-- Page thumbnail cards will be rendered here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="pdf-editor-cancel" class="btn btn-secondary" data-close>Cancel</button>
|
||||
<button id="pdf-editor-process" class="btn btn-primary">Process</button>
|
||||
|
||||
+57
-2
@@ -849,6 +849,20 @@ function createMenu() {
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Custom Preview CSS',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Load Custom Preview CSS...',
|
||||
click: () => mainWindow.webContents.send('load-custom-css')
|
||||
},
|
||||
{
|
||||
label: 'Clear Custom Preview CSS',
|
||||
click: () => mainWindow.webContents.send('clear-custom-css')
|
||||
}
|
||||
]
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' },
|
||||
{ label: 'Toggle DevTools', accelerator: 'F12', role: 'toggleDevTools' },
|
||||
{ type: 'separator' },
|
||||
@@ -2297,8 +2311,32 @@ function performExportWithOptions(format, options) {
|
||||
dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export: ${err.message}`));
|
||||
}
|
||||
} else if (format === 'revealjs') {
|
||||
pandocCmd = `${getPandocPath()} "${currentFile}" -t revealjs -s -o "${outputFile}" --slide-level=2`;
|
||||
exportWithPandoc(pandocCmd, outputFile, format);
|
||||
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);
|
||||
@@ -3989,8 +4027,25 @@ ipcMain.handle('list-directory', async (event, dirPath) => {
|
||||
console.error('list-directory error:', err);
|
||||
return null;
|
||||
}
|
||||
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'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (result && result[0]) {
|
||||
const filePath = result[0];
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, content };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
|
||||
ipcMain.handle('read-file', async (event, filePath) => {
|
||||
const validation = validatePath(filePath);
|
||||
if (!validation.valid || !isPathAccessible(validation.resolved)) {
|
||||
|
||||
@@ -23,6 +23,7 @@ const ALLOWED_SEND_CHANNELS = [
|
||||
'save-recent-files',
|
||||
'clear-recent-files',
|
||||
'renderer-ready',
|
||||
'select-custom-css',
|
||||
|
||||
// Theme
|
||||
'get-theme',
|
||||
@@ -150,6 +151,8 @@ const ALLOWED_RECEIVE_CHANNELS = [
|
||||
'get-content-for-save',
|
||||
'get-content-for-spreadsheet',
|
||||
'recent-files-cleared',
|
||||
'load-custom-css',
|
||||
'clear-custom-css',
|
||||
|
||||
// UI toggles
|
||||
'toggle-preview',
|
||||
|
||||
+443
-15
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* MarkdownConverter Renderer Process
|
||||
* @version 4.4.1
|
||||
* @version 4.4.2
|
||||
*/
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
@@ -11,7 +11,16 @@ const DOMPurify = createDOMPurify(window);
|
||||
const hljs = require('highlight.js');
|
||||
const { createEditor } = require('./editor/codemirror-setup');
|
||||
const { undo, redo } = require('@codemirror/commands');
|
||||
const { ModalManager } = require('./utils/ModalManager');
|
||||
// Use window.ModalManager if already set by script tag, otherwise require it.
|
||||
// This prevents "Identifier 'ModalManager' has already been declared" when
|
||||
// both the script tag in index.html and CommonJS require() declare it.
|
||||
let ModalManager;
|
||||
if (typeof window !== 'undefined' && window.ModalManager) {
|
||||
ModalManager = window.ModalManager;
|
||||
} else {
|
||||
const result = require('./utils/ModalManager');
|
||||
ModalManager = result.ModalManager || result;
|
||||
}
|
||||
// Lazy-loaded modules — defer heavy imports until first use
|
||||
let _SidebarManager, _renderTemplatesPanel, _renderExplorerPanel, _renderGitPanel, _renderSnippetsPanel;
|
||||
let _ReplPanel, _CommandPalette, _PrintPreview, _createWelcomeContent;
|
||||
@@ -102,19 +111,24 @@ marked.use({
|
||||
start(src) { return src.match(/^:::(note|warning|tip|danger|info)/m)?.index; },
|
||||
tokenizer(src) {
|
||||
const match = src.match(/^:::(note|warning|tip|danger|info)\s*\n([\s\S]*?)^:::\s*$/m);
|
||||
if (match) {
|
||||
if (match && match.index === 0) {
|
||||
const admonitionType = match[1];
|
||||
const text = match[2].trim();
|
||||
const tokens = [];
|
||||
this.lexer.blockTokens(text, tokens);
|
||||
return {
|
||||
type: 'admonition',
|
||||
raw: match[0],
|
||||
admonitionType: match[1],
|
||||
text: match[2].trim()
|
||||
admonitionType,
|
||||
text,
|
||||
tokens
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
const icons = { note: '\u2139', warning: '\u26A0', tip: '\uD83D\uDCA1', danger: '\uD83D\uDD34', info: '\u2139' };
|
||||
const icon = icons[token.admonitionType] || '\u2139';
|
||||
const inner = this.parser.parse(token.text);
|
||||
const inner = this.parser.parse(token.tokens || []);
|
||||
return `<div class="admonition admonition-${token.admonitionType}">
|
||||
<div class="admonition-title">${icon} ${token.admonitionType.charAt(0).toUpperCase() + token.admonitionType.slice(1)}</div>
|
||||
<div class="admonition-content">${inner}</div>
|
||||
@@ -131,6 +145,18 @@ function plantumlEncode(text) {
|
||||
return '~h' + hex;
|
||||
}
|
||||
|
||||
// Scopes user custom CSS by prefixing all selectors to protect application UI
|
||||
function scopeCSS(cssText, scopeSelector) {
|
||||
if (!cssText) return '';
|
||||
return cssText.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|(?=[^{]*{))/g, (match, selector, separator) => {
|
||||
const trimmed = selector.trim();
|
||||
if (!trimmed || trimmed.startsWith('@') || trimmed.startsWith(':root') || trimmed.startsWith('from') || trimmed.startsWith('to') || /^\d+%$/.test(trimmed)) {
|
||||
return match;
|
||||
}
|
||||
return scopeSelector + ' ' + trimmed + (separator || '');
|
||||
});
|
||||
}
|
||||
|
||||
// Module-level reference set by DOMContentLoaded after sidebar registration
|
||||
const outlinePanelContainer = null;
|
||||
|
||||
@@ -147,6 +173,7 @@ class TabManager {
|
||||
this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]');
|
||||
this.previewDebounceTimers = new Map(); // Debounce timers per tab
|
||||
this.previewDebounceDelay = 300; // 300ms debounce
|
||||
this.previewCache = new Map(); // Performance optimization render cache
|
||||
|
||||
// Initialize first tab
|
||||
this.tabs.set(1, {
|
||||
@@ -463,6 +490,17 @@ class TabManager {
|
||||
onChange: (newContent) => {
|
||||
tab.content = newContent;
|
||||
tab.isDirty = true;
|
||||
// Dynamically enable/disable Large File Mode on edit
|
||||
if (newContent.length > 1024 * 1024) {
|
||||
if (!tab.largeFileMode) {
|
||||
tab.largeFileMode = true;
|
||||
this.isPreviewVisible = false;
|
||||
this.updatePreviewVisibility();
|
||||
notifyUser('Large content detected (>1MB). Large File Mode enabled to maintain peak responsiveness. Live preview auto-render is disabled.', 'warning');
|
||||
}
|
||||
} else {
|
||||
tab.largeFileMode = false;
|
||||
}
|
||||
this.updatePreview(tab.id);
|
||||
this.updateWordCount();
|
||||
this.updateTabBar();
|
||||
@@ -661,6 +699,11 @@ class TabManager {
|
||||
const tab = this.tabs.get(tabId);
|
||||
if (!tab || tab.type === 'pdf') return;
|
||||
|
||||
// Guard against auto-renders in large file mode
|
||||
if (tab.largeFileMode && !immediate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing debounce timer for this tab
|
||||
if (this.previewDebounceTimers.has(tabId)) {
|
||||
clearTimeout(this.previewDebounceTimers.get(tabId));
|
||||
@@ -679,6 +722,16 @@ class TabManager {
|
||||
}, this.previewDebounceDelay));
|
||||
}
|
||||
|
||||
_hash(str) {
|
||||
if (!str) return 0;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
_renderPreview(tabId) {
|
||||
const tab = this.tabs.get(tabId);
|
||||
const preview = document.getElementById(`preview-${tabId}`);
|
||||
@@ -693,16 +746,32 @@ class TabManager {
|
||||
preview.innerHTML = '<div class="preview-error"><div class="preview-error-icon">⚠️</div><div class="preview-error-title">Libraries Not Loaded</div><div class="preview-error-message">Required libraries (marked/DOMPurify) could not be loaded. Please check your installation.</div></div>';
|
||||
return;
|
||||
}
|
||||
console.log('[_renderPreview] About to call marked.parse, content length:', tab.content.length);
|
||||
|
||||
// Cache lookup
|
||||
const contentHash = this._hash(tab.content);
|
||||
let sanitizedHtml;
|
||||
if (this.previewCache.has(contentHash)) {
|
||||
console.log('[_renderPreview] Cache hit for hash:', contentHash);
|
||||
sanitizedHtml = this.previewCache.get(contentHash);
|
||||
} else {
|
||||
console.log('[_renderPreview] Cache miss. About to call marked.parse, content length:', tab.content.length);
|
||||
const html = marked.parse(tab.content);
|
||||
console.log('[_renderPreview] marked.parse returned type:', typeof html, 'isPromise:', html instanceof Promise);
|
||||
if (html instanceof Promise) {
|
||||
console.error('[_renderPreview] marked.parse returned a Promise! Need to await it.');
|
||||
return;
|
||||
}
|
||||
let sanitizedHtml = DOMPurify.sanitize(html);
|
||||
sanitizedHtml = DOMPurify.sanitize(html);
|
||||
console.log('[_renderPreview] DOMPurify.sanitize returned, length:', sanitizedHtml.length);
|
||||
|
||||
// Add to cache and maintain cache size limit (100 entries)
|
||||
this.previewCache.set(contentHash, sanitizedHtml);
|
||||
if (this.previewCache.size > 100) {
|
||||
const oldestKey = this.previewCache.keys().next().value;
|
||||
this.previewCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
// TOC generation
|
||||
if (sanitizedHtml.includes('[[toc]]') || sanitizedHtml.includes('[TOC]')) {
|
||||
const headingRegex = /<h([1-6])[^>]*>(.*?)<\/h[1-6]>/gi;
|
||||
@@ -1337,6 +1406,17 @@ class TabManager {
|
||||
}
|
||||
}
|
||||
|
||||
checkForLargeFile(tab, content) {
|
||||
if (content && content.length > 1024 * 1024) { // >1MB
|
||||
tab.largeFileMode = true;
|
||||
this.isPreviewVisible = false;
|
||||
this.updatePreviewVisibility();
|
||||
notifyUser('Large file detected (>1MB). Large File Mode enabled to maintain peak responsiveness. Live preview auto-render is disabled.', 'warning');
|
||||
} else {
|
||||
tab.largeFileMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
// File operations
|
||||
openFile(filePath, content) {
|
||||
console.log('openFile called with:', filePath, 'content length:', content.length);
|
||||
@@ -1355,10 +1435,12 @@ class TabManager {
|
||||
tab.originalContent = content;
|
||||
tab.isDirty = false;
|
||||
|
||||
this.checkForLargeFile(tab, content);
|
||||
|
||||
// Update the editor and preview
|
||||
this.setEditorContent(this.activeTabId, content);
|
||||
console.log('openFile after setEditorContent, tab state:', { id: tab.id, hasEditorView: !!tab.editorView, contentLength: tab.content.length });
|
||||
this.updatePreview(this.activeTabId);
|
||||
this.updatePreview(this.activeTabId, true); // immediate=true for initial load
|
||||
this.updateWordCount();
|
||||
} else {
|
||||
// Create new tab for the file
|
||||
@@ -1372,10 +1454,12 @@ class TabManager {
|
||||
tab.originalContent = content;
|
||||
tab.isDirty = false;
|
||||
|
||||
this.checkForLargeFile(tab, content);
|
||||
|
||||
// Set content in the CodeMirror editor
|
||||
this.setEditorContent(this.activeTabId, content);
|
||||
console.log('openFile after setEditorContent, tab state:', { id: tab.id, hasEditorView: !!tab.editorView, contentLength: tab.content.length });
|
||||
this.updatePreview(this.activeTabId);
|
||||
this.updatePreview(this.activeTabId, true); // immediate=true for initial load
|
||||
this.updateWordCount();
|
||||
}
|
||||
this.startAutoSave();
|
||||
@@ -1460,6 +1544,13 @@ let replPanel;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
tabManager = new TabManager();
|
||||
|
||||
// Load saved Custom Preview CSS if present
|
||||
const savedCSSContent = localStorage.getItem('customPreviewCSSContent');
|
||||
if (savedCSSContent) {
|
||||
applyCustomPreviewCSS(savedCSSContent);
|
||||
}
|
||||
|
||||
const ReplPanel = getReplPanel();
|
||||
replPanel = new ReplPanel();
|
||||
|
||||
@@ -1781,6 +1872,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
commandPalette.register('Insert Image', '', () => tabManager.wrapSelection(''));
|
||||
commandPalette.register('Toggle Zen Mode', 'F11', () => zenMode.toggle());
|
||||
commandPalette.register('Writing Analytics', 'Ctrl+Shift+A', () => getShowAnalyticsModal()(tabManager));
|
||||
commandPalette.register('Load Custom Preview CSS', '', triggerLoadCustomCSS);
|
||||
commandPalette.register('Clear Custom Preview CSS', '', triggerClearCustomCSS);
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -1948,7 +2041,48 @@ ipcRenderer.on('redo', () => {
|
||||
redo(tab.editorView);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Custom Preview CSS event handlers and trigger helpers
|
||||
function applyCustomPreviewCSS(cssContent) {
|
||||
let styleTag = document.getElementById('custom-preview-style');
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style');
|
||||
styleTag.id = 'custom-preview-style';
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
const scopedCSS = scopeCSS(cssContent, '.preview-content');
|
||||
styleTag.textContent = scopedCSS;
|
||||
}
|
||||
|
||||
async function triggerLoadCustomCSS() {
|
||||
try {
|
||||
const result = await window.electronAPI.invoke('select-custom-css');
|
||||
if (result) {
|
||||
console.log('[RENDERER] Custom CSS loaded from:', result.path);
|
||||
localStorage.setItem('customPreviewCSSPath', result.path);
|
||||
localStorage.setItem('customPreviewCSSContent', result.content);
|
||||
applyCustomPreviewCSS(result.content);
|
||||
notifyUser('Custom preview stylesheet applied successfully.', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[RENDERER] Error loading custom CSS:', err);
|
||||
notifyUser('Failed to load custom CSS stylesheet.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function triggerClearCustomCSS() {
|
||||
localStorage.removeItem('customPreviewCSSPath');
|
||||
localStorage.removeItem('customPreviewCSSContent');
|
||||
const styleTag = document.getElementById('custom-preview-style');
|
||||
if (styleTag) {
|
||||
styleTag.remove();
|
||||
}
|
||||
notifyUser('Custom preview stylesheet cleared.', 'info');
|
||||
}
|
||||
|
||||
ipcRenderer.on('load-custom-css', triggerLoadCustomCSS);
|
||||
ipcRenderer.on('clear-custom-css', triggerClearCustomCSS);
|
||||
|
||||
|
||||
|
||||
// Font size adjustment
|
||||
let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15;
|
||||
@@ -2069,13 +2203,39 @@ function initializeExportForm(format) {
|
||||
document.getElementById('export-citeproc').checked = false;
|
||||
document.getElementById('export-toc-depth').value = 3;
|
||||
|
||||
// PDF-specific fields
|
||||
// Toggle and reset PDF-specific fields
|
||||
const pdfOnly = document.querySelector('.pdf-only');
|
||||
if (pdfOnly) {
|
||||
pdfOnly.style.display = (format === 'pdf') ? 'block' : 'none';
|
||||
}
|
||||
if (format === 'pdf') {
|
||||
document.getElementById('pdf-engine').value = 'xelatex';
|
||||
document.getElementById('pdf-geometry').value = 'margin=1in';
|
||||
document.getElementById('custom-geometry').style.display = 'none';
|
||||
}
|
||||
|
||||
// Toggle and reset Reveal.js-specific fields
|
||||
const revealjsOnly = document.querySelector('.revealjs-only');
|
||||
if (revealjsOnly) {
|
||||
revealjsOnly.style.display = (format === 'revealjs') ? 'block' : 'none';
|
||||
}
|
||||
const revealTheme = document.getElementById('reveal-theme');
|
||||
if (revealTheme) revealTheme.value = 'black';
|
||||
const revealTransition = document.getElementById('reveal-transition');
|
||||
if (revealTransition) revealTransition.value = 'slide';
|
||||
const revealSpeed = document.getElementById('reveal-speed');
|
||||
if (revealSpeed) revealSpeed.value = 'default';
|
||||
const revealSlideNumber = document.getElementById('reveal-slide-number');
|
||||
if (revealSlideNumber) revealSlideNumber.checked = false;
|
||||
const revealControls = document.getElementById('reveal-controls');
|
||||
if (revealControls) revealControls.checked = true;
|
||||
const revealProgress = document.getElementById('reveal-progress');
|
||||
if (revealProgress) revealProgress.checked = true;
|
||||
const revealHistory = document.getElementById('reveal-history');
|
||||
if (revealHistory) revealHistory.checked = true;
|
||||
const revealCenter = document.getElementById('reveal-center');
|
||||
if (revealCenter) revealCenter.checked = true;
|
||||
|
||||
// Clear bibliography fields
|
||||
document.getElementById('bibliography-file').value = '';
|
||||
document.getElementById('csl-file').value = '';
|
||||
@@ -2135,6 +2295,18 @@ function collectExportOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
// Reveal.js-specific options
|
||||
if (currentExportFormat === 'revealjs') {
|
||||
options.revealTheme = document.getElementById('reveal-theme').value;
|
||||
options.revealTransition = document.getElementById('reveal-transition').value;
|
||||
options.revealTransitionSpeed = document.getElementById('reveal-speed').value;
|
||||
options.revealSlideNumber = document.getElementById('reveal-slide-number').checked;
|
||||
options.revealControls = document.getElementById('reveal-controls').checked;
|
||||
options.revealProgress = document.getElementById('reveal-progress').checked;
|
||||
options.revealHistory = document.getElementById('reveal-history').checked;
|
||||
options.revealCenter = document.getElementById('reveal-center').checked;
|
||||
}
|
||||
|
||||
// Bibliography
|
||||
const bibFile = document.getElementById('bibliography-file').value.trim();
|
||||
const cslFile = document.getElementById('csl-file').value.trim();
|
||||
@@ -2145,6 +2317,15 @@ function collectExportOptions() {
|
||||
if (currentExportFormat === 'pdf') {
|
||||
options.pdfEngine = 'xelatex';
|
||||
options.geometry = 'margin=1in';
|
||||
} else if (currentExportFormat === 'revealjs') {
|
||||
options.revealTheme = 'black';
|
||||
options.revealTransition = 'slide';
|
||||
options.revealTransitionSpeed = 'default';
|
||||
options.revealSlideNumber = false;
|
||||
options.revealControls = true;
|
||||
options.revealProgress = true;
|
||||
options.revealHistory = true;
|
||||
options.revealCenter = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3062,6 +3243,18 @@ function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
section.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Hide visual thumbnail sidebar by default
|
||||
const sidebar = document.getElementById('pdf-thumbnail-sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('hidden');
|
||||
}
|
||||
const body = document.querySelector('.pdf-editor-body');
|
||||
if (body) {
|
||||
body.classList.remove('side-by-side');
|
||||
}
|
||||
pdfDocInstance = null;
|
||||
pdfPreviewState = { pages: [], filePath: '' };
|
||||
|
||||
// Show the appropriate section and set title
|
||||
let sectionId, titleText;
|
||||
switch (operation) {
|
||||
@@ -3095,7 +3288,11 @@ function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
titleText = 'Rotate Pages';
|
||||
if (openedFilePath) {
|
||||
const rotateInput = document.getElementById('rotate-input-path');
|
||||
if (rotateInput) rotateInput.value = openedFilePath;
|
||||
if (rotateInput) {
|
||||
rotateInput.value = openedFilePath;
|
||||
// Trigger thumbnail generation
|
||||
setTimeout(() => onPDFFileSelected('rotate-input-path', openedFilePath), 50);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
@@ -3103,7 +3300,11 @@ function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
titleText = 'Delete Pages';
|
||||
if (openedFilePath) {
|
||||
const deleteInput = document.getElementById('delete-input-path');
|
||||
if (deleteInput) deleteInput.value = openedFilePath;
|
||||
if (deleteInput) {
|
||||
deleteInput.value = openedFilePath;
|
||||
// Trigger thumbnail generation
|
||||
setTimeout(() => onPDFFileSelected('delete-input-path', openedFilePath), 50);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'reorder':
|
||||
@@ -3111,7 +3312,11 @@ function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
titleText = 'Reorder Pages';
|
||||
if (openedFilePath) {
|
||||
const reorderInput = document.getElementById('reorder-input-path');
|
||||
if (reorderInput) reorderInput.value = openedFilePath;
|
||||
if (reorderInput) {
|
||||
reorderInput.value = openedFilePath;
|
||||
// Trigger thumbnail generation
|
||||
setTimeout(() => onPDFFileSelected('reorder-input-path', openedFilePath), 50);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'watermark':
|
||||
@@ -3272,6 +3477,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
document.getElementById(button.inputId).value = file.path;
|
||||
onPDFFileSelected(button.inputId, file.path);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
@@ -3281,6 +3487,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
document.getElementById(button.inputId).value = file.path;
|
||||
onPDFFileSelected(button.inputId, file.path);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
@@ -4889,3 +5096,224 @@ ipcRenderer.on('show-video-tool', (event, tool) => {
|
||||
{ duration: 5000 }
|
||||
);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// PDF EDITOR VISUAL PAGE SIDEBAR HANDLERS
|
||||
// ============================================
|
||||
let pdfDocInstance = null;
|
||||
let pdfPreviewState = { pages: [], filePath: '' };
|
||||
|
||||
function onPDFFileSelected(inputId, filePath) {
|
||||
const eligibleInputs = ['rotate-input-path', 'delete-input-path', 'reorder-input-path'];
|
||||
if (!eligibleInputs.includes(inputId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sidebar = document.getElementById('pdf-thumbnail-sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('hidden');
|
||||
}
|
||||
const body = document.querySelector('.pdf-editor-body');
|
||||
if (body) {
|
||||
body.classList.add('side-by-side');
|
||||
}
|
||||
|
||||
loadPDFThumbnails(filePath);
|
||||
}
|
||||
|
||||
async function loadPDFThumbnails(filePath) {
|
||||
const grid = document.getElementById('pdf-thumbnail-grid');
|
||||
if (!grid) return;
|
||||
|
||||
// Reset previous inputs
|
||||
const rotateInput = document.getElementById('rotate-pages');
|
||||
if (rotateInput) rotateInput.value = '';
|
||||
const deleteInput = document.getElementById('delete-pages');
|
||||
if (deleteInput) deleteInput.value = '';
|
||||
const reorderInput = document.getElementById('reorder-pages');
|
||||
if (reorderInput) reorderInput.value = '';
|
||||
|
||||
grid.innerHTML = '<div class="loading-thumbnails">Loading page thumbnails...</div>';
|
||||
|
||||
try {
|
||||
const pdfjs = getPdfjsLib();
|
||||
const loadingTask = pdfjs.getDocument(filePath);
|
||||
pdfDocInstance = await loadingTask.promise;
|
||||
|
||||
pdfPreviewState.filePath = filePath;
|
||||
pdfPreviewState.pages = [];
|
||||
|
||||
for (let i = 1; i <= pdfDocInstance.numPages; i++) {
|
||||
pdfPreviewState.pages.push({
|
||||
originalPageNum: i,
|
||||
rotation: 0,
|
||||
isDeleted: false
|
||||
});
|
||||
}
|
||||
|
||||
renderThumbnailGrid();
|
||||
} catch (err) {
|
||||
console.error('Error loading PDF thumbnails:', err);
|
||||
grid.innerHTML = '<div class="thumbnail-error">Failed to load PDF thumbnails.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderThumbnailGrid() {
|
||||
const grid = document.getElementById('pdf-thumbnail-grid');
|
||||
if (!grid || !pdfDocInstance) return;
|
||||
|
||||
grid.innerHTML = '';
|
||||
|
||||
pdfPreviewState.pages.forEach((pageInfo, index) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'pdf-thumbnail-card';
|
||||
if (pageInfo.isDeleted) {
|
||||
card.classList.add('marked-delete');
|
||||
}
|
||||
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'pdf-thumbnail-header';
|
||||
heading.innerHTML = `<span class="badge">Page ${pageInfo.originalPageNum}</span>`;
|
||||
|
||||
const canvasContainer = document.createElement('div');
|
||||
canvasContainer.className = 'canvas-container';
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'pdf-thumbnail-canvas';
|
||||
if (pageInfo.rotation > 0) {
|
||||
canvas.classList.add('rot' + pageInfo.rotation);
|
||||
}
|
||||
canvasContainer.appendChild(canvas);
|
||||
|
||||
// Render thumbnail canvas
|
||||
renderThumbnail(pdfDocInstance, pageInfo.originalPageNum, canvas, pageInfo.rotation);
|
||||
|
||||
const controlsRow = document.createElement('div');
|
||||
controlsRow.className = 'pdf-thumbnail-controls';
|
||||
|
||||
// Rotate Button
|
||||
const rotateBtn = document.createElement('button');
|
||||
rotateBtn.type = 'button';
|
||||
rotateBtn.className = 'btn-rotate-thumbnail';
|
||||
rotateBtn.innerHTML = '↻ Rotate 90°';
|
||||
rotateBtn.addEventListener('click', () => {
|
||||
pageInfo.rotation = (pageInfo.rotation + 90) % 360;
|
||||
canvas.className = 'pdf-thumbnail-canvas';
|
||||
if (pageInfo.rotation > 0) {
|
||||
canvas.classList.add('rot' + pageInfo.rotation);
|
||||
}
|
||||
renderThumbnail(pdfDocInstance, pageInfo.originalPageNum, canvas, pageInfo.rotation);
|
||||
syncRotateInput();
|
||||
});
|
||||
|
||||
// Delete Checkbox
|
||||
const deleteLabel = document.createElement('label');
|
||||
deleteLabel.className = 'delete-thumbnail-checkbox';
|
||||
|
||||
const deleteChk = document.createElement('input');
|
||||
deleteChk.type = 'checkbox';
|
||||
deleteChk.checked = pageInfo.isDeleted;
|
||||
deleteChk.addEventListener('change', (e) => {
|
||||
pageInfo.isDeleted = e.target.checked;
|
||||
card.classList.toggle('marked-delete', pageInfo.isDeleted);
|
||||
syncDeleteInput();
|
||||
});
|
||||
deleteLabel.appendChild(deleteChk);
|
||||
deleteLabel.appendChild(document.createTextNode(' Delete'));
|
||||
|
||||
// Reorder buttons
|
||||
const reorderDiv = document.createElement('div');
|
||||
reorderDiv.className = 'pdf-thumbnail-reorder';
|
||||
|
||||
const leftBtn = document.createElement('button');
|
||||
leftBtn.type = 'button';
|
||||
leftBtn.className = 'btn-reorder-left';
|
||||
leftBtn.innerHTML = '◀';
|
||||
leftBtn.disabled = (index === 0);
|
||||
leftBtn.addEventListener('click', () => {
|
||||
const temp = pdfPreviewState.pages[index];
|
||||
pdfPreviewState.pages[index] = pdfPreviewState.pages[index - 1];
|
||||
pdfPreviewState.pages[index - 1] = temp;
|
||||
renderThumbnailGrid();
|
||||
syncReorderInput();
|
||||
});
|
||||
|
||||
const rightBtn = document.createElement('button');
|
||||
rightBtn.type = 'button';
|
||||
rightBtn.className = 'btn-reorder-right';
|
||||
rightBtn.innerHTML = '▶';
|
||||
rightBtn.disabled = (index === pdfPreviewState.pages.length - 1);
|
||||
rightBtn.addEventListener('click', () => {
|
||||
const temp = pdfPreviewState.pages[index];
|
||||
pdfPreviewState.pages[index] = pdfPreviewState.pages[index + 1];
|
||||
pdfPreviewState.pages[index + 1] = temp;
|
||||
renderThumbnailGrid();
|
||||
syncReorderInput();
|
||||
});
|
||||
|
||||
reorderDiv.appendChild(leftBtn);
|
||||
reorderDiv.appendChild(rightBtn);
|
||||
|
||||
controlsRow.appendChild(rotateBtn);
|
||||
controlsRow.appendChild(deleteLabel);
|
||||
controlsRow.appendChild(reorderDiv);
|
||||
|
||||
card.appendChild(heading);
|
||||
card.appendChild(canvasContainer);
|
||||
card.appendChild(controlsRow);
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
async function renderThumbnail(pdfDoc, originalPageNum, canvasElement, rotation) {
|
||||
try {
|
||||
const page = await pdfDoc.getPage(originalPageNum);
|
||||
const ctx = canvasElement.getContext('2d');
|
||||
|
||||
// Render at a fixed width of 100px
|
||||
const unscaledViewport = page.getViewport({ scale: 1, rotation: 0 });
|
||||
const scale = 100 / unscaledViewport.width;
|
||||
const viewport = page.getViewport({ scale: scale, rotation: 0 });
|
||||
|
||||
canvasElement.width = viewport.width;
|
||||
canvasElement.height = viewport.height;
|
||||
|
||||
await page.render({
|
||||
canvasContext: ctx,
|
||||
viewport: viewport
|
||||
}).promise;
|
||||
} catch (err) {
|
||||
console.error('Error rendering page canvas:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function syncRotateInput() {
|
||||
const rotatedPages = pdfPreviewState.pages
|
||||
.filter(p => p.rotation > 0)
|
||||
.map(p => p.originalPageNum);
|
||||
|
||||
const rotateInput = document.getElementById('rotate-pages');
|
||||
if (rotateInput) {
|
||||
rotateInput.value = rotatedPages.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
function syncDeleteInput() {
|
||||
const deletedPages = pdfPreviewState.pages
|
||||
.filter(p => p.isDeleted)
|
||||
.map(p => p.originalPageNum);
|
||||
|
||||
const deleteInput = document.getElementById('delete-pages');
|
||||
if (deleteInput) {
|
||||
deleteInput.value = deletedPages.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
function syncReorderInput() {
|
||||
const order = pdfPreviewState.pages.map(p => p.originalPageNum);
|
||||
const reorderInput = document.getElementById('reorder-pages');
|
||||
if (reorderInput) {
|
||||
reorderInput.value = order.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2372,3 +2372,254 @@ body[class*="dark"] .breadcrumb-bar {
|
||||
@keyframes analyticsSlideUp { from{transform:translateY(16px);opacity:0} to{transform:translateY(0);opacity:1} }
|
||||
body[class*="dark"] .analytics-modal { background:var(--gray-800,#1f2937); }
|
||||
body[class*="dark"] .word-tag { background:var(--gray-700,#374151); }
|
||||
|
||||
/* ========================================
|
||||
Interactive PDF Thumbnail Sidebar Styles
|
||||
======================================== */
|
||||
.pdf-editor-body.side-by-side {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
height: 550px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-editor-left-panel {
|
||||
flex: 1.2;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pdf-editor-right-panel {
|
||||
flex: 0.8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid var(--border-color, #e5e7eb);
|
||||
padding-left: 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-editor-right-panel h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.pdf-editor-right-panel .sidebar-help {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #6b7280);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary, #ffffff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 8px;
|
||||
transition: all var(--transition-speed, 0.3s) var(--transition-ease);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--primary-light, #8b9aff);
|
||||
}
|
||||
|
||||
.pdf-thumbnail-card.marked-delete {
|
||||
border-color: var(--error, #ef4444);
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.pdf-thumbnail-card.marked-delete::after {
|
||||
content: "DELETE";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-15deg);
|
||||
background: rgba(239, 68, 68, 0.95);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
box-shadow: var(--shadow-sm);
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-header .badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: var(--bg-tertiary, #f3f4f6);
|
||||
color: var(--text-primary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--bg-secondary, #f9fafb);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* CSS Rotations */
|
||||
.rot90 {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.rot180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.rot270 {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.pdf-thumbnail-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-controls button,
|
||||
.pdf-thumbnail-controls label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-rotate-thumbnail {
|
||||
background: var(--bg-tertiary, #f3f4f6);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
color: var(--text-primary);
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-rotate-thumbnail:hover {
|
||||
background: var(--primary-light, #8b9aff);
|
||||
color: #fff;
|
||||
border-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.delete-thumbnail-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.delete-thumbnail-checkbox input {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pdf-thumbnail-reorder {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-reorder-left,
|
||||
.btn-reorder-right {
|
||||
flex: 1;
|
||||
background: var(--bg-tertiary, #f3f4f6);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
color: var(--text-primary);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-reorder-left:hover:not(:disabled),
|
||||
.btn-reorder-right:hover:not(:disabled) {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-reorder-left:disabled,
|
||||
.btn-reorder-right:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-thumbnails,
|
||||
.thumbnail-error {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.thumbnail-error {
|
||||
color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
body[class*="dark"] .pdf-thumbnail-card {
|
||||
background: var(--gray-800, #1f2937);
|
||||
border-color: var(--gray-700, #374151);
|
||||
}
|
||||
body[class*="dark"] .pdf-thumbnail-header .badge {
|
||||
background: var(--gray-700, #374151);
|
||||
color: var(--gray-200);
|
||||
}
|
||||
body[class*="dark"] .canvas-container {
|
||||
background: var(--gray-900, #111827);
|
||||
border-color: var(--gray-700);
|
||||
}
|
||||
body[class*="dark"] .btn-rotate-thumbnail,
|
||||
body[class*="dark"] .btn-reorder-left,
|
||||
body[class*="dark"] .btn-reorder-right {
|
||||
background: var(--gray-700, #374151);
|
||||
border-color: var(--gray-600);
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* ModalManager - Unified modal system with accessibility support
|
||||
* @version 4.4.1
|
||||
* @version 4.4.2
|
||||
*/
|
||||
class ModalManager {
|
||||
#modal;
|
||||
|
||||
@@ -76,6 +76,75 @@ describe('Markdown Extensions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Admonitions full parsing integration (mocked environment)', () => {
|
||||
const extension = {
|
||||
name: 'admonition',
|
||||
level: 'block',
|
||||
start(src) { return src.match(/^:::(note|warning|tip|danger|info)/m)?.index; },
|
||||
tokenizer(src) {
|
||||
const match = src.match(/^:::(note|warning|tip|danger|info)\s*\n([\s\S]*?)^:::\s*$/m);
|
||||
if (match && match.index === 0) {
|
||||
const admonitionType = match[1];
|
||||
const text = match[2].trim();
|
||||
const tokens = [];
|
||||
this.lexer.blockTokens(text, tokens);
|
||||
return {
|
||||
type: 'admonition',
|
||||
raw: match[0],
|
||||
admonitionType,
|
||||
text,
|
||||
tokens
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
const icons = { note: 'ℹ', warning: '⚠', tip: '💡', danger: '🔴', info: 'ℹ' };
|
||||
const icon = icons[token.admonitionType] || 'ℹ';
|
||||
const inner = this.parser.parse(token.tokens || []);
|
||||
return `<div class="admonition admonition-${token.admonitionType}">
|
||||
<div class="admonition-title">${icon} ${token.admonitionType.charAt(0).toUpperCase() + token.admonitionType.slice(1)}</div>
|
||||
<div class="admonition-content">${inner}</div>
|
||||
</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
test('tokenizer correctly extracts tokens and calls blockTokens', () => {
|
||||
const src = ':::note\nThis is a note.\n:::';
|
||||
const mockLexer = {
|
||||
blockTokens: jest.fn((text, tokens) => {
|
||||
tokens.push({ type: 'text', text });
|
||||
})
|
||||
};
|
||||
const context = { lexer: mockLexer };
|
||||
const result = extension.tokenizer.call(context, src);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.type).toBe('admonition');
|
||||
expect(result.admonitionType).toBe('note');
|
||||
expect(result.text).toBe('This is a note.');
|
||||
expect(mockLexer.blockTokens).toHaveBeenCalledWith('This is a note.', expect.any(Array));
|
||||
expect(result.tokens).toEqual([{ type: 'text', text: 'This is a note.' }]);
|
||||
});
|
||||
|
||||
test('renderer correctly translates tokens to HTML', () => {
|
||||
const token = {
|
||||
type: 'admonition',
|
||||
admonitionType: 'warning',
|
||||
tokens: [{ type: 'text', text: 'Be careful!' }]
|
||||
};
|
||||
const mockParser = {
|
||||
parse: jest.fn((tokens) => '<p>Be careful!</p>')
|
||||
};
|
||||
const context = { parser: mockParser };
|
||||
const html = extension.renderer.call(context, token);
|
||||
|
||||
expect(html).toContain('admonition admonition-warning');
|
||||
expect(html).toContain('⚠ Warning');
|
||||
expect(html).toContain('<p>Be careful!</p>');
|
||||
expect(mockParser.parse).toHaveBeenCalledWith(token.tokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlantUML hex encoding', () => {
|
||||
const plantumlEncode = (text) => {
|
||||
const hex = Array.from(Buffer.from(text, 'utf-8'))
|
||||
@@ -127,4 +196,53 @@ describe('Markdown Extensions', () => {
|
||||
expect(slugify('simple')).toBe('simple');
|
||||
});
|
||||
});
|
||||
|
||||
describe('scopeCSS utility', () => {
|
||||
const scopeCSS = (cssText, scopeSelector) => {
|
||||
if (!cssText) return '';
|
||||
return cssText.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|(?=[^{]*{))/g, (match, selector, separator) => {
|
||||
const trimmed = selector.trim();
|
||||
if (!trimmed || trimmed.startsWith('@') || trimmed.startsWith(':root') || trimmed.startsWith('from') || trimmed.startsWith('to') || /^\d+%$/.test(trimmed)) {
|
||||
return match;
|
||||
}
|
||||
return scopeSelector + ' ' + trimmed + (separator || '');
|
||||
});
|
||||
};
|
||||
|
||||
test('scopes standard tag selector', () => {
|
||||
const css = 'h1 { color: red; }';
|
||||
const scoped = scopeCSS(css, '.preview-content');
|
||||
expect(scoped).toBe('.preview-content h1{ color: red; }');
|
||||
});
|
||||
|
||||
test('scopes multiple class selectors', () => {
|
||||
const css = '.title, .content { font-family: sans-serif; }';
|
||||
const scoped = scopeCSS(css, '.preview-content');
|
||||
expect(scoped).toBe('.preview-content .title,.preview-content .content{ font-family: sans-serif; }');
|
||||
});
|
||||
|
||||
test('ignores @rules like @media', () => {
|
||||
const css = '@media (max-width: 600px) { h1 { color: blue; } }';
|
||||
const scoped = scopeCSS(css, '.preview-content');
|
||||
expect(scoped).toContain('@media (max-width: 600px)');
|
||||
});
|
||||
|
||||
test('ignores :root selector', () => {
|
||||
const css = ':root { --color: red; }';
|
||||
const scoped = scopeCSS(css, '.preview-content');
|
||||
expect(scoped).toBe(':root { --color: red; }');
|
||||
});
|
||||
|
||||
test('ignores keyframe percentages', () => {
|
||||
const css = '0% { opacity: 0; } 100% { opacity: 1; }';
|
||||
const scoped = scopeCSS(css, '.preview-content');
|
||||
expect(scoped).toContain('0%');
|
||||
expect(scoped).toContain('100%');
|
||||
});
|
||||
|
||||
test('handles empty input', () => {
|
||||
expect(scopeCSS('', '.preview-content')).toBe('');
|
||||
expect(scopeCSS(null, '.preview-content')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user