%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/ |
Current File : //lib/calibre/calibre/gui2/html_transform_rules.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> from qt.core import ( QComboBox, QFrame, QHBoxLayout, QIcon, QLabel, QLineEdit, QPushButton, QScrollArea, Qt, QToolButton, QVBoxLayout, QWidget, pyqtSignal ) from calibre import prepare_string_for_xml from calibre.ebooks.html_transform_rules import ( ACTION_MAP, MATCH_TYPE_MAP, export_rules, import_rules, transform_html, validate_rule ) from calibre.gui2 import elided_text, error_dialog from calibre.gui2.convert.xpath_wizard import XPathEdit from calibre.gui2.css_transform_rules import ( RulesWidget as RulesWidgetBase, Tester as TesterBase ) from calibre.gui2.tag_mapper import ( RuleEditDialog as RuleEditDialogBase, RuleItem as RuleItemBase, Rules as RulesBase, RulesDialog as RulesDialogBase ) # Classes for rule edit widget {{{ class TagAction(QWidget): remove_action = pyqtSignal(object) def __init__(self, parent=None): super().__init__(parent) self.l = l = QVBoxLayout(self) self.h = h = QHBoxLayout() l.addLayout(h) english_sentence = '{action_type} {action_data}' sentence = _('{action_type} {action_data}') if set(sentence.split()) != set(english_sentence.split()): sentence = english_sentence parts = sentence.split() for clause in parts: if clause == '{action_data}': self.action_data = w = QLineEdit(self) w.setClearButtonEnabled(True) elif clause == '{action_type}': self.action_type = w = QComboBox(self) for action, ac in ACTION_MAP.items(): w.addItem(ac.short_text, action) w.currentIndexChanged.connect(self.update_state) h.addWidget(w) if clause is not parts[-1]: h.addWidget(QLabel('\xa0')) self.h2 = h = QHBoxLayout() l.addLayout(h) self.remove_button = b = QToolButton(self) b.setToolTip(_('Remove this action')), b.setIcon(QIcon(I('minus.png'))) b.clicked.connect(self.request_remove) h.addWidget(b) self.action_desc = la = QLabel('') la.setWordWrap(True) la.setTextFormat(Qt.TextFormat.RichText) h.addWidget(la) self.sep = sep = QFrame(self) sep.setFrameShape(QFrame.Shape.HLine) l.addWidget(sep) self.update_state() def request_remove(self): self.remove_action.emit(self) @property def as_dict(self): return {'type': self.action_type.currentData(), 'data': self.action_data.text()} @as_dict.setter def as_dict(self, val): self.action_data.setText(val.get('data') or '') at = val.get('type') if at: idx = self.action_type.findData(at) if idx > -1: self.action_type.setCurrentIndex(idx) def update_state(self): val = self.as_dict ac = ACTION_MAP[val['type']] self.action_desc.setText(ac.long_text) if ac.placeholder: self.action_data.setVisible(True) self.action_data.setPlaceholderText(ac.placeholder) else: self.action_data.setVisible(False) class ActionsContainer(QScrollArea): def __init__(self, parent=None): super().__init__(parent) self.setWidgetResizable(True) self.w = w = QWidget() self.setWidget(w) w.l = QVBoxLayout(w) w.l.addStretch(1) self.all_actions = [] self.new_action() def new_action(self): a = TagAction(self) self.all_actions.append(a) l = self.w.l a.remove_action.connect(self.remove_action) l.insertWidget(l.count() - 1, a) a.action_type.setFocus(Qt.FocusReason.OtherFocusReason) return a def remove_action(self, ac): if ac in self.all_actions: self.w.l.removeWidget(ac) del self.all_actions[self.all_actions.index(ac)] ac.deleteLater() def sizeHint(self): ans = super().sizeHint() ans.setHeight(ans.height() + 200) return ans @property def as_list(self): return [t.as_dict for t in self.all_actions] @as_list.setter def as_list(self, val): for ac in tuple(self.all_actions): self.remove_action(ac) for entry in val: self.new_action().as_dict = entry class GenericEdit(QLineEdit): def __init__(self, parent=None): super().__init__(parent) self.setClearButtonEnabled(True) @property def value(self): return self.text() @value.setter def value(self, val): self.setText(str(val)) class CSSEdit(QWidget): def __init__(self, parent=None): super().__init__(parent) l = QHBoxLayout(self) l.setContentsMargins(0, 0, 0, 0) self.edit = le = GenericEdit(self) l.addWidget(le) l.addSpacing(5) self.la = la = QLabel(_('<a href="{}">CSS selector help</a>').format('https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors')) la.setOpenExternalLinks(True) l.addWidget(la) self.setPlaceholderText = self.edit.setPlaceholderText @property def value(self): return self.edit.value @value.setter def value(self, val): self.edit.value = val # }}} class RuleEdit(QWidget): # {{{ MSG = _('Create the rule to transform HTML tags below') def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout(self) self.h = h = QHBoxLayout() self.la = la = QLabel(self.MSG) la.setWordWrap(True) l.addWidget(la) l.addLayout(h) english_sentence = '{preamble} {match_type}' sentence = _('{preamble} {match_type}') if set(sentence.split()) != set(english_sentence.split()): sentence = english_sentence parts = sentence.split() for clause in parts: if clause == '{preamble}': self.preamble = w = QLabel(_('If the tag')) elif clause == '{match_type}': self.match_type = w = QComboBox(self) for action, m in MATCH_TYPE_MAP.items(): w.addItem(m.text, action) w.currentIndexChanged.connect(self.update_state) h.addWidget(w) if clause is not parts[-1]: h.addWidget(QLabel('\xa0')) h.addStretch(1) self.generic_query = gq = GenericEdit(self) self.css_query = cq = CSSEdit(self) self.xpath_query = xq = XPathEdit(self, object_name='html_transform_rules_xpath', show_msg=False) l.addWidget(gq), l.addWidget(cq), l.addWidget(xq) self.thenl = QLabel(_('Then:')) l.addWidget(self.thenl) self.actions = a = ActionsContainer(self) l.addWidget(a) self.add_button = b = QPushButton(QIcon(I('plus.png')), _('Add another action')) b.clicked.connect(self.actions.new_action) l.addWidget(b) self.update_state() def sizeHint(self): a = QWidget.sizeHint(self) a.setHeight(a.height() + 375) a.setWidth(a.width() + 125) return a @property def current_query_widget(self): return {'css': self.css_query, 'xpath': self.xpath_query}.get(self.match_type.currentData(), self.generic_query) def update_state(self): r = self.rule mt = r['match_type'] self.generic_query.setVisible(False), self.css_query.setVisible(False), self.xpath_query.setVisible(False) self.current_query_widget.setVisible(True) self.current_query_widget.setPlaceholderText(MATCH_TYPE_MAP[mt].placeholder) @property def rule(self): try: return { 'match_type': self.match_type.currentData(), 'query': self.current_query_widget.value, 'actions': self.actions.as_list, } except Exception: import traceback traceback.print_exc() raise @rule.setter def rule(self, rule): def sc(name): c = getattr(self, name) c.setCurrentIndex(max(0, c.findData(str(rule.get(name, ''))))) sc('match_type') self.current_query_widget.value = str(rule.get('query', '')).strip() self.actions.as_list = rule.get('actions') or [] self.update_state() def validate(self): rule = self.rule title, msg = validate_rule(rule) if msg is not None and title is not None: error_dialog(self, title, msg, show=True) return False return True # }}} class RuleEditDialog(RuleEditDialogBase): # {{{ PREFS_NAME = 'edit-html-transform-rule' DIALOG_TITLE = _('Edit rule') RuleEditClass = RuleEdit # }}} class RuleItem(RuleItemBase): # {{{ @staticmethod def text_from_rule(rule, parent): try: query = elided_text(rule['query'], font=parent.font(), width=200, pos='right') text = _('If the tag <b>{match_type}</b> <b>{query}</b>').format( match_type=MATCH_TYPE_MAP[rule['match_type']].text, query=prepare_string_for_xml(query)) for action in rule['actions']: text += '<br>' + ACTION_MAP[action['type']].short_text if action.get('data'): ad = elided_text(action['data'], font=parent.font(), width=200, pos='right') text += f' <code>{prepare_string_for_xml(ad)}</code>' except Exception: import traceback traceback.print_exc() text = _('This rule is invalid, please remove it') return text # }}} class Rules(RulesBase): # {{{ RuleItemClass = RuleItem RuleEditDialogClass = RuleEditDialog ACTION_KEY = 'actions' MSG = _('You can specify rules to transform HTML here. Click the "Add rule" button' ' below to get started.') # }}} class Tester(TesterBase): # {{{ DIALOG_TITLE = _('Test HTML transform rules') PREFS_NAME = 'test-html-transform-rules' LABEL = _('Enter an HTML document below and click the "Test" button') SYNTAX = 'html' RESULTS = '<!-- %s -->\n\n' % _('Resulting HTML') def compile_rules(self, rules): return rules def do_test(self): changed, html = transform_html('\n' + self.value + '\n', self.rules) self.set_result(html) # }}} class RulesDialog(RulesDialogBase): # {{{ DIALOG_TITLE = _('Edit HTML transform rules') PREFS_NAME = 'edit-html-transform-rules' PREFS_OBJECT_NAME = 'html-transform-rules' RulesClass = Rules TesterClass = Tester def extra_bottom_widget(self): self.scope_cb = cb = QComboBox() cb.addItem(_('Current HTML file'), 'current') cb.addItem(_('All HTML files'), 'all') cb.addItem(_('Open HTML files'), 'open') cb.addItem(_('Selected HTML files'), 'selected') return cb @property def transform_scope(self): return self.scope_cb.currentData() @transform_scope.setter def transform_scope(self, val): idx = self.scope_cb.findData(val) self.scope_cb.setCurrentIndex(max(0, idx)) # }}} class HtmlRulesWidget(RulesWidgetBase): # {{{ PREFS_NAME = 'html-transform-rules' INITIAL_FILE_NAME = 'html-rules.txt' DIR_SAVE_NAME = 'export-html-transform-rules' export_func = export_rules import_func = import_rules TesterClass = Tester RulesClass = Rules # }}} if __name__ == '__main__': from calibre.gui2 import Application app = Application([]) d = RulesDialog() d.rules = [ {'match_type':'xpath', 'query':'//h:h2', 'actions':[{'type': 'remove'}]}, ] d.exec() from pprint import pprint pprint(d.rules) del d, app