markdownLinks
Where a Markdown file links, outside code (fences and inline code): [text](target), [label]: target, and
@link tags naming code (code). Offsets are the target's.
export function markdownLinks(text: string): { start: number; end: number; target: string; code: boolean }[] { const out: { start: number; end: number; target: string; code: boolean }[] = []; let fence: string | undefined; let at = 0; for (const line of text.split("\n")) { const start = at; at += line.length + 1; const open = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1]; if (fence) { if (open && open[0] === fence[0] && open.length >= fence.length && !line.slice(line.indexOf(open) + open.length).trim()) fence = undefined; continue; } if (open) { fence = open; continue; } // inline code and HTML comments read as spaces: their links aren't links const plain = line.replace(/(`+)[\s\S]*?\1|<!--[\s\S]*?-->/g, (m) => " ".repeat(m.length)); for (const m of plain.matchAll(/\]\((<[^>]*>|[^)\s]+)(?:\s+"[^"]*")?\)/g)) { const target = m[1]!.replace(/^<|>$/g, ""); const s = start + m.index! + 2 + (m[1]!.startsWith("<") ? 1 : 0); out.push({ start: s, end: s + target.length, target, code: false }); } const def = /^ {0,3}\[[^\]]+\]:\s*(\S+)/.exec(plain); if (def) { const s = start + def[0].length - def[1]!.length; out.push({ start: s, end: s + def[1]!.length, target: def[1]!, code: false }); } for (const m of plain.matchAll(/\{@link\s+([\w$.-]+)/g)) { const s = start + m.index! + m[0].length - m[1]!.length; out.push({ start: s, end: s + m[1]!.length, target: m[1]!, code: true }); } } return out.sort((a, b) => a.start - b.start); }