Enhance InterviewWizardModal and SectionEdgesOverviewModal for improved link handling
- Integrated LinkTargetPickerModal into InterviewWizardModal to allow users to select link targets dynamically, enhancing user experience during note linking. - Updated link handling logic to support full links and improved edge type resolution in SectionEdgesOverviewModal, ensuring accurate target type identification. - Refactored code to streamline the extraction of note links and their associated types, improving overall functionality and maintainability.
This commit is contained in:
parent
054cfcf82d
commit
c044d6e8db
136
src/interview/targetTypeResolver.ts
Normal file
136
src/interview/targetTypeResolver.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Ermittelt Zieltyp für Note-Links: Note-Type (ganze Note) oder Sektionstyp (bei Note#Abschnitt).
|
||||
* Wird in Kantenübersicht und Toolbar-Link-Auswahl genutzt.
|
||||
*/
|
||||
|
||||
import type { App } from "obsidian";
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
/** Link ohne Alias: "Note" oder "Note#Abschnitt" */
|
||||
export function parseNoteLink(linkTarget: string): { basename: string; heading: string | null } {
|
||||
const withoutAlias = linkTarget.split("|")[0]?.trim() || linkTarget;
|
||||
const sharp = withoutAlias.indexOf("#");
|
||||
if (sharp === -1) {
|
||||
return { basename: withoutAlias.trim(), heading: null };
|
||||
}
|
||||
const basename = withoutAlias.slice(0, sharp).trim();
|
||||
const heading = withoutAlias.slice(sharp + 1).trim() || null;
|
||||
return { basename, heading };
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest aus Dateiinhalt den Sektionstyp für eine Überschrift.
|
||||
* Erwartet Format: ## Überschrift (optional ^block-id), nächste Zeile > [!section] typ
|
||||
*/
|
||||
export function getSectionTypeForHeading(content: string, heading: string): string | null {
|
||||
const lines = content.split("\n");
|
||||
const headingNorm = heading.trim().toLowerCase();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line === undefined) continue;
|
||||
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s+\^[\w-]+)?\s*$/);
|
||||
if (match && match[2]) {
|
||||
const lineHeading = match[2].trim();
|
||||
if (lineHeading.toLowerCase() === headingNorm) {
|
||||
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
||||
const nextLine = lines[j];
|
||||
if (nextLine === undefined) continue;
|
||||
const sectionMatch = nextLine.match(/^\s*>\s*\[!section\]\s+(.+)\s*$/);
|
||||
if (sectionMatch && sectionMatch[1]) {
|
||||
return sectionMatch[1].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface ResolvedTargetType {
|
||||
targetType: string | null;
|
||||
displayLabel: string;
|
||||
basename: string;
|
||||
heading: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt Zieltyp für einen Note-Link (ganze Note → Note-Type, mit #Abschnitt → Sektionstyp).
|
||||
* @param app Obsidian App
|
||||
* @param linkTarget z.B. "Note" oder "Note#Abschnitt" (ohne Alias)
|
||||
* @param sourcePath Pfad der Quelldatei für getFirstLinkpathDest
|
||||
*/
|
||||
export async function resolveTargetTypeForNoteLink(
|
||||
app: App,
|
||||
linkTarget: string,
|
||||
sourcePath: string
|
||||
): Promise<ResolvedTargetType> {
|
||||
const { basename, heading } = parseNoteLink(linkTarget);
|
||||
const displayLabel = heading ? `${basename}#${heading}` : basename;
|
||||
|
||||
let targetType: string | null = null;
|
||||
try {
|
||||
const targetFile = app.metadataCache.getFirstLinkpathDest(basename, sourcePath);
|
||||
if (!targetFile || !(targetFile instanceof TFile)) {
|
||||
return { targetType: null, displayLabel, basename, heading };
|
||||
}
|
||||
|
||||
if (heading) {
|
||||
const content = await app.vault.read(targetFile);
|
||||
targetType = getSectionTypeForHeading(content, heading);
|
||||
}
|
||||
|
||||
if (targetType === null) {
|
||||
const cache = app.metadataCache.getFileCache(targetFile);
|
||||
if (cache?.frontmatter) {
|
||||
const t = cache.frontmatter.type ?? cache.frontmatter.noteType;
|
||||
targetType = t != null ? String(t) : null;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return { targetType, displayLabel, basename, heading };
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest aus einer Note alle Überschriften mit zugehörigem Sektionstyp (falls vorhanden).
|
||||
* Für Toolbar-Auswahl: "Ganze Note" oder "## Überschrift (typ)".
|
||||
*/
|
||||
export interface HeadingWithType {
|
||||
heading: string;
|
||||
sectionType: string | null;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export async function getHeadingsWithSectionTypes(
|
||||
app: App,
|
||||
file: TFile
|
||||
): Promise<HeadingWithType[]> {
|
||||
const content = await app.vault.read(file);
|
||||
const lines = content.split("\n");
|
||||
const result: HeadingWithType[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line === undefined) continue;
|
||||
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s+\^[\w-]+)?\s*$/);
|
||||
if (match && match[1] && match[2]) {
|
||||
const level = match[1].length;
|
||||
const heading = match[2].trim();
|
||||
let sectionType: string | null = null;
|
||||
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
||||
const nextLine = lines[j];
|
||||
if (nextLine === undefined) continue;
|
||||
const sectionMatch = nextLine.match(/^\s*>\s*\[!section\]\s+(.+)\s*$/);
|
||||
if (sectionMatch && sectionMatch[1]) {
|
||||
sectionType = sectionMatch[1].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
result.push({ heading, sectionType, level });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ import { buildSemanticMappings, type BuildResult } from "../mapping/semanticMapp
|
|||
import type { MindnetSettings } from "../settings";
|
||||
import { InlineEdgeTypeModal, type InlineEdgeTypeResult } from "./InlineEdgeTypeModal";
|
||||
import { SectionEdgesOverviewModal, type SectionEdgesOverviewResult } from "./SectionEdgesOverviewModal";
|
||||
import { LinkTargetPickerModal } from "./LinkTargetPickerModal";
|
||||
import { getSectionKeyForWizardContext } from "../interview/sectionKeyResolver";
|
||||
import type { PendingEdgeAssignment } from "../interview/wizardState";
|
||||
import { VocabularyLoader } from "../vocab/VocabularyLoader";
|
||||
|
|
@ -601,30 +602,35 @@ export class InterviewWizardModal extends Modal {
|
|||
app,
|
||||
this.noteIndex,
|
||||
async (result: EntityPickerResult) => {
|
||||
// Check if inline micro edging is enabled (also for toolbar)
|
||||
// Support: inline_micro, both (inline_micro + post_run)
|
||||
const edgingMode = this.profile.edging?.mode;
|
||||
const shouldRunInlineMicro =
|
||||
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
||||
this.settings?.inlineMicroEnabled !== false;
|
||||
|
||||
let linkText = `[[${result.basename}]]`;
|
||||
|
||||
if (shouldRunInlineMicro) {
|
||||
// Get current step for section key resolution
|
||||
const currentStep = getCurrentStep(this.state);
|
||||
if (currentStep) {
|
||||
console.log("[Mindnet] Starting inline micro edging from toolbar");
|
||||
const edgeType = await this.handleInlineMicroEdging(currentStep, result.basename, result.path);
|
||||
if (edgeType && typeof edgeType === "string") {
|
||||
// Use [[rel:type|link]] format
|
||||
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
||||
}
|
||||
|
||||
let linkTarget = result.basename;
|
||||
if (shouldRunInlineMicro && currentStep) {
|
||||
const file = this.app.vault.getAbstractFileByPath(result.path);
|
||||
if (file && file instanceof TFile) {
|
||||
const picker = new LinkTargetPickerModal(
|
||||
this.app,
|
||||
file,
|
||||
result.basename,
|
||||
this.file?.path
|
||||
);
|
||||
const pick = await picker.show();
|
||||
if (pick) linkTarget = pick.linkTarget;
|
||||
}
|
||||
}
|
||||
|
||||
let linkText = `[[${linkTarget}]]`;
|
||||
if (shouldRunInlineMicro && currentStep) {
|
||||
const edgeType = await this.handleInlineMicroEdging(currentStep, linkTarget, result.path);
|
||||
if (edgeType && typeof edgeType === "string") {
|
||||
linkText = `[[rel:${edgeType}|${linkTarget}]]`;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert link with rel: prefix if edge type was selected
|
||||
// Extract inner part (without [[ and ]])
|
||||
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||
}
|
||||
|
|
@ -633,7 +639,6 @@ export class InterviewWizardModal extends Modal {
|
|||
|
||||
blockIdModal.open();
|
||||
} else {
|
||||
// Keine Block-IDs vorhanden, öffne direkt Entity-Picker
|
||||
if (!this.noteIndex) {
|
||||
new Notice("Note index not available");
|
||||
return;
|
||||
|
|
@ -642,30 +647,35 @@ export class InterviewWizardModal extends Modal {
|
|||
app,
|
||||
this.noteIndex,
|
||||
async (result: EntityPickerResult) => {
|
||||
// Check if inline micro edging is enabled (also for toolbar)
|
||||
// Support: inline_micro, both (inline_micro + post_run)
|
||||
const edgingMode = this.profile.edging?.mode;
|
||||
const shouldRunInlineMicro =
|
||||
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
||||
this.settings?.inlineMicroEnabled !== false;
|
||||
|
||||
let linkText = `[[${result.basename}]]`;
|
||||
|
||||
if (shouldRunInlineMicro) {
|
||||
// Get current step for section key resolution
|
||||
const currentStep = getCurrentStep(this.state);
|
||||
if (currentStep) {
|
||||
console.log("[Mindnet] Starting inline micro edging from toolbar");
|
||||
const edgeType = await this.handleInlineMicroEdging(currentStep, result.basename, result.path);
|
||||
if (edgeType && typeof edgeType === "string") {
|
||||
// Use [[rel:type|link]] format
|
||||
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
||||
}
|
||||
|
||||
let linkTarget = result.basename;
|
||||
if (shouldRunInlineMicro && currentStep) {
|
||||
const file = this.app.vault.getAbstractFileByPath(result.path);
|
||||
if (file && file instanceof TFile) {
|
||||
const picker = new LinkTargetPickerModal(
|
||||
this.app,
|
||||
file,
|
||||
result.basename,
|
||||
this.file?.path
|
||||
);
|
||||
const pick = await picker.show();
|
||||
if (pick) linkTarget = pick.linkTarget;
|
||||
}
|
||||
}
|
||||
|
||||
let linkText = `[[${linkTarget}]]`;
|
||||
if (shouldRunInlineMicro && currentStep) {
|
||||
const edgeType = await this.handleInlineMicroEdging(currentStep, linkTarget, result.path);
|
||||
if (edgeType && typeof edgeType === "string") {
|
||||
linkText = `[[rel:${edgeType}|${linkTarget}]]`;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert link with rel: prefix if edge type was selected
|
||||
// Extract inner part (without [[ and ]])
|
||||
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||
}
|
||||
|
|
@ -2593,7 +2603,8 @@ export class InterviewWizardModal extends Modal {
|
|||
this.state.sectionSequence,
|
||||
this.vocabulary,
|
||||
graphSchema,
|
||||
this.state.collectedData // Übergebe gesammelte Daten für Note-Link-Extraktion
|
||||
this.state.collectedData,
|
||||
this.file?.path // Quelldatei für Link-Auflösung (Zieltyp ermitteln)
|
||||
);
|
||||
|
||||
const result = await overviewModal.show();
|
||||
|
|
@ -2906,27 +2917,26 @@ export class InterviewWizardModal extends Modal {
|
|||
sourceNoteId = extractFrontmatterId(sourceContent) || undefined;
|
||||
}
|
||||
|
||||
// Get target note ID and type
|
||||
// Zieltyp ermitteln: Note-Type (ganze Note) oder Sektionstyp (bei Note#Abschnitt)
|
||||
try {
|
||||
const { resolveTargetTypeForNoteLink } = await import("../interview/targetTypeResolver");
|
||||
const resolved = await resolveTargetTypeForNoteLink(
|
||||
this.app,
|
||||
linkBasename,
|
||||
this.file?.path || ""
|
||||
);
|
||||
targetType = resolved.targetType ?? undefined;
|
||||
const targetFile = this.app.vault.getAbstractFileByPath(linkPath);
|
||||
if (targetFile && targetFile instanceof TFile) {
|
||||
const targetContent = await this.app.vault.read(targetFile);
|
||||
targetNoteId = extractFrontmatterId(targetContent) || undefined;
|
||||
const targetFrontmatter = targetContent.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (targetFrontmatter && targetFrontmatter[1]) {
|
||||
const typeMatch = targetFrontmatter[1].match(/^type:\s*(.+)$/m);
|
||||
if (typeMatch && typeMatch[1]) {
|
||||
targetType = typeMatch[1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Target note might not exist yet, that's OK
|
||||
console.debug("[Mindnet] Could not read target note for inline micro:", e);
|
||||
console.debug("[Mindnet] Could not resolve target type for inline micro:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Show inline edge type modal
|
||||
// Show inline edge type modal (linkBasename kann "Note" oder "Note#Abschnitt" sein)
|
||||
const modal = new InlineEdgeTypeModal(
|
||||
this.app,
|
||||
linkBasename,
|
||||
|
|
|
|||
139
src/ui/LinkTargetPickerModal.ts
Normal file
139
src/ui/LinkTargetPickerModal.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* Modal zur Auswahl des Link-Ziels: Ganze Note oder ein Abschnitt (Überschrift inkl. Sektionstyp).
|
||||
* Wird in der Interview-Toolbar nach der Note-Auswahl angezeigt.
|
||||
* Speichert Link als [[Note]] oder [[Note#Abschnitt]].
|
||||
*/
|
||||
|
||||
import { App, Modal, TFile } from "obsidian";
|
||||
import {
|
||||
getHeadingsWithSectionTypes,
|
||||
resolveTargetTypeForNoteLink,
|
||||
type HeadingWithType,
|
||||
} from "../interview/targetTypeResolver";
|
||||
|
||||
export interface LinkTargetPick {
|
||||
linkTarget: string; // "Note" oder "Note#Abschnitt"
|
||||
targetType: string | null;
|
||||
}
|
||||
|
||||
export class LinkTargetPickerModal extends Modal {
|
||||
private file: TFile;
|
||||
private basename: string;
|
||||
private sourceFilePath: string;
|
||||
private resolve: ((pick: LinkTargetPick | null) => void) | null = null;
|
||||
|
||||
constructor(app: App, file: TFile, basename: string, sourceFilePath?: string) {
|
||||
super(app);
|
||||
this.file = file;
|
||||
this.basename = basename;
|
||||
this.sourceFilePath = sourceFilePath || "";
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("link-target-picker-modal");
|
||||
|
||||
contentEl.createEl("h2", { text: "Link-Ziel wählen" });
|
||||
contentEl.createEl("p", {
|
||||
text: `Zielnote: ${this.basename}. Ganze Note oder einen Abschnitt auswählen.`,
|
||||
cls: "link-target-picker-description",
|
||||
});
|
||||
|
||||
let noteType: string | null = null;
|
||||
try {
|
||||
const resolved = await resolveTargetTypeForNoteLink(
|
||||
this.app,
|
||||
this.basename,
|
||||
this.sourceFilePath
|
||||
);
|
||||
noteType = resolved.targetType;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const list = contentEl.createEl("div", { cls: "link-target-picker-list" });
|
||||
|
||||
// Option: Ganze Note
|
||||
const wholeNoteBtn = list.createEl("button", { cls: "link-target-option" });
|
||||
wholeNoteBtn.style.display = "block";
|
||||
wholeNoteBtn.style.width = "100%";
|
||||
wholeNoteBtn.style.textAlign = "left";
|
||||
wholeNoteBtn.style.padding = "0.5em 0.75em";
|
||||
wholeNoteBtn.style.marginBottom = "0.25em";
|
||||
wholeNoteBtn.style.cursor = "pointer";
|
||||
wholeNoteBtn.style.border = "1px solid var(--background-modifier-border)";
|
||||
wholeNoteBtn.style.borderRadius = "4px";
|
||||
const wholeLabel = wholeNoteBtn.createEl("span", { text: "Ganze Note" });
|
||||
wholeLabel.style.fontWeight = "bold";
|
||||
if (noteType) {
|
||||
wholeNoteBtn.createEl("span", { text: ` (Typ: ${noteType})`, cls: "target-type" }).style.color =
|
||||
"var(--text-muted)";
|
||||
}
|
||||
wholeNoteBtn.onclick = () => {
|
||||
if (this.resolve) {
|
||||
this.resolve({ linkTarget: this.basename, targetType: noteType });
|
||||
this.resolve = null;
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
|
||||
let headings: HeadingWithType[] = [];
|
||||
try {
|
||||
headings = await getHeadingsWithSectionTypes(this.app, this.file);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const h of headings) {
|
||||
const headingBtn = list.createEl("button", { cls: "link-target-option" });
|
||||
headingBtn.style.display = "block";
|
||||
headingBtn.style.width = "100%";
|
||||
headingBtn.style.textAlign = "left";
|
||||
headingBtn.style.padding = "0.5em 0.75em";
|
||||
headingBtn.style.marginBottom = "0.25em";
|
||||
headingBtn.style.cursor = "pointer";
|
||||
headingBtn.style.border = "1px solid var(--background-modifier-border)";
|
||||
headingBtn.style.borderRadius = "4px";
|
||||
const prefix = "#".repeat(h.level) + " ";
|
||||
headingBtn.createEl("span", { text: prefix + h.heading });
|
||||
if (h.sectionType) {
|
||||
headingBtn
|
||||
.createEl("span", { text: ` (Sektion: ${h.sectionType})`, cls: "target-type" })
|
||||
.style.color = "var(--text-muted)";
|
||||
}
|
||||
const linkTarget = `${this.basename}#${h.heading}`;
|
||||
headingBtn.onclick = () => {
|
||||
if (this.resolve) {
|
||||
this.resolve({ linkTarget, targetType: h.sectionType });
|
||||
this.resolve = null;
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
const cancelBtn = contentEl.createEl("button", { text: "Abbrechen", cls: "mod-secondary" });
|
||||
cancelBtn.style.marginTop = "1em";
|
||||
cancelBtn.onclick = () => {
|
||||
if (this.resolve) {
|
||||
this.resolve(null);
|
||||
this.resolve = null;
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
show(): Promise<LinkTargetPick | null> {
|
||||
return new Promise((resolve) => {
|
||||
this.resolve = resolve;
|
||||
this.open();
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
if (this.resolve) {
|
||||
this.resolve(null);
|
||||
this.resolve = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import type { EdgeVocabulary } from "../vocab/types";
|
|||
import type { GraphSchema } from "../mapping/graphSchema";
|
||||
import { EdgeTypeChooserModal, type EdgeTypeChoice } from "./EdgeTypeChooserModal";
|
||||
import { getHints } from "../mapping/graphSchema";
|
||||
import { resolveTargetTypeForNoteLink } from "../interview/targetTypeResolver";
|
||||
|
||||
export interface SectionEdge {
|
||||
fromSection: SectionInfo;
|
||||
|
|
@ -20,10 +21,10 @@ export interface SectionEdge {
|
|||
|
||||
export interface NoteEdge {
|
||||
fromSection: SectionInfo | null; // null = Note-Level
|
||||
toNote: string; // Note-Basename
|
||||
toNote: string; // Vollständiger Link: Note oder Note#Abschnitt (für Anzeige und Kantenzuordnung)
|
||||
edgeType: string; // Aktueller Edge-Type (kann geändert werden)
|
||||
suggestedType: string | null; // Vorgeschlagener Edge-Type aus graph_schema
|
||||
targetType: string | null; // Target-Note-Type (aus Frontmatter)
|
||||
targetType: string | null; // Zieltyp: Note-Type (ganze Note) oder Sektionstyp (bei Note#Abschnitt)
|
||||
}
|
||||
|
||||
export interface SectionEdgesOverviewResult {
|
||||
|
|
@ -37,6 +38,7 @@ export class SectionEdgesOverviewModal extends Modal {
|
|||
private vocabulary: EdgeVocabulary | null;
|
||||
private graphSchema: GraphSchema | null;
|
||||
private collectedData: Map<string, unknown>; // Gesammelte Daten aus dem Interview
|
||||
private activeFilePath: string; // Quelldatei für Link-Auflösung (getFirstLinkpathDest)
|
||||
private sectionEdges: SectionEdge[] = [];
|
||||
private noteEdges: NoteEdge[] = [];
|
||||
private result: SectionEdgesOverviewResult | null = null;
|
||||
|
|
@ -47,13 +49,15 @@ export class SectionEdgesOverviewModal extends Modal {
|
|||
sectionSequence: SectionInfo[],
|
||||
vocabulary: EdgeVocabulary | null,
|
||||
graphSchema: GraphSchema | null,
|
||||
collectedData?: Map<string, unknown>
|
||||
collectedData?: Map<string, unknown>,
|
||||
activeFilePath?: string
|
||||
) {
|
||||
super(app);
|
||||
this.sectionSequence = sectionSequence;
|
||||
this.vocabulary = vocabulary;
|
||||
this.graphSchema = graphSchema;
|
||||
this.collectedData = collectedData || new Map();
|
||||
this.activeFilePath = activeFilePath || "";
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
|
|
@ -468,13 +472,12 @@ export class SectionEdgesOverviewModal extends Modal {
|
|||
private async buildNoteEdgeList(): Promise<void> {
|
||||
this.noteEdges = [];
|
||||
|
||||
// Extrahiere alle Note-Links aus gesammelten Daten
|
||||
const noteLinksBySection = new Map<string | null, Set<string>>(); // blockId -> Set<noteBasename>
|
||||
// Extrahiere alle Note-Links aus gesammelten Daten (vollständiger Link: Note oder Note#Abschnitt)
|
||||
const noteLinksBySection = new Map<string | null, Set<string>>(); // blockId -> Set<fullLink>
|
||||
|
||||
for (const [key, value] of this.collectedData.entries()) {
|
||||
if (!value || typeof value !== "string") continue;
|
||||
|
||||
// Finde zugehörige Section für diesen Step
|
||||
let sectionInfo: SectionInfo | null = null;
|
||||
for (const section of this.sectionSequence) {
|
||||
if (section.stepKey === key) {
|
||||
|
|
@ -483,63 +486,46 @@ export class SectionEdgesOverviewModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
// Extrahiere Wikilinks aus dem Text
|
||||
const wikilinkRegex = /\[\[([^\]]+?)\]\]/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = wikilinkRegex.exec(value)) !== null) {
|
||||
if (match[1]) {
|
||||
const linkTarget = match[1].trim();
|
||||
if (!linkTarget) continue;
|
||||
|
||||
// Prüfe, ob es ein Block-ID-Link ist ([[#^...]])
|
||||
if (linkTarget.startsWith("#^")) {
|
||||
continue; // Block-ID-Links sind Section-Edges, keine Note-Edges
|
||||
}
|
||||
|
||||
// Normalisiere Link (entferne Alias und Heading)
|
||||
const normalized = linkTarget.split("|")[0]?.split("#")[0]?.trim() || linkTarget;
|
||||
if (!normalized) continue;
|
||||
if (linkTarget.startsWith("#^")) continue; // Block-ID-Links sind Section-Edges
|
||||
// Ziel-Link: Bei [[rel:type|Note#Abschnitt]] den Teil nach dem ersten | nehmen, sonst ganzer Link
|
||||
const fullLink = linkTarget.includes("|")
|
||||
? linkTarget.split("|").slice(1).join("|").trim() || linkTarget.trim()
|
||||
: linkTarget.split("|")[0]?.trim() || linkTarget;
|
||||
if (!fullLink || fullLink.startsWith("rel:")) continue; // rel: allein ist kein gültiger Ziel-Link
|
||||
|
||||
const blockId = sectionInfo?.blockId || null;
|
||||
if (!noteLinksBySection.has(blockId)) {
|
||||
noteLinksBySection.set(blockId, new Set());
|
||||
}
|
||||
noteLinksBySection.get(blockId)!.add(normalized);
|
||||
noteLinksBySection.get(blockId)!.add(fullLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Erstelle NoteEdge-Objekte
|
||||
for (const [blockId, noteLinks] of noteLinksBySection.entries()) {
|
||||
const sectionInfo = blockId
|
||||
? this.sectionSequence.find(s => s.blockId === blockId) || null
|
||||
: null;
|
||||
|
||||
// WP-26: Verwende effektiven Section-Type (mit Heading-Level-basierter Fallback-Logik)
|
||||
const sectionIndex = blockId ? this.sectionSequence.findIndex(s => s.blockId === blockId) : -1;
|
||||
const sourceType = sectionInfo && sectionIndex >= 0
|
||||
? this.getEffectiveSectionType(sectionInfo, sectionIndex)
|
||||
: null;
|
||||
|
||||
for (const noteBasename of noteLinks) {
|
||||
// Ermittle Target-Note-Type
|
||||
let targetType: string | null = null;
|
||||
try {
|
||||
const targetFile = this.app.metadataCache.getFirstLinkpathDest(noteBasename, "");
|
||||
if (targetFile) {
|
||||
const cache = this.app.metadataCache.getFileCache(targetFile);
|
||||
if (cache?.frontmatter) {
|
||||
targetType = cache.frontmatter.type || cache.frontmatter.noteType || null;
|
||||
if (targetType && typeof targetType !== "string") {
|
||||
targetType = String(targetType);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
for (const fullLink of noteLinks) {
|
||||
const resolved = await resolveTargetTypeForNoteLink(
|
||||
this.app,
|
||||
fullLink,
|
||||
this.activeFilePath
|
||||
);
|
||||
const targetType = resolved.targetType;
|
||||
|
||||
// Ermittle vorgeschlagenen Edge-Type aus graph_schema
|
||||
let suggestedType: string | null = null;
|
||||
if (this.graphSchema && sourceType && targetType) {
|
||||
const hints = getHints(this.graphSchema, sourceType, targetType);
|
||||
|
|
@ -547,15 +533,13 @@ export class SectionEdgesOverviewModal extends Modal {
|
|||
suggestedType = hints.typical[0] || null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: references
|
||||
if (!suggestedType) {
|
||||
suggestedType = "references";
|
||||
}
|
||||
|
||||
this.noteEdges.push({
|
||||
fromSection: sectionInfo,
|
||||
toNote: noteBasename,
|
||||
toNote: resolved.displayLabel,
|
||||
edgeType: suggestedType,
|
||||
suggestedType: suggestedType,
|
||||
targetType: targetType,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user