backlink retrieval
This commit is contained in:
parent
c466f0b16f
commit
57032c6c28
|
|
@ -43,6 +43,7 @@ from .utils import (
|
||||||
get_template_content,
|
get_template_content,
|
||||||
apply_template_placeholders,
|
apply_template_placeholders,
|
||||||
paginate,
|
paginate,
|
||||||
|
get_backlinks,
|
||||||
)
|
)
|
||||||
from .plugins import PluginManager
|
from .plugins import PluginManager
|
||||||
from .themes import get_available_themes, get_theme_css
|
from .themes import get_available_themes, get_theme_css
|
||||||
|
|
@ -1076,23 +1077,28 @@ async def list_notes(
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/notes/{note_path:path}", tags=["Notes"])
|
@api_router.get("/notes/{note_path:path}", tags=["Notes"])
|
||||||
async def get_note(note_path: str):
|
async def get_note(note_path: str, include_backlinks: bool = True):
|
||||||
"""Get a specific note's content"""
|
"""Get a specific note's content with optional backlinks"""
|
||||||
try:
|
try:
|
||||||
content = get_note_content(config['storage']['notes_dir'], note_path)
|
content = get_note_content(config['storage']['notes_dir'], note_path)
|
||||||
if content is None:
|
if content is None:
|
||||||
raise HTTPException(status_code=404, detail="Note not found")
|
raise HTTPException(status_code=404, detail="Note not found")
|
||||||
|
|
||||||
# Run on_note_load hook (can transform content, e.g., decrypt)
|
# Run on_note_load hook (can transform content, e.g., decrypt)
|
||||||
transformed_content = plugin_manager.run_hook('on_note_load', note_path=note_path, content=content)
|
transformed_content = plugin_manager.run_hook('on_note_load', note_path=note_path, content=content)
|
||||||
if transformed_content is not None:
|
if transformed_content is not None:
|
||||||
content = transformed_content
|
content = transformed_content
|
||||||
|
|
||||||
return {
|
response = {
|
||||||
"path": note_path,
|
"path": note_path,
|
||||||
"content": content,
|
"content": content,
|
||||||
"metadata": create_note_metadata(config['storage']['notes_dir'], note_path)
|
"metadata": create_note_metadata(config['storage']['notes_dir'], note_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if include_backlinks:
|
||||||
|
response["backlinks"] = get_backlinks(config['storage']['notes_dir'], note_path)
|
||||||
|
|
||||||
|
return response
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
114
backend/utils.py
114
backend/utils.py
|
|
@ -984,3 +984,117 @@ def apply_template_placeholders(content: str, note_path: str) -> str:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
backlinks = []
|
||||||
|
notes, _folders = scan_notes_fast_walk(notes_dir, include_media=False)
|
||||||
|
|
||||||
|
# Normalize target path for matching
|
||||||
|
target_path = target_note_path
|
||||||
|
target_path_no_ext = target_path.replace('.md', '')
|
||||||
|
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(),
|
||||||
|
target_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
for note in notes:
|
||||||
|
if note.get('type') != 'note':
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_path = note['path']
|
||||||
|
|
||||||
|
# Skip self-references
|
||||||
|
if source_path == target_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Read note content
|
||||||
|
full_path = Path(notes_dir) / source_path
|
||||||
|
try:
|
||||||
|
with open(full_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines = content.split('\n')
|
||||||
|
found_links = []
|
||||||
|
|
||||||
|
for line_num, line in enumerate(lines, 1):
|
||||||
|
# Find wikilinks: [[target]] or [[target|display]]
|
||||||
|
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
|
||||||
|
start = max(0, match.start() - 30)
|
||||||
|
end = min(len(line), match.end() + 30)
|
||||||
|
context = line[start:end]
|
||||||
|
if start > 0:
|
||||||
|
context = '...' + context
|
||||||
|
if end < len(line):
|
||||||
|
context = context + '...'
|
||||||
|
|
||||||
|
found_links.append({
|
||||||
|
"line_number": line_num,
|
||||||
|
"context": context,
|
||||||
|
"type": "wikilink"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Find markdown links: [text](path)
|
||||||
|
markdown_matches = re.finditer(r'\[([^\]]+)\]\((?!https?://|mailto:|#|data:)([^\)]+)\)', line)
|
||||||
|
for match in markdown_matches:
|
||||||
|
link_path = match.group(2).split('#')[0] # Remove anchor
|
||||||
|
if not link_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
link_path = urllib.parse.unquote(link_path)
|
||||||
|
if link_path.startswith('./'):
|
||||||
|
link_path = link_path[2:]
|
||||||
|
|
||||||
|
link_path_lower = link_path.lower()
|
||||||
|
link_path_no_ext = link_path_lower.replace('.md', '')
|
||||||
|
|
||||||
|
# Check if this link points to our target
|
||||||
|
if link_path_lower in target_refs or link_path_no_ext in target_refs:
|
||||||
|
start = max(0, match.start() - 30)
|
||||||
|
end = min(len(line), match.end() + 30)
|
||||||
|
context = line[start:end]
|
||||||
|
if start > 0:
|
||||||
|
context = '...' + context
|
||||||
|
if end < len(line):
|
||||||
|
context = context + '...'
|
||||||
|
|
||||||
|
found_links.append({
|
||||||
|
"line_number": line_num,
|
||||||
|
"context": context,
|
||||||
|
"type": "markdown"
|
||||||
|
})
|
||||||
|
|
||||||
|
# If we found links in this note, add it to backlinks
|
||||||
|
if found_links:
|
||||||
|
backlinks.append({
|
||||||
|
"path": source_path,
|
||||||
|
"name": note['name'].replace('.md', ''),
|
||||||
|
"references": found_links[:3] # Limit to 3 references per note
|
||||||
|
})
|
||||||
|
|
||||||
|
return backlinks
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue