Inline question choices, compact one-click inbox cards, drop snooze

This commit is contained in:
Ian Gulliver
2026-10-14 23:34:39 -07:00
parent 4dc86e5479
commit c7cfad88a1
30 changed files with 1022 additions and 647 deletions
+54 -10
View File
@@ -2,6 +2,12 @@ import { h } from "./dom.js";
type Block = HTMLElement;
export type ChoiceHook = (text: string) => HTMLElement | null;
const choiceMarker = /^(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$/;
const detailsOpen = /^\s*<details>\s*(?:<summary>(.*?)<\/summary>)?\s*$/i;
const summaryLine = /^\s*<summary>(.*?)<\/summary>\s*$/i;
const detailsClose = /^\s*<\/details>\s*$/i;
const fence = /^\s*(```|~~~)/;
const heading = /^(#{1,6})\s+(.*)$/;
const bullet = /^(\s*)[-*+]\s+(.*)$/;
@@ -10,15 +16,15 @@ const quote = /^\s*>\s?(.*)$/;
const rule = /^\s*([-*_])(\s*\1){2,}\s*$/;
const tableSep = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
export function markdown(src: string, className = "md"): HTMLElement {
return h("div", { class: className }, ...blocks(src.replace(/\r\n?/g, "\n").split("\n")));
export function markdown(src: string, className = "md", choice?: ChoiceHook): HTMLElement {
return h("div", { class: className }, ...blocks(src.replace(/\r\n?/g, "\n").split("\n"), choice));
}
export function inlineMarkdown(src: string): HTMLElement {
return h("span", { class: "md-inline" }, ...inline(src));
}
function blocks(lines: string[]): Block[] {
function blocks(lines: string[], choice?: ChoiceHook): Block[] {
const out: Block[] = [];
let i = 0;
@@ -44,6 +50,35 @@ function blocks(lines: string[]): Block[] {
continue;
}
if (detailsClose.test(line)) {
i++;
continue;
}
const d = detailsOpen.exec(line);
if (d) {
let title = d[1];
i++;
if (title === undefined && i < lines.length && summaryLine.test(lines[i]!)) {
title = summaryLine.exec(lines[i]!)![1];
i++;
}
const body: string[] = [];
let depth = 1;
while (i < lines.length) {
const l = lines[i]!;
i++;
if (detailsOpen.test(l)) {
depth++;
} else if (detailsClose.test(l) && --depth === 0) {
break;
}
body.push(l);
}
out.push(h("details", { class: "md-details" }, h("summary", {}, ...inline(title?.trim() || "Details")), h("div", { class: "md-details-body" }, ...blocks(body, choice))));
continue;
}
const hd = heading.exec(line);
if (hd) {
const level = Math.min(hd[1]!.length + 2, 6) as 3 | 4 | 5 | 6;
@@ -69,7 +104,7 @@ function blocks(lines: string[]): Block[] {
}
if (bullet.test(line) || ordered.test(line)) {
const [list, next] = parseList(lines, i);
const [list, next] = parseList(lines, i, choice);
out.push(list);
i = next;
continue;
@@ -101,6 +136,8 @@ function startsBlock(lines: string[], i: number): boolean {
const line = lines[i]!;
return (
fence.test(line) ||
detailsOpen.test(line) ||
detailsClose.test(line) ||
heading.test(line) ||
rule.test(line) ||
quote.test(line) ||
@@ -125,7 +162,7 @@ function indentOf(line: string): number {
return line.length - line.trimStart().length;
}
function parseList(lines: string[], start: number): [HTMLElement, number] {
function parseList(lines: string[], start: number, choice?: ChoiceHook): [HTMLElement, number] {
const first = lines[start]!;
const isOrdered = !bullet.test(first);
const base = indentOf(first);
@@ -164,7 +201,7 @@ function parseList(lines: string[], start: number): [HTMLElement, number] {
body.push(indentOf(next) > base ? next.slice(Math.min(indentOf(next), base + 2)) : next.trim());
i++;
}
list.append(item(body));
list.append(item(body, isOrdered ? undefined : choice));
while (i < lines.length && lines[i]!.trim() === "" && lines[i + 1] !== undefined && indentOf(lines[i + 1]!) === base && (isOrdered ? ordered : bullet).test(lines[i + 1]!)) {
i++;
}
@@ -172,20 +209,27 @@ function parseList(lines: string[], start: number): [HTMLElement, number] {
return [list, i];
}
function item(body: string[]): HTMLElement {
function item(body: string[], choice?: ChoiceHook): HTMLElement {
const li = h("li");
const m = choice && choiceMarker.exec(body[0]!);
const picker = m ? choice(m[2]!) : null;
if (picker) {
li.className = "choice";
li.append(picker);
}
const first = picker ? [] : [body[0]!];
const rest = body.slice(1);
if (rest.every((l) => !startsBlockLine(l)) && !rest.includes("")) {
li.append(...joinLines(body.map((l) => l.trim())));
li.append(...joinLines([...first, ...rest].map((l) => l.trim())));
return li;
}
const head: string[] = [body[0]!];
const head: string[] = first;
let k = 1;
while (k < body.length && body[k] !== "" && !startsBlockLine(body[k]!)) {
head.push(body[k]!.trim());
k++;
}
li.append(...joinLines(head), ...blocks(body.slice(k)));
li.append(...joinLines(head), ...blocks(body.slice(k), choice));
return li;
}