Template exporting FTW

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

View file

@ -0,0 +1,19 @@
---
extension: txt
---
{% block journal %}
{% for entry in entries %}
{% include entry %}
{% endfor %}
{% endblock %}
{% block entry %}
{{ entry.title }}
{{ "-" * len(entry.title) }}
{{ entry.body }}
{% endblock %}
`

View file

@ -27,6 +27,23 @@ Feature: Exporting a Journal
and the output should be parsable as json
and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1"
Scenario: Exporting using custom templates
Given we use the config "basic.yaml"
Given we load template "sample.template"
When we run "jrnl --export sample"
Then the output should be
"""
My first entry.
---------------
Everything is alright
Life is good.
-------------
But I'm better.
"""
Scenario: Increasing Headings on Markdown export
Given we use the config "markdown-headings-335.yaml"
When we run "jrnl --export markdown"

View file

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from __future__ import absolute_import
from behave import given, when, then
from jrnl import cli, install, Journal, util
from jrnl import cli, install, Journal, util, plugins
from jrnl import __version__
from dateutil import parser as date_parser
from collections import defaultdict
@ -13,8 +13,8 @@ import keyring
class TestKeyring(keyring.backend.KeyringBackend):
"""A test keyring that just stores its valies in a hash
"""
"""A test keyring that just stores its valies in a hash"""
priority = 1
keys = defaultdict(dict)
@ -97,6 +97,13 @@ def run(context, command):
context.exit_status = e.code
@given('we load template "{filename}"')
def load_template(context, filename):
full_path = os.path.join("features/data/templates", filename)
exporter = plugins.template_exporter.__exporter_from_file(full_path)
plugins.__exporter_types[exporter.names[0]] = exporter
@when('we set the keychain password of "{journal}" to "{password}"')
def set_keychain(context, journal, password):
keyring.set_password('jrnl', journal, password)

View file

@ -13,7 +13,6 @@ from . import Journal
from . import util
from . import install
from . import plugins
from .export import Exporter
from .util import ERROR_COLOR, RESET_COLOR
import jrnl
import argparse
@ -44,9 +43,9 @@ def parse_args(args=None):
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.BaseExporter.PLUGIN_NAMES, help='Export your journal. TYPE can be {}.'.format(plugins.BaseExporter.get_plugin_types_string()), default=False, const=None)
exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.EXPORT_FORMATS, help='Export your journal. TYPE can be {}.'.format(plugins.util.oxford_list(plugins.EXPORT_FORMATS)), default=False, const=None)
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.', default=False, const=None)
exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.BaseImporter.PLUGIN_NAMES, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.BaseImporter.get_plugin_types_string()), default=False, const='jrnl', nargs='?')
exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.IMPORT_FORMATS, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, const='jrnl', nargs='?')
exporting.add_argument('-i', metavar='INPUT', dest='input', help='Optionally specifies input file when using --import.', default=False, const=None)
exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None)
exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None)
@ -246,7 +245,7 @@ def run(manual_args=None):
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
elif args.export is not False:
exporter = Exporter(args.export)
exporter = plugins.get_exporter(args.export)
print(exporter.export(journal, args.output))
elif args.encrypt is not False:

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"]

View file

@ -1,5 +1,6 @@
import re
import asteval
import yaml
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
@ -18,6 +19,15 @@ class Template(object):
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()

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]

View file

@ -1,33 +0,0 @@
---
extension: json
---
{% block journal %}
{
"tags": {
{% for tag in tags %}
"{{ tag.name }}": {{ tag.count }}{% if not __last_iteration %},{% endif %}
{% endfor %}
},
"entries": [
{% for entry in entries %}
{% include entry %}{% if not __last_iteration %},{% endif %}
{% endfor %}
]
}
{% endblock %}
{% block entry %}
{
"title": "{{ entry.title }}",
"body": "{{ entry.body.replace("\n", "\\n") }}",
"date": "{{ entry.date.strftime('%Y-%m-%d') }}",
"time": "{{ entry.date.strftime('%H:%M') }}",
{% if entry.uuid %}
"uuid": "{{ entry.uuid }}",
{% endif %}
"starred": {{ "true" if entry.starred else "false" }}
}
{% endblock %}
Hey There

View file

@ -0,0 +1,18 @@
---
extension: txt
---
{% block journal %}
{% for entry in entries %}
{% include entry %}
{% endfor %}
{% endblock %}
{% block entry %}
{{ entry.title }}
{{ "-" * len(entry.title) }}
{{ entry.body }}
{% endblock %}