diff --git a/src/calibre/ebooks/metadata/tag_mapper.py b/src/calibre/ebooks/metadata/tag_mapper.py index e8a2d54c3d..2e307355fc 100644 --- a/src/calibre/ebooks/metadata/tag_mapper.py +++ b/src/calibre/ebooks/metadata/tag_mapper.py @@ -31,6 +31,10 @@ def matcher(rule): pat = compile_pat(rule['query']) return lambda x: pat.match(x) is None + if mt == 'has': + s = rule['query'] + return lambda x: s in x + return lambda x: False @@ -83,6 +87,14 @@ def apply_rules(tag, rules): if ac == 'upper': ans.append(icu_upper(tag)) break + if ac == 'split': + stags = filter(None, [x.strip() for x in tag.split(rule['replace'])]) + if stags: + if stags[0] == tag: + ans.append(tag) + else: + tags.extendleft(reversed(stags)) + break else: # no rule matched, default keep ans.append(tag) @@ -141,6 +153,10 @@ def test(): run([rule('replace', 't1', 't2'), rule('replace', 't2', 't1')], 't1,t2', 't1,t2') run(rule('replace', 'a', 'A'), 'a,b', 'A,b') run(rule('replace', 'a,b', 'A,B'), 'a,b', 'A,B') + run(rule('split', '/', '/', 'has'), 'a/b/c,d', 'a,b,c,d') + run(rule('split', '/', '/', 'has'), '/,d', 'd') + run(rule('split', '/', '/', 'has'), '/a/', 'a') + run(rule('split', 'a,b', '/'), 'a,b', 'a,b') if __name__ == '__main__': test() diff --git a/src/calibre/gui2/add_filters.py b/src/calibre/gui2/add_filters.py index b52b3839ad..1884fe26f8 100644 --- a/src/calibre/gui2/add_filters.py +++ b/src/calibre/gui2/add_filters.py @@ -50,6 +50,7 @@ class RuleEdit(RuleEditBase): tt = _('A case-insensitive filename pattern, for example: {0} or {1}').format('*.pdf', 'number-?.epub') else: tt = _('A regular expression') + self.regex_help.setVisible('matches' in q) self.query.setToolTip(tt) @property diff --git a/src/calibre/gui2/css_transform_rules.py b/src/calibre/gui2/css_transform_rules.py index 810604830b..94058429df 100644 --- a/src/calibre/gui2/css_transform_rules.py +++ b/src/calibre/gui2/css_transform_rules.py @@ -14,10 +14,11 @@ from calibre.ebooks.css_transform_rules import ( 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, SaveLoadMixin) + RuleEdit as RE, RuleEditDialog as RuleEditDialogBase, Rules as RulesBase, + RulesDialog as RulesDialogBase, RuleItem as RuleItemBase, SaveLoadMixin) from calibre.gui2.widgets2 import Dialog from calibre.utils.config import JSONConfig +from calibre.utils.localization import localize_user_manual_link class RuleEdit(QWidget): # {{{ @@ -76,6 +77,13 @@ class RuleEdit(QWidget): # {{{ if clause is not parts[-1]: h.addWidget(QLabel('\xa0')) + self.regex_help = la = QLabel('

' + RE.REGEXP_HELP_TEXT % localize_user_manual_link( + 'http://manual.calibre-ebook.com/regexp.html')) + la.setOpenExternalLinks(True) + la.setWordWrap(True) + l.addWidget(la) + l.addStretch(10) + self.update_state() def sizeHint(self): @@ -108,6 +116,7 @@ class RuleEdit(QWidget): # {{{ elif ac in '+=*/': tt = _('A number') self.action_data.setToolTip(tt) + self.regex_help.setVisible('matches' in mt) @property def rule(self): diff --git a/src/calibre/gui2/tag_mapper.py b/src/calibre/gui2/tag_mapper.py index e9da3db7a8..5e15bec0de 100644 --- a/src/calibre/gui2/tag_mapper.py +++ b/src/calibre/gui2/tag_mapper.py @@ -7,6 +7,7 @@ from __future__ import (unicode_literals, division, absolute_import, from collections import OrderedDict from functools import partial +import textwrap from PyQt5.Qt import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QIcon, @@ -19,6 +20,7 @@ from calibre.gui2 import error_dialog, elided_text, Application, question_dialog from calibre.gui2.ui import get_gui from calibre.gui2.widgets2 import Dialog from calibre.utils.config import JSONConfig +from calibre.utils.localization import localize_user_manual_link tag_maps = JSONConfig('tag-map-rules') @@ -38,18 +40,30 @@ class RuleEdit(QWidget): ('capitalize', _('Capitalize')), ('lower', _('Lower-case')), ('upper', _('Upper-case')), + ('split', _('Split')), )) MATCH_TYPE_MAP = OrderedDict(( ('one_of', _('is one of')), ('not_one_of', _('is not one of')), ('matches', _('matches pattern')), - ('not_matches', _('does not match pattern')) + ('not_matches', _('does not match pattern')), + ('has', _('contains')), )) MSG = _('Create the rule below, the rule can be used to remove or replace tags') SUBJECT = _('the tag, if it') VALUE_ERROR = _('You must provide a value for the tag to match') + REPLACE_TEXT = _('with the tag:') + SPLIT_TEXT = _('on the character:') + SPLIT_TOOLTIP = _( + 'The character on which to split tags. Note that technically you can specify' + ' a sub-string, not just a single character. Then splitting will happen on the sub-string.') + REPLACE_TOOLTIP = _( + 'What to replace the tag with. Note that if you use a pattern to match' + ' tags, you can replace with parts of the matched pattern. See ' + ' the User Manual on how to use regular expressions for details.') + REGEXP_HELP_TEXT = _('For help with regex pattern matching, see the User Manual') def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -83,10 +97,16 @@ class RuleEdit(QWidget): b.setVisible(self.can_use_tag_editor) self.h2 = h = QHBoxLayout() l.addLayout(h) - self.la3 = la = QLabel(_('with the tag:') + '\xa0') + self.la3 = la = QLabel(self.REPLACE_TEXT + '\xa0') h.addWidget(la) self.replace = r = QLineEdit(self) h.addWidget(r) + self.regex_help = la = QLabel('

' + self.REGEXP_HELP_TEXT % localize_user_manual_link( + 'http://manual.calibre-ebook.com/regexp.html')) + la.setOpenExternalLinks(True) + la.setWordWrap(True) + l.addWidget(la) + la.setVisible(False) l.addStretch(10) self.la3.setVisible(False), self.replace.setVisible(False) self.update_state() @@ -102,14 +122,22 @@ class RuleEdit(QWidget): return self.SUBJECT is RuleEdit.SUBJECT and 'matches' not in self.match_type.currentData() and get_gui() is not None def update_state(self): - replace = self.action.currentData() == 'replace' - self.la3.setVisible(replace), self.replace.setVisible(replace) + a = self.action.currentData() + replace = a == 'replace' + split = a == 'split' + self.la3.setVisible(replace or split), self.replace.setVisible(replace or split) tt = _('A comma separated list of tags') - is_match = 'matches' in self.match_type.currentData() + m = self.match_type.currentData() + is_match = 'matches' in m self.tag_editor_button.setVisible(self.can_use_tag_editor) if is_match: tt = _('A regular expression') + elif m == 'has': + tt = _('Tags that contain this string will match') + self.regex_help.setVisible(is_match) + self.la3.setText((self.SPLIT_TEXT if split else self.REPLACE_TEXT) + '\xa0') self.query.setToolTip(tt) + self.replace.setToolTip(textwrap.fill(self.SPLIT_TOOLTIP if split else self.REPLACE_TOOLTIP)) def specialise_context_menu(self, menu): if self.can_use_tag_editor: @@ -188,6 +216,8 @@ class RuleItem(QListWidgetItem): action=RuleEdit.ACTION_MAP[rule['action']], match_type=RuleEdit.MATCH_TYPE_MAP[rule['match_type']], query=query) if rule['action'] == 'replace': text += '
' + _('with the tag:') + ' %s' % rule['replace'] + if rule['action'] == 'split': + text += '
' + _('on the character:') + ' %s' % rule['replace'] return text def __init__(self, rule, parent): @@ -467,6 +497,7 @@ if __name__ == '__main__': d.rules = [ {'action':'remove', 'query':'moose', 'match_type':'one_of', 'replace':''}, {'action':'replace', 'query':'moose', 'match_type':'one_of', 'replace':'xxxx'}, + {'action':'split', 'query':'/', 'match_type':'has', 'replace':'/'}, ] d.exec_() from pprint import pprint