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:
Huang Xin
2026-05-06 01:39:26 +08:00
committed by GitHub
parent 9b0072173c
commit c27245e980
3 changed files with 55 additions and 3 deletions
@@ -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>
+2
View File
@@ -11,6 +11,8 @@ export type NoteTemplateData = {
cfi?: string;
bookHash?: string;
link?: string;
webLink?: string;
appLink?: string;
text: string;
note?: string;
style?: string;