From d0350532034c7e16bb9fd9be71cdd27a8652f3cf0d2e5648755a81303fe7630c Mon Sep 17 00:00:00 2001 From: Discsearcher Date: Sun, 16 Aug 2026 15:00:44 -0400 Subject: [PATCH] Updated to work a lot better. --- app.py | 131 +++++++++++++++++++++++++--- config/repositories.json.example | 9 +- docker-compose.override.yml.example | 2 - docker-compose.yml | 16 +--- static/style.css | 59 +++++++++++++ templates/index.html | 16 +--- 6 files changed, 185 insertions(+), 48 deletions(-) diff --git a/app.py b/app.py index 5f93d23..5ba875d 100644 --- a/app.py +++ b/app.py @@ -8,11 +8,12 @@ import subprocess import json from pathlib import Path from datetime import datetime -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, send_file from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger import markdown import logging +import re from whoosh.index import create_in, open_dir from whoosh.fields import Schema, TEXT, ID from whoosh.qparser import QueryParser @@ -48,6 +49,88 @@ def create_search_index(): ix = create_search_index() +def is_hidden_path(file_path): + """Check if a file path contains any hidden directories or files (starting with dot).""" + path_obj = Path(file_path) if isinstance(file_path, str) else file_path + # Check all parts of the path including the filename + return any(part.startswith('.') for part in path_obj.parts) + +def rewrite_image_paths(md_content, repo, filepath): + """Rewrite relative image paths to be served by Flask.""" + # Get the directory of the current file (relative to repo root) + file_dir = os.path.dirname(filepath) + + # Pattern to match markdown image syntax: ![alt](path) + def replace_image_path(match): + alt_text = match.group(1) + img_path = match.group(2) + + # Skip absolute URLs and data URIs + if img_path.startswith(('http://', 'https://', 'data:')): + return match.group(0) + + # Resolve relative paths + if img_path.startswith('/'): + # Absolute path from repo root + resolved_path = img_path.lstrip('/') + else: + # Relative path - resolve it relative to the file's directory + if file_dir: + resolved_path = os.path.normpath(os.path.join(file_dir, img_path)) + else: + resolved_path = img_path + + # Ensure forward slashes for URL + resolved_path = resolved_path.replace(os.sep, '/') + image_url = f"/image/{repo}/{resolved_path}" + + return f"![{alt_text}]({image_url})" + + # Replace all markdown image references + md_content = re.sub(r'!\[([^\]]*)\]\(([^\)]+)\)', replace_image_path, md_content) + return md_content + +def build_directory_tree(repo_path): + """Build a hierarchical tree of markdown files organized by directory.""" + tree = {} + + if not os.path.exists(repo_path): + logger.warning(f"Repo path does not exist: {repo_path}") + return tree + + file_count = 0 + for md_file in Path(repo_path).rglob('*'): + if md_file.suffix.lower() not in MARKDOWN_EXTENSIONS: + continue + if is_hidden_path(md_file): + logger.debug(f"Skipping hidden path: {md_file}") + continue + + file_count += 1 + rel_path = md_file.relative_to(repo_path) + parts = rel_path.parts[:-1] # All parts except filename + filename = md_file.stem # Name without extension + + logger.debug(f"Adding to tree: {rel_path} (name: {filename})") + + # Navigate/create nested dict structure + current = tree + for part in parts: + if part not in current: + current[part] = {} + current = current[part] + + # Add file to current level + if '_files' not in current: + current['_files'] = [] + current['_files'].append({ + 'name': filename, + 'path': str(rel_path) + }) + + logger.info(f"Built tree for {repo_path}: found {file_count} markdown files") + return tree + def load_repositories(): """Load repository configuration from file.""" if not os.path.exists(CONFIG_FILE): @@ -115,7 +198,7 @@ def update_search_index(): continue for md_file in repo_dir.rglob('*'): - if md_file.suffix.lower() in MARKDOWN_EXTENSIONS: + if md_file.suffix.lower() in MARKDOWN_EXTENSIONS and not is_hidden_path(md_file): try: content = md_file.read_text(encoding='utf-8', errors='ignore') # Extract title from filename or first heading @@ -181,24 +264,43 @@ def index(): for repo_name in repos.keys(): repo_path = os.path.join(REPOS_DIR, repo_name) - markdown_files = [] - - if os.path.exists(repo_path): - for md_file in Path(repo_path).rglob('*'): - if md_file.suffix.lower() in MARKDOWN_EXTENSIONS: - rel_path = str(md_file.relative_to(repo_path)) - markdown_files.append({ - 'name': md_file.stem, - 'path': rel_path - }) + file_tree = build_directory_tree(repo_path) repo_list.append({ 'name': repo_name, - 'files': sorted(markdown_files, key=lambda x: x['path']) + 'tree': file_tree }) return render_template('index.html', repositories=repo_list) +@app.route('/image//') +def serve_image(repo, filepath): + """Serve images from repository directories.""" + # Security: prevent directory traversal + if '..' in filepath or filepath.startswith('/'): + return "Invalid path", 400 + + file_path = os.path.join(REPOS_DIR, repo, filepath) + + # Ensure the file is within the repo directory + try: + file_path = os.path.realpath(file_path) + repo_path = os.path.realpath(os.path.join(REPOS_DIR, repo)) + if not file_path.startswith(repo_path): + return "Access denied", 403 + except Exception: + return "Invalid path", 400 + + if not os.path.exists(file_path): + logger.warning(f"Image not found: {file_path}") + return "Image not found", 404 + + try: + return send_file(file_path) + except Exception as e: + logger.error(f"Error serving image {file_path}: {e}") + return f"Error serving image: {e}", 500 + @app.route('/api/search', methods=['GET']) def search(): """Search markdown files.""" @@ -249,6 +351,9 @@ def view_file(repo, filepath): with open(file_path, 'r', encoding='utf-8') as f: md_content = f.read() + # Rewrite image paths to be served by Flask + md_content = rewrite_image_paths(md_content, repo, filepath) + # Convert markdown to HTML html_content = markdown.markdown( md_content, diff --git a/config/repositories.json.example b/config/repositories.json.example index f9f7ace..e93bff2 100644 --- a/config/repositories.json.example +++ b/config/repositories.json.example @@ -1,12 +1,7 @@ { - "my-wiki": { - "url": "https://github.com/username/my-wiki.git", + "Biscuits": { + "url": "https://git.biscuitsdiscgolf.com/biscuits/markmycontent.git", "enabled": true, "schedule": "0 */6 * * *" - }, - "project-docs": { - "url": "https://github.com/username/project-docs.git", - "enabled": true, - "schedule": "0 9 * * *" } } diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example index c15a5a0..7da857e 100644 --- a/docker-compose.override.yml.example +++ b/docker-compose.override.yml.example @@ -1,8 +1,6 @@ # Optional override file for local development # Copy to docker-compose.override.yml and customize -version: '3.8' - services: markmywords: # Enable debug mode diff --git a/docker-compose.yml b/docker-compose.yml index 1583374..0e1ab04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: markmywords: build: . @@ -8,22 +6,12 @@ services: - "5000:5000" volumes: # Configuration directory with repositories.json - - ./config:/config:ro + - ./config:/config:rw # Data directory for cloned repositories and search index - - markmywords_data:/data + - ./data:/data:rw environment: # Set Flask environment FLASK_ENV: production # Python unbuffered output for real-time logs PYTHONUNBUFFERED: 1 restart: unless-stopped - networks: - - markmywords - -networks: - markmywords: - driver: bridge - -volumes: - markmywords_data: - driver: local diff --git a/static/style.css b/static/style.css index 4269db1..7c5dc04 100644 --- a/static/style.css +++ b/static/style.css @@ -237,6 +237,65 @@ main { font-style: italic; } +/* Tree View Styles */ +.file-tree { + margin-top: 1rem; +} + +.tree-list { + list-style: none; + padding-left: 0; +} + +.tree-list .tree-list { + padding-left: 1.5rem; + margin-top: 0.25rem; +} + +.tree-folder, +.tree-file { + padding: 0.25rem 0; + margin: 0.25rem 0; +} + +.folder-toggle { + cursor: pointer; + padding: 0.25rem 0.5rem; + display: inline-flex; + align-items: center; + gap: 0.5rem; + user-select: none; + font-weight: 500; + color: var(--primary-color); + transition: color 0.2s ease; +} + +.folder-toggle:hover { + color: var(--secondary-color); +} + +details > summary::-webkit-details-marker { + display: none; +} + +details > summary::before { + content: "▶ "; + display: inline-block; + margin-right: 0.25rem; + transition: transform 0.2s ease; +} + +details[open] > summary::before { + transform: rotate(90deg); +} + +.tree-children { + margin-top: 0.25rem; + padding-left: 0.5rem; + border-left: 2px solid var(--border-color); + margin-left: 0.25rem; +} + .empty-state { text-align: center; padding: 3rem; diff --git a/templates/index.html b/templates/index.html index 83bfaff..eb97ce4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -28,23 +28,15 @@
-

Repositories

+

Site Navigation

{% if repositories %}
{% for repo in repositories %}

{{ repo.name }}

- {% if repo.files %} -
- + {% if repo.tree %} +
+ {% include 'tree_render.html' %}
{% else %}

No markdown files found