A command-line journaling application
at exploded 95 lines 2.9 kB view raw
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}