/** * Pure logic: compute section content after inserting an edge into the abstract block. * Used by insertEdgeInSection and by tests. */ import { extractExistingMappings } from "../mapping/mappingExtractor"; import { buildMappingBlock, insertMappingBlock } from "../mapping/mappingBuilder"; export interface InsertEdgeOptions { wrapperCalloutType: string; wrapperTitle: string; wrapperFolded: boolean; } /** Separator between edge blocks inside abstract: exactly one line with exactly one ">". */ const EDGE_GROUP_SEPARATOR = "\n>\n"; /** * Compute new section content after inserting one edge (edgeType → targetLink). * - If section has an abstract block: insert into it (append to same edge-type group, or add new group separated by ">"). * - If no abstract block: append new mapping block at end. */ export function computeSectionContentAfterInsertEdge( sectionContent: string, edgeType: string, targetLink: string, options: InsertEdgeOptions ): string { const { wrapperCalloutType, wrapperTitle, wrapperFolded } = options; const mappingState = extractExistingMappings(sectionContent, wrapperCalloutType, wrapperTitle); const hasMappingBlock = mappingState.wrapperBlockStartLine !== null && mappingState.wrapperBlockEndLine !== null; if (hasMappingBlock && mappingState.wrapperBlockStartLine !== null && mappingState.wrapperBlockEndLine !== null) { const sectionLines = sectionContent.split(/\r?\n/); const wrapperStart = mappingState.wrapperBlockStartLine; const wrapperEnd = mappingState.wrapperBlockEndLine; const wrapperBlockContent = sectionLines.slice(wrapperStart, wrapperEnd).join("\n"); // Same edge type already exists → append target to that group // Match includes the separator ">\n" if present before next edge type const edgeTypeGroupRegex = new RegExp( `(>>\\s*\\[!edge\\]\\s+${edgeType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?)(?=\\n\\n|\\n>>\\s*\\[!edge\\]|\\n>\\s*\\^map-|$)` ); const edgeTypeMatch = wrapperBlockContent.match(edgeTypeGroupRegex); if (edgeTypeMatch && edgeTypeMatch[1]) { // Get the matched group const matchedGroup = edgeTypeMatch[1]; // Check what comes after the matched group in the original content const matchEndIndex = edgeTypeMatch.index! + edgeTypeMatch[0].length; const afterMatch = wrapperBlockContent.slice(matchEndIndex); // Check if there's a separator ">\n" immediately after the group const hasSeparatorAfter = afterMatch.match(/^\n>\s*\n/); // Also check if the group itself ends with a separator const trimmedGroup = matchedGroup.trimEnd(); const endsWithSeparator = trimmedGroup.endsWith("\n>"); const endsWithTarget = trimmedGroup.match(/>>\s*\[\[[^\]]+\]\]\s*$/); let updatedGroup: string; let updatedAfterMatch = afterMatch; if (endsWithSeparator || hasSeparatorAfter) { // Group has a separator (either at end or after), insert new target before separator if (endsWithSeparator) { // Separator is part of the group, remove it, add target, restore separator const separatorIndex = trimmedGroup.lastIndexOf("\n>"); let groupWithoutSeparator: string; if (separatorIndex >= 0) { // Remove separator and everything after it groupWithoutSeparator = trimmedGroup.slice(0, separatorIndex); // Remove trailing whitespace but preserve the newline structure // We want to keep exactly one newline before adding the new target groupWithoutSeparator = groupWithoutSeparator.replace(/[ \t]+$/, ""); // Remove trailing spaces/tabs only } else { groupWithoutSeparator = trimmedGroup.replace(/\n>\s*$/, "").trimEnd(); } // Add target directly followed by separator (>\n), no blank line in between // The separator should be "\n>\n" (one line with ">", then newline for next group) // But we need to ensure no extra blank line after the separator updatedGroup = groupWithoutSeparator + "\n>> [[" + targetLink + "]]\n>"; } else { // Separator is after the group, add target before it // Ensure no blank line between target and separator updatedGroup = trimmedGroup.trimEnd() + "\n>> [[" + targetLink + "]]"; // Remove any blank lines before separator in afterMatch updatedAfterMatch = afterMatch.replace(/^\s*\n+/, "\n"); } } else if (endsWithTarget) { // Group ends with a target, append new target updatedGroup = trimmedGroup + "\n>> [[" + targetLink + "]]\n"; } else { // Fallback: append with newline updatedGroup = trimmedGroup + "\n>> [[" + targetLink + "]]\n"; } // Ensure no blank line between separator and next edge type // The separator ends with ">\n", and the next edge type should come immediately after (no blank line) // Remove any blank lines (multiple newlines) between separator and next edge type let cleanedAfterMatch = updatedAfterMatch; // If afterMatch starts with newline(s) followed by >> [!edge], ensure only one newline if (cleanedAfterMatch.match(/^\n+>>\s*\[!edge\]/)) { // Replace multiple newlines with single newline cleanedAfterMatch = cleanedAfterMatch.replace(/^(\n+)(>>\s*\[!edge\])/, "\n$2"); } const updatedWrapperBlock = wrapperBlockContent.slice(0, edgeTypeMatch.index!) + updatedGroup + cleanedAfterMatch; const beforeWrapper = sectionLines.slice(0, wrapperStart).join("\n"); const afterWrapper = sectionLines.slice(wrapperEnd).join("\n"); return beforeWrapper + "\n" + updatedWrapperBlock + "\n" + afterWrapper; } // New edge type → add new group, separated by exactly one line with exactly one ">" const blockIdMatch = wrapperBlockContent.match(/\n>?\s*\^map-/); const insertPos = blockIdMatch ? blockIdMatch.index ?? wrapperBlockContent.length : wrapperBlockContent.length; const endsWithNewline = insertPos > 0 && wrapperBlockContent[insertPos - 1] === "\n"; const newEdgeGroup = (endsWithNewline ? ">\n" : EDGE_GROUP_SEPARATOR) + `>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`; const updatedWrapperBlock = wrapperBlockContent.slice(0, insertPos) + newEdgeGroup + wrapperBlockContent.slice(insertPos); const beforeWrapper = sectionLines.slice(0, wrapperStart).join("\n"); const afterWrapper = sectionLines.slice(wrapperEnd).join("\n"); return beforeWrapper + "\n" + updatedWrapperBlock + "\n" + afterWrapper; } // No abstract block: append new mapping block at end (use full targetLink for display) const targetLinkNorm = targetLink.split("|")[0]?.trim() || targetLink; const existingMappings = new Map(); existingMappings.set(targetLinkNorm, edgeType); const foldMarker = wrapperFolded ? "-" : "+"; const newMappingBlock = buildMappingBlock( [targetLinkNorm], existingMappings, { wrapperCalloutType, wrapperTitle, wrapperFolded, defaultEdgeType: "", assignUnmapped: "none", } ); if (newMappingBlock) { return insertMappingBlock(sectionContent, newMappingBlock); } const mappingBlock = `\n\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`; return sectionContent.trimEnd() + mappingBlock; }