diff --git a/manual/edit.rst b/manual/edit.rst index 3475b54a3d..bc92fb7ff4 100644 --- a/manual/edit.rst +++ b/manual/edit.rst @@ -433,9 +433,12 @@ the location you want, simply click and the split will be performed. Splitting the file will automatically update all links and references that pointed into the bottom half of the file and will open the newly split file in -an editor. If you want to repeatedly split a file, you can do that, and then -use the bulk renaming facility of the Files Browser, described above, to give -the split off files sensible names. +an editor. + +You can also split a single HTML file at multiple locations automatically, by +right clicking inside the file in the editor and choosing :guilabel:`Split at +multiple locations`. This will allow you to easily split a large file at all +heading tags or all tags having a certain class and so on. Miscellaneous Tools diff --git a/src/calibre/ebooks/oeb/polish/split.py b/src/calibre/ebooks/oeb/polish/split.py index 114133deca..a6d4124498 100644 --- a/src/calibre/ebooks/oeb/polish/split.py +++ b/src/calibre/ebooks/oeb/polish/split.py @@ -222,6 +222,34 @@ def split(container, name, loc_or_xpath, before=True): container.dirty(container.opf_name) return bottom_name +def multisplit(container, name, xpath, before=True): + root = container.parsed(name) + nodes = root.xpath(xpath, namespaces=XPNSMAP) + if not nodes: + raise AbortError(_('The expression %s did not match any nodes') % xpath) + for split_point in nodes: + if in_table(split_point): + raise AbortError('Cannot split inside tables') + if split_point.tag.endswith('}body'): + raise AbortError('Cannot split on the tag') + + for i, tag in enumerate(nodes): + tag.set('calibre-split-point', str(i)) + + current = name + all_names = [name] + for i in xrange(len(nodes)): + current = split(container, current, '//*[@calibre-split-point="%d"]' % i, before=before) + all_names.append(current) + + for x in all_names: + for tag in container.parsed(x).xpath('//*[@calibre-split-point]'): + tag.attrib.pop('calibre-split-point') + container.dirty(x) + + return all_names[1:] + + class MergeLinkReplacer(object): def __init__(self, base, anchor_map, master, container): diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 29f7cf821b..e114806730 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -22,7 +22,7 @@ from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_cont from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_recommended_folders, rationalize_folders -from calibre.ebooks.oeb.polish.split import split, merge, AbortError +from calibre.ebooks.oeb.polish.split import split, merge, AbortError, multisplit from calibre.ebooks.oeb.polish.toc import remove_names_from_toc, find_existing_toc from calibre.ebooks.oeb.polish.utils import link_stylesheets from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog, choose_save_file @@ -36,7 +36,7 @@ from calibre.gui2.tweak_book.toc import TOCEditor from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook from calibre.gui2.tweak_book.preferences import Preferences -from calibre.gui2.tweak_book.widgets import RationalizeFolders +from calibre.gui2.tweak_book.widgets import RationalizeFolders, MultiSplit def get_container(*args, **kwargs): kwargs['tweak_mode'] = True @@ -821,6 +821,29 @@ class Boss(QObject): self.apply_container_update_to_gui() self.edit_file(bottom_name, 'html') + def multisplit(self): + ed = self.gui.central.current_editor + if ed.syntax != 'html': + return + name = None + for n, x in editors.iteritems(): + if ed is x: + name = n + break + if name is None: + return + d = MultiSplit(self.gui) + if d.exec_() == d.Accepted: + with BusyCursor(): + self.commit_all_editors_to_container() + self.add_savepoint(_('Split %s') % self.gui.elided_text(name)) + try: + multisplit(current_container(), name, d.xpath) + except AbortError: + self.rewind_savepoint() + raise + self.apply_container_update_to_gui() + @in_thread_job def link_clicked(self, name, anchor): if not name: diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index b5f1b95997..f2be7e4119 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -268,6 +268,8 @@ class Editor(QMainWindow): m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) + if self.syntax == 'html': + m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos)) diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 3fa558a137..2bec7db44d 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -403,6 +403,9 @@ class Main(MainWindow): self.action_browse_images = reg( 'view-image.png', _('&Browse images in book'), self.boss.browse_images, 'browse-images', (), _( 'Browse images in the books visually')) + self.action_multiple_split = reg( + 'auto_author_sort.png', _('&Split at multiple locations'), self.boss.multisplit, 'multisplit', (), _( + 'Split HTML file at multiple locations')) def create_menubar(self): p, q = self.create_application_menubar() diff --git a/src/calibre/gui2/tweak_book/widgets.py b/src/calibre/gui2/tweak_book/widgets.py index c7a9ba0033..e2dc4824aa 100644 --- a/src/calibre/gui2/tweak_book/widgets.py +++ b/src/calibre/gui2/tweak_book/widgets.py @@ -6,8 +6,10 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -from PyQt4.Qt import (QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit) +from PyQt4.Qt import ( + QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit, QVBoxLayout) +from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import tprefs class Dialog(QDialog): @@ -46,7 +48,7 @@ class Dialog(QDialog): def setup_ui(self): raise NotImplementedError('You must implement this method in Dialog subclasses') -class RationalizeFolders(Dialog): +class RationalizeFolders(Dialog): # {{{ TYPE_MAP = ( ('text', _('Text (HTML) files')), @@ -103,3 +105,38 @@ class RationalizeFolders(Dialog): def accept(self): tprefs['folders_for_types'] = self.folder_map return Dialog.accept(self) +# }}} + +class MultiSplit(Dialog): # {{{ + + def __init__(self, parent=None): + Dialog.__init__(self, _('Specify locations to split at'), 'multisplit-xpath', parent=parent) + + def setup_ui(self): + from calibre.gui2.convert.xpath_wizard import XPathEdit + self.l = l = QVBoxLayout(self) + self.setLayout(l) + + self.la = la = QLabel(_( + 'Specify the locations to split at, using an XPath expression (click' + ' the wizard button for help with generating XPath expressions).')) + la.setWordWrap(True) + l.addWidget(la) + + self._xpath = xp = XPathEdit(self) + xp.set_msg(_('&XPath expression:')) + xp.setObjectName('editor-multisplit-xpath-edit') + l.addWidget(xp) + l.addWidget(self.bb) + + def accept(self): + if not self._xpath.check(): + return error_dialog(self, _('Invalid XPath expression'), _( + 'The XPath expression %s is invalid.') % self.xpath) + return Dialog.accept(self) + + @property + def xpath(self): + return self._xpath.xpath + +# }}}