From 40c76d555e25cc094887100dfb20d1f2b7774350 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 17:31:38 +0530 Subject: [PATCH 01/18] fix(update): return an unsubscribe handle for update notifications --- apps/preload/src/index.ts | 8 +++++--- packages/shared-types/src/markdown-type.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/preload/src/index.ts b/apps/preload/src/index.ts index 8c73d60..8969574 100644 --- a/apps/preload/src/index.ts +++ b/apps/preload/src/index.ts @@ -54,9 +54,11 @@ const apiContract: MarkdownReaderAPI = { getPathForFile: (file: File) => webUtils.getPathForFile(file), onUpdateAvailable: (callback: (version: string) => void) => { - ipcRenderer.on(IPC_CONSTANTS.UPDATE_AVAILABLE, (_event, version: string) => { - callback(version); - }); + const handler = (_event: IpcRendererEvent, version: string) => callback(version); + ipcRenderer.on(IPC_CONSTANTS.UPDATE_AVAILABLE, handler); + return () => { + ipcRenderer.removeListener(IPC_CONSTANTS.UPDATE_AVAILABLE, handler); + }; }, downloadUpdate: () => ipcRenderer.send(IPC_CONSTANTS.DOWNLOAD_UPDATE), }; diff --git a/packages/shared-types/src/markdown-type.ts b/packages/shared-types/src/markdown-type.ts index ef602f6..ea11037 100644 --- a/packages/shared-types/src/markdown-type.ts +++ b/packages/shared-types/src/markdown-type.ts @@ -29,6 +29,6 @@ export type MarkdownReaderAPI = { exportPDF(html: string, css: string, outputPath: string): Promise; exportDOCX(html: string, css: string, outputPath: string): Promise; getPathForFile(file: File): string; - onUpdateAvailable: (callback: (version: string) => void) => void; + onUpdateAvailable: (callback: (version: string) => void) => () => void; downloadUpdate: () => void; }; From 8c227009841381b1282089266251a585b694c145 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 17:51:44 +0530 Subject: [PATCH 02/18] fix(search): add cleanup on unmount to prevent state updates --- apps/renderer/src/hooks/useFolderSearch.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/renderer/src/hooks/useFolderSearch.ts b/apps/renderer/src/hooks/useFolderSearch.ts index ead9170..b083409 100644 --- a/apps/renderer/src/hooks/useFolderSearch.ts +++ b/apps/renderer/src/hooks/useFolderSearch.ts @@ -1,4 +1,4 @@ -import { useCallback, useState, useRef } from 'react'; +import { useCallback, useState, useRef, useEffect } from 'react'; import { FolderSearchResult } from '@package/shared-types'; export function useFolderSearch(folderPath: string | null) { @@ -39,6 +39,12 @@ export function useFolderSearch(folderPath: string | null) { [folderPath] ); + useEffect(() => { + return () => { + requestId.current += 1; + }; + }, []); + return { isFolderSearchOpen, folderQuery, From 6b0dc4d0e7fcae0cebb2e9d0350ef8deadb52898 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 17:58:47 +0530 Subject: [PATCH 03/18] fix(export): make sanitizeCss fixed point to prevent reconstitution of dangerous constructs --- apps/main-processor/src/export/sanitizeCss.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/main-processor/src/export/sanitizeCss.ts b/apps/main-processor/src/export/sanitizeCss.ts index b58640e..04ee48d 100644 --- a/apps/main-processor/src/export/sanitizeCss.ts +++ b/apps/main-processor/src/export/sanitizeCss.ts @@ -2,9 +2,14 @@ import { EXPORT_CONST } from '../utils/constants/export-constants'; export function sanitizeCss(css: string): string { if (!css || typeof css !== 'string') return ''; - const sanitized = EXPORT_CONST.DANGEROUS_CSS_PATTERNS.reduce( - (safeCss, pattern) => safeCss.replace(pattern, ''), - css - ); + let sanitized = css; + let previous: string; + do { + previous = sanitized; + sanitized = EXPORT_CONST.DANGEROUS_CSS_PATTERNS.reduce( + (safeCss, pattern) => safeCss.replace(pattern, ''), + sanitized + ); + } while (sanitized !== previous); return sanitized; } From 64fefc695927e5f15d2463ce385b04848ecedf40 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 18:08:03 +0530 Subject: [PATCH 04/18] fix(export): add inline images before DOCX conversion --- apps/main-processor/src/export/exportDocx.ts | 4 +++- apps/main-processor/src/export/exportPdf.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/main-processor/src/export/exportDocx.ts b/apps/main-processor/src/export/exportDocx.ts index b51b3b7..b01e28f 100644 --- a/apps/main-processor/src/export/exportDocx.ts +++ b/apps/main-processor/src/export/exportDocx.ts @@ -1,10 +1,12 @@ import HTMLtoDOCX from 'html-to-docx'; import { writeFile } from 'node:fs/promises'; import { buildDocument } from './buildDocument'; +import { inlineImages } from './inlineImage'; import { sanitizeCss } from './sanitizeCss'; export async function exportDOCX(bodyHtml: string, css: string, outputPath: string): Promise { - const html = buildDocument(bodyHtml, sanitizeCss(css)); + const htmlWithInlineImages = await inlineImages(bodyHtml); + const html = buildDocument(htmlWithInlineImages, sanitizeCss(css)); const result = await HTMLtoDOCX(html, null, { table: { row: { cantSplit: true } }, diff --git a/apps/main-processor/src/export/exportPdf.ts b/apps/main-processor/src/export/exportPdf.ts index 058619a..1912017 100644 --- a/apps/main-processor/src/export/exportPdf.ts +++ b/apps/main-processor/src/export/exportPdf.ts @@ -8,7 +8,7 @@ export async function exportPDF(bodyHtml: string, css: string, outputPath: strin const pdfWindow = new BrowserWindow({ show: false, webPreferences: { - sandbox: false, + sandbox: true, }, }); From 11d7cbfae3d58b0f4b47fa580fd5da41d6cad262 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 18:13:06 +0530 Subject: [PATCH 05/18] fix(heading): fix marked heading to render inline markdown and keep TOC heading ids consistent --- apps/renderer/src/utils/helpers/heading-helper.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/renderer/src/utils/helpers/heading-helper.ts b/apps/renderer/src/utils/helpers/heading-helper.ts index c9dd5de..1e351c6 100644 --- a/apps/renderer/src/utils/helpers/heading-helper.ts +++ b/apps/renderer/src/utils/helpers/heading-helper.ts @@ -14,10 +14,10 @@ export function getHeadingId(text: string): string { // assigns id to the headings export function heading({ text, depth }: HeadingProps) { - const plainText = stripHtml(text); - const id = getHeadingId(plainText); - const safeText = escapeHtml(plainText); - return `${safeText}\n`; + const parsedInline = parseInline(text) as string; + const idText = decodeHtml(stripHtml(parsedInline)); + const id = getHeadingId(idText); + return `${parseInline}\n`; } //removes inline html @@ -28,7 +28,7 @@ export function stripHtml(html: string): string { export function headingText(token: Tokens.Heading): string { const inlineHtml = parseInline(token.text) as string; - return stripHtml(inlineHtml).trim(); + return decodeHtml(stripHtml(inlineHtml).trim()); } export function isHeadingToken(token: unknown): token is Tokens.Heading { From 391137b6477cc798753c35d00482ac580c08cebe Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 22:11:09 +0530 Subject: [PATCH 06/18] fix: ipc handler clean up & package override issue --- .github/workflows/production.yml | 4 -- apps/main-processor/src/file.ts | 18 +++++---- apps/main-processor/src/ipc.ts | 2 - .../src/utils/constants/ipc-validation.ts | 18 +++++++-- apps/renderer/src/components/UpdateBanner.tsx | 3 +- apps/renderer/src/hooks/useSettings.ts | 40 +++++++++---------- .../src/utils/helpers/heading-helper.ts | 10 ++--- docs/src/components/Homepage/hero.tsx | 2 +- package.json | 2 +- 9 files changed, 55 insertions(+), 44 deletions(-) diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index ad3cb95..064f47f 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -21,8 +21,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Install dependencies run: pnpm install --frozen-lockfile @@ -51,8 +49,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Setup Node uses: actions/setup-node@v4 diff --git a/apps/main-processor/src/file.ts b/apps/main-processor/src/file.ts index 6f2670d..4cfed77 100644 --- a/apps/main-processor/src/file.ts +++ b/apps/main-processor/src/file.ts @@ -34,8 +34,17 @@ export async function watchFile( pollInterval: 50, }, }); - await new Promise((resolve) => { - watcher.on('ready', resolve); + currentWatchers.set(filePath, watcher); + await new Promise((resolve, reject) => { + watcher.once('ready', resolve); + + watcher.on('error', (error) => { + const watcherError = error instanceof Error ? error : new Error(String(error)); + + void unWatchFile(filePath).then(() => onError?.(watcherError)); + + reject(watcherError); + }); }); watcher.on('change', () => { const existingTimer = debounceTimers.get(filePath); @@ -56,11 +65,6 @@ export async function watchFile( watcher.on('unlink', () => { void unWatchFile(filePath).then(() => onDeleted?.()); }); - watcher.on('error', (error) => { - const watcherError = error instanceof Error ? error : new Error(String(error)); - void unWatchFile(filePath).then(() => onError?.(watcherError)); - }); - currentWatchers.set(filePath, watcher); } //unwatch file diff --git a/apps/main-processor/src/ipc.ts b/apps/main-processor/src/ipc.ts index 6410d76..42398c8 100644 --- a/apps/main-processor/src/ipc.ts +++ b/apps/main-processor/src/ipc.ts @@ -199,9 +199,7 @@ export function registerIPCHandlers(): void { if (!validateSender(event)) { throw new Error('Untrusted sender'); } - const safeFolderPath = await resolveDirectoryPath(folderPath); - allowedFolderRoots.add(safeFolderPath); const isAllowed = Array.from(allowedFolderRoots).some( (root) => safeFolderPath === root || safeFolderPath.startsWith(`${root}/`) ); diff --git a/apps/main-processor/src/utils/constants/ipc-validation.ts b/apps/main-processor/src/utils/constants/ipc-validation.ts index 7a28049..f6f62d5 100644 --- a/apps/main-processor/src/utils/constants/ipc-validation.ts +++ b/apps/main-processor/src/utils/constants/ipc-validation.ts @@ -2,14 +2,26 @@ import path from 'path'; import { IpcMainInvokeEvent } from 'electron'; // production and dev urls -const allowedOrigin = ['file://', 'http://localhost']; export const ALLOWED_MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown']); export const allowedFolderRoots = new Set(); //validate the sender export function validateSender(event: IpcMainInvokeEvent): boolean { - const url = event.senderFrame?.url || ''; - return allowedOrigin.some((origin) => url.startsWith(origin)); + const url = event.senderFrame?.url; + + if (!url) return false; + + try { + const parsedUrl = new URL(url); + + if (parsedUrl.protocol === 'file:') { + return true; + } + + return parsedUrl.protocol === 'http:' && parsedUrl.hostname === 'localhost'; + } catch { + return false; + } } //validate path type diff --git a/apps/renderer/src/components/UpdateBanner.tsx b/apps/renderer/src/components/UpdateBanner.tsx index d1f4847..bd86ab8 100644 --- a/apps/renderer/src/components/UpdateBanner.tsx +++ b/apps/renderer/src/components/UpdateBanner.tsx @@ -4,9 +4,10 @@ import { Icons } from '../utils/constants/icon-contants'; export function UpdateBanner() { const [updateVersion, setUpdateVersion] = useState(null); useEffect(() => { - window.api.onUpdateAvailable((version: string) => { + const removeUpdateAvailable=window.api.onUpdateAvailable((version: string) => { setUpdateVersion(version); }); + return removeUpdateAvailable; }, []); if (!updateVersion) { return null; diff --git a/apps/renderer/src/hooks/useSettings.ts b/apps/renderer/src/hooks/useSettings.ts index e5ab38e..0f37458 100644 --- a/apps/renderer/src/hooks/useSettings.ts +++ b/apps/renderer/src/hooks/useSettings.ts @@ -40,24 +40,6 @@ export function useSettings() { style.textContent = settings.customCss || ''; }, [settings.customCss]); - const increaseFontSize = useCallback(() => { - setSettings((current) => ({ - ...current, - fontSize: Math.min(FONT_SIZE.MAX, current.fontSize + FONT_SIZE.INCREMENT), - })); - }, []); - - const decreaseFontSize = useCallback(() => { - setSettings((current) => ({ - ...current, - fontSize: Math.max(FONT_SIZE.MIN, current.fontSize - FONT_SIZE.INCREMENT), - })); - }, []); - - const resetFontSize = useCallback(() => { - setSettings((current) => ({ ...current, fontSize: FONT_SIZE.DEFAULT })); - }, []); - const updateSettings = useCallback(async (partial: Partial) => { if (!window.api) { setSettings((current) => ({ ...current, ...partial })); @@ -67,10 +49,28 @@ export function useSettings() { try { const next = await window.api.saveSettings(partial); setSettings(next); - } catch { - setSettings((current) => ({ ...current, ...partial })); + } catch (error) { + console.error('Failed to save settings:', error); + throw error; } }, []); + + const increaseFontSize = useCallback(() => { + void updateSettings({ + fontSize: Math.min(FONT_SIZE.MAX, settings.fontSize + FONT_SIZE.INCREMENT), + }); + }, [settings.fontSize, updateSettings]); + + const decreaseFontSize = useCallback(() => { + void updateSettings({ + fontSize: Math.max(FONT_SIZE.MIN, settings.fontSize - FONT_SIZE.INCREMENT), + }); + }, [settings.fontSize, updateSettings]); + + const resetFontSize = useCallback(() => { + void updateSettings({ fontSize: FONT_SIZE.DEFAULT }); + }, [updateSettings]); + return { settings, fontSize, diff --git a/apps/renderer/src/utils/helpers/heading-helper.ts b/apps/renderer/src/utils/helpers/heading-helper.ts index 1e351c6..c9dd5de 100644 --- a/apps/renderer/src/utils/helpers/heading-helper.ts +++ b/apps/renderer/src/utils/helpers/heading-helper.ts @@ -14,10 +14,10 @@ export function getHeadingId(text: string): string { // assigns id to the headings export function heading({ text, depth }: HeadingProps) { - const parsedInline = parseInline(text) as string; - const idText = decodeHtml(stripHtml(parsedInline)); - const id = getHeadingId(idText); - return `${parseInline}\n`; + const plainText = stripHtml(text); + const id = getHeadingId(plainText); + const safeText = escapeHtml(plainText); + return `${safeText}\n`; } //removes inline html @@ -28,7 +28,7 @@ export function stripHtml(html: string): string { export function headingText(token: Tokens.Heading): string { const inlineHtml = parseInline(token.text) as string; - return decodeHtml(stripHtml(inlineHtml).trim()); + return stripHtml(inlineHtml).trim(); } export function isHeadingToken(token: unknown): token is Tokens.Heading { diff --git a/docs/src/components/Homepage/hero.tsx b/docs/src/components/Homepage/hero.tsx index bf50cb4..beb1316 100644 --- a/docs/src/components/Homepage/hero.tsx +++ b/docs/src/components/Homepage/hero.tsx @@ -53,7 +53,7 @@ export function HeroSection() { =7.0.3", + "serialize-javascript": ">=7.0.5", "fast-uri": ">=3.1.2", "tmp": ">=0.2.6" } From 60342079b38dfa77c9eacf55fdae2efd977bb991 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 22:37:48 +0530 Subject: [PATCH 07/18] fix(settings): add window api mock and fix settings test --- .../__tests__/hooks/useSettings.test.ts | 33 ++++++++++++------- apps/renderer/setup.ts | 17 ++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/apps/renderer/__tests__/hooks/useSettings.test.ts b/apps/renderer/__tests__/hooks/useSettings.test.ts index 2caea62..4144de6 100644 --- a/apps/renderer/__tests__/hooks/useSettings.test.ts +++ b/apps/renderer/__tests__/hooks/useSettings.test.ts @@ -12,34 +12,45 @@ describe('useSettings font size', () => { expect(result.current.fontSize).toBe(16); }); - it('should increase font size by adding 2px', () => { + it('should increase font size by adding 2px', async () => { const { result } = renderHook(() => useSettings()); - act(() => result.current.increaseFontSize()); + await act(async () => { + result.current.increaseFontSize(); + }); expect(result.current.fontSize).toBe(18); }); - it('should decrease font size by subracting 2px', () => { + it('should decrease font size by subracting 2px', async () => { const { result } = renderHook(() => useSettings()); - act(() => result.current.decreaseFontSize()); + await act(async () => { + result.current.decreaseFontSize(); + }); expect(result.current.fontSize).toBe(14); }); - it('should not exceed 24px', () => { + it('should not exceed 24px', async () => { const { result } = renderHook(() => useSettings()); - act(() => { - for (let i = 0; i < 10; i++) { + + for (let i = 0; i < 10; i++) { + await act(async () => { result.current.increaseFontSize(); - } - }); + }); + } + expect(result.current.fontSize).toBe(24); }); - it('should return recent font size as 16', () => { + it('should return recent font size as 16', async () => { const { result } = renderHook(() => useSettings()); - act(() => { + + await act(async () => { result.current.increaseFontSize(); + }); + + await act(async () => { result.current.resetFontSize(); }); + expect(result.current.fontSize).toBe(16); }); }); diff --git a/apps/renderer/setup.ts b/apps/renderer/setup.ts index 9f3f123..5987813 100644 --- a/apps/renderer/setup.ts +++ b/apps/renderer/setup.ts @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import { vi } from 'vitest'; // mock svg measurments for jsdom environment for mermaid testing if (typeof window !== 'undefined' && window.SVGElement) { @@ -20,3 +21,19 @@ if (typeof window !== 'undefined' && window.SVGElement) { proto.getComputedTextLength = () => 100; } } + +Object.defineProperty(window, 'api', { + value: { + getSettings: vi.fn(async () => ({ + fontSize: 16, + readingWidth: 'default', + customCss: '', + })), + saveSettings: vi.fn(async (partial) => ({ + fontSize: partial.fontSize ?? 16, + readingWidth: 'default', + customCss: '', + })), + }, + writable: true, +}); From f7e84b827a96e460e504c2565a99a9a3e7075094 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 22:48:58 +0530 Subject: [PATCH 08/18] chore: update pnpm lock file --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa7719e..acd5263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - serialize-javascript: '>=7.0.3' + serialize-javascript: '>=7.0.5' fast-uri: '>=3.1.2' tmp: '>=0.2.6' From c53bf0fc9ce9b9e1aa73cd947f0ee66a5fd2c9b2 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 23:10:50 +0530 Subject: [PATCH 09/18] fix: add ipc gaurd,and return promise --- apps/main-processor/src/file.ts | 8 +++++-- apps/renderer/setup.ts | 21 ++++++++++--------- apps/renderer/src/components/UpdateBanner.tsx | 1 + apps/renderer/src/hooks/useSettings.ts | 6 +++--- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/apps/main-processor/src/file.ts b/apps/main-processor/src/file.ts index 4cfed77..6c8da74 100644 --- a/apps/main-processor/src/file.ts +++ b/apps/main-processor/src/file.ts @@ -41,7 +41,9 @@ export async function watchFile( watcher.on('error', (error) => { const watcherError = error instanceof Error ? error : new Error(String(error)); - void unWatchFile(filePath).then(() => onError?.(watcherError)); + void unWatchFile(filePath) + .catch(() => {}) + .finally(() => onError?.(watcherError)); reject(watcherError); }); @@ -63,7 +65,9 @@ export async function watchFile( debounceTimers.set(filePath, timer); }); watcher.on('unlink', () => { - void unWatchFile(filePath).then(() => onDeleted?.()); + void unWatchFile(filePath) + .catch(() => {}) + .finally(() => onDeleted?.()); }); } diff --git a/apps/renderer/setup.ts b/apps/renderer/setup.ts index 5987813..3cb5ddf 100644 --- a/apps/renderer/setup.ts +++ b/apps/renderer/setup.ts @@ -22,18 +22,19 @@ if (typeof window !== 'undefined' && window.SVGElement) { } } +let mockSettings = { + fontSize: 16, + readingWidth: 'default', + customCss: '', +}; + Object.defineProperty(window, 'api', { value: { - getSettings: vi.fn(async () => ({ - fontSize: 16, - readingWidth: 'default', - customCss: '', - })), - saveSettings: vi.fn(async (partial) => ({ - fontSize: partial.fontSize ?? 16, - readingWidth: 'default', - customCss: '', - })), + getSettings: vi.fn(async () => mockSettings), + saveSettings: vi.fn(async (partial) => { + mockSettings = { ...mockSettings, ...partial }; + return mockSettings; + }), }, writable: true, }); diff --git a/apps/renderer/src/components/UpdateBanner.tsx b/apps/renderer/src/components/UpdateBanner.tsx index bd86ab8..010b305 100644 --- a/apps/renderer/src/components/UpdateBanner.tsx +++ b/apps/renderer/src/components/UpdateBanner.tsx @@ -4,6 +4,7 @@ import { Icons } from '../utils/constants/icon-contants'; export function UpdateBanner() { const [updateVersion, setUpdateVersion] = useState(null); useEffect(() => { + if(!window.api?.onUpdateAvailable) return; const removeUpdateAvailable=window.api.onUpdateAvailable((version: string) => { setUpdateVersion(version); }); diff --git a/apps/renderer/src/hooks/useSettings.ts b/apps/renderer/src/hooks/useSettings.ts index 0f37458..86c7b56 100644 --- a/apps/renderer/src/hooks/useSettings.ts +++ b/apps/renderer/src/hooks/useSettings.ts @@ -56,19 +56,19 @@ export function useSettings() { }, []); const increaseFontSize = useCallback(() => { - void updateSettings({ + return updateSettings({ fontSize: Math.min(FONT_SIZE.MAX, settings.fontSize + FONT_SIZE.INCREMENT), }); }, [settings.fontSize, updateSettings]); const decreaseFontSize = useCallback(() => { - void updateSettings({ + return updateSettings({ fontSize: Math.max(FONT_SIZE.MIN, settings.fontSize - FONT_SIZE.INCREMENT), }); }, [settings.fontSize, updateSettings]); const resetFontSize = useCallback(() => { - void updateSettings({ fontSize: FONT_SIZE.DEFAULT }); + return updateSettings({ fontSize: FONT_SIZE.DEFAULT }); }, [updateSettings]); return { From 60b5a47a248ddbb6cac54b02730d77e82396cfb3 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 1 Jun 2026 23:52:31 +0530 Subject: [PATCH 10/18] fix: remove husky run in release --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 952dc5b..90fd5bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,9 @@ on: permissions: contents: write + +env: + HUSKY: 0 jobs: # add change set From 1d6711ab50e0a5c49cb4acd50623b7b3b847dc52 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 2 Jun 2026 00:35:30 +0530 Subject: [PATCH 11/18] fix(shortcuts): fix key bord shortcut --- apps/renderer/src/components/FileBrowser.tsx | 2 -- apps/renderer/src/hooks/useShortcuts.ts | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/renderer/src/components/FileBrowser.tsx b/apps/renderer/src/components/FileBrowser.tsx index cdd83ca..04a0d64 100644 --- a/apps/renderer/src/components/FileBrowser.tsx +++ b/apps/renderer/src/components/FileBrowser.tsx @@ -1,6 +1,5 @@ import { FileBrowserProps } from '../types/component-types'; import { FileTree } from './FileTree'; -import { Icons } from '../utils/constants/icon-contants'; export function FileBrowser({ tree, @@ -16,7 +15,6 @@ export function FileBrowser({ >
-

Explorer

diff --git a/apps/renderer/src/hooks/useShortcuts.ts b/apps/renderer/src/hooks/useShortcuts.ts index 929ed7d..cbb8bc9 100644 --- a/apps/renderer/src/hooks/useShortcuts.ts +++ b/apps/renderer/src/hooks/useShortcuts.ts @@ -73,12 +73,14 @@ export function useShortcuts({ return; } - if (e.key === '[') { + if (mod && e.key === '[') { + e.preventDefault(); onToggleSidebar(); return; } - if (e.key === '\\') { + if (mod && e.key === '\\') { + e.preventDefault(); onToggleFileBrowser(); return; } From 790f60b2b9e0b1213e90fb47a42c857f5a1a41d7 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 2 Jun 2026 00:50:20 +0530 Subject: [PATCH 12/18] fix(security): allow watching explicitly opened markdown file --- apps/main-processor/src/ipc.ts | 12 ++++++++++-- .../src/utils/constants/ipc-validation.ts | 1 + .../src/utils/helper/ipc-path-resolver.ts | 16 +++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/main-processor/src/ipc.ts b/apps/main-processor/src/ipc.ts index 42398c8..76c28ee 100644 --- a/apps/main-processor/src/ipc.ts +++ b/apps/main-processor/src/ipc.ts @@ -1,7 +1,12 @@ import { app, ipcMain, dialog } from 'electron'; import { readFile, unWatchFile, watchFile } from './file'; import { getFolder } from './folder'; -import { validatePath, validateSender, allowedFolderRoots } from './utils/constants/ipc-validation'; +import { + validatePath, + validateSender, + allowedFolderRoots, + allowedMarkdownFiles, +} from './utils/constants/ipc-validation'; import { IPC_CONSTANTS } from '@package/shared-constants'; import { getRecentFiles } from './recent/getRecentFile'; import { addRecentFile } from './recent/addRecentFile'; @@ -26,6 +31,7 @@ export function registerIPCHandlers(): void { throw new Error('Untrusted sender'); } const safeFilePath = await resolveMarkdownFilePath(filePath); + allowedMarkdownFiles.add(safeFilePath); return await readFile(safeFilePath); }); @@ -47,7 +53,9 @@ export function registerIPCHandlers(): void { } const selected = result.filePaths[0]; if (!selected) return null; - return await resolveMarkdownFilePath(selected); + const safeFilePath = await resolveMarkdownFilePath(selected); + allowedMarkdownFiles.add(safeFilePath); + return safeFilePath; }); // watches a file diff --git a/apps/main-processor/src/utils/constants/ipc-validation.ts b/apps/main-processor/src/utils/constants/ipc-validation.ts index f6f62d5..13d2238 100644 --- a/apps/main-processor/src/utils/constants/ipc-validation.ts +++ b/apps/main-processor/src/utils/constants/ipc-validation.ts @@ -4,6 +4,7 @@ import { IpcMainInvokeEvent } from 'electron'; // production and dev urls export const ALLOWED_MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown']); export const allowedFolderRoots = new Set(); +export const allowedMarkdownFiles = new Set(); //validate the sender export function validateSender(event: IpcMainInvokeEvent): boolean { diff --git a/apps/main-processor/src/utils/helper/ipc-path-resolver.ts b/apps/main-processor/src/utils/helper/ipc-path-resolver.ts index 2212f78..916556c 100644 --- a/apps/main-processor/src/utils/helper/ipc-path-resolver.ts +++ b/apps/main-processor/src/utils/helper/ipc-path-resolver.ts @@ -5,6 +5,7 @@ import { isPathInside, ALLOWED_MARKDOWN_EXTENSIONS, allowedFolderRoots, + allowedMarkdownFiles, } from '../constants/ipc-validation'; /* Validates, resolves symlink and ensures a path is a valid Markdown file inside an optional root directory */ @@ -54,16 +55,17 @@ export async function resolveDirectoryPath(folderPath: string): Promise /*Resolves a Markdown file path against a dynamic set of allowed root folders throwing an error if it matches none*/ export async function resolveWatchedMarkdownPath(filePath: string): Promise { - let lastError: unknown; + const safeFilePath = await resolveMarkdownFilePath(filePath); + + if (allowedMarkdownFiles.has(safeFilePath)) { + return safeFilePath; + } for (const allowedRoot of allowedFolderRoots) { try { return await resolveMarkdownFilePath(filePath, allowedRoot); - } catch (error) { - lastError = error; + } catch { + continue; } } - if (allowedFolderRoots.size > 0) { - throw lastError instanceof Error ? lastError : new Error('Path escapes allowed directory'); - } - return await resolveMarkdownFilePath(filePath); + throw new Error('Path escapes allowed directory'); } From 7d8315eabae642b1e8bfa54eed6c67087b159849 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 2 Jun 2026 13:00:15 +0530 Subject: [PATCH 13/18] feat(tab-bar): add plus button to open multiple file from tab bar --- apps/renderer/src/App.tsx | 1 + apps/renderer/src/components/TabBar.tsx | 16 +++++++++++++--- apps/renderer/src/types/component-types.ts | 1 + .../src/utils/constants/icon-contants.tsx | 14 +++++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/renderer/src/App.tsx b/apps/renderer/src/App.tsx index 7d209ca..e97d7ac 100644 --- a/apps/renderer/src/App.tsx +++ b/apps/renderer/src/App.tsx @@ -162,6 +162,7 @@ useShortcuts({ activeTabId={state.activeTabId} onSwitch={(id) => dispatch({ type: 'SWITCH_TAB', payload: { tabId: id } })} onClose={(id) => dispatch({ type: 'CLOSE_TAB', payload: { tabId: id } })} + plusOpen={openFileDialog} /> )} diff --git a/apps/renderer/src/components/TabBar.tsx b/apps/renderer/src/components/TabBar.tsx index 0e2ee95..788c947 100644 --- a/apps/renderer/src/components/TabBar.tsx +++ b/apps/renderer/src/components/TabBar.tsx @@ -1,7 +1,7 @@ import type { TabBarProps } from '../types/component-types'; import { Icons } from '../utils/constants/icon-contants'; -export function TabBar({ tabs, activeTabId, onSwitch, onClose }: TabBarProps) { +export function TabBar({ tabs, activeTabId, onSwitch, onClose ,plusOpen}: TabBarProps) { if (tabs.length === 0) return null; return ( @@ -33,7 +33,7 @@ export function TabBar({ tabs, activeTabId, onSwitch, onClose }: TabBarProps) { ].join(' ')} > {tab.fileName} - + -
+
); })} +
+ +
); } diff --git a/apps/renderer/src/types/component-types.ts b/apps/renderer/src/types/component-types.ts index ff242e4..e09bd34 100644 --- a/apps/renderer/src/types/component-types.ts +++ b/apps/renderer/src/types/component-types.ts @@ -108,6 +108,7 @@ export interface TabBarProps { activeTabId: string | null; onSwitch: (id: string) => void; onClose: (id: string) => void; + plusOpen: () => void; } export interface Tab { diff --git a/apps/renderer/src/utils/constants/icon-contants.tsx b/apps/renderer/src/utils/constants/icon-contants.tsx index 3905986..7d0f3c7 100644 --- a/apps/renderer/src/utils/constants/icon-contants.tsx +++ b/apps/renderer/src/utils/constants/icon-contants.tsx @@ -155,7 +155,19 @@ const ArrowDown = ({ size = 20, ...props }: IconProps) => ( ); +const Plus = ({ size = 20, ...props }: IconProps) => ( + + + + +); export const Icons = { -X,Hamburger,ZoomIn,ZoomOut,Sun,Moon,Folder,ChevronRight,ChevronDown,ArrowUp,ArrowDown +X,Hamburger,ZoomIn,ZoomOut,Sun,Moon,Folder,ChevronRight,ChevronDown,ArrowUp,ArrowDown,Plus }; From 51024b271ccdc443e219c09d1c1dfcefe64f8f39 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 2 Jun 2026 14:45:25 +0530 Subject: [PATCH 14/18] fix(watcher): add timer in watcher ready and error --- apps/main-processor/src/file.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/main-processor/src/file.ts b/apps/main-processor/src/file.ts index 6c8da74..8ba0509 100644 --- a/apps/main-processor/src/file.ts +++ b/apps/main-processor/src/file.ts @@ -36,9 +36,16 @@ export async function watchFile( }); currentWatchers.set(filePath, watcher); await new Promise((resolve, reject) => { - watcher.once('ready', resolve); + const timeout = setTimeout(() => { + resolve(); + }, 1000); + watcher.once('ready', () => { + clearTimeout(timeout); + resolve(); + }); - watcher.on('error', (error) => { + watcher.once('error', (error) => { + clearTimeout(timeout); const watcherError = error instanceof Error ? error : new Error(String(error)); void unWatchFile(filePath) From d9cbfe76d128e58194553684d4320b4b78bf133a Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 2 Jun 2026 15:41:38 +0530 Subject: [PATCH 15/18] feat(renderer): add aria labels to components --- apps/renderer/src/components/DragDrop.tsx | 2 +- apps/renderer/src/components/Error.tsx | 3 ++- apps/renderer/src/components/FileBrowser.tsx | 1 + apps/renderer/src/components/FileTree.tsx | 4 ++++ apps/renderer/src/components/Loading.tsx | 4 ++-- apps/renderer/src/components/ReaderToolbar.tsx | 9 +++++++-- apps/renderer/src/components/SearchBar.tsx | 9 +++++++-- apps/renderer/src/components/SettingsPanel.tsx | 9 +++++---- apps/renderer/src/components/StatusBar.tsx | 10 +++++----- apps/renderer/src/components/TabBar.tsx | 8 ++++++-- apps/renderer/src/components/Toast.tsx | 2 +- apps/renderer/src/components/UpdateBanner.tsx | 3 ++- 12 files changed, 43 insertions(+), 21 deletions(-) diff --git a/apps/renderer/src/components/DragDrop.tsx b/apps/renderer/src/components/DragDrop.tsx index deccd80..09e2338 100644 --- a/apps/renderer/src/components/DragDrop.tsx +++ b/apps/renderer/src/components/DragDrop.tsx @@ -4,7 +4,7 @@ when a file is dragged and dropped on the application */ export function DragDrop() { return ( -
+
Drop Markdown file to open
diff --git a/apps/renderer/src/components/Error.tsx b/apps/renderer/src/components/Error.tsx index 180d108..e050b3f 100644 --- a/apps/renderer/src/components/Error.tsx +++ b/apps/renderer/src/components/Error.tsx @@ -3,9 +3,10 @@ import { ErrorProps } from "../types/component-types" //error message display function export function Error({message,onRetry}:ErrorProps){ return ( -
+

Error: {message}

-
+ {isFolderMode && ( -
+
{!hasFolder && ( -
+
Open a folder to search.
)} @@ -142,6 +146,7 @@ export function SearchBar({ From f915df4c573d9744a784bab4377d7b98c65412ce Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 2 Jun 2026 15:48:37 +0530 Subject: [PATCH 16/18] test(tab-bar): fix tab bar component test --- apps/renderer/__tests__/components/TabBar.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/renderer/__tests__/components/TabBar.test.tsx b/apps/renderer/__tests__/components/TabBar.test.tsx index 2b10148..d93c128 100644 --- a/apps/renderer/__tests__/components/TabBar.test.tsx +++ b/apps/renderer/__tests__/components/TabBar.test.tsx @@ -31,7 +31,7 @@ describe('TabBar', () => { it('marks the active tab', () => { render( {}} onClose={() => {}} />); - expect(screen.getByRole('tab', { name: /README/i })).toHaveAttribute('aria-current', 'true'); + expect(screen.getByRole('tab', { name: /README/i })).toHaveAttribute('aria-selected', 'true'); }); it('switches tab when a tab is clicked', () => { @@ -49,7 +49,7 @@ describe('TabBar', () => { render( {}} onClose={onClose} />); - fireEvent.click(screen.getAllByRole('button', { name: /close tab/i })[0]); + fireEvent.click(screen.getAllByRole('button', { name: /close .* tab/i })[0]); expect(onClose).toHaveBeenCalledWith('tab-1'); }); From 0798859a18026c56b7eb04b64ce461e9739dc22d Mon Sep 17 00:00:00 2001 From: Ashminita Date: Wed, 3 Jun 2026 11:36:59 +0530 Subject: [PATCH 17/18] feat(renderer): add error boundary for app crash fallback --- apps/renderer/src/App.tsx | 7 +++- .../renderer/src/components/ErrorBoundary.tsx | 42 +++++++++++++++++++ apps/renderer/src/main.tsx | 3 ++ apps/renderer/src/types/component-types.ts | 5 +++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 apps/renderer/src/components/ErrorBoundary.tsx diff --git a/apps/renderer/src/App.tsx b/apps/renderer/src/App.tsx index e97d7ac..00cf48f 100644 --- a/apps/renderer/src/App.tsx +++ b/apps/renderer/src/App.tsx @@ -1,4 +1,4 @@ -import React,{ useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { useFile } from './hooks/useFile'; import { Welcome } from './components/Welcome'; import { Reader } from './components/Reader'; @@ -31,6 +31,7 @@ import { useFilePersistence } from './hooks/useFilePersistence'; import { ReaderToolbar } from './components/ReaderToolbar'; import { useFolderSearch } from './hooks/useFolderSearch'; import { SettingsPanel } from './components/SettingsPanel'; +import { ErrorBoundary } from './components/ErrorBoundary'; export default function App() { const { error, isLoading, openFile, toc,recentFiles,loadFile } =useFile(); @@ -212,7 +213,9 @@ useShortcuts({ className="flex-1 overflow-y-auto" onScroll={scroll} > - + + +
)} diff --git a/apps/renderer/src/components/ErrorBoundary.tsx b/apps/renderer/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..bb09d0a --- /dev/null +++ b/apps/renderer/src/components/ErrorBoundary.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { ErrorBoundaryState } from '../types/component-types'; + + +export class ErrorBoundary extends React.Component { + state: ErrorBoundaryState = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: unknown): void { + console.error('Renderer error boundary caught an error:', error); + } + + render(): React.ReactNode { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

+ The renderer hit an unexpected error. You can try rendering the app again. +

+ {import.meta.env.DEV && this.state.error && ( +
+              {this.state.error.message}
+            
+ )} + +
+ ); + } + + return this.props.children; + } +} diff --git a/apps/renderer/src/main.tsx b/apps/renderer/src/main.tsx index cd5612e..990290f 100644 --- a/apps/renderer/src/main.tsx +++ b/apps/renderer/src/main.tsx @@ -7,13 +7,16 @@ import "./index.css"; import { ThemeProvider } from './context/ThemeProvider' import 'katex/dist/katex.min.css'; import { TabProvider } from './context/TabProvider'; +import { ErrorBoundary } from './components/ErrorBoundary'; createRoot(document.getElementById('root')!).render( + + ) diff --git a/apps/renderer/src/types/component-types.ts b/apps/renderer/src/types/component-types.ts index e09bd34..82a1cde 100644 --- a/apps/renderer/src/types/component-types.ts +++ b/apps/renderer/src/types/component-types.ts @@ -183,3 +183,8 @@ export interface SettingsPanelProps { onChange: (settings: Partial) => void; appVersion?: string; } + +export type ErrorBoundaryState = { + hasError: boolean; + error: Error | null; +}; From a70fb174e415135f00d6b4e62f6d05af40f34396 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Wed, 3 Jun 2026 12:07:15 +0530 Subject: [PATCH 18/18] fix(renderer): add missing id and fix broken tailwind class --- apps/renderer/src/components/DragDrop.tsx | 2 +- apps/renderer/src/components/SearchBar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/renderer/src/components/DragDrop.tsx b/apps/renderer/src/components/DragDrop.tsx index 09e2338..4e36766 100644 --- a/apps/renderer/src/components/DragDrop.tsx +++ b/apps/renderer/src/components/DragDrop.tsx @@ -4,7 +4,7 @@ when a file is dragged and dropped on the application */ export function DragDrop() { return ( -
+
Drop Markdown file to open
diff --git a/apps/renderer/src/components/SearchBar.tsx b/apps/renderer/src/components/SearchBar.tsx index 9b498f7..799666e 100644 --- a/apps/renderer/src/components/SearchBar.tsx +++ b/apps/renderer/src/components/SearchBar.tsx @@ -79,7 +79,7 @@ export function SearchBar({ /> {query && !isFolderMode && ( -
+
{matchCount > 0 ? ( Match {currentMatch}