- Added the `Interview_Config_Guide.md` for comprehensive instructions on creating interview profiles and utilizing various note types. - Updated `00_Dokumentations_Index.md` to include links to the new guide and improved navigation for WP-26 related resources. - Enhanced `06_Konfigurationsdateien_Referenz.md` with references to the new guide and clarified YAML structure for interview configurations. - Introduced `audit_geburtsdatei.md` for detailed analysis of section connections and edge types, highlighting critical issues and recommendations for improvement. - Improved renderer tests to ensure proper handling of section types and edge generation, aligning with the new WP-26 features.
1202 lines
40 KiB
TypeScript
1202 lines
40 KiB
TypeScript
/**
|
|
* Markdown renderer for interview profiles.
|
|
* Converts collected answers into markdown output based on profile configuration.
|
|
* WP-26: Erweitert um Section-Types, Block-IDs und automatische Edge-Vorschläge.
|
|
*/
|
|
|
|
import type { InterviewProfile, InterviewStep, CaptureTextStep, CaptureTextLineStep, CaptureFrontmatterStep, LoopStep, EntityPickerStep } from "./types";
|
|
import type { SectionInfo } from "./wizardState";
|
|
import type { GraphSchema } from "../mapping/graphSchema";
|
|
import type { Vocabulary } from "../vocab/Vocabulary";
|
|
import { slugify } from "./slugify";
|
|
import { getHints } from "../mapping/graphSchema";
|
|
|
|
export interface RenderAnswers {
|
|
collectedData: Map<string, unknown>;
|
|
loopContexts: Map<string, unknown[]>;
|
|
// WP-26: Optional - Section-Sequenz für automatische Edge-Vorschläge
|
|
sectionSequence?: SectionInfo[];
|
|
}
|
|
|
|
// WP-26: Render-Optionen für Section-Types und Edge-Vorschläge
|
|
export interface RenderOptions {
|
|
graphSchema?: GraphSchema | null;
|
|
vocabulary?: Vocabulary | null;
|
|
noteType?: string; // Fallback für effective_type
|
|
sectionEdgeTypes?: Map<string, Map<string, string>>; // fromBlockId -> (toBlockId -> edgeType) für manuell geänderte Edge-Types
|
|
}
|
|
|
|
/**
|
|
* Render profile answers to markdown string.
|
|
* Deterministic and testable.
|
|
* WP-26: Erweitert um Section-Types, Block-IDs und automatische Edge-Vorschläge.
|
|
*/
|
|
export function renderProfileToMarkdown(
|
|
profile: InterviewProfile,
|
|
answers: RenderAnswers,
|
|
options?: RenderOptions
|
|
): string {
|
|
// WP-26: Verwende übergebene Section-Sequenz oder erstelle neue
|
|
// Die Section-Sequenz wird während des interaktiven Wizard-Durchlaufs getrackt
|
|
const sectionSequence: SectionInfo[] = answers.sectionSequence ? [...answers.sectionSequence] : [];
|
|
const generatedBlockIds = new Map<string, SectionInfo>();
|
|
|
|
console.log(`[WP-26] renderProfileToMarkdown:`, {
|
|
sectionSequenceLength: sectionSequence.length,
|
|
sectionSequence: sectionSequence.map(s => ({
|
|
stepKey: s.stepKey,
|
|
blockId: s.blockId,
|
|
sectionType: s.sectionType,
|
|
})),
|
|
});
|
|
|
|
// WP-26: Wenn Section-Sequenz übergeben wurde, fülle generatedBlockIds aus der Sequenz
|
|
if (answers.sectionSequence && answers.sectionSequence.length > 0) {
|
|
for (const sectionInfo of answers.sectionSequence) {
|
|
if (sectionInfo.blockId) {
|
|
generatedBlockIds.set(sectionInfo.blockId, sectionInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
const context: RenderContext = {
|
|
profile,
|
|
sectionSequence,
|
|
generatedBlockIds,
|
|
options: options || {},
|
|
};
|
|
|
|
// WP-26: Erster Durchlauf: Rendere alle Steps und sammle gerenderte Sections
|
|
const renderedSections: Array<{ sectionInfo: SectionInfo; content: string }> = [];
|
|
const output: string[] = [];
|
|
|
|
for (const step of profile.steps) {
|
|
// WP-26: Debug-Log für Steps mit WP-26 Feldern
|
|
if (step.type === "capture_text" || step.type === "capture_text_line") {
|
|
const captureStep = step as CaptureTextStep | CaptureTextLineStep;
|
|
console.log(`[WP-26] Renderer: Step ${step.key} hat WP-26 Felder:`, {
|
|
section_type: captureStep.section_type,
|
|
block_id: captureStep.block_id,
|
|
generate_block_id: captureStep.generate_block_id,
|
|
references: captureStep.references,
|
|
});
|
|
}
|
|
|
|
const stepOutput = renderStep(step, answers, context);
|
|
|
|
if (stepOutput) {
|
|
// WP-26: Prüfe, ob diese Step eine Section mit Block-ID erzeugt hat
|
|
if (step.type === "capture_text" || step.type === "capture_text_line") {
|
|
const captureStep = step as CaptureTextStep | CaptureTextLineStep;
|
|
let blockId: string | null = null;
|
|
if (captureStep.block_id) {
|
|
blockId = captureStep.block_id;
|
|
} else if (captureStep.generate_block_id) {
|
|
blockId = slugify(captureStep.key);
|
|
}
|
|
|
|
if (blockId && generatedBlockIds.has(blockId)) {
|
|
const sectionInfo = generatedBlockIds.get(blockId)!;
|
|
renderedSections.push({ sectionInfo, content: stepOutput });
|
|
}
|
|
}
|
|
|
|
output.push(stepOutput);
|
|
}
|
|
}
|
|
|
|
// WP-26: Zweiter Durchlauf: Füge Backward-Edges zu den entsprechenden Sections hinzu
|
|
// Backward-Edges werden in der Ziel-Sektion (vorherige Section) eingefügt
|
|
// Wenn Section A -> Section B (Forward-Edge), dann wird Backward-Edge (B -> A) in Section A eingefügt
|
|
// Spezifikation: Zu jedem Forward-Link wird automatisch ein Rückwärts-Edge in der Ziel-Section generiert
|
|
const updatedOutput: string[] = [];
|
|
const backwardEdgesMap = new Map<string, string>(); // blockId der prevSection -> backwardEdges
|
|
|
|
// Sammle Backward-Edges für ALLE vorherigen Sections
|
|
// Für jede aktuelle Section generiere Backward-Edges zu allen vorherigen Sections
|
|
for (const rendered of renderedSections) {
|
|
const currentIndex = context.sectionSequence.findIndex(
|
|
s => s.blockId === rendered.sectionInfo.blockId && s.stepKey === rendered.sectionInfo.stepKey
|
|
);
|
|
|
|
if (currentIndex > 0 && rendered.sectionInfo.blockId) {
|
|
// ALLE vorherigen Sections (nicht nur die direkt vorherige)
|
|
const prevSections = context.sectionSequence.slice(0, currentIndex);
|
|
|
|
// Für jede vorherige Section generiere eine Backward-Edge
|
|
for (let prevIdx = 0; prevIdx < prevSections.length; prevIdx++) {
|
|
const prevSection = prevSections[prevIdx];
|
|
if (!prevSection || !prevSection.blockId) {
|
|
continue;
|
|
}
|
|
|
|
const backwardEdge = renderSingleBackwardEdge(
|
|
rendered.sectionInfo,
|
|
prevSection,
|
|
currentIndex,
|
|
prevIdx,
|
|
context
|
|
);
|
|
if (backwardEdge) {
|
|
const existing = backwardEdgesMap.get(prevSection.blockId) || "";
|
|
const combined = existing ? `${existing}\n${backwardEdge}` : backwardEdge;
|
|
backwardEdgesMap.set(prevSection.blockId, combined);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Füge Backward-Edges zu den entsprechenden Sections hinzu
|
|
for (let i = 0; i < output.length; i++) {
|
|
const currentOutput = output[i];
|
|
if (!currentOutput) {
|
|
updatedOutput.push("");
|
|
continue;
|
|
}
|
|
|
|
// Prüfe, ob diese Output eine Section mit Block-ID ist
|
|
let sectionInfo: SectionInfo | null = null;
|
|
for (const rendered of renderedSections) {
|
|
if (rendered.content === currentOutput) {
|
|
sectionInfo = rendered.sectionInfo;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (sectionInfo && sectionInfo.blockId) {
|
|
const backwardEdges = backwardEdgesMap.get(sectionInfo.blockId);
|
|
if (backwardEdges) {
|
|
// Füge Backward-Edges zum abstract wrapper hinzu
|
|
const updatedContent = addEdgesToAbstractWrapper(currentOutput, backwardEdges);
|
|
updatedOutput.push(updatedContent);
|
|
} else {
|
|
updatedOutput.push(currentOutput);
|
|
}
|
|
} else {
|
|
updatedOutput.push(currentOutput);
|
|
}
|
|
}
|
|
|
|
return updatedOutput.join("\n\n").trim();
|
|
}
|
|
|
|
// WP-26: Render-Kontext für Section-Types und Block-IDs
|
|
interface RenderContext {
|
|
profile: InterviewProfile;
|
|
sectionSequence: SectionInfo[];
|
|
generatedBlockIds: Map<string, SectionInfo>;
|
|
options: RenderOptions;
|
|
}
|
|
|
|
/**
|
|
* Render a single step to markdown.
|
|
* WP-26: Erweitert um RenderContext für Section-Types und Block-IDs.
|
|
*/
|
|
function renderStep(
|
|
step: InterviewStep,
|
|
answers: RenderAnswers,
|
|
context: RenderContext
|
|
): string | null {
|
|
switch (step.type) {
|
|
case "capture_text":
|
|
return renderCaptureText(step, answers, context);
|
|
case "capture_text_line":
|
|
return renderCaptureTextLine(step, answers, context);
|
|
case "capture_frontmatter":
|
|
// Frontmatter is handled separately, skip here
|
|
return null;
|
|
case "loop":
|
|
return renderLoop(step, answers, context);
|
|
case "entity_picker":
|
|
return renderEntityPicker(step, answers);
|
|
case "instruction":
|
|
case "llm_dialog":
|
|
case "review":
|
|
// These don't produce markdown output
|
|
return null;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render capture_text step.
|
|
* WP-26: Erweitert um Section-Types, Block-IDs und Edge-Vorschläge.
|
|
*/
|
|
function renderCaptureText(
|
|
step: CaptureTextStep,
|
|
answers: RenderAnswers,
|
|
context: RenderContext
|
|
): string | null {
|
|
const value = answers.collectedData.get(step.key);
|
|
if (!value || String(value).trim() === "") {
|
|
return null;
|
|
}
|
|
|
|
const text = String(value);
|
|
|
|
console.log(`[WP-26] renderCaptureText für Step ${step.key}:`, {
|
|
hasBlockId: !!step.block_id,
|
|
blockId: step.block_id,
|
|
hasGenerateBlockId: !!step.generate_block_id,
|
|
generateBlockId: step.generate_block_id,
|
|
hasSectionType: !!step.section_type,
|
|
sectionType: step.section_type,
|
|
hasSection: !!step.section,
|
|
section: step.section,
|
|
});
|
|
|
|
// WP-26: Block-ID-Generierung
|
|
let blockId: string | null = null;
|
|
if (step.block_id) {
|
|
blockId = step.block_id;
|
|
} else if (step.generate_block_id) {
|
|
blockId = slugify(step.key);
|
|
}
|
|
|
|
console.log(`[WP-26] Generierte Block-ID für Step ${step.key}:`, blockId);
|
|
|
|
// WP-26: Heading mit Block-ID erweitern
|
|
let heading = step.section || "";
|
|
if (heading && blockId) {
|
|
// Füge Block-ID zu Heading hinzu: "## Heading" -> "## Heading ^block-id"
|
|
heading = heading.replace(/^(\s*#{1,6}\s+)(.+)$/, `$1$2 ^${blockId}`);
|
|
}
|
|
|
|
// WP-26: Section-Type-Callout generieren (direkt nach Heading)
|
|
const sectionTypeCallout = step.section_type
|
|
? `> [!section] ${step.section_type}`
|
|
: "";
|
|
|
|
// WP-26: Section-Info für Tracking
|
|
const sectionInfo: SectionInfo = {
|
|
stepKey: step.key,
|
|
sectionType: step.section_type || null,
|
|
heading: heading || step.key,
|
|
blockId: blockId,
|
|
noteType: context.options.noteType || context.profile.note_type,
|
|
};
|
|
|
|
// WP-26: Block-ID tracken
|
|
if (blockId) {
|
|
context.generatedBlockIds.set(blockId, sectionInfo);
|
|
|
|
// WP-26: Auch Basis-Block-ID tracken (für Referenzen ohne Index bei Loop-Items)
|
|
// Z.B. "action_heading-1" wird auch als "action_heading" getrackt
|
|
const baseBlockId = blockId.replace(/-\d+$/, "");
|
|
if (baseBlockId !== blockId && !context.generatedBlockIds.has(baseBlockId)) {
|
|
context.generatedBlockIds.set(baseBlockId, sectionInfo);
|
|
}
|
|
}
|
|
|
|
// WP-26: Section-Sequenz aktualisieren (nur wenn Section vorhanden)
|
|
if (step.section) {
|
|
context.sectionSequence.push(sectionInfo);
|
|
}
|
|
|
|
// WP-26: Referenzen generieren (Forward-Edges zu anderen Sections)
|
|
const references = renderReferences(step.references || [], context, sectionInfo);
|
|
|
|
// WP-26: Automatische Forward-Edges generieren (prevSection -> currentSection)
|
|
const forwardEdges = renderForwardEdges(sectionInfo, context);
|
|
|
|
// WP-26: Automatische Backward-Edges generieren (currentSection -> nachfolgende Sections)
|
|
// Diese werden später in einem zweiten Durchlauf hinzugefügt
|
|
const backwardEdges = ""; // Wird später hinzugefügt
|
|
|
|
// Use template if provided
|
|
if (step.output?.template) {
|
|
const templateText = renderTemplate(step.output.template, {
|
|
text,
|
|
field: step.key,
|
|
value: text,
|
|
});
|
|
|
|
// WP-26: Template mit Heading, Section-Type und Edges kombinieren
|
|
return combineSectionParts(heading, sectionTypeCallout, templateText, references, forwardEdges, backwardEdges);
|
|
}
|
|
|
|
// Default: use section if provided, otherwise just the text
|
|
if (step.section) {
|
|
return combineSectionParts(heading, sectionTypeCallout, text, references, forwardEdges, backwardEdges);
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Render capture_text_line step.
|
|
* WP-26: Erweitert um Section-Types, Block-IDs und Edge-Vorschläge.
|
|
*/
|
|
function renderCaptureTextLine(
|
|
step: CaptureTextLineStep,
|
|
answers: RenderAnswers,
|
|
context: RenderContext
|
|
): string | null {
|
|
const value = answers.collectedData.get(step.key);
|
|
if (!value || String(value).trim() === "") {
|
|
return null;
|
|
}
|
|
|
|
const text = String(value);
|
|
|
|
// Get heading level if configured
|
|
const headingLevelKey = `${step.key}_heading_level`;
|
|
const headingLevel = answers.collectedData.get(headingLevelKey);
|
|
let headingPrefix = "";
|
|
if (step.heading_level?.enabled) {
|
|
if (typeof headingLevel === "number") {
|
|
const level = Math.max(1, Math.min(6, headingLevel));
|
|
headingPrefix = "#".repeat(level) + " ";
|
|
} else if (step.heading_level.default) {
|
|
// Fallback to default if not set
|
|
const level = Math.max(1, Math.min(6, step.heading_level.default));
|
|
headingPrefix = "#".repeat(level) + " ";
|
|
}
|
|
}
|
|
|
|
// WP-26: Block-ID-Generierung
|
|
// Für Loop-Items kann die Block-ID bereits nummeriert sein (z.B. "action_heading-1")
|
|
let blockId: string | null = null;
|
|
if (step.block_id) {
|
|
blockId = step.block_id;
|
|
} else if (step.generate_block_id) {
|
|
blockId = slugify(step.key);
|
|
}
|
|
|
|
// WP-26: Heading mit Block-ID erweitern (wenn heading_prefix vorhanden)
|
|
let heading = "";
|
|
if (headingPrefix) {
|
|
heading = headingPrefix + text;
|
|
if (blockId) {
|
|
// Füge Block-ID zu Heading hinzu
|
|
heading = heading.replace(/^(\s*#{1,6}\s+)(.+)$/, `$1$2 ^${blockId}`);
|
|
}
|
|
}
|
|
|
|
// WP-26: Section-Type-Callout generieren (direkt nach Heading)
|
|
const sectionTypeCallout = step.section_type
|
|
? `> [!section] ${step.section_type}`
|
|
: "";
|
|
|
|
// WP-26: Section-Info für Tracking (nur wenn Heading vorhanden)
|
|
let sectionInfo: SectionInfo | null = null;
|
|
if (headingPrefix && blockId) {
|
|
sectionInfo = {
|
|
stepKey: step.key,
|
|
sectionType: step.section_type || null,
|
|
heading: heading || text,
|
|
blockId: blockId,
|
|
noteType: context.options.noteType || context.profile.note_type,
|
|
};
|
|
|
|
// WP-26: Block-ID tracken (auch wenn nummeriert für Loop-Items)
|
|
context.generatedBlockIds.set(blockId, sectionInfo);
|
|
|
|
// WP-26: Auch Basis-Block-ID tracken (für Referenzen ohne Index)
|
|
// Z.B. "action_heading-1" wird auch als "action_heading" getrackt
|
|
const baseBlockId = blockId.replace(/-\d+$/, "");
|
|
if (baseBlockId !== blockId && !context.generatedBlockIds.has(baseBlockId)) {
|
|
context.generatedBlockIds.set(baseBlockId, sectionInfo);
|
|
}
|
|
|
|
// WP-26: Section-Sequenz aktualisieren
|
|
context.sectionSequence.push(sectionInfo);
|
|
}
|
|
|
|
// WP-26: Referenzen generieren (Forward-Edges zu anderen Sections)
|
|
const references = sectionInfo
|
|
? renderReferences(step.references || [], context, sectionInfo)
|
|
: "";
|
|
|
|
// WP-26: Automatische Forward-Edges generieren (prevSection -> currentSection)
|
|
const forwardEdges = sectionInfo
|
|
? renderForwardEdges(sectionInfo, context)
|
|
: "";
|
|
|
|
// Use template if provided
|
|
if (step.output?.template) {
|
|
const templateText = renderTemplate(step.output.template, {
|
|
text: headingPrefix + text,
|
|
field: step.key,
|
|
value: headingPrefix + text,
|
|
heading_level: headingLevel ? String(headingLevel) : "",
|
|
});
|
|
|
|
// WP-26: Template mit Heading, Section-Type und Edges kombinieren (wenn Heading vorhanden)
|
|
if (headingPrefix) {
|
|
// Backward-Edges werden später im zweiten Durchlauf hinzugefügt
|
|
return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, "");
|
|
}
|
|
|
|
return templateText;
|
|
}
|
|
|
|
// WP-26: Wenn Heading vorhanden, mit Section-Type und Edges kombinieren
|
|
if (headingPrefix) {
|
|
// Backward-Edges werden später im zweiten Durchlauf hinzugefügt
|
|
return combineSectionParts(heading, sectionTypeCallout, "", references, forwardEdges, "");
|
|
}
|
|
|
|
// Default: text with heading prefix if configured
|
|
return headingPrefix + text;
|
|
}
|
|
|
|
/**
|
|
* Render entity_picker step.
|
|
*/
|
|
function renderEntityPicker(step: EntityPickerStep, answers: RenderAnswers): string | null {
|
|
const basename = answers.collectedData.get(step.key);
|
|
if (!basename || typeof basename !== "string" || !basename.trim()) {
|
|
return null;
|
|
}
|
|
|
|
// Check if there's a label field
|
|
let label: string | undefined;
|
|
if (step.labelField) {
|
|
const labelValue = answers.collectedData.get(step.labelField);
|
|
if (labelValue && typeof labelValue === "string" && labelValue.trim()) {
|
|
label = labelValue.trim();
|
|
}
|
|
}
|
|
|
|
// Render wikilink
|
|
if (label) {
|
|
return `[[${basename}|${label}]]`;
|
|
} else {
|
|
return `[[${basename}]]`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render capture_frontmatter step.
|
|
* Note: This is typically handled separately, but included for completeness.
|
|
*/
|
|
function renderCaptureFrontmatter(step: CaptureFrontmatterStep, answers: RenderAnswers): string | null {
|
|
const value = answers.collectedData.get(step.key);
|
|
if (value === undefined || value === null || String(value).trim() === "") {
|
|
return null;
|
|
}
|
|
|
|
// Use template if provided
|
|
if (step.output?.template) {
|
|
return renderTemplate(step.output.template, {
|
|
text: String(value),
|
|
field: step.field,
|
|
value: String(value),
|
|
});
|
|
}
|
|
|
|
// Default: no markdown output (frontmatter is handled separately)
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Render loop step (recursive, supports arbitrary nesting depth).
|
|
* WP-26: Erweitert um RenderContext für Section-Types und Block-IDs.
|
|
*/
|
|
function renderLoop(step: LoopStep, answers: RenderAnswers, context: RenderContext): string | null {
|
|
return renderLoopRecursive(step, answers, context, 0);
|
|
}
|
|
|
|
/**
|
|
* Recursive helper to render loop items with nested steps.
|
|
* Supports arbitrary nesting depth (no hard limit, but practical limits apply due to stack size).
|
|
* WP-26: Erweitert um RenderContext für Section-Types und Block-IDs.
|
|
*/
|
|
function renderLoopRecursive(
|
|
step: LoopStep,
|
|
answers: RenderAnswers,
|
|
context: RenderContext,
|
|
depth: number
|
|
): string | null {
|
|
// Safety check: prevent infinite recursion (practical limit: 100 levels)
|
|
if (depth > 100) {
|
|
console.warn(`Loop nesting depth ${depth} exceeds practical limit, stopping recursion`);
|
|
return null;
|
|
}
|
|
|
|
const items = answers.loopContexts.get(step.key);
|
|
if (!items || items.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const itemOutputs: string[] = [];
|
|
|
|
// WP-26: Tracke Item-Index für eindeutige Block-IDs
|
|
let itemIndex = 0;
|
|
|
|
for (const item of items) {
|
|
if (!item || typeof item !== "object") {
|
|
continue;
|
|
}
|
|
|
|
itemIndex++;
|
|
const itemParts: string[] = [];
|
|
|
|
// Render each nested step for this item (recursively)
|
|
for (const nestedStep of step.items) {
|
|
const fieldValue = (item as Record<string, unknown>)[nestedStep.key];
|
|
|
|
if (nestedStep.type === "capture_text") {
|
|
if (fieldValue && String(fieldValue).trim()) {
|
|
const text = String(fieldValue);
|
|
const captureStep = nestedStep as CaptureTextStep;
|
|
|
|
// WP-26: Erstelle temporären Step mit nummerierter Block-ID für Loop-Items
|
|
const loopStepWithBlockId: CaptureTextStep = {
|
|
...captureStep,
|
|
block_id: captureStep.generate_block_id
|
|
? `${slugify(captureStep.key)}-${itemIndex}`
|
|
: captureStep.block_id,
|
|
};
|
|
|
|
// WP-26: Verwende erweiterte renderCaptureText Funktion
|
|
const rendered = renderCaptureText(loopStepWithBlockId, {
|
|
collectedData: new Map([[nestedStep.key, fieldValue]]),
|
|
loopContexts: answers.loopContexts,
|
|
}, context);
|
|
|
|
if (rendered) {
|
|
itemParts.push(rendered);
|
|
}
|
|
}
|
|
} else if (nestedStep.type === "capture_text_line") {
|
|
if (fieldValue && String(fieldValue).trim()) {
|
|
const captureStep = nestedStep as CaptureTextLineStep;
|
|
|
|
// WP-26: Erstelle temporäres collectedData mit allen Item-Daten
|
|
const itemData = new Map<string, unknown>();
|
|
itemData.set(nestedStep.key, fieldValue);
|
|
|
|
// Füge heading_level hinzu falls vorhanden
|
|
const headingLevelKey = `${nestedStep.key}_heading_level`;
|
|
const headingLevel = (item as Record<string, unknown>)[headingLevelKey];
|
|
if (headingLevel !== undefined) {
|
|
itemData.set(headingLevelKey, headingLevel);
|
|
}
|
|
|
|
// WP-26: Erstelle temporären Step mit nummerierter Block-ID für Loop-Items
|
|
const loopStepWithBlockId: CaptureTextLineStep = {
|
|
...captureStep,
|
|
block_id: captureStep.generate_block_id
|
|
? `${slugify(captureStep.key)}-${itemIndex}`
|
|
: captureStep.block_id,
|
|
};
|
|
|
|
// WP-26: Verwende erweiterte renderCaptureTextLine Funktion
|
|
const rendered = renderCaptureTextLine(loopStepWithBlockId, {
|
|
collectedData: itemData,
|
|
loopContexts: answers.loopContexts,
|
|
}, context);
|
|
|
|
if (rendered) {
|
|
itemParts.push(rendered);
|
|
}
|
|
}
|
|
} else if (nestedStep.type === "capture_frontmatter") {
|
|
if (fieldValue && String(fieldValue).trim()) {
|
|
const captureStep = nestedStep as CaptureFrontmatterStep;
|
|
|
|
// Use template if provided
|
|
if (captureStep.output?.template) {
|
|
itemParts.push(renderTemplate(captureStep.output.template, {
|
|
text: String(fieldValue),
|
|
field: captureStep.field,
|
|
value: String(fieldValue),
|
|
}));
|
|
}
|
|
}
|
|
} else if (nestedStep.type === "loop") {
|
|
// Recursively handle nested loop: fieldValue should be an array of items
|
|
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
|
|
const nestedLoopStep = nestedStep as LoopStep;
|
|
|
|
// Create a temporary answers object for the nested loop
|
|
// The nested loop items are stored in fieldValue, so we need to create
|
|
// a temporary loopContexts map for the nested loop
|
|
const nestedAnswers: RenderAnswers = {
|
|
collectedData: answers.collectedData,
|
|
loopContexts: new Map(answers.loopContexts),
|
|
};
|
|
nestedAnswers.loopContexts.set(nestedLoopStep.key, fieldValue);
|
|
|
|
// Recursively render the nested loop
|
|
const nestedOutput = renderLoopRecursive(nestedLoopStep, nestedAnswers, context, depth + 1);
|
|
if (nestedOutput) {
|
|
itemParts.push(nestedOutput);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (itemParts.length > 0) {
|
|
itemOutputs.push(itemParts.join("\n\n"));
|
|
}
|
|
}
|
|
|
|
if (itemOutputs.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Check if first nested step has a section header (for backwards compatibility)
|
|
const firstNestedStep = step.items[0];
|
|
let sectionHeader: string | null = null;
|
|
if (firstNestedStep && firstNestedStep.type === "capture_text" && firstNestedStep.section) {
|
|
sectionHeader = firstNestedStep.section;
|
|
}
|
|
|
|
// Join items with configured separator or default newline
|
|
const joinStr = step.output?.join || "\n\n";
|
|
const loopContent = itemOutputs.join(joinStr);
|
|
|
|
// Prepend section header if available
|
|
if (sectionHeader) {
|
|
return `${sectionHeader}\n\n${loopContent}`;
|
|
}
|
|
|
|
return loopContent;
|
|
}
|
|
|
|
/**
|
|
* Render template string with token replacement.
|
|
* Tokens: {text}, {field}, {value}
|
|
*/
|
|
function renderTemplate(template: string, tokens: { text: string; field: string; value: string; heading_level?: string }): string {
|
|
let result = template
|
|
.replace(/\{text\}/g, tokens.text)
|
|
.replace(/\{field\}/g, tokens.field)
|
|
.replace(/\{value\}/g, tokens.value);
|
|
|
|
if (tokens.heading_level !== undefined) {
|
|
result = result.replace(/\{heading_level\}/g, tokens.heading_level);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// WP-26: Helper-Funktionen für Section-Types und Block-IDs
|
|
|
|
/**
|
|
* Kombiniert Heading, Section-Type-Callout, Content und Edges zu einer Section.
|
|
* Formatierungsregel: Section-Type direkt nach Heading, Edges im abstract wrapper am Ende.
|
|
*/
|
|
function combineSectionParts(
|
|
heading: string,
|
|
sectionTypeCallout: string,
|
|
content: string,
|
|
references: string,
|
|
autoEdges: string,
|
|
backwardEdges: string = ""
|
|
): string {
|
|
const parts: string[] = [];
|
|
|
|
if (heading) {
|
|
parts.push(heading);
|
|
}
|
|
|
|
if (sectionTypeCallout) {
|
|
parts.push(sectionTypeCallout);
|
|
}
|
|
|
|
if (content) {
|
|
parts.push(content);
|
|
}
|
|
|
|
// WP-26: Alle Edges im abstract wrapper am Ende der Sektion
|
|
const allEdges = [references, autoEdges, backwardEdges].filter(e => e.trim());
|
|
if (allEdges.length > 0) {
|
|
const abstractWrapper = buildAbstractWrapper(allEdges.join("\n"));
|
|
if (abstractWrapper) {
|
|
parts.push(abstractWrapper);
|
|
}
|
|
}
|
|
|
|
return parts.join("\n\n");
|
|
}
|
|
|
|
/**
|
|
* Erstellt einen abstract wrapper für Edges.
|
|
* Format: > [!abstract]- 🕸️ Semantic Mapping\n>> [!edge] type\n>> [[link]]
|
|
*/
|
|
function buildAbstractWrapper(edgesContent: string): string | null {
|
|
if (!edgesContent.trim()) {
|
|
return null;
|
|
}
|
|
|
|
// Parse edges aus dem Content
|
|
const edgeLines = edgesContent.split("\n").filter(line => line.trim());
|
|
const edgeGroups = new Map<string, string[]>();
|
|
|
|
let currentEdgeType: string | null = null;
|
|
for (const line of edgeLines) {
|
|
// Prüfe auf Edge-Type: > [!edge] type
|
|
const edgeMatch = line.match(/^>\s*\[!edge\]\s+(.+)$/);
|
|
if (edgeMatch && edgeMatch[1]) {
|
|
currentEdgeType = edgeMatch[1].trim();
|
|
if (!edgeGroups.has(currentEdgeType)) {
|
|
edgeGroups.set(currentEdgeType, []);
|
|
}
|
|
} else {
|
|
// Prüfe auf Link: > [[#^block-id]]
|
|
const linkMatch = line.match(/^>\s*(\[\[#\^.+?\]\])$/);
|
|
if (linkMatch && linkMatch[1] && currentEdgeType) {
|
|
const links = edgeGroups.get(currentEdgeType) || [];
|
|
links.push(linkMatch[1]);
|
|
edgeGroups.set(currentEdgeType, links);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (edgeGroups.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Build abstract wrapper
|
|
const wrapperLines: string[] = [];
|
|
wrapperLines.push(`> [!abstract]- 🕸️ Semantic Mapping`);
|
|
|
|
// Sort edge types alphabetically
|
|
const sortedEdgeTypes = Array.from(edgeGroups.keys()).sort();
|
|
|
|
for (let i = 0; i < sortedEdgeTypes.length; i++) {
|
|
const edgeType = sortedEdgeTypes[i];
|
|
if (!edgeType) continue;
|
|
const links = edgeGroups.get(edgeType) || [];
|
|
|
|
if (links.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Add separator between groups (except before first)
|
|
if (i > 0) {
|
|
wrapperLines.push(">");
|
|
}
|
|
|
|
// Edge header
|
|
wrapperLines.push(`>> [!edge] ${edgeType}`);
|
|
|
|
// Links (sorted for determinism)
|
|
const sortedLinks = [...links].sort();
|
|
for (const link of sortedLinks) {
|
|
wrapperLines.push(`>> ${link}`);
|
|
}
|
|
}
|
|
|
|
// Add block-id marker
|
|
wrapperLines.push(`> ^map-${Date.now()}`);
|
|
|
|
return wrapperLines.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Generiert Edge-Callouts für explizite Referenzen.
|
|
* Format: > [!edge] <edge_type>\n> [[#^block-id]]
|
|
*
|
|
* WP-26: Unterstützt auch Basis-Block-IDs für Loop-Items (z.B. "action_heading" wird zu "action_heading-1", "action_heading-2" aufgelöst)
|
|
*/
|
|
function renderReferences(
|
|
references: Array<{ block_id: string; edge_type?: string }>,
|
|
context: RenderContext,
|
|
currentSection: SectionInfo
|
|
): string {
|
|
if (!references || references.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
const edgeCallouts: string[] = [];
|
|
|
|
for (const ref of references) {
|
|
// WP-26: Auflösung der Block-ID (für Loop-Items mit Basis-Block-ID)
|
|
let resolvedBlockId = ref.block_id;
|
|
|
|
// Prüfe, ob Block-ID direkt existiert
|
|
if (!context.generatedBlockIds.has(ref.block_id)) {
|
|
// Versuche, Block-ID mit Index aufzulösen (für Loop-Items)
|
|
// Suche nach Block-IDs, die mit der Basis-Block-ID beginnen
|
|
let foundBlockId: string | null = null;
|
|
for (const [blockId] of context.generatedBlockIds.entries()) {
|
|
// Prüfe, ob Block-ID mit Basis-Block-ID beginnt (z.B. "action_heading-1", "action_heading-2")
|
|
if (blockId.startsWith(`${ref.block_id}-`)) {
|
|
// Verwende die neueste gefundene Block-ID (höchster Index)
|
|
if (!foundBlockId || blockId > foundBlockId) {
|
|
foundBlockId = blockId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundBlockId) {
|
|
resolvedBlockId = foundBlockId;
|
|
} else {
|
|
console.warn(`[WP-26] Block-ID "${ref.block_id}" nicht gefunden für Referenz`);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// WP-26: Prüfe auf Selbstreferenz
|
|
if (resolvedBlockId === currentSection.blockId) {
|
|
console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${resolvedBlockId}" zeigt auf sich selbst`);
|
|
continue;
|
|
}
|
|
|
|
const edgeType = ref.edge_type || "related_to";
|
|
edgeCallouts.push(`> [!edge] ${edgeType}`);
|
|
edgeCallouts.push(`> [[#^${resolvedBlockId}]]`);
|
|
}
|
|
|
|
return edgeCallouts.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Ermittelt den effektiven Section-Type basierend auf Heading-Level.
|
|
* Wenn eine Section keinen expliziten Type hat, wird der Type der vorherigen Section
|
|
* auf dem gleichen oder höheren Level verwendet, sonst Note-Type.
|
|
*/
|
|
function getEffectiveSectionType(
|
|
section: SectionInfo,
|
|
index: number,
|
|
sectionSequence: SectionInfo[]
|
|
): string {
|
|
// Wenn expliziter Section-Type vorhanden, verwende diesen
|
|
if (section.sectionType) {
|
|
return section.sectionType;
|
|
}
|
|
|
|
// Extrahiere Heading-Level aus der Überschrift
|
|
const headingMatch = section.heading.match(/^(#{1,6})\s+/);
|
|
const currentLevel = headingMatch ? headingMatch[1]?.length || 0 : 0;
|
|
|
|
// Suche rückwärts nach der letzten Section mit explizitem Type auf gleichem oder höherem Level
|
|
for (let i = index - 1; i >= 0; i--) {
|
|
const prevSection = sectionSequence[i];
|
|
if (!prevSection) continue;
|
|
|
|
const prevHeadingMatch = prevSection.heading.match(/^(#{1,6})\s+/);
|
|
const prevLevel = prevHeadingMatch ? prevHeadingMatch[1]?.length || 0 : 0;
|
|
|
|
// Wenn vorherige Section auf gleichem oder höherem Level einen expliziten Type hat
|
|
if (prevLevel <= currentLevel && prevSection.sectionType) {
|
|
return prevSection.sectionType;
|
|
}
|
|
|
|
// Wenn wir auf ein höheres Level stoßen, stoppe die Suche
|
|
if (prevLevel < currentLevel) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fallback: Note-Type
|
|
return section.noteType;
|
|
}
|
|
|
|
/**
|
|
* Generiert automatische Forward-Edges zwischen Sections.
|
|
* Forward-Edge: prevSection -> currentSection (in aktueller Section)
|
|
* Generiert Edges zu ALLEN vorherigen Sections (nicht nur zur direkt vorherigen).
|
|
* Verwendet graph_schema.md für typische Edge-Types.
|
|
*
|
|
* Spezifikation: Alle Sections haben Edges zu allen anderen Sections.
|
|
* Edge-Types werden aus graph_schema.md abgeleitet.
|
|
*/
|
|
function renderForwardEdges(
|
|
currentSection: SectionInfo,
|
|
context: RenderContext
|
|
): string {
|
|
// Nur wenn aktuelle Section eine Block-ID hat
|
|
if (!currentSection.blockId) {
|
|
return "";
|
|
}
|
|
|
|
// Finde Index der aktuellen Section in der Sequenz
|
|
const currentIndex = context.sectionSequence.findIndex(
|
|
s => s.blockId === currentSection.blockId && s.stepKey === currentSection.stepKey
|
|
);
|
|
|
|
if (currentIndex === -1 || currentIndex === 0) {
|
|
return ""; // Keine vorherigen Sections
|
|
}
|
|
|
|
// ALLE vorherigen Sections (nicht nur die direkt vorherige)
|
|
const prevSections = context.sectionSequence.slice(0, currentIndex);
|
|
|
|
const edgeCallouts: string[] = [];
|
|
const { graphSchema } = context.options;
|
|
|
|
for (let prevIdx = 0; prevIdx < prevSections.length; prevIdx++) {
|
|
const prevSection = prevSections[prevIdx];
|
|
// Nur wenn vorherige Section auch eine Block-ID hat
|
|
if (!prevSection || !prevSection.blockId) {
|
|
continue;
|
|
}
|
|
|
|
// WP-26: Prüfe auf Selbstreferenz
|
|
if (prevSection.blockId === currentSection.blockId) {
|
|
console.warn(`[WP-26] Selbstreferenz verhindert: Block-ID "${prevSection.blockId}" zeigt auf sich selbst`);
|
|
continue;
|
|
}
|
|
|
|
// WP-26: Auflösung der Block-ID für Loop-Items
|
|
// Prüfe zuerst, ob die Block-ID direkt existiert
|
|
let resolvedPrevBlockId = prevSection.blockId;
|
|
if (!context.generatedBlockIds.has(prevSection.blockId)) {
|
|
// Versuche, Block-ID mit Index aufzulösen (für Loop-Items)
|
|
// Suche nach Block-IDs, die mit der Basis-Block-ID beginnen
|
|
let foundBlockId: string | null = null;
|
|
for (const [blockId] of context.generatedBlockIds.entries()) {
|
|
if (blockId.startsWith(`${prevSection.blockId}-`)) {
|
|
// Verwende die neueste gefundene Block-ID (höchster Index)
|
|
if (!foundBlockId || blockId > foundBlockId) {
|
|
foundBlockId = blockId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundBlockId) {
|
|
resolvedPrevBlockId = foundBlockId;
|
|
}
|
|
}
|
|
|
|
// WP-26: Prüfe zuerst, ob ein manuell geänderter Edge-Type vorhanden ist
|
|
let forwardEdgeType: string | null = null;
|
|
if (context.options.sectionEdgeTypes) {
|
|
const fromBlockId = prevSection.blockId;
|
|
const toBlockId = currentSection.blockId;
|
|
if (fromBlockId && toBlockId) {
|
|
const fromMap = context.options.sectionEdgeTypes.get(fromBlockId);
|
|
if (fromMap) {
|
|
forwardEdgeType = fromMap.get(toBlockId) || null;
|
|
if (forwardEdgeType) {
|
|
console.log(`[WP-26] Verwende manuell geänderten Edge-Type: ${fromBlockId} -> ${toBlockId} = ${forwardEdgeType}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Falls kein manuell geänderter Edge-Type vorhanden, verwende graph_schema
|
|
if (!forwardEdgeType) {
|
|
// WP-26: Ermittle effective_types mit Heading-Level-basierter Fallback-Logik
|
|
const prevType = getEffectiveSectionType(prevSection, prevIdx, context.sectionSequence);
|
|
const currentType = getEffectiveSectionType(currentSection, currentIndex, context.sectionSequence);
|
|
|
|
// Lookup in graph_schema.md: prevType -> currentType
|
|
if (graphSchema && prevType && currentType) {
|
|
const hints = getHints(graphSchema, prevType, currentType);
|
|
if (hints.typical.length > 0) {
|
|
forwardEdgeType = hints.typical[0] || null;
|
|
} else {
|
|
console.debug(`[WP-26] Keine typischen Edges gefunden für ${prevType} -> ${currentType}, verwende Fallback`);
|
|
}
|
|
}
|
|
|
|
// Fallback: related_to für experience -> experience, references für andere
|
|
if (!forwardEdgeType) {
|
|
if (prevType === currentType && prevType === "experience") {
|
|
forwardEdgeType = "related_to";
|
|
} else {
|
|
forwardEdgeType = "references";
|
|
}
|
|
}
|
|
}
|
|
|
|
// WP-26: Forward-Edge generieren (prevSection -> currentSection)
|
|
// Diese Edge wird in der aktuellen Section eingefügt
|
|
edgeCallouts.push(`> [!edge] ${forwardEdgeType}`);
|
|
edgeCallouts.push(`> [[#^${resolvedPrevBlockId}]]`);
|
|
}
|
|
|
|
return edgeCallouts.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Generiert eine einzelne Backward-Edge zwischen zwei Sections.
|
|
* Backward-Edge: currentSection -> prevSection (wird in prevSection eingefügt)
|
|
*
|
|
* Wenn Section A -> Section B (Forward-Edge), dann wird Backward-Edge (B -> A) in Section A eingefügt.
|
|
*/
|
|
function renderSingleBackwardEdge(
|
|
currentSection: SectionInfo,
|
|
prevSection: SectionInfo,
|
|
currentIndex: number,
|
|
prevIndex: number,
|
|
context: RenderContext
|
|
): string {
|
|
// Nur wenn beide Sections Block-IDs haben
|
|
if (!currentSection.blockId || !prevSection.blockId) {
|
|
return "";
|
|
}
|
|
|
|
// WP-26: Prüfe auf Selbstreferenz
|
|
if (prevSection.blockId === currentSection.blockId) {
|
|
return "";
|
|
}
|
|
|
|
const { graphSchema, vocabulary } = context.options;
|
|
|
|
// WP-26: Prüfe zuerst, ob ein manuell geänderter Edge-Type vorhanden ist
|
|
let forwardEdgeType: string | null = null;
|
|
if (context.options.sectionEdgeTypes) {
|
|
const fromBlockId = prevSection.blockId;
|
|
const toBlockId = currentSection.blockId;
|
|
if (fromBlockId && toBlockId) {
|
|
const fromMap = context.options.sectionEdgeTypes.get(fromBlockId);
|
|
if (fromMap) {
|
|
forwardEdgeType = fromMap.get(toBlockId) || null;
|
|
if (forwardEdgeType) {
|
|
console.log(`[WP-26] Verwende manuell geänderten Edge-Type für Backward-Edge: ${fromBlockId} -> ${toBlockId} = ${forwardEdgeType}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Falls kein manuell geänderter Edge-Type vorhanden, verwende graph_schema
|
|
if (!forwardEdgeType) {
|
|
// WP-26: Ermittle effective_types mit Heading-Level-basierter Fallback-Logik
|
|
const prevType = getEffectiveSectionType(prevSection, prevIndex, context.sectionSequence);
|
|
const currentType = getEffectiveSectionType(currentSection, currentIndex, context.sectionSequence);
|
|
|
|
// Lookup in graph_schema.md: prevType -> currentType (Forward-Edge-Type)
|
|
if (graphSchema && prevType && currentType) {
|
|
const hints = getHints(graphSchema, prevType, currentType);
|
|
if (hints.typical.length > 0) {
|
|
forwardEdgeType = hints.typical[0] || null;
|
|
}
|
|
}
|
|
|
|
// Fallback: related_to
|
|
if (!forwardEdgeType) {
|
|
forwardEdgeType = "related_to";
|
|
}
|
|
}
|
|
|
|
// WP-26: Backward-Edge generieren (currentSection -> prevSection)
|
|
// Das ist die inverse Edge zu der Forward-Edge (prevSection -> currentSection)
|
|
let backwardEdgeType: string = forwardEdgeType;
|
|
if (vocabulary) {
|
|
const inverse = vocabulary.getInverse(forwardEdgeType);
|
|
if (inverse) {
|
|
backwardEdgeType = inverse;
|
|
}
|
|
}
|
|
|
|
// WP-26: Backward-Edge (currentSection -> prevSection) wird in prevSection eingefügt
|
|
// Diese Edge zeigt von currentSection zurück zu prevSection
|
|
return `> [!edge] ${backwardEdgeType}\n> [[#^${currentSection.blockId}]]`;
|
|
}
|
|
|
|
/**
|
|
* Fügt Edges zu einem bestehenden abstract wrapper hinzu oder erstellt einen neuen.
|
|
*/
|
|
function addEdgesToAbstractWrapper(content: string, newEdges: string): string {
|
|
if (!newEdges.trim()) {
|
|
return content;
|
|
}
|
|
|
|
// Prüfe, ob bereits ein abstract wrapper vorhanden ist
|
|
const abstractWrapperMatch = content.match(/(> \[!abstract\][^\n]*\n(?:>>?[^\n]*\n)*> \^map-\d+)/);
|
|
|
|
if (abstractWrapperMatch && abstractWrapperMatch[1]) {
|
|
// Füge neue Edges zum bestehenden wrapper hinzu
|
|
const existingWrapper = abstractWrapperMatch[1];
|
|
const updatedWrapper = mergeEdgesIntoWrapper(existingWrapper, newEdges);
|
|
if (abstractWrapperMatch[0]) {
|
|
return content.replace(abstractWrapperMatch[0], updatedWrapper);
|
|
}
|
|
}
|
|
|
|
{
|
|
// Erstelle neuen abstract wrapper
|
|
const abstractWrapper = buildAbstractWrapper(newEdges);
|
|
if (abstractWrapper) {
|
|
return content.trimEnd() + "\n\n" + abstractWrapper;
|
|
}
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merged neue Edges in einen bestehenden abstract wrapper.
|
|
*/
|
|
function mergeEdgesIntoWrapper(existingWrapper: string, newEdges: string): string {
|
|
// Parse bestehenden wrapper
|
|
const lines = existingWrapper.split("\n");
|
|
const wrapperHeader = lines[0] || "> [!abstract]- 🕸️ Semantic Mapping";
|
|
const mapMarker = lines[lines.length - 1] || `> ^map-${Date.now()}`;
|
|
|
|
// Parse bestehende Edges
|
|
const existingGroups = new Map<string, string[]>();
|
|
let currentEdgeType: string | null = null;
|
|
|
|
for (let i = 1; i < lines.length - 1; i++) {
|
|
const line = lines[i];
|
|
if (!line) continue;
|
|
|
|
const edgeMatch = line.match(/^>>\s*\[!edge\]\s+(.+)$/);
|
|
if (edgeMatch && edgeMatch[1]) {
|
|
currentEdgeType = edgeMatch[1].trim();
|
|
if (!existingGroups.has(currentEdgeType)) {
|
|
existingGroups.set(currentEdgeType, []);
|
|
}
|
|
} else {
|
|
const linkMatch = line.match(/^>>\s*(\[\[#\^.+?\]\])$/);
|
|
if (linkMatch && linkMatch[1] && currentEdgeType) {
|
|
const links = existingGroups.get(currentEdgeType) || [];
|
|
links.push(linkMatch[1]);
|
|
existingGroups.set(currentEdgeType, links);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse neue Edges
|
|
const newEdgeLines = newEdges.split("\n").filter(line => line.trim());
|
|
for (const line of newEdgeLines) {
|
|
const edgeMatch = line.match(/^>\s*\[!edge\]\s+(.+)$/);
|
|
if (edgeMatch && edgeMatch[1]) {
|
|
currentEdgeType = edgeMatch[1].trim();
|
|
if (!existingGroups.has(currentEdgeType)) {
|
|
existingGroups.set(currentEdgeType, []);
|
|
}
|
|
} else {
|
|
const linkMatch = line.match(/^>\s*(\[\[#\^.+?\]\])$/);
|
|
if (linkMatch && linkMatch[1] && currentEdgeType) {
|
|
const links = existingGroups.get(currentEdgeType) || [];
|
|
// Prüfe auf Duplikate
|
|
if (!links.includes(linkMatch[1])) {
|
|
links.push(linkMatch[1]);
|
|
}
|
|
existingGroups.set(currentEdgeType, links);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build merged wrapper
|
|
const wrapperLines: string[] = [];
|
|
wrapperLines.push(wrapperHeader);
|
|
|
|
const sortedEdgeTypes = Array.from(existingGroups.keys()).sort();
|
|
|
|
for (let i = 0; i < sortedEdgeTypes.length; i++) {
|
|
const edgeType = sortedEdgeTypes[i];
|
|
if (!edgeType) continue;
|
|
const links = existingGroups.get(edgeType) || [];
|
|
|
|
if (links.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (i > 0) {
|
|
wrapperLines.push(">");
|
|
}
|
|
|
|
wrapperLines.push(`>> [!edge] ${edgeType}`);
|
|
|
|
const sortedLinks = [...links].sort();
|
|
for (const link of sortedLinks) {
|
|
wrapperLines.push(`>> ${link}`);
|
|
}
|
|
}
|
|
|
|
wrapperLines.push(mapMarker);
|
|
|
|
return wrapperLines.join("\n");
|
|
}
|