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