Edit Book: New tool to specify semantics in EPUB books (semantics are items in the guide such as preface, title-page, dedication, etc.). Fixes #1287025 [[ebook-edit] Implement of defining more reference types of guide section](https://bugs.launchpad.net/calibre/+bug/1287025)

This commit is contained in:
Kovid Goyal 2014-03-18 13:18:07 +05:30
parent c53e46512e
commit f973436000
4 changed files with 233 additions and 12 deletions

View File

@ -20,26 +20,32 @@ def get_book_language(container):
return code
def set_guide_item(container, item_type, title, name, frag=None):
guides = container.opf_xpath('//opf:guide')
if not guides:
g = container.opf.makeelement('{%s}guide' % OPF_NAMESPACES['opf'], nsmap={'opf':OPF_NAMESPACES['opf']})
container.insert_into_xml(container.opf, g)
guides = [g]
ref_tag = '{%s}reference' % OPF_NAMESPACES['opf']
href = None
if name:
href = container.name_to_href(name, container.opf_name)
if frag:
href += '#' + frag
guides = container.opf_xpath('//opf:guide')
if not guides and href:
g = container.opf.makeelement('{%s}guide' % OPF_NAMESPACES['opf'], nsmap={'opf':OPF_NAMESPACES['opf']})
container.insert_into_xml(container.opf, g)
guides = [g]
for guide in guides:
matches = []
for child in guide.iterchildren(etree.Element):
if child.tag == ref_tag and child.get('type', '').lower() == item_type.lower():
matches.append(child)
if not matches:
if not matches and href:
r = guide.makeelement(ref_tag, type=item_type, nsmap={'opf':OPF_NAMESPACES['opf']})
container.insert_into_xml(guide, r)
matches.append(r)
for m in matches:
if href:
m.set('title', title), m.set('href', href), m.set('type', item_type)
else:
container.remove_from_xml(m)
container.dirty(container.opf_name)

View File

@ -38,7 +38,7 @@ from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook
from calibre.gui2.tweak_book.preferences import Preferences
from calibre.gui2.tweak_book.widgets import (
RationalizeFolders, MultiSplit, ImportForeign, QuickOpen, InsertLink)
RationalizeFolders, MultiSplit, ImportForeign, QuickOpen, InsertLink, InsertSemantics)
_diff_dialogs = []
@ -649,6 +649,18 @@ class Boss(QObject):
else:
ed.action_triggered(action)
def set_semantics(self):
self.commit_all_editors_to_container()
c = current_container()
if c.book_type == 'azw3':
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_() == d.Accepted and d.changed_type_map:
self.add_savepoint(_('Before: Set Semantics'))
d.apply_changes(current_container())
self.apply_container_update_to_gui()
def show_find(self):
self.gui.central.show_find()
ed = self.gui.central.current_editor

View File

@ -343,6 +343,8 @@ class Main(MainWindow):
_('Insert special character'))
self.action_rationalize_folders = reg('mimetypes/dir.png', _('&Arrange into folders'), self.boss.rationalize_folders, 'rationalize-folders', (),
_('Arrange into folders'))
self.action_set_semantics = reg('tags.png', _('Set &Semantics'), self.boss.set_semantics, 'set-semantics', (),
_('Set Semantics'))
# Polish actions
group = _('Polish Book')
@ -472,6 +474,7 @@ class Main(MainWindow):
e.addAction(self.action_fix_html_all)
e.addAction(self.action_pretty_all)
e.addAction(self.action_rationalize_folders)
e.addAction(self.action_set_semantics)
e.addAction(self.action_check_book)
e = b.addMenu(_('&View'))

View File

@ -8,18 +8,19 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import os
from itertools import izip
from collections import OrderedDict
from PyQt4.Qt import (
QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit, QVBoxLayout,
QFormLayout, QHBoxLayout, QToolButton, QIcon, QApplication, Qt, QWidget,
QPoint, QSizePolicy, QPainter, QStaticText, pyqtSignal, QTextOption,
QAbstractListModel, QModelIndex, QVariant, QStyledItemDelegate, QStyle,
QListView, QTextDocument, QSize)
QListView, QTextDocument, QSize, QComboBox, QFrame)
from calibre import prepare_string_for_xml
from calibre.gui2 import error_dialog, choose_files, choose_save_file, NONE
from calibre.gui2 import error_dialog, choose_files, choose_save_file, NONE, info_dialog
from calibre.gui2.tweak_book import tprefs
from calibre.utils.icu import primary_sort_key
from calibre.utils.icu import primary_sort_key, sort_key
from calibre.utils.matcher import get_char, Matcher
ROOT = QModelIndex()
@ -520,6 +521,11 @@ class NamesModel(QAbstractListModel):
self.reset()
self.filtered.emit(not bool(query))
def find_name(self, name):
for i, (text, positions) in enumerate(self.items):
if text == name:
return i
def create_filterable_names_list(names, filter_text=None, parent=None):
nl = QListView(parent)
nl.m = m = NamesModel(names, parent=nl)
@ -642,6 +648,200 @@ class InsertLink(Dialog):
# }}}
# Insert Semantics {{{
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()
Dialog.__init__(self, _('Set Semantics'), 'insert-semantics', parent=parent)
def sizeHint(self):
return QSize(800, 600)
def create_known_type_map(self):
_ = lambda x: x
self.known_type_map = {
'title-page': _('Title Page'),
'toc': _('Table of Contents'),
'index': _('Index'),
'glossary': _('Glossary'),
'acknowledgements': _('Acknowledgements'),
'bibliography': _('Bibliography'),
'colophon': _('Colophon'),
'copyright-page': _('Copyright page'),
'dedication': _('Dedication'),
'epigraph': _('Epigraph'),
'foreword': _('Foreword'),
'loi': _('List of Illustrations'),
'lot': _('List of Tables'),
'notes:': _('Notes'),
'preface': _('Preface'),
'text': _('Text'),
}
_ = __builtins__['_']
type_map_help = {
'title-page': _('Page with title, author, publisher, etc.'),
'index': _('Back-of-book style index'),
'text': _('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 self.known_type_map.iteritems()]
all_types.sort(key=lambda x: sort_key(x[1]))
self.all_types = OrderedDict(all_types)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.setLayout(l)
self.tl = tl = QFormLayout()
self.semantic_type = QComboBox(self)
for key, val in self.all_types.iteritems():
self.semantic_type.addItem(val, key)
tl.addRow(_('Type of &semantics:'), self.semantic_type)
self.target = t = QLineEdit(self)
t.setPlaceholderText(_('The destination (href) for the link'))
tl.addRow(_('&Target:'), t)
l.addLayout(tl)
self.hline = hl = QFrame(self)
hl.setFrameStyle(hl.HLine)
l.addWidget(hl)
self.h = h = QHBoxLayout()
l.addLayout(h)
names = [n for n, linear in self.container.spine_names]
fn, f = create_filterable_names_list(names, filter_text=_('Filter files'), parent=self)
self.file_names, self.file_names_filter = fn, f
fn.selectionModel().selectionChanged.connect(self.selected_file_changed)
self.fnl = fnl = QVBoxLayout()
self.la1 = la = QLabel(_('Choose a &file:'))
la.setBuddy(fn)
fnl.addWidget(la), fnl.addWidget(f), fnl.addWidget(fn)
h.addLayout(fnl), h.setStretch(0, 2)
fn, f = create_filterable_names_list([], filter_text=_('Filter locations'), parent=self)
self.anchor_names, self.anchor_names_filter = fn, f
fn.selectionModel().selectionChanged.connect(self.update_target)
fn.doubleClicked.connect(self.accept, type=Qt.QueuedConnection)
self.anl = fnl = QVBoxLayout()
self.la2 = la = QLabel(_('Choose a &location (anchor) in the file:'))
la.setBuddy(fn)
fnl.addWidget(la), fnl.addWidget(f), fnl.addWidget(fn)
h.addLayout(fnl), h.setStretch(1, 1)
self.bb.addButton(self.bb.Help)
self.bb.helpRequested.connect(self.help_requested)
l.addWidget(self.bb)
self.semantic_type_changed()
self.semantic_type.currentIndexChanged.connect(self.semantic_type_changed)
self.target.textChanged.connect(self.target_text_changed)
def help_requested(self):
d = info_dialog(self, _('About semantics'), _(
'Semantics refer to additional information about specific locations in the book.'
' For example, you can specify that a particular location is the dedication or the preface'
' or the table of contents and so on.\n\nFirst choose the type of semantic information, then'
' choose a file and optionally a location within the file to point to.\n\nThe'
' semantic information will be written in the <guide> section of the opf file.'))
d.resize(d.sizeHint())
d.exec_()
def semantic_type_changed(self):
item_type = unicode(self.semantic_type.itemData(self.semantic_type.currentIndex()).toString())
name, frag = self.final_type_map.get(item_type, (None, None))
self.show_type(name, frag)
def show_type(self, name, frag):
self.file_names_filter.clear(), self.anchor_names_filter.clear()
self.file_names.clearSelection(), self.anchor_names.clearSelection()
if name is not None:
row = self.file_names.model().find_name(name)
if row is not None:
sm = self.file_names.selectionModel()
sm.select(self.file_names.model().index(row), sm.ClearAndSelect)
if frag:
row = self.anchor_names.model().find_name(frag)
if row is not None:
sm = self.anchor_names.selectionModel()
sm.select(self.anchor_names.model().index(row), sm.ClearAndSelect)
self.target.blockSignals(True)
if name is not None:
self.target.setText(name + (('#' + frag) if frag else ''))
else:
self.target.setText('')
self.target.blockSignals(False)
def target_text_changed(self):
name, frag = unicode(self.target.text()).partition('#')[::2]
item_type = unicode(self.semantic_type.itemData(self.semantic_type.currentIndex()).toString())
self.final_type_map[item_type] = (name, frag or None)
def selected_file_changed(self, *args):
rows = list(self.file_names.selectionModel().selectedRows())
if not rows:
self.anchor_names.model().set_names([])
else:
name, positions = self.file_names.model().data(rows[0], Qt.UserRole).toPyObject()
self.populate_anchors(name)
def populate_anchors(self, name):
if name not in self.anchor_cache:
from calibre.ebooks.oeb.base import XHTML_NS
root = self.container.parsed(name)
self.anchor_cache[name] = sorted(
(set(root.xpath('//*/@id')) | set(root.xpath('//h:a/@name', namespaces={'h':XHTML_NS}))) - {''}, key=primary_sort_key)
self.anchor_names.model().set_names(self.anchor_cache[name])
self.update_target()
def update_target(self):
rows = list(self.file_names.selectionModel().selectedRows())
if not rows:
return
name = self.file_names.model().data(rows[0], Qt.UserRole).toPyObject()[0]
href = name
frag = ''
rows = list(self.anchor_names.selectionModel().selectedRows())
if rows:
anchor = self.anchor_names.model().data(rows[0], Qt.UserRole).toPyObject()[0]
if anchor:
frag = '#' + anchor
href += frag
self.target.setText(href or '#')
@property
def changed_type_map(self):
return {k:v for k, v in self.final_type_map.iteritems() 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.translations.dynamic import translate
lang = get_book_language(container)
for item_type, (name, frag) in self.changed_type_map.iteritems():
title = self.known_type_map[item_type]
if lang:
title = translate(lang, title)
set_guide_item(container, item_type, title, name, frag=frag)
@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)
if d.exec_() == d.Accepted:
import pprint
pprint.pprint(d.changed_type_map)
d.apply_changes(d.container)
# }}}
if __name__ == '__main__':
app = QApplication([])
InsertLink.test()
InsertSemantics.test()