From bade08b0b84bba699077cdaa9debaf482d19afaf Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Tue, 24 Mar 2026 13:23:47 +0100 Subject: [PATCH] graph rendering bugfix --- backend/main.py | 102 ++++++++++++++++++++++++++++++----------------- backend/utils.py | 52 ++++++++++++++++++------ 2 files changed, 104 insertions(+), 50 deletions(-) diff --git a/backend/main.py b/backend/main.py index 325ea42..a1915f9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1333,6 +1333,12 @@ async def get_graph(): # Match links that don't start with http://, https://, mailto:, #, etc. markdown_links = re.findall(r'\[([^\]]+)\]\((?!https?://|mailto:|#|data:)([^\)]+)\)', content) + # Get source note's folder for resolving relative links + # Use forward slashes consistently (note_paths uses forward slashes) + source_folder = str(Path(note['path']).parent).replace('\\', '/') + if source_folder == '.': + source_folder = '' + # Process wikilinks for target in wikilinks: target = target.strip() @@ -1341,20 +1347,34 @@ async def get_graph(): # Try to match target to an existing note target_path = None - # 1. Exact path match - if target in note_paths: - target_path = target if target.endswith('.md') else target + '.md' - # 2. Path with .md extension - elif target + '.md' in note_paths: - target_path = target + '.md' - # 3. Case-insensitive path match (e.g., [[Folder/Note]] -> folder/note.md) - elif target_lower in note_paths_lower: - target_path = note_paths_lower[target_lower] - elif target_lower + '.md' in note_paths_lower: - target_path = note_paths_lower[target_lower + '.md'] - # 4. Just note name (case-insensitive) - elif target_lower in note_names: - target_path = note_names[target_lower] + # 1. Try resolving relative to source note's folder first + if source_folder and '/' not in target: + relative_path = f"{source_folder}/{target}" + relative_path_lower = relative_path.lower() + + if relative_path in note_paths: + target_path = relative_path if relative_path.endswith('.md') else relative_path + '.md' + elif relative_path + '.md' in note_paths: + target_path = relative_path + '.md' + elif relative_path_lower in note_paths_lower: + target_path = note_paths_lower[relative_path_lower] + elif relative_path_lower + '.md' in note_paths_lower: + target_path = note_paths_lower[relative_path_lower + '.md'] + + # 2. Exact path match (absolute or already has folder) + if not target_path: + if target in note_paths: + target_path = target if target.endswith('.md') else target + '.md' + elif target + '.md' in note_paths: + target_path = target + '.md' + # 3. Case-insensitive path match (e.g., [[Folder/Note]] -> folder/note.md) + elif target_lower in note_paths_lower: + target_path = note_paths_lower[target_lower] + elif target_lower + '.md' in note_paths_lower: + target_path = note_paths_lower[target_lower + '.md'] + # 4. Just note name (case-insensitive) - global match + elif target_lower in note_names: + target_path = note_names[target_lower] if target_path and target_path != note['path']: edges.append({ @@ -1381,34 +1401,42 @@ async def get_graph(): # Add .md extension if not present and doesn't have other extension link_path_with_md = link_path if link_path.endswith('.md') else link_path + '.md' - link_path_lower = link_path.lower() - link_path_with_md_lower = link_path_with_md.lower() # Try to match target to an existing note target_path = None - # 1. Exact path match (with or without .md) - if link_path in note_paths: - target_path = link_path if link_path.endswith('.md') else link_path + '.md' - elif link_path_with_md in note_paths: - target_path = link_path_with_md - # 2. Case-insensitive path match - elif link_path_lower in note_paths_lower: - target_path = note_paths_lower[link_path_lower] - elif link_path_with_md_lower in note_paths_lower: - target_path = note_paths_lower[link_path_with_md_lower] - # 3. Try matching by filename only (for relative links) - else: - # Extract just the filename - filename = link_path.split('/')[-1] - filename_lower = filename.lower() - filename_with_md = filename if filename.endswith('.md') else filename + '.md' - filename_with_md_lower = filename_with_md.lower() + # 1. First, try resolving relative to source note's folder + if source_folder and not link_path.startswith('/'): + relative_path = f"{source_folder}/{link_path}" + relative_path_with_md = f"{source_folder}/{link_path_with_md}" + relative_path_lower = relative_path.lower() + relative_path_with_md_lower = relative_path_with_md.lower() - if filename_lower in note_names: - target_path = note_names[filename_lower] - elif filename_with_md_lower in note_names: - target_path = note_names[filename_with_md_lower] + if relative_path in note_paths: + target_path = relative_path if relative_path.endswith('.md') else relative_path + '.md' + elif relative_path_with_md in note_paths: + target_path = relative_path_with_md + elif relative_path_lower in note_paths_lower: + target_path = note_paths_lower[relative_path_lower] + elif relative_path_with_md_lower in note_paths_lower: + target_path = note_paths_lower[relative_path_with_md_lower] + + # 2. Try exact path match from root (for absolute paths or notes at root) + if not target_path: + link_path_lower = link_path.lower() + link_path_with_md_lower = link_path_with_md.lower() + + if link_path in note_paths: + target_path = link_path if link_path.endswith('.md') else link_path + '.md' + elif link_path_with_md in note_paths: + target_path = link_path_with_md + # Case-insensitive path match + elif link_path_lower in note_paths_lower: + target_path = note_paths_lower[link_path_lower] + elif link_path_with_md_lower in note_paths_lower: + target_path = note_paths_lower[link_path_with_md_lower] + + # No global filename fallback for markdown links - they must resolve as paths if target_path and target_path != note['path']: edges.append({ diff --git a/backend/utils.py b/backend/utils.py index a5f1dea..1292165 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -989,11 +989,11 @@ def apply_template_placeholders(content: str, note_path: str) -> str: def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]: """ Find all notes that link TO the specified note (reverse links / backlinks). - + Args: notes_dir: Base directory containing notes target_note_path: Path of the note to find backlinks for - + Returns: List of backlink objects with path, context, and line_number """ @@ -1002,13 +1002,15 @@ def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]: # Normalize target path for matching target_path = target_note_path + target_path_lower = target_path.lower() target_path_no_ext = target_path.replace('.md', '') + target_path_no_ext_lower = target_path_no_ext.lower() target_name = Path(target_path).stem.lower() - # Build set of ways to reference the target note - target_refs = { - target_path.lower(), - target_path_no_ext.lower(), + # For wikilinks: global name matching (find note anywhere by name) + wikilink_refs = { + target_path_lower, + target_path_no_ext_lower, target_name, } @@ -1022,6 +1024,11 @@ def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]: if source_path == target_path: continue + # Get source folder for resolving relative markdown links + source_folder = str(Path(source_path).parent).replace('\\', '/') + if source_folder == '.': + source_folder = '' + # Read note content full_path = Path(notes_dir) / source_path try: @@ -1035,14 +1042,14 @@ def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]: for line_num, line in enumerate(lines, 1): # Find wikilinks: [[target]] or [[target|display]] + # Wikilinks use GLOBAL matching (find note anywhere by name) wikilink_matches = re.finditer(r'\[\[([^\]|]+)(?:\|[^\]]+)?\]\]', line) for match in wikilink_matches: link_target = match.group(1).strip().lower() link_target_no_ext = link_target.replace('.md', '') - # Check if this link points to our target - if link_target in target_refs or link_target_no_ext in target_refs: - # Extract context around the match + # Check if this wikilink points to our target (global match) + if link_target in wikilink_refs or link_target_no_ext in wikilink_refs: start = max(0, match.start() - 30) end = min(len(line), match.end() + 30) context = line[start:end] @@ -1058,6 +1065,7 @@ def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]: }) # Find markdown links: [text](path) + # Markdown links must RESOLVE as paths (relative to source or absolute) markdown_matches = re.finditer(r'\[([^\]]+)\]\((?!https?://|mailto:|#|data:)([^\)]+)\)', line) for match in markdown_matches: link_path = match.group(2).split('#')[0] # Remove anchor @@ -1068,11 +1076,29 @@ def get_backlinks(notes_dir: str, target_note_path: str) -> List[Dict]: if link_path.startswith('./'): link_path = link_path[2:] - link_path_lower = link_path.lower() - link_path_no_ext = link_path_lower.replace('.md', '') + # Add .md if not present + link_path_with_md = link_path if link_path.endswith('.md') else link_path + '.md' - # Check if this link points to our target - if link_path_lower in target_refs or link_path_no_ext in target_refs: + # Resolve the link path to get the actual target + resolved_path = None + + # 1. Try resolving relative to source folder + if source_folder and not link_path.startswith('/'): + relative_path = f"{source_folder}/{link_path_with_md}" + if relative_path.lower() == target_path_lower: + resolved_path = target_path + elif f"{source_folder}/{link_path}".lower() == target_path_no_ext_lower: + resolved_path = target_path + + # 2. Try as absolute path from root + if not resolved_path: + if link_path_with_md.lower() == target_path_lower: + resolved_path = target_path + elif link_path.lower() == target_path_no_ext_lower: + resolved_path = target_path + + # Only add if the resolved path matches the target + if resolved_path: start = max(0, match.start() - 30) end = min(len(line), match.end() + 30) context = line[start:end]