mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
E-book viewer: Highlights: Make URLs in the notes for highlights clickable
This commit is contained in:
parent
833b117be1
commit
76f595274b
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 {{{
|
||||
|
Loading…
x
Reference in New Issue
Block a user