entry modules refactor

This commit is contained in:
olari
2021-06-27 15:18:48 +03:00
parent 760d072982
commit 954e0c12af
2 changed files with 138 additions and 143 deletions

View File

@@ -11,6 +11,7 @@ import random
import re
import sys
import textwrap
import traceback
### GLOBALS
@@ -18,6 +19,9 @@ JOURNAL_PATH = Path.home() / '.journal.json'
### UTILS
def compose(*fns):
return partial(reduce, flip(apply), fns)
def nth_or_default(n, l, default):
return l[n] if n < len(l) else default
@@ -287,60 +291,8 @@ def create_sticky(journal, date):
### ENTRY MODULES
def parse_post(block):
block = block.removeprefix('@post').strip()
try:
timestamp = int(parse_timestamp(block[:19]).timestamp())
block = block[19:]
except:
timestamp = None
content = block.strip()
result = {}
if content:
result['content'] = content
if timestamp:
result['timestamp'] = timestamp
return result
def generate_post(block):
result = '@post'
if ts := block.get('timestamp'):
result += f' {format_timestamp(ts)}'
if content := block.get('content'):
result += f' {content}'
return wrap_text(result)
def parse_notes(block):
tag, source, title = block.splitlines()
return {'source': source, 'title': title}
def generate_notes(block):
parts = ['@notes']
if source := block.get('source'):
parts.append(source)
if title := block.get('title'):
parts.append(title)
return '\n'.join(parts)
def parse_diet(block):
tag, amount, food = block.split()
amount = int(amount.removesuffix('g'))
return {'amount': amount, 'food': food}
def generate_diet(block):
_, amount, food = block.values()
return f'@diet {amount}g {food}'
def parse_timer(block):
tag, *rest = block.split()
rest = block.split()
name = None
timestamp = None
@@ -358,7 +310,7 @@ def parse_timer(block):
return result
def generate_timer(block):
parts = [f'@{block["type"]}']
parts = []
if name := block.get('name'):
parts.append(name)
@@ -369,7 +321,7 @@ def generate_timer(block):
return ' '.join(parts)
def parse_exercise(block):
tag, *parts = block.split()
parts = block.split()
if parts[0] == 'walk':
kind, minutes, distance, steps = parts
@@ -394,25 +346,62 @@ def parse_exercise(block):
def generate_exercise(block):
if block['kind'] == 'walk':
return f'@exercise walk {block["minutes"]}min {block["distance"]}km {block["steps"]}steps'
return f'walk {block["minutes"]}min {block["distance"]}km {block["steps"]}steps'
elif block['kind'] == 'calisthenics':
return f'@exercise calisthenics {block["sets"]}x{block["reps"]} {block["exercise"]}'
return f'calisthenics {block["sets"]}x{block["reps"]} {block["exercise"]}'
assert False
def parse_notify(block):
tag, day, *rest = block.split()
DEFAULT_PARSER = lambda b: {'value': b}
DEFAULT_GENERATOR = lambda b: b['value']
return {'day': day.strip(), 'message': ' '.join(rest)}
entry_modules = {
'diet': (
lambda b: {'amount': int(b.split()[0].removesuffix('g')), 'food': b.split()[1].strip()},
lambda b: f'{b["amount"]}g {b["food"]}'),
'exercise': (parse_exercise, generate_exercise),
'behavior': (DEFAULT_PARSER, DEFAULT_GENERATOR),
def generate_notify(block):
return f'@notify {block["day"]} {block["message"]}'
'hide': (lambda _: {}, lambda _: ''),
'info': (DEFAULT_PARSER, compose(DEFAULT_GENERATOR, wrap_text)),
def generate_default(block):
return f'@{block["type"]} {block["value"]}'
'post': (
lambda b: {'timestamp': int(parse_timestamp(b.removeprefix('@post ').strip()).timestamp())},
lambda b: format_timestamp(b["timestamp"])
),
'notes': (
compose(
lambda b: b.splitlines(),
lambda s: {'source': s[0], 'title': s[1]},
),
lambda b: f'{b["source"]}\n{b["title"]}'
),
def generate_info(block):
return wrap_text(f'@info {block["value"]}')
'task': (DEFAULT_PARSER, DEFAULT_GENERATOR),
'start': (parse_timer, generate_timer),
'stop': (parse_timer, generate_timer),
'done': (parse_timer, generate_timer),
'notify': (
compose(
lambda b: b.split(maxsplit=1),
lambda s: {'day': s[0], 'message': s[1]}
),
lambda b: f'{b["day"]} {b["message"]}'
),
}
def parse_entry_module(block: str) -> dict:
tag = block.split()[0].removeprefix('@')
block = block.removeprefix(f'@{tag}').strip()
return {'type': tag} | entry_modules[tag][0](block)
def generate_entry_module(block: dict) -> str:
if block['type'] == 'notes':
return f'@notes\n{entry_modules[block["type"]][1](block)}'
return f'@{block["type"]} {entry_modules[block["type"]][1](block)}'
### READ-ONLY STATS SECTION FUNCTIONS
@@ -475,7 +464,6 @@ def generate_stats(page):
return result
### PAGE FUNCTIONS
def create_header(journal, date):
@@ -537,25 +525,6 @@ def create_entry(journal, date):
}
def parse_entry(entry):
def create_entry_module_parser(name, handler=None):
handler = handler or (lambda b: {'value': b.removeprefix(f'@{name} ')})
return (name, lambda b: {'type': name} | handler(b))
entry_modules = dict([
create_entry_module_parser('hide', lambda _: {}),
create_entry_module_parser('post', parse_post),
create_entry_module_parser('info'),
create_entry_module_parser('notes', parse_notes),
create_entry_module_parser('behavior'),
create_entry_module_parser('diet', parse_diet),
create_entry_module_parser('task'),
create_entry_module_parser('start', parse_timer),
create_entry_module_parser('stop', parse_timer),
create_entry_module_parser('done', parse_timer),
create_entry_module_parser('exercise', parse_exercise),
create_entry_module_parser('notify', parse_notify),
])
def merge_notes_block(l):
res = []
@@ -606,71 +575,66 @@ def parse_entry(entry):
return res
def split_into_blocks(text):
return reduce(flip(apply), [
# split the text into sections by newline and tag symbol, keeping the separators
partial(split_keep, ('\n', '@')),
def split_post_block(l):
res = []
# merge sequential newlines together into a single whitespace block
partial(merge_if, lambda p, c: p == c == '\n'),
POST_BLOCK_LENGTH = len('@post 2020-02-02 02:02:02')
# attach escaped tags
partial(merge_if, lambda p, c: c == '@' and p[-1] == '\\'),
# attach tag
partial(merge_if, lambda p, c: p == '@'),
# attach tags which do not come after newline or another tag
partial(merge_if, lambda p, c: c[0] == '@' and not (not p[-1] != '\n' or (p[0] == '@' and p[-1] == ' '))),
i = 0
while i < len(l):
curr = l[i]
# merge notes block
merge_notes_block,
# strip all non-whitespace blocks
partial(map, lambda s: s if s.isspace() else s.rstrip()), list,
# merge escaped tags with following text
partial(merge_if, lambda p, c: p.endswith('\\@')),
# merge wrapped lines
merge_wrapped_lines,
# remove trailing whitespace block
lambda b: b if b and not all(c == '\n' for c in b[-1]) else b[:-1],
], text)
if curr.startswith('@post'):
res.append(curr[:POST_BLOCK_LENGTH])
res.append(curr[POST_BLOCK_LENGTH+1:])
else:
res.append(curr)
def parse_module_block(block):
tag = block.split()[0][1:]
return entry_modules[tag](block)
i += 1
return res
def parse_block(block):
if block.startswith('@'):
return parse_module_block(block)
else:
return block
split_into_blocks = compose(
# split the text into sections by newline and tag symbol, keeping the separators
partial(split_keep, ('\n', '@')),
# merge sequential newlines together into a single whitespace block
partial(merge_if, lambda p, c: p == c == '\n'),
## TAG PARSING
# attach escaped tags
partial(merge_if, lambda p, c: c == '@' and p[-1] == '\\'),
# attach tag
partial(merge_if, lambda p, c: p == '@'),
# attach tags which do not come after newline or another tag
partial(merge_if, lambda p, c: c[0] == '@' and not (not p[-1] != '\n' or (p[0] == '@' and p[-1] == ' '))),
## SPECIAL BLOCK PARSING
# merge notes block (because it spans 3 lines or 5 blocks)
merge_notes_block,
# split post block (because next block could be attached to it)
split_post_block,
# strip all non-whitespace blocks
partial(map, lambda s: s if s.isspace() else s.rstrip()), list,
# merge escaped tags with following text
partial(merge_if, lambda p, c: p.endswith('\\@')),
# merge wrapped lines
merge_wrapped_lines,
# remove trailing whitespace block
lambda b: b if b and not all(c == '\n' for c in b[-1]) else b[:-1],
)
timestamp, content = entry
return {
'timestamp': int(parse_timestamp(timestamp.strip()).timestamp()),
'blocks': [parse_block(b) for b in split_into_blocks(content)],
'blocks': [parse_entry_module(b) if b.startswith('@') else b for b in split_into_blocks(content)],
}
def generate_entry(entry):
entry_modules = {
'diet': generate_diet,
'exercise': generate_exercise,
'behavior': generate_default,
'hide': lambda _: '@hide',
'info': generate_info,
'post': generate_post,
'notes': generate_notes,
'task': generate_default,
'start': generate_timer,
'stop': generate_timer,
'done': generate_timer,
'notify': generate_notify,
}
def format_block(block, is_first):
def format_text(text):
if all(c == '\n' for c in block):
@@ -691,10 +655,7 @@ def generate_entry(entry):
return text
def format_module(module):
return entry_modules[module['type']](module)
formatted = format_text(block) if isinstance(block, str) else format_module(block)
formatted = format_text(block) if isinstance(block, str) else generate_entry_module(block)
if result[-1] != '\n' and not all(c == '\n' for c in formatted):
formatted = ' ' + formatted
@@ -816,7 +777,7 @@ def open_journal(date):
new_journal = import_journal(tmpdir)
break
except Exception as e:
print('Error:', e)
traceback.print_exc()
input('Press enter to try again...')
save_journal(new_journal)

View File

@@ -0,0 +1,34 @@
from copy import deepcopy
from pathlib import Path
from shutil import copy
import json
journal_path = Path.home() / '.journal.json'
copy(str(journal_path), str(journal_path.with_suffix('.bkp')))
journal = json.loads(journal_path.read_text())
new_journal = deepcopy(journal)
for day in journal['days']:
new_entries = []
for entry in journal['days'][day]['entries']:
new_blocks = []
for block in entry['blocks']:
if not isinstance(block, str) and block['type'] == 'post':
new_blocks.append({
'type': 'post',
'timestamp': block.get('timestamp', entry['timestamp'] + 30)
})
if content := block.get('content'):
new_blocks.append(content)
else:
new_blocks.append(block)
entry['blocks'] = new_blocks
new_entries.append(entry)
new_journal['days'][day]['entries'] = new_entries
journal_path.write_text(json.dumps(new_journal))