MVP 1.1.0
Implement edge type selection enhancements in Mindnet plugin - Added a new command for changing edge types in Markdown files, integrating edge type selection into the editor context. - Enhanced the InterviewWizardModal to support edge type selection for textarea inputs, improving user interaction during interviews. - Updated the LinkPromptModal to display the full link text when available, preserving the rel:type|link format. - Introduced a button in the markdown toolbar for quick access to edge type selection, streamlining the user experience. - Improved error handling and logging for edge type changes, ensuring better feedback during operations.
This commit is contained in:
parent
7e256bd2e9
commit
9f051423ce
66
src/main.ts
66
src/main.ts
|
|
@ -43,6 +43,7 @@ import {
|
||||||
type PendingCreateHint,
|
type PendingCreateHint,
|
||||||
} from "./unresolvedLink/adoptHelpers";
|
} from "./unresolvedLink/adoptHelpers";
|
||||||
import { AdoptNoteModal } from "./ui/AdoptNoteModal";
|
import { AdoptNoteModal } from "./ui/AdoptNoteModal";
|
||||||
|
import { detectEdgeSelectorContext, changeEdgeTypeForLinks } from "./mapping/edgeTypeSelector";
|
||||||
|
|
||||||
export default class MindnetCausalAssistantPlugin extends Plugin {
|
export default class MindnetCausalAssistantPlugin extends Plugin {
|
||||||
settings: MindnetSettings;
|
settings: MindnetSettings;
|
||||||
|
|
@ -360,11 +361,53 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "mindnet-change-edge-type",
|
||||||
|
name: "Mindnet: Edge-Type ändern",
|
||||||
|
editorCallback: async (editor) => {
|
||||||
|
try {
|
||||||
|
console.log("[Main] Edge-Type ändern command called");
|
||||||
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
|
if (!activeFile || activeFile.extension !== "md") {
|
||||||
|
new Notice("Bitte öffnen Sie eine Markdown-Datei");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Main] Active file:", activeFile.path);
|
||||||
|
const content = editor.getValue();
|
||||||
|
console.log("[Main] Content length:", content.length);
|
||||||
|
const context = detectEdgeSelectorContext(editor, content);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
console.warn("[Main] Context could not be detected");
|
||||||
|
new Notice("Kontext konnte nicht erkannt werden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Main] Context detected:", context.mode);
|
||||||
|
await changeEdgeTypeForLinks(
|
||||||
|
this.app,
|
||||||
|
editor,
|
||||||
|
activeFile,
|
||||||
|
this.settings,
|
||||||
|
context,
|
||||||
|
{ ensureGraphSchemaLoaded: () => this.ensureGraphSchemaLoaded() }
|
||||||
|
);
|
||||||
|
console.log("[Main] changeEdgeTypeForLinks completed");
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
console.error("[Main] Error in edge-type command:", e);
|
||||||
|
new Notice(`Fehler beim Ändern des Edge-Types: ${msg}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "mindnet-build-semantic-mappings",
|
id: "mindnet-build-semantic-mappings",
|
||||||
name: "Mindnet: Build semantic mapping blocks (by section)",
|
name: "Mindnet: Build semantic mapping blocks (by section)",
|
||||||
callback: async () => {
|
editorCallback: async (editor) => {
|
||||||
try {
|
try {
|
||||||
|
console.log("[Main] Build semantic mapping blocks command called");
|
||||||
const activeFile = this.app.workspace.getActiveFile();
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
if (!activeFile) {
|
if (!activeFile) {
|
||||||
new Notice("No active file");
|
new Notice("No active file");
|
||||||
|
|
@ -376,6 +419,27 @@ export default class MindnetCausalAssistantPlugin extends Plugin {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check context first - if there's a selection or cursor in link, use edge-type selector
|
||||||
|
const content = editor.getValue();
|
||||||
|
const context = detectEdgeSelectorContext(editor, content);
|
||||||
|
|
||||||
|
if (context && (context.mode === "single-link" || context.mode === "selection-links" || context.mode === "create-link")) {
|
||||||
|
// Use edge-type selector for specific links or create new link
|
||||||
|
console.log("[Main] Using edge-type selector for context:", context.mode);
|
||||||
|
await changeEdgeTypeForLinks(
|
||||||
|
this.app,
|
||||||
|
editor,
|
||||||
|
activeFile,
|
||||||
|
this.settings,
|
||||||
|
context,
|
||||||
|
{ ensureGraphSchemaLoaded: () => this.ensureGraphSchemaLoaded() }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, process whole note
|
||||||
|
console.log("[Main] Processing whole note");
|
||||||
|
|
||||||
// Check if overwrite is needed
|
// Check if overwrite is needed
|
||||||
let allowOverwrite = false;
|
let allowOverwrite = false;
|
||||||
if (this.settings.allowOverwriteExistingMappings) {
|
if (this.settings.allowOverwriteExistingMappings) {
|
||||||
|
|
|
||||||
852
src/mapping/edgeTypeSelector.ts
Normal file
852
src/mapping/edgeTypeSelector.ts
Normal file
|
|
@ -0,0 +1,852 @@
|
||||||
|
/**
|
||||||
|
* Edge Type Selector: Change edge types for links in notes or interview fields.
|
||||||
|
* Handles different contexts: cursor in link, selection with links, whole note, etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { App, Editor, TFile, Notice } from "obsidian";
|
||||||
|
import type { MindnetSettings } from "../settings";
|
||||||
|
import { extractWikilinks } from "./sectionParser";
|
||||||
|
import { normalizeLinkTarget } from "../unresolvedLink/linkHelpers";
|
||||||
|
import { LinkPromptModal, type LinkPromptDecision } from "../ui/LinkPromptModal";
|
||||||
|
import { VocabularyLoader } from "../vocab/VocabularyLoader";
|
||||||
|
import { parseEdgeVocabulary } from "../vocab/parseEdgeVocabulary";
|
||||||
|
import type { EdgeVocabulary } from "../vocab/types";
|
||||||
|
import { parseGraphSchema, type GraphSchema } from "./graphSchema";
|
||||||
|
import { getSourceType } from "./worklistBuilder";
|
||||||
|
import type { LinkWorkItem } from "./worklistBuilder";
|
||||||
|
import { extractExistingMappings } from "./mappingExtractor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find wikilink at cursor position in content.
|
||||||
|
* Returns link info with start/end positions and target.
|
||||||
|
*/
|
||||||
|
function findWikilinkAtPosition(content: string, cursorOffset: number): { start: number; end: number; target: string; basename: string } | null {
|
||||||
|
// Find all [[...]] pairs in content (including [[rel:type|link]] format)
|
||||||
|
const linkPattern = /\[\[([^\]]+)\]\]/g;
|
||||||
|
const matches: Array<{ start: number; end: number; target: string }> = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = linkPattern.exec(content)) !== null) {
|
||||||
|
const start = match.index;
|
||||||
|
const end = match.index + match[0].length;
|
||||||
|
const target = match[1] || "";
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
matches.push({ start, end, target });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) return null;
|
||||||
|
|
||||||
|
// Find the link that contains the cursor or is closest to it
|
||||||
|
for (const link of matches) {
|
||||||
|
if (cursorOffset >= link.start && cursorOffset <= link.end) {
|
||||||
|
// Cursor is inside this link
|
||||||
|
let basename: string;
|
||||||
|
// Check if it's a rel: link format [[rel:type|link]]
|
||||||
|
if (link.target.startsWith("rel:")) {
|
||||||
|
// Extract link part after |
|
||||||
|
const parts = link.target.split("|");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
// It's [[rel:type|link]] format, extract the link part
|
||||||
|
const linkPart = parts[1] || "";
|
||||||
|
basename = normalizeLinkTarget(linkPart);
|
||||||
|
} else {
|
||||||
|
// Invalid format, skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal link format [[link]]
|
||||||
|
basename = normalizeLinkTarget(link.target);
|
||||||
|
}
|
||||||
|
if (basename) {
|
||||||
|
return { ...link, basename };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't find nearest link - only return if cursor is directly inside a link
|
||||||
|
// This prevents accidentally selecting a link when cursor is just near it
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EdgeSelectorContext {
|
||||||
|
mode: "single-link" | "selection-links" | "whole-note" | "create-link";
|
||||||
|
linkBasename?: string; // For single-link mode
|
||||||
|
linkBasenames?: string[]; // For selection-links mode
|
||||||
|
selectedText?: string; // For create-link mode
|
||||||
|
startPos?: number; // Start position in content
|
||||||
|
endPos?: number; // End position in content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect context from editor state (cursor position, selection).
|
||||||
|
* Works with both Editor and HTMLTextAreaElement.
|
||||||
|
*/
|
||||||
|
export function detectEdgeSelectorContext(
|
||||||
|
editorOrTextarea: Editor | HTMLTextAreaElement,
|
||||||
|
content: string
|
||||||
|
): EdgeSelectorContext | null {
|
||||||
|
let cursorOffset: number;
|
||||||
|
let selection: string;
|
||||||
|
let selectionStart: number;
|
||||||
|
let selectionEnd: number;
|
||||||
|
|
||||||
|
if (editorOrTextarea instanceof HTMLTextAreaElement) {
|
||||||
|
// Textarea element
|
||||||
|
const textarea = editorOrTextarea;
|
||||||
|
cursorOffset = textarea.selectionStart;
|
||||||
|
selection = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||||
|
selectionStart = textarea.selectionStart;
|
||||||
|
selectionEnd = textarea.selectionEnd;
|
||||||
|
} else {
|
||||||
|
// Editor instance
|
||||||
|
const editor = editorOrTextarea;
|
||||||
|
const from = editor.getCursor("from");
|
||||||
|
const to = editor.getCursor("to");
|
||||||
|
selectionStart = editor.posToOffset(from);
|
||||||
|
selectionEnd = editor.posToOffset(to);
|
||||||
|
|
||||||
|
// Check if there's actually a selection (from != to)
|
||||||
|
const hasSelection = from.line !== to.line || from.ch !== to.ch;
|
||||||
|
|
||||||
|
if (hasSelection) {
|
||||||
|
selection = editor.getSelection();
|
||||||
|
} else {
|
||||||
|
selection = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate cursor offset in content (use "from" position)
|
||||||
|
const cursor = from;
|
||||||
|
const lines = content.split(/\r?\n/);
|
||||||
|
cursorOffset = 0;
|
||||||
|
for (let i = 0; i < cursor.line && i < lines.length; i++) {
|
||||||
|
cursorOffset += lines[i]?.length || 0;
|
||||||
|
cursorOffset += 1; // Newline
|
||||||
|
}
|
||||||
|
cursorOffset += cursor.ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[EdgeSelector] Context detection:", {
|
||||||
|
hasSelection: !!selection && selection.trim().length > 0,
|
||||||
|
selection: selection,
|
||||||
|
selectionLength: selection?.length || 0,
|
||||||
|
cursorOffset,
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
selectionStartEqualsEnd: selectionStart === selectionEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there's a selection (both string and position-based check)
|
||||||
|
const hasActualSelection = selection && selection.trim().length > 0 && selectionStart !== selectionEnd;
|
||||||
|
|
||||||
|
if (hasActualSelection) {
|
||||||
|
// Extract all links in selection (including [[rel:type|link]] format)
|
||||||
|
const selectionText = content.substring(selectionStart, selectionEnd);
|
||||||
|
const linksInSelection = extractWikilinks(selectionText);
|
||||||
|
|
||||||
|
console.log("[EdgeSelector] Selection contains links:", linksInSelection.length, linksInSelection);
|
||||||
|
|
||||||
|
if (linksInSelection.length > 0) {
|
||||||
|
// Selection contains links - process all links in selection
|
||||||
|
console.log("[EdgeSelector] Mode: selection-links");
|
||||||
|
return {
|
||||||
|
mode: "selection-links",
|
||||||
|
linkBasenames: linksInSelection,
|
||||||
|
startPos: selectionStart,
|
||||||
|
endPos: selectionEnd,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Selection without links - create link from selected text
|
||||||
|
console.log("[EdgeSelector] Mode: create-link");
|
||||||
|
return {
|
||||||
|
mode: "create-link",
|
||||||
|
selectedText: selection.trim(),
|
||||||
|
startPos: selectionStart,
|
||||||
|
endPos: selectionEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No selection - check if cursor is inside a link
|
||||||
|
const linkAtCursor = findWikilinkAtPosition(content, cursorOffset);
|
||||||
|
if (linkAtCursor) {
|
||||||
|
console.log("[EdgeSelector] Mode: single-link, basename:", linkAtCursor.basename);
|
||||||
|
return {
|
||||||
|
mode: "single-link",
|
||||||
|
linkBasename: linkAtCursor.basename,
|
||||||
|
startPos: linkAtCursor.start,
|
||||||
|
endPos: linkAtCursor.end,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor outside link, no selection - process whole note
|
||||||
|
console.log("[EdgeSelector] Mode: whole-note");
|
||||||
|
return {
|
||||||
|
mode: "whole-note",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change edge type for a single link or multiple links.
|
||||||
|
* Works with both Editor and HTMLTextAreaElement.
|
||||||
|
*/
|
||||||
|
export async function changeEdgeTypeForLinks(
|
||||||
|
app: App,
|
||||||
|
editorOrTextarea: Editor | HTMLTextAreaElement,
|
||||||
|
file: TFile | null, // null for textarea (interview mode)
|
||||||
|
settings: MindnetSettings,
|
||||||
|
context: EdgeSelectorContext,
|
||||||
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<GraphSchema | null> },
|
||||||
|
onUpdate?: (newContent: string) => void // Callback for textarea updates
|
||||||
|
): Promise<void> {
|
||||||
|
// Store settings for use in helper functions
|
||||||
|
const wrapperCalloutType = settings.mappingWrapperCalloutType;
|
||||||
|
const wrapperTitle = settings.mappingWrapperTitle;
|
||||||
|
|
||||||
|
// Load vocabulary and schema
|
||||||
|
let vocabulary: EdgeVocabulary | null = null;
|
||||||
|
let graphSchema: GraphSchema | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const vocabText = await VocabularyLoader.loadText(app, settings.edgeVocabularyPath);
|
||||||
|
vocabulary = parseEdgeVocabulary(vocabText);
|
||||||
|
} catch (e) {
|
||||||
|
new Notice(`Failed to load edge vocabulary: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin && plugin.ensureGraphSchemaLoaded) {
|
||||||
|
graphSchema = await plugin.ensureGraphSchemaLoaded();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const schemaText = await VocabularyLoader.loadText(app, settings.graphSchemaPath);
|
||||||
|
graphSchema = parseGraphSchema(schemaText);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Graph schema not available: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get source type (only if file is available)
|
||||||
|
let sourceType: string | null = null;
|
||||||
|
if (file) {
|
||||||
|
sourceType = getSourceType(app, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get content
|
||||||
|
let content: string;
|
||||||
|
if (file) {
|
||||||
|
content = await app.vault.read(file);
|
||||||
|
} else if (editorOrTextarea instanceof HTMLTextAreaElement) {
|
||||||
|
content = editorOrTextarea.value;
|
||||||
|
} else {
|
||||||
|
content = editorOrTextarea.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[EdgeSelector] Processing context:", context.mode);
|
||||||
|
|
||||||
|
if (context.mode === "single-link") {
|
||||||
|
// Process single link
|
||||||
|
console.log("[EdgeSelector] Processing single link:", context.linkBasename, "at", context.startPos, "-", context.endPos);
|
||||||
|
await processSingleLink(app, editorOrTextarea, file, content, context.linkBasename!, vocabulary, sourceType, graphSchema, wrapperCalloutType, wrapperTitle, settings, plugin, onUpdate, context.startPos, context.endPos);
|
||||||
|
|
||||||
|
// In normal editor mode (with file), update mapping blocks after changing link
|
||||||
|
if (file && editorOrTextarea instanceof Editor) {
|
||||||
|
// Read updated content from editor
|
||||||
|
const updatedContent = editorOrTextarea.getValue();
|
||||||
|
// Write it back to file temporarily so updateMappingBlocks can process it
|
||||||
|
await app.vault.modify(file, updatedContent);
|
||||||
|
|
||||||
|
// Update mapping blocks without prompting for other links
|
||||||
|
const { updateMappingBlocksForRelLinks } = await import("./updateMappingBlocks");
|
||||||
|
await updateMappingBlocksForRelLinks(app, file, settings);
|
||||||
|
}
|
||||||
|
} else if (context.mode === "selection-links") {
|
||||||
|
// Process all links in selection
|
||||||
|
console.log("[EdgeSelector] Processing selection links:", context.linkBasenames);
|
||||||
|
await processMultipleLinks(app, editorOrTextarea, file, content, context.linkBasenames!, vocabulary, sourceType, graphSchema, context.startPos!, context.endPos!, wrapperCalloutType, wrapperTitle, settings, plugin, onUpdate);
|
||||||
|
|
||||||
|
// In normal editor mode (with file), update mapping blocks after changing links
|
||||||
|
if (file && editorOrTextarea instanceof Editor) {
|
||||||
|
// Read updated content from editor
|
||||||
|
const updatedContent = editorOrTextarea.getValue();
|
||||||
|
// Write it back to file temporarily so updateMappingBlocks can process it
|
||||||
|
await app.vault.modify(file, updatedContent);
|
||||||
|
|
||||||
|
// Update mapping blocks without prompting for other links
|
||||||
|
const { updateMappingBlocksForRelLinks } = await import("./updateMappingBlocks");
|
||||||
|
await updateMappingBlocksForRelLinks(app, file, settings);
|
||||||
|
}
|
||||||
|
} else if (context.mode === "whole-note") {
|
||||||
|
// Process whole note - only if file is available (not for textarea)
|
||||||
|
console.log("[EdgeSelector] Processing whole note");
|
||||||
|
if (file) {
|
||||||
|
await processWholeNote(app, file, settings, vocabulary, graphSchema, plugin);
|
||||||
|
} else {
|
||||||
|
new Notice("Ganze Notiz neu zuordnen ist im Interview-Modus nicht verfügbar. Bitte verwenden Sie die Markierung oder positionieren Sie den Cursor in einem Link.");
|
||||||
|
}
|
||||||
|
} else if (context.mode === "create-link") {
|
||||||
|
// Create link from selected text and assign edge type
|
||||||
|
console.log("[EdgeSelector] Creating link from text:", context.selectedText);
|
||||||
|
await createLinkWithEdgeType(app, editorOrTextarea, file, content, context.selectedText!, vocabulary, sourceType, graphSchema, context.startPos!, context.endPos!, settings, plugin, onUpdate);
|
||||||
|
|
||||||
|
// In normal editor mode (with file), update mapping blocks after creating link
|
||||||
|
if (file && editorOrTextarea instanceof Editor) {
|
||||||
|
// Read updated content from editor
|
||||||
|
const updatedContent = editorOrTextarea.getValue();
|
||||||
|
// Write it back to file temporarily so updateMappingBlocks can process it
|
||||||
|
await app.vault.modify(file, updatedContent);
|
||||||
|
|
||||||
|
// Update mapping blocks without prompting for other links
|
||||||
|
const { updateMappingBlocksForRelLinks } = await import("./updateMappingBlocks");
|
||||||
|
await updateMappingBlocksForRelLinks(app, file, settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single link at cursor position.
|
||||||
|
*/
|
||||||
|
async function processSingleLink(
|
||||||
|
app: App,
|
||||||
|
editorOrTextarea: Editor | HTMLTextAreaElement,
|
||||||
|
file: TFile | null,
|
||||||
|
content: string,
|
||||||
|
linkBasename: string,
|
||||||
|
vocabulary: EdgeVocabulary,
|
||||||
|
sourceType: string | null,
|
||||||
|
graphSchema: GraphSchema | null,
|
||||||
|
wrapperCalloutType: string,
|
||||||
|
wrapperTitle: string,
|
||||||
|
settings: MindnetSettings,
|
||||||
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<GraphSchema | null> },
|
||||||
|
onUpdate?: (newContent: string) => void,
|
||||||
|
linkStart?: number,
|
||||||
|
linkEnd?: number
|
||||||
|
): Promise<void> {
|
||||||
|
// Get target type
|
||||||
|
const targetFile = app.metadataCache.getFirstLinkpathDest(linkBasename, "");
|
||||||
|
const targetType = targetFile ? (app.metadataCache.getFileCache(targetFile)?.frontmatter?.type as string | undefined) || null : null;
|
||||||
|
|
||||||
|
// Use link position from parameters if provided, otherwise find it
|
||||||
|
let actualLinkStart: number;
|
||||||
|
let actualLinkEnd: number;
|
||||||
|
|
||||||
|
if (linkStart !== undefined && linkEnd !== undefined) {
|
||||||
|
// Use provided positions
|
||||||
|
actualLinkStart = linkStart;
|
||||||
|
actualLinkEnd = linkEnd;
|
||||||
|
} else {
|
||||||
|
// Find link position from cursor
|
||||||
|
let cursorOffset: number;
|
||||||
|
if (editorOrTextarea instanceof HTMLTextAreaElement) {
|
||||||
|
cursorOffset = editorOrTextarea.selectionStart;
|
||||||
|
} else {
|
||||||
|
const cursor = editorOrTextarea.getCursor();
|
||||||
|
cursorOffset = editorOrTextarea.posToOffset(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkAtCursor = findWikilinkAtPosition(content, cursorOffset);
|
||||||
|
if (!linkAtCursor) {
|
||||||
|
console.warn("[EdgeSelector] Could not find link at cursor position");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actualLinkStart = linkAtCursor.start;
|
||||||
|
actualLinkEnd = linkAtCursor.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cursor position for section detection
|
||||||
|
let cursorOffset: number;
|
||||||
|
if (editorOrTextarea instanceof HTMLTextAreaElement) {
|
||||||
|
cursorOffset = editorOrTextarea.selectionStart;
|
||||||
|
} else {
|
||||||
|
const cursor = editorOrTextarea.getCursor();
|
||||||
|
cursorOffset = editorOrTextarea.posToOffset(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split(/\r?\n/);
|
||||||
|
// Calculate cursor line
|
||||||
|
let cursorLine = 0;
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const lineLength = lines[i]?.length || 0;
|
||||||
|
if (offset + lineLength >= cursorOffset) {
|
||||||
|
cursorLine = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += lineLength + 1; // +1 for newline
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find section containing cursor
|
||||||
|
let sectionContent = "";
|
||||||
|
let sectionHeading: string | null = null;
|
||||||
|
let inSection = false;
|
||||||
|
|
||||||
|
for (let i = cursorLine; i >= 0; i--) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line === undefined) continue;
|
||||||
|
|
||||||
|
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||||
|
if (headingMatch) {
|
||||||
|
sectionHeading = headingMatch[2]?.trim() || null;
|
||||||
|
inSection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect section content
|
||||||
|
const sectionStart = inSection ? (lines.findIndex((l, i) => i <= cursorLine && l.match(/^(#{1,6})\s+/)) + 1) : 0;
|
||||||
|
const sectionEnd = lines.findIndex((l, i) => i > cursorLine && l.match(/^(#{1,6})\s+/));
|
||||||
|
const sectionLines = sectionEnd >= 0 ? lines.slice(sectionStart, sectionEnd) : lines.slice(sectionStart);
|
||||||
|
sectionContent = sectionLines.join("\n");
|
||||||
|
|
||||||
|
// Extract existing mappings from section
|
||||||
|
const mappingState = extractExistingMappings(
|
||||||
|
sectionContent,
|
||||||
|
wrapperCalloutType,
|
||||||
|
wrapperTitle
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentType = mappingState.existingMappings.get(linkBasename) || null;
|
||||||
|
|
||||||
|
// Get the original link text from content to preserve rel:type| format
|
||||||
|
const originalLinkText = content.substring(actualLinkStart, actualLinkEnd);
|
||||||
|
// Extract the inner part (without [[ and ]])
|
||||||
|
const innerLinkText = originalLinkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||||
|
|
||||||
|
// Create work item with display link
|
||||||
|
const item: LinkWorkItem = {
|
||||||
|
link: linkBasename,
|
||||||
|
targetType,
|
||||||
|
currentType,
|
||||||
|
displayLink: innerLinkText, // Preserve rel:type|link format for display
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show prompt modal
|
||||||
|
const prompt = new LinkPromptModal(app, item, vocabulary, sourceType, graphSchema);
|
||||||
|
const decision = await prompt.show();
|
||||||
|
|
||||||
|
if (decision.action === "skip" || decision.action === "keep") {
|
||||||
|
return; // No change needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply edge type change: convert link to [[rel:type|link]] format
|
||||||
|
|
||||||
|
// Ensure linkBasename is not empty
|
||||||
|
if (!linkBasename || linkBasename.trim() === "") {
|
||||||
|
new Notice("Link-Basename ist leer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get final edge type from decision
|
||||||
|
let finalEdgeType: string;
|
||||||
|
if (decision.action === "change" || decision.action === "setTypical") {
|
||||||
|
finalEdgeType = decision.alias || decision.edgeType;
|
||||||
|
} else {
|
||||||
|
return; // Skip or keep - no change
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure finalEdgeType is not empty
|
||||||
|
if (!finalEdgeType || finalEdgeType.trim() === "") {
|
||||||
|
new Notice("Edge-Type ist leer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLink = `[[rel:${finalEdgeType}|${linkBasename}]]`;
|
||||||
|
|
||||||
|
console.log("[EdgeSelector] Replacing link:", {
|
||||||
|
linkBasename,
|
||||||
|
finalEdgeType,
|
||||||
|
newLink,
|
||||||
|
linkStart: actualLinkStart,
|
||||||
|
linkEnd: actualLinkEnd,
|
||||||
|
oldLink: content.substring(actualLinkStart, actualLinkEnd),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace link in editor or textarea
|
||||||
|
if (editorOrTextarea instanceof HTMLTextAreaElement) {
|
||||||
|
const textarea = editorOrTextarea;
|
||||||
|
const newContent =
|
||||||
|
content.substring(0, actualLinkStart) +
|
||||||
|
newLink +
|
||||||
|
content.substring(actualLinkEnd);
|
||||||
|
|
||||||
|
// Update textarea
|
||||||
|
textarea.value = newContent;
|
||||||
|
// Set cursor after the link
|
||||||
|
const newCursorPos = actualLinkStart + newLink.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Call update callback if provided
|
||||||
|
if (onUpdate) {
|
||||||
|
onUpdate(newContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const editor = editorOrTextarea;
|
||||||
|
const from = editor.offsetToPos(actualLinkStart);
|
||||||
|
const to = editor.offsetToPos(actualLinkEnd);
|
||||||
|
editor.replaceRange(newLink, from, to);
|
||||||
|
|
||||||
|
// In normal editor mode (with file), update mapping blocks after changing link
|
||||||
|
if (file) {
|
||||||
|
// Read updated content from editor
|
||||||
|
const updatedContent = editor.getValue();
|
||||||
|
// Write it back to file temporarily so updateMappingBlocks can process it
|
||||||
|
await app.vault.modify(file, updatedContent);
|
||||||
|
|
||||||
|
// Update mapping blocks without prompting for other links
|
||||||
|
const { updateMappingBlocksForRelLinks } = await import("./updateMappingBlocks");
|
||||||
|
await updateMappingBlocksForRelLinks(app, file, settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Notice(`Edge type set to: ${finalEdgeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process multiple links in selection.
|
||||||
|
*/
|
||||||
|
async function processMultipleLinks(
|
||||||
|
app: App,
|
||||||
|
editorOrTextarea: Editor | HTMLTextAreaElement,
|
||||||
|
file: TFile | null,
|
||||||
|
content: string,
|
||||||
|
linkBasenames: string[],
|
||||||
|
vocabulary: EdgeVocabulary,
|
||||||
|
sourceType: string | null,
|
||||||
|
graphSchema: GraphSchema | null,
|
||||||
|
selectionStart: number,
|
||||||
|
selectionEnd: number,
|
||||||
|
wrapperCalloutType: string,
|
||||||
|
wrapperTitle: string,
|
||||||
|
settings: MindnetSettings,
|
||||||
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<GraphSchema | null> },
|
||||||
|
onUpdate?: (newContent: string) => void
|
||||||
|
): Promise<void> {
|
||||||
|
// Process each link in selection
|
||||||
|
const selectionText = content.substring(selectionStart, selectionEnd);
|
||||||
|
const lines = content.split(/\r?\n/);
|
||||||
|
|
||||||
|
// Find section containing selection
|
||||||
|
const startLine = content.substring(0, selectionStart).split(/\r?\n/).length - 1;
|
||||||
|
let sectionHeading: string | null = null;
|
||||||
|
let inSection = false;
|
||||||
|
|
||||||
|
for (let i = startLine; i >= 0; i--) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line === undefined) continue;
|
||||||
|
|
||||||
|
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||||
|
if (headingMatch) {
|
||||||
|
sectionHeading = headingMatch[2]?.trim() || null;
|
||||||
|
inSection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract existing mappings
|
||||||
|
const sectionStart = inSection ? (lines.findIndex((l, i) => i <= startLine && l.match(/^(#{1,6})\s+/)) + 1) : 0;
|
||||||
|
const sectionEnd = lines.findIndex((l, i) => i > startLine && l.match(/^(#{1,6})\s+/));
|
||||||
|
const sectionLines = sectionEnd >= 0 ? lines.slice(sectionStart, sectionEnd) : lines.slice(sectionStart);
|
||||||
|
const sectionContent = sectionLines.join("\n");
|
||||||
|
|
||||||
|
// Extract existing mappings
|
||||||
|
const mappingState = extractExistingMappings(
|
||||||
|
sectionContent,
|
||||||
|
wrapperCalloutType,
|
||||||
|
wrapperTitle
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process each link in the order they appear in the text
|
||||||
|
const relLinkRegex = /\[\[([^\]]+?)\]\]/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
const replacements: Array<{ start: number; end: number; newText: string }> = [];
|
||||||
|
|
||||||
|
// First, collect all links in order of appearance in selection
|
||||||
|
const linksInOrder: Array<{ basename: string; matchIndex: number }> = [];
|
||||||
|
relLinkRegex.lastIndex = 0;
|
||||||
|
while ((match = relLinkRegex.exec(selectionText)) !== null) {
|
||||||
|
const linkText = match[1] || "";
|
||||||
|
// Check if it's a rel: link format [[rel:type|link]]
|
||||||
|
let basename: string;
|
||||||
|
if (linkText.startsWith("rel:")) {
|
||||||
|
// Extract link part after |
|
||||||
|
const parts = linkText.split("|");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
// It's [[rel:type|link]] format, extract the link part
|
||||||
|
const linkPart = parts[1] || "";
|
||||||
|
basename = normalizeLinkTarget(linkPart);
|
||||||
|
} else {
|
||||||
|
// Invalid format, skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal link format [[link]]
|
||||||
|
basename = normalizeLinkTarget(linkText);
|
||||||
|
}
|
||||||
|
if (basename) {
|
||||||
|
linksInOrder.push({
|
||||||
|
basename,
|
||||||
|
matchIndex: match.index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, collect all decisions in order
|
||||||
|
const decisions = new Map<string, LinkPromptDecision>();
|
||||||
|
// Also store the original link text for each basename to preserve display format
|
||||||
|
const linkTextMap = new Map<string, string>(); // basename -> original inner link text
|
||||||
|
|
||||||
|
// Build link text map first
|
||||||
|
relLinkRegex.lastIndex = 0;
|
||||||
|
while ((match = relLinkRegex.exec(selectionText)) !== null) {
|
||||||
|
const linkText = match[1] || "";
|
||||||
|
// Check if it's a rel: link format [[rel:type|link]]
|
||||||
|
let basename: string;
|
||||||
|
if (linkText.startsWith("rel:")) {
|
||||||
|
// Extract link part after |
|
||||||
|
const parts = linkText.split("|");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
// It's [[rel:type|link]] format, extract the link part
|
||||||
|
const linkPart = parts[1] || "";
|
||||||
|
basename = normalizeLinkTarget(linkPart);
|
||||||
|
} else {
|
||||||
|
// Invalid format, skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal link format [[link]]
|
||||||
|
basename = normalizeLinkTarget(linkText);
|
||||||
|
}
|
||||||
|
if (basename) {
|
||||||
|
linkTextMap.set(basename, linkText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { basename } of linksInOrder) {
|
||||||
|
const targetFile = app.metadataCache.getFirstLinkpathDest(basename, "");
|
||||||
|
const targetType = targetFile ? (app.metadataCache.getFileCache(targetFile)?.frontmatter?.type as string | undefined) || null : null;
|
||||||
|
const currentType = mappingState.existingMappings.get(basename) || null;
|
||||||
|
|
||||||
|
// Get the original link text from map to preserve rel:type| format
|
||||||
|
const originalLinkText = linkTextMap.get(basename) || basename;
|
||||||
|
|
||||||
|
const item: LinkWorkItem = {
|
||||||
|
link: basename,
|
||||||
|
targetType,
|
||||||
|
currentType,
|
||||||
|
displayLink: originalLinkText, // Preserve rel:type|link format for display
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = new LinkPromptModal(app, item, vocabulary, sourceType, graphSchema);
|
||||||
|
const decision = await prompt.show();
|
||||||
|
|
||||||
|
if (decision.action !== "skip") {
|
||||||
|
decisions.set(basename, decision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply replacements in reverse order
|
||||||
|
relLinkRegex.lastIndex = 0;
|
||||||
|
const allMatches: Array<{ match: RegExpExecArray; basename: string }> = [];
|
||||||
|
|
||||||
|
while ((match = relLinkRegex.exec(selectionText)) !== null) {
|
||||||
|
const linkText = match[1] || "";
|
||||||
|
// Check if it's a rel: link format [[rel:type|link]]
|
||||||
|
let basename: string;
|
||||||
|
if (linkText.startsWith("rel:")) {
|
||||||
|
// Extract link part after |
|
||||||
|
const parts = linkText.split("|");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
// It's [[rel:type|link]] format, extract the link part
|
||||||
|
const linkPart = parts[1] || "";
|
||||||
|
basename = normalizeLinkTarget(linkPart);
|
||||||
|
} else {
|
||||||
|
// Invalid format, skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal link format [[link]]
|
||||||
|
basename = normalizeLinkTarget(linkText);
|
||||||
|
}
|
||||||
|
if (basename && decisions.has(basename)) {
|
||||||
|
const decision = decisions.get(basename)!;
|
||||||
|
let finalEdgeType: string;
|
||||||
|
if (decision.action === "change" || decision.action === "setTypical") {
|
||||||
|
finalEdgeType = decision.alias || decision.edgeType;
|
||||||
|
} else {
|
||||||
|
continue; // Skip or keep - no change
|
||||||
|
}
|
||||||
|
// Ensure basename and finalEdgeType are not empty
|
||||||
|
if (!basename || basename.trim() === "" || !finalEdgeType || finalEdgeType.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLink = `[[rel:${finalEdgeType}|${basename}]]`;
|
||||||
|
|
||||||
|
allMatches.push({
|
||||||
|
match,
|
||||||
|
basename,
|
||||||
|
});
|
||||||
|
|
||||||
|
replacements.push({
|
||||||
|
start: selectionStart + match.index,
|
||||||
|
end: selectionStart + match.index + match[0].length,
|
||||||
|
newText: newLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply replacements in reverse order
|
||||||
|
replacements.sort((a, b) => b.start - a.start);
|
||||||
|
|
||||||
|
if (editorOrTextarea instanceof HTMLTextAreaElement) {
|
||||||
|
const textarea = editorOrTextarea;
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
for (const replacement of replacements) {
|
||||||
|
newContent =
|
||||||
|
newContent.substring(0, replacement.start) +
|
||||||
|
replacement.newText +
|
||||||
|
newContent.substring(replacement.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update textarea
|
||||||
|
textarea.value = newContent;
|
||||||
|
// Set cursor at end of selection
|
||||||
|
const lastReplacement = replacements[replacements.length - 1];
|
||||||
|
if (lastReplacement) {
|
||||||
|
const newCursorPos = lastReplacement.start + lastReplacement.newText.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call update callback if provided
|
||||||
|
if (onUpdate) {
|
||||||
|
onUpdate(newContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const editor = editorOrTextarea;
|
||||||
|
for (const replacement of replacements) {
|
||||||
|
const from = editor.offsetToPos(replacement.start);
|
||||||
|
const to = editor.offsetToPos(replacement.end);
|
||||||
|
editor.replaceRange(replacement.newText, from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In normal editor mode (with file), we just update the links
|
||||||
|
// The user can manually trigger "Build semantic mapping blocks" if needed
|
||||||
|
// This avoids processing all links in the note
|
||||||
|
}
|
||||||
|
|
||||||
|
new Notice(`Edge types updated for ${decisions.size} link(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process whole note using buildSemanticMappings.
|
||||||
|
*/
|
||||||
|
async function processWholeNote(
|
||||||
|
app: App,
|
||||||
|
file: TFile,
|
||||||
|
settings: MindnetSettings,
|
||||||
|
vocabulary: EdgeVocabulary | null,
|
||||||
|
graphSchema: GraphSchema | null,
|
||||||
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<GraphSchema | null> }
|
||||||
|
): Promise<void> {
|
||||||
|
// Use existing buildSemanticMappings function
|
||||||
|
const { buildSemanticMappings } = await import("./semanticMappingBuilder");
|
||||||
|
await buildSemanticMappings(app, file, settings, false, plugin);
|
||||||
|
new Notice("Edge types processed for whole note");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create link from selected text and assign edge type.
|
||||||
|
*/
|
||||||
|
async function createLinkWithEdgeType(
|
||||||
|
app: App,
|
||||||
|
editorOrTextarea: Editor | HTMLTextAreaElement,
|
||||||
|
file: TFile | null,
|
||||||
|
content: string,
|
||||||
|
selectedText: string,
|
||||||
|
vocabulary: EdgeVocabulary,
|
||||||
|
sourceType: string | null,
|
||||||
|
graphSchema: GraphSchema | null,
|
||||||
|
selectionStart: number,
|
||||||
|
selectionEnd: number,
|
||||||
|
settings: MindnetSettings,
|
||||||
|
plugin?: { ensureGraphSchemaLoaded?: () => Promise<GraphSchema | null> },
|
||||||
|
onUpdate?: (newContent: string) => void
|
||||||
|
): Promise<void> {
|
||||||
|
// Use selected text as link basename - only remove invalid filename characters
|
||||||
|
// Invalid characters for filenames: < > : " / \ | ? *
|
||||||
|
// Keep spaces, Umlaute, and other valid characters
|
||||||
|
const linkBasename = selectedText
|
||||||
|
.replace(/[<>:"/\\|?*]/g, "") // Remove invalid filename characters
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Show edge type selector for new link
|
||||||
|
const targetFile = app.metadataCache.getFirstLinkpathDest(linkBasename, "");
|
||||||
|
const targetType = targetFile ? (app.metadataCache.getFileCache(targetFile)?.frontmatter?.type as string | undefined) || null : null;
|
||||||
|
|
||||||
|
const item: LinkWorkItem = {
|
||||||
|
link: linkBasename,
|
||||||
|
targetType,
|
||||||
|
currentType: null, // New link, no current type
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = new LinkPromptModal(app, item, vocabulary, sourceType, graphSchema);
|
||||||
|
const decision = await prompt.show();
|
||||||
|
|
||||||
|
if (decision.action === "skip") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create link with edge type
|
||||||
|
let finalEdgeType: string;
|
||||||
|
if (decision.action === "change" || decision.action === "setTypical") {
|
||||||
|
finalEdgeType = decision.alias || decision.edgeType;
|
||||||
|
} else {
|
||||||
|
return; // Keep - shouldn't happen for new link
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure linkBasename and finalEdgeType are not empty
|
||||||
|
if (!linkBasename || linkBasename.trim() === "") {
|
||||||
|
new Notice("Link-Basename ist leer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!finalEdgeType || finalEdgeType.trim() === "") {
|
||||||
|
new Notice("Edge-Type ist leer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLink = `[[rel:${finalEdgeType}|${linkBasename}]]`;
|
||||||
|
|
||||||
|
// Replace selected text with link
|
||||||
|
if (editorOrTextarea instanceof HTMLTextAreaElement) {
|
||||||
|
const textarea = editorOrTextarea;
|
||||||
|
const newContent =
|
||||||
|
content.substring(0, selectionStart) +
|
||||||
|
newLink +
|
||||||
|
content.substring(selectionEnd);
|
||||||
|
|
||||||
|
// Update textarea
|
||||||
|
textarea.value = newContent;
|
||||||
|
// Set cursor after the link
|
||||||
|
const newCursorPos = selectionStart + newLink.length;
|
||||||
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
|
||||||
|
// Call update callback if provided
|
||||||
|
if (onUpdate) {
|
||||||
|
onUpdate(newContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const editor = editorOrTextarea;
|
||||||
|
const from = editor.offsetToPos(selectionStart);
|
||||||
|
const to = editor.offsetToPos(selectionEnd);
|
||||||
|
editor.replaceRange(newLink, from, to);
|
||||||
|
|
||||||
|
// In normal editor mode (with file), we just create the link
|
||||||
|
// The user can manually trigger "Build semantic mapping blocks" if needed
|
||||||
|
// This avoids processing all links in the note
|
||||||
|
}
|
||||||
|
|
||||||
|
new Notice(`Link created with edge type: ${finalEdgeType}`);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
* Section parser: Split markdown by headings and extract wikilinks per section.
|
* Section parser: Split markdown by headings and extract wikilinks per section.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { normalizeLinkTarget } from "../unresolvedLink/linkHelpers";
|
||||||
|
|
||||||
export interface NoteSection {
|
export interface NoteSection {
|
||||||
heading: string | null; // null for content before first heading
|
heading: string | null; // null for content before first heading
|
||||||
headingLevel: number; // 0 for content before first heading
|
headingLevel: number; // 0 for content before first heading
|
||||||
|
|
@ -99,9 +101,31 @@ export function extractWikilinks(markdown: string): string[] {
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
while ((match = wikilinkRegex.exec(markdown)) !== null) {
|
while ((match = wikilinkRegex.exec(markdown)) !== null) {
|
||||||
if (match[1]) {
|
if (match[1]) {
|
||||||
const link = match[1].trim();
|
const target = match[1].trim();
|
||||||
if (link) {
|
if (target) {
|
||||||
links.add(link);
|
// 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) {
|
||||||
|
// Normalize: remove alias (|alias) and heading (#heading)
|
||||||
|
const normalized = normalizeLinkTarget(linkPart);
|
||||||
|
if (normalized) {
|
||||||
|
links.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal link format [[link]]
|
||||||
|
// Normalize: remove alias (|alias) and heading (#heading)
|
||||||
|
const normalized = normalizeLinkTarget(target);
|
||||||
|
if (normalized) {
|
||||||
|
links.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
158
src/mapping/updateMappingBlocks.ts
Normal file
158
src/mapping/updateMappingBlocks.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/**
|
||||||
|
* Update mapping blocks for specific links without prompting for all links.
|
||||||
|
* Used when user changes specific links (single-link, selection-links, create-link).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { App, TFile } from "obsidian";
|
||||||
|
import type { MindnetSettings } from "../settings";
|
||||||
|
import { splitIntoSections, type NoteSection } from "./sectionParser";
|
||||||
|
import { parseEdgesFromCallouts } from "../parser/parseEdgesFromCallouts";
|
||||||
|
import {
|
||||||
|
extractExistingMappings,
|
||||||
|
removeWrapperBlock,
|
||||||
|
type SectionMappingState,
|
||||||
|
} from "./mappingExtractor";
|
||||||
|
import {
|
||||||
|
buildMappingBlock,
|
||||||
|
insertMappingBlock,
|
||||||
|
type MappingBuilderOptions,
|
||||||
|
} from "./mappingBuilder";
|
||||||
|
import { convertRelLinksToEdges } from "../parser/parseRelLinks";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update mapping blocks for sections containing rel: links, without prompting.
|
||||||
|
* Only processes links that are already in [[rel:type|link]] format.
|
||||||
|
*/
|
||||||
|
export async function updateMappingBlocksForRelLinks(
|
||||||
|
app: App,
|
||||||
|
file: TFile,
|
||||||
|
settings: MindnetSettings
|
||||||
|
): Promise<void> {
|
||||||
|
let content = await app.vault.read(file);
|
||||||
|
const lines = content.split(/\r?\n/);
|
||||||
|
|
||||||
|
// Convert [[rel:type|link]] links to normal [[link]] and extract edge mappings
|
||||||
|
const { convertedContent, edgeMappings: relLinkMappings } = convertRelLinksToEdges(content);
|
||||||
|
content = convertedContent;
|
||||||
|
|
||||||
|
if (relLinkMappings.size === 0) {
|
||||||
|
// No rel: links to process
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[UpdateMappingBlocks] Converting ${relLinkMappings.size} rel: links to edge mappings`);
|
||||||
|
|
||||||
|
// Split into sections
|
||||||
|
const sections = splitIntoSections(content);
|
||||||
|
|
||||||
|
// Process sections in reverse order (to preserve line indices when modifying)
|
||||||
|
const modifiedSections: Array<{ section: NoteSection; newContent: string }> = [];
|
||||||
|
|
||||||
|
for (const section of sections) {
|
||||||
|
if (section.links.length === 0) {
|
||||||
|
// No links, skip
|
||||||
|
modifiedSections.push({
|
||||||
|
section,
|
||||||
|
newContent: section.content,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract existing mappings
|
||||||
|
const mappingState = extractExistingMappings(
|
||||||
|
section.content,
|
||||||
|
settings.mappingWrapperCalloutType,
|
||||||
|
settings.mappingWrapperTitle
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge rel: link mappings (from [[rel:type|link]] format) into existing mappings
|
||||||
|
// These take precedence over file content mappings (update even if already mapped)
|
||||||
|
for (const [linkBasename, edgeType] of relLinkMappings.entries()) {
|
||||||
|
// Check if this link exists in this section
|
||||||
|
const normalizedBasename = linkBasename.split("|")[0]?.split("#")[0]?.trim() || linkBasename;
|
||||||
|
if (section.links.some(link => {
|
||||||
|
const normalizedLink = link.split("|")[0]?.split("#")[0]?.trim() || link;
|
||||||
|
return normalizedLink === normalizedBasename || link === normalizedBasename;
|
||||||
|
})) {
|
||||||
|
// Always update rel: link mappings (they represent user's explicit choice)
|
||||||
|
mappingState.existingMappings.set(normalizedBasename, edgeType);
|
||||||
|
console.log(`[UpdateMappingBlocks] Updated rel: link mapping: ${normalizedBasename} -> ${edgeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove wrapper block if exists
|
||||||
|
let sectionContentWithoutWrapper = section.content;
|
||||||
|
if (mappingState.wrapperBlockStartLine !== null && mappingState.wrapperBlockEndLine !== null) {
|
||||||
|
sectionContentWithoutWrapper = removeWrapperBlock(
|
||||||
|
section.content,
|
||||||
|
mappingState.wrapperBlockStartLine,
|
||||||
|
mappingState.wrapperBlockEndLine
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build mapping block with updated mappings
|
||||||
|
const mappingOptions: MappingBuilderOptions = {
|
||||||
|
wrapperCalloutType: settings.mappingWrapperCalloutType,
|
||||||
|
wrapperTitle: settings.mappingWrapperTitle,
|
||||||
|
wrapperFolded: settings.mappingWrapperFolded,
|
||||||
|
defaultEdgeType: settings.defaultEdgeType,
|
||||||
|
assignUnmapped: "none", // Don't assign unmapped links, just use existing mappings
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappingBlock = buildMappingBlock(
|
||||||
|
section.links,
|
||||||
|
mappingState.existingMappings,
|
||||||
|
mappingOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mappingBlock) {
|
||||||
|
const newContent = insertMappingBlock(sectionContentWithoutWrapper, mappingBlock);
|
||||||
|
modifiedSections.push({
|
||||||
|
section,
|
||||||
|
newContent,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No mapping block needed
|
||||||
|
modifiedSections.push({
|
||||||
|
section,
|
||||||
|
newContent: sectionContentWithoutWrapper,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct file content with modified sections
|
||||||
|
let newContent = "";
|
||||||
|
let currentLine = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < modifiedSections.length; i++) {
|
||||||
|
const { section, newContent: sectionNewContent } = modifiedSections[i]!;
|
||||||
|
|
||||||
|
// Add lines before this section
|
||||||
|
if (currentLine < section.startLine) {
|
||||||
|
newContent += lines.slice(currentLine, section.startLine).join("\n");
|
||||||
|
if (currentLine < section.startLine) {
|
||||||
|
newContent += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add modified section content
|
||||||
|
newContent += sectionNewContent;
|
||||||
|
|
||||||
|
currentLine = section.endLine;
|
||||||
|
|
||||||
|
// Add newline between sections (except for last section)
|
||||||
|
if (i < modifiedSections.length - 1) {
|
||||||
|
newContent += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining lines after last section
|
||||||
|
if (currentLine < lines.length) {
|
||||||
|
newContent += "\n" + lines.slice(currentLine).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
await app.vault.modify(file, newContent);
|
||||||
|
|
||||||
|
console.log(`[UpdateMappingBlocks] Updated mapping blocks for ${modifiedSections.length} sections`);
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ export interface LinkWorkItem {
|
||||||
link: string; // Wikilink basename
|
link: string; // Wikilink basename
|
||||||
targetType: string | null; // Note type from metadataCache/frontmatter
|
targetType: string | null; // Note type from metadataCache/frontmatter
|
||||||
currentType: string | null; // Existing edge type mapping, if any
|
currentType: string | null; // Existing edge type mapping, if any
|
||||||
|
displayLink?: string; // Optional: Full link text to display (e.g., "rel:type|link" or "link|alias")
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SectionWorklist {
|
export interface SectionWorklist {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,12 @@ export function extractRelLinks(content: string): RelLink[] {
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
while ((match = relLinkRegex.exec(content)) !== null) {
|
while ((match = relLinkRegex.exec(content)) !== null) {
|
||||||
const edgeType = match[1]?.trim();
|
const edgeType = match[1]?.trim();
|
||||||
const linkPart = match[2]?.trim() || match[1]?.trim(); // If no |, use type as link (fallback)
|
const linkPart = match[2]?.trim();
|
||||||
|
|
||||||
|
// If no link part after |, skip this match (invalid format)
|
||||||
|
if (!linkPart || linkPart.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (edgeType && linkPart) {
|
if (edgeType && linkPart) {
|
||||||
// Extract basename (remove alias and heading)
|
// Extract basename (remove alias and heading)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { extractFrontmatterId } from "../parser/parseFrontmatter";
|
||||||
import {
|
import {
|
||||||
createMarkdownToolbar,
|
createMarkdownToolbar,
|
||||||
} from "./markdownToolbar";
|
} from "./markdownToolbar";
|
||||||
|
import { detectEdgeSelectorContext, changeEdgeTypeForLinks } from "../mapping/edgeTypeSelector";
|
||||||
import { renderProfileToMarkdown, type RenderAnswers } from "../interview/renderer";
|
import { renderProfileToMarkdown, type RenderAnswers } from "../interview/renderer";
|
||||||
import { NoteIndex } from "../entityPicker/noteIndex";
|
import { NoteIndex } from "../entityPicker/noteIndex";
|
||||||
import { EntityPickerModal, type EntityPickerResult } from "./EntityPickerModal";
|
import { EntityPickerModal, type EntityPickerResult } from "./EntityPickerModal";
|
||||||
|
|
@ -506,6 +507,10 @@ export class InterviewWizardModal extends Modal {
|
||||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||||
}
|
}
|
||||||
).open();
|
).open();
|
||||||
|
},
|
||||||
|
async (app: App, textarea: HTMLTextAreaElement) => {
|
||||||
|
// Edge-Type-Selektor für Interview-Eingabefeld
|
||||||
|
await this.handleEdgeTypeSelectorForTextarea(app, textarea, step, textEditorContainer);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
textEditorContainer.insertBefore(toolbar, textEditorContainer.firstChild);
|
textEditorContainer.insertBefore(toolbar, textEditorContainer.firstChild);
|
||||||
|
|
@ -1354,69 +1359,9 @@ export class InterviewWizardModal extends Modal {
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
const itemToolbar = createMarkdownToolbar(
|
const itemToolbar = createMarkdownToolbar(
|
||||||
textarea,
|
textarea,
|
||||||
async () => {
|
undefined, // No preview toggle for loop items
|
||||||
// Get current value from textarea before toggling
|
|
||||||
let currentValue = textarea.value;
|
|
||||||
console.log("Preview toggle clicked (loop)", {
|
|
||||||
textareaValue: currentValue,
|
|
||||||
textareaValueLength: currentValue?.length || 0,
|
|
||||||
existingValue: existingValue,
|
|
||||||
existingValueLength: existingValue?.length || 0,
|
|
||||||
previewKey: previewKey,
|
|
||||||
loopKey: loopKey,
|
|
||||||
nestedStepKey: nestedStep.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If textarea is empty, try to get from draft
|
|
||||||
if (!currentValue || currentValue.trim() === "") {
|
|
||||||
const currentLoopState = this.state.loopRuntimeStates.get(loopKey);
|
|
||||||
console.log("Textarea empty, checking draft", {
|
|
||||||
loopStateExists: !!currentLoopState,
|
|
||||||
draftValue: currentLoopState?.draft[nestedStep.key],
|
|
||||||
});
|
|
||||||
if (currentLoopState) {
|
|
||||||
const draftValue = currentLoopState.draft[nestedStep.key];
|
|
||||||
if (draftValue) {
|
|
||||||
currentValue = String(draftValue);
|
|
||||||
console.log("Using draft value", { draftValue: currentValue });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update draft with current value
|
|
||||||
onFieldChange(nestedStep.key, currentValue);
|
|
||||||
|
|
||||||
// Toggle preview mode
|
|
||||||
const newPreviewMode = !this.previewMode.get(previewKey);
|
|
||||||
this.previewMode.set(previewKey, newPreviewMode);
|
|
||||||
console.log("Preview mode toggled", {
|
|
||||||
newPreviewMode: newPreviewMode,
|
|
||||||
previewKey: previewKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If switching to preview mode, render preview immediately
|
|
||||||
if (newPreviewMode) {
|
|
||||||
// Update preview container visibility
|
|
||||||
previewContainer.style.display = "block";
|
|
||||||
textEditorContainer.style.display = "none";
|
|
||||||
backToEditWrapper.style.display = "block";
|
|
||||||
// Render preview content (use existingValue as fallback)
|
|
||||||
const valueToRender = currentValue || existingValue || "";
|
|
||||||
console.log("Rendering preview", {
|
|
||||||
valueToRender: valueToRender,
|
|
||||||
valueLength: valueToRender.length,
|
|
||||||
valuePreview: valueToRender.substring(0, 100),
|
|
||||||
});
|
|
||||||
await this.updatePreview(previewContainer, valueToRender);
|
|
||||||
} else {
|
|
||||||
// Switching back to edit mode
|
|
||||||
previewContainer.style.display = "none";
|
|
||||||
textEditorContainer.style.display = "block";
|
|
||||||
backToEditWrapper.style.display = "none";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(app: App) => {
|
(app: App) => {
|
||||||
// Open entity picker modal for loop nested step
|
// Entity picker for loop items
|
||||||
if (!this.noteIndex) {
|
if (!this.noteIndex) {
|
||||||
new Notice("Note index not available");
|
new Notice("Note index not available");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1425,8 +1370,7 @@ export class InterviewWizardModal extends Modal {
|
||||||
app,
|
app,
|
||||||
this.noteIndex,
|
this.noteIndex,
|
||||||
async (result: EntityPickerResult) => {
|
async (result: EntityPickerResult) => {
|
||||||
// Check if inline micro edging is enabled (also for toolbar in loops)
|
// Check if inline micro edging is enabled
|
||||||
// Support: inline_micro, both (inline_micro + post_run)
|
|
||||||
const edgingMode = this.profile.edging?.mode;
|
const edgingMode = this.profile.edging?.mode;
|
||||||
const shouldRunInlineMicro =
|
const shouldRunInlineMicro =
|
||||||
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
(edgingMode === "inline_micro" || edgingMode === "both") &&
|
||||||
|
|
@ -1435,21 +1379,21 @@ export class InterviewWizardModal extends Modal {
|
||||||
let linkText = `[[${result.basename}]]`;
|
let linkText = `[[${result.basename}]]`;
|
||||||
|
|
||||||
if (shouldRunInlineMicro) {
|
if (shouldRunInlineMicro) {
|
||||||
// Get current step for section key resolution (use nested step in loop context)
|
// nestedStep is already available in this scope
|
||||||
console.log("[Mindnet] Starting inline micro edging from toolbar (loop)");
|
|
||||||
const edgeType = await this.handleInlineMicroEdging(nestedStep, result.basename, result.path);
|
const edgeType = await this.handleInlineMicroEdging(nestedStep, result.basename, result.path);
|
||||||
if (edgeType && typeof edgeType === "string") {
|
if (edgeType && typeof edgeType === "string") {
|
||||||
// Use [[rel:type|link]] format
|
|
||||||
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
linkText = `[[rel:${edgeType}|${result.basename}]]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert link with rel: prefix if edge type was selected
|
|
||||||
// Extract inner part (without [[ and ]])
|
|
||||||
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
const innerLink = linkText.replace(/^\[\[/, "").replace(/\]\]$/, "");
|
||||||
insertWikilinkIntoTextarea(textarea, innerLink);
|
insertWikilinkIntoTextarea(textarea, innerLink);
|
||||||
}
|
}
|
||||||
).open();
|
).open();
|
||||||
|
},
|
||||||
|
async (app: App, textarea: HTMLTextAreaElement) => {
|
||||||
|
// Edge-Type-Selektor für Loop-Items
|
||||||
|
await this.handleEdgeTypeSelectorForTextarea(app, textarea, nestedStep, textEditorContainer);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild);
|
||||||
|
|
@ -2236,6 +2180,13 @@ export class InterviewWizardModal extends Modal {
|
||||||
insertWikilinkIntoTextarea(textarea, result.basename);
|
insertWikilinkIntoTextarea(textarea, result.basename);
|
||||||
}
|
}
|
||||||
).open();
|
).open();
|
||||||
|
},
|
||||||
|
async (app: App, textarea: HTMLTextAreaElement) => {
|
||||||
|
// Edge-Type-Selektor für LLM-Dialog (optional)
|
||||||
|
const currentStep = getCurrentStep(this.state);
|
||||||
|
if (currentStep) {
|
||||||
|
await this.handleEdgeTypeSelectorForTextarea(app, textarea, currentStep, llmEditorContainer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
llmEditorContainer.insertBefore(llmToolbar, llmEditorContainer.firstChild);
|
llmEditorContainer.insertBefore(llmToolbar, llmEditorContainer.firstChild);
|
||||||
|
|
@ -2380,6 +2331,60 @@ export class InterviewWizardModal extends Modal {
|
||||||
* Handle inline micro edging after entity picker selection.
|
* Handle inline micro edging after entity picker selection.
|
||||||
* Returns the selected edge type, or null if skipped/cancelled.
|
* Returns the selected edge type, or null if skipped/cancelled.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Handle edge type selector for textarea in interview mode.
|
||||||
|
*/
|
||||||
|
private async handleEdgeTypeSelectorForTextarea(
|
||||||
|
app: App,
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
step: InterviewStep,
|
||||||
|
containerEl: HTMLElement
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const content = textarea.value;
|
||||||
|
const context = detectEdgeSelectorContext(textarea, content);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
new Notice("Kontext konnte nicht erkannt werden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update callback to sync with state
|
||||||
|
const onUpdate = (newContent: string) => {
|
||||||
|
textarea.value = newContent;
|
||||||
|
// Update stored value
|
||||||
|
this.currentInputValues.set(step.key, newContent);
|
||||||
|
this.state.collectedData.set(step.key, newContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
// For interview mode, we don't have a file, so pass null
|
||||||
|
// We'll need to get source/target types differently
|
||||||
|
if (!this.settings) {
|
||||||
|
new Notice("Einstellungen nicht verfügbar");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await changeEdgeTypeForLinks(
|
||||||
|
app,
|
||||||
|
textarea,
|
||||||
|
null, // No file in interview mode
|
||||||
|
this.settings,
|
||||||
|
context,
|
||||||
|
this.plugin?.ensureGraphSchemaLoaded ? { ensureGraphSchemaLoaded: async () => {
|
||||||
|
if (this.plugin?.ensureGraphSchemaLoaded) {
|
||||||
|
return await this.plugin.ensureGraphSchemaLoaded();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} } : undefined,
|
||||||
|
onUpdate
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
new Notice(`Fehler beim Ändern des Edge-Types: ${msg}`);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async handleInlineMicroEdging(
|
private async handleInlineMicroEdging(
|
||||||
step: InterviewStep,
|
step: InterviewStep,
|
||||||
linkBasename: string,
|
linkBasename: string,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,9 @@ export class LinkPromptModal extends Modal {
|
||||||
|
|
||||||
// Link info
|
// Link info
|
||||||
const linkInfo = contentEl.createEl("div", { cls: "link-info" });
|
const linkInfo = contentEl.createEl("div", { cls: "link-info" });
|
||||||
linkInfo.createEl("h2", { text: `Link: [[${this.item.link}]]` });
|
// Use displayLink if available (preserves rel:type|link format), otherwise use link
|
||||||
|
const linkDisplayText = this.item.displayLink || this.item.link;
|
||||||
|
linkInfo.createEl("h2", { text: `Link: [[${linkDisplayText}]]` });
|
||||||
|
|
||||||
if (this.item.targetType) {
|
if (this.item.targetType) {
|
||||||
linkInfo.createEl("p", { text: `Target type: ${this.item.targetType}` });
|
linkInfo.createEl("p", { text: `Target type: ${this.item.targetType}` });
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,8 @@ function applyToTextarea(
|
||||||
export function createMarkdownToolbar(
|
export function createMarkdownToolbar(
|
||||||
textarea: HTMLTextAreaElement,
|
textarea: HTMLTextAreaElement,
|
||||||
onTogglePreview?: () => void,
|
onTogglePreview?: () => void,
|
||||||
onPickNote?: (app: any) => void
|
onPickNote?: (app: any) => void,
|
||||||
|
onChangeEdgeType?: (app: any, textarea: HTMLTextAreaElement) => Promise<void>
|
||||||
): HTMLElement {
|
): HTMLElement {
|
||||||
const toolbar = document.createElement("div");
|
const toolbar = document.createElement("div");
|
||||||
toolbar.className = "markdown-toolbar";
|
toolbar.className = "markdown-toolbar";
|
||||||
|
|
@ -490,17 +491,6 @@ export function createMarkdownToolbar(
|
||||||
});
|
});
|
||||||
toolbar.appendChild(codeBtn);
|
toolbar.appendChild(codeBtn);
|
||||||
|
|
||||||
// Link (WikiLink)
|
|
||||||
const linkBtn = createToolbarButton("🔗", "Wiki Link", () => {
|
|
||||||
const result = applyWikiLink(
|
|
||||||
textarea.value,
|
|
||||||
textarea.selectionStart,
|
|
||||||
textarea.selectionEnd
|
|
||||||
);
|
|
||||||
applyToTextarea(textarea, result);
|
|
||||||
});
|
|
||||||
toolbar.appendChild(linkBtn);
|
|
||||||
|
|
||||||
// Pick note button (if callback provided)
|
// Pick note button (if callback provided)
|
||||||
if (onPickNote) {
|
if (onPickNote) {
|
||||||
const pickNoteBtn = createToolbarButton("📎", "Pick note…", () => {
|
const pickNoteBtn = createToolbarButton("📎", "Pick note…", () => {
|
||||||
|
|
@ -514,6 +504,19 @@ export function createMarkdownToolbar(
|
||||||
});
|
});
|
||||||
toolbar.appendChild(pickNoteBtn);
|
toolbar.appendChild(pickNoteBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edge-Type-Selektor Button
|
||||||
|
if (onChangeEdgeType) {
|
||||||
|
const edgeTypeBtn = createToolbarButton("🔗", "Edge-Type ändern", async () => {
|
||||||
|
const app = (window as any).app;
|
||||||
|
if (app && onChangeEdgeType) {
|
||||||
|
await onChangeEdgeType(app, textarea);
|
||||||
|
} else {
|
||||||
|
console.warn("App not available for edge type selector");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
toolbar.appendChild(edgeTypeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
// Preview toggle
|
// Preview toggle
|
||||||
if (onTogglePreview) {
|
if (onTogglePreview) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user