167 lines
5.6 KiB
Python
167 lines
5.6 KiB
Python
"""
|
|
Wiki-style link processing for Notes feature.
|
|
Supports [[note-title]] for links and ![[note-title]] for embedding.
|
|
"""
|
|
|
|
import re
|
|
from flask import g, url_for
|
|
from models import Note, NoteVisibility
|
|
from sqlalchemy import or_, and_
|
|
|
|
|
|
def process_wiki_links(content, current_note_id=None):
|
|
"""
|
|
Process wiki-style links in content.
|
|
- [[note-title]] becomes a link to the note
|
|
- ![[note-title]] embeds the note content
|
|
|
|
Args:
|
|
content: Markdown content with potential wiki links
|
|
current_note_id: ID of the current note (to avoid circular embeds)
|
|
|
|
Returns:
|
|
Processed content with wiki links replaced
|
|
"""
|
|
# Process embeds first (![[...]]) to avoid link processing inside embedded content
|
|
content = process_wiki_embeds(content, current_note_id)
|
|
|
|
# Then process regular links ([[...]])
|
|
content = process_wiki_regular_links(content)
|
|
|
|
return content
|
|
|
|
|
|
def process_wiki_regular_links(content):
|
|
"""Convert [[note-title]] to HTML links"""
|
|
def replace_link(match):
|
|
link_text = match.group(1).strip()
|
|
|
|
# Try to find the note by title or slug
|
|
note = find_note_by_reference(link_text)
|
|
|
|
if note:
|
|
# Create a link to the note
|
|
url = url_for('notes.view_note', slug=note.slug)
|
|
return f'<a href="{url}" class="wiki-link" title="View: {note.title}">{link_text}</a>'
|
|
else:
|
|
# Note not found - create a broken link indicator
|
|
return f'<span class="wiki-link-broken" title="Note not found: {link_text}">{link_text}</span>'
|
|
|
|
# Pattern to match [[text]] but not ![[text]]
|
|
pattern = r'(?<!\!)\[\[([^\]]+)\]\]'
|
|
return re.sub(pattern, replace_link, content)
|
|
|
|
|
|
def process_wiki_embeds(content, current_note_id=None):
|
|
"""Convert ![[note-title]] to embedded content"""
|
|
# Keep track of embedded notes to prevent infinite recursion
|
|
embedded_notes = set()
|
|
if current_note_id:
|
|
embedded_notes.add(current_note_id)
|
|
|
|
def replace_embed(match):
|
|
embed_ref = match.group(1).strip()
|
|
|
|
# Try to find the note by title or slug
|
|
note = find_note_by_reference(embed_ref)
|
|
|
|
if note:
|
|
# Check if we've already embedded this note (circular reference)
|
|
if note.id in embedded_notes:
|
|
return f'<div class="wiki-embed-error">Circular reference detected: {embed_ref}</div>'
|
|
|
|
# Check if user can view the note
|
|
if not note.can_user_view(g.user):
|
|
return f'<div class="wiki-embed-error">Access denied: {embed_ref}</div>'
|
|
|
|
# Add to embedded set
|
|
embedded_notes.add(note.id)
|
|
|
|
# Get the note content without frontmatter
|
|
from frontmatter_utils import parse_frontmatter
|
|
_, body = parse_frontmatter(note.content)
|
|
|
|
# Process any wiki links in the embedded content (but not embeds to avoid deep recursion)
|
|
embedded_content = process_wiki_regular_links(body)
|
|
|
|
# Render the embedded content
|
|
import markdown
|
|
html_content = markdown.markdown(embedded_content, extensions=['extra', 'codehilite', 'toc'])
|
|
|
|
# Create an embedded note block
|
|
embed_html = f'''
|
|
<div class="wiki-embed" data-note-id="{note.id}">
|
|
<div class="wiki-embed-header">
|
|
<a href="{url_for('notes.view_note', slug=note.slug)}" class="wiki-embed-title">
|
|
<i class="ti ti-file-text"></i> {note.title}
|
|
</a>
|
|
</div>
|
|
<div class="wiki-embed-content">
|
|
{html_content}
|
|
</div>
|
|
</div>'''
|
|
|
|
return embed_html
|
|
else:
|
|
# Note not found
|
|
return f'<div class="wiki-embed-error">Note not found: {embed_ref}</div>'
|
|
|
|
# Pattern to match ![[text]]
|
|
pattern = r'\!\[\[([^\]]+)\]\]'
|
|
return re.sub(pattern, replace_embed, content)
|
|
|
|
|
|
def find_note_by_reference(reference):
|
|
"""
|
|
Find a note by title or slug reference.
|
|
First tries exact slug match, then exact title match, then case-insensitive title match.
|
|
|
|
Args:
|
|
reference: The note reference (title or slug)
|
|
|
|
Returns:
|
|
Note object or None
|
|
"""
|
|
if not g.user or not g.user.company_id:
|
|
return None
|
|
|
|
# Build base query for notes the user can see
|
|
base_query = Note.query.filter(
|
|
Note.company_id == g.user.company_id,
|
|
Note.is_archived == False
|
|
).filter(
|
|
or_(
|
|
# Private notes created by user
|
|
and_(Note.visibility == NoteVisibility.PRIVATE, Note.created_by_id == g.user.id),
|
|
# Team notes from user's team
|
|
and_(Note.visibility == NoteVisibility.TEAM, Note.created_by.has(team_id=g.user.team_id)),
|
|
# Company notes
|
|
Note.visibility == NoteVisibility.COMPANY
|
|
)
|
|
)
|
|
|
|
# Try exact slug match first
|
|
note = base_query.filter_by(slug=reference).first()
|
|
if note:
|
|
return note
|
|
|
|
# Try exact title match
|
|
note = base_query.filter_by(title=reference).first()
|
|
if note:
|
|
return note
|
|
|
|
# Try case-insensitive title match
|
|
note = base_query.filter(Note.title.ilike(reference)).first()
|
|
if note:
|
|
return note
|
|
|
|
# Try slug-ified version of the reference
|
|
import re
|
|
slugified = re.sub(r'[^\w\s-]', '', reference.lower())
|
|
slugified = re.sub(r'[-\s]+', '-', slugified).strip('-')
|
|
if slugified != reference:
|
|
note = base_query.filter_by(slug=slugified).first()
|
|
if note:
|
|
return note
|
|
|
|
return None |