Adding new entries to the ToC implemented

This commit is contained in:
Kovid Goyal 2013-03-13 13:20:52 +05:30
parent 086ede3705
commit 4928433f75
4 changed files with 217 additions and 44 deletions

View File

@ -222,6 +222,10 @@ class Container(object):
self.encoding_map[name] = self.used_encoding
return ans
def replace(self, name, obj):
self.parsed_cache[name] = obj
self.dirty(name)
@property
def opf(self):
return self.parsed(self.opf_name)
@ -417,12 +421,13 @@ class Container(object):
data = re.sub(br'(<[/]{0,1})opf:', r'\1', data)
return data
def commit_item(self, name):
def commit_item(self, name, keep_parsed=False):
if name not in self.parsed_cache:
return
data = self.serialize_item(name)
self.dirtied.remove(name)
self.parsed_cache.pop(name)
self.dirtied.discard(name)
if not keep_parsed:
self.parsed_cache.pop(name)
with open(self.name_path_map[name], 'wb') as f:
f.write(data)

View File

@ -7,17 +7,23 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
from urlparse import urlparse
from collections import deque
from functools import partial
from lxml import etree
from calibre.ebooks.oeb.base import XPath
from calibre import __version__
from calibre.ebooks.oeb.base import XPath, uuid_id, xml2text, NCX, NCX_NS, XML
from calibre.ebooks.oeb.polish.container import guess_type
from calibre.utils.localization import get_lang, canonicalize_lang, lang_as_iso639_1
ns = etree.FunctionNamespace('calibre_xpath_extensions')
ns.prefix = 'calibre'
ns['lower-case'] = lambda c, x: x.lower() if hasattr(x, 'lower') else x
class TOC(object):
def __init__(self, title=None, dest=None, frag=None):
@ -43,9 +49,18 @@ class TOC(object):
for gc in child.iterdescendants():
yield gc
@property
def depth(self):
"""The maximum depth of the navigation tree rooted at this node."""
try:
return max(node.depth for node in self) + 1
except ValueError:
return 1
def child_xpath(tag, name):
return tag.xpath('./*[calibre:lower-case(local-name()) = "%s"]'%name)
def add_from_navpoint(container, navpoint, parent, ncx_name):
dest = frag = text = None
nl = child_xpath(navpoint, 'navlabel')
@ -64,20 +79,32 @@ def add_from_navpoint(container, navpoint, parent, ncx_name):
frag = urlparse(href).fragment or None
return parent.add(text or None, dest or None, frag or None)
def process_ncx_node(container, node, toc_parent, ncx_name):
for navpoint in node.xpath('./*[calibre:lower-case(local-name()) = "navpoint"]'):
child = add_from_navpoint(container, navpoint, toc_parent, ncx_name)
if child is not None:
process_ncx_node(container, navpoint, child, ncx_name)
def parse_ncx(container, ncx_name):
root = container.parsed(ncx_name)
toc_root = TOC()
navmaps = root.xpath('//*[calibre:lower-case(local-name()) = "navmap"]')
if navmaps:
process_ncx_node(container, navmaps[0], toc_root, ncx_name)
toc_root.lang = toc_root.uid = None
for attr, val in root.attrib.iteritems():
if attr.endswith('lang'):
toc_root.lang = unicode(val)
break
for uid in root.xpath('//*[calibre:lower-case(local-name()) = "meta" and @name="dtb:uid"]/@content'):
if uid:
toc_root.uid = unicode(uid)
break
return toc_root
def verify_toc_destinations(container, toc):
anchor_map = {}
anchor_xpath = XPath('//*/@id|//h:a/@name')
@ -108,7 +135,8 @@ def verify_toc_destinations(container, toc):
'The anchor %(a)s does not exist in file %(f)s')%dict(
a=item.frag, f=name)
def get_toc(container, verify_destinations=True):
def find_existing_toc(container):
toc = container.opf_xpath('//opf:spine/@toc')
if toc:
toc = container.manifest_id_map.get(toc[0], None)
@ -117,9 +145,107 @@ def get_toc(container, verify_destinations=True):
toc = container.manifest_type_map.get(ncx, [None])[0]
if not toc:
return None
return toc
def get_toc(container, verify_destinations=True):
toc = find_existing_toc(container)
if toc is None:
ans = TOC()
ans.lang = ans.uid = None
return ans
ans = parse_ncx(container, toc)
if verify_destinations:
verify_toc_destinations(container, ans)
return ans
def add_id(container, name, loc):
root = container.parsed(name)
body = root.xpath('//*[local-name()="body"]')[0]
locs = deque(loc)
node = body
while locs:
children = tuple(node.iterchildren(etree.Element))
node = children[locs[0]]
locs.popleft()
node.set('id', node.get('id', uuid_id()))
container.commit_item(name, keep_parsed=True)
return node.get('id')
def create_ncx(toc, to_href, btitle, lang, uid):
lang = lang.replace('_', '-')
ncx = etree.Element(NCX('ncx'),
attrib={'version': '2005-1', XML('lang'): lang},
nsmap={None: NCX_NS})
head = etree.SubElement(ncx, NCX('head'))
etree.SubElement(head, NCX('meta'),
name='dtb:uid', content=unicode(uid))
etree.SubElement(head, NCX('meta'),
name='dtb:depth', content=str(toc.depth))
generator = ''.join(['calibre (', __version__, ')'])
etree.SubElement(head, NCX('meta'),
name='dtb:generator', content=generator)
etree.SubElement(head, NCX('meta'), name='dtb:totalPageCount', content='0')
etree.SubElement(head, NCX('meta'), name='dtb:maxPageNumber', content='0')
title = etree.SubElement(ncx, NCX('docTitle'))
text = etree.SubElement(title, NCX('text'))
text.text = btitle
navmap = etree.SubElement(ncx, NCX('navMap'))
spat = re.compile(r'\s+')
def process_node(xml_parent, toc_parent, play_order=0):
for child in toc_parent:
play_order += 1
point = etree.SubElement(xml_parent, NCX('navPoint'), id=uuid_id(),
playOrder=str(play_order))
label = etree.SubElement(point, NCX('navLabel'))
title = child.title
if title:
title = spat.sub(' ', title)
etree.SubElement(label, NCX('text')).text = title
if child.dest:
href = to_href(child.dest)
if child.frag:
href += '#'+child.frag
etree.SubElement(point, NCX('content'), src=href)
process_node(point, child, play_order)
process_node(navmap, toc)
return ncx
def commit_toc(container, toc, lang=None, uid=None):
tocname = find_existing_toc(container)
if tocname is None:
item = container.generate_item('toc.ncx', id_prefix='toc')
tocname = container.href_to_name(item.get('href'),
base=container.opf_name)
if not lang:
lang = get_lang()
for l in container.opf_xpath('//dc:language'):
l = canonicalize_lang(xml2text(l).strip())
if l:
lang = l
lang = lang_as_iso639_1(l) or l
break
lang = lang_as_iso639_1(lang) or lang
if not uid:
uid = uuid_id()
eid = container.opf.get('unique-identifier', None)
if eid:
m = container.opf_xpath('//*[@id="%s"]'%eid)
if m:
uid = xml2text(m[0])
title = _('Table of Contents')
m = container.opf_xpath('//dc:title')
if m:
x = xml2text(m[0]).strip()
title = x or title
to_href = partial(container.name_to_href, base=tocname)
root = create_ncx(toc, to_href, title, lang, uid)
container.replace(tocname, root)

View File

@ -131,15 +131,14 @@ class ItemEdit(QWidget):
la.setWordWrap(True)
l.addWidget(la)
f.la2 = la = QLabel(_('&Name of the ToC entry:'))
f.la2 = la = QLabel('<b>'+_('&Name of the ToC entry:'))
l.addWidget(la)
self.name = QLineEdit(self)
la.setBuddy(self.name)
l.addWidget(self.name)
self.base_msg = _('Currently selected destination:')
self.base_msg = '<b>'+_('Currently selected destination:')+'</b>'
self.dest_label = la = QLabel(self.base_msg)
la.setTextFormat(Qt.PlainText)
la.setWordWrap(True)
la.setStyleSheet('QLabel { margin-top: 20px }')
l.addWidget(la)
@ -163,8 +162,8 @@ class ItemEdit(QWidget):
load_html(path, self.view, codec=encoding,
mime_type=self.container.mime_map[name])
self.view.load_js()
self.dest_label.setText(self.base_msg + '\n' + _('File:') + ' ' +
name + '\n' + _('Top of the file'))
self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' +
name + '<br>' + _('Top of the file'))
def __call__(self, item, where):
self.current_item, self.current_where = item, where
@ -173,23 +172,22 @@ class ItemEdit(QWidget):
if item is None:
self.dest_list.setCurrentRow(0)
self.name.setText(_('(Untitled)'))
self.dest_label.setText(self.base_msg + '\n' + _('None'))
def elem_clicked(self, tag, frac, elem_id, loc):
self.current_frag = elem_id or loc
frac = int(round(frac * 100))
base = _('Location: A <%s> tag inside the file')%tag
base = _('Location: A &lt;%s&gt; tag inside the file')%tag
if frac == 0:
loctext = _('Top of the file')
else:
loctext = _('Approximately %d%% from the top')%frac
loctext = base + ' [%s]'%loctext
self.dest_label.setText(self.base_msg + '\n' +
_('File:') + ' ' + self.current_name + '\n' + loctext)
self.dest_label.setText(self.base_msg + '<br>' +
_('File:') + ' ' + self.current_name + '<br>' + loctext)
@property
def result(self):
return (self.current_item, self.current_where, self.current_name,
self.current_frag)
self.current_frag, unicode(self.name.text()))

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import (QPushButton, QFrame,
QToolButton, QItemSelectionModel)
from calibre.ebooks.oeb.polish.container import get_container
from calibre.ebooks.oeb.polish.toc import get_toc
from calibre.ebooks.oeb.polish.toc import get_toc, add_id, TOC, commit_toc
from calibre.gui2 import Application
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.toc.location import ItemEdit
@ -191,43 +191,50 @@ class TOCView(QWidget): # {{{
def update_status_tip(self, item):
c = item.data(0, Qt.UserRole).toPyObject()
frag = c.frag or ''
if frag:
frag = '#'+frag
item.setStatusTip(0, _('<b>Title</b>: {0} <b>Dest</b>: {1}{2}').format(
c.title, c.dest, frag))
if c is not None:
frag = c.frag or ''
if frag:
frag = '#'+frag
item.setStatusTip(0, _('<b>Title</b>: {0} <b>Dest</b>: {1}{2}').format(
c.title, c.dest, frag))
def data_changed(self, top_left, bottom_right):
for r in xrange(top_left.row(), bottom_right.row()+1):
idx = self.tocw.model().index(r, 0, top_left.parent())
new_title = unicode(idx.data(Qt.DisplayRole).toString()).strip()
toc = idx.data(Qt.UserRole).toPyObject()
toc.title = new_title or _('(Untitled)')
if toc is not None:
toc.title = new_title or _('(Untitled)')
item = self.tocw.itemFromIndex(idx)
self.update_status_tip(item)
def create_item(self, parent, child):
c = QTreeWidgetItem(parent)
c.setData(0, Qt.DisplayRole, child.title or _('(Untitled)'))
c.setData(0, Qt.UserRole, child)
c.setFlags(Qt.ItemIsDragEnabled|Qt.ItemIsEditable|Qt.ItemIsEnabled|
Qt.ItemIsSelectable|Qt.ItemIsDropEnabled)
c.setData(0, Qt.DecorationRole, self.icon_map[child.dest_exists])
if child.dest_exists is False:
c.setData(0, Qt.ToolTipRole, _(
'The location this entry point to does not exist:\n%s')
%child.dest_error)
self.update_status_tip(c)
return c
def __call__(self, ebook):
self.ebook = ebook
self.toc = get_toc(self.ebook)
blank = self.blank = QIcon(I('blank.png'))
ok = self.ok = QIcon(I('ok.png'))
err = self.err = QIcon(I('dot_red.png'))
icon_map = {None:blank, True:ok, False:err}
self.toc_lang, self.toc_uid = self.toc.lang, self.toc.uid
self.blank = QIcon(I('blank.png'))
self.ok = QIcon(I('ok.png'))
self.err = QIcon(I('dot_red.png'))
self.icon_map = {None:self.blank, True:self.ok, False:self.err}
def process_item(node, parent):
for child in node:
c = QTreeWidgetItem(parent)
c.setData(0, Qt.DisplayRole, child.title or _('(Untitled)'))
c.setData(0, Qt.UserRole, child)
c.setFlags(Qt.ItemIsDragEnabled|Qt.ItemIsEditable|Qt.ItemIsEnabled|
Qt.ItemIsSelectable|Qt.ItemIsDropEnabled)
c.setData(0, Qt.DecorationRole, icon_map[child.dest_exists])
if child.dest_exists is False:
c.setData(0, Qt.ToolTipRole, _(
'The location this entry point to does not exist:\n%s')
%child.dest_error)
self.update_status_tip(c)
def process_item(toc_node, parent):
for child in toc_node:
c = self.create_item(parent, child)
process_item(child, c)
root = self.root = self.tocw.invisibleRootItem()
@ -235,10 +242,38 @@ class TOCView(QWidget): # {{{
process_item(self.toc, root)
self.tocw.model().dataChanged.connect(self.data_changed)
self.tocw.currentItemChanged.connect(self.current_item_changed)
self.tocw.setCurrentItem(None)
def current_item_changed(self, current, previous):
self.item_view(current)
def update_item(self, item, where, name, frag, title):
if isinstance(frag, tuple):
frag = add_id(self.ebook, name, frag)
if item is None:
if where is None:
where = self.tocw.invisibleRootItem()
child = TOC(title, name, frag)
child.dest_exists = True
c = self.create_item(where, child)
self.tocw.setCurrentItem(c, 0, QItemSelectionModel.ClearAndSelect)
self.tocw.scrollToItem(c)
def create_toc(self):
root = TOC()
def process_node(parent, toc_parent):
for i in xrange(parent.childCount()):
item = parent.child(i)
title = unicode(item.data(0, Qt.DisplayRole).toString()).strip()
toc = item.data(0, Qt.UserRole).toPyObject()
dest, frag = toc.dest, toc.frag
toc = toc_parent.add(title, dest, frag)
process_node(item, toc)
process_node(self.tocw.invisibleRootItem(), root)
return root
# }}}
class TOCEditor(QDialog): # {{{
@ -283,6 +318,7 @@ class TOCEditor(QDialog): # {{{
self.explode_done.connect(self.read_toc, type=Qt.QueuedConnection)
self.resize(950, 630)
self.working = True
def add_new_item(self, item, where):
self.item_edit(item, where)
@ -290,15 +326,18 @@ class TOCEditor(QDialog): # {{{
def accept(self):
if self.stacks.currentIndex() == 2:
self.toc_view.update_item(self.item_edit.result)
self.toc_view.update_item(*self.item_edit.result)
self.stacks.setCurrentIndex(1)
else:
self.write_toc()
self.working = False
super(TOCEditor, self).accept()
def reject(self):
if self.stacks.currentIndex() == 2:
self.stacks.setCurrentIndex(1)
else:
self.working = False
super(TOCEditor, self).accept()
def start(self):
@ -309,9 +348,8 @@ class TOCEditor(QDialog): # {{{
def explode(self):
self.ebook = get_container(self.pathtobook, log=self.log)
if not self.isVisible():
return
self.explode_done.emit()
if self.working:
self.explode_done.emit()
def read_toc(self):
self.pi.stopAnimation()
@ -319,6 +357,12 @@ class TOCEditor(QDialog): # {{{
self.item_edit.load(self.ebook)
self.stacks.setCurrentIndex(1)
def write_toc(self):
toc = self.toc_view.create_toc()
commit_toc(self.ebook, toc, lang=self.toc_view.toc_lang,
uid=self.toc_view.toc_uid)
self.ebook.commit()
# }}}
if __name__ == '__main__':