Merge branch 'master' of https://github.com/cbhaley/calibre into master

This commit is contained in:
Kovid Goyal 2020-10-03 09:06:24 +05:30
commit 927cf43a4c
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
9 changed files with 678 additions and 162 deletions

View File

@ -187,7 +187,7 @@ The example shows several things:
The language is similar to ``functional`` languages in that it is built almost entirely from functions. An expression is generally a function. Constants and identifiers can be thought of as functions returning the value indicated by the constant or stored in the identifier.
The syntax of the language is shown by the following grammar. For a discussion of 'compare' and 'if_expression' see :ref:`General Program Mode <general_mode>`:::
The syntax of the language is shown by the following grammar. For a discussion of 'compare','if_expression', and 'template_call' see :ref:`General Program Mode <general_mode>`:::
program ::= expression_list
expression_list ::= expression [ ';' expression ]*
@ -254,7 +254,7 @@ The following functions are available in addition to those described in single-f
returns "yes" if the yes/no field ``"#bool"`` is either undefined (neither True nor False) or True. More than one of ``is_undefined``, ``is_false``, or ``is_true`` can be set to 1. This function is usually used by the ``test()`` or ``is_empty()`` functions.
* ``ceiling(x)`` -- returns the smallest integer greater than or equal to x. Throws an exception if x is not a number.
* ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``connected_device_name(storage_location)`` -- if a device is connected then return the device name, otherwise return the empty string. Each storage location on a device can have a different name. The location names are 'main', 'carda' and 'cardb'. This function works only in the GUI.
* ``connected_device_name(storage_location)`` -- if a device is connected then return the device name, otherwise return the empty string. Each storage location on a device can have a different name. The location names are 'main', 'carda' and 'cardb'. This function works only in the GUI.
* ``current_library_name()`` -- return the last name on the path to the current calibre library. This function can be called in template program mode using the template ``{:'current_library_name()'}``.
* ``current_library_path()`` -- return the path to the current calibre library. This function can be called in template program mode using the template ``{:'current_library_path()'}``.
* ``days_between(date1, date2)`` -- return the number of days between ``date1`` and ``date2``. The number is positive if ``date1`` is greater than ``date2``, otherwise negative. If either ``date1`` or ``date2`` are not dates, the function returns the empty string.
@ -403,6 +403,31 @@ Program mode also supports the classic relational (comparison) operators: ``==``
* ``program: if or(field('series') == 'foo', field('series') == '1632') then 'yes' else 'no' fi`` returns 'yes' if series is either 'foo' or '1632', otherwise 'no'.
* ``program: if '11' > '2' then 'yes' else 'no' fi`` returns 'no' because it is doing a lexical comparison. If you want numeric comparison instead of lexical comparison, use the operators ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``, ``>=#``. In this case the left and right values are set to zero if they are undefined or the empty string. If they are not numbers then an error is raised.
General Program Mode support saving General Program Mode templates and calling those templates from another template. You save
templates using :guilabel:`Preferences->Advanced->Template functions`. More information is provided in that dialog. You call
a template the same way you call a function, passing positional arguments if desired. An argument can be any expression.
Examples of calling a template, assuming the stored template is named ``foo``:
* ``foo()`` -- call the template passing no arguments.
* ``foo(a, b)`` call the template passing the values of the two variables ``a`` and ``b``.
* ``foo(if field('series') then field('series_index') else 0 fi)`` -- if the book has a ``series`` then pass the ``series_index``, otherwise pass the value ``0``.
In the stored template you retrieve the arguments passed in the call using the ``arguments`` function. It both declares and
initializes local variables. The variables are positional; they get the value of the value given in the call in the same position.
If the corresponding parameter is not provided in the call then ``arguments`` gives that parameter the provided default value. If there is no default value then the argument is set to the empty string. For example, the following ``arguments`` function declares 2 variables, ``key``, ``alternate``::
``arguments(key, alternate='series')
Examples, again assuming the stored template is named ``foo``:
* ``foo('#myseries')`` -- argument``key`` will have the value ``myseries`` and the argument ``alternate`` will have the value ``series``.
* ``foo('series', '#genre')`` the variable ``key`` is assigned the value ``series`` and the variable ``alternate`` is assigned the value ``#genre``.
* ``foo()`` -- the variable ``key`` is assigned the empty string and the variable ``alternate`` is assigned the value ``#genre``.
An easy way to test stored templates is using the ``Template tester`` dialog. Give it a keyboard shortcut in
:guilabel:`Preferences->Advanced->Keyboard shortcuts->Template tester`. Giving the ``Stored templates`` dialog a
shortcut will help switching more rapidly between the tester and editing the stored template's source code.
The following example is a `program:` mode implementation of a recipe on the MobileRead forum: "Put series into the title, using either initials or a shortened form. Strip leading articles from the series name (any)." For example, for the book The Two Towers in the Lord of the Rings series, the recipe gives `LotR [02] The Two Towers`. Using standard templates, the recipe requires three custom columns and a plugboard, as explained in the following:
The solution requires creating three composite columns. The first column is used to remove the leading articles. The second is used to compute the 'shorten' form. The third is to compute the 'initials' form. Once you have these columns, the plugboard selects between them. You can hide any or all of the three columns on the library view::
@ -484,10 +509,10 @@ The following program produces the same results as the original recipe, using on
It would be possible to do the above with no custom columns by putting the program into the template box of the plugboard. However, to do so, all comments must be removed because the plugboard text box does not support multi-line editing. It is debatable whether the gain of not having the custom column is worth the vast increase in difficulty caused by the program being one giant line.
User-defined template functions
User-defined python template functions
-------------------------------
You can add your own functions to the template processor. Such functions are written in Python, and can be used in any of the three template programming modes. The functions are added by going to Preferences -> Advanced -> Template functions. Instructions are shown in that dialog.
You can add your own python functions to the template processor. Such functions are written in Python, and can be used in any of the three template programming modes. The functions are added by going to guilabel`Preferences -> Advanced -> Template functions`. Instructions are shown in that dialog.
Special notes for save/send templates
-------------------------------------

View File

@ -935,6 +935,12 @@ class ActionTemplateTester(InterfaceActionBase):
description = _('Show an editor for testing templates')
class ActionTemplateFunctions(InterfaceActionBase):
name = 'Template Functions'
actual_plugin = 'calibre.gui2.actions.show_stored_templates:ShowTemplateFunctionsAction'
description = _('Show a dialog for creating and managing template functions and stored templates')
class ActionSaveToDisk(InterfaceActionBase):
name = 'Save To Disk'
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
@ -1099,7 +1105,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore,
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper,
ActionVirtualLibrary, ActionBrowseAnnotations]
ActionVirtualLibrary, ActionBrowseAnnotations, ActionTemplateFunctions]
# }}}

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.preferences.main import Preferences
class ShowTemplateFunctionsAction(InterfaceAction):
name = 'Template Functions'
action_spec = (_('Template Functions'), 'debug.png', None, ())
dont_add_to = frozenset(('context-menu-device',))
action_type = 'current'
def genesis(self):
self.previous_text = _('Manage template functions')
self.first_time = True
self.qaction.triggered.connect(self.show_template_editor)
def show_template_editor(self, *args):
d = Preferences(self.gui, initial_plugin=('Advanced', 'TemplateFunctions'),
close_after_initial=True)
d.exec_()

View File

@ -213,7 +213,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
def __init__(self, parent, text, mi=None, fm=None, color_field=None,
icon_field_key=None, icon_rule_kind=None, doing_emblem=False,
text_is_placeholder=False):
text_is_placeholder=False, dialog_is_st_editor=False):
QDialog.__init__(self, parent)
Ui_TemplateDialog.__init__(self)
self.setupUi(self)
@ -221,6 +221,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.coloring = color_field is not None
self.iconing = icon_field_key is not None
self.embleming = doing_emblem
self.dialog_is_st_editor = dialog_is_st_editor
cols = []
if fm is not None:
@ -273,6 +274,14 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.icon_kind.setCurrentIndex(dex)
self.icon_field.setCurrentIndex(self.icon_field.findData(icon_field_key))
if dialog_is_st_editor:
self.buttonBox.setVisible(False)
else:
self.new_doc_label.setVisible(False)
self.new_doc.setVisible(False)
self.template_name_label.setVisible(False)
self.template_name.setVisible(False)
if mi:
self.mi = mi
else:
@ -294,6 +303,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
from calibre.gui2.ui import get_gui
self.mi.set_all_user_metadata(
get_gui().current_db.new_api.field_metadata.custom_field_metadata())
for col in self.mi.get_all_user_metadata(False):
self.mi.set(col, (col,), 0)
# Remove help icon on title bar
icon = self.windowIcon()
@ -313,6 +324,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
if text is not None:
if text_is_placeholder:
self.textbox.setPlaceholderText(text)
self.textbox.clear()
else:
self.textbox.setPlainText(text)
self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK'))
@ -429,12 +441,17 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
name = unicode_type(toWhat)
self.source_code.clear()
self.documentation.clear()
self.func_type.clear()
if name in self.funcs:
self.documentation.setPlainText(self.funcs[name].doc)
if name in self.builtins and name in self.builtin_source_dict:
self.source_code.setPlainText(self.builtin_source_dict[name])
else:
self.source_code.setPlainText(self.funcs[name].program_text)
if self.funcs[name].is_python:
self.func_type.setText(_('Template function in python'))
else:
self.func_type.setText(_('Stored template'))
def accept(self):
txt = unicode_type(self.textbox.toPlainText()).rstrip()
@ -462,6 +479,27 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.rule = ('', txt)
QDialog.accept(self)
def reject(self):
QDialog.reject(self)
if self.dialog_is_st_editor:
parent = self.parent()
while True:
if hasattr(parent, 'reject'):
parent.reject()
break
parent = parent.parent()
if parent is None:
break
class EmbeddedTemplateDialog(TemplateDialog):
def __init__(self, parent):
TemplateDialog.__init__(self, parent, _('A General Program Mode Template'), text_is_placeholder=True,
dialog_is_st_editor=True)
self.setParent(parent)
self.setWindowFlags(Qt.Widget)
if __name__ == '__main__':
app = QApplication([])

View File

@ -141,11 +141,68 @@
</layout>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="textbox"/>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="template_name_label">
<property name="text">
<string>Template &amp;name:</string>
</property>
<property name="buddy">
<cstring>template_name</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="template_name">
<property name="editable">
<bool>true</bool>
</property>
<property name="toolTip">
<string>The name of the callable template</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel">
<property name="text">
<string>T&amp;emplate:</string>
</property>
<property name="buddy">
<cstring>textbox</cstring>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPlainTextEdit" name="textbox">
<property name="toolTip">
<string>The template program text</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="new_doc_label">
<property name="text">
<string>D&amp;ocumentation:</string>
</property>
<property name="buddy">
<cstring>new_doc</cstring>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QTextEdit" name="new_doc">
<property name="toolTip">
<string>Documentation for the function being defined or edited</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel">
<property name="text">
@ -173,7 +230,7 @@
</property>
</widget>
</item>
<item row="6" column="1">
<item row="6" column="1" colspan="3">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QSpinBox" name="font_size_box"/>
@ -203,14 +260,24 @@
</item>
</layout>
</item>
<item row="7" column="0" colspan="2">
<item row="7" column="0" colspan="5">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
</widget>
</item>
<item row="8" column="0">
<item row="8" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>Template Function Reference</string>
</property>
<property name="buddy">
<cstring>function</cstring>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Function &amp;name:</string>
@ -220,10 +287,30 @@
</property>
</widget>
</item>
<item row="8" column="1">
<item row="9" column="1" colspan="3">
<widget class="QComboBox" name="function"/>
</item>
<item row="9" column="0">
<item row="10" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>&amp;Function type:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="buddy">
<cstring>func_type</cstring>
</property>
</widget>
</item>
<item row="10" column="1" colspan="3">
<widget class="QLineEdit" name="func_type">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Documentation:</string>
@ -236,10 +323,10 @@
</property>
</widget>
</item>
<item row="10" column="0">
<item row="12" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Python &amp;code:</string>
<string>&amp;Code:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
@ -249,7 +336,7 @@
</property>
</widget>
</item>
<item row="9" column="1">
<item row="11" column="1" colspan="3">
<widget class="QPlainTextEdit" name="documentation">
<property name="maximumSize">
<size>
@ -259,17 +346,17 @@
</property>
</widget>
</item>
<item row="10" column="1">
<item row="12" column="1" colspan="3">
<widget class="QPlainTextEdit" name="source_code"/>
</item>
<item row="11" column="1">
<item row="13" column="1">
<widget class="QLabel" name="template_tutorial">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="12" column="1">
<item row="14" column="1">
<widget class="QLabel" name="template_func_reference">
<property name="openExternalLinks">
<bool>true</bool>

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import json, traceback
from PyQt5.Qt import QDialogButtonBox
from PyQt5.Qt import Qt, QDialogButtonBox, QSizePolicy
from calibre.gui2 import error_dialog, warning_dialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
@ -16,7 +16,8 @@ from calibre.gui2.preferences.template_functions_ui import Ui_Form
from calibre.gui2.widgets import PythonHighlighter
from calibre.utils.formatter_functions import (formatter_functions,
compile_user_function, compile_user_template_functions,
load_user_template_functions)
load_user_template_functions, function_pref_is_python,
function_pref_name)
from polyglot.builtins import iteritems, native_string_type, unicode_type
@ -77,6 +78,18 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
</p>
''')
self.textBrowser.setHtml(help_text)
help_text = '<p>' + _('''
Here you can add and remove stored templates used in template processing.
You use a stored template in another template with the '{0}' template
function, as in '{0}(some_name, arguments...). Stored templates must use
General Program Mode -- they must begin with the text '{1}'.
In the stored template you retrieve the arguments using the '{2}()'
template function, as in '{2}(var1, var2, ...)'. The calling arguments
are copied to the named variables. See the template language tutorial
for more information.
''') + '</p>'
self.st_textBrowser.setHtml(help_text.format('call', 'program:', 'arguments'))
self.st_textBrowser.adjustSize()
def initialize(self):
try:
@ -86,9 +99,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
traceback.print_exc()
self.builtin_source_dict = {}
self.funcs = formatter_functions().get_functions()
self.funcs = dict((k,v) for k,v in formatter_functions().get_functions().items()
if v.is_python)
self.builtins = formatter_functions().get_builtins_and_aliases()
self.st_funcs = {}
for v in self.db.prefs.get('user_template_functions', []):
if not function_pref_is_python(v):
self.st_funcs.update({function_pref_name(v):compile_user_function(*v)})
self.build_function_names_box()
self.function_name.currentIndexChanged[native_string_type].connect(self.function_index_changed)
self.function_name.editTextChanged.connect(self.function_name_edited)
@ -105,6 +125,22 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.program.setTabStopWidth(20)
self.highlighter = PythonHighlighter(self.program.document())
self.st_build_function_names_box()
self.template_editor.template_name.currentIndexChanged[native_string_type].connect(self.st_function_index_changed)
self.template_editor.template_name.editTextChanged.connect(self.st_template_name_edited)
self.st_create_button.clicked.connect(self.st_create_button_clicked)
self.st_delete_button.clicked.connect(self.st_delete_button_clicked)
self.st_create_button.setEnabled(False)
self.st_delete_button.setEnabled(False)
self.st_replace_button.setEnabled(False)
self.st_clear_button.clicked.connect(self.st_clear_button_clicked)
self.st_replace_button.clicked.connect(self.st_replace_button_clicked)
self.st_button_layout.insertSpacing(0, 90)
self.template_editor.new_doc.setFixedHeight(50)
# Python funtion tab
def enable_replace_button(self):
self.replace_button.setEnabled(self.delete_button.isEnabled())
@ -116,16 +152,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.create_button.setEnabled(False)
self.delete_button.setEnabled(False)
def build_function_names_box(self, scroll_to='', set_to=''):
def build_function_names_box(self, scroll_to=''):
self.function_name.blockSignals(True)
func_names = sorted(self.funcs)
self.function_name.clear()
self.function_name.addItem('')
self.function_name.addItems(func_names)
self.function_name.setCurrentIndex(0)
if set_to:
self.function_name.setEditText(set_to)
self.create_button.setEnabled(True)
self.function_name.blockSignals(False)
if scroll_to:
idx = self.function_name.findText(scroll_to)
@ -144,19 +177,25 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit()
self.create_button.setEnabled(True)
self.delete_button.setEnabled(False)
self.build_function_names_box(set_to=name)
self.build_function_names_box()
self.program.setReadOnly(False)
else:
error_dialog(self.gui, _('Template functions'),
_('Function not defined'), show=True)
def create_button_clicked(self):
def create_button_clicked(self, use_name=None):
self.changed_signal.emit()
name = unicode_type(self.function_name.currentText())
name = use_name if use_name else unicode_type(self.function_name.currentText())
if name in self.funcs:
error_dialog(self.gui, _('Template functions'),
_('Name %s already used')%(name,), show=True)
return
if name in {function_pref_name(v) for v in
self.db.prefs.get('user_template_functions', [])
if not function_pref_is_python(v)}:
error_dialog(self.gui, _('Template functions'),
_('Name %s is already used for stored template')%(name,), show=True)
return
if self.argument_count.value() == 0:
box = warning_dialog(self.gui, _('Template functions'),
_('Argument count should be -1 or greater than zero. '
@ -215,18 +254,102 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.replace_button.setEnabled(False)
def replace_button_clicked(self):
name = unicode_type(self.function_name.currentText())
self.delete_button_clicked()
self.create_button_clicked()
self.create_button_clicked(use_name=name)
def refresh_gui(self, gui):
pass
# Stored template tab
def st_clear_button_clicked(self):
self.st_build_function_names_box()
self.template_editor.textbox.clear()
self.template_editor.new_doc.clear()
self.st_create_button.setEnabled(False)
self.st_delete_button.setEnabled(False)
def st_build_function_names_box(self, scroll_to=''):
self.template_editor.template_name.blockSignals(True)
func_names = sorted(self.st_funcs)
self.template_editor.template_name.clear()
self.template_editor.template_name.addItem('')
self.template_editor.template_name.addItems(func_names)
self.template_editor.template_name.setCurrentIndex(0)
self.template_editor.template_name.blockSignals(False)
if scroll_to:
idx = self.template_editor.template_name.findText(scroll_to)
if idx >= 0:
self.template_editor.template_name.setCurrentIndex(idx)
def st_delete_button_clicked(self):
name = unicode_type(self.template_editor.template_name.currentText())
if name in self.st_funcs:
del self.st_funcs[name]
self.changed_signal.emit()
self.st_create_button.setEnabled(True)
self.st_delete_button.setEnabled(False)
self.st_build_function_names_box()
self.template_editor.textbox.setReadOnly(False)
else:
error_dialog(self.gui, _('Stored templates'),
_('Function not defined'), show=True)
def st_create_button_clicked(self, use_name=None):
self.changed_signal.emit()
name = use_name if use_name else unicode_type(self.template_editor.template_name.currentText())
for k,v in formatter_functions().get_functions().items():
if k == name and v.is_python:
error_dialog(self.gui, _('Stored templates'),
_('Name %s is already used for template function')%(name,), show=True)
try:
prog = unicode_type(self.template_editor.textbox.toPlainText())
if not prog.startswith('program:'):
error_dialog(self.gui, _('Stored templates'),
_('The stored template must begin with "program:"'), show=True)
cls = compile_user_function(name, unicode_type(self.template_editor.new_doc.toPlainText()),
0, prog)
self.st_funcs[name] = cls
self.st_build_function_names_box(scroll_to=name)
except:
error_dialog(self.gui, _('Stored templates'),
_('Exception while storing template'), show=True,
det_msg=traceback.format_exc())
def st_template_name_edited(self, txt):
b = txt in self.st_funcs
self.st_create_button.setEnabled(not b)
self.st_replace_button.setEnabled(b)
self.st_delete_button.setEnabled(b)
self.template_editor.textbox.setReadOnly(False)
def st_function_index_changed(self, txt):
txt = unicode_type(txt)
self.st_create_button.setEnabled(False)
if not txt:
self.template_editor.textbox.clear()
self.template_editor.new_doc.clear()
return
func = self.st_funcs[txt]
self.template_editor.new_doc.setPlainText(func.doc)
self.template_editor.textbox.setPlainText(func.program_text)
self.st_template_name_edited(txt)
def st_replace_button_clicked(self):
name = unicode_type(self.template_editor.template_name.currentText())
self.st_delete_button_clicked()
self.st_create_button_clicked(use_name=name)
def commit(self):
# formatter_functions().reset_to_builtins()
pref_value = []
for name, cls in iteritems(self.funcs):
print(name)
if name not in self.builtins:
pref_value.append((cls.name, cls.doc, cls.arg_count, cls.program_text))
pref_value.append(cls.to_pref())
for v in self.st_funcs.values():
pref_value.append(v.to_pref())
self.db.new_api.set_pref('user_template_functions', pref_value)
funcs = compile_user_template_functions(pref_value)
self.db.new_api.set_user_template_functions(funcs)

View File

@ -6,155 +6,257 @@
<rect>
<x>0</x>
<y>0</y>
<width>798</width>
<height>672</height>
<width>788</width>
<height>663</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<item row="0" column="0">
<widget class="ScrollingTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Function:</string>
</property>
<property name="buddy">
<cstring>function_name</cstring>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>&amp;Stored Templates</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2" stretch="0">
<widget class="QTextBrowser" name="st_textBrowser"/>
</item>
<item row="3" column="1" stretch="1">
<widget class="EmbeddedTemplateDialog" name="template_editor">
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="function_name">
<property name="toolTip">
<string>Enter the name of the function to create.</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>Argument &amp;count:</string>
</property>
<property name="buddy">
<cstring>argument_count</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="argument_count">
<property name="toolTip">
<string>Set this to -1 if the function takes a variable number of arguments</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QTextEdit" name="documentation"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Documentation:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="buddy">
<cstring>documentation</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item row="3" column="0">
<layout class="QVBoxLayout" name="st_button_layout">
<item>
<widget class="QPushButton" name="clear_button">
<widget class="QPushButton" name="st_clear_button">
<property name="text">
<string>&amp;Clear</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="delete_button">
<widget class="QPushButton" name="st_delete_button">
<property name="text">
<string>&amp;Delete</string>
<string>D&amp;elete</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="replace_button">
<widget class="QPushButton" name="st_replace_button">
<property name="text">
<string>&amp;Replace</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="create_button">
<widget class="QPushButton" name="st_create_button">
<property name="text">
<string>C&amp;reate</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="horizontalLayout1">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Program code (Follow Python indenting rules):</string>
<item row="5" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="buddy">
<cstring>program</cstring>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="program">
<property name="minimumSize">
<property name="sizeHint" stdset="0">
<size>
<width>400</width>
<height>0</height>
<width>20</width>
<height>400</height>
</size>
</property>
<property name="documentTitle">
<string notr="true"/>
</property>
<property name="tabStopWidth">
<number>30</number>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>&amp;Template Functions</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>F&amp;unction:</string>
</property>
<property name="buddy">
<cstring>function_name</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="function_name">
<property name="toolTip">
<string>Enter the name of the function to create.</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>Argument &amp;count:</string>
</property>
<property name="buddy">
<cstring>argument_count</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="argument_count">
<property name="toolTip">
<string>Set this to -1 if the function takes a variable number of arguments</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QTextEdit" name="documentation"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>D&amp;ocumentation:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="buddy">
<cstring>documentation</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="clear_button">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="delete_button">
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="replace_button">
<property name="text">
<string>Replace</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="create_button">
<property name="text">
<string>C&amp;reate</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="horizontalLayout1">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>P&amp;rogram code (Follow Python indenting rules):</string>
</property>
<property name="buddy">
<cstring>program</cstring>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="program">
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="documentTitle">
<string notr="true"/>
</property>
<property name="tabStopWidth">
<number>30</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTextBrowser" name="textBrowser"/>
</item>
</layout>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTextBrowser" name="textBrowser"/>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
<customwidgets>
<customwidget>
<class>EmbeddedTemplateDialog</class>
<extends>TemplateDialog</extends>
<header>calibre/gui2/dialogs/template_dialog.h</header>
</customwidget>
<customwidget>
<class>TemplateDialog</class>
<extends>QDialog</extends>
<header>calibre/gui2/dialogs/template_dialog.h</header>
</customwidget>
<customwidget>
<class>ScrollingTabWidget</class>
<extends>QTabWidget</extends>
<header>calibre/gui2/widgets2.h</header>
<container>1</container>
</customwidget>
</customwidgets>
</ui>

View File

@ -28,6 +28,8 @@ class Node(object):
NODE_CONSTANT = 7
NODE_FIELD = 8
NODE_RAW_FIELD = 9
NODE_CALL = 10
NODE_ARGUMENTS = 11
class IfNode(Node):
@ -55,6 +57,21 @@ class FunctionNode(Node):
self.expression_list = expression_list
class CallNode(Node):
def __init__(self, function, expression_list):
Node.__init__(self)
self.node_type = self.NODE_CALL
self.function = function
self.expression_list = expression_list
class ArgumentsNode(Node):
def __init__(self, expression_list):
Node.__init__(self)
self.node_type = self.NODE_ARGUMENTS
self.expression_list = expression_list
class StringInfixNode(Node):
def __init__(self, operator, left, right):
Node.__init__(self)
@ -188,6 +205,13 @@ class _Parser(object):
except:
return False
def token_is_call(self):
try:
token = self.prog[self.lex_pos]
return token[1] == 'call' and token[0] == self.LEX_KEYWORD
except:
return False
def token_is_if(self):
try:
token = self.prog[self.lex_pos]
@ -228,9 +252,11 @@ class _Parser(object):
except:
return True
def program(self, funcs, prog):
def program(self, parent, funcs, prog):
self.lex_pos = 0
self.parent = parent
self.funcs = funcs
self.func_names = frozenset(set(self.funcs.keys()) | {'arguments',})
self.prog = prog[0]
self.prog_len = len(self.prog)
if prog[1] != '':
@ -276,6 +302,18 @@ class _Parser(object):
return NumericInfixNode(operator, left, self.expr())
return left
def call_expression(self, name, arguments):
subprog = self.funcs[name].cached_parse_tree
if subprog is None:
text = self.funcs[name].program_text
if not text.startswith('program:'):
self.error((_('A stored template must begin with program:')))
text = text[len('program:'):]
subprog = _Parser().program(self, self.funcs,
self.parent.lex_scanner.scan(text))
self.funcs[name].cached_parse_tree = subprog
return CallNode(subprog, arguments)
def expr(self):
if self.token_is_if():
return self.if_expression()
@ -293,7 +331,7 @@ class _Parser(object):
# Check if it is a known one. We do this here so error reporting is
# better, as it can identify the tokens near the problem.
id_ = id_.strip()
if id_ not in self.funcs:
if id_ not in self.func_names:
self.error(_('Unknown function {0}').format(id_))
# Eat the paren
self.consume()
@ -314,9 +352,21 @@ class _Parser(object):
return IfNode(arguments[0], (arguments[1],), (arguments[2],))
if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE):
return AssignNode(arguments[0].name, arguments[1])
if id_ == 'arguments':
new_args = []
for arg in arguments:
if arg.node_type not in (Node.NODE_ASSIGN, Node.NODE_RVALUE):
self.error(_("Parameters to 'arguments' must be "
"variables or assignments"))
if arg.node_type == Node.NODE_RVALUE:
arg = AssignNode(arg.name, ConstantNode(''))
new_args.append(arg)
return ArgumentsNode(new_args)
if id_ in self.func_names and not self.funcs[id_].is_python:
return self.call_expression(id_, arguments)
cls = self.funcs[id_]
if cls.arg_count != -1 and len(arguments) != cls.arg_count:
self.error(_('Incorrect number of expression_list for function {0}').format(id_))
self.error(_('Incorrect number of arguments for function {0}').format(id_))
return FunctionNode(id_, arguments)
elif self.token_is_constant():
# String or number
@ -330,12 +380,14 @@ class _Interpreter(object):
m = 'Interpreter: ' + message
raise ValueError(m)
def program(self, funcs, parent, prog, val):
def program(self, funcs, parent, prog, val, is_call=False, args=None):
self.parent = parent
self.parent_kwargs = parent.kwargs
self.parent_book = parent.book
self.funcs = funcs
self.locals = {'$':val}
if is_call:
return self.do_node_call(CallNode(prog, None), args=args)
return self.expression_list(prog)
def expression_list(self, prog):
@ -408,6 +460,25 @@ class _Interpreter(object):
return cls.eval_(self.parent, self.parent_kwargs,
self.parent_book, self.locals, *args)
def do_node_call(self, prog, args=None):
if args is None:
args = list()
for arg in prog.expression_list:
# evaluate the expression (recursive call)
args.append(self.expr(arg))
saved_locals = self.locals
self.locals = {}
for dex, v in enumerate(args):
self.locals['*arg_'+ str(dex)] = v
val = self.expression_list(prog.function)
self.locals = saved_locals
return val
def do_node_arguments(self, prog):
for dex,arg in enumerate(prog.expression_list):
self.locals[arg.left] = self.locals.get('*arg_'+ str(dex), self.expr(arg.right))
return ''
def do_node_constant(self, prog):
return prog.value
@ -454,6 +525,8 @@ class _Interpreter(object):
Node.NODE_RAW_FIELD: do_node_raw_field,
Node.NODE_STRING_INFIX: do_node_string_infix,
Node.NODE_NUMERIC_INFIX: do_node_numeric_infix,
Node.NODE_ARGUMENTS: do_node_arguments,
Node.NODE_CALL: do_node_call,
}
def expr(self, prog):
@ -548,18 +621,24 @@ class TemplateFormatter(string.Formatter):
], flags=re.DOTALL)
def _eval_program(self, val, prog, column_name):
# keep a cache of the lex'ed program under the theory that re-lexing
# is much more expensive than the cache lookup. This is certainly true
# for more than a few tokens, but it isn't clear for simple programs.
if column_name is not None and self.template_cache is not None:
tree = self.template_cache.get(column_name, None)
if not tree:
tree = self.gpm_parser.program(self.funcs, self.lex_scanner.scan(prog))
tree = self.gpm_parser.program(self, self.funcs, self.lex_scanner.scan(prog))
self.template_cache[column_name] = tree
else:
tree = self.gpm_parser.program(self.funcs, self.lex_scanner.scan(prog))
tree = self.gpm_parser.program(self, self.funcs, self.lex_scanner.scan(prog))
return self.gpm_interpreter.program(self.funcs, self, tree, val)
def _eval_sfm_call(self, template_name, args):
func = self.funcs[template_name]
tree = func.cached_parse_tree
if tree is None:
tree = self.gpm_parser.program(self, self.funcs,
self.lex_scanner.scan(func.program_text[len('program:'):]))
func.cached_parse_tree = tree
return self.gpm_interpreter.program(self.funcs, self, tree, None,
is_call=True, args=args)
# ################# Override parent classes methods #####################
def get_value(self, key, args, kwargs):
@ -613,17 +692,22 @@ class TemplateFormatter(string.Formatter):
else:
args = self.arg_parser.scan(fmt[p+1:])[0]
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
if (func.arg_count == 1 and (len(args) != 1 or args[0])) or \
(func.arg_count > 1 and func.arg_count != len(args)+1):
raise ValueError('Incorrect number of expression_list for function '+ fmt[0:p])
if func.arg_count == 1:
val = func.eval_(self, self.kwargs, self.book, self.locals, val)
if self.strip_results:
val = val.strip()
if not func.is_python:
args.insert(0, val)
val = self._eval_sfm_call(fname, args)
else:
val = func.eval_(self, self.kwargs, self.book, self.locals, val, *args)
if self.strip_results:
val = val.strip()
if (func.arg_count == 1 and (len(args) != 1 or args[0])) or \
(func.arg_count > 1 and func.arg_count != len(args)+1):
raise ValueError(
_('Incorrect number of arguments for function {0}').format(fmt[0:p]))
if func.arg_count == 1:
val = func.eval_(self, self.kwargs, self.book, self.locals, val)
if self.strip_results:
val = val.strip()
else:
val = func.eval_(self, self.kwargs, self.book, self.locals, val, *args)
if self.strip_results:
val = val.strip()
else:
return _('%s: unknown function')%fname
if val:

View File

@ -126,6 +126,7 @@ class FormatterFunction(object):
category = 'Unknown'
arg_count = 0
aliases = []
is_python = True
def evaluate(self, formatter, kwargs, mi, locals, *args):
raise NotImplementedError()
@ -1813,17 +1814,36 @@ _formatter_builtins = [
class FormatterUserFunction(FormatterFunction):
def __init__(self, name, doc, arg_count, program_text):
def __init__(self, name, doc, arg_count, program_text, is_python):
self.is_python = is_python
self.name = name
self.doc = doc
self.arg_count = arg_count
self.program_text = program_text
self.cached_parse_tree = None
def to_pref(self):
return [self.name, self.doc, self.arg_count, self.program_text]
tabs = re.compile(r'^\t*')
def function_pref_is_python(pref):
if isinstance(pref, list):
pref = pref[3]
if pref.startswith('def'):
return True
if pref.startswith('program'):
return False
raise ValueError('Unknown program type in formatter function pref')
def function_pref_name(pref):
return pref[0]
def compile_user_function(name, doc, arg_count, eval_func):
if not function_pref_is_python(eval_func):
return FormatterUserFunction(name, doc, arg_count, eval_func, False)
def replace_func(mo):
return mo.group().replace('\t', ' ')
@ -1838,7 +1858,7 @@ class UserFunction(FormatterUserFunction):
if DEBUG and tweaks.get('enable_template_debug_printing', False):
print(prog)
exec(prog, locals_)
cls = locals_['UserFunction'](name, doc, arg_count, eval_func)
cls = locals_['UserFunction'](name, doc, arg_count, eval_func, True)
return cls
@ -1855,6 +1875,7 @@ def compile_user_template_functions(funcs):
# then white space differences don't cause them to compare differently
cls = compile_user_function(*func)
cls.is_python = function_pref_is_python(func)
compiled_funcs[cls.name] = cls
except Exception:
try:
@ -1862,7 +1883,7 @@ def compile_user_template_functions(funcs):
except Exception:
func_name = 'Unknown'
prints('**** Compilation errors in user template function "%s" ****' % func_name)
traceback.print_exc(limit=0)
traceback.print_exc(limit=10)
prints('**** End compilation errors in %s "****"' % func_name)
return compiled_funcs