/** * Section parser: Split markdown by headings and extract wikilinks per section. */ import { normalizeLinkTarget } from "../unresolvedLink/linkHelpers"; export interface NoteSection { heading: string | null; // null for content before first heading headingLevel: number; // 0 for content before first heading content: string; // Full content of section (including heading) startLine: number; // Line index where section starts endLine: number; // Line index where section ends (exclusive) links: string[]; // Deduplicated wikilinks found in this section } /** * Split markdown content into sections by headings. */ export function splitIntoSections(markdown: string): NoteSection[] { const lines = markdown.split(/\r?\n/); const sections: NoteSection[] = []; let currentSection: NoteSection | null = null; let currentContent: string[] = []; let currentStartLine = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line === undefined) continue; // Check if line is a heading const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { // Save previous section if exists if (currentSection !== null || currentContent.length > 0) { const content = currentContent.join("\n"); const links = extractWikilinks(content); sections.push({ heading: currentSection?.heading || null, headingLevel: currentSection?.headingLevel || 0, content: content, startLine: currentStartLine, endLine: i, links: links, }); } // Start new section const headingLevel = (headingMatch[1]?.length || 0); const headingText = (headingMatch[2]?.trim() || ""); currentSection = { heading: headingText, headingLevel: headingLevel, content: line, startLine: i, endLine: i + 1, links: [], }; currentContent = [line]; currentStartLine = i; } else { // Add line to current section if (currentSection) { currentContent.push(line); currentSection.content = currentContent.join("\n"); currentSection.endLine = i + 1; } else { // Content before first heading currentContent.push(line); } } } // Save last section if (currentSection || currentContent.length > 0) { const content = currentContent.join("\n"); const links = extractWikilinks(content); sections.push({ heading: currentSection?.heading || null, headingLevel: currentSection?.headingLevel || 0, content: content, startLine: currentStartLine, endLine: lines.length, links: links, }); } return sections; } /** * Extract all wikilinks from markdown text (deduplicated). */ export function extractWikilinks(markdown: string): string[] { const links = new Set(); const wikilinkRegex = /\[\[([^\]]+?)\]\]/g; let match: RegExpExecArray | null; while ((match = wikilinkRegex.exec(markdown)) !== null) { if (match[1]) { const target = match[1].trim(); if (target) { // Check if it's a rel: link format [[rel:type|link]] if (target.startsWith("rel:")) { // Extract link part after | const parts = target.split("|"); if (parts.length >= 2) { // It's [[rel:type|link]] format, extract the link part const linkPart = parts[1] || ""; if (linkPart) { // Normalize: remove alias (|alias) and heading (#heading) const normalized = normalizeLinkTarget(linkPart); if (normalized) { links.add(normalized); } } } } else { // Normal link format [[link]] // Normalize: remove alias (|alias) and heading (#heading) const normalized = normalizeLinkTarget(target); if (normalized) { links.add(normalized); } } } } } return Array.from(links).sort(); // Deterministic ordering }