Skip to content

Commit 9c20a19

Browse files
committed
Export bookmarks as markdown file with clickable links
- Add export button in bookmarks section header - Export to markdown format with file:line links - Include bookmark notes and line content preview - Sort bookmarks by line number - Auto-open exported file in new tab - Link format compatible with VSCode/editors
1 parent 1b38dd1 commit 9c20a19

6 files changed

Lines changed: 106 additions & 0 deletions

File tree

src/main/index.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,72 @@ ipcMain.handle('bookmark-update', async (_, bookmark: Bookmark) => {
701701
return { success: false, error: 'Bookmark not found' };
702702
});
703703

704+
// Export bookmarks to file
705+
ipcMain.handle('export-bookmarks', async () => {
706+
if (!currentFilePath || bookmarks.size === 0) {
707+
return { success: false, error: 'No bookmarks to export' };
708+
}
709+
710+
try {
711+
const handler = getFileHandler();
712+
const fileInfo = handler?.getFileInfo();
713+
if (!fileInfo) {
714+
return { success: false, error: 'No file info available' };
715+
}
716+
717+
// Generate export filename
718+
const currentDir = path.dirname(fileInfo.path);
719+
const baseName = path.basename(fileInfo.path, path.extname(fileInfo.path));
720+
const timestamp = new Date().toISOString().substring(0, 10).replace(/-/g, '');
721+
const exportPath = path.join(currentDir, `${baseName}_bookmarks_${timestamp}.md`);
722+
723+
// Build markdown content with clickable links
724+
const lines: string[] = [
725+
`# Bookmarks`,
726+
``,
727+
`**Source:** \`${fileInfo.path}\``,
728+
`**Exported:** ${new Date().toISOString().replace('T', ' ').substring(0, 19)}`,
729+
`**Total Bookmarks:** ${bookmarks.size}`,
730+
``,
731+
`---`,
732+
``,
733+
];
734+
735+
// Sort bookmarks by line number
736+
const sortedBookmarks = Array.from(bookmarks.values())
737+
.sort((a, b) => a.lineNumber - b.lineNumber);
738+
739+
for (const bookmark of sortedBookmarks) {
740+
// Get the line text
741+
const [lineData] = handler?.getLines(bookmark.lineNumber, 1) || [];
742+
const lineText = lineData?.text || '';
743+
const truncatedText = lineText.length > 100 ? lineText.substring(0, 100) + '...' : lineText;
744+
745+
lines.push(`## Line ${bookmark.lineNumber + 1}`);
746+
lines.push(``);
747+
if (bookmark.label) {
748+
lines.push(`**Note:** ${bookmark.label}`);
749+
lines.push(``);
750+
}
751+
// File link in format that some editors/tools can open (VSCode, etc)
752+
lines.push(`**Link:** \`${fileInfo.path}:${bookmark.lineNumber + 1}\``);
753+
lines.push(``);
754+
lines.push(`\`\`\``);
755+
lines.push(truncatedText);
756+
lines.push(`\`\`\``);
757+
lines.push(``);
758+
lines.push(`---`);
759+
lines.push(``);
760+
}
761+
762+
fs.writeFileSync(exportPath, lines.join('\n'), 'utf-8');
763+
764+
return { success: true, filePath: exportPath };
765+
} catch (error) {
766+
return { success: false, error: String(error) };
767+
}
768+
});
769+
704770
// === Highlights ===
705771

706772
ipcMain.handle('highlight-add', async (_, highlight: Highlight) => {

src/preload/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ const api = {
6161
clearBookmarks: (): Promise<{ success: boolean }> =>
6262
ipcRenderer.invoke('bookmark-clear'),
6363

64+
exportBookmarks: (): Promise<{ success: boolean; filePath?: string; error?: string }> =>
65+
ipcRenderer.invoke('export-bookmarks'),
66+
6467
// Highlights
6568
addHighlight: (highlight: any): Promise<{ success: boolean }> =>
6669
ipcRenderer.invoke('highlight-add', highlight),

src/renderer/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
<div class="sidebar-section" id="section-bookmarks">
128128
<div class="section-header" data-section="bookmarks">
129129
<span class="section-title">Bookmarks</span>
130+
<button id="btn-export-bookmarks" class="icon-btn" title="Export Bookmarks">&#128190;</button>
130131
<span class="section-toggle">&#9660;</span>
131132
</div>
132133
<div class="section-content">

src/renderer/renderer.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ const elements = {
255255
patternsList: document.getElementById('patterns-list') as HTMLDivElement,
256256
duplicatesList: document.getElementById('duplicates-list') as HTMLDivElement,
257257
bookmarksList: document.getElementById('bookmarks-list') as HTMLDivElement,
258+
btnExportBookmarks: document.getElementById('btn-export-bookmarks') as HTMLButtonElement,
258259
highlightsList: document.getElementById('highlights-list') as HTMLDivElement,
259260
btnAddHighlight: document.getElementById('btn-add-highlight') as HTMLButtonElement,
260261
statusFile: document.getElementById('status-file') as HTMLSpanElement,
@@ -3452,6 +3453,21 @@ function init(): void {
34523453
}
34533454
});
34543455

3456+
// Export bookmarks
3457+
elements.btnExportBookmarks.addEventListener('click', async () => {
3458+
if (state.bookmarks.length === 0) {
3459+
alert('No bookmarks to export');
3460+
return;
3461+
}
3462+
const result = await window.api.exportBookmarks();
3463+
if (result.success && result.filePath) {
3464+
// Open the exported file in a new tab
3465+
await loadFileAsInactiveTab(result.filePath);
3466+
} else if (result.error) {
3467+
alert(`Export failed: ${result.error}`);
3468+
}
3469+
});
3470+
34553471
// Notes modal
34563472
elements.btnSaveNotes.addEventListener('click', () => hideNotesModal(true));
34573473
elements.btnCancelNotes.addEventListener('click', () => hideNotesModal(false));

src/renderer/styles.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,25 @@ body {
296296
line-height: 1;
297297
}
298298

299+
/* Icon button in section headers */
300+
.icon-btn {
301+
background: none;
302+
border: none;
303+
color: var(--text-secondary);
304+
font-size: 12px;
305+
cursor: pointer;
306+
padding: 2px 4px;
307+
margin-left: auto;
308+
margin-right: 4px;
309+
opacity: 0.7;
310+
transition: opacity 0.2s;
311+
}
312+
313+
.icon-btn:hover {
314+
opacity: 1;
315+
color: var(--accent-color);
316+
}
317+
299318
.section-btn:hover {
300319
color: var(--accent-color);
301320
}

src/renderer/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ interface Api {
154154
updateBookmark: (bookmark: Bookmark) => Promise<{ success: boolean }>;
155155
listBookmarks: () => Promise<{ success: boolean; bookmarks?: Bookmark[] }>;
156156
clearBookmarks: () => Promise<{ success: boolean }>;
157+
exportBookmarks: () => Promise<{ success: boolean; filePath?: string; error?: string }>;
157158

158159
// Highlights
159160
addHighlight: (highlight: HighlightConfig) => Promise<{ success: boolean }>;

0 commit comments

Comments
 (0)