Something went wrong. Try again.
🌿 Collaborative wiki on ATProto forked from juprodh.me/lichen.wiki
Something went wrong. Try again.
3.3 kB · 90 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091import type { MarkdownIt, StateCore, Token } from "markdown-it";
const TASK_RE = /^\[([^\s\]])\] /;
function isCheckbox(token: Token): boolean { return ( token.type === "html_inline" && token.content.includes("task-list-item-checkbox") );}
function htmlInline(state: StateCore, content: string): Token { const token = new state.Token("html_inline", "", 0); token.content = content; return token;}
function checkboxHtml( escapeHtml: (value: string) => string, task: string,): string { const checked = task === "x" ? ' checked=""' : ""; return `<input class="task-list-item-checkbox" data-task="${escapeHtml(task)}"${checked} disabled="" type="checkbox">`;}
function addClass(token: Token | undefined, name: string): void { if (!token) return; const current = String(token.attrGet("class") ?? ""); if (!current.split(" ").includes(name)) token.attrJoin("class", name);}
// The list the item belongs to, found the way markdown-it-task-lists finds it.function parentToken(tokens: Token[], index: number): Token | undefined { const level = (tokens[index]?.level ?? 0) - 1; for (let i = index - 1; i >= 0; i--) { if (tokens[i]?.level === level) return tokens[i]; } return undefined;}
// Obsidian renders any single character between the brackets as a checkbox and exposes it// as `data-task` — the hook themes style `- [/]`, `- [?]` and `- [>]` from. markdown-it-task-lists// knows only space and `x`, so alternate states get their checkbox here and the two it// already handled get the same attribute. The attribute sits on the `li` too, which is what// Obsidian themes select on and what public/style.css draws the marker from.//// Alternate states render *unchecked*: every convention using them (`/` in progress,// `?` question, `>` forwarded, `-` cancelled) means not done, and a ticked box would say// the opposite to a reader with no theme installed.export function taskStatePlugin(mdi: MarkdownIt): void { const escapeHtml = mdi.utils.escapeHtml;
mdi.core.ruler.after("github-task-lists", "task_states", (state) => { const tokens = state.tokens; for (let i = 2; i < tokens.length; i++) { const inline = tokens[i]; const item = tokens[i - 2]; if (inline?.type !== "inline") continue; if (tokens[i - 1]?.type !== "paragraph_open") continue; if (item?.type !== "list_item_open") continue;
const existing = inline.children?.find(isCheckbox); if (existing) { const task = existing.content.includes('checked=""') ? "x" : " "; existing.content = checkboxHtml(escapeHtml, task); item.attrSet("data-task", task); continue; }
const match = TASK_RE.exec(inline.content); const first = inline.children?.[0]; if (!match || first?.type !== "text") continue;
// slice(3) keeps the space before the label text, as the sibling plugin does. const task = match[1] as string; first.content = first.content.slice(3); inline.content = inline.content.slice(3); inline.children?.unshift( htmlInline(state, "<label>"), htmlInline(state, checkboxHtml(escapeHtml, task)), ); inline.children?.push(htmlInline(state, "</label>"));
addClass(item, "task-list-item"); addClass(item, "task-alt"); item.attrSet("data-task", task); addClass(parentToken(tokens, i - 2), "contains-task-list"); } });}