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>
|
||||
|
||||
|
||||
from functools import partial
|
||||
|
||||
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:
|
||||
@ -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:
|
||||
|
||||
def __init__(self, name, text, placeholder=''):
|
||||
def __init__(self, name, text, placeholder='', validator=None):
|
||||
self.name = name
|
||||
self.text = text
|
||||
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('is', _('is'), _('Tag name')),
|
||||
Match('has_class', _('has class'), _('Class name')),
|
||||
Match('not_has_class', _('does not have class'), _('Class name')),
|
||||
Match('css', _('matches CSS selector'), _('CSS selector')),
|
||||
Match('xpath', _('matches XPath selector'), _('XPath selector')),
|
||||
Match('css', _('matches CSS selector'), _('CSS selector'), validate_css_selector),
|
||||
Match('xpath', _('matches XPath selector'), _('XPath selector'), validate_xpath_selector),
|
||||
Match('*', _('is any tag')),
|
||||
)}
|
||||
|
||||
|
@ -6,14 +6,14 @@ import os
|
||||
from contextlib import suppress
|
||||
from qt.core import (
|
||||
QApplication, QBrush, QByteArray, QDialog, QDialogButtonBox, Qt, QTextCursor,
|
||||
QTextEdit, QWidget, pyqtSignal
|
||||
QTextEdit, pyqtSignal
|
||||
)
|
||||
|
||||
from calibre.constants import iswindows
|
||||
from calibre.ebooks.conversion.search_replace import compile_regular_expression
|
||||
from calibre.gui2 import choose_files, error_dialog, gprefs
|
||||
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.ptempfile import TemporaryFile
|
||||
from calibre.utils.icu import utf16_length
|
||||
@ -212,20 +212,19 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
||||
return str(self.preview.toPlainText())
|
||||
|
||||
|
||||
class RegexEdit(QWidget, Ui_Edit):
|
||||
class RegexEdit(XPathEdit):
|
||||
|
||||
doc_update = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
super().__init__(parent)
|
||||
self.edit.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
|
||||
|
||||
self.book_id = None
|
||||
self.db = None
|
||||
self.doc_cache = None
|
||||
|
||||
self.button.clicked.connect(self.builder)
|
||||
def wizard(self):
|
||||
return self.builder()
|
||||
|
||||
def builder(self):
|
||||
if self.db is None:
|
||||
@ -244,7 +243,7 @@ class RegexEdit(QWidget, Ui_Edit):
|
||||
return self.doc_cache
|
||||
|
||||
def setObjectName(self, *args):
|
||||
QWidget.setObjectName(self, *args)
|
||||
super().setObjectName(*args)
|
||||
if hasattr(self, 'edit'):
|
||||
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>'
|
||||
__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.xexp_edit_ui import Ui_Form as Ui_Edit
|
||||
from calibre.gui2.widgets import HistoryLineEdit
|
||||
from calibre.utils.localization import localize_user_manual_link
|
||||
|
||||
|
||||
@ -65,12 +68,37 @@ class Wizard(QDialog):
|
||||
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)
|
||||
self.setupUi(self)
|
||||
self.button.clicked.connect(self.wizard)
|
||||
self.h = h = QHBoxLayout(self)
|
||||
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):
|
||||
wiz = Wizard(self)
|
||||
@ -89,6 +117,11 @@ class XPathEdit(QWidget, Ui_Edit):
|
||||
def text(self):
|
||||
return str(self.edit.text())
|
||||
|
||||
@text.setter
|
||||
def text(self, val):
|
||||
self.edit.setText(str(val))
|
||||
value = text
|
||||
|
||||
@property
|
||||
def xpath(self):
|
||||
return self.text
|
||||
|
@ -102,7 +102,11 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="attribute"/>
|
||||
<widget class="QLineEdit" name="attribute">
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
|
@ -15,13 +15,13 @@ from calibre.ebooks.html_transform_rules import (
|
||||
validate_rule
|
||||
)
|
||||
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 (
|
||||
RuleEditDialog as RuleEditDialogBase, RuleItem as RuleItemBase,
|
||||
Rules as RulesBase, RulesDialog as RulesDialogBase, SaveLoadMixin
|
||||
)
|
||||
from calibre.gui2.widgets2 import Dialog
|
||||
from calibre.utils.config import JSONConfig
|
||||
from calibre.utils.localization import localize_user_manual_link
|
||||
|
||||
|
||||
class TagAction(QWidget):
|
||||
@ -138,6 +138,44 @@ class ActionsContainer(QScrollArea):
|
||||
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')
|
||||
@ -152,7 +190,7 @@ class RuleEdit(QWidget): # {{{
|
||||
l.addWidget(la)
|
||||
l.addLayout(h)
|
||||
english_sentence = '{preamble} {match_type}'
|
||||
sentence = _('{preamble} {match_type} {query}')
|
||||
sentence = _('{preamble} {match_type}')
|
||||
if set(sentence.split()) != set(english_sentence.split()):
|
||||
sentence = english_sentence
|
||||
parts = sentence.split()
|
||||
@ -168,15 +206,10 @@ class RuleEdit(QWidget): # {{{
|
||||
if clause is not parts[-1]:
|
||||
h.addWidget(QLabel('\xa0'))
|
||||
h.addStretch(1)
|
||||
self.hl = h = QHBoxLayout()
|
||||
l.addLayout(h)
|
||||
self.query = q = QLineEdit(self)
|
||||
q.setClearButtonEnabled(True)
|
||||
h.addWidget(q)
|
||||
h.addSpacing(20)
|
||||
self.query_help_label = la = QLabel(self)
|
||||
la.setOpenExternalLinks(True)
|
||||
h.addWidget(la)
|
||||
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)
|
||||
@ -193,28 +226,29 @@ class RuleEdit(QWidget): # {{{
|
||||
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.query.setVisible(mt != '*')
|
||||
self.query.setPlaceholderText(MATCH_TYPE_MAP[mt].placeholder)
|
||||
self.query_help_label.setVisible(mt in ('css', 'xpath'))
|
||||
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>')
|
||||
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):
|
||||
return {
|
||||
'match_type': self.match_type.currentData(),
|
||||
'query': self.query.text().strip(),
|
||||
'actions': self.actions.as_list,
|
||||
}
|
||||
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):
|
||||
@ -222,7 +256,7 @@ class RuleEdit(QWidget): # {{{
|
||||
c = getattr(self, name)
|
||||
c.setCurrentIndex(max(0, c.findData(str(rule.get(name, '')))))
|
||||
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.update_state()
|
||||
|
||||
@ -419,7 +453,7 @@ if __name__ == '__main__':
|
||||
app = Application([])
|
||||
d = RulesDialog()
|
||||
d.rules = [
|
||||
{'match_type':'*', 'query':'', 'actions':[{'type': 'remove'}]},
|
||||
{'match_type':'xpath', 'query':'//h:h2', 'actions':[{'type': 'remove'}]},
|
||||
]
|
||||
d.exec_()
|
||||
from pprint import pprint
|
||||
|
Loading…
x
Reference in New Issue
Block a user