/** * Section parser: Split markdown by headings and extract wikilinks per section. */ 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 /** WP-26: Section type from `> [!section] type` callout in this section; null if not set. */ sectionType: string | null; /** WP-26: Block ID from heading line (e.g. `## Title ^block-id`); null if not set. */ blockId: string | null; } /** Match `> [!section] type` callout (type = word characters). */ const SECTION_CALLOUT_REGEX = /^\s*>\s*\[!section\]\s*(\S+)\s*$/i; /** Match block ID at end of heading text: `... ^block-id`. */ const BLOCK_ID_IN_HEADING_REGEX = /\s+\^([a-zA-Z0-9_-]+)\s*$/; /** * 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, startLine: currentStartLine, endLine: i, links, sectionType: currentSection?.sectionType ?? null, blockId: currentSection?.blockId ?? null, }); } // Start new section: extract blockId from heading text (e.g. "Title ^my-id") const headingLevel = (headingMatch[1]?.length || 0); const headingText = (headingMatch[2]?.trim() || ""); const blockIdMatch = headingText.match(BLOCK_ID_IN_HEADING_REGEX); const blockId = blockIdMatch ? blockIdMatch[1] ?? null : null; currentSection = { heading: headingText, headingLevel, content: line, startLine: i, endLine: i + 1, links: [], sectionType: null, blockId, }; currentContent = [line]; currentStartLine = i; } else { // Add line to current section if (currentSection) { // WP-26: Detect `> [!section] type` callout (typically right after heading) const sectionCalloutMatch = line.match(SECTION_CALLOUT_REGEX); if (sectionCalloutMatch && sectionCalloutMatch[1]) { currentSection.sectionType = sectionCalloutMatch[1].trim(); } 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, startLine: currentStartLine, endLine: lines.length, links, sectionType: currentSection?.sectionType ?? null, blockId: currentSection?.blockId ?? null, }); } 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) { // Alias entfernen (|), Abschnitt (#) behalten für Edge-Block const linkTarget = linkPart.split("|")[0]?.trim() || linkPart; if (linkTarget) { links.add(linkTarget); } } } } else { // Normal link format [[link]] oder [[link#Abschnitt]] // Alias entfernen (|), Abschnitt (#) behalten für Edge-Block const linkTarget = target.split("|")[0]?.trim() || target; if (linkTarget) { links.add(linkTarget); } } } } } return Array.from(links).sort(); // Deterministic ordering }