feat(reader): support deeplink and web link in annotation export (#4067)
Expose `annotation.appLink` (readest://) and `annotation.webLink` (https://web.readest.com) as template variables for custom export templates. The shipped default template now emits the readest:// app deeplink for the page link so exported notes open the native app. The non-template export mode keeps the universal https link. Preview links also gain target="_blank" so they open in a new tab instead of replacing the dialog. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -518,6 +518,45 @@ describe('renderNoteTemplate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Annotation link variants', () => {
|
||||
const linkData: NoteTemplateData = {
|
||||
title: 'Book',
|
||||
author: 'Author',
|
||||
exportDate: '2024-01-15',
|
||||
chapters: [
|
||||
{
|
||||
title: 'Ch1',
|
||||
annotations: [
|
||||
{
|
||||
text: 'quote',
|
||||
webLink: 'https://web.readest.com/o/book/abc/annotation/n1',
|
||||
appLink: 'readest://book/abc/annotation/n1',
|
||||
link: 'https://web.readest.com/o/book/abc/annotation/n1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('should render annotation.webLink', () => {
|
||||
const template = '{{ chapters[0].annotations[0].webLink }}';
|
||||
const result = renderNoteTemplate(template, linkData);
|
||||
expect(result).toBe('https://web.readest.com/o/book/abc/annotation/n1');
|
||||
});
|
||||
|
||||
it('should render annotation.appLink with readest:// scheme', () => {
|
||||
const template = '{{ chapters[0].annotations[0].appLink }}';
|
||||
const result = renderNoteTemplate(template, linkData);
|
||||
expect(result).toBe('readest://book/abc/annotation/n1');
|
||||
});
|
||||
|
||||
it('should still render legacy annotation.link', () => {
|
||||
const template = '{{ chapters[0].annotations[0].link }}';
|
||||
const result = renderNoteTemplate(template, linkData);
|
||||
expect(result).toBe('https://web.readest.com/o/book/abc/annotation/n1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Whitespace handling', () => {
|
||||
it('should trim blocks correctly', () => {
|
||||
const template = `Start
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BookNote, BooknoteGroup, NoteExportConfig } from '@/types/book';
|
||||
import { DEFAULT_NOTE_EXPORT_CONFIG } from '@/services/constants';
|
||||
import { saveViewSettings } from '@/helpers/settings';
|
||||
import { renderNoteTemplate, formatBlockQuote } from '@/utils/note';
|
||||
import { buildAnnotationWebUrl } from '@/utils/deeplink';
|
||||
import { buildAnnotationAppUrl, buildAnnotationWebUrl } from '@/utils/deeplink';
|
||||
import Dialog from '@/components/Dialog';
|
||||
|
||||
interface ExportMarkdownDialogProps {
|
||||
@@ -67,7 +67,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
{% if annotation.note %}
|
||||
**${_('Note:')}** {{ annotation.note }}
|
||||
{% endif %}
|
||||
*{% if annotation.link %}[${_('Page:')} {{ annotation.page }}]({{ annotation.link }}){% else %}${_('Page:')} {{ annotation.page }}{% endif %} · ${_('Time:')} {{ annotation.timestamp | date('%Y-%m-%d %H:%M') }}*
|
||||
*{% if annotation.appLink %}[${_('Page:')} {{ annotation.page }}]({{ annotation.appLink }}){% else %}${_('Page:')} {{ annotation.page }}{% endif %} · ${_('Time:')} {{ annotation.timestamp | date('%Y-%m-%d %H:%M') }}*
|
||||
{% endfor %}
|
||||
|
||||
---
|
||||
@@ -130,6 +130,8 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
cfi: note.cfi,
|
||||
bookHash,
|
||||
link: buildAnnotationWebUrl({ bookHash, noteId: note.id, cfi: note.cfi }),
|
||||
webLink: buildAnnotationWebUrl({ bookHash, noteId: note.id, cfi: note.cfi }),
|
||||
appLink: buildAnnotationAppUrl({ bookHash, noteId: note.id, cfi: note.cfi }),
|
||||
text: note.text || '',
|
||||
note: note.note || '',
|
||||
style: note.style,
|
||||
@@ -239,7 +241,8 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
// Convert markdown to HTML for preview
|
||||
const htmlPreview = useMemo(() => {
|
||||
if (!markdownPreview) return '';
|
||||
return marked.parse(markdownPreview);
|
||||
const html = marked.parse(markdownPreview) as string;
|
||||
return html.replace(/<a href=/g, '<a target="_blank" rel="noopener noreferrer" href=');
|
||||
}, [markdownPreview]);
|
||||
|
||||
const handleToggle = (field: keyof NoteExportConfig) => {
|
||||
@@ -507,6 +510,14 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
|
||||
<code className='bg-base-300 rounded px-1'>annotation.timestamp</code> -{' '}
|
||||
{_('Annotation time')}
|
||||
</li>
|
||||
<li className='ml-8'>
|
||||
<code className='bg-base-300 rounded px-1'>annotation.appLink</code> -{' '}
|
||||
{_('App deeplink (readest://)')}
|
||||
</li>
|
||||
<li className='ml-8'>
|
||||
<code className='bg-base-300 rounded px-1'>annotation.webLink</code> -{' '}
|
||||
{_('Universal web link (https://)')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -11,6 +11,8 @@ export type NoteTemplateData = {
|
||||
cfi?: string;
|
||||
bookHash?: string;
|
||||
link?: string;
|
||||
webLink?: string;
|
||||
appLink?: string;
|
||||
text: string;
|
||||
note?: string;
|
||||
style?: string;
|
||||
|
||||
Reference in New Issue
Block a user