Enhance section creation logic and edge handling in notes
- Introduced new edge properties in the CreateSectionResult interface to support forward and inverse edge types, improving edge management. - Updated the createSectionInNote function to handle section content and edge insertion more effectively, ensuring accurate edge linking between notes. - Implemented logic to resolve target file paths more robustly, accommodating variations in file extensions. - Enhanced edge insertion functionality to dynamically add edges to the appropriate sections, improving the overall linking process in notes.
This commit is contained in:
parent
0a346d3886
commit
523a850ebb
99
src/workbench/createSectionAction.test.ts
Normal file
99
src/workbench/createSectionAction.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { removeBlanksAndInsert } from "./edgeInsertHelper";
|
||||
|
||||
describe("removeBlanksAndInsert", () => {
|
||||
it("removes blank line at insert position and inserts edge lines", () => {
|
||||
const lines = [
|
||||
"> [!abstract]- 🕸️ Semantic Mapping",
|
||||
">",
|
||||
">> [!edge] guides",
|
||||
">> [[SomeNote#^next]]",
|
||||
"",
|
||||
"",
|
||||
];
|
||||
const edgeLines = [">", ">> [!edge] impacted_by", ">> [[Other#^per]]"];
|
||||
removeBlanksAndInsert(lines, 4, 6, edgeLines);
|
||||
expect(lines).toEqual([
|
||||
"> [!abstract]- 🕸️ Semantic Mapping",
|
||||
">",
|
||||
">> [!edge] guides",
|
||||
">> [[SomeNote#^next]]",
|
||||
">",
|
||||
">> [!edge] impacted_by",
|
||||
">> [[Other#^per]]",
|
||||
]);
|
||||
expect(lines.some((l) => l.trim() === "" && !l.startsWith(">"))).toBe(false);
|
||||
});
|
||||
|
||||
it("removes multiple consecutive blank lines at insert position", () => {
|
||||
const lines = [
|
||||
">> [!edge] related_to",
|
||||
">> [[#^impact]]",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
];
|
||||
const edgeLines = [">", ">> [!edge] foundation_for", ">> [[#^next]]"];
|
||||
removeBlanksAndInsert(lines, 2, 5, edgeLines);
|
||||
expect(lines).toEqual([
|
||||
">> [!edge] related_to",
|
||||
">> [[#^impact]]",
|
||||
">",
|
||||
">> [!edge] foundation_for",
|
||||
">> [[#^next]]",
|
||||
]);
|
||||
});
|
||||
|
||||
it("inserts at position when no blank lines at insert position", () => {
|
||||
const lines = [
|
||||
">> [!edge] beherrscht_von",
|
||||
">> [[#^situation]]",
|
||||
">",
|
||||
">> [!edge] related_to",
|
||||
">> [[#^impact]]",
|
||||
];
|
||||
const edgeLines = [">", ">> [!edge] foundation_for", ">> [[#^next]]"];
|
||||
removeBlanksAndInsert(lines, 3, 5, edgeLines);
|
||||
expect(lines).toEqual([
|
||||
">> [!edge] beherrscht_von",
|
||||
">> [[#^situation]]",
|
||||
">",
|
||||
">",
|
||||
">> [!edge] foundation_for",
|
||||
">> [[#^next]]",
|
||||
">> [!edge] related_to",
|
||||
">> [[#^impact]]",
|
||||
]);
|
||||
});
|
||||
|
||||
it("result contains no empty string lines within block (only lines starting with >)", () => {
|
||||
const lines = [
|
||||
"> [!abstract]- Title",
|
||||
">> [!edge] type_a",
|
||||
">> [[A]]",
|
||||
"",
|
||||
];
|
||||
const edgeLines = [">", ">> [!edge] type_b", ">> [[B]]"];
|
||||
removeBlanksAndInsert(lines, 3, 4, edgeLines);
|
||||
const inBlock = lines.slice(0, 7);
|
||||
const hasBlank = inBlock.some((l) => l === "" || (l.trim() === "" && !l.startsWith(">")));
|
||||
expect(hasBlank).toBe(false);
|
||||
});
|
||||
|
||||
it("insert after last content line: blank at insert position is removed, new edge directly after last >> [[...]]", () => {
|
||||
const lines = [
|
||||
"> [!abstract]- 🕸️ Semantic Mapping",
|
||||
">",
|
||||
">> [!edge] guides",
|
||||
">> [[Geburt unserer Kinder Rouven und Rohan#Nächster Schritt ^next]]",
|
||||
"",
|
||||
];
|
||||
const edgeLines = [">", ">> [!edge] impacted_by", ">> [[Geburt unserer Kinder Rouven und Rohan#^per]]"];
|
||||
removeBlanksAndInsert(lines, 4, 5, edgeLines);
|
||||
expect(lines[3]).toBe(">> [[Geburt unserer Kinder Rouven und Rohan#Nächster Schritt ^next]]");
|
||||
expect(lines[4]).toBe(">");
|
||||
expect(lines[5]).toBe(">> [!edge] impacted_by");
|
||||
expect(lines[6]).toBe(">> [[Geburt unserer Kinder Rouven und Rohan#^per]]");
|
||||
expect(lines).not.toContain("");
|
||||
});
|
||||
});
|
||||
|
|
@ -12,6 +12,8 @@ import type { ChainTemplate, ChainRolesConfig, ChainTemplatesConfig } from "../d
|
|||
import type { TemplateMatch } from "../analysis/chainInspector";
|
||||
import { EntityPickerModal } from "../ui/EntityPickerModal";
|
||||
import { NoteIndex } from "../entityPicker/noteIndex";
|
||||
import { detectZoneFromContent } from "./zoneDetector";
|
||||
import { removeBlanksAndInsert } from "./edgeInsertHelper";
|
||||
|
||||
/** Common section/node types for dropdown when slot has no allowed_node_types. */
|
||||
const FALLBACK_SECTION_TYPES = [
|
||||
|
|
@ -31,10 +33,15 @@ export interface CreateSectionResult {
|
|||
blockId: string | null;
|
||||
sectionType: string | null;
|
||||
sectionBody: string;
|
||||
/** Edges in der neuen Sektion (mit gewähltem edgeType) */
|
||||
initialEdges: Array<{
|
||||
edgeType: string;
|
||||
targetNote: string;
|
||||
targetHeading: string | null;
|
||||
/** Kanonischer Vorwärts-Typ (für Gegenkante in der anderen Note) */
|
||||
forwardEdgeType: string;
|
||||
/** true = diese Kante ist Rückwärtskante; Gegenkante (Vorwärts) in targetNote einfügen */
|
||||
isInverse: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +83,6 @@ export async function createSectionInNote(
|
|||
edgeVocabulary
|
||||
);
|
||||
|
||||
// Chain notes: unique file paths from slot assignments
|
||||
const chainNotePaths = getChainNotePaths(match);
|
||||
const defaultSectionType = getDefaultSectionType(template, todo);
|
||||
const sectionTypeOptions = getSectionTypeOptions(todo, template);
|
||||
|
|
@ -109,6 +115,12 @@ export async function createSectionInNote(
|
|||
sectionContent += `> [!section] ${result.sectionType}\n\n`;
|
||||
}
|
||||
|
||||
// Zuerst Fließtext der Sektion
|
||||
if (result.sectionBody.trim()) {
|
||||
sectionContent += result.sectionBody.trim() + "\n\n";
|
||||
}
|
||||
|
||||
// Abstract-Block mit Kanten am Ende der Sektion
|
||||
if (result.initialEdges.length > 0) {
|
||||
const wrapperType = settings.mappingWrapperCalloutType || "abstract";
|
||||
const wrapperTitle = settings.mappingWrapperTitle || "";
|
||||
|
|
@ -127,12 +139,13 @@ export async function createSectionInNote(
|
|||
sectionContent += "\n";
|
||||
}
|
||||
|
||||
if (result.sectionBody.trim()) {
|
||||
sectionContent += result.sectionBody.trim() + "\n\n";
|
||||
}
|
||||
|
||||
// Resolve target file
|
||||
const targetFile = app.vault.getAbstractFileByPath(result.targetFilePath);
|
||||
// Resolve target file (modal may store path with or without .md)
|
||||
const targetPathNorm = result.targetFilePath.endsWith(".md")
|
||||
? result.targetFilePath
|
||||
: `${result.targetFilePath}.md`;
|
||||
const targetFile =
|
||||
app.vault.getAbstractFileByPath(result.targetFilePath) ??
|
||||
app.vault.getAbstractFileByPath(targetPathNorm);
|
||||
if (!targetFile || !(targetFile instanceof TFile)) {
|
||||
new Notice("Zieldatei nicht gefunden");
|
||||
return;
|
||||
|
|
@ -144,8 +157,39 @@ export async function createSectionInNote(
|
|||
|
||||
await app.vault.modify(targetFile, content + sectionToInsert);
|
||||
|
||||
// Die Rückwärtskanten stehen in der neuen Sektion (initialEdges sind bereits Inverse-Typen und zeigen auf Quell-Notes).
|
||||
// Kein separater Eintrag in anderen Notes nötig.
|
||||
// Gegenkanten in den anderen Notes einfügen (Vorwärts- bzw. Rückwärtskante)
|
||||
const sourcePath = result.targetFilePath.replace(/\.md$/, "");
|
||||
const sourceFragment =
|
||||
result.blockId && result.heading
|
||||
? `${result.heading} ^${result.blockId}`
|
||||
: result.blockId
|
||||
? `^${result.blockId}`
|
||||
: result.heading ?? "";
|
||||
const sourceLink = sourceFragment ? `${sourcePath}#${sourceFragment}` : sourcePath;
|
||||
|
||||
for (const edge of result.initialEdges) {
|
||||
const otherPathRaw = edge.targetNote.replace(/\.md$/, "");
|
||||
const otherFilePath = `${otherPathRaw}.md`;
|
||||
let otherFile =
|
||||
app.vault.getAbstractFileByPath(otherFilePath) ??
|
||||
app.metadataCache.getFirstLinkpathDest(otherPathRaw, targetFile.path);
|
||||
if (!otherFile || !(otherFile instanceof TFile)) {
|
||||
if (debugLogging) {
|
||||
console.warn("[createSectionInNote] Other note not found for edge, skipping:", otherPathRaw, "resolved from", targetFile.path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const edgeTypeInOther = edge.isInverse
|
||||
? edge.forwardEdgeType
|
||||
: (vocabulary.getInverse(vocabulary.getCanonical(edge.edgeType) ?? edge.forwardEdgeType) ?? "related_to");
|
||||
|
||||
if (edge.targetHeading) {
|
||||
await insertEdgeIntoSection(app, otherFile, edge.targetHeading, edgeTypeInOther, sourceLink, settings, debugLogging);
|
||||
} else {
|
||||
await insertEdgeIntoFile(app, otherFile, edgeTypeInOther, sourceLink, settings, debugLogging);
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(`Sektion "${result.heading}" in ${targetFile.basename} erstellt`);
|
||||
|
||||
|
|
@ -154,6 +198,221 @@ export async function createSectionInNote(
|
|||
}
|
||||
}
|
||||
|
||||
/** Trennzeile zwischen Edge-Blöcken im Abstract: genau eine Zeile mit nur ">" (keine echte Leerzeile). */
|
||||
const EDGE_GROUP_SEPARATOR_LINE = ">";
|
||||
|
||||
/** Fügt eine Kante in die Note-Verbindungen-Zone einer Datei ein (ohne Editor). */
|
||||
async function insertEdgeIntoFile(
|
||||
app: App,
|
||||
file: TFile,
|
||||
edgeType: string,
|
||||
targetLink: string,
|
||||
settings: MindnetSettings,
|
||||
debugLogging?: boolean
|
||||
): Promise<void> {
|
||||
const wrapperCalloutType = settings.mappingWrapperCalloutType || "abstract";
|
||||
const wrapperTitle = settings.mappingWrapperTitle || "🕸️ Semantic Mapping";
|
||||
const foldMarker = settings.mappingWrapperFolded ? "-" : "+";
|
||||
|
||||
const content = await app.vault.read(file);
|
||||
const zone = detectZoneFromContent(content, "note_links");
|
||||
const newEdgeLines = `>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
|
||||
|
||||
if (!zone.exists) {
|
||||
const newZone = `\n\n## Note-Verbindungen\n\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n${newEdgeLines}`;
|
||||
await app.vault.modify(file, content + newZone);
|
||||
if (debugLogging) console.log("[createSectionInNote] Edge: created Note-Verbindungen in", file.path);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = content.split(/\r?\n/);
|
||||
const zoneContent = zone.content;
|
||||
const hasWrapper =
|
||||
zoneContent.includes(`> [!${wrapperCalloutType}]`) && zoneContent.includes(wrapperTitle);
|
||||
|
||||
let insertLine = zone.endLine - 1;
|
||||
const edgeLinesToInsert = [
|
||||
EDGE_GROUP_SEPARATOR_LINE,
|
||||
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
|
||||
];
|
||||
if (hasWrapper) {
|
||||
const wrapperEnd = findWrapperBlockEnd(lines, zone.startLine, wrapperCalloutType, wrapperTitle);
|
||||
if (wrapperEnd != null) {
|
||||
const afterSameType = findInsertLineAfterSameEdgeType(lines, zone.startLine, wrapperEnd, edgeType);
|
||||
const lastContentLine = findLastContentLineInWrapper(lines, zone.startLine, wrapperEnd);
|
||||
insertLine = afterSameType ?? (lastContentLine + 1);
|
||||
removeBlanksAndInsert(lines, insertLine, wrapperEnd, edgeLinesToInsert);
|
||||
} else {
|
||||
lines.splice(insertLine, 0, ...edgeLinesToInsert);
|
||||
}
|
||||
} else {
|
||||
const wrapper = `\n> [!${wrapperCalloutType}]${foldMarker} ${wrapperTitle}\n${newEdgeLines.trimEnd()}`;
|
||||
const wrapperLines = wrapper.trimStart().split("\n").filter((line) => line.trim().length > 0);
|
||||
lines.splice(insertLine, 0, ...wrapperLines);
|
||||
await app.vault.modify(file, lines.join("\n"));
|
||||
if (debugLogging) console.log("[createSectionInNote] Edge: added wrapper + edge in", file.path);
|
||||
return;
|
||||
}
|
||||
|
||||
await app.vault.modify(file, lines.join("\n"));
|
||||
if (debugLogging) console.log("[createSectionInNote] Edge: inserted into", file.path);
|
||||
}
|
||||
|
||||
/** Findet die Zeilen einer Sektion anhand Überschrift/Block-ID (targetHeading z. B. "Reflexion ^learning"). */
|
||||
function findSectionBounds(lines: string[], targetHeading: string): { startLine: number; endLine: number } | null {
|
||||
const targetNorm = targetHeading.trim();
|
||||
const targetBlockId = targetNorm.includes(" ^") ? targetNorm.split(" ^").pop()?.trim() : null;
|
||||
const targetHeadingOnly = targetBlockId ? targetNorm.replace(/\s*\^\s*\S+$/, "").trim() : targetNorm;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line?.startsWith("## ")) continue;
|
||||
const afterHash = line.replace(/^##\s+/, "").trim();
|
||||
const matchesHeading = afterHash === targetNorm || afterHash === targetHeadingOnly || afterHash.includes(targetHeadingOnly);
|
||||
const matchesBlockId = !targetBlockId || afterHash.includes(`^${targetBlockId}`);
|
||||
if (matchesHeading || matchesBlockId) {
|
||||
let endLine = lines.length;
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (lines[j]?.match(/^##\s+/)) {
|
||||
endLine = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { startLine: i, endLine };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fügt die Rückwärtskante in die angegebene Sektion ein (in deren Abstract-Block am Sektionsende). */
|
||||
async function insertEdgeIntoSection(
|
||||
app: App,
|
||||
file: TFile,
|
||||
targetHeading: string,
|
||||
edgeType: string,
|
||||
targetLink: string,
|
||||
settings: MindnetSettings,
|
||||
debugLogging?: boolean
|
||||
): Promise<void> {
|
||||
const wrapperCalloutType = settings.mappingWrapperCalloutType || "abstract";
|
||||
const wrapperTitle = settings.mappingWrapperTitle || "🕸️ Semantic Mapping";
|
||||
const foldMarker = settings.mappingWrapperFolded ? "-" : "+";
|
||||
|
||||
const content = await app.vault.read(file);
|
||||
const lines = content.split(/\r?\n/);
|
||||
const bounds = findSectionBounds(lines, targetHeading);
|
||||
if (!bounds) {
|
||||
if (debugLogging) console.warn("[createSectionInNote] Section not found for heading:", targetHeading, "in", file.path);
|
||||
return;
|
||||
}
|
||||
|
||||
const newEdgeLines = `>> [!edge] ${edgeType}\n>> [[${targetLink}]]\n`;
|
||||
const sectionStart = bounds.startLine;
|
||||
const sectionEnd = bounds.endLine;
|
||||
|
||||
const wrapperEnd = findWrapperBlockEnd(lines, sectionStart, wrapperCalloutType, wrapperTitle, sectionEnd);
|
||||
if (wrapperEnd != null) {
|
||||
const afterSameType = findInsertLineAfterSameEdgeType(lines, sectionStart, wrapperEnd, edgeType);
|
||||
const lastContentLine = findLastContentLineInWrapper(lines, sectionStart, wrapperEnd);
|
||||
const insertLine = afterSameType ?? (lastContentLine + 1);
|
||||
const edgeLines = [
|
||||
EDGE_GROUP_SEPARATOR_LINE,
|
||||
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
|
||||
];
|
||||
removeBlanksAndInsert(lines, insertLine, wrapperEnd, edgeLines);
|
||||
} else {
|
||||
const wrapper = `\n> [!${wrapperCalloutType}]${foldMarker}${wrapperTitle ? ` ${wrapperTitle}` : ""}\n${newEdgeLines.trimEnd()}`;
|
||||
const insertAt = sectionEnd;
|
||||
const wrapperLines = wrapper.trimStart().split("\n").filter((line) => line.trim().length > 0);
|
||||
lines.splice(insertAt, 0, "", ...wrapperLines);
|
||||
}
|
||||
|
||||
await app.vault.modify(file, lines.join("\n"));
|
||||
if (debugLogging) console.log("[createSectionInNote] Edge: inserted into section", targetHeading, "in", file.path);
|
||||
}
|
||||
|
||||
/** Findet die Einfügezeile, damit die neue Kante direkt nach der letzten Kante desselben Typs steht. */
|
||||
function findInsertLineAfterSameEdgeType(
|
||||
lines: string[],
|
||||
wrapperStart: number,
|
||||
wrapperEnd: number,
|
||||
edgeType: string
|
||||
): number | null {
|
||||
const edgeDeclRegex = /^\s*>>\s*\[!edge\]\s+(.+?)\s*$/;
|
||||
let lastInsertLine: number | null = null;
|
||||
|
||||
for (let i = wrapperStart; i < wrapperEnd; i++) {
|
||||
const line = lines[i];
|
||||
if (!line?.startsWith(">")) continue;
|
||||
const m = line.match(edgeDeclRegex);
|
||||
if (m?.[1]?.trim() === edgeType) {
|
||||
const nextLine = lines[i + 1];
|
||||
const hasLink = nextLine?.trim().match(/^>>\s*\[\[.+\]\]\s*$/);
|
||||
lastInsertLine = hasLink ? i + 2 : i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return lastInsertLine;
|
||||
}
|
||||
|
||||
function findWrapperBlockEnd(
|
||||
lines: string[],
|
||||
startLine: number,
|
||||
calloutType: string,
|
||||
title: string,
|
||||
maxLine?: number
|
||||
): number | null {
|
||||
const end = maxLine ?? lines.length;
|
||||
const calloutTypeLower = calloutType.toLowerCase();
|
||||
const titleLower = title.toLowerCase();
|
||||
const calloutHeaderRegex = new RegExp(
|
||||
`^\\s*>\\s*\\[!${calloutTypeLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\s*[+-]?\\s*(.+)$`,
|
||||
"i"
|
||||
);
|
||||
|
||||
let inWrapper = false;
|
||||
let quoteLevel = 0;
|
||||
|
||||
for (let i = startLine; i < end; i++) {
|
||||
const line = lines[i];
|
||||
if (line === undefined) continue;
|
||||
|
||||
const trimmed = line.trim();
|
||||
const match = line.match(calloutHeaderRegex);
|
||||
|
||||
if (match && match[1]) {
|
||||
const headerTitle = match[1].trim().toLowerCase();
|
||||
if (headerTitle.includes(titleLower) || titleLower.includes(headerTitle)) {
|
||||
inWrapper = true;
|
||||
quoteLevel = (line.match(/^\s*(>+)/)?.[1]?.length ?? 0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (inWrapper) {
|
||||
if (trimmed.match(/^\^map-/)) return i + 1;
|
||||
if (trimmed === "" || !line.startsWith(">")) return i;
|
||||
const currentQuoteLevel = (line.match(/^\s*(>+)/)?.[1]?.length ?? 0);
|
||||
if (currentQuoteLevel < quoteLevel) return i;
|
||||
if (trimmed.match(/^#{1,6}\s+/)) return i;
|
||||
}
|
||||
}
|
||||
|
||||
return inWrapper ? end : null;
|
||||
}
|
||||
|
||||
/** Index der letzten Zeile im Wrapper, die mit ">" beginnt (echter Blockinhalt). */
|
||||
function findLastContentLineInWrapper(
|
||||
lines: string[],
|
||||
wrapperStart: number,
|
||||
wrapperEnd: number
|
||||
): number {
|
||||
for (let i = wrapperEnd - 1; i >= wrapperStart; i--) {
|
||||
if (lines[i]?.startsWith(">")) return i;
|
||||
}
|
||||
return wrapperStart;
|
||||
}
|
||||
|
||||
function getChainNotePaths(match: TemplateMatch): string[] {
|
||||
const paths = new Set<string>();
|
||||
for (const a of Object.values(match.slotAssignments)) {
|
||||
|
|
@ -188,15 +447,21 @@ function getSectionTypeOptions(todo: MissingSlotTodo, template: ChainTemplate):
|
|||
return [...FALLBACK_SECTION_TYPES];
|
||||
}
|
||||
|
||||
async function showCreateSectionModal(
|
||||
app: App,
|
||||
todo: MissingSlotTodo,
|
||||
requiredEdges: Array<{
|
||||
export interface RequiredEdgeForSlot {
|
||||
edgeType: string;
|
||||
targetNote: string;
|
||||
targetHeading: string | null;
|
||||
suggestedAlias?: string;
|
||||
}>,
|
||||
forwardEdgeType: string;
|
||||
isInverse: boolean;
|
||||
/** Optionen für Dropdown: kanonischer Typ + Aliase (erster = Vorschlag) */
|
||||
allowedTypes: string[];
|
||||
}
|
||||
|
||||
async function showCreateSectionModal(
|
||||
app: App,
|
||||
todo: MissingSlotTodo,
|
||||
requiredEdges: RequiredEdgeForSlot[],
|
||||
chainNotePaths: string[],
|
||||
activeFilePath: string | null,
|
||||
defaultSectionType: string,
|
||||
|
|
@ -219,6 +484,8 @@ async function showCreateSectionModal(
|
|||
? activeFilePath
|
||||
: chainNotePaths[0] ?? activeFilePath ?? "") as string;
|
||||
initialEdges: CreateSectionResult["initialEdges"] = [];
|
||||
/** Gewählter Edge-Typ pro Index (Reihenfolge wie requiredEdges) */
|
||||
chosenEdgeTypes: string[] = [];
|
||||
|
||||
constructor() {
|
||||
super(app);
|
||||
|
|
@ -292,24 +559,30 @@ async function showCreateSectionModal(
|
|||
});
|
||||
});
|
||||
|
||||
// Initial edges (read-only info, all applied)
|
||||
// Verbindungen: pro Edge Typ wählbar (Dropdown)
|
||||
if (requiredEdges.length > 0) {
|
||||
contentEl.createEl("h3", { text: "Verbindungen (werden angelegt)" });
|
||||
const ul = contentEl.createEl("ul", { cls: "create-section-edges-list" });
|
||||
for (const edge of requiredEdges) {
|
||||
const li = ul.createEl("li");
|
||||
li.textContent = `${edge.suggestedAlias || edge.edgeType} → ${edge.targetNote}${edge.targetHeading ? `#${edge.targetHeading}` : ""}`;
|
||||
this.chosenEdgeTypes = requiredEdges.map((e) => e.suggestedAlias ?? e.edgeType);
|
||||
requiredEdges.forEach((edge, index) => {
|
||||
const targetLabel = `${edge.targetNote}${edge.targetHeading ? `#${edge.targetHeading}` : ""}`;
|
||||
new Setting(contentEl)
|
||||
.setName(`Kante nach ${targetLabel}`)
|
||||
.setDesc("Edge-Typ (kanonisch oder Alias)")
|
||||
.addDropdown((dropdown) => {
|
||||
for (const opt of edge.allowedTypes) {
|
||||
dropdown.addOption(opt, opt);
|
||||
}
|
||||
this.initialEdges = requiredEdges.map((e) => ({
|
||||
edgeType: e.suggestedAlias || e.edgeType,
|
||||
targetNote: e.targetNote,
|
||||
targetHeading: e.targetHeading,
|
||||
}));
|
||||
dropdown.setValue(this.chosenEdgeTypes[index] ?? edge.edgeType);
|
||||
dropdown.onChange((value) => {
|
||||
this.chosenEdgeTypes[index] = value;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Section body text
|
||||
const bodyDesc = contentEl.createEl("div", { cls: "setting-item-description" });
|
||||
bodyDesc.setText("Inhalt der Sektion (optional). Wird unter den Verbindungen eingefügt.");
|
||||
bodyDesc.setText("Inhalt der Sektion (optional). Der Block mit Verbindungen steht am Ende der Sektion.");
|
||||
const bodyContainer = contentEl.createDiv({ cls: "setting-item" });
|
||||
const control = bodyContainer.createDiv({ cls: "setting-item-control" });
|
||||
const textarea = control.createEl("textarea", {
|
||||
|
|
@ -339,6 +612,13 @@ async function showCreateSectionModal(
|
|||
new Notice("Bitte eine Ziel-Note wählen");
|
||||
return;
|
||||
}
|
||||
this.initialEdges = requiredEdges.map((e, i) => ({
|
||||
edgeType: this.chosenEdgeTypes[i] ?? e.suggestedAlias ?? e.edgeType,
|
||||
targetNote: e.targetNote,
|
||||
targetHeading: e.targetHeading,
|
||||
forwardEdgeType: e.forwardEdgeType,
|
||||
isInverse: e.isInverse,
|
||||
}));
|
||||
this.result = {
|
||||
targetFilePath: this.targetFilePath,
|
||||
heading: this.heading.trim(),
|
||||
|
|
@ -388,18 +668,8 @@ function getRequiredEdgesForSlot(
|
|||
chainRoles: ChainRolesConfig | null,
|
||||
vocabulary: Vocabulary,
|
||||
edgeVocabulary: EdgeVocabulary | null
|
||||
): Array<{
|
||||
edgeType: string;
|
||||
targetNote: string;
|
||||
targetHeading: string | null;
|
||||
suggestedAlias?: string;
|
||||
}> {
|
||||
const requiredEdges: Array<{
|
||||
edgeType: string;
|
||||
targetNote: string;
|
||||
targetHeading: string | null;
|
||||
suggestedAlias?: string;
|
||||
}> = [];
|
||||
): RequiredEdgeForSlot[] {
|
||||
const requiredEdges: RequiredEdgeForSlot[] = [];
|
||||
|
||||
const normalizedLinks = template.links || [];
|
||||
for (const link of normalizedLinks) {
|
||||
|
|
@ -414,7 +684,17 @@ function getRequiredEdgesForSlot(
|
|||
}
|
||||
const forwardEdgeType = suggestedEdgeTypes[0] || "related_to";
|
||||
|
||||
// Links, die ZU unserem Slot zeigen: In der neuen Sektion Rückwärtskante (Inverse) zur Quell-Note
|
||||
const buildAllowedTypes = (canonical: string): string[] => {
|
||||
const list: string[] = [canonical];
|
||||
if (edgeVocabulary) {
|
||||
const entry = edgeVocabulary.byCanonical.get(canonical);
|
||||
if (entry?.aliases?.length) {
|
||||
list.push(...entry.aliases);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
if (link.to === slotId) {
|
||||
const sourceAssignment = match.slotAssignments[link.from];
|
||||
if (!sourceAssignment) continue;
|
||||
|
|
@ -422,6 +702,7 @@ function getRequiredEdgesForSlot(
|
|||
const canonical = vocabulary.getCanonical(forwardEdgeType);
|
||||
const inverseType = canonical ? vocabulary.getInverse(canonical) : null;
|
||||
const edgeTypeForSection = inverseType ?? forwardEdgeType;
|
||||
const allowedTypes = inverseType ? buildAllowedTypes(inverseType) : [edgeTypeForSection];
|
||||
let suggestedAlias: string | undefined;
|
||||
if (edgeVocabulary && edgeTypeForSection) {
|
||||
const entry = edgeVocabulary.byCanonical.get(edgeTypeForSection);
|
||||
|
|
@ -434,14 +715,17 @@ function getRequiredEdgesForSlot(
|
|||
targetNote: sourceAssignment.file.replace(/\.md$/, ""),
|
||||
targetHeading: sourceAssignment.heading ?? null,
|
||||
suggestedAlias,
|
||||
forwardEdgeType,
|
||||
isInverse: true,
|
||||
allowedTypes: allowedTypes.length ? allowedTypes : [edgeTypeForSection],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Links, die VON unserem Slot ausgehen: In der neuen Sektion Vorwärtskante zur Ziel-Note
|
||||
if (link.from === slotId) {
|
||||
const targetAssignment = match.slotAssignments[link.to];
|
||||
if (!targetAssignment) continue;
|
||||
const allowedTypes = buildAllowedTypes(forwardEdgeType);
|
||||
let suggestedAlias: string | undefined;
|
||||
if (edgeVocabulary) {
|
||||
const entry = edgeVocabulary.byCanonical.get(forwardEdgeType);
|
||||
|
|
@ -454,6 +738,9 @@ function getRequiredEdgesForSlot(
|
|||
targetNote: targetAssignment.file.replace(/\.md$/, ""),
|
||||
targetHeading: targetAssignment.heading ?? null,
|
||||
suggestedAlias,
|
||||
forwardEdgeType,
|
||||
isInverse: false,
|
||||
allowedTypes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
src/workbench/edgeInsertHelper.ts
Normal file
22
src/workbench/edgeInsertHelper.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Pure helper for inserting edge lines into a line array without introducing
|
||||
* blank lines inside abstract/callout blocks.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Removes blank lines at the insert position (within blockEnd) and splices in edgeLines.
|
||||
* Ensures no empty string or whitespace-only lines remain at the insert spot.
|
||||
*/
|
||||
export function removeBlanksAndInsert(
|
||||
lines: string[],
|
||||
insertLine: number,
|
||||
blockEnd: number,
|
||||
edgeLines: string[]
|
||||
): void {
|
||||
let end = blockEnd;
|
||||
while (insertLine < end && lines[insertLine]?.trim() === "") {
|
||||
lines.splice(insertLine, 1);
|
||||
end--;
|
||||
}
|
||||
lines.splice(insertLine, 0, ...edgeLines);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user