%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/tweak_book/ |
Current File : //lib/calibre/calibre/gui2/tweak_book/function_replace.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' import re, io, weakref, sys from qt.core import ( pyqtSignal, QVBoxLayout, QHBoxLayout, QPlainTextEdit, QLabel, QFontMetrics, QSize, Qt, QApplication, QIcon, QDialogButtonBox) from calibre.ebooks.oeb.polish.utils import apply_func_to_match_groups, apply_func_to_html_text from calibre.gui2 import error_dialog from calibre.gui2.complete2 import EditWithComplete from calibre.gui2.tweak_book import dictionaries from calibre.gui2.tweak_book.widgets import Dialog from calibre.gui2.tweak_book.editor.text import TextEdit from calibre.utils.config import JSONConfig from calibre.utils.icu import capitalize, upper, lower, swapcase from calibre.utils.titlecase import titlecase from calibre.utils.localization import localize_user_manual_link from polyglot.builtins import iteritems from polyglot.io import PolyglotStringIO user_functions = JSONConfig('editor-search-replace-functions') def compile_code(src, name='<string>'): if not isinstance(src, str): match = re.search(br'coding[:=]\s*([-\w.]+)', src[:200]) enc = match.group(1).decode('utf-8') if match else 'utf-8' src = src.decode(enc) if not src or not src.strip(): src = EMPTY_FUNC # Python complains if there is a coding declaration in a unicode string src = re.sub(r'^#.*coding\s*[:=]\s*([-\w.]+)', '#', src, flags=re.MULTILINE) # Translate newlines to \n src = io.StringIO(src, newline=None).getvalue() code = compile(src, name, 'exec') namespace = {} exec(code, namespace) return namespace class Function: def __init__(self, name, source=None, func=None): self._source = source self.is_builtin = source is None self.name = name if func is None: self.mod = compile_code(source, name) self.func = self.mod['replace'] else: self.func = func self.mod = None if not callable(self.func): raise ValueError('%r is not a function' % self.func) self.file_order = getattr(self.func, 'file_order', None) def init_env(self, name=''): from calibre.gui2.tweak_book.boss import get_boss self.context_name = name or '' self.match_index = 0 self.boss = get_boss() self.data = {} self.debug_buf = PolyglotStringIO() self.functions = {name:func.mod for name, func in iteritems(functions()) if func.mod is not None} def __hash__(self): return hash(self.name) def __eq__(self, other): return self.name == getattr(other, 'name', None) def __ne__(self, other): return not self.__eq__(other) def __call__(self, match): self.match_index += 1 oo, oe, sys.stdout, sys.stderr = sys.stdout, sys.stderr, self.debug_buf, self.debug_buf try: return self.func(match, self.match_index, self.context_name, self.boss.current_metadata, dictionaries, self.data, self.functions) finally: sys.stdout, sys.stderr = oo, oe @property def source(self): if self.is_builtin: import json return json.loads(P('editor-functions.json', data=True, allow_user_override=False))[self.name] return self._source def end(self): if getattr(self.func, 'call_after_last_match', False): oo, oe, sys.stdout, sys.stderr = sys.stdout, sys.stderr, self.debug_buf, self.debug_buf try: return self.func(None, self.match_index, self.context_name, self.boss.current_metadata, dictionaries, self.data, self.functions) finally: sys.stdout, sys.stderr = oo, oe self.data, self.boss, self.functions = {}, None, {} class DebugOutput(Dialog): def __init__(self, parent=None): Dialog.__init__(self, 'Debug output', 'sr-function-debug-output') self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) def setup_ui(self): self.l = l = QVBoxLayout(self) self.text = t = QPlainTextEdit(self) self.log_text = '' l.addWidget(t) l.addWidget(self.bb) self.bb.setStandardButtons(QDialogButtonBox.StandardButton.Close) self.cb = b = self.bb.addButton(_('&Copy to clipboard'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.copy_to_clipboard) b.setIcon(QIcon(I('edit-copy.png'))) def show_log(self, name, text): if isinstance(text, bytes): text = text.decode('utf-8', 'replace') self.setWindowTitle(_('Debug output from %s') % name) self.text.setPlainText(self.windowTitle() + '\n\n' + text) self.log_text = text self.show() self.raise_() def sizeHint(self): fm = QFontMetrics(self.text.font()) return QSize(fm.averageCharWidth() * 120, 400) def copy_to_clipboard(self): QApplication.instance().clipboard().setText(self.log_text) def builtin_functions(): for name, obj in iteritems(globals()): if name.startswith('replace_') and callable(obj) and hasattr(obj, 'imports'): yield obj _functions = None def functions(refresh=False): global _functions if _functions is None or refresh: ans = _functions = {} for func in builtin_functions(): ans[func.name] = Function(func.name, func=func) for name, source in iteritems(user_functions): try: f = Function(name, source=source) except Exception: continue ans[f.name] = f return _functions def remove_function(name, gui_parent=None): funcs = functions() if not name: return False if name not in funcs: error_dialog(gui_parent, _('No such function'), _( 'There is no function named %s') % name, show=True) return False if name not in user_functions: error_dialog(gui_parent, _('Cannot remove builtin function'), _( 'The function %s is a builtin function, it cannot be removed.') % name, show=True) del user_functions[name] functions(refresh=True) refresh_boxes() return True boxes = [] def refresh_boxes(): for ref in boxes: box = ref() if box is not None: box.refresh() class FunctionBox(EditWithComplete): save_search = pyqtSignal() show_saved_searches = pyqtSignal() def __init__(self, parent=None, show_saved_search_actions=False): EditWithComplete.__init__(self, parent) self.set_separator(None) self.show_saved_search_actions = show_saved_search_actions self.refresh() self.setToolTip(_('Choose a function to run on matched text (by name)')) boxes.append(weakref.ref(self)) def refresh(self): self.update_items_cache(set(functions())) def contextMenuEvent(self, event): menu = self.lineEdit().createStandardContextMenu() if self.show_saved_search_actions: menu.addSeparator() menu.addAction(_('Save current search'), self.save_search.emit) menu.addAction(_('Show saved searches'), self.show_saved_searches.emit) menu.exec(event.globalPos()) class FunctionEditor(Dialog): def __init__(self, func_name='', parent=None): self._func_name = func_name Dialog.__init__(self, _('Create/edit a function'), 'edit-sr-func', parent=parent) def setup_ui(self): self.l = l = QVBoxLayout(self) self.h = h = QHBoxLayout() l.addLayout(h) self.la1 = la = QLabel(_('F&unction name:')) h.addWidget(la) self.fb = fb = FunctionBox(self) la.setBuddy(fb) h.addWidget(fb, stretch=10) self.la3 = la = QLabel(_('&Code:')) self.source_code = TextEdit(self) self.source_code.load_text('', 'python') la.setBuddy(self.source_code) l.addWidget(la), l.addWidget(self.source_code) if self._func_name: self.fb.setText(self._func_name) func = functions().get(self._func_name) if func is not None: self.source_code.setPlainText(func.source or ('\n' + EMPTY_FUNC)) else: self.source_code.setPlainText('\n' + EMPTY_FUNC) self.la2 = la = QLabel(_( 'For help with creating functions, see the <a href="%s">User Manual</a>') % localize_user_manual_link('https://manual.calibre-ebook.com/function_mode.html')) la.setOpenExternalLinks(True) l.addWidget(la) l.addWidget(self.bb) def sizeHint(self): fm = QFontMetrics(self.font()) return QSize(fm.averageCharWidth() * 120, 600) @property def func_name(self): return self.fb.text().strip() @property def source(self): return self.source_code.toPlainText() def accept(self): if not self.func_name: return error_dialog(self, _('Must specify name'), _( 'You must specify a name for this function.'), show=True) source = self.source try: mod = compile_code(source, self.func_name) except Exception as err: return error_dialog(self, _('Invalid Python code'), _( 'The code you created is not valid Python code, with error: %s') % err, show=True) if not callable(mod.get('replace')): return error_dialog(self, _('No replace function'), _( 'You must create a Python function named replace in your code'), show=True) user_functions[self.func_name] = source functions(refresh=True) refresh_boxes() Dialog.accept(self) # Builtin functions ########################################################## def builtin(name, *args): def f(func): func.name = name func.imports = args return func return f EMPTY_FUNC = '''\ def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): return '' ''' @builtin('Upper-case text', upper, apply_func_to_match_groups) def replace_uppercase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text upper case. If the regular expression contains groups, only the text in the groups will be changed, otherwise the entire text is changed.''' return apply_func_to_match_groups(match, upper) @builtin('Lower-case text', lower, apply_func_to_match_groups) def replace_lowercase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text lower case. If the regular expression contains groups, only the text in the groups will be changed, otherwise the entire text is changed.''' return apply_func_to_match_groups(match, lower) @builtin('Capitalize text', capitalize, apply_func_to_match_groups) def replace_capitalize(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Capitalize matched text. If the regular expression contains groups, only the text in the groups will be changed, otherwise the entire text is changed.''' return apply_func_to_match_groups(match, capitalize) @builtin('Title-case text', titlecase, apply_func_to_match_groups) def replace_titlecase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Title-case matched text. If the regular expression contains groups, only the text in the groups will be changed, otherwise the entire text is changed.''' return apply_func_to_match_groups(match, titlecase) @builtin('Swap the case of text', swapcase, apply_func_to_match_groups) def replace_swapcase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Swap the case of the matched text. If the regular expression contains groups, only the text in the groups will be changed, otherwise the entire text is changed.''' return apply_func_to_match_groups(match, swapcase) @builtin('Upper-case text (ignore tags)', upper, apply_func_to_html_text) def replace_uppercase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text upper case, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, upper) @builtin('Lower-case text (ignore tags)', lower, apply_func_to_html_text) def replace_lowercase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text lower case, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, lower) @builtin('Capitalize text (ignore tags)', capitalize, apply_func_to_html_text) def replace_capitalize_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Capitalize matched text, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, capitalize) @builtin('Title-case text (ignore tags)', titlecase, apply_func_to_html_text) def replace_titlecase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Title-case matched text, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, titlecase) @builtin('Swap the case of text (ignore tags)', swapcase, apply_func_to_html_text) def replace_swapcase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Swap the case of the matched text, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, swapcase) if __name__ == '__main__': app = QApplication([]) FunctionEditor().exec() del app