import type { MarkdownIt, Token } from "markdown-it"; import { blockAnchor, STANDALONE_BLOCK_ID_RE, TRAILING_BLOCK_ID_RE, } from "../block-ids.ts"; // Tight list items hide their paragraph, so an id set there would never reach the HTML. function anchorToken(tokens: Token[], inlineIndex: number): Token | null { let i = inlineIndex - 1; while (tokens[i]?.hidden && tokens[i - 1]?.nesting === 1) i--; return tokens[i] ?? null; } function isBlockStart(token: Token): boolean { return token.nesting !== -1 && token.type !== "inline"; } // Obsidian block ids: "text ^id" ending a block, or "^id" alone on the line after one. // Headings are excluded — Obsidian addresses those with "#Heading", and their id is // already the heading anchor. export function blockIdPlugin(mdi: MarkdownIt): void { mdi.core.ruler.after("block", "block_id", (state) => { const tokens = state.tokens; const standalone: number[] = []; let previousBlock: Token | null = null; let currentBlock: Token | null = null; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (!token) continue; if (token.level === 0 && isBlockStart(token)) { previousBlock = currentBlock; currentBlock = token; } if (token.type !== "inline") continue; const content = token.content.trimEnd(); const alone = STANDALONE_BLOCK_ID_RE.exec(content); if (alone?.[1] && token.level === 1 && previousBlock) { const anchor = blockAnchor(alone[1]); if (anchor) { previousBlock.attrSet("id", anchor); standalone.push(i - 1); currentBlock = previousBlock; continue; } } const trailing = TRAILING_BLOCK_ID_RE.exec(content); if (!trailing?.[1]) continue; const target = anchorToken(tokens, i); if (!target || target.type === "heading_open") continue; const anchor = blockAnchor(trailing[1]); if (!anchor) continue; target.attrSet("id", anchor); token.content = content.slice(0, trailing.index); } // Back to front: the marker paragraph itself leaves no output. for (let i = standalone.length - 1; i >= 0; i--) { tokens.splice(standalone[i] as number, 3); } }); }