mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-11 09:13:57 -04:00
Adding new entries to the ToC implemented
This commit is contained in:
parent
086ede3705
commit
4928433f75
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 <%s> 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()))
|
||||
|
||||
|
||||
|
@ -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__':
|
||||
|
Loading…
x
Reference in New Issue
Block a user