Seperation of title and entry is now purely virtual.

Fixes #360
This commit is contained in:
Manuel Ebert 2015-12-28 21:24:39 -08:00
parent 849dc89557
commit 95d399d5c3
5 changed files with 90 additions and 83 deletions

View file

@ -48,12 +48,9 @@ class DayOne(Journal.Journal):
timezone = tzlocal.get_localzone() timezone = tzlocal.get_localzone()
date = dict_entry['Creation Date'] date = dict_entry['Creation Date']
date = date + timezone.utcoffset(date, is_dst=False) date = date + timezone.utcoffset(date, is_dst=False)
raw = dict_entry['Entry Text'] entry = Entry.Entry(self, date, text=dict_entry['Entry Text'], starred=dict_entry["Starred"])
sep = re.search("\n|[\?!.]+ +\n?", raw)
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
entry = Entry.Entry(self, date, title, body, starred=dict_entry["Starred"])
entry.uuid = dict_entry["UUID"] entry.uuid = dict_entry["UUID"]
entry.tags = [self.config['tagsymbols'][0] + tag for tag in dict_entry.get("Tags", [])] entry._tags = [self.config['tagsymbols'][0] + tag for tag in dict_entry.get("Tags", [])]
self.entries.append(entry) self.entries.append(entry)
self.sort() self.sort()
@ -129,7 +126,7 @@ class DayOne(Journal.Journal):
# Now, update our current entries if they changed # Now, update our current entries if they changed
for entry in entries: for entry in entries:
entry.parse_tags() entry._parse_text()
matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid] matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid]
if matched_entries: if matched_entries:
# This entry is an existing entry # This entry is an existing entry

View file

@ -5,15 +5,15 @@ from __future__ import unicode_literals
import re import re
import textwrap import textwrap
from datetime import datetime from datetime import datetime
from .util import split_title
class Entry: class Entry:
def __init__(self, journal, date=None, title="", body="", starred=False): def __init__(self, journal, date=None, text="", starred=False):
self.journal = journal # Reference to journal mainly to access it's config self.journal = journal # Reference to journal mainly to access its config
self.date = date or datetime.now() self.date = date or datetime.now()
self.title = title.rstrip("\n ") self.text = text
self.body = body.rstrip("\n ") self._title = self._body = self._tags = None
self.tags = self.parse_tags()
self.starred = starred self.starred = starred
self.modified = False self.modified = False
@ -21,17 +21,42 @@ class Entry:
def fulltext(self): def fulltext(self):
return self.title + " " + self.body return self.title + " " + self.body
def _parse_text(self):
raw_text = self.text
lines = raw_text.splitlines()
if lines[0].strip().endswith("*"):
self.starred = True
raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:])
self._title, self._body = split_title(raw_text)
if self._tags is None:
self._tags = list(self._parse_tags())
@property
def title(self):
if self._title is None:
self._parse_text()
return self._title
@property
def body(self):
if self._body is None:
self._parse_text()
return self._body
@property
def tags(self):
if self._tags is None:
self._parse_text()
return self._tags
@staticmethod @staticmethod
def tag_regex(tagsymbols): def tag_regex(tagsymbols):
pattern = r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=tagsymbols) pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
return re.compile(pattern, re.UNICODE) return re.compile(pattern, re.UNICODE)
def parse_tags(self): def _parse_tags(self):
fulltext = " " + " ".join([self.title, self.body]).lower()
tagsymbols = self.journal.config['tagsymbols'] tagsymbols = self.journal.config['tagsymbols']
tags = re.findall(Entry.tag_regex(tagsymbols), fulltext) return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text))
self.tags = tags
return set(tags)
def __unicode__(self): def __unicode__(self):
"""Returns a string representation of the entry to be written into a journal file.""" """Returns a string representation of the entry to be written into a journal file."""

View file

@ -88,40 +88,22 @@ class Journal(object):
"""Parses a journal that's stored in a string and returns a list of entries""" """Parses a journal that's stored in a string and returns a list of entries"""
# Initialise our current entry # Initialise our current entry
entries = [] entries = []
current_entry = None date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ")
date_blob_re = re.compile("^\[[^\\]]+\] ") last_entry_pos = 0
for line in journal_txt.splitlines(): for match in date_blob_re.finditer(journal_txt):
line = line.rstrip() date_blob = match.groups()[0]
date_blob = date_blob_re.findall(line) new_date = time.parse(date_blob)
if date_blob:
date_blob = date_blob[0]
new_date = time.parse(date_blob.strip(" []"))
if new_date: if new_date:
# Found a date at the start of the line: This is a new entry. if entries:
if current_entry: entries[-1].text = journal_txt[last_entry_pos:match.start()]
entries.append(current_entry) last_entry_pos = match.end()
entries.append(Entry.Entry(self, date=new_date))
# Finish the last entry
if entries:
entries[-1].text = journal_txt[last_entry_pos:]
if line.endswith("*"):
starred = True
line = line[:-1]
else:
starred = False
current_entry = Entry.Entry(
self,
date=new_date,
title=line[len(date_blob):],
starred=starred
)
elif current_entry:
# Didn't find a date - keep on feeding to current entry.
current_entry.body += line + "\n"
# Append last entry
if current_entry:
entries.append(current_entry)
for entry in entries: for entry in entries:
entry.parse_tags() entry._parse_text()
return entries return entries
def __unicode__(self): def __unicode__(self):
@ -183,20 +165,7 @@ class Journal(object):
and (not start_date or entry.date >= start_date) and (not start_date or entry.date >= start_date)
and (not end_date or entry.date <= end_date) and (not end_date or entry.date <= end_date)
] ]
if short:
if tags:
for e in self.entries:
res = []
for tag in tags:
matches = [m for m in re.finditer(tag, e.body)]
for m in matches:
date = e.date.strftime(self.config['timeformat'])
excerpt = e.body[m.start():min(len(e.body), m.end() + 60)]
res.append('{0} {1} ..'.format(date, excerpt))
e.body = "\n".join(res)
else:
for e in self.entries:
e.body = ''
self.entries = result self.entries = result
def new_entry(self, raw, date=None, sort=True): def new_entry(self, raw, date=None, sort=True):
@ -207,23 +176,20 @@ class Journal(object):
starred = False starred = False
# Split raw text into title and body # Split raw text into title and body
sep = re.search("\n|[\?!.]+ +\n?", raw) sep = re.search("\n|[\?!.]+ +\n?", raw)
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "") first_line = raw[:sep.end()].strip() if sep else raw
starred = False starred = False
if not date: if not date:
if title.find(": ") > 0: colon_pos = first_line.find(": ")
starred = "*" in title[:title.find(": ")] if colon_pos > 0:
date = time.parse(title[:title.find(": ")], default_hour=self.config['default_hour'], default_minute=self.config['default_minute']) date = time.parse(raw[:colon_pos], default_hour=self.config['default_hour'], default_minute=self.config['default_minute'])
if date or starred: # Parsed successfully, strip that from the raw text if date: # Parsed successfully, strip that from the raw text
title = title[title.find(": ") + 1:].strip() starred = raw[:colon_pos].strip().endswith("*")
elif title.strip().startswith("*"): raw = raw[colon_pos + 1:].strip()
starred = True starred = starred or first_line.startswith("*") or first_line.endswith("*")
title = title[1:].strip()
elif title.strip().endswith("*"):
starred = True
title = title[:-1].strip()
if not date: # Still nothing? Meh, just live in the moment. if not date: # Still nothing? Meh, just live in the moment.
date = time.parse("now") date = time.parse("now")
entry = Entry.Entry(self, date, title, body, starred=starred) entry = Entry.Entry(self, date, raw, starred=starred)
entry.modified = True entry.modified = True
self.entries.append(entry) self.entries.append(entry)
if sort: if sort:
@ -264,8 +230,7 @@ class PlainJournal(Journal):
class LegacyJournal(Journal): class LegacyJournal(Journal):
"""Legacy class to support opening journals formatted with the jrnl 1.x """Legacy class to support opening journals formatted with the jrnl 1.x
standard. Main difference here is that in 1.x, timestamps were not cuddled standard. Main difference here is that in 1.x, timestamps were not cuddled
by square brackets, and the line break between the title and the rest of by square brackets. You'll not be able to save these journals anymore."""
the entry was not enforced. You'll not be able to save these journals anymore."""
def _load(self, filename): def _load(self, filename):
with codecs.open(filename, "r", "utf-8") as f: with codecs.open(filename, "r", "utf-8") as f:
return f.read() return f.read()
@ -295,18 +260,18 @@ class LegacyJournal(Journal):
else: else:
starred = False starred = False
current_entry = Entry.Entry(self, date=new_date, title=line[date_length + 1:], starred=starred) current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred)
except ValueError: except ValueError:
# Happens when we can't parse the start of the line as an date. # Happens when we can't parse the start of the line as an date.
# In this case, just append line to our body. # In this case, just append line to our body.
if current_entry: if current_entry:
current_entry.body += line + u"\n" current_entry.text += line + u"\n"
# Append last entry # Append last entry
if current_entry: if current_entry:
entries.append(current_entry) entries.append(current_entry)
for entry in entries: for entry in entries:
entry.parse_tags() entry._parse_text()
return entries return entries

View file

@ -5,7 +5,7 @@ from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter from .text_exporter import TextExporter
import re import re
import sys import sys
from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR from ..util import WARNING_COLOR, RESET_COLOR
class MarkdownExporter(TextExporter): class MarkdownExporter(TextExporter):
@ -52,7 +52,7 @@ class MarkdownExporter(TextExporter):
if warn_on_heading_level is True: if warn_on_heading_level is True:
print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr) print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
return "{md} {date} {title} {body} {space}".format( return "{md} {date} {title}\n{body} {space}".format(
md=heading, md=heading,
date=date_str, date=date_str,
title=entry.title, title=entry.title,

View file

@ -30,6 +30,18 @@ WARNING_COLOR = "\033[33m"
ERROR_COLOR = "\033[31m" ERROR_COLOR = "\033[31m"
RESET_COLOR = "\033[0m" RESET_COLOR = "\033[0m"
# Based on Segtok by Florian Leitner
# https://github.com/fnl/segtok
SENTENCE_SPLITTER = re.compile(r"""
( # A sentence ends at one of two sequences:
[.!?\u203C\u203D\u2047\u2048\u2049\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61] # Either, a sequence starting with a sentence terminal,
[\'\u2019\"\u201D]? # an optional right quote,
[\]\)]* # optional closing brackets and
\s+ # a sequence of required spaces.
| # Otherwise,
\n # a sentence also terminates newlines.
)""", re.UNICODE | re.VERBOSE)
def getpass(prompt="Password: "): def getpass(prompt="Password: "):
if not TEST: if not TEST:
@ -186,3 +198,11 @@ def byte2int(b):
"""Converts a byte to an integer. """Converts a byte to an integer.
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3.""" This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
return ord(b)if PY2 else b return ord(b)if PY2 else b
def split_title(text):
"""Splits the first sentence off from a text."""
punkt = SENTENCE_SPLITTER.search(text)
if not punkt:
return text, ""
return text[:punkt.end()].rstrip(), text[punkt.end():].lstrip()