mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 08:38:32 +02:00
Template exporting FTW
This commit is contained in:
parent
f4dfecb62f
commit
60a955de20
12 changed files with 164 additions and 90 deletions
19
features/data/templates/sample.template
Normal file
19
features/data/templates/sample.template
Normal 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 %}
|
||||
`
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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()
|
49
jrnl/plugins/template_exporter.py
Normal file
49
jrnl/plugins/template_exporter.py
Normal 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))
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
18
jrnl/templates/sample.template
Normal file
18
jrnl/templates/sample.template
Normal 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 %}
|
Loading…
Add table
Reference in a new issue