Edit book: Set semantics tool: Add support for EPUB 3 landmarks

This commit is contained in:
Kovid Goyal 2021-10-23 16:21:24 +05:30
parent 47143b0506
commit e416b1c30b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 77 additions and 35 deletions

View File

@ -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()

View File

@ -5,27 +5,41 @@
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
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)