diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py
index 8416c5a581..fa15c0a973 100644
--- a/src/calibre/gui2/preferences/template_functions.py
+++ b/src/calibre/gui2/preferences/template_functions.py
@@ -38,7 +38,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
This parameter can be None in some cases, such as when evaluating
non-book templates.
locals: the local variables assigned to by the current
- template program. Your_arguments must be one or more parameter (number
+ template program.
+ Your_arguments must be one or more parameter (number
matching the arg count box), or the value *args for a variable number
of arguments. These are values passed into the function. One argument
is required, and is usually the value of the field being operated upon.
diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst
index f64a413d3e..f63a7c4e95 100644
--- a/src/calibre/manual/template_lang.rst
+++ b/src/calibre/manual/template_lang.rst
@@ -308,6 +308,12 @@ 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
+-------------------------------
+
+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.
+
Special notes for save/send templates
-------------------------------------
diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py
index 2d74885942..abcb3021be 100644
--- a/src/calibre/utils/formatter.py
+++ b/src/calibre/utils/formatter.py
@@ -26,7 +26,7 @@ class _Parser(object):
if prog[1] != '':
self.error(_('failed to scan program. Invalid input {0}').format(prog[1]))
self.parent = parent
- self.variables = {'$':val}
+ self.parent.locals = {'$':val}
def error(self, message):
m = 'Formatter: ' + message + _(' near ')
@@ -88,18 +88,20 @@ class _Parser(object):
def expr(self):
if self.token_is_id():
+ funcs = formatter_functions.get_functions()
# We have an identifier. Determine if it is a function
id = self.token()
if not self.token_op_is_a('('):
if self.token_op_is_a('='):
# classic assignment statement
self.consume()
- return self._assign(id, self.expr())
- return self.variables.get(id, _('unknown id ') + id)
+ cls = funcs['assign']
+ return cls.eval(self.parent, self.parent.kwargs,
+ self.parent.book, self.parent.locals, id, self.expr())
+ return self.parent.locals.get(id, _('unknown id ') + id)
# We have a function.
# 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.
- funcs = formatter_functions.get_functions()
if id not in funcs:
self.error(_('unknown function {0}').format(id))
@@ -129,7 +131,7 @@ class _Parser(object):
if cls.arg_count != -1 and len(args) != cls.arg_count:
self.error('incorrect number of arguments for function {}'.format(id))
return cls.eval(self.parent, self.parent.kwargs,
- self.parent.book, locals, *args)
+ self.parent.book, self.parent.locals, *args)
else:
f = self.parent.functions[id]
if f[0] != -1 and len(args) != f[0]+1:
@@ -159,6 +161,7 @@ class TemplateFormatter(string.Formatter):
self.book = None
self.kwargs = None
self.program_cache = {}
+ self.locals = {}
def _do_format(self, val, fmt):
if not fmt or not val:
@@ -286,9 +289,9 @@ class TemplateFormatter(string.Formatter):
print args
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])
if func.arg_count == 1:
- val = func.eval(self, self.kwargs, self.book, locals, val).strip()
+ val = func.eval(self, self.kwargs, self.book, self.locals, val).strip()
else:
- val = func.eval(self, self.kwargs, self.book, locals,
+ val = func.eval(self, self.kwargs, self.book, self.locals,
val, *args).strip()
if val:
val = self._do_format(val, dispfmt)
@@ -309,6 +312,7 @@ class TemplateFormatter(string.Formatter):
self.kwargs = kwargs
self.book = book
self.composite_values = {}
+ self.locals = {}
try:
ans = self.vformat(fmt, [], kwargs).strip()
except Exception, e:
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index c0ef6b1e81..b0895ce1b3 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -74,6 +74,7 @@ class FormatterFunction(object):
if isinstance(ret, list):
return ','.join(list)
except:
+ traceback.print_exc()
return _('Function threw exception' + traceback.format_exc())
class BuiltinStrcmp(FormatterFunction):
@@ -327,7 +328,7 @@ class BuiltinEvaluate(FormatterFunction):
return value_if_empty
class BuiltinShorten(FormatterFunction):
- name = 'shorten '
+ name = 'shorten'
arg_count = 4
doc = _('shorten(val, left chars, middle text, right chars) -- Return a '
'shortened version of the field, consisting of `left chars` '
@@ -448,8 +449,13 @@ class FormatterUserFunction(FormatterFunction):
self.arg_count = arg_count
self.program_text = program_text
+tabs = re.compile(r'^\t*')
def compile_user_function(name, doc, arg_count, eval_func):
- func = '\t' + eval_func.replace('\n', '\n\t')
+ def replace_func(mo):
+ return mo.group().replace('\t', ' ')
+
+ func = ' ' + '\n '.join([tabs.sub(replace_func, line )
+ for line in eval_func.splitlines()])
prog = '''
from calibre.utils.formatter_functions import FormatterUserFunction
class UserFunction(FormatterUserFunction):