Skip to content

Dev#91

Open
Ashminita28 wants to merge 153 commits into
mainfrom
dev
Open

Dev#91
Ashminita28 wants to merge 153 commits into
mainfrom
dev

Conversation

@Ashminita28
Copy link
Copy Markdown
Collaborator

@Ashminita28 Ashminita28 commented Jun 1, 2026

Description

This PR raised to merge dev into main

Type of Change

  • Bug fix
  • New feature
  • Refactor (no functional change)
  • Documentation update
  • Style / UI change
  • Build / CI change

Checklist

  • My code follows the project's coding guidelines
  • I have tested my changes locally
  • I have used conventional commit messages
  • I have updated documentation if needed
  • My changes do not introduce new warnings or errors

Summary by CodeRabbit

  • New Features

    • Export to HTML, PDF, and DOCX; user save dialog support and in-app export actions
    • Folder-wide search with inline results and folder-search UI
    • Auto-update notifications with download-on-quit option
    • Settings panel (theme, font size, reading width, custom CSS) and drag‑and‑drop file opening
  • Improvements

    • More robust file watching with debounce and lifecycle fixes
    • Accessibility enhancements (ARIA, keyboard/tab roles) and error boundary UI
    • Safer file/folder validations and export sanitization
  • Documentation

    • Added Architecture and IPC API docs

Ashminita28 and others added 30 commits May 13, 2026 12:04
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
.github/workflows/release.yml (1)

101-102: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent pnpm/action-setup version across jobs.

The version_and_tag job uses @v4 (Line 30) while the build job here uses @v3. Align them to @v4 for consistent pnpm resolution behavior, and double-check that a version is resolvable (see the development.yml note about the dropped with.version).

🔧 Proposed fix
       - name: Setup pnpm
-        uses: pnpm/action-setup@v3
+        uses: pnpm/action-setup@v4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 101 - 102, The pnpm setup action
in the build job is using pnpm/action-setup@v3 while the version_and_tag job
uses `@v4`; update the build job's action reference from pnpm/action-setup@v3 to
pnpm/action-setup@v4 (ensure any with.version inputs are compatible or removed
per development.yml notes) so both jobs use the same pnpm/action-setup@v4
behavior; locate the build job's "Setup pnpm" step and make the change.
apps/main-processor/src/index.ts (2)

87-95: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same unhandled-rejection risk in second-instance.

sendFilePathToRenderer(filePath) at Line 92 is also unawaited and uncaught. Add a .catch to log failures consistently with the open-file handler.

🛡️ Proposed fix
       mainWindow.focus();
-      sendFilePathToRenderer(filePath);
+      void sendFilePathToRenderer(filePath).catch((error) => {
+        console.error('Failed to open file:', error);
+      });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/src/index.ts` around lines 87 - 95, The call to
sendFilePathToRenderer(filePath) in the second-instance handler is unawaited and
unhandled; update the branch that runs when mainWindow exists (the block
referencing mainWindow, mainWindow.restore(), mainWindow.focus()) to call
sendFilePathToRenderer(filePath).catch(...) and log any errors with the same
logger used in the open-file handler so failures are consistently reported;
leave the pendingFilePath assignment in the else branch unchanged.

61-68: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle rejection from async sendFilePathToRenderer.

sendFilePathToRenderer is now async and resolveMarkdownFilePath can reject (invalid path, disallowed extension, path escapes root). Here it's invoked fire-and-forget without a .catch, so a rejection becomes an unhandled promise rejection. The open-file handler at Line 76 already guards this; apply the same here.

🛡️ Proposed fix
   mainWindow.webContents.once('did-finish-load', () => {
     const cliFilePath = parseFilePathFromArgv(process.argv);
     const filePathToOpen = pendingFilePath ?? cliFilePath;
     if (filePathToOpen) {
-      sendFilePathToRenderer(filePathToOpen);
+      void sendFilePathToRenderer(filePathToOpen).catch((error) => {
+        console.error('Failed to open file:', error);
+      });
     }
     pendingFilePath = null;
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/src/index.ts` around lines 61 - 68,
sendFilePathToRenderer is async and can reject (via resolveMarkdownFilePath);
update the did-finish-load handler to handle rejections the same way the
"open-file" handler does: make the callback async (or explicitly attach .catch)
and wrap the sendFilePathToRenderer(filePathToOpen) call in try/catch (or
.catch) to log the error and show the same invalid-file handling used by the
open-file handler, then still set pendingFilePath = null. Target the
did-finish-load callback and the sendFilePathToRenderer call for the change.
apps/main-processor/src/ipc.ts (1)

73-79: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent resolver usage for UNWATCH_FILE

  • WATCH_FILE uses resolveWatchedMarkdownPath (restricts to allowedMarkdownFiles / allowedFolderRoots), but UNWATCH_FILE uses resolveMarkdownFilePath without the allowed-root checks, so UNWATCH_FILE accepts paths that WATCH_FILE would reject.
  • If the goal is to only unwatch files that were previously allowed/watched, align UNWATCH_FILE to use resolveWatchedMarkdownPath (or enforce the same allowlist checks).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/src/ipc.ts` around lines 73 - 79, UNWATCH_FILE currently
calls resolveMarkdownFilePath which bypasses the allowlist checks used by
WATCH_FILE; update the handler for IPC_CONSTANTS.UNWATCH_FILE to use the same
restricted resolver (resolveWatchedMarkdownPath) or otherwise perform the same
allowed-root/allowed-file checks before calling unWatchFile so only files that
could have been watched are accepted; keep validateSender and the async flow,
just replace resolveMarkdownFilePath with resolveWatchedMarkdownPath (or add
explicit allowlist validation) to ensure consistent behavior with WATCH_FILE.
apps/renderer/src/context/ThemeProvider.tsx (1)

11-27: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Initial OS theme is not applied on first launch.

getSystemTheme() is only invoked inside the matchMedia change handler, so when there is no saved app-theme the app stays on the default 'github-light' until the user actually toggles their OS theme. For the OS-theme-detection feature, the initial mount should fall back to the system theme when nothing is persisted.

🐛 Proposed fix
   useEffect(()=>{
     const saved=localStorage.getItem('app-theme');
     if(saved && APPTHEMES.includes(saved as Theme)){
         setTheme(saved as Theme);
+    } else {
+        setThemeState(getSystemTheme());
     }
   },[])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/context/ThemeProvider.tsx` around lines 11 - 27, The
initial mount effect that reads localStorage should fall back to the system
theme when nothing is persisted: in the first useEffect (the one reading
localStorage 'app-theme') call getSystemTheme() and
setThemeState(getSystemTheme()) when saved is falsy instead of leaving the
default; ensure you keep the existing APPTHEMES check and only use
setTheme(saved as Theme) when a valid saved theme exists, otherwise call
setThemeState(getSystemTheme()) so the app applies the OS theme immediately on
first launch (update the effect surrounding localStorage.getItem, setTheme, and
setThemeState accordingly).
apps/renderer/src/components/FileTree.tsx (1)

9-17: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

role="group" is placed on the label row, not around the grouped items.

The role="group" and aria-label sit on the folder header div (chevron + name only). The child entries are rendered in the sibling div at Lines 19-29, so the announced group has no member items. Move the grouping role to the container that actually wraps the children so assistive tech associates the label with the folder contents.

♻️ Suggested placement
     <div className="mb-1">
       <div
-        role='group'
-        aria-label={`${node.name} folder`}
         style={{ paddingLeft }}
         className="flex items-center gap-2 py-2 text-[11px] font-semibold uppercase tracking-wider text-text-muted"
       >
         <Icons.ChevronRight size={12} />
         <span className="truncate">{node.name}</span>
       </div>

-      <div className="space-y-0.5">
+      <div role="group" aria-label={`${node.name} folder`} className="space-y-0.5">
         {node.children?.map((child) => (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/components/FileTree.tsx` around lines 9 - 17, The folder
header currently applies role="group" and aria-label to the header div (the
chevron + <span>{node.name}</span>) so the announced group has no member items;
move the role="group" and aria-label={`${node.name} folder`} from that header
div into the parent/container that actually wraps the child entries (the element
rendered by the FileTree component that contains the children entries for node),
ensuring the group role surrounds the child items so assistive tech associates
the label with the folder contents; update the header div to be a plain
button/label element and keep accessibility on the wrapping container
(reference: node, FileTree component, header div with Icons.ChevronRight and
span).
apps/renderer/src/hooks/useShortcuts.ts (1)

28-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Duplicate mod + Shift + F condition makes the focus-mode branch dead code.

Both blocks test mod && e.shiftKey && e.key.toLowerCase() === 'f'. The first (Lines 28–32) returns after calling onOpenFolderSearch, so the second block (Lines 34–38) calling onToggleFocusMode is unreachable. If folder search replaced focus mode on this shortcut, remove the stale block; otherwise pick a distinct key for one of them. Note onToggleFocusMode is then unused here and can be dropped from the destructure and dependency array.

🐛 Remove the unreachable block
       if (mod && e.shiftKey && e.key.toLowerCase() === 'f') {
         e.preventDefault();
         onOpenFolderSearch();
         return;
       }
-
-      if (mod && e.shiftKey && e.key.toLowerCase() === 'f') {
-        e.preventDefault();
-        onToggleFocusMode();
-        return;
-      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/hooks/useShortcuts.ts` around lines 28 - 38, The shortcut
handler in useShortcuts uses the same condition (mod && e.shiftKey &&
e.key.toLowerCase() === 'f') twice so the second branch calling
onToggleFocusMode is dead code; fix by either removing the stale
onToggleFocusMode branch (the second if block) if folder search should own
mod+Shift+F, or change one branch to a distinct key combination (and update its
condition accordingly), and then remove onToggleFocusMode from the hook's
destructure and the dependency array if you delete it; ensure you update any
related tests or callers that rely on onToggleFocusMode.
🧹 Nitpick comments (19)
apps/renderer/src/components/SettingsPanel.tsx (2)

16-43: 🏗️ Heavy lift

Modal has no focus trap; keyboard focus can escape the dialog.

The dialog sets role="dialog"/aria-modal="true" and focuses the section on open, but nothing constrains Tab/Shift+Tab to elements inside the modal. Keyboard users can tab into the obscured background content while the modal is up. Consider trapping focus within the dialog (cycle from last→first focusable and vice-versa) while isOpen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/components/SettingsPanel.tsx` around lines 16 - 43, The
dialog currently focuses dialogRef on open but does not trap Tab/Shift+Tab, so
keyboard focus can escape; add a focus-trap in the SettingsPanel by registering
a keydown handler (inside the existing useEffect that watches isOpen) that
intercepts Tab/Shift+Tab, finds all focusable elements inside the section
referenced by dialogRef, and when Tab would move past the last element move
focus to the first (and when Shift+Tab at the first move to the last); ensure
this handler is added/removed alongside the Escape handler (or merge into the
same onKeyDown) and use dialogRef, isOpen, and onClose to control activation and
cleanup.

86-97: 💤 Low value

Redundant labeling on the Custom CSS textarea.

The textarea is labeled via aria-labelledby="custom-css-title" (the sr-only span on Line 96), while a visible "Custom CSS" <span> on Line 87 isn't programmatically associated. Pointing aria-labelledby at the visible label (give Line 87 an id) is simpler and keeps the visible/accessible names in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/components/SettingsPanel.tsx` around lines 86 - 97, The
Custom CSS textarea in SettingsPanel has an aria-labelledby pointing to the
hidden sr-only span instead of the visible label; give the visible label span
(the one with text "Custom CSS") a unique id and update the textarea's
aria-labelledby to reference that id, and remove the redundant sr-only span so
the programmatic and visible labels stay in sync (update references in the JSX
for the label span and the textarea).
.github/workflows/development.yml (2)

47-48: 💤 Low value

Consider persist-credentials: false on this read-only checkout.

The security job only scans the tree and never pushes, so disabling credential persistence reduces token exposure surface (zizmor artipacked).

♻️ Suggested hardening
       - name: Checkout
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/development.yml around lines 47 - 48, The Checkout step
currently uses actions/checkout@v4 with default credential persistence; for a
read-only security scan, set persist-credentials: false on the "Checkout" step
to avoid storing the GITHUB_TOKEN in the working directory and reduce token
exposure—update the step named "Checkout" to include persist-credentials: false
alongside the uses: actions/checkout@v4 attribute.

23-23: ⚡ Quick win

Confirm pnpm version resolution without with.version in CI

pnpm/action-setup@v4 in .github/workflows/development.yml (setup pnpm steps at lines 23 and 50-51) will resolve the pnpm version from the root package.json packageManager field (pnpm@10.33.0), so removing with.version should not break setup.

Optional hardening: consider adding persist-credentials: false on actions/checkout@v4 steps if your security linter requests it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/development.yml at line 23, CI currently uses
pnpm/action-setup@v4 without an explicit with.version and that's fine because
the action will resolve pnpm from the repository package.json packageManager
field (e.g., pnpm@10.33.0); verify that your root package.json has the correct
packageManager entry and you can safely remove any with.version keys from the
setup steps (`uses: pnpm/action-setup@v4`) or leave them if you prefer pinning.
As an optional hardening step, add persist-credentials: false to the
actions/checkout@v4 steps to satisfy security linters.
docs/src/components/Homepage/hero.tsx (1)

75-77: ⚡ Quick win

Add aria-label to the video element for screen reader accessibility.

The video element should include an aria-label describing its purpose or content, even though it's decorative. This improves accessibility for users with screen readers.

♿ Proposed accessibility improvement
-                     <video autoPlay loop muted playsInline preload="metadata" className="w-full rounded-xl">
+                     <video autoPlay loop muted playsInline preload="metadata" className="w-full rounded-xl" aria-label="Markdown Reader product demonstration video">
                        <source src={videoUrl} type="video/mp4" />
                      </video>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/components/Homepage/hero.tsx` around lines 75 - 77, The video
element in the hero component (the <video> using source src={videoUrl} and
className="w-full rounded-xl") lacks an accessibility label; add an aria-label
attribute to that video element with a brief descriptive string (e.g., "Product
demo video showing feature X" or "Background decorative video") so screen
readers can identify its purpose while keeping autoplay, loop, muted behavior
unchanged.
apps/main-processor/__tests__/folder.test.ts (1)

7-7: 💤 Low value

Avoid an async callback on describe.

describe callbacks are collected synchronously; the returned promise is not awaited, so any async work here (none currently, but it invites it) would not be reliably sequenced before the tests run. Drop async and keep async logic inside the it blocks.

♻️ Proposed change
-describe('folder reader testing', async () => {
+describe('folder reader testing', () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/__tests__/folder.test.ts` at line 7, The describe
callback in the test suite is declared async which is unnecessary and unsafe
because describe is executed synchronously; remove the async keyword from the
describe(...) callback (the describe call that wraps the suite) and ensure any
asynchronous setup is moved into individual it/test hooks or
beforeEach/afterEach as needed (e.g., convert async work into async functions
inside the it blocks or use beforeEach/afterEach async functions) so that
describe remains synchronous and async operations are properly awaited.
apps/main-processor/src/utils/helper/ipc-path-resolver.ts (1)

1-1: 💤 Low value

Use the node: protocol for the path import.

node:fs/promises is already used on Line 2; for consistency and explicit core-module resolution, import from node:path.

-import path from 'path';
+import path from 'node:path';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/src/utils/helper/ipc-path-resolver.ts` at line 1, Update
the core path import to use the node: protocol for consistency with other core
imports; replace the existing import of path (the module referenced by the
top-level import statement) so that the module is imported as node:path (keep
the same imported identifier `path`) to match `node:fs/promises` usage and
ensure explicit core-module resolution.
apps/renderer/src/renderer/toc.ts (1)

5-9: 💤 Low value

Rename htmlmarkdown and update the comment.

The implementation now tokenizes Markdown via lexer(), but the parameter and comment still say HTML, which is misleading after this change.

♻️ Suggested rename
-// extracts table of content from HTML string
-export function extractTOC(html: string): TOCType[] {
+// extracts table of contents from a Markdown string
+export function extractTOC(markdown: string): TOCType[] {
   const items: TOCType[] = [];
   const idCount = new Map<string, number>();
-  const tokens = lexer(html);
+  const tokens = lexer(markdown);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/renderer/toc.ts` around lines 5 - 9, Rename the parameter
and comment to reflect Markdown: change extractTOC(html: string) and its leading
comment to use "markdown" instead of "html" (e.g., extractTOC(markdown: string):
TOCType[] and update the comment to "extracts table of content from Markdown
string"); update all uses inside the function (e.g., the call to lexer(html) →
lexer(markdown)) and any other references to the old variable name so the
function, comment and internal calls consistently refer to "markdown".
apps/renderer/src/utils/helpers/heading-helper.ts (1)

29-32: 💤 Low value

parseInline return type assumes synchronous parsing.

parseInline(token.text) as string is safe only while the default marked instance stays synchronous; if parseInline is ever called with async: true, it returns a Promise<string> and stripHtml would receive "[object Promise]". Consider passing { async: false } explicitly to make the contract intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/utils/helpers/heading-helper.ts` around lines 29 - 32, The
headingText function assumes parseInline is synchronous; update the call in
headingText to explicitly request synchronous parsing by calling
parseInline(token.text, { async: false }) and then pass the resulting string
into stripHtml (e.g., const inlineHtml = parseInline(token.text, { async: false
}) as string; return stripHtml(inlineHtml).trim();). This makes the contract
explicit for parseInline and prevents passing a Promise (which would stringify
to "[object Promise]") into stripHtml; keep the return type of headingText as
string.
apps/renderer/src/hooks/useExport.ts (1)

6-31: ⚡ Quick win

Add error handling around the export calls. window.api.export* can reject (disk full, permission denied, conversion error). Currently failures surface as unhandled rejections with no user feedback. Wrap in try/catch and report via the toast/error channel.

The three handlers are also near-identical; consider extracting a small helper to remove duplication:

♻️ Sketch
const runExport = useCallback(
  async (defaultExt: 'html' | 'pdf' | 'docx',
         fn: (html: string, css: string, out: string) => Promise<unknown>) => {
    if (!activeTab) return;
    const outPath = await window.api.showSaveDialog({ defaultExt });
    if (!outPath) return;
    try {
      await fn(activeTab.html, exportCss, outPath);
    } catch (err) {
      // surface to user via toast/error state
    }
  },
  [activeTab],
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/hooks/useExport.ts` around lines 6 - 31, The three export
handlers (exportHtml, exportPdf, exportDocx) call
window.api.exportHTML/exportPDF/exportDOCX without error handling and duplicate
the same flow; wrap the export call in a try/catch that reports failures to the
user via your toast/error channel (e.g., show a toast with the error message)
and avoid unhandled rejections, and refactor the repeated logic into a single
helper (e.g., runExport) that accepts the defaultExt and the specific export
function, uses activeTab and exportCss, runs showSaveDialog, and executes the
export inside the try/catch so each handler just calls runExport with the
appropriate parameters.
apps/main-processor/src/export/inlineImage.ts (1)

23-25: 💤 Low value

No-op replacement in the catch branch.

output.replace(fullMatch, fullMatch) rewrites the first occurrence with itself, which has no effect. The intent (leave the original src unchanged on failure) is already satisfied by doing nothing.

♻️ Proposed cleanup
-    } catch {
-      output = output.replace(fullMatch, fullMatch);
-    }
+    } catch {
+      // leave the original src unchanged on failure
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/src/export/inlineImage.ts` around lines 23 - 25, In the
catch block remove the no-op replacement (output = output.replace(fullMatch,
fullMatch)); — instead leave the original output unchanged by either making the
catch empty or logging a concise warning; locate the try/catch around the inline
image processing that uses variables fullMatch and output (in inlineImage.ts)
and delete the replace call so failures simply preserve the original src.
apps/main-processor/src/export/exportDocx.ts (1)

19-27: 💤 Low value

Defensive fallback cast may hide unexpected return types.

The else branch casts result to Uint8Array without verification. If html-to-docx returns an unexpected type (e.g., string or object), Buffer.from() may silently produce garbage or throw a confusing error.

Consider adding explicit type checking or logging for unexpected types:

Proposed improvement
   } else if (typeof Blob !== 'undefined' && result instanceof Blob) {
     buffer = Buffer.from(await result.arrayBuffer());
+  } else if (result instanceof Uint8Array) {
+    buffer = Buffer.from(result);
   } else {
-    buffer = Buffer.from(result as unknown as Uint8Array);
+    throw new Error(`Unexpected result type from html-to-docx: ${typeof result}`);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/src/export/exportDocx.ts` around lines 19 - 27, The
fallback branch in exportDocx.ts blindly casts result to Uint8Array which can
hide unexpected return types; update the conversion logic in the block handling
result (the Buffer.isBuffer / ArrayBuffer / Blob branches) to explicitly handle
known types: check for string (use Buffer.from(string,'utf8')), check for
Uint8Array or ArrayBuffer (use Buffer.from), and if none match log/throw a clear
TypeError that includes typeof/result.constructor to fail fast and aid debugging
rather than silently producing garbage.
apps/main-processor/src/file.ts (1)

38-57: 💤 Low value

Timeout resolves silently when watcher fails to become ready.

If chokidar never emits ready (e.g., due to permission issues that don't trigger an error), the 1-second timeout resolves successfully and the code proceeds as if the watcher is operational. Consider logging a warning or tracking this state to aid debugging file-watch issues in production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/src/file.ts` around lines 38 - 57, The timeout handler
currently calls resolve() silently when chokidar never emits 'ready'; change
that handler so it creates a descriptive TimeoutError (e.g., "watcher ready
timed out for {filePath}"), call onError?.(timeoutError) and/or log a warning,
perform the same cleanup via unWatchFile(filePath).catch(() => {}), then resolve
or reject depending on desired behavior (recommend resolve but mark/log the
timeout) so the code no longer proceeds without any warning; update the timeout
callback in the Promise that references watcher, unWatchFile, onError, and
filePath accordingly.
electron-builder.ts (1)

20-28: 💤 Low value

Redundant extraResources entry.

The first entry (from: 'assets', to: 'assets') already bundles unsigned-install-notice.txt into resources, and nsis.license references the source path directly. The second mapping to UNSIGNED_INSTALL_NOTICE.txt is only needed if something consumes that exact top-level resource name at runtime — otherwise it can be dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron-builder.ts` around lines 20 - 28, The extraResources array currently
contains a redundant mapping for 'assets/unsigned-install-notice.txt' ->
'UNSIGNED_INSTALL_NOTICE.txt'; remove that second object from extraResources so
only the directory mapping (from: 'assets', to: 'assets') remains, and ensure
any code that references nsis.license or other runtime consumers points to the
bundled path 'assets/unsigned-install-notice.txt' (or adjust those references to
the top-level name if you intentionally need 'UNSIGNED_INSTALL_NOTICE.txt');
keep the symbol names extraResources and nsis.license in mind when making the
change.
apps/renderer/src/components/ReaderToolbar.tsx (1)

46-50: 💤 Low value

Hard-coded dark-theme list will silently break if new dark themes are added.

The Sun/Moon decision enumerates github-dark, dracula, nord. Any future dark theme defaults to the Moon (light) icon. Consider centralizing a DARK_THEMES set in theme-constants and reusing it here so the toggle stays in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/components/ReaderToolbar.tsx` around lines 46 - 50,
ReaderToolbar currently hard-codes dark-theme names when choosing Icons.Sun vs
Icons.Moon, which will break if themes change; update the component
(ReaderToolbar) to import and use a centralized DARK_THEMES (or similar) set
from theme-constants instead of enumerating 'github-dark', 'dracula', 'nord',
and then use DARK_THEMES.has(theme) (or equivalent) to decide between Icons.Sun
and Icons.Moon so the toggle stays in sync with the canonical theme list.
packages/shared-constants/src/theme-constants.ts (2)

12-19: ⚖️ Poor tradeoff

Font-size/width constants fragmented across modules.

These numeric constants overlap with the FONT_SIZE/WIDTH_MAP values consumed in useSettings.ts (via ../types/component-types) and the literals in settings-default.ts (e.g. fontSize: 16). Three independent definitions of the same domain values can drift. Consider consolidating to one source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-constants/src/theme-constants.ts` around lines 12 - 19,
Consolidate the font-size and width domain values into a single canonical source
by making the constants in theme-constants (MIN_FONT_SIZE, MAX_FONT_SIZE,
DEFAULT_FONT_SIZE, FONT_SIZE_INCREMENT, NARROW_WIDTH, DEFAULT_WIDTH, WIDE_WIDTH)
the single truth; update consumers (the FONT_SIZE and WIDTH_MAP usages
referenced in ../types/component-types, the logic in useSettings.ts, and the
literals in settings-default.ts) to import and use these exported symbols
instead of duplicating literals or separate maps, and remove or replace any
duplicate definitions so all modules reference theme-constants.

1-10: ⚖️ Poor tradeoff

Make THEMES the single source of truth for Theme.

packages/shared-types/src/settings-type.ts hard-codes a Theme string union that duplicates packages/shared-constants/src/theme-constants.ts (THEMES + derived Theme). Derive Theme in settings-type.ts from the exported THEMES value in @package/shared-constants (note: shared-types currently has no imports/dependency on shared-constants, so you’ll need to add the dependency/import first).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-constants/src/theme-constants.ts` around lines 1 - 10, Add a
dependency from packages/shared-types to packages/shared-constants and import
the THEMES constant into packages/shared-types/src/settings-type.ts, then
replace the hard-coded Theme string union with a derived type: export type Theme
= (typeof THEMES)[number]; reference the exported THEMES constant and ensure any
existing Theme type declaration in settings-type.ts is removed/updated to use
that derived type.
apps/renderer/src/hooks/useFolderSearch.ts (1)

19-40: ⚡ Quick win

Consider debouncing folder search to avoid per-keystroke IPC.

searchFolder is wired to onQueryChange in App.tsx, so every keystroke triggers a window.api.searchFolder round-trip. The stale-response guard prevents incorrect results, but on large folders this still issues a burst of redundant traversals. A short debounce (e.g. 150–250ms) would reduce main-process load while keeping responsiveness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/hooks/useFolderSearch.ts` around lines 19 - 40,
searchFolder in useFolderSearch is called on every keystroke (via onQueryChange)
causing excessive IPC; debounce the search to batch rapid input. Wrap the
existing async searchFolder logic in a debounced function (150–250ms) or use a
useDebouncedCallback inside the hook so requests to
window.api.searchFolder(folderPath, query) only fire after the pause, while
preserving the current requestId stale-response guard, setFolderQuery behavior,
and setIsSearchingFolder handling; ensure cancellation/cleanup on unmount by
clearing the debounce timer or cancelling the debounced callback so no
out-of-date calls run.
apps/renderer/src/types/hook-types.ts (1)

76-76: 💤 Low value

Typo in public contract member name: getHiglightedHtml.

The interface member is misspelled (should be getHighlightedHtml). It matches the current useSearch implementation so it's not breaking, but renaming the contract and its usages now avoids cementing the typo into the public type surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/types/hook-types.ts` at line 76, Rename the misspelled
interface member getHiglightedHtml to getHighlightedHtml in the hook-types
interface and update every usage to match (including the useSearch
implementation and any callers/exports that reference getHiglightedHtml) so the
public contract and implementation use the correct spelling; search for the
symbol getHiglightedHtml, rename it to getHighlightedHtml, update type
annotations and implementations (e.g., the function assigned in useSearch) and
run type checks to ensure no remaining references remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7309821c-4f48-4a6a-b4eb-a2de1ff7bc0a

📥 Commits

Reviewing files that changed from the base of the PR and between 4e91144 and 09ce480.

⛔ Files ignored due to path filters (5)
  • apps/renderer/public/icons/app-icon.svg is excluded by !**/*.svg
  • docs/static/img/docusaurus-social-card.jpg is excluded by !**/*.jpg
  • docs/static/img/markdown-social-card.png is excluded by !**/*.png
  • docs/static/video/hero.mp4 is excluded by !**/*.mp4
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (140)
  • .coderabbit.yaml
  • .github/workflows/development.yml
  • .github/workflows/production.yml
  • .github/workflows/release.yml
  • .gitignore
  • .husky/pre-push
  • .prettierignore
  • README.md
  • apps/main-processor/__tests__/cli.test.ts
  • apps/main-processor/__tests__/docx.test.ts
  • apps/main-processor/__tests__/export.test.ts
  • apps/main-processor/__tests__/file.test.ts
  • apps/main-processor/__tests__/folder-search.test.ts
  • apps/main-processor/__tests__/folder.test.ts
  • apps/main-processor/__tests__/ipc.test.ts
  • apps/main-processor/__tests__/menu.test.ts
  • apps/main-processor/__tests__/recent.test.ts
  • apps/main-processor/__tests__/settings.test.ts
  • apps/main-processor/package.json
  • apps/main-processor/setup.ts
  • apps/main-processor/src/cli.ts
  • apps/main-processor/src/export/buildDocument.ts
  • apps/main-processor/src/export/exportDocx.ts
  • apps/main-processor/src/export/exportHtml.ts
  • apps/main-processor/src/export/exportPdf.ts
  • apps/main-processor/src/export/getImage.ts
  • apps/main-processor/src/export/inlineImage.ts
  • apps/main-processor/src/export/sanitizeCss.ts
  • apps/main-processor/src/file.ts
  • apps/main-processor/src/folder-search.ts
  • apps/main-processor/src/folder.ts
  • apps/main-processor/src/index.ts
  • apps/main-processor/src/ipc.ts
  • apps/main-processor/src/menu.ts
  • apps/main-processor/src/recent/getUniqueRecentFile.ts
  • apps/main-processor/src/settings/get-settings.ts
  • apps/main-processor/src/settings/save-settings.ts
  • apps/main-processor/src/types/watch-file-types.ts
  • apps/main-processor/src/updater.ts
  • apps/main-processor/src/utils/constants/export-constants.ts
  • apps/main-processor/src/utils/constants/folder-constants.ts
  • apps/main-processor/src/utils/constants/ipc-validation.ts
  • apps/main-processor/src/utils/constants/path-constants.ts
  • apps/main-processor/src/utils/constants/setting-constants.ts
  • apps/main-processor/src/utils/constants/window-constants.ts
  • apps/main-processor/src/utils/helper/folder-search-helper.ts
  • apps/main-processor/src/utils/helper/html-template.ts
  • apps/main-processor/src/utils/helper/ipc-path-resolver.ts
  • apps/main-processor/src/utils/helper/menu-helper.ts
  • apps/main-processor/src/utils/helper/path-helper.ts
  • apps/main-processor/src/utils/helper/setting-helper.ts
  • apps/main-processor/src/utils/ipc-validation.ts
  • apps/main-processor/tests/ipc.test.ts
  • apps/main-processor/vitest.config.ts
  • apps/preload/src/index.ts
  • apps/preload/src/utils/menu-event-helper.ts
  • apps/renderer/__tests__/components/Sidebar.test.tsx
  • apps/renderer/__tests__/components/StatusBar.test.tsx
  • apps/renderer/__tests__/components/TabBar.test.tsx
  • apps/renderer/__tests__/components/Welcome.test.tsx
  • apps/renderer/__tests__/hooks/useCollapsibleToc.test.ts
  • apps/renderer/__tests__/hooks/useSearch.test.ts
  • apps/renderer/__tests__/hooks/useSettings.test.ts
  • apps/renderer/__tests__/renderer/callout.test.ts
  • apps/renderer/__tests__/renderer/drag-drop.test.ts
  • apps/renderer/__tests__/renderer/katex.test.ts
  • apps/renderer/__tests__/renderer/markdown.test.ts
  • apps/renderer/__tests__/renderer/mermaid.test.ts
  • apps/renderer/__tests__/renderer/sanitize.test.ts
  • apps/renderer/__tests__/renderer/sikhi.test.ts
  • apps/renderer/__tests__/renderer/toc.test.ts
  • apps/renderer/__tests__/store/tabStore.test.ts
  • apps/renderer/index.html
  • apps/renderer/setup.ts
  • apps/renderer/src/App.tsx
  • apps/renderer/src/components/DragDrop.tsx
  • apps/renderer/src/components/Error.tsx
  • apps/renderer/src/components/ErrorBoundary.tsx
  • apps/renderer/src/components/FileBrowser.tsx
  • apps/renderer/src/components/FileTree.tsx
  • apps/renderer/src/components/Loading.tsx
  • apps/renderer/src/components/ReaderToolbar.tsx
  • apps/renderer/src/components/SearchBar.tsx
  • apps/renderer/src/components/SettingsPanel.tsx
  • apps/renderer/src/components/Sidebar.tsx
  • apps/renderer/src/components/StatusBar.tsx
  • apps/renderer/src/components/TabBar.tsx
  • apps/renderer/src/components/Toast.tsx
  • apps/renderer/src/components/UpdateBanner.tsx
  • apps/renderer/src/config/marked.ts
  • apps/renderer/src/context/ThemeProvider.tsx
  • apps/renderer/src/hooks/useCollapsibleToc.ts
  • apps/renderer/src/hooks/useDragDrop.ts
  • apps/renderer/src/hooks/useExport.ts
  • apps/renderer/src/hooks/useFile.ts
  • apps/renderer/src/hooks/useFileActions.ts
  • apps/renderer/src/hooks/useFilePersistence.ts
  • apps/renderer/src/hooks/useFolderSearch.ts
  • apps/renderer/src/hooks/useLayout.ts
  • apps/renderer/src/hooks/useMenuEvents.ts
  • apps/renderer/src/hooks/useOpenFilePath.ts
  • apps/renderer/src/hooks/useSearch.ts
  • apps/renderer/src/hooks/useSettings.ts
  • apps/renderer/src/hooks/useShortcuts.ts
  • apps/renderer/src/hooks/useTabNavigation.ts
  • apps/renderer/src/main.tsx
  • apps/renderer/src/renderer/drag-drop.ts
  • apps/renderer/src/renderer/shiki.ts
  • apps/renderer/src/renderer/toc.ts
  • apps/renderer/src/store/tabStore.ts
  • apps/renderer/src/styles/export.css
  • apps/renderer/src/types/component-types.ts
  • apps/renderer/src/types/hook-types.ts
  • apps/renderer/src/utils/constants/icon-contants.tsx
  • apps/renderer/src/utils/helpers/heading-helper.ts
  • apps/renderer/src/utils/helpers/sidebar-helper.ts
  • apps/renderer/src/utils/helpers/tab-helper.tsx
  • apps/renderer/src/utils/helpers/theme-helper.ts
  • assets/unsigned-install-notice.txt
  • docs/docs/architecture.md
  • docs/docs/ipc-api.md
  • docs/docusaurus.config.ts
  • docs/sidebars.ts
  • docs/src/components/Homepage/hero.tsx
  • docs/versioned_docs/version-1.0.0/architecture.md
  • docs/versioned_docs/version-1.0.0/ipc-api.md
  • docs/versioned_sidebars/version-1.0.0-sidebars.json
  • electron-builder.ts
  • package.json
  • packages/shared-constants/src/index.ts
  • packages/shared-constants/src/ipc-constants.ts
  • packages/shared-constants/src/menu-constants.ts
  • packages/shared-constants/src/path-constants.ts
  • packages/shared-constants/src/theme-constants.ts
  • packages/shared-types/package.json
  • packages/shared-types/src/index.ts
  • packages/shared-types/src/markdown-type.ts
  • packages/shared-types/src/search-type.ts
  • packages/shared-types/src/settings-default.ts
  • packages/shared-types/src/settings-type.ts
💤 Files with no reviewable changes (2)
  • apps/main-processor/tests/ipc.test.ts
  • apps/main-processor/src/utils/ipc-validation.ts

Comment thread .coderabbit.yaml Outdated
Comment on lines +28 to +32
it('it should create a non-empty DOCX file', () => {
const stats = statSync(docxPath);

expect(stats.size).toBeGreaterThan(100);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test depends on file produced by a previous test.

statSync(docxPath) relies on the first test having created the file. If the first test fails or the suite is reordered/filtered (e.g. it.only), this test throws ENOENT instead of giving a clear failure. Consider folding the size check into the creation test, or recreating the file in this test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/main-processor/__tests__/docx.test.ts` around lines 28 - 32, The test
currently calls statSync(docxPath) which depends on a previous test having
created the file and will throw ENOENT if the file is missing; update this test
to be self-contained by either invoking the same creation routine used in the
creation test (call the function that produces the DOCX before asserting) or by
merging the size assertion into the DOCX creation test; reference the docxPath
constant and replace the standalone statSync(docxPath) call with a creation step
followed by statSync(docxPath) and the expect(stats.size).toBeGreaterThan(100)
assertion so the test no longer relies on test order.

Comment thread apps/main-processor/src/export/exportPdf.ts Outdated
Comment thread apps/main-processor/src/ipc.ts
Comment thread apps/main-processor/src/menu.ts
Comment thread apps/renderer/src/hooks/useFilePersistence.ts Outdated
Comment thread apps/renderer/src/renderer/shiki.ts
Comment thread docs/docs/architecture.md Outdated
Comment thread docs/versioned_docs/version-1.0.0/architecture.md Outdated
EXPORT: 'Export',
EXPORT_HTML: 'Export as HTML',
EXPORT_PDF: 'Export as PDF',
EXPORT_DOCX: 'Export as Docx',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent label casing.

EXPORT_DOCX: 'Export as Docx' is inconsistent with the sibling 'Export as PDF' / 'Export as HTML'. Use 'Export as DOCX' for a consistent, user-facing menu label.

✏️ Proposed fix
-  EXPORT_DOCX: 'Export as Docx',
+  EXPORT_DOCX: 'Export as DOCX',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-constants/src/menu-constants.ts` at line 11, The menu label
for EXPORT_DOCX is inconsistent with its siblings; update the value of the
EXPORT_DOCX constant in menu-constants.ts from 'Export as Docx' to 'Export as
DOCX' so the user-facing label uses the same uppercase extension style as
'Export as PDF' and 'Export as HTML'.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/renderer/src/utils/helpers/heading-helper.ts (1)

36-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Decode HTML entities before building TOC text and ids.

parseInline(token.text) escapes entities first, so a heading like ## A & B becomes A &amp; B here. getHeadingId() then slugs that to a-amp-b, while the rendered heading ID is still derived from the raw text and stays a-b, so the sidebar TOC link points at the wrong anchor.

💡 Proposed fix
 export function headingText(token: Tokens.Heading): string {
   const inlineHtml = parseInline(token.text) as string;
-  return stripHtml(inlineHtml).trim();
+  return decodeHtml(stripHtml(inlineHtml)).trim();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/utils/helpers/heading-helper.ts` around lines 36 - 38,
headingText() is using parseInline(token.text) which returns escaped HTML
entities, causing mismatched TOC IDs vs rendered headings; update headingText
(and ensure getHeadingId uses the same source) to decode HTML entities after
parseInline and before stripHtml/slugging so "A &amp; B" becomes "A & B"; use
your project's HTML-decoding utility (or DOMParser/textarea/unescape helper) on
the parsed inline string, then call stripHtml(...).trim() and pass that decoded
string to getHeadingId to keep TOC links consistent with rendered heading IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/renderer/src/utils/helpers/heading-helper.ts`:
- Around line 23-27: The heading renderer currently escapes the stripped text
and loses inline markdown while headingText builds plain text differently,
causing TOC ID drift; update heading({ text, depth }, registry) to render the
inner HTML from the inline tokens via this.parser.parseInline(tokens) (i.e., use
the parser’s inline-rendered HTML for the hN content) and compute the id by
decoding entities from the same plain source passed to getHeadingId so both
paths normalize identically; in practice use the parser to produce safe HTML for
the element body and run decodeHtml(...) (or the same decode path used by
headingText) on the plain-result before calling getHeadingId (keeping
getHeadingId usage intact) to keep TOC slugs and emitted IDs synchronized.

---

Outside diff comments:
In `@apps/renderer/src/utils/helpers/heading-helper.ts`:
- Around line 36-38: headingText() is using parseInline(token.text) which
returns escaped HTML entities, causing mismatched TOC IDs vs rendered headings;
update headingText (and ensure getHeadingId uses the same source) to decode HTML
entities after parseInline and before stripHtml/slugging so "A &amp; B" becomes
"A & B"; use your project's HTML-decoding utility (or
DOMParser/textarea/unescape helper) on the parsed inline string, then call
stripHtml(...).trim() and pass that decoded string to getHeadingId to keep TOC
links consistent with rendered heading IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c16d786-ac72-45e6-a54d-ac359bb6f055

📥 Commits

Reviewing files that changed from the base of the PR and between 09ce480 and e621a5f.

📒 Files selected for processing (20)
  • .coderabbit.yaml
  • apps/main-processor/src/export/exportPdf.ts
  • apps/main-processor/src/index.ts
  • apps/main-processor/src/ipc.ts
  • apps/main-processor/src/menu.ts
  • apps/main-processor/src/register-menu.ts
  • apps/main-processor/src/settings/get-settings.ts
  • apps/main-processor/src/updater.ts
  • apps/main-processor/src/utils/constants/ipc-validation.ts
  • apps/renderer/index.html
  • apps/renderer/src/App.tsx
  • apps/renderer/src/config/marked.ts
  • apps/renderer/src/hooks/useFileActions.ts
  • apps/renderer/src/hooks/useFilePersistence.ts
  • apps/renderer/src/renderer/markdown.ts
  • apps/renderer/src/renderer/shiki.ts
  • apps/renderer/src/renderer/toc.ts
  • apps/renderer/src/utils/helpers/heading-helper.ts
  • docs/docs/architecture.md
  • docs/versioned_docs/version-1.0.0/architecture.md
💤 Files with no reviewable changes (2)
  • docs/docs/architecture.md
  • docs/versioned_docs/version-1.0.0/architecture.md
🚧 Files skipped from review as they are similar to previous changes (12)
  • apps/main-processor/src/updater.ts
  • apps/renderer/src/renderer/shiki.ts
  • apps/main-processor/src/menu.ts
  • apps/main-processor/src/export/exportPdf.ts
  • apps/renderer/index.html
  • apps/renderer/src/hooks/useFileActions.ts
  • apps/renderer/src/hooks/useFilePersistence.ts
  • apps/main-processor/src/settings/get-settings.ts
  • apps/main-processor/src/index.ts
  • apps/renderer/src/App.tsx
  • apps/main-processor/src/ipc.ts
  • .coderabbit.yaml

Comment on lines +23 to +27
export function heading({ text, depth }: HeadingProps, registry: Map<string, number>) {
const plainText = stripHtml(text);
const id = getHeadingId(plainText, registry);
const safeText = escapeHtml(plainText);
return `<h${depth} id="${id}">${safeText}</h${depth}>\n`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Official Marked docs for custom heading renderers: should renderer.headingrender heading content viathis.parser.parseInline(tokens) to preserve inline markdown formatting?

💡 Result:

Yes. In Marked’s official extensibility docs, the renderer.heading override example renders the heading’s inline content by calling this.parser.parseInline(tokens) (where tokens is the heading token’s inline token array), specifically so inline markdown inside the heading (e.g., **bold**, links, code spans) is preserved in the generated HTML. [1]

Source

  • [1] Marked Documentation – “Extending Marked” / Renderer example (heading({ tokens, depth }) { const text = this.parser.parseInline(tokens); ... }) (marked.js.org)

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and show the relevant sections (small excerpts)
FILE="apps/renderer/src/utils/helpers/heading-helper.ts"
if [ -f "$FILE" ]; then
  echo "== $FILE =="
  wc -l "$FILE"
  sed -n '1,120p' "$FILE" | nl -ba | sed -n '1,120p'
else
  echo "Missing $FILE"
fi

# Find where heading-helper's functions are used (especially for TOC)
rg -n "headingHelper|headingText|getHeadingId|createHeadingRegistry|decodeHtml|heading\(" apps/renderer/src -S || true
rg -n "toc|table of contents|TOC" apps/renderer/src -S || true
rg -n "getHeadingId\(" -S . || true

Repository: mindfiredigital/markdown-reader

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/renderer/src/utils/helpers/heading-helper.ts"
if [ -f "$FILE" ]; then
  echo "== $FILE =="
  wc -l "$FILE"
  sed -n '1,120p' "$FILE" | cat -n
else
  echo "Missing $FILE"
fi

echo "== usages in apps/renderer/src =="
rg -n "heading-helper|headingText|getHeadingId|createHeadingRegistry|decodeHtml|renderer\.heading|heading\(" apps/renderer/src -S || true
echo "== TOC related =="
rg -n "(toc|table of contents|TOC)" apps/renderer/src -S || true
echo "== getHeadingId references across repo =="
rg -n "getHeadingId\(" -S . || true

Repository: mindfiredigital/markdown-reader

Length of output: 9849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' apps/renderer/src/config/marked.ts | cat -n
echo "----"
sed -n '1,120p' apps/renderer/src/renderer/toc.ts | cat -n
echo "----"
sed -n '1,120p' apps/renderer/src/renderer/markdown.ts | cat -n

Repository: mindfiredigital/markdown-reader

Length of output: 3821


🏁 Script executed:

sed -n '1,120p' apps/renderer/src/hooks/useFile.ts | cat -n

Repository: mindfiredigital/markdown-reader

Length of output: 2538


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print the HeadingProps type definition
rg -n "export (type|interface) HeadingProps|HeadingProps" apps/renderer/src/types/component-types.ts apps/renderer/src -S || true

# Show around the match
MATCH_FILE="apps/renderer/src/types/component-types.ts"
if [ -f "$MATCH_FILE" ]; then
  # Print entire file if small enough; otherwise print around likely area
  wc -l "$MATCH_FILE"
  sed -n '1,220p' "$MATCH_FILE" | cat -n
fi

Repository: mindfiredigital/markdown-reader

Length of output: 5948


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json =="
ls -la package.json apps/renderer/package.json 2>/dev/null || true
cat package.json 2>/dev/null | head -n 120 || true
cat apps/renderer/package.json 2>/dev/null | head -n 120 || true

echo "== marked version from lockfile (if present) =="
ls -ლა package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null || true
rg -n "\"marked\"\\s*:" package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null || true
rg -n "marked@" pnpm-lock.yaml yarn.lock 2>/dev/null || true

echo "== check for marked package availability =="
node -e "try{const m=require('marked'); console.log('marked loaded', typeof m, Object.keys(m).slice(0,5));}catch(e){console.error('marked require failed:', e.message); process.exit(0)}"

Repository: mindfiredigital/markdown-reader

Length of output: 3763


🌐 Web query:

Marked v18 renderer.heading props shape { tokens, depth } or { text, depth } and recommended usage to preserve inline markdown

💡 Result:

In Marked v18, renderer methods receive the token object itself rather than a list of individual parameters [1][2]. For a heading renderer, the expected prop shape is { tokens, depth, raw } (where tokens is an array of inline tokens) [3][4][1]. Recommended usage to preserve inline markdown involves using the provided tokens and the parser's built-in methods [1][5]. Specifically, you should use this.parser.parseInline(tokens) to convert the heading's inline tokens into HTML, which ensures that markdown formatting (like bold, italics, or links) within the heading text is correctly processed [3][1][5]. Example of correct implementation: renderer: { heading({ tokens, depth }) { // Correctly parse the inline tokens to preserve markdown formatting const text = this.parser.parseInline(tokens); return <h${depth}>${text}</h${depth}>\n; } } Key points for v18 renderers: - Token Object: The renderer receives a single token object of type Tokens.Heading [4][1]. - Parser Access: The renderer context (this) provides access to the parser, specifically this.parser.parseInline for inline elements and this.parser.parse for block elements [3][5]. - Avoiding Deprecation: Ensure you are accepting the token object instead of positional arguments, as older signatures may no longer be supported or are deprecated [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/renderer/src/utils/constants/regex-constants.ts"
if [ -f "$FILE" ]; then
  wc -l "$FILE"
  sed -n '1,200p' "$FILE" | cat -n
else
  echo "Missing $FILE"
fi

Repository: mindfiredigital/markdown-reader

Length of output: 1267


🌐 Web query:

Marked v18 Tokens.Heading properties: what is token.text and what is renderer.heading props.text vs tokens

💡 Result:

In Marked v18, the distinction between token properties and renderer arguments centers on how the library handles raw markdown versus parsed content [1][2]. Token.text The text property on a Heading token (Tokens.Heading) contains the raw markdown text of the heading, excluding the leading hashes (#) and surrounding whitespace that define the heading level [1][3]. It represents the content that needs to be further parsed to handle inline markdown elements like bold, italic, or links [2]. Renderer.heading Props In recent versions of Marked, the renderer methods (including heading) receive a single token object as an argument, rather than a list of individual parameters [2][4]. Heading Renderer Signature: The heading renderer expects a Tokens.Heading object, which contains: - tokens: An array of parsed inline tokens generated from the heading text [1][3]. This should be passed to this.parser.parseInline(tokens) to convert the heading's inner markdown into HTML [2]. - depth: The numeric level of the heading (e.g., 1 for h1, 2 for h2) [1][3]. - raw: The original, full markdown string that produced the token [2]. - text: The raw text content of the heading [1]. Key Difference When customizing the renderer, you should use token.tokens to render the content to HTML, as it allows Marked to correctly process inline elements [2]. Using token.text directly would treat the heading content as plain text, bypassing the parser and failing to render inline markdown (like formatting) correctly [2].

Citations:


Fix heading rendering to preserve inline markdown and keep TOC IDs in sync

  • apps/renderer/src/utils/helpers/heading-helper.ts: heading() renders <h*> from stripHtml(text) + escapeHtml(...), which outputs escaped raw heading content and drops inline markdown formatting (e.g., *emphasis*, code, [link](...)). Update the Marked renderer.heading implementation to generate the heading HTML from the heading’s inline tokens via this.parser.parseInline(tokens) (as Marked’s extension API intends), rather than escaping the raw text.
  • apps/renderer/src/utils/helpers/heading-helper.ts: headingText() runs parseInline(token.text) and strips tags, but it never decodes HTML entities, so getHeadingId() can produce different slugs vs the IDs emitted by heading() (the file’s decodeHtml() is currently unused). Decode entities before slug generation, or normalize both code paths from the same plain-text source so TOC anchors don’t drift.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/renderer/src/utils/helpers/heading-helper.ts` around lines 23 - 27, The
heading renderer currently escapes the stripped text and loses inline markdown
while headingText builds plain text differently, causing TOC ID drift; update
heading({ text, depth }, registry) to render the inner HTML from the inline
tokens via this.parser.parseInline(tokens) (i.e., use the parser’s
inline-rendered HTML for the hN content) and compute the id by decoding entities
from the same plain source passed to getHeadingId so both paths normalize
identically; in practice use the parser to produce safe HTML for the element
body and run decodeHtml(...) (or the same decode path used by headingText) on
the plain-result before calling getHeadingId (keeping getHeadingId usage intact)
to keep TOC slugs and emitted IDs synchronized.

Copy link
Copy Markdown
Collaborator

@mind-murtaza mind-murtaza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks like a solid update with several good improvements around IPC validation, file handling, Shiki initialization, and TOC generation. However, I found a few items that should be addressed before merging.

1. Path Traversal / Arbitrary File Read in Export
File: apps/main-processor/src/export/inlineImage.ts

The export flow reads image paths directly from rendered HTML and inlines them without validating the resolved path. This could allow a crafted markdown document to reference arbitrary local files, which would then be embedded into exported PDF/HTML/DOCX outputs.

Recommendation: Validate image paths before reading them and ensure they resolve within approved directories (e.g., markdown workspace roots). Using realpath() and verifying the resolved path is contained within an allowed root would provide stronger protection.


2. CSS Sanitization Coverage
File: apps/main-processor/src/utils/constants/export-constants.ts

The current CSS sanitization rules cover several legacy injection vectors but do not block external resource loading through @import or remote url(...) references.

Recommendation: Extend the sanitization patterns to block:

  • @import
  • url(http://...)
  • url(https://...)
  • protocol-relative URLs (url(//...))

This will reduce the risk of exported HTML loading remote resources when opened in a browser.


3. Marked Instance Recreation
File: apps/renderer/src/config/marked.ts

The previous implementation cached the Marked instance, whereas the new implementation creates a fresh instance and re-registers extensions on every render. This introduces unnecessary allocations and setup work for large documents.

Recommendation: Consider extracting reusable configuration and caching the shared portions of the setup to avoid repeated initialization overhead.


4. Theme Selection via Array Index
File: apps/renderer/src/config/marked.ts

Using THEMES[1] creates a hidden dependency on array ordering and can silently break if the list changes in the future.

Recommendation: Use an explicit theme constant (e.g. 'github-dark') or introduce a named default theme constant.


5. Unbounded customCss Length
File: apps/main-processor/src/utils/helper/setting-helper.ts

customCss currently accepts strings of any size. Extremely large values could negatively impact export performance and increase processing overhead.

Recommendation: Introduce a reasonable maximum size limit consistent with the validation approach used for other settings.

Minor / Follow-up Items

  • Remove the redundant guard inside the debounce callback in useFilePersistence.ts.
  • Add an isMounted check inside the scroll persistence timeout callback.
  • Consider removing the silent realpath() fallback in folder.ts since the current flow already resolves paths earlier.
  • Tighten validateSender() by restricting accepted file:// origins to known renderer locations.
  • Consider moving the ErrorBoundary higher in the component tree to provide broader crash protection.

Overall, I think the merge is very close. The export-related security concerns should be resolved before merging, while the remaining items can be addressed either in this PR or as follow-up improvements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants