mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 08:38:32 +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__)
|
||||
|
||||
|
||||
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):
|
||||
def __init__(self, name='default', **kwargs):
|
||||
self.config = {
|
||||
|
@ -141,6 +153,18 @@ class Journal(object):
|
|||
if 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):
|
||||
"""Removes all entries from the journal that don't match the filter.
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ from . import Journal
|
|||
from . import util
|
||||
from . import install
|
||||
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 argparse
|
||||
import sys
|
||||
|
@ -188,7 +189,7 @@ def run(manual_args=None):
|
|||
template = ""
|
||||
if config['template']:
|
||||
try:
|
||||
template = open(config['template']).read()
|
||||
template = open(config['template']).read()
|
||||
except:
|
||||
util.prompt("[Could not read template at '']".format(config['template']))
|
||||
sys.exit(1)
|
||||
|
@ -244,7 +245,7 @@ def run(manual_args=None):
|
|||
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
|
||||
|
||||
elif args.export is not False:
|
||||
exporter = plugins.get_exporter(args.export)
|
||||
exporter = Exporter(args.export)
|
||||
print(exporter.export(journal, args.output))
|
||||
|
||||
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