markdownLinks

tcmts.cli.files.markdownLinks · #5nuactlt · term · runs anywhere

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);
}

used by