A command-line journaling application
at bun-experiment 93 lines 2.8 kB view raw
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}