Edit book: When right clicking on a class in a HTML file, add an option to rename the class throughout the book

This commit is contained in:
Kovid Goyal 2021-03-24 16:12:02 +05:30
parent 1d6694b04f
commit 4ad268224f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 85 additions and 2 deletions

View File

@ -824,6 +824,10 @@ You can also hold down the :kbd:`Ctrl` key and click on any filename inside a li
to open that file in the editor automatically. Similarly, :kbd:`Ctrl` clicking to open that file in the editor automatically. Similarly, :kbd:`Ctrl` clicking
a class name will take you to the first style rule that matches the tag and class. a class name will take you to the first style rule that matches the tag and class.
Right clicking a class name in an HTML file will allow you to rename the class,
changing all occurrences of the class throughout the book and all its
stylesheets.
.. _editor_auto_complete: .. _editor_auto_complete:
Auto-complete Auto-complete

View File

@ -5,6 +5,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from collections import defaultdict from collections import defaultdict
from functools import partial from functools import partial
from operator import itemgetter from operator import itemgetter
@ -463,3 +464,55 @@ def add_stylesheet_links(container, name, text):
head.append(link) head.append(link)
pretty_xml_tree(head) pretty_xml_tree(head)
return serialize(root, 'text/html') return serialize(root, 'text/html')
def rename_class_in_rule_list(css_rules, old_name, new_name):
pat = re.compile(rf'\b{re.escape(old_name)}\b')
changed = False
for rule in css_rules:
if rule.type == rule.STYLE_RULE:
old = rule.selectorText
q = pat.sub(new_name, old)
if q != old:
changed = True
rule.selectorText = q
elif hasattr(rule, 'cssRules'):
if rename_class_in_rule_list(rule.cssRules, old_name, new_name):
changed = True
return changed
def rename_class_in_doc(container, root, old_name, new_name):
changed = False
pat = re.compile(rf'\b{re.escape(old_name)}\b')
for elem in root.xpath('//*[@class]'):
old = elem.get('class')
if old:
new = pat.sub(new_name, old)
if new != old:
changed = True
elem.set('class', new)
for style in root.xpath('//*[local-name()="style"]'):
if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(style.text)
if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
changed = True
style.text = force_unicode(sheet.cssText, 'utf-8')
return changed
def rename_class(container, old_name, new_name):
changed = False
if not old_name or old_name == new_name:
return changed
for sheet_name in container.manifest_items_of_type(lambda mt: mt in OEB_STYLES):
sheet = container.parsed(sheet_name)
if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
container.dirty(sheet_name)
changed = True
for doc_name in container.manifest_items_of_type(lambda mt: mt in OEB_DOCS):
doc = container.parsed(doc_name)
if rename_class_in_doc(container, doc, old_name, new_name):
container.dirty(doc_name)
changed = True
return changed

View File

@ -25,7 +25,7 @@ from calibre.ebooks.oeb.polish.container import (
from calibre.ebooks.oeb.polish.cover import ( from calibre.ebooks.oeb.polish.cover import (
mark_as_cover, mark_as_titlepage, set_cover mark_as_cover, mark_as_titlepage, set_cover
) )
from calibre.ebooks.oeb.polish.css import filter_css from calibre.ebooks.oeb.polish.css import filter_css, rename_class
from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish
from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all
from calibre.ebooks.oeb.polish.replace import ( from calibre.ebooks.oeb.polish.replace import (
@ -956,6 +956,22 @@ class Boss(QObject):
else: else:
ed.action_triggered(action) ed.action_triggered(action)
def rename_class(self, class_name):
self.commit_all_editors_to_container()
text, ok = QInputDialog.getText(self.gui, _('New class name'), _(
'Rename the class {} to?').format(class_name))
if ok:
self.add_savepoint(_('Before: Rename {}').format(class_name))
with BusyCursor():
changed = rename_class(current_container(), class_name, text.strip())
if changed:
self.apply_container_update_to_gui()
self.show_current_diff()
else:
self.rewind_savepoint()
return info_dialog(self.gui, _('No matches'), _(
'No class {} found to change').format(class_name), show=True)
def set_semantics(self): def set_semantics(self):
self.commit_all_editors_to_container() self.commit_all_editors_to_container()
c = current_container() c = current_container()
@ -1609,6 +1625,8 @@ class Boss(QObject):
editor.link_clicked.connect(self.editor_link_clicked) editor.link_clicked.connect(self.editor_link_clicked)
if hasattr(editor, 'class_clicked'): if hasattr(editor, 'class_clicked'):
editor.class_clicked.connect(self.editor_class_clicked) editor.class_clicked.connect(self.editor_class_clicked)
if hasattr(editor, 'rename_class'):
editor.rename_class.connect(self.rename_class)
if getattr(editor, 'syntax', None) == 'html': if getattr(editor, 'syntax', None) == 'html':
editor.smart_highlighting_updated.connect(self.gui.live_css.sync_to_editor) editor.smart_highlighting_updated.connect(self.gui.live_css.sync_to_editor)
if hasattr(editor, 'set_request_completion'): if hasattr(editor, 'set_request_completion'):

View File

@ -23,7 +23,8 @@ from calibre.gui2.tweak_book import (
editors, tprefs, update_mark_text_action editors, tprefs, update_mark_text_action
) )
from calibre.gui2.tweak_book.editor import ( from calibre.gui2.tweak_book.editor import (
CSS_PROPERTY, LINK_PROPERTY, SPELL_PROPERTY, TAG_NAME_PROPERTY CLASS_ATTRIBUTE_PROPERTY, CSS_PROPERTY, LINK_PROPERTY, SPELL_PROPERTY,
TAG_NAME_PROPERTY
) )
from calibre.gui2.tweak_book.editor.help import help_url from calibre.gui2.tweak_book.editor.help import help_url
from calibre.gui2.tweak_book.editor.text import TextEdit from calibre.gui2.tweak_book.editor.text import TextEdit
@ -142,6 +143,7 @@ class Editor(QMainWindow):
word_ignored = pyqtSignal(object, object) word_ignored = pyqtSignal(object, object)
link_clicked = pyqtSignal(object) link_clicked = pyqtSignal(object)
class_clicked = pyqtSignal(object) class_clicked = pyqtSignal(object)
rename_class = pyqtSignal(object)
smart_highlighting_updated = pyqtSignal() smart_highlighting_updated = pyqtSignal()
def __init__(self, syntax, parent=None): def __init__(self, syntax, parent=None):
@ -579,6 +581,12 @@ class Editor(QMainWindow):
href = self.editor.text_for_range(origc.block(), origr) href = self.editor.text_for_range(origc.block(), origr)
m.addAction(_('Open %s') % href, partial(self.link_clicked.emit, href)) m.addAction(_('Open %s') % href, partial(self.link_clicked.emit, href))
if origr is not None and origr.format.property(CLASS_ATTRIBUTE_PROPERTY):
cls = self.editor.class_for_position(pos)
if cls:
class_name = cls['class']
m.addAction(_('Rename the class {}').format(class_name), partial(self.rename_class.emit, class_name))
if origr is not None and (origr.format.property(TAG_NAME_PROPERTY) or origr.format.property(CSS_PROPERTY)): if origr is not None and (origr.format.property(TAG_NAME_PROPERTY) or origr.format.property(CSS_PROPERTY)):
word = self.editor.text_for_range(origc.block(), origr) word = self.editor.text_for_range(origc.block(), origr)
item_type = 'tag_name' if origr.format.property(TAG_NAME_PROPERTY) else 'css_property' item_type = 'tag_name' if origr.format.property(TAG_NAME_PROPERTY) else 'css_property'