diff --git a/README.md b/README.md
index d4366c2f1a77d70fce9d513910bcd79af230e694..1a374a3f9b457b31329eed6f7114ed1285bd3041 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
-# slider
+# Slider
 
-Slide overlay software based on beamer and inkscape
\ No newline at end of file
+Slide overlay software based on beamer and inkscape. This project is currently used in coursebox. 
+The software also offers a package for jinja2 (jinjafy) which offers a handful of convenient extensions.
\ No newline at end of file
diff --git a/inkscape2tex.py b/inkscape2tex.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b7e68465555c97f1f4f3169a8eb0e905e7aa13d
--- /dev/null
+++ b/inkscape2tex.py
@@ -0,0 +1,183 @@
+# #!/usr/bin/env python
+# """Convert inkscape SVG files to TeX input.
+#
+# - SVG to PDF or EPS with inkscape, optionally with LaTeX output.
+# - DOT to SVG
+#
+# Skips conversion if PDF file found newer than SVG source.
+# Requires `inkscape` in path.
+# """
+# # Copyright 2010-2017 by Ioannis Filippidis
+# # All rights reserved. Licensed under BSD-2.
+# #
+# import argparse
+# import datetime
+# import fnmatch
+# import logging
+# import os
+# import shlex
+# import subprocess
+# import time
+#
+# import humanize
+#
+# import svg2latex as convert
+# # from svglatex import convert
+#
+#
+# log = logging.getLogger(__name__)
+#
+#
+# def main():
+#     """Start from here."""
+#     args = parse_args()
+#     f = '{name}.svg'.format(name=args.input_file)
+#     out_type = args.method
+#     if './img/' in f:
+#         files = [f]
+#     else:
+#         files = locate(f, './img')
+#     svg = None
+#     for svg in files:
+#         log.info('Will convert SVG file "{f}" to {t}'.format(
+#             f=svg, t=out_type))
+#         convert_if_svg_newer(svg, out_type)
+#     if svg is None:
+#         raise Exception(
+#             'SVG file "{f}" not found! '
+#             'Cannot export to PDF.'.format(f=f))
+#
+#
+# def parse_args():
+#     """Parse command-line arguments using."""
+#     parser = argparse.ArgumentParser()
+#     parser.add_argument(
+#         '-i', '--input-file', type=str,
+#         help=(
+#             'Name (w/o extension) of SVG file. '
+#             'Either file name to search for under `./img`, '
+#             'or path that starts with `./img`.'))
+#     choices = [
+#         'latex-pdf', 'pdf',
+#         'latex-eps', 'eps']
+#     parser.add_argument(
+#         '-m', '--method', type=str, choices=choices,
+#         help=(
+#             'Export to this file type. '
+#             'The prefix "latex" produces also a file `*.pdf_tex` '
+#             'that contains the text from the SVG. '
+#             'The command `\includesvgpdf` passes `pdf`, '
+#             'and `\includesvg` passes `latex-pdf`.'))
+#     args = parser.parse_args()
+#     return args
+#
+#
+# def convert_if_svg_newer(svg, out_type):
+#     """Convert SVG file to PDF or EPS."""
+#     base, ext = os.path.splitext(svg)
+#     assert ext == '.svg', ext
+#     if 'pdf' in out_type:
+#         out = base + '.pdf'
+#     elif 'eps' in out_type:
+#         out = base + '.eps'
+#     else:
+#         raise ValueError(out_type)
+#     if not os.access(svg, os.F_OK):
+#         raise FileNotFoundError(
+#             'No SVG file "{f}"'.format(f=svg))
+#     fresh = is_newer(out, svg)
+#     if out_type == 'latex-pdf':
+#         pdf_tex = base + '.pdf_tex'
+#         fresh &= is_newer(pdf_tex, svg)
+#     if fresh:
+#         log.info('No update needed, target newer than SVG.')
+#         return
+#     log.info('File not found or old. Converting from SVG...')
+#     convert_svg(svg, out, out_type)
+#
+#
+# def is_newer(target, source):
+#     """Return `True` if `target` newer than `source` file."""
+#     assert os.path.isfile(source), source
+#     if not os.path.isfile(target):
+#         return False
+#     t_src = os.stat(source)[8]
+#     t_tgt = os.stat(target)[8]
+#     _print_dates(source, target, t_src, t_tgt)
+#     return t_src < t_tgt
+#
+#
+# def _print_dates(source, target, t_src, t_tgt):
+#     s = _format_time(t_src)
+#     t = _format_time(t_tgt)
+#     log.info((
+#         'last modification dates:\n'
+#         '    Source ({source}): {s}\n'
+#         '    Target ({target}): {t}').format(
+#             source=source, target=target,
+#             s=s, t=t))
+#
+#
+# def _format_time(t):
+#     """Return time readable by humans."""
+#     return humanize.naturaltime(
+#         datetime.datetime.fromtimestamp(t))
+#
+#
+# def convert_svg(svg, out, out_type):
+#     """Convert from SVG to output format."""
+#     # TODO: implement options `latex-eps`, `eps`
+#     assert out_type in ('latex-pdf', 'pdf'), out_type
+#     if out_type == 'latex-pdf':
+#         convert.main(svg)
+#     elif out_type == 'pdf':
+#         inkscape = convert.which_inkscape()
+#         svg_path = os.path.realpath(svg)
+#         out_path = os.path.realpath(out)
+#         args = [
+#             inkscape,
+#             '--without-gui',
+#             '--export-area-drawing',
+#             '--export-ignore-filters',
+#             '--export-dpi={dpi}'.format(dpi=96),
+#             '--export-pdf={out}'.format(out=out_path),
+#             svg_path]
+#         r = subprocess.call(args)
+#         if r != 0:
+#             raise Exception('Conversion error')
+#
+#
+# def convert_svg_using_inkscape(svg, out, out_type):
+#     """Convert from SVG to output format."""
+#     # inkscape need be called with an absolute path on OS X
+#     # http://wiki.inkscape.org/wiki/index.php/MacOS_X
+#     symlink_relpath = 'bin/inkscape'
+#     home = os.path.expanduser('~')
+#     symlink_abspath = os.path.join(home, symlink_relpath)
+#     inkscape_abspath = os.path.realpath(symlink_abspath)
+#     svg_abspath = os.path.realpath(svg)
+#     args = ['{inkscape_abspath} -z -D --file={svg}'.format(
+#         inkscape_abspath=inkscape_abspath, svg=svg_abspath)]
+#     if 'pdf' in out_type:
+#         args.append('--export-pdf={pdf}'.format(pdf=out))
+#     if 'eps' in out_type:
+#         args.append('--export-eps={eps}'.format(eps=out))
+#     if 'latex' in out_type:
+#         args.append('--export-latex')
+#     args = shlex.split(' '.join(args))
+#     r = subprocess.call(args)
+#     if r != 0:
+#         raise Exception(
+#             'conversion from "{svg}" to "{out}" failed'.format(
+#                 svg=svg, out=out))
+#
+#
+# def locate(pattern, root=os.curdir):
+#     """Locate all files matching supplied filename pattern under `root`."""
+#     for path, dirs, files in os.walk(os.path.abspath(root)):
+#         for filename in fnmatch.filter(files, pattern):
+#             yield os.path.join(path, filename)
+#
+#
+# if __name__ == '__main__':
+#     main()
diff --git a/setup.py b/setup.py
index 0ebabc692c7c2d72bbd0aa9ad88dcd7a418dd6bf..0076be64071cdafef02c2781ce15af8de75b86af 100644
--- a/setup.py
+++ b/setup.py
@@ -27,5 +27,5 @@ setuptools.setup(
     package_dir={"": "src"},
     packages=setuptools.find_packages(where="src"),
     python_requires=">=3.8",
-    install_requires=['jinja2',],
+    install_requires=['jinja2', 'numpy', 'scipy', 'bs4', 'lxml', 'codecs', 'optparse', 'PyPDF2', 'pickle'],
 )
diff --git a/src/jinjafy/__init__.py b/src/jinjafy/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a00d8cd422222546dfd99ba2f052660bc562784
--- /dev/null
+++ b/src/jinjafy/__init__.py
@@ -0,0 +1,26 @@
+from jinjafy.jinjafy import jinjafy_comment
+from jinjafy.jinjafy import jinjafy_template
+from jinjafy.jinja_matlab_load import matlab_load
+from jinjafy.textools import mat2table
+import subprocess
+# from subprocess import subprocess
+
+# def get_system_name():
+#     if is_win():
+#         return "Win"
+#     if is_compute():
+#         return "thinlinc.compute.dtu.dk"
+#     if is_cogsys_cluster():
+#         return "cogys cluster"
+
+def execute_command(command, shell=True):
+    if not isinstance(command, list):
+        command = [command]
+    # if not is_compute():
+    # result = subprocess.run(command, stdout=subprocess.PIPE, shell=shell)
+    # out = result.stdout
+    # else:
+    out = subprocess.check_output(command, shell=shell)
+    s = out.decode("utf-8")
+    OK = True
+    return s, OK
\ No newline at end of file
diff --git a/src/jinjafy/cache/__init__.py b/src/jinjafy/cache/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2e522c2f3baf4762462cd8dac312e446a3e4323
--- /dev/null
+++ b/src/jinjafy/cache/__init__.py
@@ -0,0 +1,8 @@
+from thtools.cache.simplecache import cache_update_str as cache_update_str
+from thtools.cache.simplecache import cache_contains_str as cache_contains_str
+
+from thtools.cache.simplecache import cache_update_file as cache_update_file
+from thtools.cache.simplecache import cache_contains_file as cache_contains_file
+
+from thtools.cache.simplecache import cache_update_dir as cache_update_dir
+from thtools.cache.simplecache import cache_contains_dir as cache_contains_dir
\ No newline at end of file
diff --git a/src/jinjafy/cache/simplecache.py b/src/jinjafy/cache/simplecache.py
new file mode 100644
index 0000000000000000000000000000000000000000..e24ab194c44642e727a0eae4ce6e281f3a607fd8
--- /dev/null
+++ b/src/jinjafy/cache/simplecache.py
@@ -0,0 +1,94 @@
+from hashlib import md5
+import os
+import pickle
+import glob
+
+def dir_content_cache_(dir, pattern="*"):
+    fl = glob.glob(dir + "/" + pattern)
+    s = ''.join(fl)
+    key = "key_"+dir
+    return fl, s,key
+
+def cache_contains_dir(cache_base, dir, pattern="*"):
+    # fl = glob.glob(dir)
+    fl,s,key =  dir_content_cache_(dir, pattern=pattern)
+
+    v = [cache_contains_file(cache_base, f) for f in fl]
+    if all(v) and cache_contains_str(cache_base, key, s):
+        return True
+    return False
+
+def cache_update_dir(cache_base, dir, pattern="*"):
+    fl, s, key = dir_content_cache_(dir, pattern=pattern)
+    cache_update_str(cache_base, key, s)
+    for f in fl:
+        cache_update_file(cache_base, f)
+
+
+def cache_contains_str(cache_base,key=None,value=None):
+    assert(key or value)
+    value = hash_binary_(value.encode())
+    if not key: key = value
+    return cache_contains_hash(cache_base, key, value)
+
+def cache_update_str(cache_base,key,value):
+    assert(key or value)
+    value = hash_binary_(value.encode())
+    if not key: key = value
+    return cache_update_hash(cache_base, key, value)
+
+
+def cache_contains_file(cache_base,file):
+    key = os.path.abspath(file)
+    if not os.path.exists(file):
+        return False
+    value = hash_file_(file)
+    return cache_contains_hash(cache_base, key, value)
+
+def hash_file_(file):
+    import hashlib
+    hasher = hashlib.md5()
+    with open(file, 'rb') as afile:
+        buf = afile.read()
+        hasher.update(buf)
+    return hasher.hexdigest()
+
+def cache_update_file(cache_base, file):
+    key = os.path.abspath(file)
+    value = hash_file_(file)
+    return cache_update_hash(cache_base, key, value)
+
+
+def cache_contains_hash(cache_base,key,hash_val):
+    cc = load_cache(cache_base)
+    return cc.get(key,"Not found") == hash_val
+
+def cache_update_hash(cache_base,key,hash_val):
+    cc = load_cache(cache_base)
+    cc[key] = hash_val
+    save_cache(cache_base, cc)
+
+
+def hash_binary_(str_bin):
+    return md5(str_bin).hexdigest()
+
+
+def cache_file(cache_base):
+    return os.path.join(cache_base, "cache.pkl")
+
+def save_cache(cache_base, cache):
+    with open(cache_file(cache_base), 'wb') as f:
+        pickle.dump(cache,f)
+
+def load_cache(cache_base):
+    if not os.path.exists(cache_file(cache_base)):
+        save_cache(cache_base, {'default' : 42})
+        return load_cache(cache_base)
+    with open(cache_file(cache_base), 'rb') as f:
+        return pickle.load(f)
+
+
+if __name__ == "__main__":
+    cache_base = "./"
+
+    print("Hello World")
diff --git a/src/jinjafy/jinja_env.py b/src/jinjafy/jinja_env.py
new file mode 100644
index 0000000000000000000000000000000000000000..102ef966eba2420c374279e5d1d32d67cdfaa4ba
--- /dev/null
+++ b/src/jinjafy/jinja_env.py
@@ -0,0 +1,136 @@
+import numpy as np
+from fractions import Fraction
+import jinja2
+
+
+def format_list_symbols(list, pattern, symbol="x", seperator=",\ "):
+    return format_join(list, pattern=symbol+"_{%i}", seperator=seperator)
+
+
+def n2w(i):
+    w = {0: 'zero', 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six', 7: 'seven', 8: 'eight',
+         9: 'nine', 10: 'ten'}
+    return i if i < 0 or i > 10 else w[i]
+
+
+def format_list(list_, pattern):
+    list_ = tolist(list_)
+    return [pattern % s for s in list_]
+
+
+def format_join(list, pattern, seperator=",\ ",withand=False,withor=False,lastsep=None):
+    ls = format_list(list, pattern)
+    if withand:
+        return seperator.join(ls[:-1]) + "$, and $" + ls[-1]
+    if withor:
+        return seperator.join(ls[:-1]) + "$, or $" + ls[-1]
+    return seperator.join(ls)
+
+
+def format_join_enum(list, pattern="x_{%i}=%g", seperator=",\ "):
+    list = tolist(list)
+    return seperator.join(format_list( zip( range(1,len(list)+1 ), list), pattern))
+
+
+def as_set(l, symbol='f_{%i}'):
+    if type(l) != list and type(l) != np.ndarray:
+        l = [l]
+    l = tolist(l)
+    s = [symbol%(i,) for i in l]
+    s = '\{' + ", ".join(s) + "\}"
+    return s
+
+
+def as_set_list(ll, symbol='%g'):
+    s = []
+    for l in ll.flat:
+        l = tolist(l)
+        s.append(as_set(l, symbol))
+    s = ["$"+ds+"$" for ds in s]
+    s = ", ".join(s)
+    return s
+
+
+def infty(n,tol=10^8):
+    if n > tol:
+        s = '\infty'
+    else:
+        s = str(n)
+    return s
+
+
+def flatten(ar):
+    v = []
+    if type(ar) is np.ndarray or type(ar) is np.array:
+        for x in ar.flat:
+            m = flatten(x)
+            if type(m) == list:
+                v = v + m
+            else:
+                v.append(m)
+    else:
+        v = ar
+    return v
+
+
+def tolist(l):
+    if type(l) == np.ndarray:
+        l2 = []
+        for x in l.flat:
+            l2.append(  x.tolist() if isinstance(x,np.ndarray) else x  )
+        l = l2
+
+    elif type(l) == list or hasattr(l, '__iter__'):
+        pass
+    else:
+        l = [l]
+    return l
+
+
+def jget(A,n=0):
+    A = flatten(A)
+    return A[n]
+
+
+def as_rational(x, output='tex', always_frac=False):
+    if type(x) == jinja2.runtime.Undefined:
+        return "UNDEFINED(jinja2)"
+    b = Fraction.from_float(x).limit_denominator(10000)
+    s = "output_error_in_as_rational_filter"
+    if output == 'tex':
+        if (b.denominator == 1 or b.numerator == 0) and not always_frac:
+            s = '%i'%b.numerator
+        else:
+            s = "\\frac{%i}{%i}"%(b.numerator, b.denominator)
+    return s
+
+
+def mylen(l):
+    if isinstance(l, np.ndarray):
+        sz = l.size
+    else:
+        sz = len(l)
+    return sz
+
+
+def permute_exam_answers(section,permutation):
+    v = section.split("\\item")
+    v = v[:5] + v[-1:]
+    assert(len(v) == 6)
+    permutation = [0] + permutation + [5]
+    v[0] = "\\begin{answer}[%i]\n"%permutation.index(1)
+    v2 = "\\item".join( [v[i] for i in permutation] )
+    return v2
+
+
+def startswithvowel(value):
+    if value.lower().startswith(("a", "e", "i", "o","u")):
+        return True
+    else:
+        return False
+
+
+def aan(s):
+    if s.startswith("no "):
+        return ""
+    return "an" if startswithvowel(s) else "a"
diff --git a/src/jinjafy/jinja_matlab_load.py b/src/jinjafy/jinja_matlab_load.py
new file mode 100644
index 0000000000000000000000000000000000000000..c67581542aaf2fc407f65489a2db9a1713770335
--- /dev/null
+++ b/src/jinjafy/jinja_matlab_load.py
@@ -0,0 +1,149 @@
+import numpy as np
+import scipy.io as spio
+
+def matlab_load(mfile):
+    j = mfile.rfind('.')
+    if j > -1:
+        ex = mfile[j + 1:]
+        base = mfile[:j]
+    else:
+        ex = ''
+        base = mfile
+    mat = loadmat(base + '.mat')
+    mat = uuroll(mat)
+    mat = fix_1_arrays(mat)
+    mat = fix_strings(mat)
+    mat = fix_simple_lists(mat)
+    return mat
+
+
+def loadmat(filename):
+    '''
+    this function should be called instead of direct spio.loadmat
+    as it cures the problem of not properly recovering python dictionaries
+    from mat files. It calls the function check keys to cure all entries
+    which are still mat-objects
+    '''
+    data = spio.loadmat(filename,struct_as_record=False)
+    data2 = _check_keys(data)
+    return data2
+
+
+def _check_keys(dd):
+    '''
+    checks if entries in dictionary are mat-objects. If yes
+    todict is called to change them to nested dictionaries
+    '''
+    if isinstance(dd, spio.matlab.mio5_params.mat_struct):
+        dd = _check_keys(_todict(dd))
+    elif type(dd) == dict:
+        for key in dd:
+            kv = flist(dd[key])
+            if type( kv ) == spio.matlab.mio5_params.mat_struct:
+                dd[key] = _check_keys(kv)
+            else:
+                dd[key] = _check_keys(dd[key])
+    elif type(dd) == list:
+        dd = [_check_keys(l) for l in dd]
+    elif type(dd) == np.ndarray:
+        if dd.dtype.str == '|O' and dd.size > 0:
+            if type( flist(dd.flat[0]) ) == spio.matlab.mio5_params.mat_struct:
+                for i in range( dd.size ):
+                    dd.flat[i] = _check_keys( flist( dd.flat[i]) )
+        else:
+            for i in range(dd.size):
+                dd.flat[i] = _check_keys(dd.flat[i])
+
+    return dd
+
+def fix_simple_lists(l):
+    if type(l) == dict:
+        for k,v in l.items():
+            l[k] = fix_simple_lists(v)
+    elif type(l) == np.ndarray and l.dtype.name == "uint8" and l.shape[0] == 1 and l.ndim == 2:
+        # l = l.tolist()
+        l = l.tolist()[0]
+    return l
+
+def apply_recursively(l, myfun):
+    if type(l) == dict:
+        for k,v in l.items():
+            l[k] = apply_recursively(v, myfun)
+    elif type(l) == np.ndarray and l.dtype.str == '|O' and l.size > 0:
+        for i in range( l.size ):
+            l.flat[i] = apply_recursively( l.flat[i], myfun)
+    else:
+        l = myfun(l)
+    return l
+
+
+def fix_1_arrays(l):
+    def _fix_1_arrays(l):
+        if type(l) == np.ndarray and l.size == 1 and np.issubdtype(l.dtype, np.number):
+            l = l.flat[0]
+        return l
+    l = apply_recursively(l, _fix_1_arrays)
+    return l
+
+
+def fix_strings(l):
+    if type(l) == dict:
+        for k,v in l.items():
+            l[k] = fix_strings(v)
+    elif type(l) == np.ndarray and l.size > 0:
+        tp = type(superpop(l.flat[0]))
+        if tp == str or tp == np.str_:
+            l = [superpop(x) for x in l.flat ]
+            if len(l) == 1:
+                l = l.pop()
+    return l
+
+
+def superpop(l):
+    if type(l) == list and len(l) == 1:
+        return superpop(l[0])
+    if type(l) == np.ndarray and l.size == 1:
+        return superpop(l.tolist())
+    return l
+
+
+def flist(l):
+    if type(l) == list and len(l) == 1:
+        l = flist( l.pop() )
+
+    if type(l) == np.ndarray and l.dtype.name == "object":
+        l3 = [flist(v) for v in l.flat]
+        l = flist( l3 )
+    return l
+
+
+def _todict(matobj):
+    '''
+    A recursive function which constructs from matobjects nested dictionaries
+    '''
+    dict = {}
+    for strg in matobj._fieldnames:
+        elem = matobj.__dict__[strg]
+        if isinstance(elem, spio.matlab.mio5_params.mat_struct):
+            dict[strg] = _todict(elem)
+        else:
+            dict[strg] = elem
+    return dict
+
+
+def uuroll(v):
+    if type(v) is dict:
+        for key,val in v.items():
+            v[key] = uuroll(val)
+    if type(v) is np.ndarray or type(v) is np.array:
+        for j in range(v.size):
+            v.flat[j] = uuroll(v.flat[j])
+    return v
+
+
+def uroll(mat):
+    for k in mat.keys():
+        v = mat[k]
+        v = uuroll(v)
+        mat[k] = v
+    return mat
\ No newline at end of file
diff --git a/src/jinjafy/jinjafy.py b/src/jinjafy/jinjafy.py
new file mode 100644
index 0000000000000000000000000000000000000000..4eee3d35cfeeb307daf3016d8dc24fbe5dcbf391
--- /dev/null
+++ b/src/jinjafy/jinjafy.py
@@ -0,0 +1,215 @@
+import inspect
+import jinja2
+from math import floor, log10
+import os
+import numpy as np
+from jinjafy import jinja_env
+
+
+def jinjafy_template(data,file_in,file_out=None, filters={},template_searchpath=None):
+    if template_searchpath:
+        file_in = os.path.relpath(file_in, template_searchpath)
+
+    return jinjafy_comment(data, file_in=file_in, file_out=file_out,jinja_tag=None, filters=filters,template_searchpath=template_searchpath)
+
+
+def jinjafy_comment(data,file_in=None,file_out=None,jinja_tag="jinja",jinja_code=None,trim_whitespace=True,trim_comments=True,comment_char="#",
+                    filters={},template_searchpath=None):
+    # Extract all comments from the given file and jinjafy them.
+    if file_in is None:
+        frame = inspect.stack()[1]
+        module = inspect.getmodule(frame[0])
+        file_in = module.__file__
+    elif not jinja_tag:
+        trim_comments=False
+        trim_whitespace=False
+
+    if not template_searchpath:
+        with open(file_in,'r') as f:
+            s = f.read()
+            if jinja_tag:
+                stag = "<" + jinja_tag + ">"
+                etag = "</" + jinja_tag + ">"
+
+                i_start = s.find(stag)
+                i_end = s.find(etag)
+                s = s[i_start+len(stag):i_end]
+            ss = [s]
+            if trim_comments:
+                ss = [ds.strip()[1:] for ds in s.splitlines() if len(ds.strip()) > 0 and ds.strip()[0] in ["#", "%"] ]
+            if trim_whitespace:
+                ss = [ds.strip() for ds in ss]
+
+            jinja_code = '\n'.join(ss)
+
+    from thtools.jinjafy.snipper import SnipperExtension
+    extensions = [SnipperExtension]
+    if template_searchpath:
+        if not isinstance(template_searchpath, list):
+            template_searchpath = [template_searchpath]
+        template_searchpath = [ts.replace("\\", "/") for ts in template_searchpath]
+        templateLoader = jinja2.FileSystemLoader(searchpath=template_searchpath)
+        env = jinja2.Environment(lstrip_blocks=True, trim_blocks=True,loader=templateLoader, extensions=extensions)
+    else:
+        env = jinja2.Environment(lstrip_blocks=True, trim_blocks=True, extensions=extensions)
+
+    import math
+    env.globals['exp'] = math.exp
+    env.globals['sqrt'] = math.sqrt
+    env.globals['cos'] = math.cos
+    env.globals['sin'] = math.sin
+
+    env.globals['mround'] = mround
+    env.globals['bold'] = bold
+    env.globals['fmat'] = fmat
+    env.globals['enumerate'] = enumerate
+    env.globals['zip'] = zip
+    env.globals['ensure_numpy'] = ensure_numpy
+    env.globals['transpose'] = transpose
+    import math
+    env.globals['ceil'] = math.ceil
+    env.globals['floor'] = math.floor
+
+    import thtools
+    if not thtools.is_cluster():
+        from pylatexenc import latexencode
+        env.globals['utf8tolatex'] = latexencode.utf8tolatex
+    env.globals['as_set'] = jinja_env.as_set
+    env.globals['as_set_list'] = jinja_env.as_set_list
+    env.globals['len'] = jinja_env.mylen
+    env.globals['get'] = jinja_env.jget
+    env.globals['tolist'] = jinja_env.tolist
+
+    filters['as_set'] =  jinja_env.as_set
+    filters['format_list'] =jinja_env.format_list
+    filters['format_join'] = jinja_env.format_join
+    filters['format_join_enum'] = jinja_env.format_join_enum
+    filters['pm'] = lambda x: f" {x}" if x < 0 else f"+{x}"
+    filters['bold'] = bold
+    filters['capfirst'] = lambda x: (x[0].upper() + x[1:] if len(x) > 1 else x.upper()) if x != None and isinstance(x, str) else x
+    filters['lowerfirst'] = lambda x: (x[0].lower() + x[1:] if len(x) > 1 else x.lower()) if x != None and isinstance(x, str) else x
+    filters['infty'] = jinja_env.infty
+    filters['n2w'] = jinja_env.n2w
+    def latex_url(url):
+        if not isinstance(url, str):
+            return url
+        url = url.replace("%", r"\%")
+        return url
+    filters['latex_url'] = latex_url
+    filters['format_list_symbols'] = jinja_env.format_list_symbols
+    filters['mround'] = mround
+    def eround(val,l):
+        x = str(mround(val, l))
+        if l == 0:
+            return x
+        if '.' not in x:
+            x = x + "."
+        n = l - (len(x) - x.find(".") - 1)
+        if n > 0:
+            x = x + "0"*n
+        return x
+
+    filters['eround'] = eround
+    filters['get'] = jinja_env.jget
+    filters['flatten'] = jinja_env.flatten
+    filters['aan'] = jinja_env.aan
+    filters['bracket'] = bracket
+    filters['tolist'] = jinja_env.tolist
+    filters['rational'] = jinja_env.as_rational
+    filters['permute_exam_answers'] = jinja_env.permute_exam_answers
+    env.filters.update(filters)
+
+    data['block_start_string'] = '{%'
+    if not template_searchpath:
+        jinja_out = env.from_string(jinja_code).render(data)
+    else:
+        file_in = file_in.replace("\\", "/")
+        template = env.get_template(file_in)
+        jinja_out = template.render(data)
+
+    if file_out is not None:
+        with open(file_out,'w',encoding='utf-8') as f:
+            # jinja_out = jinja_out.encode('utf-8')
+
+            f.write(jinja_out)
+            print("Writing to: " + file_out)
+
+    return jinja_out
+
+
+def bold(bob,d=True) :
+    if not isinstance(bob, str) :
+        bob = str(bob)
+    if d :
+        bob = '\\textbf{' + bob +"}"
+    return bob
+
+
+def fmat(bob,l=2,dobold=False) :
+    bob = mround(bob,l)
+    bob = bold(bob, dobold)
+    return bob
+
+def bracket(s):
+    return "{"+str(s)+"}"
+
+def un2str(x, xe, precision=2):
+    """pretty print nominal value and uncertainty
+
+        x  - nominal value
+        xe - uncertainty
+        precision - number of significant digits in uncertainty
+
+        returns shortest string representation of `x +- xe` either as
+        x.xx(ee)e+xx
+        or as
+        xxx.xx(ee)"""
+    # base 10 exponents
+    x_exp = int(floor(log10(x)))
+    xe_exp = int(floor(log10(xe)))
+
+    # uncertainty
+    un_exp = xe_exp - precision + 1
+    un_int = round(xe * 10 ** (-un_exp))
+
+    # nominal value
+    no_exp = un_exp
+    no_int = round(x * 10 ** (-no_exp))
+
+    # format - nom(unc)exp
+    fieldw = x_exp - no_exp
+    fmt = '%%.%df' % fieldw
+    result1 = (fmt + '(%.0f)e%d') % (no_int * 10 ** (-fieldw), un_int, x_exp)
+
+    # format - nom(unc)
+    fieldw = max(0, -no_exp)
+    fmt = '%%.%df' % fieldw
+    result2 = (fmt + '(%.0f)') % (no_int * 10 ** no_exp, un_int * 10 ** max(0, un_exp))
+
+    # return shortest representation
+    if len(result2) <= len(result1):
+        return result2
+    else:
+        return result1
+
+
+def mround(val, l=2):
+    if not isinstance(l, int):
+        return un2str(val, l, 1)
+    else:
+        if isinstance(val, np.ndarray):
+            return np.round(val * 10 ** l) / (10 ** l)
+        else:
+            return round(val * 10 ** l) / (10 ** l)
+
+
+def transpose(X):
+    return np.transpose( ensure_numpy( X) )
+
+
+def ensure_numpy(X):
+    if type(X) != np.ndarray:
+        X = np.asarray(X)
+    if X.ndim == 1:
+        X = np.transpose( np.expand_dims(X,1) )
+    return X
\ No newline at end of file
diff --git a/src/jinjafy/snipper.py b/src/jinjafy/snipper.py
new file mode 100644
index 0000000000000000000000000000000000000000..a0f8fd02f8517cb2f204446426914a6778f38d40
--- /dev/null
+++ b/src/jinjafy/snipper.py
@@ -0,0 +1,90 @@
+from jinja2 import nodes
+from jinja2.ext import Extension
+import os
+import thtools
+
+
+class SnipperExtension(Extension):
+    # a set of names that trigger the extension.
+    tags = set(['snipper'])
+
+    def __init__(self, environment):
+        super(SnipperExtension, self).__init__(environment)
+
+        # add the defaults to the environment
+        environment.extend(
+            fragment_cache_prefix='',
+            fragment_cache=None
+        )
+        self.ofile = ""
+
+    def parse(self, parser):
+        # the first token is the token that started the tag.  In our case
+        # we only listen to ``'cache'`` so this will be a name token with
+        # `cache` as value.  We get the line number so that we can give
+        # that line number to the nodes we create by hand.
+        lineno = next(parser.stream).lineno
+
+        # now we parse a single expression that is used as cache key.
+        args = [parser.parse_expression()]
+        ofile = os.path.join(os.path.dirname(parser.filename), args[0].value)
+        args[0].value = ofile
+        thtools.ensure_dir_exists(os.path.dirname(ofile))
+        self.ofile = ofile
+        print("Snipper args", args, "ofile", ofile)
+
+        # if there is a comma, the user provided a timeout.  If not use
+        # None as second parameter.
+        if parser.stream.skip_if('comma'):
+            args.append(parser.parse_expression())
+        else:
+            args.append(nodes.Const(None))
+
+        # now we parse the body of the cache block up to `endcache` and
+        # drop the needle (which would always be `endcache` in that case)
+        body = parser.parse_statements(['name:endsnipper'], drop_needle=True)
+
+        # now return a `CallBlock` node that calls our _cache_support
+        # helper method on this extension.
+        return nodes.CallBlock(self.call_method('_snip_method', args),
+                                [], [], body).set_lineno(lineno)
+
+        # parser.environment.loader.searchpath
+
+        # parser.parse_statements(body)
+        return body
+
+    def _snip_method(self, name, timeout, caller):
+        # rv = 0
+        # key = self.environment.fragment_cache_prefix + name
+
+        # try to load the block from the cache
+        # if there is no fragment in the cache, render it and store
+        # it in the cache.
+        # rv = self.environment.fragment_cache.get(key)
+        # if rv is not None:
+        #     return rv
+        rv = caller()
+        outfile = name
+        print("Actually snipping to ", self.ofile, "name", name, "timeout", timeout)
+        with open(name, 'w') as f:
+            f.write(rv)
+        # print("Actually snipping to ", self.ofile, 'writing', rv)
+
+        # self.environment.fragment_cache.add(key, rv, timeout)
+        return rv
+
+
+    def _cache_support(self, name, timeout, caller):
+        """Helper callback."""
+        key = self.environment.fragment_cache_prefix + name
+
+        # try to load the block from the cache
+        # if there is no fragment in the cache, render it and store
+        # it in the cache.
+        rv = self.environment.fragment_cache.get(key)
+        if rv is not None:
+            return rv
+        rv = caller()
+        self.environment.fragment_cache.add(key, rv, timeout)
+        return rv
\ No newline at end of file
diff --git a/src/jinjafy/textools.py b/src/jinjafy/textools.py
new file mode 100644
index 0000000000000000000000000000000000000000..662f393640a5140d3c6c2837ce0773a45f528e31
--- /dev/null
+++ b/src/jinjafy/textools.py
@@ -0,0 +1,187 @@
+from jinjafy import jinjafy_comment
+import numpy as np
+
+#"<jinja1>"
+#\begin{tabular}{ {{cc}} }
+# {% if bookstabs %}\toprule{% endif %}
+# {% if vvlabels %}
+#   {% for vl in vvlabels %}
+#       {% if loop.index > 1 %} & {% endif %}  \multicolumn{ {{vl[0]}} }{ {{vl[2]}} }{ {{vl[1]}} }
+#   {% endfor %} \\
+#   {% for vl in vvlabels %}
+#       {% if vl[3] %}
+# 	     \cmidrule(r){ {{vl[3]}} }
+#       {% endif %}
+#   {% endfor %}
+# {% endif %}
+# {% for row in X %}
+# {% if bookstabs and loop.index == 2%}\midrule{% endif %}
+# {% for c in row %}
+# {% if loop.index > 1 %} & {% endif %} {{ c['tex'] }} {% if loop.index == W %} \\ {% endif %}
+# {% endfor %}
+# {% endfor %}
+# {% if bookstabs %}\bottomrule{% endif %}
+#\end{tabular}
+#</jinja1>
+# Convert a matrix to a table super quickly
+def mat2table(X,vlabels=None,hlabels=None,file_out = None, bookstabs=True, vvlabels=None,plot=False,pdf_out=None, standalone=False):
+    X, Xx, Xerr,Xdl = fmat_X2dict(X)
+    if pdf_out: plot = True
+    #%%
+    if plot:
+        import matplotlib.pyplot as plt
+        #plt.style.use('ggplot')
+        plt.style.use('seaborn')
+        fig = plt.figure()
+        ax = fig.gca()
+        #ax = plt.gca()
+        ls = []
+        for j in range(X.shape[0]):
+            ls.append(ax.plot(Xx[j, :]).pop() )
+
+            if Xerr[j]:
+                plt.errorbar(range(X.shape[1]), Xx[j,:], yerr=Xerr[j], color=ls[j].get_color())
+
+            for i in range( X.shape[1] ):
+                if 'xs' in X[j,i]:
+                    plt.plot([i]*len(X[j,i]['xs']), X[j,i]['xs'], '.', color=ls[j].get_color())
+
+        if vlabels:
+            plt.legend(ls, vlabels, bbox_to_anchor=(1.04, 1), loc="upper left")
+        if hlabels:
+            plt.xticks(range(X.shape[1]), hlabels[1:])
+        #plt.subplots_adjust(right=0.5)
+        plt.tight_layout(rect=[0, 0, 1, 1])
+        plt.show()
+        #if pdf_out:
+        #    fig.savefig(pdf_out, bbox_inches='tight')
+
+
+    if vlabels:
+        vltex =  [{'tex': v} for v in vlabels]
+        for i in range(len(Xdl)):
+            Xdl[i] = [vltex[i]] + Xdl[i]
+
+    if hlabels:
+        Xdl = [ [{'tex': h} for h in hlabels] ] + Xdl
+
+    if vvlabels:
+        cc = 1
+        for i in range(len(vvlabels)):
+            if len(vvlabels[i]) < 3:
+                vvlabels[i].append("c")
+            dl = vvlabels[i][0]
+            if dl == 1:
+                a = None
+            else:
+                a = "%i-%i"%(cc, cc+dl-1)
+            cc = cc + dl
+            vvlabels[i] = vvlabels[i] + [a]
+
+    H = len(Xdl)
+    W = len(Xdl[0])
+    cc = ["c" for i in range(W)]
+    if vlabels:
+        cc[0] = "l"
+    cc = "".join(cc)
+
+    def fmat(x):
+        if isinstance(x, int):
+            x = str(x)
+        if isinstance(x, float):
+            x = "%2.3f"%x
+        return x
+
+    #X = [ [fmat(x) for x in row] for row in X]
+
+    data = {'X' : Xdl, 'hlabels': hlabels, 'vlabels': vlabels, 'cc': cc, 'H':H, 'W': W, 'bookstabs': bookstabs,
+            'vvlabels': vvlabels}
+
+    from thtools.jinjafy.jinjafy import jinjafy_comment
+    s = jinjafy_comment(data,jinja_tag="jinja1")
+    if file_out:
+        print("Writing to: " + file_out)
+
+        if standalone:
+            s = jinjafy_comment({"s": s}, jinja_tag="jinja3")
+
+        with open(file_out, 'w') as f:
+            f.write(s)
+        if standalone:
+            from thtools import latexmk
+            latexmk(file_out)
+
+
+
+    return s
+# "<jinja3>"
+# \documentclass[crop]{standalone}
+# \usepackage{booktabs}
+# \usepackage{siunitx}
+# \begin{document}
+# {{s}}
+# \end{document}
+# </jinja3>
+
+def fmat_X2dict(X):
+    X = np.asarray(X, dtype=np.object)
+    if len(X.shape) > 2:
+        X2 = np.ndarray(X.shape[:2], dtype=np.object)
+        for i in range(X.shape[0]):
+            for j in range(X.shape[1]):
+                X2[i, j] = X[i, j, :].squeeze()
+        X = X2
+    X = np.reshape(X, X.shape[:2])
+
+    for i in range(X.shape[0]):
+        for j in range(X.shape[1]):
+            dx = X[i,j]
+            if isinstance(dx, (list, np.ndarray)):
+                dx = [x for x in np.ravel(dx)]
+
+            if not isinstance(dx, dict):
+                dx = {'x': dx}
+            elif not isinstance(dx['x'], str):
+                x = dx['x']
+                # if isinstance(x, np.ndarray):
+                if 'tex' not in dx:
+                    dx['std'] = np.std(x)
+                    dx['std_mean'] = np.std(x) / np.sqrt( len(x))
+                    dx['xs'] = x
+                    dx['x'] = np.mean(x)
+                    x2, u2 = mround( dx['x'], dx['std_mean'] )
+
+                    dx['tex'] = '\\SI{%g\\pm %.2f}{}'%(x2, u2)
+
+            if 'tex' not in dx:
+                dx['tex'] = dx['x']
+
+            X[i,j] = dx
+
+    Xerr = [None] * X.shape[0]
+    Xx = np.zeros(X.shape)
+
+    for i in range(X.shape[0]):
+        if "std" in X[0,0]:
+            Xerr[i] = [dx['std_mean'] for dx in X[i]]
+
+        for j in range(X.shape[1]):
+            Xx[i,j] = X[i,j]['x']
+
+    Xdl = []
+    for i in range(X.shape[0]):
+        dx = []
+        for j in range(X.shape[1]):
+            dx.append(X[i,j])
+        Xdl.append(dx)
+
+
+    return X,Xx,Xerr,Xdl
+
+import math
+def mround(x,u):
+    n = np.floor(np.log10(x)+1)
+    dx = np.round(x / np.power(10.0, n), 2)
+    du = np.round(u / np.power(10.0, n), 2)
+    return dx * np.power(10, n), du * np.power(10.0,n)
+
diff --git a/src/slider/convert.py b/src/slider/convert.py
index 6737d3c5884ffe624df5d92a2f0d403312ab64e2..f4b00969f75d835da5715ac9487e5b1bc7a33d74 100644
--- a/src/slider/convert.py
+++ b/src/slider/convert.py
@@ -1,4 +1,7 @@
-from thtools import execute_command
+from jinjafy import execute_command
+import os
+from bs4 import BeautifulSoup
+# import thtools
 
 def svg2pdf(fin, fout=None, crop=True, text_to_path=False, export_area_page=True):
     """
@@ -65,9 +68,6 @@ def pdfcrop(fin, fout=None):
     execute_command(cmd.split())
 
 
-import os
-from bs4 import BeautifulSoup
-import thtools
 
 def svg_edit_to_importable(svg_edit_file,verbose=False, keep_background_layer=True):
     """
@@ -114,7 +114,7 @@ def svg_edit_to_importable(svg_edit_file,verbose=False, keep_background_layer=Tr
                 f2.write(s2.encode("UTF-8"))
 
             cmd = ['inkscape', '-C', '-T', '--without-gui', '--file=%s'%svg_fonts_layers[-1], '--export-pdf=%s' % pdf_nofonts_layers[-1]]
-            thtools.execute_command(cmd)
+            execute_command(cmd)
 
     if verbose:
         print("svg_edit_to_importable called. Converting svg file\n  > %s\nto files:"%svg_edit_file)
diff --git a/src/slider/inkscape2tex.py b/src/slider/inkscape2tex.py
deleted file mode 100644
index 989f495ad4a46c8c9991f3a1fc376264ea912cdf..0000000000000000000000000000000000000000
--- a/src/slider/inkscape2tex.py
+++ /dev/null
@@ -1,183 +0,0 @@
-#!/usr/bin/env python
-"""Convert inkscape SVG files to TeX input.
-
-- SVG to PDF or EPS with inkscape, optionally with LaTeX output.
-- DOT to SVG
-
-Skips conversion if PDF file found newer than SVG source.
-Requires `inkscape` in path.
-"""
-# Copyright 2010-2017 by Ioannis Filippidis
-# All rights reserved. Licensed under BSD-2.
-#
-import argparse
-import datetime
-import fnmatch
-import logging
-import os
-import shlex
-import subprocess
-import time
-
-import humanize
-
-import svg2latex as convert
-# from svglatex import convert
-
-
-log = logging.getLogger(__name__)
-
-
-def main():
-    """Start from here."""
-    args = parse_args()
-    f = '{name}.svg'.format(name=args.input_file)
-    out_type = args.method
-    if './img/' in f:
-        files = [f]
-    else:
-        files = locate(f, './img')
-    svg = None
-    for svg in files:
-        log.info('Will convert SVG file "{f}" to {t}'.format(
-            f=svg, t=out_type))
-        convert_if_svg_newer(svg, out_type)
-    if svg is None:
-        raise Exception(
-            'SVG file "{f}" not found! '
-            'Cannot export to PDF.'.format(f=f))
-
-
-def parse_args():
-    """Parse command-line arguments using."""
-    parser = argparse.ArgumentParser()
-    parser.add_argument(
-        '-i', '--input-file', type=str,
-        help=(
-            'Name (w/o extension) of SVG file. '
-            'Either file name to search for under `./img`, '
-            'or path that starts with `./img`.'))
-    choices = [
-        'latex-pdf', 'pdf',
-        'latex-eps', 'eps']
-    parser.add_argument(
-        '-m', '--method', type=str, choices=choices,
-        help=(
-            'Export to this file type. '
-            'The prefix "latex" produces also a file `*.pdf_tex` '
-            'that contains the text from the SVG. '
-            'The command `\includesvgpdf` passes `pdf`, '
-            'and `\includesvg` passes `latex-pdf`.'))
-    args = parser.parse_args()
-    return args
-
-
-def convert_if_svg_newer(svg, out_type):
-    """Convert SVG file to PDF or EPS."""
-    base, ext = os.path.splitext(svg)
-    assert ext == '.svg', ext
-    if 'pdf' in out_type:
-        out = base + '.pdf'
-    elif 'eps' in out_type:
-        out = base + '.eps'
-    else:
-        raise ValueError(out_type)
-    if not os.access(svg, os.F_OK):
-        raise FileNotFoundError(
-            'No SVG file "{f}"'.format(f=svg))
-    fresh = is_newer(out, svg)
-    if out_type == 'latex-pdf':
-        pdf_tex = base + '.pdf_tex'
-        fresh &= is_newer(pdf_tex, svg)
-    if fresh:
-        log.info('No update needed, target newer than SVG.')
-        return
-    log.info('File not found or old. Converting from SVG...')
-    convert_svg(svg, out, out_type)
-
-
-def is_newer(target, source):
-    """Return `True` if `target` newer than `source` file."""
-    assert os.path.isfile(source), source
-    if not os.path.isfile(target):
-        return False
-    t_src = os.stat(source)[8]
-    t_tgt = os.stat(target)[8]
-    _print_dates(source, target, t_src, t_tgt)
-    return t_src < t_tgt
-
-
-def _print_dates(source, target, t_src, t_tgt):
-    s = _format_time(t_src)
-    t = _format_time(t_tgt)
-    log.info((
-        'last modification dates:\n'
-        '    Source ({source}): {s}\n'
-        '    Target ({target}): {t}').format(
-            source=source, target=target,
-            s=s, t=t))
-
-
-def _format_time(t):
-    """Return time readable by humans."""
-    return humanize.naturaltime(
-        datetime.datetime.fromtimestamp(t))
-
-
-def convert_svg(svg, out, out_type):
-    """Convert from SVG to output format."""
-    # TODO: implement options `latex-eps`, `eps`
-    assert out_type in ('latex-pdf', 'pdf'), out_type
-    if out_type == 'latex-pdf':
-        convert.main(svg)
-    elif out_type == 'pdf':
-        inkscape = convert.which_inkscape()
-        svg_path = os.path.realpath(svg)
-        out_path = os.path.realpath(out)
-        args = [
-            inkscape,
-            '--without-gui',
-            '--export-area-drawing',
-            '--export-ignore-filters',
-            '--export-dpi={dpi}'.format(dpi=96),
-            '--export-pdf={out}'.format(out=out_path),
-            svg_path]
-        r = subprocess.call(args)
-        if r != 0:
-            raise Exception('Conversion error')
-
-
-def convert_svg_using_inkscape(svg, out, out_type):
-    """Convert from SVG to output format."""
-    # inkscape need be called with an absolute path on OS X
-    # http://wiki.inkscape.org/wiki/index.php/MacOS_X
-    symlink_relpath = 'bin/inkscape'
-    home = os.path.expanduser('~')
-    symlink_abspath = os.path.join(home, symlink_relpath)
-    inkscape_abspath = os.path.realpath(symlink_abspath)
-    svg_abspath = os.path.realpath(svg)
-    args = ['{inkscape_abspath} -z -D --file={svg}'.format(
-        inkscape_abspath=inkscape_abspath, svg=svg_abspath)]
-    if 'pdf' in out_type:
-        args.append('--export-pdf={pdf}'.format(pdf=out))
-    if 'eps' in out_type:
-        args.append('--export-eps={eps}'.format(eps=out))
-    if 'latex' in out_type:
-        args.append('--export-latex')
-    args = shlex.split(' '.join(args))
-    r = subprocess.call(args)
-    if r != 0:
-        raise Exception(
-            'conversion from "{svg}" to "{out}" failed'.format(
-                svg=svg, out=out))
-
-
-def locate(pattern, root=os.curdir):
-    """Locate all files matching supplied filename pattern under `root`."""
-    for path, dirs, files in os.walk(os.path.abspath(root)):
-        for filename in fnmatch.filter(files, pattern):
-            yield os.path.join(path, filename)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/slider/legacy_importer.py b/src/slider/legacy_importer.py
index fd19de6decd4de9864a3b672e25b5cdc1f8aa543..983f3fce5b47114c78af0fe2304d4b5bd735c199 100644
--- a/src/slider/legacy_importer.py
+++ b/src/slider/legacy_importer.py
@@ -3,10 +3,11 @@
 # https://github.com/eea/odfpy
 import os
 import shutil
-import thtools
-from thtools.jinjafy import jinjafy_comment
+# import thtools
+from jinjafy import jinjafy_comment
 from bs4 import BeautifulSoup
 import glob
+from jinjafy import execute_command
 
 CDIR = os.path.dirname(os.path.realpath(__file__))
 CDIR = CDIR.replace('\\','/')
@@ -26,7 +27,7 @@ def join_pdfs(slide_deck_pdf, outfile):
     files = [os.path.relpath(os.path.dirname(pdf), start=dn) + "/" + os.path.basename(pdf) for pdf in slide_deck_pdf]
     outf = os.path.relpath(os.path.dirname(outfile), start=dn) + "/" + os.path.basename(outfile)
     cmd = "cd " + dn + " && pdftk " + " ".join(files) + " cat output " + outf
-    thtools.execute_command(cmd.split())
+    execute_command(cmd.split())
 
 
 def li_import(slide_deck_pdf, tex_output_path=None, num_to_take=None, force=False, svg_pfix="osvg", svg_height=743.75, svg_width=992.5,
diff --git a/src/slider/slider.py b/src/slider/slider.py
index fe7b51fc41339ecf382331a46c224a0ef275e7ae..bbf3721a68a1e4f75a3b2cad005cfc413587cdea 100644
--- a/src/slider/slider.py
+++ b/src/slider/slider.py
@@ -1,11 +1,11 @@
-from thtools.slider import legacy_importer
+from slider import legacy_importer
 import PyPDF2
 import os
-import thtools
-from thtools.slider.legacy_importer import SVG_EDIT_RELPATH, SVG_TMP_RELPATH, move_template_files, DTU_beamer_base
-from thtools.cache import cache_update_str, cache_contains_str, cache_contains_file, cache_update_file
+# import thtools
+from slider.legacy_importer import SVG_EDIT_RELPATH, SVG_TMP_RELPATH, move_template_files, DTU_beamer_base
+from jinjafy.cache import cache_update_str, cache_contains_str, cache_contains_file, cache_update_file
 import shutil
-from thtools.slider.slide_fixer import check_svg_file_and_fix_if_broken
+from slider.slide_fixer import check_svg_file_and_fix_if_broken
 dc = "\\documentclass"
 
 def fix_handout(s):