- Introduced a wide two-column layout for the Chain Workbench modal, improving user experience and accessibility. - Added new styles for workbench components, including headers, filters, and main containers, to enhance visual organization. - Updated chain templates to allow for multiple distinct matches per template, improving flexibility in template matching. - Enhanced documentation to clarify the new settings and commands related to the Chain Workbench and edge detection features. - Implemented logging for better tracking of missing configurations, ensuring users are informed about any loading issues.
150 lines
7.3 KiB
TypeScript
150 lines
7.3 KiB
TypeScript
/**
|
|
* 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<string, string>();
|
|
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;
|
|
}
|