Compare commits

...
3 Commits
Author SHA1 Message Date
amitwh baf644d62b fix(modal): prevent duplicate ModalManager declaration
window.ModalManager was set unconditionally, causing "Identifier
'ModalManager' has already been declared" when script tag in HTML
also loaded ModalManager before renderer.js required it.

Now checks !window.ModalManager before setting.

Amit Haridas
2026-05-22 22:08:06 +05:30
amitwh f9a5420ad2 4.4.1: update version everywhere, fix DOMPurify initialization
- Bump version to 4.4.1
- DOMPurify now initialized with window context (fixes markdown rendering)
- Add 'it' to eslint globals

Amit Haridas
2026-05-22 21:54:05 +05:30
amitwh f8361174f2 chore: add it to eslint globals, add debug logging to createEditor
Amit Haridas
2026-05-22 21:42:34 +05:30
11 changed files with 346 additions and 8 deletions
+1
View File
@@ -75,6 +75,7 @@ module.exports = [
jest: 'readonly', jest: 'readonly',
describe: 'readonly', describe: 'readonly',
test: 'readonly', test: 'readonly',
it: 'readonly',
expect: 'readonly', expect: 'readonly',
beforeEach: 'readonly', beforeEach: 'readonly',
afterEach: 'readonly', afterEach: 'readonly',
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "markdown-converter", "name": "markdown-converter",
"version": "4.3.0", "version": "4.4.1",
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting", "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -4,7 +4,7 @@
* Implements file system operations for Electron using IPC. * Implements file system operations for Electron using IPC.
* This abstracts file operations to enable easier testing and migration. * This abstracts file operations to enable easier testing and migration.
* *
* @version 4.3.0 * @version 4.4.1
*/ */
/** /**
+1 -1
View File
@@ -5,7 +5,7 @@
* Adapters abstract file system, conversion, and system operations * Adapters abstract file system, conversion, and system operations
* to enable easier testing and future platform migration. * to enable easier testing and future platform migration.
* *
* @version 4.3.0 * @version 4.4.1
*/ */
/** /**
+5
View File
@@ -59,6 +59,11 @@ const jetBrainsMonoTheme = EditorView.theme({
* @returns {EditorView} the created editor view * @returns {EditorView} the created editor view
*/ */
function createEditor(parentElement, options = {}) { function createEditor(parentElement, options = {}) {
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;
}
const { const {
content = '', content = '',
onChange = () => {}, onChange = () => {},
+1 -1
View File
@@ -9,7 +9,7 @@
* - All IPC channels are explicitly whitelisted * - All IPC channels are explicitly whitelisted
* - Prevents XSS from escalating to full system access * - Prevents XSS from escalating to full system access
* *
* @version 4.3.0 * @version 4.4.1
*/ */
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
+1 -1
View File
@@ -1,6 +1,6 @@
/** /**
* MarkdownConverter Renderer Process * MarkdownConverter Renderer Process
* @version 4.3.0 * @version 4.4.1
*/ */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
+3 -3
View File
@@ -1,6 +1,6 @@
/** /**
* ModalManager - Unified modal system with accessibility support * ModalManager - Unified modal system with accessibility support
* @version 4.3.0 * @version 4.4.1
*/ */
class ModalManager { class ModalManager {
#modal; #modal;
@@ -237,8 +237,8 @@ class ModalManager {
} }
} }
// Export for use in renderer // Export for use in renderer - avoid duplicate declaration
if (typeof window !== 'undefined') { if (typeof window !== 'undefined' && !window.ModalManager) {
window.ModalManager = ModalManager; window.ModalManager = ModalManager;
} }
+123
View File
@@ -0,0 +1,123 @@
/**
* Git Operations Utilities Test Suite
* Tests for async patterns and error handling
* @version 4.3.0
*/
describe('GitOperations Utilities', () => {
describe('error handling patterns', () => {
it('should handle git errors gracefully', () => {
const errorResponse = { error: 'Not a git repository' };
expect(errorResponse).toHaveProperty('error');
expect(errorResponse.error).toContain('repository');
});
it('should return error object on failure', () => {
const failureResult = { error: 'Failed to commit' };
expect(failureResult).toBeDefined();
expect(failureResult.error).toBeTruthy();
});
});
describe('async patterns', () => {
it('should handle async operations', async () => {
const asyncFn = async () => {
return { status: 'success' };
};
const result = await asyncFn();
expect(result).toHaveProperty('status');
});
it('should handle async errors', async () => {
const asyncFnWithError = async () => {
throw new Error('Git operation failed');
};
try {
await asyncFnWithError();
expect(true).toBe(false); // Should not reach here
} catch (err) {
expect(err.message).toContain('Git');
}
});
});
describe('git operations', () => {
it('should document expected operations', () => {
const gitOps = [
'getStatus',
'stage',
'commit',
'log',
'diff',
'branch',
'checkout',
'push',
'pull'
];
expect(gitOps.length).toBeGreaterThan(0);
gitOps.forEach(op => {
expect(typeof op).toBe('string');
expect(op.length).toBeGreaterThan(0);
});
});
it('should handle directory paths', () => {
const paths = [
'/home/user/project',
'./current/dir',
'../parent/dir'
];
paths.forEach(pathStr => {
expect(typeof pathStr).toBe('string');
expect(pathStr.length).toBeGreaterThan(0);
});
});
it('should handle commit messages', () => {
const messages = [
'fix: bug in git panel',
'feat: add new feature',
'refactor: clean up code'
];
messages.forEach(msg => {
expect(typeof msg).toBe('string');
expect(msg.length).toBeGreaterThan(0);
});
});
});
describe('response structures', () => {
it('should return status info', () => {
const statusResponse = {
conflicted: [],
created: [],
deleted: [],
modified: [],
renamed: [],
staged: ['file.md']
};
expect(statusResponse).toHaveProperty('staged');
expect(Array.isArray(statusResponse.staged)).toBe(true);
});
it('should return log entries', () => {
const logResponse = {
all: [
{ hash: 'abc123', message: 'fix: something' }
],
latest: { hash: 'abc123', message: 'fix: something' }
};
expect(logResponse).toHaveProperty('all');
expect(Array.isArray(logResponse.all)).toBe(true);
});
});
});
+95
View File
@@ -0,0 +1,95 @@
/**
* PDF Operations Utilities Test Suite
* Tests for helper functions and logic patterns
* @version 4.3.0
*/
describe('PDFOperations Utilities', () => {
describe('page range parsing', () => {
it('should parse single page numbers', () => {
// Test logic: parsing "1" should extract page index 0
const input = '1';
const pages = [0]; // Parsed result
expect(pages.length).toBe(1);
expect(pages[0]).toBe(0);
});
it('should parse page ranges', () => {
// Test logic: parsing "1-3" should extract pages 0, 1, 2
const input = '1-3';
const pages = [0, 1, 2]; // Expected result
expect(pages.length).toBe(3);
expect(pages).toEqual([0, 1, 2]);
});
it('should handle multiple ranges', () => {
// Test logic: parsing "1-2,4-5" should extract pages 0,1,3,4
const input = '1-2,4-5';
const pages = [0, 1, 3, 4]; // Expected result
expect(pages.length).toBe(4);
expect(pages).toEqual([0, 1, 3, 4]);
});
it('should sort pages in ascending order', () => {
const unsorted = [2, 0, 3, 1];
const sorted = unsorted.sort((a, b) => a - b);
expect(sorted).toEqual([0, 1, 2, 3]);
});
});
describe('hex color conversion', () => {
it('should validate hex color format', () => {
const validHex = '#FF5733';
const isValid = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.test(validHex);
expect(isValid).toBe(true);
});
it('should detect invalid hex colors', () => {
const invalidColors = ['#GG5733', '#12345', 'notahex'];
invalidColors.forEach(color => {
const isValid = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.test(color);
expect(isValid).toBe(false);
});
});
});
describe('error handling', () => {
it('should handle empty input', () => {
const input = '';
expect(input.length).toBe(0);
// Should not process empty strings
expect(input === '').toBe(true);
});
it('should handle invalid page numbers', () => {
const testPages = [
{ input: '0', isValid: false },
{ input: '-1', isValid: false },
{ input: 'abc', isValid: false },
{ input: '1', isValid: true },
{ input: '5', isValid: true }
];
testPages.forEach(({ input, isValid }) => {
const num = parseInt(input);
if (isNaN(num)) {
// Non-numeric string
expect(isValid).toBe(false);
} else if (num < 1) {
// Zero or negative
expect(isValid).toBe(false);
} else {
// Positive number
expect(isValid).toBe(true);
}
});
});
});
});
+114
View File
@@ -0,0 +1,114 @@
/**
* Security: Path Traversal Prevention Test Suite
* @version 4.3.0
*/
const path = require('path');
describe('Security: Path Handling', () => {
describe('path traversal prevention', () => {
it('should detect path traversal patterns', () => {
const maliciousPaths = [
'../etc/passwd',
'../../sensitive',
'./../outside'
];
maliciousPaths.forEach(pathStr => {
// Path traversal attempts contain .. patterns
expect(pathStr).toMatch(/\.\./);
});
});
it('should normalize relative paths safely', () => {
const safePaths = [
'./documents/file.md',
'relative/path/file.txt'
];
safePaths.forEach(pathStr => {
const normalized = path.normalize(pathStr);
// Safe relative paths should normalize cleanly
expect(normalized).toBeDefined();
expect(typeof normalized).toBe('string');
});
});
it('should detect absolute paths', () => {
const absolutePath = '/etc/passwd';
const isAbsolute = path.isAbsolute(absolutePath);
// Linux/Mac: /path is absolute
if (process.platform !== 'win32') {
expect(isAbsolute).toBe(true);
}
});
it('should safely join paths with base directory', () => {
const baseDir = '/safe/base/directory';
const userInput = 'documents/file.md';
const joined = path.join(baseDir, userInput);
// Result should contain the safe base
expect(joined).toContain('base');
expect(joined).toContain('documents');
});
});
describe('filename safety', () => {
it('should identify safe filenames', () => {
const safeNames = [
'document.md',
'my-file.txt',
'file_name.pdf',
'report_2026_04_24.xlsx'
];
safeNames.forEach(name => {
// Safe names should not contain path separators or null bytes
const isSafe = !/[\\/\0]/.test(name) && name.length > 0;
expect(isSafe).toBe(true);
});
});
it('should flag filenames with path separators', () => {
const problematicNames = [
'file/with/slashes.txt',
'file\\with\\backslashes.txt'
];
problematicNames.forEach(name => {
// These contain path separators and should be flagged
const hasPathSeparators = /[\\/]/.test(name);
expect(hasPathSeparators).toBe(true);
});
});
it('should enforce minimum filename length', () => {
const emptyName = '';
expect(emptyName.length).toBe(0);
expect(emptyName.length > 0).toBe(false);
});
});
describe('validation patterns', () => {
it('should validate path existence check pattern', () => {
const validationPattern = /^[a-zA-Z0-9._\-/]+$/;
const validPaths = ['documents/file.md', 'folder_2026/data.csv'];
validPaths.forEach(pathStr => {
// These should match a reasonable filename pattern
expect(typeof pathStr).toBe('string');
});
});
it('should prevent null byte injection', () => {
const pathWithNullByte = 'file.txt\0.exe';
const isSafe = !pathWithNullByte.includes('\0');
expect(isSafe).toBe(false); // Has null byte, not safe
});
});
});