A command-line journaling application
1import { DateTime } from "https://esm.sh/luxon@3.4.4";
2import { chunk } from "https://deno.land/std@0.219.0/collections/chunk.ts";
3
4export { DateTime };
5
6export interface Entry {
7 date: DateTime;
8 title: string;
9 body: string;
10}
11
12export const DATE_STRING = "yyyy-MM-dd HH:mm";
13
14export function printEntry(entry: Entry, summary: boolean | undefined) {
15 console.log(
16 `[${entry.date.toFormat(DATE_STRING)}] ${entry.title}`,
17 );
18 if (!summary) console.log(entry.body.trimEnd());
19}
20
21function parseEntry(header: string, body: string): Entry {
22 const m = header.match(
23 /\[(?<date>\d{4}-\d{2}-\d{2} \d{2}:\d{2})\] (?<title>.*)/,
24 );
25 if (m == null) throw new Error(`Bad header: ${header} (body is ${body})`);
26 const [dateChunk, title] = [m.groups!["date"], m.groups!["title"]];
27 const date = DateTime.fromFormat(dateChunk, DATE_STRING);
28 return { date, title, body };
29}
30
31export function parseEntries(bodies: string): Entry[] {
32 const entries = bodies.split(/^(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\] .*)\n/m);
33 if (entries[0].trim() === "") entries.splice(0, 1);
34 return chunk(entries, 2).map((e) => parseEntry(e[0], e[1].trim()));
35}
36
37export function renderEntry(entry: Entry): string {
38 let text = `[${entry.date.toFormat(DATE_STRING)}] ${entry.title.trim()}\n`;
39 const body = entry.body?.trim();
40 if (body != null && body !== "") {
41 text += body + "\n";
42 }
43 return text;
44}
45
46export function saveEntries(path: string, entries: Entry[]) {
47 let file;
48 const encoder = new TextEncoder();
49 try {
50 entries.sort((a, b) => a.date.valueOf() - b.date.valueOf());
51 file = Deno.openSync(path, { write: true });
52 file.truncateSync();
53 let first = true;
54 const nl = new Uint8Array([10]);
55 for (const entry of entries) {
56 if (!first) {
57 file.writeSync(nl);
58 }
59 first = false;
60 file.writeSync(encoder.encode(renderEntry(entry)));
61 }
62 } finally {
63 file?.close();
64 }
65}
66
67export function makeEntry(rawText: string, created?: DateTime): Entry {
68 const components = rawText.match(/([^.?!]+[.?!]*)(.*)/s);
69 const title = components?.[1]?.trim() ?? "";
70 const body = components?.[2]?.trim() ?? "";
71 const date = created ?? DateTime.now();
72 return { date, title, body };
73}
74
75export interface FilterParams {
76 from?: DateTime;
77 to?: DateTime;
78 limit?: number;
79 tags?: string[];
80}
81
82export function matchesFilter(entry: Entry, params: FilterParams) {
83 return (params.from ? params.from <= entry.date : true) &&
84 (params.to ? params.to >= entry.date : true) &&
85 (params.tags
86 ? params.tags.every((t) =>
87 entry.title.includes(t) || entry.body.includes(t)
88 )
89 : true);
90}
91
92export function filterEntries(entries: Entry[], params: FilterParams): Entry[] {
93 const filtered = entries.filter((e) => matchesFilter(e, params));
94 return params.limit ? filtered.slice(params.limit) : filtered;
95}