mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Exports
This commit is contained in:
parent
ba05411a80
commit
85934c1980
8 changed files with 273 additions and 3 deletions
|
@ -15,6 +15,18 @@ import logging
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(object):
|
||||||
|
def __init__(self, name, count=0):
|
||||||
|
self.name = name
|
||||||
|
self.count = count
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Tag '{}'>".format(self.name)
|
||||||
|
|
||||||
|
|
||||||
class Journal(object):
|
class Journal(object):
|
||||||
def __init__(self, name='default', **kwargs):
|
def __init__(self, name='default', **kwargs):
|
||||||
self.config = {
|
self.config = {
|
||||||
|
@ -141,6 +153,18 @@ class Journal(object):
|
||||||
if n:
|
if n:
|
||||||
self.entries = self.entries[-n:]
|
self.entries = self.entries[-n:]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self):
|
||||||
|
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||||
|
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||||
|
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||||
|
tags = [tag
|
||||||
|
for entry in self.entries
|
||||||
|
for tag in set(entry.tags)]
|
||||||
|
# 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(tag, count=count) for count, tag in sorted(tag_counts)]
|
||||||
|
|
||||||
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False):
|
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False):
|
||||||
"""Removes all entries from the journal that don't match the filter.
|
"""Removes all entries from the journal that don't match the filter.
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@ from . import Journal
|
||||||
from . import util
|
from . import util
|
||||||
from . import install
|
from . import install
|
||||||
from . import plugins
|
from . import plugins
|
||||||
from .util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
|
from .export import Exporter
|
||||||
|
from .util import ERROR_COLOR, RESET_COLOR
|
||||||
import jrnl
|
import jrnl
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
@ -244,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 = plugins.get_exporter(args.export)
|
exporter = 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:
|
||||||
|
|
67
jrnl/export.py
Normal file
67
jrnl/export.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
from .util import ERROR_COLOR, RESET_COLOR
|
||||||
|
from .util import slugify, u
|
||||||
|
from .template import Template
|
||||||
|
import os
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
|
||||||
|
class Exporter(object):
|
||||||
|
"""This Exporter can convert entries and journals into text files."""
|
||||||
|
def __init__(self, format):
|
||||||
|
with open("jrnl/templates/" + format + ".template") as f:
|
||||||
|
front_matter, body = f.read().strip("-\n").split("---", 2)
|
||||||
|
self.template = Template(body)
|
||||||
|
|
||||||
|
def export_entry(self, entry):
|
||||||
|
"""Returns a unicode representation of a single entry."""
|
||||||
|
return entry.__unicode__()
|
||||||
|
|
||||||
|
def _get_vars(self, journal):
|
||||||
|
return {
|
||||||
|
'journal': journal,
|
||||||
|
'entries': journal.entries,
|
||||||
|
'tags': journal.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
def export_journal(self, journal):
|
||||||
|
"""Returns a unicode representation of an entire journal."""
|
||||||
|
print("EXPORTING")
|
||||||
|
return self.template.render_block("journal", **self._get_vars(journal))
|
||||||
|
|
||||||
|
def write_file(self, journal, path):
|
||||||
|
"""Exports a journal into a single file."""
|
||||||
|
try:
|
||||||
|
with codecs.open(path, "w", "utf-8") as f:
|
||||||
|
f.write(self.export_journal(journal))
|
||||||
|
return "[Journal exported to {0}]".format(path)
|
||||||
|
except IOError as e:
|
||||||
|
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
||||||
|
|
||||||
|
def make_filename(self, entry):
|
||||||
|
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension))
|
||||||
|
|
||||||
|
def write_files(self, journal, path):
|
||||||
|
"""Exports a journal into individual files for each entry."""
|
||||||
|
for entry in journal.entries:
|
||||||
|
try:
|
||||||
|
full_path = os.path.join(path, self.make_filename(entry))
|
||||||
|
with codecs.open(full_path, "w", "utf-8") as f:
|
||||||
|
f.write(self.export_entry(entry))
|
||||||
|
except IOError as e:
|
||||||
|
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
||||||
|
return "[Journal exported to {0}]".format(path)
|
||||||
|
|
||||||
|
def export(self, journal, format="text", output=None):
|
||||||
|
"""Exports to individual files if output is an existing path, or into
|
||||||
|
a single file if output is a file name, or returns the exporter's
|
||||||
|
representation as unicode if output is None."""
|
||||||
|
if output and os.path.isdir(output): # multiple files
|
||||||
|
return self.write_files(journal, output)
|
||||||
|
elif output: # single file
|
||||||
|
return self.write_file(journal, output)
|
||||||
|
else:
|
||||||
|
return self.export_journal(journal)
|
113
jrnl/template.py
Normal file
113
jrnl/template.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import re
|
||||||
|
import asteval
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
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)
|
34
jrnl/templates/json.template
Normal file
34
jrnl/templates/json.template
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
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 }}",
|
||||||
|
"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
|
17
tst/2015-09-10_i-am-back-from-burning-man.md
Normal file
17
tst/2015-09-10_i-am-back-from-burning-man.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
title: I am back from Burning Man.
|
||||||
|
date: 2015-09-10 09:00
|
||||||
|
stared: False
|
||||||
|
tags:
|
||||||
|
There's a lot to process.
|
||||||
|
|
||||||
|
During the two weeks before the burn, I had sever anxiety issues - very physical symptoms; my chest felt constricted, my heart was pounding hard, my throat block, I was squashed between the walls. I think this anxiety has been building up since at least Australia, but now things got out of control.
|
||||||
|
|
||||||
|
In direct comparison, this year's burn was maybe a little less exciting than last year's. But I took two very important things from it:
|
||||||
|
|
||||||
|
1) a few days in, with no phone reception and nothing to plan or worry about, all of my anxiety symptoms vanished, and I felt light and free. It's not entirely intrinsic to me. Feeling better is possible, and it's close.
|
||||||
|
2) I started cultivating a wonderful friendship with Stef - who is currently dating Simon and Laurel, as a couple. Recap: Simon and Laurel were hitting on Lucy and me pretty hard at a dinner quite exactly a year ago. Small world.
|
||||||
|
|
||||||
|
I started a therapy before the burn, and yesterday was my second session. We dug through some of the anxiety that was related to my parents visiting SF. Here’s a realisation: my parents, especially my mother, never really valued academic achievements, and even half jokingly, half dismissively called me a “Streber” when I brought home straight As. I assume that this has to do something with her feeling inferior to my dad, but at any rate this was probably the point where I realised that their world and mine were different, and whatever I wanted to achieve I had to achieve myself.
|
||||||
|
|
||||||
|
I didn’t ask them for help until the day I broke up with Beth, two years ago.
|
||||||
|
|
8
tst/2015-09-13_what-an-exciting-life.md
Normal file
8
tst/2015-09-13_what-an-exciting-life.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
title: What an exciting life!
|
||||||
|
date: 2015-09-13 09:00
|
||||||
|
stared: False
|
||||||
|
tags: kari
|
||||||
|
I spent the evening with @Kari, singing Dresden Dolls, drinking wine, falling a little bit in love, having sex on a piano, getting unintentionally electrocuted by her Hitachi, waking up next to a beautiful person.
|
||||||
|
|
||||||
|
And during the Burn I got a lot closer to Stef, too. Oh, life!
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
title: a part of me wants to submit itself to the scintillating haze that a glass of wine on a sun drenched afternoon promises.
|
||||||
|
date: 2015-09-19 13:00
|
||||||
|
stared: False
|
||||||
|
tags:
|
||||||
|
Another part wants to fight for clarity of mind.
|
||||||
|
|
Loading…
Add table
Reference in a new issue