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)
This commit is contained in:
Kovid Goyal 2021-02-09 12:28:55 +05:30
parent 9d41474d9d
commit 61efa239ec
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 138 additions and 26 deletions

54
imgsrc/split.svg Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="300px"
width="300px"
fill="#000000"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 97.8 97.8"
enable-background="new 0 0 97.8 97.8"
xml:space="preserve"
id="svg4"
sodipodi:docname="split.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
id="metadata10"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs8" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1400"
id="namedview6"
showgrid="false"
inkscape:zoom="3.42"
inkscape:cx="69.444444"
inkscape:cy="150"
inkscape:window-x="0"
inkscape:window-y="40"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" /><g
id="g852"><path
d="m 19.3408,34.258128 c -0.162508,-4.4e-4 -0.314692,0.04583 -0.448514,0.148541 L 0.52457424,48.433157 c -0.13860367,0.105527 -0.25984757,0.258831 -0.29900965,0.424402 -0.0130483,0.05519 0,0.111894 0,0.169761 0,0.231446 0.11420026,0.453466 0.29900965,0.594163 L 18.913644,63.669191 c 0.13453,0.102691 0.265721,0.14854 0.427156,0.14854 0.159311,0 0.33605,-0.04795 0.469872,-0.14854 0.269053,-0.201203 0.379495,-0.579601 0.256293,-0.891244 l -3.694899,-9.273185 h 21.656811 c 0.41421,0 0.747523,-0.331166 0.747523,-0.742704 v -7.448256 c 0,-0.411537 -0.333313,-0.763924 -0.747523,-0.763924 H 16.372066 l 3.694899,-9.294406 c 0.123909,-0.310939 0.01277,-0.647607 -0.256293,-0.848803 -0.13453,-0.10095 -0.307363,-0.148101 -0.469872,-0.148541 z"
id="path839"
style="fill:#2caf45;fill-opacity:1;stroke-width:1.22124" /><path
d="m 78.459201,34.130807 c -0.162511,4.41e-4 -0.335342,0.04758 -0.469872,0.148542 -0.269061,0.201195 -0.380203,0.537863 -0.256294,0.848803 l 3.6949,9.294406 H 59.771122 c -0.41421,0 -0.747524,0.352387 -0.747524,0.763924 v 7.448256 c 0,0.411538 0.333314,0.742702 0.747524,0.742702 h 21.656813 l -3.6949,9.273186 c -0.123198,0.311643 -0.01278,0.690042 0.256294,0.891245 0.13382,0.100597 0.31056,0.14854 0.469872,0.14854 0.161438,0 0.292625,-0.04586 0.427156,-0.14854 L 97.275426,49.494163 c 0.184798,-0.140698 0.299009,-0.362717 0.299009,-0.594164 0,-0.05786 0.01305,-0.114572 0,-0.16976 -0.03916,-0.165573 -0.160413,-0.31888 -0.299009,-0.424402 L 78.907715,34.279349 c -0.13382,-0.102716 -0.286003,-0.148981 -0.448514,-0.148542 z"
id="path837"
style="fill:#2caf45;fill-opacity:1;stroke-width:1.22124" /><path
d="m 43.902273,11.748602 c -0.781685,0 -1.409615,1.11798 -1.409615,2.509715 v 69.283364 c 0,1.391734 0.627222,2.510931 1.409615,2.509716 h 9.546938 c 0.781686,0 1.430973,-1.117982 1.430973,-2.509716 V 14.258317 c 0,-1.391735 -0.649287,-2.509715 -1.430973,-2.509715 z"
id="path2"
style="fill:#2271d5;fill-opacity:1;stroke-width:1.63481" /></g></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
resources/images/split.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -5,23 +5,27 @@
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
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>%s' % (new_name, text)
else:
text = re.sub(r'^<\s*/\s*[a-zA-Z0-9]+', '</%s' % new_name, text)
cursor.insertText(text)
text = select_tag(cursor, opening_tag)
if insert:
text += '<%s>' % 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>%s' % (new_name, text)
else:
text = re.sub(r'^<\s*/\s*[a-zA-Z0-9]+', '</%s' % new_name, text)
cursor.insertText(text)
text = select_tag(cursor, opening_tag)
if insert:
text += '<%s>' % 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()

View File

@ -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():

View File

@ -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(_('<h3>Remove tag</h3>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(_('<h3>Split tag</h3>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']