E-book viewer: Highlights: Make URLs in the notes for highlights clickable

This commit is contained in:
Kovid Goyal 2021-06-02 20:40:10 +05:30
parent 833b117be1
commit 76f595274b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 115 additions and 20 deletions

View File

@ -5,7 +5,8 @@
import codecs
import json
import os
from functools import partial
import re
from functools import lru_cache, partial
from qt.core import (
QAbstractItemView, QApplication, QCheckBox, QComboBox, QCursor, QDateTime,
QDialog, QDialogButtonBox, QFont, QFormLayout, QFrame, QHBoxLayout, QIcon,
@ -16,16 +17,17 @@ from qt.core import (
from urllib.parse import quote
from calibre import prepare_string_for_xml
from calibre.ebooks.metadata import authors_to_string, fmt_sidx
from calibre.db.backend import FTSQueryError
from calibre.gui2 import Application, choose_save_file, config, error_dialog, gprefs
from calibre.ebooks.metadata import authors_to_string, fmt_sidx
from calibre.gui2 import (
Application, choose_save_file, config, error_dialog, gprefs, safe_open_url
)
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox
from calibre.gui2.widgets2 import Dialog
# rendering {{{
def render_highlight_as_text(hl, lines, as_markdown=False, link_prefix=None):
lines.append(hl['highlighted_text'])
date = QDateTime.fromString(hl['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate)
@ -62,11 +64,57 @@ def render_bookmark_as_text(b, lines, as_markdown=False, link_prefix=None):
lines.append('')
url_prefixes = 'http', 'https'
url_delimiters = (
'\x00-\x09\x0b-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f'
'\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U00013430-\U00013438'
'\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd'
)
url_pattern = r'\b(?:{})://[^{}]{{3,}}'.format('|'.join(url_prefixes), url_delimiters)
@lru_cache(maxsize=2)
def url_pat():
return re.compile(url_pattern, flags=re.I)
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
def url(text: str, s: int, e: int):
while text[e - 1] in '.,?!' and e > 1: # remove trailing punctuation
e -= 1
# truncate url at closing bracket/quote
if s > 0 and e <= len(text) and text[s-1] in closing_bracket_map:
q = closing_bracket_map[text[s-1]]
idx = text.find(q, s)
if idx > s:
e = idx
return s, e
def render_note_line(line):
urls = []
for m in url_pat().finditer(line):
s, e = url(line, m.start(), m.end())
urls.append((s, e))
if not urls:
yield prepare_string_for_xml(line)
return
pos = 0
for (s, e) in urls:
if s > pos:
yield prepare_string_for_xml(line[pos:s])
yield '<a href="{0}">{0}</a>'.format(prepare_string_for_xml(line[s:e], True))
if urls[-1][1] < len(line):
yield prepare_string_for_xml(line[urls[-1][1]:])
def render_notes(notes, tag='p'):
current_lines = []
for line in notes.splitlines():
if line:
current_lines.append(line)
current_lines.append(''.join(render_note_line(line)))
else:
if current_lines:
yield '<{0}>{1}</{0}>'.format(tag, '\n'.join(current_lines))
@ -697,7 +745,10 @@ class DetailsPanel(QWidget):
self.show_result(None)
def link_clicked(self, qurl):
getattr(self, qurl.host())()
if qurl.scheme() == 'calibre':
getattr(self, qurl.host())()
else:
safe_open_url(qurl)
def open_result(self):
if self.current_result is not None:

View File

@ -18,7 +18,7 @@ from calibre.constants import (
builtin_colors_dark, builtin_colors_light, builtin_decorations
)
from calibre.ebooks.epub.cfi.parse import cfi_sort_key
from calibre.gui2 import error_dialog, is_dark_theme
from calibre.gui2 import error_dialog, is_dark_theme, safe_open_url
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.library.annotations import (
Details, Export as ExportBase, render_highlight_as_text, render_notes
@ -397,7 +397,7 @@ class NotesDisplay(Details):
def __init__(self, parent=None):
Details.__init__(self, parent)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum)
self.anchorClicked.connect(self.edit_notes)
self.anchorClicked.connect(self.anchor_clicked)
self.current_notes = ''
def show_notes(self, text=''):
@ -405,10 +405,17 @@ class NotesDisplay(Details):
self.setVisible(bool(text))
self.current_notes = text
html = '\n'.join(render_notes(text))
self.setHtml('<div><a href="edit://moo" style="text-decoration: none">{}</a></div>{}'.format(_('Edit notes'), html))
self.setHtml('<div><a href="edit://moo">{}</a></div>{}'.format(_('Edit notes'), html))
self.document().setDefaultStyleSheet('a[href] { text-decoration: none }')
h = self.document().size().height() + 2
self.setMaximumHeight(h)
def anchor_clicked(self, qurl):
if qurl.scheme() == 'edit':
self.edit_notes()
else:
safe_open_url(qurl)
def edit_notes(self):
current_text = self.current_notes
d = NotesEditDialog(current_text, self)

View File

@ -14,7 +14,7 @@ from modals import (
create_custom_dialog, error_dialog, get_text_dialog, question_dialog,
warning_dialog
)
from read_book.globals import current_book, is_dark_theme, runtime
from read_book.globals import current_book, is_dark_theme, runtime, ui_operations
from widgets import create_button
ICON_SIZE_VAL = 3
@ -712,23 +712,60 @@ add_extra_css(def():
return ans
)
url_pat = /\bhttps?:\/\/\S{3,}/ig
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
opening_brackets = Object.keys(closing_bracket_map).join('')
def url(text: str, s: int, e: int):
while '.,?!'.indexOf(text[e-1]) > -1 and e > 1: # remove trailing punctuation
e -= 1
# truncate url at closing bracket/quote
if s > 0 and e <= text.length and closing_bracket_map[text[s-1]]:
q = closing_bracket_map[text[s-1]]
idx = text.indexOf(q, s)
if idx > s:
e = idx
return s, e
def render_notes(notes, container, make_urls_clickable):
current_para = E.p()
def render_notes(notes, container):
current_block = ''
def add_para():
nonlocal current_block
container.appendChild(E.p(current_block))
nonlocal current_para
container.appendChild(current_para)
if container.childNodes.length > 1:
container.lastChild.style.marginTop = '2ex'
current_block = ''
current_para = E.p()
def add_line(line):
url_pat.lastIndex = 0
urls = v'[]'
while make_urls_clickable:
m = url_pat.exec(line)
if not m:
break
urls.push(url(line, m.index, url_pat.lastIndex))
if not urls.length:
current_para.appendChild(document.createTextNode(line))
return
pos = 0
for (s, e) in urls:
if s > pos:
current_para.appendChild(document.createTextNode(line[pos:s]))
current_para.appendChild(E.a(line[s:e], href='javascript: void(0)', class_='blue-link', onclick=ui_operations.open_url.bind(None, line[s:e])))
if urls[-1][1] < line.length:
current_para.appendChild(document.createTextNode(line[urls[-1][1]:]))
for line in notes.splitlines():
if not line or not line.strip():
if current_block:
if current_para.childNodes.length:
add_para()
continue
current_block += line + '\n'
if current_block:
add_line(line)
if current_para.childNodes.length:
add_para()
return container
@ -847,7 +884,7 @@ def highlight_entry(h, onclick, view, hide_panel):
E.div(class_='notes')
)
if h.notes:
render_notes(h.notes, ans.querySelector('.notes'))
render_notes(h.notes, ans.querySelector('.notes'), True)
ans.dataset.notes = h.notes or ''
ans.dataset.title = h.highlighted_text or ''
return ans

View File

@ -366,7 +366,7 @@ class SelectionBar:
c.style.maxHeight = 'min(20ex, 40vh)'
else:
c.style.maxHeight = '20ex'
render_notes(notes, c)
render_notes(notes, c, True)
# }}}
# accessors {{{