diff --git a/src/calibre/ebooks/css_transform_rules.py b/src/calibre/ebooks/css_transform_rules.py new file mode 100644 index 0000000000..034d967279 --- /dev/null +++ b/src/calibre/ebooks/css_transform_rules.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2016, Kovid Goyal + +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +import regex + +REGEX_FLAGS = regex.VERSION1 | regex.UNICODE + +def compile_pat(pat): + return regex.compile(pat, flags=REGEX_FLAGS) + +def parse_length(raw): + raise NotImplementedError('TODO: implement this') diff --git a/src/calibre/gui2/css_transform_rules.py b/src/calibre/gui2/css_transform_rules.py new file mode 100644 index 0000000000..3f62c8a7c6 --- /dev/null +++ b/src/calibre/gui2/css_transform_rules.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2016, Kovid Goyal + +from __future__ import (unicode_literals, division, absolute_import, + print_function) +from collections import OrderedDict + +from PyQt5.Qt import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit, QListWidgetItem +) + +from calibre.ebooks.css_transform_rules import compile_pat, parse_length +from calibre.gui2 import error_dialog, elided_text +from calibre.gui2.tag_mapper import RuleEditDialog as RuleEditDialogBase, Rules as RulesBase + +class RuleEdit(QWidget): # {{{ + + ACTION_MAP = OrderedDict(( + ('remove', _('Remove the property')), + ('append', _('Add extra properties')), + ('change', _('Change the value to')), + ('*', _('Multiply the value by')), + ('/', _('Divide the value by')), + ('+', _('Add to the value')), + ('-', _('Subtract from the value')), + )) + + MATCH_TYPE_MAP = OrderedDict(( + ('==', _('is')), + ('<', _('is less than')), + ('>', _('is greater than')), + ('<=', _('is less than or equal to')), + ('>=', _('is greater than or equal to')), + ('matches', _('matches pattern')), + ('not_matches', _('does not match pattern')) + )) + + 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} {property} {match_type} {query}' + sentence = _('{preamble} {property} {match_type} {query}') + 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 &property:')) + elif clause == '{property}': + self.property = w = QLineEdit(self) + w.setToolTip(_('The name of a CSS property, for example: font-size\n')) + elif clause == '{match_type}': + self.match_type = w = QComboBox(self) + for action, text in self.MATCH_TYPE_MAP.iteritems(): + w.addItem(text, action) + elif clause == '{query}': + self.query = w = QLineEdit(self) + h.addWidget(w) + if clause is not parts[-1]: + h.addWidget(QLabel('\xa0')) + + self.h2 = h = QHBoxLayout() + l.addLayout(h) + english_sentence = '{action} {action_data}' + sentence = _('{action} {action_data}') + if set(sentence.split()) != set(english_sentence.split()): + sentence = english_sentence + parts = sentence.split() + for clause in parts: + if clause == '{action}': + self.action = w = QComboBox(self) + for action, text in self.ACTION_MAP.iteritems(): + w.addItem(text, action) + elif clause == '{action_data}': + self.action_data = w = QLineEdit(self) + h.addWidget(w) + if clause is not parts[-1]: + h.addWidget(QLabel('\xa0')) + + self.update_state() + + def sizeHint(self): + a = QWidget.sizeHint(self) + a.setHeight(a.height() + 75) + a.setWidth(a.width() + 100) + return a + + def update_state(self): + r = self.rule + self.action_data.setVisible(r['action'] != 'remove') + tt = _('The CSS property value') + mt = r['match_type'] + if 'matches' in mt: + tt = _('A regular expression') + elif mt in '< > <= >='.split(): + tt = _('Either a CSS length, such as 10pt or a unit less number. If a unitless' + ' number is used it will compared with the CSS value using whatever unit' + ' the value has.') + self.query.setToolTip(tt) + tt = '' + ac = r['action'] + if ac == 'append': + tt = _('CSS properties for to add to the rule that contains the matching style. You' + ' can specify more than one property, separated by semi-colons, for example:' + ' color:red; font-weight: bold') + elif ac in '+=*/': + tt = _('A number') + self.action_data.setToolTip(tt) + + @property + def rule(self): + return { + 'property':self.property.text().strip(), + 'match_type': self.match_type.currentData(), + 'query': self.query.text().strip(), + 'action': self.action.currentData(), + 'action_data': self.action_data.text().strip(), + } + + @rule.setter + def rule(self, rule): + def sc(name): + c = getattr(self, name) + idx = c.findData(unicode(rule.get(name, ''))) + if idx < 0: + idx = 0 + c.setCurrentIndex(idx) + sc('action'), sc('match_type') + self.query.setText(unicode(rule.get('query', '')).strip()) + self.action_data.setText(unicode(rule.get('action_data', '')).strip()) + + def validate(self): + rule = self.rule + if not rule['query']: + error_dialog(self, _('Query required'), _( + 'You must specify a value for the CSS property to match'), show=True) + return False + mt = rule['match_type'] + if 'matches' in mt: + try: + compile_pat(rule['query']) + except Exception: + error_dialog(self, _('Query invalid'), _( + '%s is not a valid regular expression') % rule['query'], show=True) + return False + elif mt in '< > <= >='.split(): + try: + parse_length(rule['query']) + except Exception: + error_dialog(self, _('Query invalid'), _( + '%s is not a valid length or number') % rule['query'], show=True) + return False + ac, ad = rule['action'], rule['action_data'] + if not ad and ac != 'remove': + msg = _('You must specify a number') + if ac == 'append': + msg = _('You must specify at least one CSS property to add') + elif ac == 'change': + msg = _('You must specify a value to change the property to') + error_dialog(self, _('No data'), msg, show=True) + return False + if ac in '+-*/': + try: + float(ad) + except Exception: + error_dialog(self, _('Invalid number'), _('%s is not a number') % ad, show=True) + return False + return True +# }}} + +class RuleEditDialog(RuleEditDialogBase): # {{{ + + PREFS_NAME = 'edit-css-transform-rule' + DIALOG_TITLE = _('Edit rule') + RuleEditClass = RuleEdit +# }}} + +class RuleItem(QListWidgetItem): # {{{ + + @staticmethod + def text_from_rule(rule, parent): + query = elided_text(rule['query'], font=parent.font(), width=200, pos='right') + text = _( + 'If the property {property} {match_type} {query}
{action}').format( + property=rule['property'], action=RuleEdit.ACTION_MAP[rule['action']], + match_type=RuleEdit.MATCH_TYPE_MAP[rule['match_type']], query=query) + if rule['action_data']: + ad = elided_text(rule['action_data'], font=parent.font(), width=200, pos='right') + text += ' %s' % ad + return text +# }}} + +class Rules(RulesBase): + + RuleItemClass = RuleItem + RuleEditDialogClass = RuleEditDialog + + MSG = _('You can specify rules to transform styles here. Click the "Add Rule" button' + ' below to get started.')