From 61efa239ec72e7a5512085feec8ec04653fde39c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 9 Feb 2021 12:28:55 +0530 Subject: [PATCH] Edit book: Add a tool to split the tag at the current cursor position, creating a new tag with the same style and class attributes. To add the tool go to the Toolbars section in the editor preferences. Fixes #1912958 [Feature: Merge/split paragraphs tool in ebook editor](https://bugs.launchpad.net/calibre/+bug/1912958) --- imgsrc/split.svg | 54 +++++++++ resources/images/split.png | Bin 0 -> 1383 bytes .../gui2/tweak_book/editor/smarts/html.py | 103 +++++++++++++----- src/calibre/gui2/tweak_book/editor/text.py | 4 + src/calibre/gui2/tweak_book/editor/widget.py | 3 + 5 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 imgsrc/split.svg create mode 100644 resources/images/split.png diff --git a/imgsrc/split.svg b/imgsrc/split.svg new file mode 100644 index 0000000000..2f7880876d --- /dev/null +++ b/imgsrc/split.svg @@ -0,0 +1,54 @@ + +image/svg+xml diff --git a/resources/images/split.png b/resources/images/split.png new file mode 100644 index 0000000000000000000000000000000000000000..65cf26c8d8c6baecaa53009352738dbb4e6da2d2 GIT binary patch literal 1383 zcmZuxYfuwc6uu!q5DEge1C>^UAOf54Xuu#taUr`2kphWGeF9ZM6fKfsC6+R!v?5gc zfNDn`5+*+w3}lmtZwtgK)|LUGAutUh7{r7@nZeQp04Uvp54PfP3toamSt6R>g0`Tnauy;>^t#Mn zD?wR}vHsV%>s4`Z8>m2;7h@0^&xhc6H=;o@K10PpWN1KR6hq1Nl}80CqWv#$5f#t3 z0S_QJ3r%+QSt zC1wV>3|Jx{XzoJ5d6ug<^dUh4hr=LHt0dQdYnM%wxFjVAsa%&!rzaG}+{?MP-$=?!2tNzJXAx zE?l{Ky{Wn7_MP9_+B>^68f}lR?|%Qlz~Dpu(4%34arDX4@rkMFnb~J^^9zg5O-nDR zcTQUXU{S}%BDaclW0fNbt0Em+bAvKY*I~kNHPz}{sXp(NtzTD->ygQC*wV=o+N9bo zT(IKy*K_w4_w=?O_H#QXKXygAn0Jod-=dTMIZ|D*&bb)#ER7CMzBJ#x1D97DBoi6k zQh749;o@WWCVdDR`mM5(Hw8~4L_Y?&(XKt zq6yAKZm7K`HmOojwgI_nIaoN<>)31$`ZiKNq@v@_=?iX)U{Hg_0l5n()K!JQFb0A_ z*YWpCC?HGp`}PISivDuRJv+w^qev3qH(nn<8ZndgMG>+3%a$>QK{tI%xrfG9>o%2& zGRw}b7e5G|jel(2`N#f=li4x5G7@U7eqER=pr0^OM>oHHIgc4Sb;$&@^vxzQKAE-F z0nd5anki0d|DoZsLr=JXHyJGCx{XmBYEyKF)MffSti9!C*!JX{CzLY(Us6|=-$=mI zJ6bWX;~;;RX4b^Y-5onbMN{ZbU9y!EcA(s+i(r_jAj}y2xwXyh=@-owQf7+DLaeC^ z_{z$6{`wom1xXPgb!C0pRI{Wby;?~-@UkyHNZ8N%bf&;i=_DLZOPkNVJ3V%`NLOh0 z9n}PQX9`mD4z^wBtzFmZP}AgdI(7Q+Aw#C7TW#O@q{b(tA_|}`Y;_1PZKF`b>u0;_ ztf#`l#S9IQCnj literal 0 HcmV?d00001 diff --git a/src/calibre/gui2/tweak_book/editor/smarts/html.py b/src/calibre/gui2/tweak_book/editor/smarts/html.py index 6ca371b4cd..340c2c1666 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/html.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/html.py @@ -5,23 +5,27 @@ __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -import sys, re -from operator import itemgetter -from itertools import chain - +import re +import sys +from contextlib import contextmanager from css_parser import parseStyle -from PyQt5.Qt import QTextEdit, Qt, QTextCursor +from itertools import chain +from operator import itemgetter +from PyQt5.Qt import Qt, QTextCursor, QTextEdit from calibre import prepare_string_for_xml, xml_entity_to_unicode -from calibre.ebooks.oeb.polish.container import OEB_DOCS from calibre.ebooks.oeb.base import css_text +from calibre.ebooks.oeb.polish.container import OEB_DOCS from calibre.gui2 import error_dialog -from calibre.gui2.tweak_book.editor.syntax.html import ATTR_NAME, ATTR_END, ATTR_START, ATTR_VALUE -from calibre.gui2.tweak_book import tprefs, current_container +from calibre.gui2.tweak_book import current_container, tprefs from calibre.gui2.tweak_book.editor.smarts import NullSmarts from calibre.gui2.tweak_book.editor.smarts.utils import ( - no_modifiers, get_leading_whitespace_on_block, get_text_before_cursor, - get_text_after_cursor, smart_home, smart_backspace, smart_tab, expand_tabs) + expand_tabs, get_leading_whitespace_on_block, get_text_after_cursor, + get_text_before_cursor, no_modifiers, smart_backspace, smart_home, smart_tab +) +from calibre.gui2.tweak_book.editor.syntax.html import ( + ATTR_END, ATTR_NAME, ATTR_START, ATTR_VALUE +) from calibre.utils.icu import utf16_length from polyglot.builtins import unicode_type @@ -214,24 +218,52 @@ def find_closing_tag(tag, max_tags=sys.maxsize): def select_tag(cursor, tag): cursor.setPosition(tag.start_block.position() + tag.start_offset) cursor.setPosition(tag.end_block.position() + tag.end_offset + 1, QTextCursor.MoveMode.KeepAnchor) - return unicode_type(cursor.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') + return cursor.selectedText().replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') + + +@contextmanager +def edit_block(cursor): + cursor.beginEditBlock() + try: + yield + finally: + cursor.endEditBlock() def rename_tag(cursor, opening_tag, closing_tag, new_name, insert=False): - cursor.beginEditBlock() - text = select_tag(cursor, closing_tag) - if insert: - text = '%s' % (new_name, text) - else: - text = re.sub(r'^<\s*/\s*[a-zA-Z0-9]+', '' % new_name - else: - text = re.sub(r'^<\s*[a-zA-Z0-9]+', '<%s' % new_name, text) - cursor.insertText(text) - cursor.endEditBlock() + with edit_block(cursor): + text = select_tag(cursor, closing_tag) + if insert: + text = '%s' % (new_name, text) + else: + text = re.sub(r'^<\s*/\s*[a-zA-Z0-9]+', '' % new_name + else: + text = re.sub(r'^<\s*[a-zA-Z0-9]+', '<%s' % new_name, text) + cursor.insertText(text) + + +def split_tag(cursor, opening_tag, closing_tag): + pos = cursor.position() + with edit_block(cursor): + open_text = select_tag(cursor, opening_tag) + open_text = re.sub(r'''\bid\s*=\s*['"].*?['"]''', '', open_text) + open_text = re.sub(r'\s+', ' ', open_text) + tag_name = re.search(r'<\s*(\S+)', open_text).group(1).lower() + is_block = tag_name in BLOCK_TAG_NAMES + prefix = '' + if is_block: + cursor.setPosition(cursor.anchor()) + cursor.movePosition(QTextCursor.MoveOperation.StartOfLine, QTextCursor.MoveMode.KeepAnchor) + x = cursor.selectedText() + if x and not x.strip(): + prefix = PARAGRAPH_SEPARATOR + x + close_text = select_tag(cursor, closing_tag) + cursor.setPosition(pos) + cursor.insertText(f'{close_text}{prefix}{open_text}') def ensure_not_within_tag_definition(cursor, forward=True): @@ -397,9 +429,28 @@ class Smarts(NullSmarts): ' before trying to rename tags.') % tag.name, show=True) rename_tag(c, tag, closing_tag, new_name, insert=tag.name in {'body', 'td', 'th', 'li'}) else: - return error_dialog(editor, _('No found'), _( + return error_dialog(editor, _('No tag found'), _( 'No suitable block level tag was found to rename'), show=True) + def split_tag(self, editor): + editor.highlighter.join() + c = editor.textCursor() + block, offset = c.block(), c.positionInBlock() + tag, closing = find_tag_definition(block, offset) + if tag is not None: + return error_dialog(editor, _('Cursor inside tag'), _( + 'Cannot split as the cursor is inside the tag definition'), show=True) + tag = find_closest_containing_tag(block, offset) + if tag is None: + return error_dialog(editor, _('No tag found'), _( + 'No suitable tag was found to split'), show=True) + closing_tag = find_closing_tag(tag) + if closing_tag is None: + return error_dialog(editor, _('Invalid HTML'), _( + 'There is an unclosed %s tag. You should run the Fix HTML tool' + ' before trying to split tags.') % tag.name, show=True) + split_tag(c, tag, closing_tag) + def get_smart_selection(self, editor, update=True): editor.highlighter.join() cursor = editor.textCursor() diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index deb9fb51c8..2248e2b6ba 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -900,6 +900,10 @@ version="1.1" width="100%%" height="100%%" viewBox="0 0 {w} {h}" preserveAspectR if hasattr(self.smarts, 'remove_tag'): self.smarts.remove_tag(self) + def split_tag(self): + if hasattr(self.smarts, 'split_tag'): + self.smarts.split_tag(self) + def keyPressEvent(self, ev): if ev.key() == Qt.Key.Key_X and ev.modifiers() == Qt.KeyboardModifier.AltModifier: if self.replace_possible_unicode_sequence(): diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index a9c2b951cd..77f8740d4b 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -120,6 +120,9 @@ def register_text_editor_actions(_reg, palette): ac = reg('trash.png', _('Remove &tag'), ('remove_tag',), 'remove-tag', ('Ctrl+>'), _('Remove tag'), syntaxes=('html', 'xml')) ac.setToolTip(_('

Remove tag

Remove the currently highlighted tag')) + ac = reg('split.png', _('&Split tag'), ('split_tag',), 'split-tag', ('Ctrl+Alt+>'), _('Split current tag'), syntaxes=('html', 'xml')) + ac.setToolTip(_('

Split tag

Split the current tag at the cursor position')) + editor_toolbar_actions['html']['fix-html-current'] = actions['fix-html-current'] for s in ('xml', 'html', 'css'): editor_toolbar_actions[s]['pretty-current'] = actions['pretty-current']