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

@ -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 output should be parsable as json
and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1" 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 Scenario: Increasing Headings on Markdown export
Given we use the config "markdown-headings-335.yaml" Given we use the config "markdown-headings-335.yaml"
When we run "jrnl --export markdown" When we run "jrnl --export markdown"

View file

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
from behave import given, when, then 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 jrnl import __version__
from dateutil import parser as date_parser from dateutil import parser as date_parser
from collections import defaultdict from collections import defaultdict
@ -13,8 +13,8 @@ import keyring
class TestKeyring(keyring.backend.KeyringBackend): 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 priority = 1
keys = defaultdict(dict) keys = defaultdict(dict)
@ -97,6 +97,13 @@ def run(context, command):
context.exit_status = e.code 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}"') @when('we set the keychain password of "{journal}" to "{password}"')
def set_keychain(context, journal, password): def set_keychain(context, journal, password):
keyring.set_password('jrnl', journal, password) keyring.set_password('jrnl', journal, password)

View file

@ -13,7 +13,6 @@ from . import Journal
from . import util from . import util
from . import install from . import install
from . import plugins from . import plugins
from .export import Exporter
from .util import ERROR_COLOR, RESET_COLOR from .util import ERROR_COLOR, RESET_COLOR
import jrnl import jrnl
import argparse import argparse
@ -44,9 +43,9 @@ def parse_args(args=None):
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal') 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('-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('--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('-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('-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('--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) 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))) print(util.py2encode(plugins.get_exporter("tags").export(journal)))
elif args.export is not False: elif args.export is not False:
exporter = Exporter(args.export) exporter = plugins.get_exporter(args.export)
print(exporter.export(journal, args.output)) print(exporter.export(journal, args.output))
elif args.encrypt is not False: elif args.encrypt is not False:

View file

@ -2,57 +2,34 @@
# encoding: utf-8 # encoding: utf-8
from __future__ import absolute_import, unicode_literals 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): __exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names])
"""Called when a Plugin derived class is imported""" __importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names])
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
EXPORT_FORMATS = sorted(__exporter_types.keys())
IMPORT_FORMATS = sorted(__importer_types.keys())
def get_exporter(format): def get_exporter(format):
for exporter in BaseExporter.PLUGINS: for exporter in __exporters:
if hasattr(exporter, "names") and format in exporter.names: if hasattr(exporter, "names") and format in exporter.names:
return exporter return exporter
return None return None
def get_importer(format): def get_importer(format):
for importer in BaseImporter.PLUGINS: for importer in __importers:
if hasattr(importer, "names") and format in importer.names: if hasattr(importer, "names") and format in importer.names:
return importer return importer
return None return None

View file

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

View file

@ -1,5 +1,6 @@
import re import re
import asteval import asteval
import yaml
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*" VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*" EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
@ -18,6 +19,15 @@ class Template(object):
self.clean_template = None self.clean_template = None
self.blocks = {} 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): def render(self, **vars):
if self.clean_template is None: if self.clean_template is None:
self._get_blocks() 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 from __future__ import absolute_import, unicode_literals
import codecs import codecs
from . import BaseExporter
from ..util import u, slugify from ..util import u, slugify
import os 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.""" """This Exporter can convert entries and journals into text files."""
names = ["text", "txt"] names = ["text", "txt"]
extension = "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] # 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]) tag_counts = set([(tags.count(tag), tag) for tag in tags])
return tag_counts 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 %}