Template exporting FTW

This commit is contained in:
Manuel Ebert 2016-08-19 23:20:31 +00:00
parent f4dfecb62f
commit 60a955de20
12 changed files with 164 additions and 90 deletions

View file

@ -2,57 +2,34 @@
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import glob
import os
import importlib
from .text_exporter import TextExporter
from .jrnl_importer import JRNLImporter
from .json_exporter import JSONExporter
from .markdown_exporter import MarkdownExporter
from .tag_exporter import TagExporter
from .xml_exporter import XMLExporter
from .yaml_exporter import YAMLExporter
from .template_exporter import __all__ as template_exporters
class PluginMeta(type):
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters
__importers =[JRNLImporter]
def __init__(cls, name, bases, attrs):
"""Called when a Plugin derived class is imported"""
if not hasattr(cls, 'PLUGINS'):
cls.PLUGINS = []
cls.PLUGIN_NAMES = []
else:
cls.__register_plugin(cls)
def __register_plugin(cls, plugin):
"""Add the plugin to the plugin list and perform any registration logic"""
cls.PLUGINS.append(plugin)
cls.PLUGIN_NAMES.extend(plugin.names)
def get_plugin_types_string(cls):
plugin_names = sorted(cls.PLUGIN_NAMES)
if not plugin_names:
return "(nothing)"
elif len(plugin_names) == 1:
return plugin_names[0]
elif len(plugin_names) == 2:
return plugin_names[0] + " or " + plugin_names[1]
else:
return ', '.join(plugin_names[:-1]) + ", or " + plugin_names[-1]
# This looks a bit arcane, but is basically bilingual speak for defining a
# class with meta class 'PluginMeta' for both Python 2 and 3.
BaseExporter = PluginMeta(str('BaseExporter'), (), {'names': []})
BaseImporter = PluginMeta(str('BaseImporter'), (), {'names': []})
for module in glob.glob(os.path.dirname(__file__) + "/*.py"):
importlib.import_module("." + os.path.basename(module)[:-3], "jrnl.plugins")
del module
__exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names])
__importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names])
EXPORT_FORMATS = sorted(__exporter_types.keys())
IMPORT_FORMATS = sorted(__importer_types.keys())
def get_exporter(format):
for exporter in BaseExporter.PLUGINS:
for exporter in __exporters:
if hasattr(exporter, "names") and format in exporter.names:
return exporter
return None
def get_importer(format):
for importer in BaseImporter.PLUGINS:
for importer in __importers:
if hasattr(importer, "names") and format in importer.names:
return importer
return None

View file

@ -4,10 +4,9 @@
from __future__ import absolute_import, unicode_literals
import codecs
import sys
from . import BaseImporter
from .. import util
class JRNLImporter(BaseImporter):
class JRNLImporter(object):
"""This plugin imports entries from other jrnl files."""
names = ["jrnl"]

123
jrnl/plugins/template.py Normal file
View file

@ -0,0 +1,123 @@
import re
import asteval
import yaml
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
PRINT_RE = r"{{ *(.+?) *}}"
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
END_BLOCK_RE = r"{% *end(for|if) *%}"
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE, expression=EXPRESSION_RE)
IF_RE = r"{% *if +(.+?) *%}"
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
INCLUDE_RE = r"{% *include +(.+?) *%}"
class Template(object):
def __init__(self, template):
self.template = template
self.clean_template = None
self.blocks = {}
@classmethod
def from_file(cls, filename):
with open(filename) as f:
front_matter, body = f.read().strip("-\n").split("---", 2)
front_matter = yaml.load(front_matter)
template = cls(body)
template.__dict__.update(front_matter)
return template
def render(self, **vars):
if self.clean_template is None:
self._get_blocks()
return self._expand(self.clean_template, **vars)
def render_block(self, block, **vars):
if self.clean_template is None:
self._get_blocks()
return self._expand(self.blocks[block], **vars)
def _eval_context(self, vars):
e = asteval.Interpreter(symtable=vars, use_numpy=False, writer=None)
e.symtable['__last_iteration'] = vars.get("__last_iteration", False)
return e
def _get_blocks(self):
def s(match):
name, contents = match.groups()
self.blocks[name] = self._strip_single_nl(contents)
return ""
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
def _expand(self, template, **vars):
stack = sorted(
[(m.start(), 1, m.groups()[0]) for m in re.finditer(START_BLOCK_RE, template)] +
[(m.end(), -1, m.groups()[0]) for m in re.finditer(END_BLOCK_RE, template)]
)
last_nesting, nesting = 0, 0
start = 0
result = ""
block_type = None
if not stack:
return self._expand_vars(template, **vars)
for pos, indent, typ in stack:
nesting += indent
if nesting == 1 and last_nesting == 0:
block_type = typ
result += self._expand_vars(template[start:pos], **vars)
start = pos
elif nesting == 0 and last_nesting == 1:
if block_type == "if":
result += self._expand_cond(template[start:pos], **vars)
elif block_type == "for":
result += self._expand_loops(template[start:pos], **vars)
elif block_type == "block":
result += self._save_block(template[start:pos], **vars)
start = pos
last_nesting = nesting
result += self._expand_vars(template[stack[-1][0]:], **vars)
return result
def _expand_vars(self, template, **vars):
safe_eval = self._eval_context(vars)
expanded = re.sub(INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template)
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
def _expand_cond(self, template, **vars):
start_block = re.search(IF_RE, template, re.M)
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
expression = start_block.groups()[0]
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()])
safe_eval = self._eval_context(vars)
if safe_eval(expression):
return self._expand(sub_template)
return ""
def _strip_single_nl(self, template, strip_r=True):
if template[0] == "\n":
template = template[1:]
if strip_r and template[-1] == "\n":
template = template[:-1]
return template
def _expand_loops(self, template, **vars):
start_block = re.search(FOR_RE, template, re.M)
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
var_name, iterator = start_block.groups()
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()], strip_r=False)
safe_eval = self._eval_context(vars)
result = ''
items = safe_eval(iterator)
for idx, var in enumerate(items):
vars[var_name] = var
vars['__last_iteration'] = idx == len(items) - 1
result += self._expand(sub_template, **vars)
del vars[var_name]
return self._strip_single_nl(result)

View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .template import Template
import os
from glob import glob
class GenericTemplateExporter(TextExporter):
"""This Exporter can convert entries and journals into text files."""
@classmethod
def export_entry(cls, entry):
"""Returns a unicode representation of a single entry."""
vars = {
'entry': entry,
'tags': entry.tags
}
return cls.template.render_block("entry", **vars)
@classmethod
def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal."""
vars = {
'journal': journal,
'entries': journal.entries,
'tags': journal.tags
}
return cls.template.render_block("journal", **vars)
def __exporter_from_file(template_file):
"""Create a template class from a file"""
name = os.path.basename(template_file).replace(".template", "")
template = Template.from_file(template_file)
return type("{}Exporter".format(name.title()), (GenericTemplateExporter, ), {
"names": [name],
"extension": template.extension,
"template": template
})
__all__ = []
# Factory pattern to create Exporter classes for all available templates
for template_file in glob("jrnl/templates/*.template"):
__all__.append(__exporter_from_file(template_file))

View file

@ -3,13 +3,12 @@
from __future__ import absolute_import, unicode_literals
import codecs
from . import BaseExporter
from ..util import u, slugify
import os
from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
from ..util import ERROR_COLOR, RESET_COLOR
class TextExporter(BaseExporter):
class TextExporter(object):
"""This Exporter can convert entries and journals into text files."""
names = ["text", "txt"]
extension = "txt"

View file

@ -12,3 +12,16 @@ def get_tags_count(journal):
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = set([(tags.count(tag), tag) for tag in tags])
return tag_counts
def oxford_list(lst):
"""Return Human-readable list of things obeying the object comma)"""
lst = sorted(lst)
if not lst:
return "(nothing)"
elif len(lst) == 1:
return lst[0]
elif len(lst) == 2:
return lst[0] + " or " + lst[1]
else:
return ', '.join(lst[:-1]) + ", or " + lst[-1]