feat(export): add an option to export annotations as plain text, closes #3147 (#3165)

This commit is contained in:
Huang Xin
2026-02-04 14:50:40 +08:00
committed by GitHub
parent 3b350d6945
commit e74615ac1d
30 changed files with 193 additions and 99 deletions
@@ -1060,5 +1060,7 @@
"Library Sync Progress": "تقدم تزامن المكتبة",
"Back to library": "العودة إلى المكتبة",
"Manual": "يدوي",
"Group by...": "تجميع حسب..."
"Group by...": "تجميع حسب...",
"Export as Plain Text": "تصدير كنص بسيط",
"Export as Markdown": "تصدير بصيغة Markdown"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "লাইব্রেরি সিঙ্ক অগ্রগতি",
"Back to library": "লাইব্রেরিতে ফিরে যান",
"Manual": "ম্যানুয়াল",
"Group by...": "গ্রুপ অনুযায়ী..."
"Group by...": "গ্রুপ অনুযায়ী...",
"Export as Plain Text": "সাধারণ টেক্সট হিসেবে এক্সপোর্ট করুন",
"Export as Markdown": "Markdown হিসেবে এক্সপোর্ট করুন"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "དཔེ་མཛོད་མཉམ་འབྱུང་གི་རིམ་པ།",
"Back to library": "དཔེ་མཛོད་ལ་ལོག་པ།",
"Manual": "ལག་བཟོས།",
"Group by...": "དབྱེ་བ་འབྱེད་སྟངས།..."
"Group by...": "དབྱེ་བ་འབྱེད་སྟངས།...",
"Export as Plain Text": "ཡིག་རྐྱང་དཀྱུས་མར་ཕྱིར་འདྲེན།",
"Export as Markdown": "Markdown དུ་ཕྱིར་འདྲེན།"
}
@@ -1013,6 +1013,6 @@
"Back to library": "Zurück zur Bibliothek",
"Manual": "Manuell",
"Group by...": "Gruppieren nach...",
"Group By": "Gruppieren nach",
"Sort": "Sortieren"
"Export as Plain Text": "Als reinen Text exportieren",
"Export as Markdown": "Als Markdown exportieren"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "Πρόοδος συγχρονισμού βιβλιοθήκης",
"Back to library": "Επιστροφή στη βιβλιοθήκη",
"Manual": "Χειροκίνητα",
"Group by...": "Ομαδοποίηση κατά..."
"Group by...": "Ομαδοποίηση κατά...",
"Export as Plain Text": "Αποστολή ως απλό κείμενο",
"Export as Markdown": "Αποστολή ως Markdown"
}
@@ -1024,5 +1024,7 @@
"Library Sync Progress": "Progreso de sincronización de la biblioteca",
"Back to library": "Volver a la biblioteca",
"Manual": "Manual",
"Group by...": "Agrupar por..."
"Group by...": "Agrupar por...",
"Export as Plain Text": "Exportar como texto sin formato",
"Export as Markdown": "Exportar como Markdown"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "پیشرفت همگام‌سازی کتابخانه",
"Back to library": "بازگشت به کتابخانه",
"Manual": "دستی",
"Group by...": "گروه‌بندی بر اساس..."
"Group by...": "گروه‌بندی بر اساس...",
"Export as Plain Text": "خروجی به صورت متن ساده",
"Export as Markdown": "خروجی به صورت Markdown"
}
@@ -1024,5 +1024,7 @@
"Library Sync Progress": "Progression de la synchronisation de la bibliothèque",
"Back to library": "Retour à la bibliothèque",
"Manual": "Manuel",
"Group by...": "Grouper par..."
"Group by...": "Grouper par...",
"Export as Plain Text": "Exporter en texte brut",
"Export as Markdown": "Exporter en Markdown"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "पुस्तकालय सिंक प्रगति",
"Back to library": "लाइब्रेरी पर वापस जाएं",
"Manual": "मैनुअल",
"Group by...": "इसके द्वारा समूहबद्ध करें..."
"Group by...": "इसके द्वारा समूहबद्ध करें...",
"Export as Plain Text": "प्लेन टेक्स्ट के रूप में एक्सपोर्ट करें",
"Export as Markdown": "Markdown के रूप में एक्सपोर्ट करें"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "Kemajuan Sinkronisasi Pustaka",
"Back to library": "Kembali ke perpustakaan",
"Manual": "Manual",
"Group by...": "Kelompokkan menurut..."
"Group by...": "Kelompokkan menurut...",
"Export as Plain Text": "Ekspor sebagai Teks Biasa",
"Export as Markdown": "Ekspor sebagai Markdown"
}
@@ -1024,5 +1024,7 @@
"Library Sync Progress": "Avanzamento sincronizzazione libreria",
"Back to library": "Torna alla libreria",
"Manual": "Manuale",
"Group by...": "Raggruppa per..."
"Group by...": "Raggruppa per...",
"Export as Plain Text": "Esporta come testo normale",
"Export as Markdown": "Esporta come Markdown"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "ライブラリ同期の進捗",
"Back to library": "ライブラリに戻る",
"Manual": "手動",
"Group by...": "グループ化..."
"Group by...": "グループ化...",
"Export as Plain Text": "プレーンテキストとして書き出し",
"Export as Markdown": "Markdownとして書き出し"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "라이브러리 동기화 진행 상황",
"Back to library": "라이브러리로 돌아가기",
"Manual": "수동",
"Group by...": "그룹화 기준..."
"Group by...": "그룹화 기준...",
"Export as Plain Text": "일반 텍스트로 내보내기",
"Export as Markdown": "Markdown으로 내보내기"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "Kemajuan Segerak Perpustakaan",
"Back to library": "Kembali ke perpustakaan",
"Manual": "Manual",
"Group by...": "Kumpulan mengikut..."
"Group by...": "Kumpulan mengikut...",
"Export as Plain Text": "Eksport sebagai Teks Biasa",
"Export as Markdown": "Eksport sebagai Markdown"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "Voortgang bibliotheeksynchronisatie",
"Back to library": "Terug naar bibliotheek",
"Manual": "Handmatig",
"Group by...": "Groeperen op..."
"Group by...": "Groeperen op...",
"Export as Plain Text": "Exporteren als platte tekst",
"Export as Markdown": "Exporteren als Markdown"
}
@@ -1036,5 +1036,7 @@
"Library Sync Progress": "Postęp synchronizacji biblioteki",
"Back to library": "Powrót do biblioteki",
"Manual": "Ręcznie",
"Group by...": "Grupuj według..."
"Group by...": "Grupuj według...",
"Export as Plain Text": "Eksportuj jako zwykły tekst",
"Export as Markdown": "Eksportuj jako Markdown"
}
@@ -1024,5 +1024,7 @@
"Library Sync Progress": "Progresso de sincronização da biblioteca",
"Back to library": "Voltar à biblioteca",
"Manual": "Manual",
"Group by...": "Agrupar por..."
"Group by...": "Agrupar por...",
"Export as Plain Text": "Exportar como texto simples",
"Export as Markdown": "Exportar como Markdown"
}
@@ -1036,5 +1036,7 @@
"Library Sync Progress": "Прогресс синхронизации библиотеки",
"Back to library": "Назад в библиотеку",
"Manual": "Вручную",
"Group by...": "Группировать по..."
"Group by...": "Группировать по...",
"Export as Plain Text": "Экспортировать как обычный текст",
"Export as Markdown": "Экспортировать как Markdown"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "පුස්තකාල සමමුහුර්ත කිරීමේ ප්‍රගතිය",
"Back to library": "පුස්තකාලයට ආපසු",
"Manual": "අත්පොත",
"Group by...": "මගින් සමූහනය කරන්න..."
"Group by...": "මගින් සමූහනය කරන්න...",
"Export as Plain Text": "සාමාන්‍ය පෙළ ලෙස අපනයනය කරන්න",
"Export as Markdown": "Markdown ලෙස අපනයනය කරන්න"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "Synkroniseringsframsteg för bibliotek",
"Back to library": "Tillbaka till biblioteket",
"Manual": "Manuellt",
"Group by...": "Gruppera efter..."
"Group by...": "Gruppera efter...",
"Export as Plain Text": "Exportera som vanlig text",
"Export as Markdown": "Exportera som Markdown"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "நூலக ஒத்திசைவு நிலை",
"Back to library": "நூலகத்திற்குத் திரும்பு",
"Manual": "கையேடு",
"Group by...": "இதன் படி குழுவாக்கு..."
"Group by...": "இதன் படி குழுவாக்கு...",
"Export as Plain Text": "எளிய உரையாக ஏற்றுமதி செய்",
"Export as Markdown": "Markdown ஆக ஏற்றுமதி செய்"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "ความคืบหน้าการซิงค์ห้องสมุด",
"Back to library": "กลับไปที่ห้องสมุด",
"Manual": "ทำเอง",
"Group by...": "จัดกลุ่มตาม..."
"Group by...": "จัดกลุ่มตาม...",
"Export as Plain Text": "ส่งออกเป็นข้อความธรรมดา",
"Export as Markdown": "ส่งออกเป็น Markdown"
}
@@ -1012,5 +1012,7 @@
"Library Sync Progress": "Kütüphane Senkronizasyon İlerlemesi",
"Back to library": "Kütüphaneye dön",
"Manual": "Manuel",
"Group by...": "Şuna göre grupla..."
"Group by...": "Şuna göre grupla...",
"Export as Plain Text": "Düz Metin Olarak Dışa Aktar",
"Export as Markdown": "Markdown Olarak Dışa Aktar"
}
@@ -1036,5 +1036,7 @@
"Library Sync Progress": "Прогрес синхронізації бібліотеки",
"Back to library": "Назад до бібліотеки",
"Manual": "Вручну",
"Group by...": "Групувати за..."
"Group by...": "Групувати за...",
"Export as Plain Text": "Експортувати як звичайний текст",
"Export as Markdown": "Експортувати як Markdown"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "Tiến trình đồng bộ hóa thư viện",
"Back to library": "Quay lại thư viện",
"Manual": "Thủ công",
"Group by...": "Nhóm theo..."
"Group by...": "Nhóm theo...",
"Export as Plain Text": "Xuất dưới dạng văn bản thuần túy",
"Export as Markdown": "Xuất dưới dạng Markdown"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "库同步进度",
"Back to library": "返回书库",
"Manual": "手动",
"Group by...": "分组方式..."
"Group by...": "分组方式...",
"Export as Plain Text": "导出为纯文本",
"Export as Markdown": "导出为 Markdown"
}
@@ -1000,5 +1000,7 @@
"Library Sync Progress": "資料庫同步進度",
"Back to library": "返回書庫",
"Manual": "手動",
"Group by...": "分組方式..."
"Group by...": "分組方式...",
"Export as Plain Text": "匯出為純文字",
"Export as Markdown": "匯出為 Markdown"
}
@@ -94,8 +94,23 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exportConfig, envConfig, bookKey]);
// Helper function to strip markdown formatting
const stripMarkdown = (text: string): string => {
return text
.replace(/^#{1,6}\s+/gm, '') // Remove headers
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
.replace(/\*(.+?)\*/g, '$1') // Remove italic
.replace(/^>\s+/gm, '') // Remove blockquotes
.replace(/^---$/gm, '') // Remove horizontal rules
.replace(/^\s*[-*+]\s+/gm, '') // Remove list markers
.replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links
.trim();
};
// Generate markdown preview based on current format settings
const markdownPreview = useMemo(() => {
let output = '';
if (exportConfig.useCustomTemplate) {
// Prepare data for template rendering
const sortedGroups = Object.values(booknoteGroups).sort((a, b) => a.id - b.id);
@@ -116,75 +131,82 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
})),
};
return renderNoteTemplate(exportConfig.customTemplate, templateData);
}
output = renderNoteTemplate(exportConfig.customTemplate, templateData);
} else {
// Default formatting (non-template mode)
const sortedGroups = Object.values(booknoteGroups).sort((a, b) => a.id - b.id);
// Default formatting (non-template mode)
const sortedGroups = Object.values(booknoteGroups).sort((a, b) => a.id - b.id);
const lines: string[] = [];
const lines: string[] = [];
// Add title
if (exportConfig.includeTitle) {
lines.push(`# ${bookTitle}`);
}
// Add author
if (exportConfig.includeAuthor && bookAuthor) {
lines.push(`**${_('Author')}**: ${bookAuthor}`);
lines.push('');
}
// Add export date
if (exportConfig.includeDate) {
lines.push(`**${_('Exported from Readest')}**: ${new Date().toISOString().slice(0, 10)}`);
lines.push('');
}
if (exportConfig.includeTitle || exportConfig.includeAuthor || exportConfig.includeDate) {
lines.push('---');
lines.push('');
}
lines.push(`## ${_('Highlights & Annotations')}`);
lines.push('');
for (const group of sortedGroups) {
// Add chapter title
if (exportConfig.includeChapterTitles) {
const chapterTitle = group.label || _('Untitled');
lines.push(`### ${chapterTitle}`);
// Add title
if (exportConfig.includeTitle) {
lines.push(`# ${bookTitle}`);
}
for (const note of group.booknotes) {
// Add quote
if (exportConfig.includeQuotes && note.text) {
lines.push(`> ${note.text}`);
}
// Add note
if (exportConfig.includeNotes && note.note) {
lines.push('');
lines.push(`**${_('Note')}**:: ${note.note}`);
}
// Add timestamp
if (exportConfig.includeTimestamp && note.updatedAt) {
const timestamp = new Date(note.updatedAt).toLocaleString();
lines.push('');
lines.push(`*${_('Time:')} ${timestamp}*`);
}
lines.push(exportConfig.noteSeparator);
// Add author
if (exportConfig.includeAuthor && bookAuthor) {
lines.push(`**${_('Author')}**: ${bookAuthor}`);
lines.push('');
}
if (exportConfig.includeChapterSeparator) {
// Add export date
if (exportConfig.includeDate) {
lines.push(`**${_('Exported from Readest')}**: ${new Date().toISOString().slice(0, 10)}`);
lines.push('');
}
if (exportConfig.includeTitle || exportConfig.includeAuthor || exportConfig.includeDate) {
lines.push('---');
lines.push('');
}
lines.push(`## ${_('Highlights & Annotations')}`);
lines.push('');
for (const group of sortedGroups) {
// Add chapter title
if (exportConfig.includeChapterTitles) {
const chapterTitle = group.label || _('Untitled');
lines.push(`### ${chapterTitle}`);
}
for (const note of group.booknotes) {
// Add quote
if (exportConfig.includeQuotes && note.text) {
lines.push(`> ${note.text}`);
}
// Add note
if (exportConfig.includeNotes && note.note) {
lines.push('');
lines.push(`**${_('Note')}**: ${note.note}`);
}
// Add timestamp
if (exportConfig.includeTimestamp && note.updatedAt) {
const timestamp = new Date(note.updatedAt).toLocaleString();
lines.push('');
lines.push(`*${_('Time:')} ${timestamp}*`);
}
lines.push(exportConfig.noteSeparator);
}
if (exportConfig.includeChapterSeparator) {
lines.push('---');
lines.push('');
}
}
output = lines.join('\n');
}
return lines.join('\n');
// Strip markdown if plain text export is enabled
if (exportConfig.exportAsPlainText) {
output = stripMarkdown(output);
}
return output;
}, [exportConfig, booknoteGroups, bookTitle, bookAuthor, _]);
// Convert markdown to HTML for preview
@@ -493,11 +515,12 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
<span className='text-sm'>{_('Show Source')}</span>
</label>
</div>
{showSource ? (
{showSource || exportConfig.exportAsPlainText ? (
<div
className={clsx(
'bg-base-200 max-h-[40vh] overflow-y-auto rounded-lg p-4 font-mono text-xs',
'bg-base-200 max-h-[40vh] overflow-y-auto rounded-lg p-4 text-xs',
'select-text whitespace-pre-wrap break-words',
showSource ? 'font-mono' : 'font-sans',
)}
>
{markdownPreview || _('No content to preview')}
@@ -518,17 +541,34 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
</div>
{/* Footer Actions */}
<div className='mt-4 flex justify-end gap-4'>
<button onClick={onCancel} className='btn btn-ghost btn-sm'>
{_('Cancel')}
</button>
<button
onClick={handleExport}
className='btn btn-primary btn-sm'
disabled={booknotes.length === 0}
>
{_('Export')}
</button>
<div className='mt-4 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center'>
<div className='flex items-center gap-3'>
<label className='flex cursor-pointer items-center gap-2'>
<input
type='checkbox'
checked={exportConfig.exportAsPlainText}
onChange={() => handleToggle('exportAsPlainText')}
className='toggle'
/>
<span className='line-clamp-2 text-xs'>
{exportConfig.exportAsPlainText
? _('Export as Plain Text')
: _('Export as Markdown')}
</span>
</label>
</div>
<div className='flex gap-4 self-end sm:self-auto'>
<button onClick={onCancel} className='btn btn-ghost btn-sm'>
{_('Cancel')}
</button>
<button
onClick={handleExport}
className='btn btn-primary btn-sm'
disabled={booknotes.length === 0}
>
{_('Export')}
</button>
</div>
</div>
</div>
</Dialog>
@@ -297,6 +297,7 @@ export const DEFAULT_NOTE_EXPORT_CONFIG: NoteExportConfig = {
noteSeparator: '\n\n',
useCustomTemplate: false,
customTemplate: '',
exportAsPlainText: false,
};
export const DEFAULT_ANNOTATOR_CONFIG: AnnotatorConfig = {
+1
View File
@@ -246,6 +246,7 @@ export interface NoteExportConfig {
noteSeparator: string;
useCustomTemplate: boolean;
customTemplate: string;
exportAsPlainText: boolean;
}
export interface AnnotatorConfig {