mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
9d41474d9d
commit
61efa239ec
54
imgsrc/split.svg
Normal file
54
imgsrc/split.svg
Normal 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
BIN
resources/images/split.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -5,23 +5,27 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import sys, re
|
import re
|
||||||
from operator import itemgetter
|
import sys
|
||||||
from itertools import chain
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from css_parser import parseStyle
|
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 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.base import css_text
|
||||||
|
from calibre.ebooks.oeb.polish.container import OEB_DOCS
|
||||||
from calibre.gui2 import error_dialog
|
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 current_container, tprefs
|
||||||
from calibre.gui2.tweak_book import tprefs, current_container
|
|
||||||
from calibre.gui2.tweak_book.editor.smarts import NullSmarts
|
from calibre.gui2.tweak_book.editor.smarts import NullSmarts
|
||||||
from calibre.gui2.tweak_book.editor.smarts.utils import (
|
from calibre.gui2.tweak_book.editor.smarts.utils import (
|
||||||
no_modifiers, get_leading_whitespace_on_block, get_text_before_cursor,
|
expand_tabs, get_leading_whitespace_on_block, get_text_after_cursor,
|
||||||
get_text_after_cursor, smart_home, smart_backspace, smart_tab, expand_tabs)
|
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 calibre.utils.icu import utf16_length
|
||||||
from polyglot.builtins import unicode_type
|
from polyglot.builtins import unicode_type
|
||||||
|
|
||||||
@ -214,24 +218,52 @@ def find_closing_tag(tag, max_tags=sys.maxsize):
|
|||||||
def select_tag(cursor, tag):
|
def select_tag(cursor, tag):
|
||||||
cursor.setPosition(tag.start_block.position() + tag.start_offset)
|
cursor.setPosition(tag.start_block.position() + tag.start_offset)
|
||||||
cursor.setPosition(tag.end_block.position() + tag.end_offset + 1, QTextCursor.MoveMode.KeepAnchor)
|
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):
|
def rename_tag(cursor, opening_tag, closing_tag, new_name, insert=False):
|
||||||
cursor.beginEditBlock()
|
with edit_block(cursor):
|
||||||
text = select_tag(cursor, closing_tag)
|
text = select_tag(cursor, closing_tag)
|
||||||
if insert:
|
if insert:
|
||||||
text = '</%s>%s' % (new_name, text)
|
text = '</%s>%s' % (new_name, text)
|
||||||
else:
|
else:
|
||||||
text = re.sub(r'^<\s*/\s*[a-zA-Z0-9]+', '</%s' % new_name, text)
|
text = re.sub(r'^<\s*/\s*[a-zA-Z0-9]+', '</%s' % new_name, text)
|
||||||
cursor.insertText(text)
|
cursor.insertText(text)
|
||||||
text = select_tag(cursor, opening_tag)
|
text = select_tag(cursor, opening_tag)
|
||||||
if insert:
|
if insert:
|
||||||
text += '<%s>' % new_name
|
text += '<%s>' % new_name
|
||||||
else:
|
else:
|
||||||
text = re.sub(r'^<\s*[a-zA-Z0-9]+', '<%s' % new_name, text)
|
text = re.sub(r'^<\s*[a-zA-Z0-9]+', '<%s' % new_name, text)
|
||||||
cursor.insertText(text)
|
cursor.insertText(text)
|
||||||
cursor.endEditBlock()
|
|
||||||
|
|
||||||
|
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):
|
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)
|
' 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'})
|
rename_tag(c, tag, closing_tag, new_name, insert=tag.name in {'body', 'td', 'th', 'li'})
|
||||||
else:
|
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)
|
'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):
|
def get_smart_selection(self, editor, update=True):
|
||||||
editor.highlighter.join()
|
editor.highlighter.join()
|
||||||
cursor = editor.textCursor()
|
cursor = editor.textCursor()
|
||||||
|
@ -900,6 +900,10 @@ version="1.1" width="100%%" height="100%%" viewBox="0 0 {w} {h}" preserveAspectR
|
|||||||
if hasattr(self.smarts, 'remove_tag'):
|
if hasattr(self.smarts, 'remove_tag'):
|
||||||
self.smarts.remove_tag(self)
|
self.smarts.remove_tag(self)
|
||||||
|
|
||||||
|
def split_tag(self):
|
||||||
|
if hasattr(self.smarts, 'split_tag'):
|
||||||
|
self.smarts.split_tag(self)
|
||||||
|
|
||||||
def keyPressEvent(self, ev):
|
def keyPressEvent(self, ev):
|
||||||
if ev.key() == Qt.Key.Key_X and ev.modifiers() == Qt.KeyboardModifier.AltModifier:
|
if ev.key() == Qt.Key.Key_X and ev.modifiers() == Qt.KeyboardModifier.AltModifier:
|
||||||
if self.replace_possible_unicode_sequence():
|
if self.replace_possible_unicode_sequence():
|
||||||
|
@ -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 = 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.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']
|
editor_toolbar_actions['html']['fix-html-current'] = actions['fix-html-current']
|
||||||
for s in ('xml', 'html', 'css'):
|
for s in ('xml', 'html', 'css'):
|
||||||
editor_toolbar_actions[s]['pretty-current'] = actions['pretty-current']
|
editor_toolbar_actions[s]['pretty-current'] = actions['pretty-current']
|
||||||
|
Loading…
x
Reference in New Issue
Block a user