The amount is the headline, notes come first at full width, then the last twelve visits to the same place as a dot timeline next to the paying account's days around it with the running balance. Fields edit in place: j and k move, Enter opens and saves, Esc cancels, s c, s b, s t and s s jump to classification. A matching subscription shows as a suggestion, L links it.
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
export type Inline =
|
|
| { kind: "text" | "bold" | "code"; text: string }
|
|
| { kind: "link"; text: string; href: string };
|
|
|
|
export type Block =
|
|
| { kind: "head"; line: Inline[] }
|
|
| { kind: "list"; ordered: boolean; items: Inline[][] }
|
|
| { kind: "para"; lines: Inline[][] };
|
|
|
|
const TOKEN =
|
|
/\*\*([^*]+)\*\*|`([^`]+)`|\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()]*[^\s<>().,;:!?'"])/g;
|
|
const BULLET = /^\s*(?:[-*•]|(\d+)[.)])\s+(.*)$/;
|
|
const HEAD = /^#{1,6}\s+(.*)$/;
|
|
const LINES = /\r?\n/;
|
|
|
|
export function inline(text: string): Inline[] {
|
|
const out: Inline[] = [];
|
|
let last = 0;
|
|
for (const match of text.matchAll(TOKEN)) {
|
|
const [whole, bold, code, label, href, url] = match;
|
|
if (match.index > last) {
|
|
out.push({ kind: "text", text: text.slice(last, match.index) });
|
|
}
|
|
if (bold) {
|
|
out.push({ kind: "bold", text: bold });
|
|
} else if (code) {
|
|
out.push({ kind: "code", text: code });
|
|
} else {
|
|
out.push({ href: href ?? url, kind: "link", text: label ?? url });
|
|
}
|
|
last = match.index + whole.length;
|
|
}
|
|
if (last < text.length) {
|
|
out.push({ kind: "text", text: text.slice(last) });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function blocks(notes: string): Block[] {
|
|
const result: Block[] = [];
|
|
let open: Block | null = null;
|
|
for (const raw of notes.split(LINES)) {
|
|
const line = raw.trimEnd();
|
|
const head = line.match(HEAD);
|
|
const bullet = line.match(BULLET);
|
|
if (!line.trim()) {
|
|
open = null;
|
|
} else if (head) {
|
|
result.push({ kind: "head", line: inline(head[1]) });
|
|
open = null;
|
|
} else if (bullet) {
|
|
const ordered = Boolean(bullet[1]);
|
|
if (open?.kind !== "list" || open.ordered !== ordered) {
|
|
open = { items: [], kind: "list", ordered };
|
|
result.push(open);
|
|
}
|
|
open.items.push(inline(bullet[2]));
|
|
} else {
|
|
if (open?.kind !== "para") {
|
|
open = { kind: "para", lines: [] };
|
|
result.push(open);
|
|
}
|
|
open.lines.push(inline(line));
|
|
}
|
|
}
|
|
return result;
|
|
}
|