Conversion: Implement style transformation via rules. Look under the "Transform Styles" tab of the Look & Feel section of the conversion dialog

This commit is contained in:
Kovid Goyal 2016-03-10 16:40:16 +05:30
parent 941f395ca3
commit fa0533ec0d
4 changed files with 187 additions and 50 deletions

View File

@ -6,6 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import json
from PyQt5.Qt import Qt, QSize
from calibre.gui2.convert.look_and_feel_ui import Ui_Form
@ -40,7 +42,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
'insert_blank_line_size',
'input_encoding', 'filter_css', 'expand_css',
'asciiize', 'keep_ligatures',
'linearize_tables']
'linearize_tables', 'transform_css_rules']
)
for val, text in [
('original', _('Original')),
@ -80,6 +82,8 @@ class LookAndFeelWidget(Widget, Ui_Form):
val = unicode(g.text()).strip()
val = [x.strip() for x in val.split(',' if ',' in val else ' ') if x.strip()]
return ', '.join(val) or None
if g is self.opt_transform_css_rules:
return json.dumps(g.rules)
return Widget.get_value_handler(self, g)
def set_value_handler(self, g, val):
@ -106,6 +110,9 @@ class LookAndFeelWidget(Widget, Ui_Form):
if g is self.opt_extra_css:
g.load_text(val or '', 'css')
return True
if g is self.opt_transform_css_rules:
g.rules = json.loads(val) if val else []
return True
def connect_gui_obj_handler(self, gui_obj, slot):
if gui_obj is self.opt_filter_css:
@ -114,6 +121,9 @@ class LookAndFeelWidget(Widget, Ui_Form):
w.stateChanged.connect(slot)
self.filter_css_others.textChanged.connect(slot)
return
if gui_obj is self.opt_transform_css_rules:
gui_obj.changed.connect(slot)
return
raise NotImplementedError()
def font_key_wizard(self):

View File

@ -475,6 +475,31 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>Transform styles</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="RulesWidget" name="opt_transform_css_rules" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@ -495,6 +520,12 @@
<extends>QPlainTextEdit</extends>
<header>calibre/gui2/tweak_book/editor/text.h</header>
</customwidget>
<customwidget>
<class>RulesWidget</class>
<extends>QWidget</extends>
<header>calibre/gui2/css_transform_rules.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>

View File

@ -6,15 +6,16 @@ from __future__ import (unicode_literals, division, absolute_import,
print_function)
from PyQt5.Qt import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit, QPushButton, QSize
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit,
QPushButton, QSize, pyqtSignal, QMenu
)
from calibre.ebooks.css_transform_rules import (
validate_rule, safe_parser, compile_rules, transform_sheet, ACTION_MAP, MATCH_TYPE_MAP)
from calibre.gui2 import error_dialog, elided_text
validate_rule, safe_parser, compile_rules, transform_sheet, ACTION_MAP, MATCH_TYPE_MAP, export_rules, import_rules)
from calibre.gui2 import error_dialog, elided_text, choose_save_file, choose_files
from calibre.gui2.tag_mapper import (
RuleEditDialog as RuleEditDialogBase, Rules as RulesBase, RulesDialog as
RulesDialogBase, RuleItem as RuleItemBase)
RulesDialogBase, RuleItem as RuleItemBase, SaveLoadMixin)
from calibre.gui2.widgets2 import Dialog
from calibre.utils.config import JSONConfig
@ -152,6 +153,7 @@ 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 property <i>{property}</i> <b>{match_type}</b> <b>{query}</b><br>{action}').format(
@ -160,6 +162,10 @@ class RuleItem(RuleItemBase): # {{{
if rule['action_data']:
ad = elided_text(rule['action_data'], font=parent.font(), width=200, pos='right')
text += ' <code>%s</code>' % ad
except Exception:
import traceback
traceback.print_exc()
text = _('This rule is invalid, please remove it')
return text
# }}}
@ -218,7 +224,7 @@ class Tester(Dialog): # {{{
return QSize(800, 600)
# }}}
class RulesDialog(RulesDialogBase):
class RulesDialog(RulesDialogBase): # {{{
DIALOG_TITLE = _('Edit style transform rules')
PREFS_NAME = 'edit-style-transform-rules'
@ -230,6 +236,85 @@ class RulesDialog(RulesDialogBase):
# multiple processes
self.PREFS_OBJECT = JSONConfig('style-transform-rules')
RulesDialogBase.__init__(self, *args, **kw)
# }}}
class RulesWidget(QWidget, SaveLoadMixin): # {{{
changed = pyqtSignal()
def __init__(self, parent=None):
self.loaded_ruleset = None
QWidget.__init__(self, parent)
self.PREFS_OBJECT = JSONConfig('style-transform-rules')
l = QVBoxLayout(self)
self.rules_widget = w = Rules(self)
w.changed.connect(self.changed.emit)
l.addWidget(w)
self.h = h = QHBoxLayout()
l.addLayout(h)
self.export_button = b = QPushButton(_('E&xport'), self)
b.setToolTip(_('Export these rules to a file'))
b.clicked.connect(self.export_rules)
h.addWidget(b)
self.import_button = b = QPushButton(_('&Import'), self)
b.setToolTip(_('Import previously exported rules'))
b.clicked.connect(self.import_rules)
h.addWidget(b)
self.test_button = b = QPushButton(_('&Test rules'), self)
b.clicked.connect(self.test_rules)
h.addWidget(b)
h.addStretch(10)
self.save_button = b = QPushButton(_('&Save'), self)
b.setToolTip(_('Save this ruleset for later re-use'))
b.clicked.connect(self.save_ruleset)
h.addWidget(b)
self.export_button = b = QPushButton(_('&Load'), self)
self.load_menu = QMenu(self)
b.setMenu(self.load_menu)
b.setToolTip(_('Load a previously saved ruleset'))
b.clicked.connect(self.load_ruleset)
h.addWidget(b)
self.build_load_menu()
def export_rules(self):
rules = self.rules_widget.rules
if not rules:
return error_dialog(self, _('No rules'), _(
'There are no rules to export'), show=True)
path = choose_save_file(self, 'export-style-transform-rules', _('Choose file for exported rules'), initial_filename='rules.txt')
if path:
raw = export_rules(rules)
with open(path, 'wb') as f:
f.write(raw)
def import_rules(self):
paths = choose_files(self, 'export-style-transform-rules', _('Choose file to import rules from'), select_only_single_file=True)
if paths:
with open(paths[0], 'rb') as f:
rules = import_rules(f.read())
self.rules_widget.rules = list(rules) + list(self.rules_widget.rules)
self.changed.emit()
def load_ruleset(self, name):
SaveLoadMixin.load_ruleset(self, name)
self.changed.emit()
def test_rules(self):
Tester(self.rules_widget.rules, self).exec_()
@property
def rules(self):
return self.rules_widget.rules
@rules.setter
def rules(self, val):
try:
self.rules_widget.rules = val or []
except Exception:
import traceback
traceback.print_exc()
self.rules_widget.rules = []
# }}}
if __name__ == '__main__':
from calibre.gui2 import Application

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt5.Qt import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QIcon,
QSize, QComboBox, QLineEdit, QListWidgetItem, QStyledItemDelegate,
QStaticText, Qt, QStyle, QToolButton, QInputDialog, QMenu
QStaticText, Qt, QStyle, QToolButton, QInputDialog, QMenu, pyqtSignal
)
from calibre.ebooks.metadata.tag_mapper import map_tags, compile_pat
@ -194,6 +194,7 @@ class Rules(QWidget):
RuleItemClass = RuleItem
RuleEditDialogClass = RuleEditDialog
changed = pyqtSignal()
MSG = _('You can specify rules to filter/transform tags here. Click the "Add Rule" button'
' below to get started. The rules will be processed in order for every tag until either a'
@ -249,6 +250,7 @@ class Rules(QWidget):
if d.exec_() == d.Accepted:
i = self.RuleItemClass(d.edit_widget.rule, self.rule_list)
self.rule_list.scrollToItem(i)
self.changed.emit()
def edit_rule(self):
i = self.rule_list.currentItem()
@ -259,10 +261,15 @@ class Rules(QWidget):
rule = d.edit_widget.rule
i.setData(DATA_ROLE, rule)
i.setData(RENDER_ROLE, self.RuleItemClass.text_from_rule(rule, self.rule_list))
self.changed.emit()
def remove_rules(self):
changed = False
for item in self.rule_list.selectedItems():
self.rule_list.takeItem(self.rule_list.row(item))
changed = True
if changed:
self.changed.emit()
def move_up(self):
i = self.rule_list.currentItem()
@ -272,6 +279,7 @@ class Rules(QWidget):
self.rule_list.takeItem(row)
self.rule_list.insertItem(row - 1, i)
self.rule_list.setCurrentItem(i)
self.changed.emit()
def move_down(self):
i = self.rule_list.currentItem()
@ -281,6 +289,7 @@ class Rules(QWidget):
self.rule_list.takeItem(row)
self.rule_list.insertItem(row + 1, i)
self.rule_list.setCurrentItem(i)
self.changed.emit()
@property
def rules(self):
@ -342,41 +351,7 @@ class Tester(Dialog):
ans.setWidth(ans.width() + 150)
return ans
class RulesDialog(Dialog):
DIALOG_TITLE = _('Edit tag mapper rules')
PREFS_NAME = 'edit-tag-mapper-rules'
RulesClass = Rules
TesterClass = Tester
PREFS_OBJECT = tag_maps
def __init__(self, parent=None):
self.loaded_ruleset = None
Dialog.__init__(self, self.DIALOG_TITLE, self.PREFS_NAME, parent=parent)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.edit_widget = w = self.RulesClass(self)
l.addWidget(w)
l.addWidget(self.bb)
self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole)
b.setToolTip(_('Save this ruleset for later re-use'))
b.clicked.connect(self.save_ruleset)
self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole)
b.setToolTip(_('Load a previously saved ruleset'))
self.load_menu = QMenu(self)
b.setMenu(self.load_menu)
self.build_load_menu()
self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole)
b.clicked.connect(self.test_rules)
@property
def rules(self):
return self.edit_widget.rules
@rules.setter
def rules(self, rules):
self.edit_widget.rules = rules
class SaveLoadMixin(object):
def save_ruleset(self):
if not self.rules:
@ -418,6 +393,42 @@ class RulesDialog(Dialog):
del self.PREFS_OBJECT[name]
self.build_load_menu()
class RulesDialog(Dialog, SaveLoadMixin):
DIALOG_TITLE = _('Edit tag mapper rules')
PREFS_NAME = 'edit-tag-mapper-rules'
RulesClass = Rules
TesterClass = Tester
PREFS_OBJECT = tag_maps
def __init__(self, parent=None):
self.loaded_ruleset = None
Dialog.__init__(self, self.DIALOG_TITLE, self.PREFS_NAME, parent=parent)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.edit_widget = w = self.RulesClass(self)
l.addWidget(w)
l.addWidget(self.bb)
self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole)
b.setToolTip(_('Save this ruleset for later re-use'))
b.clicked.connect(self.save_ruleset)
self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole)
b.setToolTip(_('Load a previously saved ruleset'))
self.load_menu = QMenu(self)
b.setMenu(self.load_menu)
self.build_load_menu()
self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole)
b.clicked.connect(self.test_rules)
@property
def rules(self):
return self.edit_widget.rules
@rules.setter
def rules(self, rules):
self.edit_widget.rules = rules
def test_rules(self):
self.TesterClass(self.rules, self).exec_()