From e416b1c30b261ed967f7e80c19e6f43d1456c912 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Oct 2021 16:21:24 +0530 Subject: [PATCH] Edit book: Set semantics tool: Add support for EPUB 3 landmarks --- src/calibre/gui2/tweak_book/boss.py | 2 +- src/calibre/gui2/tweak_book/widgets.py | 110 +++++++++++++++++-------- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 8992901a44..1a3084ca81 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -994,7 +994,7 @@ class Boss(QObject): return error_dialog(self.gui, _('Not supported'), _( 'Semantics are not supported for the AZW3 format.'), show=True) d = InsertSemantics(c, parent=self.gui) - if d.exec_() == QDialog.DialogCode.Accepted and d.changed_type_map: + if d.exec_() == QDialog.DialogCode.Accepted and d.changes: self.add_savepoint(_('Before: Set Semantics')) d.apply_changes(current_container()) self.apply_container_update_to_gui() diff --git a/src/calibre/gui2/tweak_book/widgets.py b/src/calibre/gui2/tweak_book/widgets.py index 28a578bcd6..7d13120f18 100644 --- a/src/calibre/gui2/tweak_book/widgets.py +++ b/src/calibre/gui2/tweak_book/widgets.py @@ -5,27 +5,41 @@ __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -import os, textwrap, unicodedata +import os +import textwrap +import unicodedata from collections import OrderedDict - from qt.core import ( - QGridLayout, QLabel, QLineEdit, QVBoxLayout, QFormLayout, QHBoxLayout, - QToolButton, QIcon, QApplication, Qt, QWidget, QPoint, QSizePolicy, - QPainter, QStaticText, pyqtSignal, QTextOption, QAbstractListModel, QItemSelectionModel, - QModelIndex, QStyledItemDelegate, QStyle, QCheckBox, QListView, QPalette, - QTextDocument, QSize, QComboBox, QFrame, QCursor, QGroupBox, QSplitter, - QPixmap, QRect, QPlainTextEdit, QMimeData, QDialog, QEvent, QDialogButtonBox) + QAbstractListModel, QApplication, QCheckBox, QComboBox, QCursor, QDialog, + QDialogButtonBox, QEvent, QFormLayout, QFrame, QGridLayout, QGroupBox, + QHBoxLayout, QIcon, QItemSelectionModel, QLabel, QLineEdit, QListView, QMimeData, + QModelIndex, QPainter, QPalette, QPixmap, QPlainTextEdit, QPoint, QRect, QSize, + QSizePolicy, QSplitter, QStaticText, QStyle, QStyledItemDelegate, Qt, + QTextDocument, QTextOption, QToolButton, QVBoxLayout, QWidget, pyqtSignal +) -from calibre import prepare_string_for_xml, human_readable +from calibre import human_readable, prepare_string_for_xml from calibre.constants import iswindows from calibre.ebooks.oeb.polish.cover import get_raster_cover_name -from calibre.ebooks.oeb.polish.utils import lead_text, guess_type -from calibre.gui2 import error_dialog, choose_files, choose_save_file, info_dialog, choose_images -from calibre.gui2.tweak_book import tprefs, current_container -from calibre.gui2.widgets2 import Dialog as BaseDialog, HistoryComboBox, to_plain_text, PARAGRAPH_SEPARATOR -from calibre.utils.icu import primary_sort_key, sort_key, primary_contains, numeric_sort_key -from calibre.utils.matcher import get_char, Matcher, DEFAULT_LEVEL1, DEFAULT_LEVEL2, DEFAULT_LEVEL3 +from calibre.ebooks.oeb.polish.toc import ( + ensure_container_has_nav, get_guide_landmarks, get_nav_landmarks, set_landmarks +) +from calibre.ebooks.oeb.polish.upgrade import guide_epubtype_map +from calibre.ebooks.oeb.polish.utils import guess_type, lead_text +from calibre.gui2 import ( + choose_files, choose_images, choose_save_file, error_dialog, info_dialog +) from calibre.gui2.complete2 import EditWithComplete +from calibre.gui2.tweak_book import current_container, tprefs +from calibre.gui2.widgets2 import ( + PARAGRAPH_SEPARATOR, Dialog as BaseDialog, HistoryComboBox, to_plain_text +) +from calibre.utils.icu import ( + numeric_sort_key, primary_contains, primary_sort_key, sort_key +) +from calibre.utils.matcher import ( + DEFAULT_LEVEL1, DEFAULT_LEVEL2, DEFAULT_LEVEL3, Matcher, get_char +) from polyglot.builtins import iteritems ROOT = QModelIndex() @@ -774,6 +788,7 @@ class InsertLink(Dialog): @classmethod def test(cls): import sys + from calibre.ebooks.oeb.polish.container import get_container c = get_container(sys.argv[-1], tweak_mode=True) d = cls(c, next(c.spine_names)[0]) @@ -789,11 +804,11 @@ class InsertSemantics(Dialog): def __init__(self, container, parent=None): self.container = container - self.anchor_cache = {} - self.original_type_map = {item.get('type', ''):(container.href_to_name(item.get('href'), container.opf_name), item.get('href', '').partition('#')[-1]) - for item in container.opf_xpath('//opf:guide/opf:reference[@href and @type]')} - self.final_type_map = self.original_type_map.copy() self.create_known_type_map() + self.anchor_cache = {} + self.original_guide_map = {item['type']: item for item in get_guide_landmarks(container)} + self.original_nav_map = {item['type']: item for item in get_nav_landmarks(container)} + self.changes = {} Dialog.__init__(self, _('Set semantics'), 'insert-semantics', parent=parent) def sizeHint(self): @@ -801,14 +816,16 @@ class InsertSemantics(Dialog): def create_known_type_map(self): _ = lambda x: x + self.epubtype_guide_map = {v: k for k, v in guide_epubtype_map.items()} self.known_type_map = { - 'title-page': _('Title page'), + 'titlepage': _('Title page'), 'toc': _('Table of Contents'), 'index': _('Index'), 'glossary': _('Glossary'), - 'acknowledgements': _('Acknowledgements'), + 'acknowledgments': _('Acknowledgements'), 'bibliography': _('Bibliography'), 'colophon': _('Colophon'), + 'cover': _('Cover'), 'copyright-page': _('Copyright page'), 'dedication': _('Dedication'), 'epigraph': _('Epigraph'), @@ -817,13 +834,14 @@ class InsertSemantics(Dialog): 'lot': _('List of tables'), 'notes': _('Notes'), 'preface': _('Preface'), - 'text': _('Text'), + 'bodymatter': _('Text'), } _ = __builtins__['_'] type_map_help = { - 'title-page': _('Page with title, author, publisher, etc.'), + 'titlepage': _('Page with title, author, publisher, etc.'), + 'cover': _('The book cover, typically a single HTML file with a cover image inside'), 'index': _('Back-of-book style index'), - 'text': _('First "real" page of content'), + 'bodymatter': _('First "real" page of content'), } t = _ all_types = [(k, (('%s (%s)' % (t(v), type_map_help[k])) if k in type_map_help else t(v))) for k, v in iteritems(self.known_type_map)] @@ -835,6 +853,7 @@ class InsertSemantics(Dialog): self.setLayout(l) self.tl = tl = QFormLayout() + tl.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.semantic_type = QComboBox(self) for key, val in iteritems(self.all_types): self.semantic_type.addItem(val, key) @@ -888,9 +907,21 @@ class InsertSemantics(Dialog): d.resize(d.sizeHint()) d.exec_() + def dest_for_type(self, item_type): + if item_type in self.changes: + return self.changes[item_type] + if item_type in self.original_nav_map: + item = self.original_nav_map[item_type] + return item['dest'], item['frag'] + item_type = self.epubtype_guide_map.get(item_type, item_type) + if item_type in self.original_guide_map: + item = self.original_guide_map[item_type] + return item['dest'], item['frag'] + return None, None + def semantic_type_changed(self): item_type = str(self.semantic_type.itemData(self.semantic_type.currentIndex()) or '') - name, frag = self.final_type_map.get(item_type, (None, None)) + name, frag = self.dest_for_type(item_type) self.show_type(name, frag) def show_type(self, name, frag): @@ -916,7 +947,8 @@ class InsertSemantics(Dialog): def target_text_changed(self): name, frag = str(self.target.text()).partition('#')[::2] item_type = str(self.semantic_type.itemData(self.semantic_type.currentIndex()) or '') - self.final_type_map[item_type] = (name, frag or None) + if item_type: + self.changes[item_type] = (name, frag or None) def selected_file_changed(self, *args): rows = list(self.file_names.selectionModel().selectedRows()) @@ -950,23 +982,32 @@ class InsertSemantics(Dialog): href += frag self.target.setText(href or '#') - @property - def changed_type_map(self): - return {k:v for k, v in iteritems(self.final_type_map) if v != self.original_type_map.get(k, None)} - def apply_changes(self, container): - from calibre.ebooks.oeb.polish.opf import set_guide_item, get_book_language + from calibre.ebooks.oeb.polish.opf import get_book_language, set_guide_item from calibre.translations.dynamic import translate lang = get_book_language(container) - for item_type, (name, frag) in iteritems(self.changed_type_map): - title = self.known_type_map[item_type] + + def title_for_type(item_type): + title = self.known_type_map.get(item_type, item_type) if lang: title = translate(lang, title) - set_guide_item(container, item_type, title, name, frag=frag) + return title + + for item_type, (name, frag) in self.changes.items(): + set_guide_item(container, self.epubtype_guide_map[item_type], title_for_type(item_type), name, frag=frag) + + if container.opf_version_parsed.major > 2: + final = self.original_nav_map.copy() + for item_type, (name, frag) in self.changes.items(): + final[item_type] = {'dest': name, 'frag': frag or '', 'title': title_for_type(item_type), 'type': item_type} + tocname, root = ensure_container_has_nav(container, lang=lang) + set_landmarks(container, root, tocname, final.values()) + container.dirty(tocname) @classmethod def test(cls): import sys + from calibre.ebooks.oeb.polish.container import get_container c = get_container(sys.argv[-1], tweak_mode=True) d = cls(c) @@ -1187,6 +1228,7 @@ class AddCover(Dialog): @classmethod def test(cls): import sys + from calibre.ebooks.oeb.polish.container import get_container c = get_container(sys.argv[-1], tweak_mode=True) d = cls(c)