diff --git a/src/calibre/ebooks/conversion/plugins/epub_output.py b/src/calibre/ebooks/conversion/plugins/epub_output.py index f09f2560b0..5569be4304 100644 --- a/src/calibre/ebooks/conversion/plugins/epub_output.py +++ b/src/calibre/ebooks/conversion/plugins/epub_output.py @@ -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): '''%(uri.replace('"', '\\"'))) if fonts: - ans = ''' ''' - ans += (u'\n'.join(fonts)).encode('utf-8') - ans += '\n' - return ans + ans += (u'\n'.join(fonts)).encode('utf-8') + ans += '\n' + 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. ''' diff --git a/src/calibre/ebooks/mobi/writer8/toc.py b/src/calibre/ebooks/mobi/writer8/toc.py index 7bae35ae98..640e8bec5f 100644 --- a/src/calibre/ebooks/mobi/writer8/toc.py +++ b/src/calibre/ebooks/mobi/writer8/toc.py @@ -34,9 +34,17 @@ TEMPLATE = ''' ''' +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 + diff --git a/src/calibre/gui2/convert/epub_output.py b/src/calibre/gui2/convert/epub_output.py index 2fcbd751fe..5fbfd74d05 100644 --- a/src/calibre/gui2/convert/epub_output.py +++ b/src/calibre/gui2/convert/epub_output.py @@ -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): diff --git a/src/calibre/gui2/convert/epub_output.ui b/src/calibre/gui2/convert/epub_output.ui index 606ed62065..fa939d289f 100644 --- a/src/calibre/gui2/convert/epub_output.ui +++ b/src/calibre/gui2/convert/epub_output.ui @@ -6,7 +6,7 @@ 0 0 - 400 + 644 300 @@ -14,27 +14,6 @@ Form - - - - Do not &split on page breaks - - - - - - - No default &cover - - - - - - - No &SVG cover - - - @@ -42,7 +21,7 @@ - + Split files &larger than: @@ -52,7 +31,7 @@ - + KB @@ -68,7 +47,7 @@ - + Qt::Vertical @@ -81,6 +60,41 @@ + + + + No default &cover + + + + + + + No &SVG cover + + + + + + + Insert inline &Table of Contents + + + + + + + Do not &split on page breaks + + + + + + + Put inserted Table of Contents at the &end of the book + + + @@ -88,6 +102,19 @@ + + + + &Title for inserted ToC: + + + opt_toc_title + + + + + +