Allow uploading of files and folders.

This commit is contained in:
2025-07-28 11:07:40 +02:00
committed by Jens Luedicke
parent 87471e033e
commit f98e8f3e71
13 changed files with 2805 additions and 8 deletions

167
wiki_links.py Normal file
View File

@@ -0,0 +1,167 @@
"""
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