EPUB Output: Generate inline ToC

EPUB Output: Add an option to insert an inline Table of COntents into
the main text. Fixes #1201006 [Epub output: not possible to insert inline toc](https://bugs.launchpad.net/calibre/+bug/1201006)
This commit is contained in:
Kovid Goyal 2013-07-15 16:47:54 +05:30
parent dc97d6ad69
commit 12cded043f
4 changed files with 114 additions and 48 deletions

View File

@ -105,14 +105,23 @@ class EPUBOutput(OutputFormatPlugin):
' EPUB, putting all files into the top level.')
),
OptionRecommendation(name='epub_inline_toc', recommended_value=False,
help=_('Insert an inline Table of Contents that will appear as part of the main book content.')
),
OptionRecommendation(name='epub_toc_at_end', recommended_value=False,
help=_('Put the inserted inline Table of Contents at the end of the book instead of the start.')
),
OptionRecommendation(name='toc_title', recommended_value=None,
help=_('Title for any generated in-line table of contents.')
),
])
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
def workaround_webkit_quirks(self): # {{{
def workaround_webkit_quirks(self): # {{{
from calibre.ebooks.oeb.base import XPath
for x in self.oeb.spine:
root = x.data
@ -128,13 +137,13 @@ class EPUBOutput(OutputFormatPlugin):
pre.tag = 'div'
# }}}
def upshift_markup(self): # {{{
def upshift_markup(self): # {{{
'Upgrade markup to comply with XHTML 1.1 where possible'
from calibre.ebooks.oeb.base import XPath, XML
for x in self.oeb.spine:
root = x.data
if (not root.get(XML('lang'))) and (root.get('lang')):
root.set(XML('lang'), root.get('lang'))
root.set(XML('lang'), root.get('lang'))
body = XPath('//h:body')(root)
if body:
body = body[0]
@ -159,12 +168,17 @@ class EPUBOutput(OutputFormatPlugin):
else:
seen_names.add(name)
# }}}
def convert(self, oeb, output_path, input_plugin, opts, log):
self.log, self.opts, self.oeb = log, opts, oeb
if self.opts.epub_inline_toc:
from calibre.ebooks.mobi.writer8.toc import TOCAdder
opts.mobi_toc_at_start = not opts.epub_toc_at_end
opts.mobi_passthrough = False
opts.no_inline_toc = False
TOCAdder(oeb, opts, replace_previous_inline_toc=True, ignore_existing_toc=True)
if self.opts.epub_flatten:
from calibre.ebooks.oeb.transforms.filenames import FlatFilenames
FlatFilenames()(oeb, opts)
@ -234,7 +248,7 @@ class EPUBOutput(OutputFormatPlugin):
oeb_output = plugin_for_output_format('oeb')
oeb_output.convert(oeb, tdir, input_plugin, opts, log)
opf = [x for x in os.listdir(tdir) if x.endswith('.opf')][0]
self.condense_ncx([os.path.join(tdir, x) for x in os.listdir(tdir)\
self.condense_ncx([os.path.join(tdir, x) for x in os.listdir(tdir)
if x.endswith('.ncx')][0])
encryption = None
if encrypted_fonts:
@ -261,7 +275,7 @@ class EPUBOutput(OutputFormatPlugin):
zf.extractall(path=opts.extract_to)
self.log.info('EPUB extracted to', opts.extract_to)
def encrypt_fonts(self, uris, tdir, uuid): # {{{
def encrypt_fonts(self, uris, tdir, uuid): # {{{
from binascii import unhexlify
key = re.sub(r'[^a-fA-F0-9]', '', uuid)
@ -301,14 +315,14 @@ class EPUBOutput(OutputFormatPlugin):
</enc:EncryptedData>
'''%(uri.replace('"', '\\"')))
if fonts:
ans = '''<encryption
ans = '''<encryption
xmlns="urn:oasis:names:tc:opendocument:xmlns:container"
xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
xmlns:deenc="http://ns.adobe.com/digitaleditions/enc">
'''
ans += (u'\n'.join(fonts)).encode('utf-8')
ans += '\n</encryption>'
return ans
ans += (u'\n'.join(fonts)).encode('utf-8')
ans += '\n</encryption>'
return ans
# }}}
def condense_ncx(self, ncx_path):
@ -323,7 +337,7 @@ class EPUBOutput(OutputFormatPlugin):
compressed = etree.tostring(tree.getroot(), encoding='utf-8')
open(ncx_path, 'wb').write(compressed)
def workaround_ade_quirks(self): # {{{
def workaround_ade_quirks(self): # {{{
'''
Perform various markup transforms to get the output to render correctly
in the quirky ADE.
@ -462,7 +476,7 @@ class EPUBOutput(OutputFormatPlugin):
# }}}
def workaround_sony_quirks(self): # {{{
def workaround_sony_quirks(self): # {{{
'''
Perform toc link transforms to alleviate slow loading.
'''

View File

@ -34,9 +34,17 @@ TEMPLATE = '''
</html>
'''
def find_previous_calibre_inline_toc(oeb):
if 'toc' in oeb.guide:
href = urlnormalize(oeb.guide['toc'].href.partition('#')[0])
if href in oeb.manifest.hrefs:
item = oeb.manifest.hrefs[href]
if (hasattr(item.data, 'xpath') and XPath('//h:body[@id="calibre_generated_inline_toc"]')(item.data)):
return item
class TOCAdder(object):
def __init__(self, oeb, opts):
def __init__(self, oeb, opts, replace_previous_inline_toc=False, ignore_existing_toc=False):
self.oeb, self.opts, self.log = oeb, opts, oeb.log
self.title = opts.toc_title or DEFAULT_TITLE
self.at_start = opts.mobi_toc_at_start
@ -44,6 +52,12 @@ class TOCAdder(object):
self.added_toc_guide_entry = False
self.has_toc = oeb.toc and oeb.toc.count() > 1
self.tocitem = tocitem = None
if find_previous_calibre_inline_toc:
tocitem = self.tocitem = find_previous_calibre_inline_toc(oeb)
if ignore_existing_toc and 'toc' in oeb.guide:
oeb.guide.remove('toc')
if 'toc' in oeb.guide:
# Remove spurious toc entry from guide if it is not in spine or it
# does not have any hyperlinks
@ -81,13 +95,19 @@ class TOCAdder(object):
for child in self.oeb.toc:
self.process_toc_node(child, parent)
id, href = oeb.manifest.generate('contents', 'contents.xhtml')
item = self.generated_item = oeb.manifest.add(id, href, XHTML_MIME,
data=root)
if self.at_start:
oeb.spine.insert(0, item, linear=True)
if tocitem is not None:
href = tocitem.href
if oeb.spine.index(tocitem) > -1:
oeb.spine.remove(tocitem)
tocitem.data = root
else:
oeb.spine.add(item, linear=False)
id, href = oeb.manifest.generate('contents', 'contents.xhtml')
tocitem = self.generated_item = oeb.manifest.add(id, href, XHTML_MIME,
data=root)
if self.at_start:
oeb.spine.insert(0, tocitem, linear=True)
else:
oeb.spine.add(tocitem, linear=False)
oeb.guide.add('toc', 'Table of Contents', href)
@ -95,7 +115,10 @@ class TOCAdder(object):
li = parent.makeelement(XHTML('li'))
li.tail = '\n'+ ('\t'*level)
parent.append(li)
a = parent.makeelement(XHTML('a'), href=toc.href or '#')
href = toc.href
if self.tocitem is not None and href:
href = self.tocitem.relhref(toc.href)
a = parent.makeelement(XHTML('a'), href=href or '#')
a.text = toc.title
li.append(a)
if toc.count() > 0:
@ -115,3 +138,4 @@ class TOCAdder(object):
self.oeb.guide.remove('toc')
self.added_toc_guide_entry = False

View File

@ -21,6 +21,7 @@ class PluginWidget(Widget, Ui_Form):
Widget.__init__(self, parent,
['dont_split_on_page_breaks', 'flow_size',
'no_default_epub_cover', 'no_svg_cover',
'epub_inline_toc', 'epub_toc_at_end', 'toc_title',
'preserve_cover_aspect_ratio', 'epub_flatten']
)
for i in range(2):

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<width>644</width>
<height>300</height>
</rect>
</property>
@ -14,27 +14,6 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="opt_dont_split_on_page_breaks">
<property name="text">
<string>Do not &amp;split on page breaks</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="opt_no_default_epub_cover">
<property name="text">
<string>No default &amp;cover</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_no_svg_cover">
<property name="text">
<string>No &amp;SVG cover</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
<property name="text">
@ -42,7 +21,7 @@
</property>
</widget>
</item>
<item row="3" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Split files &amp;larger than:</string>
@ -52,7 +31,7 @@
</property>
</widget>
</item>
<item row="3" column="1">
<item row="5" column="1">
<widget class="QSpinBox" name="opt_flow_size">
<property name="suffix">
<string> KB</string>
@ -68,7 +47,7 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -81,6 +60,41 @@
</property>
</spacer>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="opt_no_default_epub_cover">
<property name="text">
<string>No default &amp;cover</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_no_svg_cover">
<property name="text">
<string>No &amp;SVG cover</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_epub_inline_toc">
<property name="text">
<string>Insert inline &amp;Table of Contents</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_dont_split_on_page_breaks">
<property name="text">
<string>Do not &amp;split on page breaks</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="opt_epub_toc_at_end">
<property name="text">
<string>Put inserted Table of Contents at the &amp;end of the book</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="opt_epub_flatten">
<property name="text">
@ -88,6 +102,19 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Title for inserted ToC:</string>
</property>
<property name="buddy">
<cstring>opt_toc_title</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="opt_toc_title"/>
</item>
</layout>
</widget>
<resources/>