Inline question choices, compact one-click inbox cards, drop snooze
This commit is contained in:
+54
-10
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user