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

View File

@ -475,6 +475,31 @@
</item> </item>
</layout> </layout>
</widget> </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> </widget>
</item> </item>
</layout> </layout>
@ -495,6 +520,12 @@
<extends>QPlainTextEdit</extends> <extends>QPlainTextEdit</extends>
<header>calibre/gui2/tweak_book/editor/text.h</header> <header>calibre/gui2/tweak_book/editor/text.h</header>
</customwidget> </customwidget>
<customwidget>
<class>RulesWidget</class>
<extends>QWidget</extends>
<header>calibre/gui2/css_transform_rules.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>

View File

@ -6,15 +6,16 @@ from __future__ import (unicode_literals, division, absolute_import,
print_function) print_function)
from PyQt5.Qt import ( 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 ( from calibre.ebooks.css_transform_rules import (
validate_rule, safe_parser, compile_rules, transform_sheet, ACTION_MAP, MATCH_TYPE_MAP) 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 from calibre.gui2 import error_dialog, elided_text, choose_save_file, choose_files
from calibre.gui2.tag_mapper import ( from calibre.gui2.tag_mapper import (
RuleEditDialog as RuleEditDialogBase, Rules as RulesBase, RulesDialog as 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.gui2.widgets2 import Dialog
from calibre.utils.config import JSONConfig from calibre.utils.config import JSONConfig
@ -152,14 +153,19 @@ class RuleItem(RuleItemBase): # {{{
@staticmethod @staticmethod
def text_from_rule(rule, parent): def text_from_rule(rule, parent):
query = elided_text(rule['query'], font=parent.font(), width=200, pos='right') try:
text = _( query = elided_text(rule['query'], font=parent.font(), width=200, pos='right')
'If the property <i>{property}</i> <b>{match_type}</b> <b>{query}</b><br>{action}').format( text = _(
property=rule['property'], action=ACTION_MAP[rule['action']], 'If the property <i>{property}</i> <b>{match_type}</b> <b>{query}</b><br>{action}').format(
match_type=MATCH_TYPE_MAP[rule['match_type']], query=query) property=rule['property'], action=ACTION_MAP[rule['action']],
if rule['action_data']: match_type=MATCH_TYPE_MAP[rule['match_type']], query=query)
ad = elided_text(rule['action_data'], font=parent.font(), width=200, pos='right') if rule['action_data']:
text += ' <code>%s</code>' % ad 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 return text
# }}} # }}}
@ -218,7 +224,7 @@ class Tester(Dialog): # {{{
return QSize(800, 600) return QSize(800, 600)
# }}} # }}}
class RulesDialog(RulesDialogBase): class RulesDialog(RulesDialogBase): # {{{
DIALOG_TITLE = _('Edit style transform rules') DIALOG_TITLE = _('Edit style transform rules')
PREFS_NAME = 'edit-style-transform-rules' PREFS_NAME = 'edit-style-transform-rules'
@ -230,6 +236,85 @@ class RulesDialog(RulesDialogBase):
# multiple processes # multiple processes
self.PREFS_OBJECT = JSONConfig('style-transform-rules') self.PREFS_OBJECT = JSONConfig('style-transform-rules')
RulesDialogBase.__init__(self, *args, **kw) 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__': if __name__ == '__main__':
from calibre.gui2 import Application from calibre.gui2 import Application

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt5.Qt import ( from PyQt5.Qt import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QIcon, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QIcon,
QSize, QComboBox, QLineEdit, QListWidgetItem, QStyledItemDelegate, 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 from calibre.ebooks.metadata.tag_mapper import map_tags, compile_pat
@ -194,6 +194,7 @@ class Rules(QWidget):
RuleItemClass = RuleItem RuleItemClass = RuleItem
RuleEditDialogClass = RuleEditDialog RuleEditDialogClass = RuleEditDialog
changed = pyqtSignal()
MSG = _('You can specify rules to filter/transform tags here. Click the "Add Rule" button' 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' ' 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: if d.exec_() == d.Accepted:
i = self.RuleItemClass(d.edit_widget.rule, self.rule_list) i = self.RuleItemClass(d.edit_widget.rule, self.rule_list)
self.rule_list.scrollToItem(i) self.rule_list.scrollToItem(i)
self.changed.emit()
def edit_rule(self): def edit_rule(self):
i = self.rule_list.currentItem() i = self.rule_list.currentItem()
@ -259,10 +261,15 @@ class Rules(QWidget):
rule = d.edit_widget.rule rule = d.edit_widget.rule
i.setData(DATA_ROLE, rule) i.setData(DATA_ROLE, rule)
i.setData(RENDER_ROLE, self.RuleItemClass.text_from_rule(rule, self.rule_list)) i.setData(RENDER_ROLE, self.RuleItemClass.text_from_rule(rule, self.rule_list))
self.changed.emit()
def remove_rules(self): def remove_rules(self):
changed = False
for item in self.rule_list.selectedItems(): for item in self.rule_list.selectedItems():
self.rule_list.takeItem(self.rule_list.row(item)) self.rule_list.takeItem(self.rule_list.row(item))
changed = True
if changed:
self.changed.emit()
def move_up(self): def move_up(self):
i = self.rule_list.currentItem() i = self.rule_list.currentItem()
@ -272,6 +279,7 @@ class Rules(QWidget):
self.rule_list.takeItem(row) self.rule_list.takeItem(row)
self.rule_list.insertItem(row - 1, i) self.rule_list.insertItem(row - 1, i)
self.rule_list.setCurrentItem(i) self.rule_list.setCurrentItem(i)
self.changed.emit()
def move_down(self): def move_down(self):
i = self.rule_list.currentItem() i = self.rule_list.currentItem()
@ -281,6 +289,7 @@ class Rules(QWidget):
self.rule_list.takeItem(row) self.rule_list.takeItem(row)
self.rule_list.insertItem(row + 1, i) self.rule_list.insertItem(row + 1, i)
self.rule_list.setCurrentItem(i) self.rule_list.setCurrentItem(i)
self.changed.emit()
@property @property
def rules(self): def rules(self):
@ -342,41 +351,7 @@ class Tester(Dialog):
ans.setWidth(ans.width() + 150) ans.setWidth(ans.width() + 150)
return ans return ans
class RulesDialog(Dialog): class SaveLoadMixin(object):
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 save_ruleset(self): def save_ruleset(self):
if not self.rules: if not self.rules:
@ -418,6 +393,42 @@ class RulesDialog(Dialog):
del self.PREFS_OBJECT[name] del self.PREFS_OBJECT[name]
self.build_load_menu() 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): def test_rules(self):
self.TesterClass(self.rules, self).exec_() self.TesterClass(self.rules, self).exec_()