mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Use a wizard for XPath input
This commit is contained in:
parent
8d4966f14b
commit
790b1a2b37
@ -3,7 +3,11 @@
|
|||||||
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from calibre.utils.serialize import json_dumps, json_loads
|
from calibre.utils.serialize import json_dumps, json_loads
|
||||||
|
from calibre.ebooks.oeb.base import XPath
|
||||||
|
from css_selectors.select import get_parsed_selector
|
||||||
|
|
||||||
|
|
||||||
class Action:
|
class Action:
|
||||||
@ -43,20 +47,46 @@ ACTION_MAP = {a.name: a for a in (
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
def non_empty_validator(label, val):
|
||||||
|
if not val:
|
||||||
|
return _('{} must not be empty').format(label)
|
||||||
|
|
||||||
|
|
||||||
|
def always_valid(*a):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def validate_css_selector(val):
|
||||||
|
try:
|
||||||
|
get_parsed_selector(val)
|
||||||
|
except Exception:
|
||||||
|
return _('{} is not a valid CSS selector').format(val)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_xpath_selector(val):
|
||||||
|
try:
|
||||||
|
XPath(val)
|
||||||
|
except Exception:
|
||||||
|
return _('{} is not a valid XPath selector').format(val)
|
||||||
|
|
||||||
|
|
||||||
class Match:
|
class Match:
|
||||||
|
|
||||||
def __init__(self, name, text, placeholder=''):
|
def __init__(self, name, text, placeholder='', validator=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.text = text
|
self.text = text
|
||||||
self.placeholder = placeholder
|
self.placeholder = placeholder
|
||||||
|
if validator is None and placeholder:
|
||||||
|
validator = partial(non_empty_validator, self.placeholder)
|
||||||
|
self.validator = validator or always_valid
|
||||||
|
|
||||||
|
|
||||||
MATCH_TYPE_MAP = {m.name: m for m in (
|
MATCH_TYPE_MAP = {m.name: m for m in (
|
||||||
Match('is', _('is'), _('Tag name')),
|
Match('is', _('is'), _('Tag name')),
|
||||||
Match('has_class', _('has class'), _('Class name')),
|
Match('has_class', _('has class'), _('Class name')),
|
||||||
Match('not_has_class', _('does not have class'), _('Class name')),
|
Match('not_has_class', _('does not have class'), _('Class name')),
|
||||||
Match('css', _('matches CSS selector'), _('CSS selector')),
|
Match('css', _('matches CSS selector'), _('CSS selector'), validate_css_selector),
|
||||||
Match('xpath', _('matches XPath selector'), _('XPath selector')),
|
Match('xpath', _('matches XPath selector'), _('XPath selector'), validate_xpath_selector),
|
||||||
Match('*', _('is any tag')),
|
Match('*', _('is any tag')),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -6,14 +6,14 @@ import os
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from qt.core import (
|
from qt.core import (
|
||||||
QApplication, QBrush, QByteArray, QDialog, QDialogButtonBox, Qt, QTextCursor,
|
QApplication, QBrush, QByteArray, QDialog, QDialogButtonBox, Qt, QTextCursor,
|
||||||
QTextEdit, QWidget, pyqtSignal
|
QTextEdit, pyqtSignal
|
||||||
)
|
)
|
||||||
|
|
||||||
from calibre.constants import iswindows
|
from calibre.constants import iswindows
|
||||||
from calibre.ebooks.conversion.search_replace import compile_regular_expression
|
from calibre.ebooks.conversion.search_replace import compile_regular_expression
|
||||||
from calibre.gui2 import choose_files, error_dialog, gprefs
|
from calibre.gui2 import choose_files, error_dialog, gprefs
|
||||||
from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder
|
from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder
|
||||||
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
|
from calibre.gui2.convert.xpath_wizard import XPathEdit
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.ptempfile import TemporaryFile
|
from calibre.ptempfile import TemporaryFile
|
||||||
from calibre.utils.icu import utf16_length
|
from calibre.utils.icu import utf16_length
|
||||||
@ -212,20 +212,19 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
return str(self.preview.toPlainText())
|
return str(self.preview.toPlainText())
|
||||||
|
|
||||||
|
|
||||||
class RegexEdit(QWidget, Ui_Edit):
|
class RegexEdit(XPathEdit):
|
||||||
|
|
||||||
doc_update = pyqtSignal(str)
|
doc_update = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.setupUi(self)
|
|
||||||
self.edit.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
|
self.edit.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
|
||||||
|
|
||||||
self.book_id = None
|
self.book_id = None
|
||||||
self.db = None
|
self.db = None
|
||||||
self.doc_cache = None
|
self.doc_cache = None
|
||||||
|
|
||||||
self.button.clicked.connect(self.builder)
|
def wizard(self):
|
||||||
|
return self.builder()
|
||||||
|
|
||||||
def builder(self):
|
def builder(self):
|
||||||
if self.db is None:
|
if self.db is None:
|
||||||
@ -244,7 +243,7 @@ class RegexEdit(QWidget, Ui_Edit):
|
|||||||
return self.doc_cache
|
return self.doc_cache
|
||||||
|
|
||||||
def setObjectName(self, *args):
|
def setObjectName(self, *args):
|
||||||
QWidget.setObjectName(self, *args)
|
super().setObjectName(*args)
|
||||||
if hasattr(self, 'edit'):
|
if hasattr(self, 'edit'):
|
||||||
self.edit.initialize('regex_edit_'+str(self.objectName()))
|
self.edit.initialize('regex_edit_'+str(self.objectName()))
|
||||||
|
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>Form</class>
|
|
||||||
<widget class="QWidget" name="Form">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>430</width>
|
|
||||||
<height>74</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Form</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="msg">
|
|
||||||
<property name="text">
|
|
||||||
<string>TextLabel</string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>edit</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="HistoryLineEdit" name="edit">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
|
||||||
<horstretch>100</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>350</width>
|
|
||||||
<height>0</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="sizeAdjustPolicy">
|
|
||||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
|
||||||
</property>
|
|
||||||
<property name="minimumContentsLength">
|
|
||||||
<number>30</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QToolButton" name="button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Use a wizard to help construct the Regular expression</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<customwidgets>
|
|
||||||
<customwidget>
|
|
||||||
<class>HistoryLineEdit</class>
|
|
||||||
<extends>QComboBox</extends>
|
|
||||||
<header>calibre/gui2/widgets.h</header>
|
|
||||||
</customwidget>
|
|
||||||
</customwidgets>
|
|
||||||
<resources>
|
|
||||||
<include location="../../../../resources/images.qrc"/>
|
|
||||||
</resources>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
@ -6,10 +6,13 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from qt.core import QDialog, QWidget, Qt, QDialogButtonBox, QVBoxLayout
|
from qt.core import (
|
||||||
|
QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QIcon, QLabel, QSize, Qt,
|
||||||
|
QToolButton, QVBoxLayout, QWidget
|
||||||
|
)
|
||||||
|
|
||||||
from calibre.gui2.convert.xpath_wizard_ui import Ui_Form
|
from calibre.gui2.convert.xpath_wizard_ui import Ui_Form
|
||||||
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
|
from calibre.gui2.widgets import HistoryLineEdit
|
||||||
from calibre.utils.localization import localize_user_manual_link
|
from calibre.utils.localization import localize_user_manual_link
|
||||||
|
|
||||||
|
|
||||||
@ -65,12 +68,37 @@ class Wizard(QDialog):
|
|||||||
return self.widget.xpath
|
return self.widget.xpath
|
||||||
|
|
||||||
|
|
||||||
class XPathEdit(QWidget, Ui_Edit):
|
class XPathEdit(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None, object_name='', show_msg=True):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.setupUi(self)
|
self.h = h = QHBoxLayout(self)
|
||||||
self.button.clicked.connect(self.wizard)
|
h.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.l = l = QVBoxLayout()
|
||||||
|
h.addLayout(l)
|
||||||
|
self.button = b = QToolButton(self)
|
||||||
|
b.setIcon(QIcon(I('wizard.png')))
|
||||||
|
b.setToolTip(_('Use a wizard to generate the XPath expression'))
|
||||||
|
b.clicked.connect(self.wizard)
|
||||||
|
h.addWidget(b)
|
||||||
|
self.edit = e = HistoryLineEdit(self)
|
||||||
|
e.setMinimumWidth(350)
|
||||||
|
e.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
e.setMinimumContentsLength(30)
|
||||||
|
self.msg = QLabel('')
|
||||||
|
l.addWidget(self.msg)
|
||||||
|
l.addWidget(self.edit)
|
||||||
|
if object_name:
|
||||||
|
self.setObjectName(object_name)
|
||||||
|
if show_msg:
|
||||||
|
b.setIconSize(QSize(40, 40))
|
||||||
|
self.msg.setBuddy(self.edit)
|
||||||
|
else:
|
||||||
|
self.msg.setVisible(False)
|
||||||
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
def setPlaceholderText(self, val):
|
||||||
|
self.edit.setPlaceholderText(val)
|
||||||
|
|
||||||
def wizard(self):
|
def wizard(self):
|
||||||
wiz = Wizard(self)
|
wiz = Wizard(self)
|
||||||
@ -89,6 +117,11 @@ class XPathEdit(QWidget, Ui_Edit):
|
|||||||
def text(self):
|
def text(self):
|
||||||
return str(self.edit.text())
|
return str(self.edit.text())
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, val):
|
||||||
|
self.edit.setText(str(val))
|
||||||
|
value = text
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def xpath(self):
|
def xpath(self):
|
||||||
return self.text
|
return self.text
|
||||||
|
@ -102,7 +102,11 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="attribute"/>
|
<widget class="QLineEdit" name="attribute">
|
||||||
|
<property name="clearButtonEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_3">
|
<widget class="QLabel" name="label_3">
|
||||||
|
@ -15,13 +15,13 @@ from calibre.ebooks.html_transform_rules import (
|
|||||||
validate_rule
|
validate_rule
|
||||||
)
|
)
|
||||||
from calibre.gui2 import choose_files, choose_save_file, elided_text, error_dialog
|
from calibre.gui2 import choose_files, choose_save_file, elided_text, error_dialog
|
||||||
|
from calibre.gui2.convert.xpath_wizard import XPathEdit
|
||||||
from calibre.gui2.tag_mapper import (
|
from calibre.gui2.tag_mapper import (
|
||||||
RuleEditDialog as RuleEditDialogBase, RuleItem as RuleItemBase,
|
RuleEditDialog as RuleEditDialogBase, RuleItem as RuleItemBase,
|
||||||
Rules as RulesBase, RulesDialog as RulesDialogBase, SaveLoadMixin
|
Rules as RulesBase, RulesDialog as RulesDialogBase, SaveLoadMixin
|
||||||
)
|
)
|
||||||
from calibre.gui2.widgets2 import Dialog
|
from calibre.gui2.widgets2 import Dialog
|
||||||
from calibre.utils.config import JSONConfig
|
from calibre.utils.config import JSONConfig
|
||||||
from calibre.utils.localization import localize_user_manual_link
|
|
||||||
|
|
||||||
|
|
||||||
class TagAction(QWidget):
|
class TagAction(QWidget):
|
||||||
@ -138,6 +138,44 @@ class ActionsContainer(QScrollArea):
|
|||||||
self.new_action().as_dict = entry
|
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): # {{{
|
class RuleEdit(QWidget): # {{{
|
||||||
|
|
||||||
MSG = _('Create the rule to transform HTML tags below')
|
MSG = _('Create the rule to transform HTML tags below')
|
||||||
@ -152,7 +190,7 @@ class RuleEdit(QWidget): # {{{
|
|||||||
l.addWidget(la)
|
l.addWidget(la)
|
||||||
l.addLayout(h)
|
l.addLayout(h)
|
||||||
english_sentence = '{preamble} {match_type}'
|
english_sentence = '{preamble} {match_type}'
|
||||||
sentence = _('{preamble} {match_type} {query}')
|
sentence = _('{preamble} {match_type}')
|
||||||
if set(sentence.split()) != set(english_sentence.split()):
|
if set(sentence.split()) != set(english_sentence.split()):
|
||||||
sentence = english_sentence
|
sentence = english_sentence
|
||||||
parts = sentence.split()
|
parts = sentence.split()
|
||||||
@ -168,15 +206,10 @@ class RuleEdit(QWidget): # {{{
|
|||||||
if clause is not parts[-1]:
|
if clause is not parts[-1]:
|
||||||
h.addWidget(QLabel('\xa0'))
|
h.addWidget(QLabel('\xa0'))
|
||||||
h.addStretch(1)
|
h.addStretch(1)
|
||||||
self.hl = h = QHBoxLayout()
|
self.generic_query = gq = GenericEdit(self)
|
||||||
l.addLayout(h)
|
self.css_query = cq = CSSEdit(self)
|
||||||
self.query = q = QLineEdit(self)
|
self.xpath_query = xq = XPathEdit(self, object_name='html_transform_rules_xpath', show_msg=False)
|
||||||
q.setClearButtonEnabled(True)
|
l.addWidget(gq), l.addWidget(cq), l.addWidget(xq)
|
||||||
h.addWidget(q)
|
|
||||||
h.addSpacing(20)
|
|
||||||
self.query_help_label = la = QLabel(self)
|
|
||||||
la.setOpenExternalLinks(True)
|
|
||||||
h.addWidget(la)
|
|
||||||
|
|
||||||
self.thenl = QLabel(_('Then:'))
|
self.thenl = QLabel(_('Then:'))
|
||||||
l.addWidget(self.thenl)
|
l.addWidget(self.thenl)
|
||||||
@ -193,28 +226,29 @@ class RuleEdit(QWidget): # {{{
|
|||||||
a.setWidth(a.width() + 125)
|
a.setWidth(a.width() + 125)
|
||||||
return a
|
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):
|
def update_state(self):
|
||||||
r = self.rule
|
r = self.rule
|
||||||
mt = r['match_type']
|
mt = r['match_type']
|
||||||
self.query.setVisible(mt != '*')
|
self.generic_query.setVisible(False), self.css_query.setVisible(False), self.xpath_query.setVisible(False)
|
||||||
self.query.setPlaceholderText(MATCH_TYPE_MAP[mt].placeholder)
|
self.current_query_widget.setVisible(True)
|
||||||
self.query_help_label.setVisible(mt in ('css', 'xpath'))
|
self.current_query_widget.setPlaceholderText(MATCH_TYPE_MAP[mt].placeholder)
|
||||||
if self.query_help_label.isVisible():
|
|
||||||
if mt == 'css':
|
|
||||||
url = 'https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors'
|
|
||||||
text = _('CSS selector help')
|
|
||||||
else:
|
|
||||||
url = localize_user_manual_link('https://manual.calibre-ebook.com/xpath.html')
|
|
||||||
text = _('XPath selector help')
|
|
||||||
self.query_help_label.setText(f'<a href="{url}">{text}</a>')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rule(self):
|
def rule(self):
|
||||||
return {
|
try:
|
||||||
'match_type': self.match_type.currentData(),
|
return {
|
||||||
'query': self.query.text().strip(),
|
'match_type': self.match_type.currentData(),
|
||||||
'actions': self.actions.as_list,
|
'query': self.current_query_widget.value,
|
||||||
}
|
'actions': self.actions.as_list,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
@rule.setter
|
@rule.setter
|
||||||
def rule(self, rule):
|
def rule(self, rule):
|
||||||
@ -222,7 +256,7 @@ class RuleEdit(QWidget): # {{{
|
|||||||
c = getattr(self, name)
|
c = getattr(self, name)
|
||||||
c.setCurrentIndex(max(0, c.findData(str(rule.get(name, '')))))
|
c.setCurrentIndex(max(0, c.findData(str(rule.get(name, '')))))
|
||||||
sc('match_type')
|
sc('match_type')
|
||||||
self.query.setText(str(rule.get('query', '')).strip())
|
self.current_query_widget.value = str(rule.get('query', '')).strip()
|
||||||
self.actions.as_list = rule.get('actions') or []
|
self.actions.as_list = rule.get('actions') or []
|
||||||
self.update_state()
|
self.update_state()
|
||||||
|
|
||||||
@ -419,7 +453,7 @@ if __name__ == '__main__':
|
|||||||
app = Application([])
|
app = Application([])
|
||||||
d = RulesDialog()
|
d = RulesDialog()
|
||||||
d.rules = [
|
d.rules = [
|
||||||
{'match_type':'*', 'query':'', 'actions':[{'type': 'remove'}]},
|
{'match_type':'xpath', 'query':'//h:h2', 'actions':[{'type': 'remove'}]},
|
||||||
]
|
]
|
||||||
d.exec_()
|
d.exec_()
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
Loading…
x
Reference in New Issue
Block a user