From 4ad268224fb067e3fe4f07812cdfa6ff325605b2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 24 Mar 2021 16:12:02 +0530 Subject: [PATCH] Edit book: When right clicking on a class in a HTML file, add an option to rename the class throughout the book --- manual/edit.rst | 4 ++ src/calibre/ebooks/oeb/polish/css.py | 53 ++++++++++++++++++++ src/calibre/gui2/tweak_book/boss.py | 20 +++++++- src/calibre/gui2/tweak_book/editor/widget.py | 10 +++- 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/manual/edit.rst b/manual/edit.rst index 3f5a5c3cff..547aa5c195 100644 --- a/manual/edit.rst +++ b/manual/edit.rst @@ -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 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: Auto-complete diff --git a/src/calibre/ebooks/oeb/polish/css.py b/src/calibre/ebooks/oeb/polish/css.py index 78a9edde78..d0ba2ca32f 100644 --- a/src/calibre/ebooks/oeb/polish/css.py +++ b/src/calibre/ebooks/oeb/polish/css.py @@ -5,6 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +import re from collections import defaultdict from functools import partial from operator import itemgetter @@ -463,3 +464,55 @@ def add_stylesheet_links(container, name, text): head.append(link) pretty_xml_tree(head) 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 diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index cf46022f86..04a7d15d9b 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -25,7 +25,7 @@ from calibre.ebooks.oeb.polish.container import ( from calibre.ebooks.oeb.polish.cover import ( 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.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.replace import ( @@ -956,6 +956,22 @@ class Boss(QObject): else: 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): self.commit_all_editors_to_container() c = current_container() @@ -1609,6 +1625,8 @@ class Boss(QObject): editor.link_clicked.connect(self.editor_link_clicked) if hasattr(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': editor.smart_highlighting_updated.connect(self.gui.live_css.sync_to_editor) if hasattr(editor, 'set_request_completion'): diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index 0e2e8aeb7a..a0cb8225d7 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -23,7 +23,8 @@ from calibre.gui2.tweak_book import ( editors, tprefs, update_mark_text_action ) 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.text import TextEdit @@ -142,6 +143,7 @@ class Editor(QMainWindow): word_ignored = pyqtSignal(object, object) link_clicked = pyqtSignal(object) class_clicked = pyqtSignal(object) + rename_class = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, syntax, parent=None): @@ -579,6 +581,12 @@ class Editor(QMainWindow): href = self.editor.text_for_range(origc.block(), origr) 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)): word = self.editor.text_for_range(origc.block(), origr) item_type = 'tag_name' if origr.format.property(TAG_NAME_PROPERTY) else 'css_property'