This commit is contained in:
Manuel Ebert 2016-07-17 13:35:32 -07:00
parent ba05411a80
commit 85934c1980
8 changed files with 273 additions and 3 deletions

View file

@ -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.

View file

@ -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
@ -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
View 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
View 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)

View 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

View 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. Heres 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 didnt ask them for help until the day I broke up with Beth, two years ago.

View 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!

View file

@ -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.