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 codecs
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from functools import partial
|
import re
|
||||||
|
from functools import lru_cache, partial
|
||||||
from qt.core import (
|
from qt.core import (
|
||||||
QAbstractItemView, QApplication, QCheckBox, QComboBox, QCursor, QDateTime,
|
QAbstractItemView, QApplication, QCheckBox, QComboBox, QCursor, QDateTime,
|
||||||
QDialog, QDialogButtonBox, QFont, QFormLayout, QFrame, QHBoxLayout, QIcon,
|
QDialog, QDialogButtonBox, QFont, QFormLayout, QFrame, QHBoxLayout, QIcon,
|
||||||
@ -16,16 +17,17 @@ from qt.core import (
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from calibre import prepare_string_for_xml
|
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.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.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox
|
from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox
|
||||||
from calibre.gui2.widgets2 import Dialog
|
from calibre.gui2.widgets2 import Dialog
|
||||||
|
|
||||||
|
|
||||||
# rendering {{{
|
# rendering {{{
|
||||||
|
|
||||||
def render_highlight_as_text(hl, lines, as_markdown=False, link_prefix=None):
|
def render_highlight_as_text(hl, lines, as_markdown=False, link_prefix=None):
|
||||||
lines.append(hl['highlighted_text'])
|
lines.append(hl['highlighted_text'])
|
||||||
date = QDateTime.fromString(hl['timestamp'], Qt.DateFormat.ISODate).toLocalTime().toString(Qt.DateFormat.SystemLocaleShortDate)
|
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('')
|
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'):
|
def render_notes(notes, tag='p'):
|
||||||
current_lines = []
|
current_lines = []
|
||||||
for line in notes.splitlines():
|
for line in notes.splitlines():
|
||||||
if line:
|
if line:
|
||||||
current_lines.append(line)
|
current_lines.append(''.join(render_note_line(line)))
|
||||||
else:
|
else:
|
||||||
if current_lines:
|
if current_lines:
|
||||||
yield '<{0}>{1}</{0}>'.format(tag, '\n'.join(current_lines))
|
yield '<{0}>{1}</{0}>'.format(tag, '\n'.join(current_lines))
|
||||||
@ -697,7 +745,10 @@ class DetailsPanel(QWidget):
|
|||||||
self.show_result(None)
|
self.show_result(None)
|
||||||
|
|
||||||
def link_clicked(self, qurl):
|
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):
|
def open_result(self):
|
||||||
if self.current_result is not None:
|
if self.current_result is not None:
|
||||||
|
@ -18,7 +18,7 @@ from calibre.constants import (
|
|||||||
builtin_colors_dark, builtin_colors_light, builtin_decorations
|
builtin_colors_dark, builtin_colors_light, builtin_decorations
|
||||||
)
|
)
|
||||||
from calibre.ebooks.epub.cfi.parse import cfi_sort_key
|
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.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.library.annotations import (
|
from calibre.gui2.library.annotations import (
|
||||||
Details, Export as ExportBase, render_highlight_as_text, render_notes
|
Details, Export as ExportBase, render_highlight_as_text, render_notes
|
||||||
@ -397,7 +397,7 @@ class NotesDisplay(Details):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
Details.__init__(self, parent)
|
Details.__init__(self, parent)
|
||||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum)
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum)
|
||||||
self.anchorClicked.connect(self.edit_notes)
|
self.anchorClicked.connect(self.anchor_clicked)
|
||||||
self.current_notes = ''
|
self.current_notes = ''
|
||||||
|
|
||||||
def show_notes(self, text=''):
|
def show_notes(self, text=''):
|
||||||
@ -405,10 +405,17 @@ class NotesDisplay(Details):
|
|||||||
self.setVisible(bool(text))
|
self.setVisible(bool(text))
|
||||||
self.current_notes = text
|
self.current_notes = text
|
||||||
html = '\n'.join(render_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
|
h = self.document().size().height() + 2
|
||||||
self.setMaximumHeight(h)
|
self.setMaximumHeight(h)
|
||||||
|
|
||||||
|
def anchor_clicked(self, qurl):
|
||||||
|
if qurl.scheme() == 'edit':
|
||||||
|
self.edit_notes()
|
||||||
|
else:
|
||||||
|
safe_open_url(qurl)
|
||||||
|
|
||||||
def edit_notes(self):
|
def edit_notes(self):
|
||||||
current_text = self.current_notes
|
current_text = self.current_notes
|
||||||
d = NotesEditDialog(current_text, self)
|
d = NotesEditDialog(current_text, self)
|
||||||
|
@ -14,7 +14,7 @@ from modals import (
|
|||||||
create_custom_dialog, error_dialog, get_text_dialog, question_dialog,
|
create_custom_dialog, error_dialog, get_text_dialog, question_dialog,
|
||||||
warning_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
|
from widgets import create_button
|
||||||
|
|
||||||
ICON_SIZE_VAL = 3
|
ICON_SIZE_VAL = 3
|
||||||
@ -712,23 +712,60 @@ add_extra_css(def():
|
|||||||
return ans
|
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():
|
def add_para():
|
||||||
nonlocal current_block
|
nonlocal current_para
|
||||||
container.appendChild(E.p(current_block))
|
container.appendChild(current_para)
|
||||||
if container.childNodes.length > 1:
|
if container.childNodes.length > 1:
|
||||||
container.lastChild.style.marginTop = '2ex'
|
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():
|
for line in notes.splitlines():
|
||||||
if not line or not line.strip():
|
if not line or not line.strip():
|
||||||
if current_block:
|
if current_para.childNodes.length:
|
||||||
add_para()
|
add_para()
|
||||||
continue
|
continue
|
||||||
current_block += line + '\n'
|
add_line(line)
|
||||||
if current_block:
|
if current_para.childNodes.length:
|
||||||
add_para()
|
add_para()
|
||||||
return container
|
return container
|
||||||
|
|
||||||
@ -847,7 +884,7 @@ def highlight_entry(h, onclick, view, hide_panel):
|
|||||||
E.div(class_='notes')
|
E.div(class_='notes')
|
||||||
)
|
)
|
||||||
if h.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.notes = h.notes or ''
|
||||||
ans.dataset.title = h.highlighted_text or ''
|
ans.dataset.title = h.highlighted_text or ''
|
||||||
return ans
|
return ans
|
||||||
|
@ -366,7 +366,7 @@ class SelectionBar:
|
|||||||
c.style.maxHeight = 'min(20ex, 40vh)'
|
c.style.maxHeight = 'min(20ex, 40vh)'
|
||||||
else:
|
else:
|
||||||
c.style.maxHeight = '20ex'
|
c.style.maxHeight = '20ex'
|
||||||
render_notes(notes, c)
|
render_notes(notes, c, True)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# accessors {{{
|
# accessors {{{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user