Add support for exporting/importing bookmark notes (#532)

This commit is contained in:
Sascha Ißbrücker 2023-09-10 23:37:37 +02:00 committed by GitHub
parent ffcc40b227
commit 28acf3299c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 139 additions and 7 deletions

View file

@ -31,12 +31,15 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
url = bookmark.url
title = html.escape(bookmark.resolved_title or '')
desc = html.escape(bookmark.resolved_description or '')
if bookmark.notes:
desc += f'[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]'
tags = ','.join(bookmark.tag_names)
toread = '1' if bookmark.unread else '0'
private = '0' if bookmark.shared else '1'
added = int(bookmark.date_added.timestamp())
doc.append(f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>')
doc.append(
f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>')
if desc:
doc.append(f'<DD>{desc}')

View file

@ -168,6 +168,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
'shared',
'title',
'description',
'notes',
'owner'])
# Bulk insert new bookmarks into DB
Bookmark.objects.bulk_create(bookmarks_to_create)
@ -214,5 +215,7 @@ def _copy_bookmark_data(netscape_bookmark: NetscapeBookmark, bookmark: Bookmark,
bookmark.title = netscape_bookmark.title
if netscape_bookmark.description:
bookmark.description = netscape_bookmark.description
if netscape_bookmark.notes:
bookmark.notes = netscape_bookmark.notes
if options.map_private_flag and not netscape_bookmark.private:
bookmark.shared = True

View file

@ -8,6 +8,7 @@ class NetscapeBookmark:
href: str
title: str
description: str
notes: str
date_added: str
tag_string: str
to_read: bool
@ -26,6 +27,7 @@ class BookmarkParser(HTMLParser):
self.tags = ''
self.title = ''
self.description = ''
self.notes = ''
self.toread = ''
self.private = ''
@ -58,6 +60,7 @@ class BookmarkParser(HTMLParser):
href=self.href,
title='',
description='',
notes='',
date_added=self.add_date,
tag_string=self.tags,
to_read=self.toread == '1',
@ -69,12 +72,16 @@ class BookmarkParser(HTMLParser):
self.title = data.strip()
def handle_dd_data(self, data):
self.description = data.strip()
desc = data.strip()
if '[linkding-notes]' in desc:
self.notes = desc.split('[linkding-notes]')[1].split('[/linkding-notes]')[0]
self.description = desc.split('[linkding-notes]')[0]
def add_bookmark(self):
if self.bookmark:
self.bookmark.title = self.title
self.bookmark.description = self.description
self.bookmark.notes = self.notes
self.bookmarks.append(self.bookmark)
self.bookmark = None
self.href = ''
@ -82,6 +89,7 @@ class BookmarkParser(HTMLParser):
self.tags = ''
self.title = ''
self.description = ''
self.notes = ''
self.toread = ''
self.private = ''

View file

@ -18,7 +18,10 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
self.setup_tag(name='tag3')]),
self.setup_bookmark(url='https://example.com/3', title='Title 3', added=added, unread=True),
self.setup_bookmark(url='https://example.com/4', title='Title 4', added=added, shared=True),
self.setup_bookmark(url='https://example.com/5', title='Title 5', added=added, shared=True,
description='Example description', notes='Example notes'),
self.setup_bookmark(url='https://example.com/6', title='Title 6', added=added, shared=True,
notes='Example notes'),
]
html = exporter.export_netscape_html(bookmarks)
@ -28,13 +31,18 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
f'<DT><A HREF="https://example.com/2" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag1,tag2,tag3">Title 2</A>',
f'<DT><A HREF="https://example.com/3" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="1" TAGS="">Title 3</A>',
f'<DT><A HREF="https://example.com/4" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 4</A>',
f'<DT><A HREF="https://example.com/5" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 5</A>',
'<DD>Example description[linkding-notes]Example notes[/linkding-notes]',
f'<DT><A HREF="https://example.com/6" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>',
'<DD>[linkding-notes]Example notes[/linkding-notes]',
]
self.assertIn('\n\r'.join(lines), html)
def test_escape_html_in_title_and_description(self):
def test_escape_html(self):
bookmark = self.setup_bookmark(
title='<style>: The Style Information element',
description='The <style> HTML element contains style information for a document, or part of a document.'
description='The <style> HTML element contains style information for a document, or part of a document.',
notes='Interesting notes about the <style> HTML element.',
)
html = exporter.export_netscape_html([bookmark])
@ -43,6 +51,10 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
'The &lt;style&gt; HTML element contains style information for a document, or part of a document.',
html
)
self.assertIn(
'Interesting notes about the &lt;style&gt; HTML element.',
html
)
def test_handle_empty_values(self):
bookmark = self.setup_bookmark()

View file

@ -67,7 +67,8 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
add_date='3', tags='bar-tag, other-tag'),
BookmarkHtmlTag(href='https://example.com/unread', title='Unread title', description='Unread description',
add_date='3', to_read=True),
BookmarkHtmlTag(href='https://example.com/private', title='Private title', description='Private description',
BookmarkHtmlTag(href='https://example.com/private', title='Private title',
description='Private description',
add_date='4', private=True),
]
import_html = self.render_html(tags=html_tags)
@ -90,7 +91,8 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
add_date='333', tags='updated-bar-tag, updated-other-tag'),
BookmarkHtmlTag(href='https://example.com/unread', title='Unread title', description='Unread description',
add_date='3', to_read=False),
BookmarkHtmlTag(href='https://example.com/private', title='Private title', description='Private description',
BookmarkHtmlTag(href='https://example.com/private', title='Private title',
description='Private description',
add_date='4', private=False),
BookmarkHtmlTag(href='https://baz.com', add_date='444', tags='baz-tag')
]
@ -293,6 +295,40 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
self.assertEqual(bookmark2.shared, False)
self.assertEqual(bookmark3.shared, True)
def test_notes(self):
# initial notes
test_html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Example notes[/linkding-notes]
''')
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].description, 'Example description')
self.assertEqual(Bookmark.objects.all()[0].notes, 'Example notes')
# update notes
test_html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Updated notes[/linkding-notes]
''')
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].description, 'Example description')
self.assertEqual(Bookmark.objects.all()[0].notes, 'Updated notes')
# does not override existing notes if empty
test_html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description
''')
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].description, 'Example description')
self.assertEqual(Bookmark.objects.all()[0].notes, 'Updated notes')
def test_schedule_snapshot_creation(self):
user = self.get_or_create_test_user()
test_html = self.render_html(tags_html='')

View file

@ -149,3 +149,73 @@ class ParserTestCase(TestCase, ImportTestMixin):
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].private, False)
def test_notes(self):
# no description, no notes
html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, '')
self.assertEqual(bookmarks[0].notes, '')
# description, no notes
html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, 'Example description')
self.assertEqual(bookmarks[0].notes, '')
# description, notes
html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Example notes[/linkding-notes]
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, 'Example description')
self.assertEqual(bookmarks[0].notes, 'Example notes')
# description, notes without closing tag
html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Example notes
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, 'Example description')
self.assertEqual(bookmarks[0].notes, 'Example notes')
# no description, notes
html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>[linkding-notes]Example notes[/linkding-notes]
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, '')
self.assertEqual(bookmarks[0].notes, 'Example notes')
# notes reset between bookmarks
html = self.render_html(tags_html='''
<DT><A HREF="https://example.com/1" ADD_DATE="1">Example title</A>
<DD>[linkding-notes]Example notes[/linkding-notes]
<DT><A HREF="https://example.com/2" ADD_DATE="1">Example title</A>
<DD>Example description
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, '')
self.assertEqual(bookmarks[0].notes, 'Example notes')
self.assertEqual(bookmarks[1].description, 'Example description')
self.assertEqual(bookmarks[1].notes, '')
def test_unescape_content(self):
html = self.render_html(tags_html='''
<DT><A HREF="https://example.com" ADD_DATE="1">&lt;style&gt;: The Style Information element</A>
<DD>The &lt;style&gt; HTML element contains style information for a document, or part of a document.[linkding-notes]Interesting notes about the &lt;style&gt; HTML element.[/linkding-notes]
''')
bookmarks = parse(html)
self.assertEqual(bookmarks[0].title,
'<style>: The Style Information element')
self.assertEqual(bookmarks[0].description,
'The <style> HTML element contains style information for a document, or part of a document.')
self.assertEqual(bookmarks[0].notes, 'Interesting notes about the <style> HTML element.')