mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 18:24:30 -04:00
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:
parent
dc97d6ad69
commit
12cded043f
@ -105,14 +105,23 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
' EPUB, putting all files into the top level.')
|
' 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)])
|
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
|
||||||
|
|
||||||
|
def workaround_webkit_quirks(self): # {{{
|
||||||
|
|
||||||
def workaround_webkit_quirks(self): # {{{
|
|
||||||
from calibre.ebooks.oeb.base import XPath
|
from calibre.ebooks.oeb.base import XPath
|
||||||
for x in self.oeb.spine:
|
for x in self.oeb.spine:
|
||||||
root = x.data
|
root = x.data
|
||||||
@ -128,13 +137,13 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
pre.tag = 'div'
|
pre.tag = 'div'
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def upshift_markup(self): # {{{
|
def upshift_markup(self): # {{{
|
||||||
'Upgrade markup to comply with XHTML 1.1 where possible'
|
'Upgrade markup to comply with XHTML 1.1 where possible'
|
||||||
from calibre.ebooks.oeb.base import XPath, XML
|
from calibre.ebooks.oeb.base import XPath, XML
|
||||||
for x in self.oeb.spine:
|
for x in self.oeb.spine:
|
||||||
root = x.data
|
root = x.data
|
||||||
if (not root.get(XML('lang'))) and (root.get('lang')):
|
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)
|
body = XPath('//h:body')(root)
|
||||||
if body:
|
if body:
|
||||||
body = body[0]
|
body = body[0]
|
||||||
@ -159,12 +168,17 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
else:
|
else:
|
||||||
seen_names.add(name)
|
seen_names.add(name)
|
||||||
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def convert(self, oeb, output_path, input_plugin, opts, log):
|
def convert(self, oeb, output_path, input_plugin, opts, log):
|
||||||
self.log, self.opts, self.oeb = log, opts, oeb
|
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:
|
if self.opts.epub_flatten:
|
||||||
from calibre.ebooks.oeb.transforms.filenames import FlatFilenames
|
from calibre.ebooks.oeb.transforms.filenames import FlatFilenames
|
||||||
FlatFilenames()(oeb, opts)
|
FlatFilenames()(oeb, opts)
|
||||||
@ -234,7 +248,7 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
oeb_output = plugin_for_output_format('oeb')
|
oeb_output = plugin_for_output_format('oeb')
|
||||||
oeb_output.convert(oeb, tdir, input_plugin, opts, log)
|
oeb_output.convert(oeb, tdir, input_plugin, opts, log)
|
||||||
opf = [x for x in os.listdir(tdir) if x.endswith('.opf')][0]
|
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])
|
if x.endswith('.ncx')][0])
|
||||||
encryption = None
|
encryption = None
|
||||||
if encrypted_fonts:
|
if encrypted_fonts:
|
||||||
@ -261,7 +275,7 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
zf.extractall(path=opts.extract_to)
|
zf.extractall(path=opts.extract_to)
|
||||||
self.log.info('EPUB extracted to', 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
|
from binascii import unhexlify
|
||||||
|
|
||||||
key = re.sub(r'[^a-fA-F0-9]', '', uuid)
|
key = re.sub(r'[^a-fA-F0-9]', '', uuid)
|
||||||
@ -301,14 +315,14 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
</enc:EncryptedData>
|
</enc:EncryptedData>
|
||||||
'''%(uri.replace('"', '\\"')))
|
'''%(uri.replace('"', '\\"')))
|
||||||
if fonts:
|
if fonts:
|
||||||
ans = '''<encryption
|
ans = '''<encryption
|
||||||
xmlns="urn:oasis:names:tc:opendocument:xmlns:container"
|
xmlns="urn:oasis:names:tc:opendocument:xmlns:container"
|
||||||
xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
|
xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
|
||||||
xmlns:deenc="http://ns.adobe.com/digitaleditions/enc">
|
xmlns:deenc="http://ns.adobe.com/digitaleditions/enc">
|
||||||
'''
|
'''
|
||||||
ans += (u'\n'.join(fonts)).encode('utf-8')
|
ans += (u'\n'.join(fonts)).encode('utf-8')
|
||||||
ans += '\n</encryption>'
|
ans += '\n</encryption>'
|
||||||
return ans
|
return ans
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def condense_ncx(self, ncx_path):
|
def condense_ncx(self, ncx_path):
|
||||||
@ -323,7 +337,7 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
compressed = etree.tostring(tree.getroot(), encoding='utf-8')
|
compressed = etree.tostring(tree.getroot(), encoding='utf-8')
|
||||||
open(ncx_path, 'wb').write(compressed)
|
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
|
Perform various markup transforms to get the output to render correctly
|
||||||
in the quirky ADE.
|
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.
|
Perform toc link transforms to alleviate slow loading.
|
||||||
'''
|
'''
|
||||||
|
@ -34,9 +34,17 @@ TEMPLATE = '''
|
|||||||
</html>
|
</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):
|
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.oeb, self.opts, self.log = oeb, opts, oeb.log
|
||||||
self.title = opts.toc_title or DEFAULT_TITLE
|
self.title = opts.toc_title or DEFAULT_TITLE
|
||||||
self.at_start = opts.mobi_toc_at_start
|
self.at_start = opts.mobi_toc_at_start
|
||||||
@ -44,6 +52,12 @@ class TOCAdder(object):
|
|||||||
self.added_toc_guide_entry = False
|
self.added_toc_guide_entry = False
|
||||||
self.has_toc = oeb.toc and oeb.toc.count() > 1
|
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:
|
if 'toc' in oeb.guide:
|
||||||
# Remove spurious toc entry from guide if it is not in spine or it
|
# Remove spurious toc entry from guide if it is not in spine or it
|
||||||
# does not have any hyperlinks
|
# does not have any hyperlinks
|
||||||
@ -81,13 +95,19 @@ class TOCAdder(object):
|
|||||||
for child in self.oeb.toc:
|
for child in self.oeb.toc:
|
||||||
self.process_toc_node(child, parent)
|
self.process_toc_node(child, parent)
|
||||||
|
|
||||||
id, href = oeb.manifest.generate('contents', 'contents.xhtml')
|
if tocitem is not None:
|
||||||
item = self.generated_item = oeb.manifest.add(id, href, XHTML_MIME,
|
href = tocitem.href
|
||||||
data=root)
|
if oeb.spine.index(tocitem) > -1:
|
||||||
if self.at_start:
|
oeb.spine.remove(tocitem)
|
||||||
oeb.spine.insert(0, item, linear=True)
|
tocitem.data = root
|
||||||
else:
|
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)
|
oeb.guide.add('toc', 'Table of Contents', href)
|
||||||
|
|
||||||
@ -95,7 +115,10 @@ class TOCAdder(object):
|
|||||||
li = parent.makeelement(XHTML('li'))
|
li = parent.makeelement(XHTML('li'))
|
||||||
li.tail = '\n'+ ('\t'*level)
|
li.tail = '\n'+ ('\t'*level)
|
||||||
parent.append(li)
|
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
|
a.text = toc.title
|
||||||
li.append(a)
|
li.append(a)
|
||||||
if toc.count() > 0:
|
if toc.count() > 0:
|
||||||
@ -115,3 +138,4 @@ class TOCAdder(object):
|
|||||||
self.oeb.guide.remove('toc')
|
self.oeb.guide.remove('toc')
|
||||||
self.added_toc_guide_entry = False
|
self.added_toc_guide_entry = False
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ class PluginWidget(Widget, Ui_Form):
|
|||||||
Widget.__init__(self, parent,
|
Widget.__init__(self, parent,
|
||||||
['dont_split_on_page_breaks', 'flow_size',
|
['dont_split_on_page_breaks', 'flow_size',
|
||||||
'no_default_epub_cover', 'no_svg_cover',
|
'no_default_epub_cover', 'no_svg_cover',
|
||||||
|
'epub_inline_toc', 'epub_toc_at_end', 'toc_title',
|
||||||
'preserve_cover_aspect_ratio', 'epub_flatten']
|
'preserve_cover_aspect_ratio', 'epub_flatten']
|
||||||
)
|
)
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>400</width>
|
<width>644</width>
|
||||||
<height>300</height>
|
<height>300</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -14,27 +14,6 @@
|
|||||||
<string>Form</string>
|
<string>Form</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<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 &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 &cover</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QCheckBox" name="opt_no_svg_cover">
|
|
||||||
<property name="text">
|
|
||||||
<string>No &SVG cover</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
|
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -42,7 +21,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Split files &larger than:</string>
|
<string>Split files &larger than:</string>
|
||||||
@ -52,7 +31,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="QSpinBox" name="opt_flow_size">
|
<widget class="QSpinBox" name="opt_flow_size">
|
||||||
<property name="suffix">
|
<property name="suffix">
|
||||||
<string> KB</string>
|
<string> KB</string>
|
||||||
@ -68,7 +47,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="6" column="0">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
@ -81,6 +60,41 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_no_default_epub_cover">
|
||||||
|
<property name="text">
|
||||||
|
<string>No default &cover</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_no_svg_cover">
|
||||||
|
<property name="text">
|
||||||
|
<string>No &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 &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 &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 &end of the book</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QCheckBox" name="opt_epub_flatten">
|
<widget class="QCheckBox" name="opt_epub_flatten">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -88,6 +102,19 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>&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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user