Updated to work a lot better.

This commit is contained in:
Discsearcher
2026-08-16 15:00:44 -04:00
parent 85e6af0b7d
commit d035053203
6 changed files with 185 additions and 48 deletions
+118 -13
View File
@@ -8,11 +8,12 @@ import subprocess
import json import json
from pathlib import Path from pathlib import Path
from datetime import datetime 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.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
import markdown import markdown
import logging import logging
import re
from whoosh.index import create_in, open_dir from whoosh.index import create_in, open_dir
from whoosh.fields import Schema, TEXT, ID from whoosh.fields import Schema, TEXT, ID
from whoosh.qparser import QueryParser from whoosh.qparser import QueryParser
@@ -48,6 +49,88 @@ def create_search_index():
ix = 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(): def load_repositories():
"""Load repository configuration from file.""" """Load repository configuration from file."""
if not os.path.exists(CONFIG_FILE): if not os.path.exists(CONFIG_FILE):
@@ -115,7 +198,7 @@ def update_search_index():
continue continue
for md_file in repo_dir.rglob('*'): 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: try:
content = md_file.read_text(encoding='utf-8', errors='ignore') content = md_file.read_text(encoding='utf-8', errors='ignore')
# Extract title from filename or first heading # Extract title from filename or first heading
@@ -181,24 +264,43 @@ def index():
for repo_name in repos.keys(): for repo_name in repos.keys():
repo_path = os.path.join(REPOS_DIR, repo_name) repo_path = os.path.join(REPOS_DIR, repo_name)
markdown_files = [] file_tree = build_directory_tree(repo_path)
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
})
repo_list.append({ repo_list.append({
'name': repo_name, 'name': repo_name,
'files': sorted(markdown_files, key=lambda x: x['path']) 'tree': file_tree
}) })
return render_template('index.html', repositories=repo_list) return render_template('index.html', repositories=repo_list)
@app.route('/image/<repo>/<path:filepath>')
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']) @app.route('/api/search', methods=['GET'])
def search(): def search():
"""Search markdown files.""" """Search markdown files."""
@@ -249,6 +351,9 @@ def view_file(repo, filepath):
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
md_content = f.read() 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 # Convert markdown to HTML
html_content = markdown.markdown( html_content = markdown.markdown(
md_content, md_content,
+2 -7
View File
@@ -1,12 +1,7 @@
{ {
"my-wiki": { "Biscuits": {
"url": "https://github.com/username/my-wiki.git", "url": "https://git.biscuitsdiscgolf.com/biscuits/markmycontent.git",
"enabled": true, "enabled": true,
"schedule": "0 */6 * * *" "schedule": "0 */6 * * *"
},
"project-docs": {
"url": "https://github.com/username/project-docs.git",
"enabled": true,
"schedule": "0 9 * * *"
} }
} }
-2
View File
@@ -1,8 +1,6 @@
# Optional override file for local development # Optional override file for local development
# Copy to docker-compose.override.yml and customize # Copy to docker-compose.override.yml and customize
version: '3.8'
services: services:
markmywords: markmywords:
# Enable debug mode # Enable debug mode
+2 -14
View File
@@ -1,5 +1,3 @@
version: '3.8'
services: services:
markmywords: markmywords:
build: . build: .
@@ -8,22 +6,12 @@ services:
- "5000:5000" - "5000:5000"
volumes: volumes:
# Configuration directory with repositories.json # Configuration directory with repositories.json
- ./config:/config:ro - ./config:/config:rw
# Data directory for cloned repositories and search index # Data directory for cloned repositories and search index
- markmywords_data:/data - ./data:/data:rw
environment: environment:
# Set Flask environment # Set Flask environment
FLASK_ENV: production FLASK_ENV: production
# Python unbuffered output for real-time logs # Python unbuffered output for real-time logs
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
restart: unless-stopped restart: unless-stopped
networks:
- markmywords
networks:
markmywords:
driver: bridge
volumes:
markmywords_data:
driver: local
+59
View File
@@ -237,6 +237,65 @@ main {
font-style: italic; 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 { .empty-state {
text-align: center; text-align: center;
padding: 3rem; padding: 3rem;
+4 -12
View File
@@ -28,23 +28,15 @@
</section> </section>
<section class="repositories-section"> <section class="repositories-section">
<h2>Repositories</h2> <h2>Site Navigation</h2>
{% if repositories %} {% if repositories %}
<div class="repo-grid"> <div class="repo-grid">
{% for repo in repositories %} {% for repo in repositories %}
<div class="repo-card"> <div class="repo-card">
<h3>{{ repo.name }}</h3> <h3>{{ repo.name }}</h3>
{% if repo.files %} {% if repo.tree %}
<div class="file-list"> <div class="file-tree">
<ul> {% include 'tree_render.html' %}
{% for file in repo.files %}
<li>
<a href="/view/{{ repo.name }}/{{ file.path }}" class="file-link">
📄 {{ file.name }}
</a>
</li>
{% endfor %}
</ul>
</div> </div>
{% else %} {% else %}
<p class="no-files">No markdown files found</p> <p class="no-files">No markdown files found</p>