add app config and settings generator

This commit is contained in:
nahakubuilde
2025-06-22 09:42:31 +01:00
parent e3e775a693
commit 4b73367544
12 changed files with 710 additions and 59 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
notes/** notes/**/
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.pyo *.pyo
+6
View File
@@ -0,0 +1,6 @@
# Flask application to view Obsidian notes in web browser
- just point the location of your folder with `.md` notes to app with `NOTES_DIR`
## Configuration
This app uses a `settings.ini` file for configuration. On first run, it will be created automatically if missing, and any missing values will be filled in with defaults.
+172 -47
View File
@@ -1,21 +1,31 @@
# app.py # app.py
from flask import Flask, render_template, request, redirect, url_for, jsonify from flask import Flask, render_template, request, redirect, url_for, jsonify, abort
from werkzeug.exceptions import HTTPException
import os import os
import mistune import mistune
from pathlib import Path from pathlib import Path
import uuid
import re import re
from urllib.parse import quote from app_settings_loader import ensure_settings_ini, FLASK_HOST, FLASK_PORT, SECRET_KEY, DEBUG, NOTES_DIR, MAX_CONTENT_LENGTH
def generate_slug(text):
# Remove non-alphanumeric characters, replace spaces with hyphens, and lowercase
slug = re.sub(r'[^a-zA-Z0-9\s]', '', text).strip().lower()
slug = re.sub(r'\s+', '-', slug)
return quote(slug) # URL-encode for safety
app = Flask(__name__) app = Flask(__name__)
app.config['NOTES_DIR'] = Path('notes') # Store Markdown files here ensure_settings_ini()
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
app.config['SECRET_KEY'] = SECRET_KEY
app.config['DEBUG'] = DEBUG
app.config['NOTES_DIR'] = NOTES_DIR
max_content_length = MAX_CONTENT_LENGTH * 1024 * 1024 # Convert MB to bytes
app.config['MAX_CONTENT_LENGTH'] = max_content_length
# Custom error handler using base.html
@app.errorhandler(Exception)
def handle_error(error):
if isinstance(error, HTTPException):
code = error.code
message = getattr(error, 'description', str(error))
else:
code = 500
message = str(error)
return render_template('error.html', message=message, error=error), code
# Create directory for notes if it doesn't exist # Create directory for notes if it doesn't exist
if not app.config['NOTES_DIR'].exists(): if not app.config['NOTES_DIR'].exists():
@@ -75,22 +85,42 @@ def generate_breadcrumbs(note_path, title=None):
current_path = current_path + '/' + part if current_path else part current_path = current_path + '/' + part if current_path else part
crumbs.append({ crumbs.append({
'name': part, 'name': part,
'url': url_for('index') + '#' + current_path # Using hash for directory navigation 'url': url_for('folder', folder_path=current_path)
}) })
# Add the file name (use title if provided) # Add the file name (always use file name without .md)
if len(parts) > 0: if len(parts) > 0:
if title: name = parts[-1][:-3] if parts[-1].endswith('.md') else parts[-1]
crumbs.append({'name': title, 'url': None}) url = None if parts[-1].endswith('.md') else url_for('folder', folder_path=current_path + '/' + parts[-1] if current_path else parts[-1])
else: crumbs.append({'name': name, 'url': url})
crumbs.append({'name': parts[-1][:-3], 'url': None}) # Remove .md extension
return crumbs return crumbs
@app.route('/') @app.route('/')
def index(): def index():
folder_contents = []
try:
for item in sorted(app.config['NOTES_DIR'].iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
if item.name.startswith('.'): # Skip hidden files
continue
is_dir = item.is_dir()
rel_path = str(item.relative_to(app.config['NOTES_DIR']))
if is_dir or item.suffix == '.md': # Only include directories and markdown files
folder_contents.append({
'name': item.name,
'type': 'dir' if is_dir else 'file',
'path': rel_path,
'display_name': item.name if is_dir else item.stem # Show full name for folders, remove .md for files
})
except Exception as e:
print(f"Error reading root directory: {str(e)}")
notes_tree = build_tree_structure(app.config['NOTES_DIR']) notes_tree = build_tree_structure(app.config['NOTES_DIR'])
return render_template('index.html', return render_template('folder.html',
folder_path='',
folder_contents=folder_contents,
notes_tree=notes_tree, notes_tree=notes_tree,
active_path=[], active_path=[],
current_note=None, current_note=None,
@@ -105,9 +135,6 @@ def note(note_path):
try: try:
with open(full_path, 'r', encoding='utf-8') as f: with open(full_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
title = full_path.stem
if '##' in content:
title = content.split('##')[1].split('\n')[0].strip()
# Convert markdown to HTML # Convert markdown to HTML
renderer = mistune.HTMLRenderer() renderer = mistune.HTMLRenderer()
@@ -117,6 +144,8 @@ def note(note_path):
# Build tree and get path components # Build tree and get path components
notes_tree = build_tree_structure(app.config['NOTES_DIR']) notes_tree = build_tree_structure(app.config['NOTES_DIR'])
active_path = get_path_components(note_path) active_path = get_path_components(note_path)
# Use file name (without .md) for breadcrumbs and title
title = full_path.stem
breadcrumbs = generate_breadcrumbs(note_path, title) breadcrumbs = generate_breadcrumbs(note_path, title)
return render_template('note.html', return render_template('note.html',
@@ -149,9 +178,6 @@ def edit_note(note_path):
with open(full_path, 'r', encoding='utf-8') as f: with open(full_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
title = full_path.stem title = full_path.stem
if '##' in content:
title = content.split('##')[1].split('\n')[0].strip()
notes_tree = build_tree_structure(app.config['NOTES_DIR']) notes_tree = build_tree_structure(app.config['NOTES_DIR'])
active_path = get_path_components(note_path) active_path = get_path_components(note_path)
breadcrumbs = generate_breadcrumbs(note_path, f"Edit {title}") breadcrumbs = generate_breadcrumbs(note_path, f"Edit {title}")
@@ -166,32 +192,96 @@ def edit_note(note_path):
except Exception as e: except Exception as e:
return f"Error reading note: {str(e)}", 500 return f"Error reading note: {str(e)}", 500
def get_all_folders(directory, base_path=None, level=0):
"""Get a flat list of all folders in the directory with their paths and levels."""
if base_path is None:
base_path = directory
folders = []
try:
for item in sorted(directory.iterdir(), key=lambda x: x.name.lower()):
if item.name.startswith('.'): # Skip hidden files
continue
if item.is_dir():
rel_path = str(item.relative_to(base_path))
folders.append({
'name': item.name,
'path': rel_path,
'level': level
})
# Recursively get subfolders
folders.extend(get_all_folders(item, base_path, level + 1))
except Exception as e:
print(f"Error reading directory {directory}: {str(e)}")
return folders
@app.route('/create', methods=['GET', 'POST']) @app.route('/create', methods=['GET', 'POST'])
def create_note(): def create_note():
if request.method == 'POST': if request.method == 'POST':
title = request.form.get('title', '').strip() or 'Untitled Note' title = request.form.get('title', '').strip()
if not title:
abort(400, description="Note title is required")
# Validate file name: only allow safe characters
# Letters, numbers, dash, underscore, dot (no slashes)
if not re.match(r'^[\w\-. ]+$', title):
return "Invalid file name. Only letters, numbers, dash, underscore, dot, and space are allowed.", 400
if '/' in title or '\\' in title:
return "Invalid file name. Slashes are not allowed.", 400
if title.startswith('.') or title.endswith('.'):
return "Invalid file name. Cannot start or end with a dot.", 400
if title in ['CON', 'PRN', 'AUX', 'NUL'] or re.match(r'^COM[1-9]$|^LPT[1-9]$', title, re.IGNORECASE):
return "Invalid file name.", 400
content = request.form.get('content', '').strip() content = request.form.get('content', '').strip()
path = request.form.get('path', '').strip() path = request.form.get('path', '').strip()
# Generate filename # Use the title as the file name
filename = f"{generate_slug(title)}.md" filename = f"{title}.md"
if path:
# Create directories if they don't exist
dir_path = Path(app.config['NOTES_DIR']) / path
os.makedirs(dir_path, exist_ok=True)
file_path = dir_path / filename
else:
file_path = Path(app.config['NOTES_DIR']) / filename
try: try:
with open(file_path, 'w', encoding='utf-8') as f: # Safely resolve the full path to ensure it's within NOTES_DIR
f.write(f"## {title}\n\n{content}") if path:
return redirect(url_for('note', note_path=str(file_path.relative_to(app.config['NOTES_DIR'])))) dir_path = (Path(app.config['NOTES_DIR']) / path).resolve()
except Exception as e: if not str(dir_path).startswith(str(app.config['NOTES_DIR'].resolve())):
return f"Error creating note: {str(e)}", 500 return "Invalid folder path", 400
os.makedirs(dir_path, exist_ok=True)
file_path = dir_path / filename
else:
file_path = Path(app.config['NOTES_DIR']) / filename
# Final safety check
file_path = file_path.resolve()
if not str(file_path).startswith(str(app.config['NOTES_DIR'].resolve())):
return "Invalid file path", 400
# Check if file already exists
if file_path.exists():
return "A note with this name already exists in the selected folder. Please choose a different name.", 400
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
# Use os.path.relpath for robust path calculation
rel_path = os.path.relpath(str(file_path), str(app.config['NOTES_DIR']))
return redirect(url_for('note', note_path=rel_path))
except Exception as e:
abort(500, description=f"Error creating note: {str(e)}")
# Get all available folders for autocomplete
available_folders = get_all_folders(app.config['NOTES_DIR'])
# Get current folder from query parameter if provided
current_folder = request.args.get('folder', '')
notes_tree = build_tree_structure(app.config['NOTES_DIR']) notes_tree = build_tree_structure(app.config['NOTES_DIR'])
return render_template('create.html', notes_tree=notes_tree, active_path=[], current_note=None) return render_template('create.html',
notes_tree=notes_tree,
available_folders=available_folders,
current_folder=current_folder,
active_path=current_folder.split('/') if current_folder else [],
current_note=None)
@app.route('/delete/<path:note_path>', methods=['POST']) @app.route('/delete/<path:note_path>', methods=['POST'])
def delete_note(note_path): def delete_note(note_path):
@@ -222,10 +312,7 @@ def search():
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
if query in content.lower(): if query in content.lower():
title = file_path.stem title = file_path.stem # Always use file name without .md
if '##' in content:
title = content.split('##')[1].split('\n')[0].strip()
# Find context for the match # Find context for the match
pos = content.lower().find(query) pos = content.lower().find(query)
start = max(0, pos - 50) start = max(0, pos - 50)
@@ -242,5 +329,43 @@ def search():
return jsonify(results) return jsonify(results)
@app.route('/folder/<path:folder_path>')
def folder(folder_path):
folder_full_path = Path(app.config['NOTES_DIR']) / folder_path
if not folder_full_path.is_dir():
return "Folder not found", 404
notes_tree = build_tree_structure(app.config['NOTES_DIR'])
# Get the current folder's contents
current_folder = folder_full_path
folder_contents = []
try:
for item in sorted(current_folder.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
if item.name.startswith('.'): # Skip hidden files
continue
is_dir = item.is_dir()
rel_path = str(item.relative_to(app.config['NOTES_DIR']))
if is_dir or item.suffix == '.md': # Only include directories and markdown files
folder_contents.append({
'name': item.name,
'type': 'dir' if is_dir else 'file',
'path': rel_path,
'display_name': item.name if is_dir else item.stem # Show full name for folders, remove .md for files
})
except Exception as e:
return f"Error reading folder: {str(e)}", 500
return render_template('folder.html',
folder_path=folder_path,
folder_contents=folder_contents,
notes_tree=notes_tree,
active_path=folder_path.split('/'),
current_note=None,
breadcrumbs=generate_breadcrumbs(folder_path))
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True, host=FLASK_HOST, port=FLASK_PORT)
+196
View File
@@ -0,0 +1,196 @@
"""
USAGE:
from app_settings_loader import ensure_settings_ini, get_setting
# Ensure settings.ini exists and is up to date
ensure_settings_ini()
# Get settings as needed
host = get_setting('FLASK', 'FLASK_HOST', fallback='0.0.0.0')
port = get_setting('FLASK', 'FLASK_PORT', fallback=5000, type_=int)
debug = get_setting('FLASK', 'DEBUG', fallback=False, type_=bool)
notes_dir = get_setting('FLASK', 'NOTES_DIR', fallback='notes')
# You can also use the preloaded variables if you want:
# FLASK_HOST, FLASK_PORT, SECRET_KEY, DEBUG, NOTES_DIR, MAX_CONTENT_LENGTH
"""
import configparser
from pathlib import Path
DEFAULT_CONFIG = {
'FLASK': {
'FLASK_HOST': '0.0.0.0',
'FLASK_PORT': '5000',
'SECRET_KEY': 'change-this-secret',
'DEBUG': 'False',
'Specify location where your notes .md files are at - the root folder': None,
'NOTES_DIR': 'notes',
'Maximum content length in MB': None,
'MAX_CONTENT_LENGTH': '16',
},
}
CONFIG_FILE = 'settings.ini'
def ensure_settings_ini():
"""
Ensure that settings.ini exists and contains all required settings.
- If the file does not exist, it is created with default values and comments.
- If the file exists but is missing any required setting, only the missing setting(s)
are added with default values (existing values and comments are not changed).
"""
# First check if file exists at all
if not Path(CONFIG_FILE).exists():
# Create new file with all default settings and comments
with open(CONFIG_FILE, 'w') as f:
for section, values in DEFAULT_CONFIG.items():
f.write(f'[{section}]\n')
# Get all items in order
items = list(values.items())
# Process each item in order
for i, (key, value) in enumerate(items):
if value is not None: # This is a setting
# Find the comment that belongs right before this setting
prev_comment = None
# Look backwards from current position to find the nearest comment
for prev_key, prev_val in reversed(items[:i]):
if prev_val is None: # It's a comment
# Check no other setting exists between comment and current setting
has_setting_between = any(
k for k, v in items[:i]
if v is not None and
prev_key.upper() < k.upper() < key.upper()
)
if not has_setting_between:
prev_comment = prev_key
break
# Write the comment if found
if prev_comment:
f.write(f'; {prev_comment}\n')
# Write the setting
f.write(f'{key} = {value}\n')
f.write('\n')
return
# If file exists, read it line by line to preserve comments and case
with open(CONFIG_FILE, 'r') as f:
lines = f.readlines()
# Parse existing settings while preserving case
config = configparser.ConfigParser()
config.read(CONFIG_FILE)
# Track what settings we've seen and what we need to add
current_section = None
settings_seen = {section: set() for section in DEFAULT_CONFIG}
missing_settings = {}
changed = False
# First pass: identify missing settings while preserving case
for section in DEFAULT_CONFIG:
missing_settings[section] = {}
if section not in config:
# New section needed
changed = True
continue
for key, value in DEFAULT_CONFIG[section].items():
if value is not None: # Skip comments
key_lower = key.lower()
# Check if setting exists (case-insensitive)
if not any(k.lower() == key_lower for k in config[section]):
missing_settings[section][key] = value
changed = True
else:
# Remember the actual case of existing keys
for existing_key in config[section]:
if existing_key.lower() == key_lower:
settings_seen[section].add(existing_key)
if not changed:
return
# Second pass: write the updated file
new_lines = []
current_section = None
for line in lines:
line = line.rstrip('\n')
# Handle section headers
if line.strip().startswith('[') and line.strip().endswith(']'):
if current_section and current_section in missing_settings and missing_settings[current_section]:
# Remove trailing empty lines before adding missing settings
while new_lines and not new_lines[-1].strip():
new_lines.pop()
# Add any missing settings before moving to next section
for key, value in missing_settings[current_section].items():
new_lines.append(f'{key} = {value}')
new_lines.append('') # Single newline after section
current_section = line.strip('[]').strip()
new_lines.append(line)
# If this is a new section, add all its settings
if current_section in DEFAULT_CONFIG and current_section not in config:
for key, value in DEFAULT_CONFIG[current_section].items():
if value is not None: # Only add settings, not comments
new_lines.append(f'{key} = {value}')
new_lines.append('') # Single newline after section
continue
# Skip empty lines at the end of sections
if line.strip() or not current_section:
new_lines.append(line)
# Handle missing settings in the last section
if current_section and current_section in missing_settings and missing_settings[current_section]:
# Remove trailing empty lines before adding missing settings
while new_lines and not new_lines[-1].strip():
new_lines.pop()
# Add missing settings
for key, value in missing_settings[current_section].items():
new_lines.append(f'{key} = {value}')
new_lines.append('') # Single newline after section
# Write the updated file
with open(CONFIG_FILE, 'w') as f:
f.write('\n'.join(new_lines) + '\n')
def get_setting(section, key, fallback=None, type_=str):
"""
Retrieve a setting from settings.ini.
Args:
section (str): The section in the ini file (e.g. 'FLASK').
key (str): The key to retrieve.
fallback: The value to return if the key is not found.
type_ (type): Optionally convert the value to int or bool. Default is str.
Returns:
The value from settings.ini, converted to the requested type if specified.
"""
config = configparser.ConfigParser()
config.read(CONFIG_FILE)
if config.has_option(section, key):
value = config.get(section, key)
if type_ == int:
try:
return int(value)
except Exception:
return fallback if fallback is not None else 0
if type_ == bool:
return value.lower() in ('true', '1', 'yes', 'on')
return value
return fallback
# Load settings from the configuration file settings.ini
FLASK_HOST = get_setting('FLASK', 'FLASK_HOST', fallback='0.0.0.0')
FLASK_PORT = get_setting('FLASK', 'FLASK_PORT', fallback=5000, type_=int)
SECRET_KEY = get_setting('FLASK', 'SECRET_KEY', fallback='change-this')
DEBUG = get_setting('FLASK', 'DEBUG', fallback='INFO')
NOTES_DIR = Path(get_setting('FLASK', 'NOTES_DIR', fallback='notes')).resolve()
MAX_CONTENT_LENGTH = get_setting('FLASK', 'MAX_CONTENT_LENGTH', fallback=16, type_=int)
+10
View File
@@ -0,0 +1,10 @@
## Hello You dubas :D
- blower sux out
```python
print("Not today")
def run_over()
geto = os.pwd()
return geto
```
+10
View File
@@ -0,0 +1,10 @@
[FLASK]
FLASK_HOST = 0.0.0.0
FLASK_PORT = 5000
SECRET_KEY = change-this-secret
DEBUG = False
; Specify location where your notes .md files are at - the root folder
NOTES_DIR = notes
; Maximum content length in MB
MAX_CONTENT_LENGTH = 16
+4 -1
View File
@@ -233,4 +233,7 @@ color: #6c757d;
} }
.file-tree-children { .file-tree-children {
margin-left: 0; margin-left: 0;
} }
#sidebar {
padding-bottom: 2.5rem !important;
}
+203 -6
View File
@@ -1,10 +1,55 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Create Note - Flask Blog{% endblock %} {% block title %}Create Note - Flask Blog{% endblock %}
{% block head %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<style>
.folder-suggestion {
padding: 0.5rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
}
.folder-suggestion:hover {
background-color: rgba(255,255,255,0.1);
}
.folder-suggestion i {
opacity: 0.7;
width: 16px;
}
.folder-suggestion .folder-path {
opacity: 0.8;
font-size: 0.9em;
margin-left: 0.5rem;
color: #d0d0d0;
}
.dropdown-menu.show {
display: block;
background: #2d2d2d;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 4px;
margin-top: 4px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<form method="post"> <form method="post">
<div class="mb-3"> <div class="mb-3 position-relative">
<label for="title" class="form-label">Title</label> <label for="note-path" class="form-label">Note path</label>
<input id="title" name="title" class="form-control" required> <div class="input-group">
<input type="text" id="note-path" class="form-control" placeholder="folder/subfolder/title"
autocomplete="off" autocapitalize="off" spellcheck="false"
value="{{ current_folder + '/' if current_folder else '' }}">
<input type="hidden" id="title" name="title">
<input type="hidden" id="path" name="path">
</div>
<div id="folder-suggestions" class="dropdown-menu" style="width: 100%; max-height: 300px; overflow-y: auto;"></div>
<div class="form-text" style="color: aliceblue;">Type '/' for folder autocomplete. The last segment will be the note title.</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="content" class="form-label">Content</label> <label for="content" class="form-label">Content</label>
@@ -14,11 +59,163 @@
<a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
</form> </form>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/json.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script> <script>
const easyMDE = new EasyMDE({ // Make folders data available to JavaScript
element: document.getElementById('content') window.availableFolders = {{ available_folders|tojson|safe }};
});
// Initialize highlight.js
window.hljs = hljs;
// Configure highlight.js with secure options
hljs.configure({
ignoreUnescapedHTML: true,
throwUnescapedHTML: false,
languages: ['python', 'javascript', 'bash', 'markdown', 'json']
});
const easyMDE = new EasyMDE({
element: document.getElementById('content'),
renderingConfig: {
codeSyntaxHighlighting: true,
},
previewRender: function(plainText) {
// First, escape the markdown using EasyMDE's default renderer
let preview = this.parent.markdown(plainText);
// After the markdown is rendered, initialize highlighting on any code blocks
setTimeout(() => {
document.querySelectorAll('pre code').forEach((block) => {
// Remove all classes that might be from previous highlights
block.className = block.className.replace(/hljs-.*\s*/g, '');
// Get the language if specified in the class
const language = Array.from(block.classList)
.find(cls => cls.startsWith('language-'))
?.substring(9);
try {
// Apply highlighting using the current API
if (language) {
block.className = `language-${language}`;
hljs.highlightElement(block);
} else {
hljs.highlightElement(block);
}
} catch (e) {
console.warn('Error highlighting block:', e);
}
});
}, 0);
return preview;
}
});
// GitHub-style path input handler
document.addEventListener('DOMContentLoaded', function() {
const pathInput = document.getElementById('note-path');
const titleInput = document.getElementById('title');
const pathHiddenInput = document.getElementById('path');
const suggestionsDiv = document.getElementById('folder-suggestions');
const folders = availableFolders;
let currentPath = '';
let lastKeyWasSlash = false;
function showSuggestions(searchPath) {
const parts = searchPath.split('/');
const searchTerm = parts[parts.length - 1].toLowerCase();
const parentPath = parts.slice(0, -1).join('/');
// Filter folders that match the current path and search term
const matches = folders.filter(folder => {
if (parentPath) {
return folder.path.startsWith(parentPath + '/') &&
folder.name.toLowerCase().includes(searchTerm);
}
return folder.name.toLowerCase().includes(searchTerm);
});
if (matches.length > 0) {
suggestionsDiv.innerHTML = matches.map(folder => `
<div class="folder-suggestion" data-path="${folder.path}">
<i class="fa fa-folder text-warning"></i>
<span>${folder.name}</span>
<span class="folder-path">/${folder.path}</span>
</div>
`).join('');
suggestionsDiv.classList.add('show');
} else {
suggestionsDiv.classList.remove('show');
}
}
function hideSuggestions() {
suggestionsDiv.classList.remove('show');
}
function updateInputs(value) {
const parts = value.split('/');
const title = parts.pop() || '';
const path = parts.join('/');
titleInput.value = title;
pathHiddenInput.value = path;
}
pathInput.addEventListener('input', function(e) {
const value = e.target.value;
updateInputs(value);
if (lastKeyWasSlash || value.includes('/')) {
showSuggestions(value);
} else {
hideSuggestions();
}
lastKeyWasSlash = value.endsWith('/');
});
pathInput.addEventListener('keydown', function(e) {
if (e.key === '/') {
lastKeyWasSlash = true;
} else {
lastKeyWasSlash = false;
}
});
// Handle suggestion clicks
suggestionsDiv.addEventListener('click', function(e) {
const suggestion = e.target.closest('.folder-suggestion');
if (suggestion) {
const folderPath = suggestion.dataset.path;
pathInput.value = folderPath + '/';
updateInputs(pathInput.value);
hideSuggestions();
pathInput.focus();
}
});
// Close suggestions when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('#note-path') && !e.target.closest('#folder-suggestions')) {
hideSuggestions();
}
});
// Handle initial value if any
if (pathInput.value) {
updateInputs(pathInput.value);
}
});
</script> </script>
{% endblock %} {% endblock %}
+56 -2
View File
@@ -2,6 +2,10 @@
{% block title %}Edit {{ title }} - Flask Blog{% endblock %} {% block title %}Edit {{ title }} - Flask Blog{% endblock %}
{% block head %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
{% endblock %}
{% block content %} {% block content %}
<form method="post"> <form method="post">
<div class="mb-3"> <div class="mb-3">
@@ -14,10 +18,60 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/json.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script> <script>
const easyMDE = new EasyMDE({ // Initialize highlight.js
element: document.getElementById('content') window.hljs = hljs;
// Configure highlight.js with secure options
hljs.configure({
ignoreUnescapedHTML: true,
throwUnescapedHTML: false,
languages: ['python', 'javascript', 'bash', 'markdown', 'json']
});
const easyMDE = new EasyMDE({
element: document.getElementById('content'),
renderingConfig: {
codeSyntaxHighlighting: true,
},
previewRender: function(plainText) {
// First, escape the markdown using EasyMDE's default renderer
let preview = this.parent.markdown(plainText);
// After the markdown is rendered, initialize highlighting on any code blocks
setTimeout(() => {
document.querySelectorAll('pre code').forEach((block) => {
// Remove all classes that might be from previous highlights
block.className = block.className.replace(/hljs-.*\s*/g, '');
// Get the language if specified in the class
const language = Array.from(block.classList)
.find(cls => cls.startsWith('language-'))
?.substring(9);
try {
// Apply highlighting using the current API
if (language) {
block.className = `language-${language}`;
hljs.highlightElement(block);
} else {
hljs.highlightElement(block);
}
} catch (e) {
console.warn('Error highlighting block:', e);
}
});
}, 0);
return preview;
}
}); });
</script> </script>
{% endblock %} {% endblock %}
+17
View File
@@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block title %}Error{% endblock %}
{% block content %}
<div class="container py-5">
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">An error occurred</h4>
<p>{{ message|safe }}</p>
{% if error and error.code %}
<hr>
<p class="mb-0">Error code: {{ error.code }}</p>
{% endif %}
<a href="{{ url_for('index') }}" class="btn btn-secondary mt-3">Back to Home</a>
</div>
</div>
{% endblock %}
+34
View File
@@ -0,0 +1,34 @@
{% extends 'base.html' %}
{% block title %}{% if folder_path %}{{ folder_path }}{% else %}Home{% endif %} - Flask Blog{% endblock %}
{% block content %}
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="m-0">{% if folder_path %}Folder: {{ folder_path }}{% else %}All Notes{% endif %}</h4>
<a href="{{ url_for('create_note') }}" class="btn btn-primary btn-sm">New Note</a>
</div>
{% if folder_contents %}
<div class="list-group">
{% for item in folder_contents %}
{% if item.type == 'dir' %}
<a href="{{ url_for('folder', folder_path=item.path) }}"
class="list-group-item list-group-item-action d-flex align-items-center">
<i class="fa fa-folder text-warning me-2"></i>
<span>{{ item.display_name }}</span>
</a>
{% else %}
<a href="{{ url_for('note', note_path=item.path) }}"
class="list-group-item list-group-item-action d-flex align-items-center">
<i class="fa fa-file-text-o text-secondary me-2"></i>
<span>{{ item.display_name }}</span>
</a>
{% endif %}
{% endfor %}
</div>
{% else %}
<p class="text-muted">This folder is empty.</p>
{% endif %}
</div>
{% endblock %}
+1 -2
View File
@@ -4,8 +4,7 @@
{% block content %} {% block content %}
<div class="mb-3"> <div class="mb-3">
<h1>{{ title }}</h1> <div class="mb-3">
<div class="mb-4">
<a href="{{ url_for('edit_note', note_path=current_note) }}" class="btn btn-outline-secondary btn-sm">Edit</a> <a href="{{ url_for('edit_note', note_path=current_note) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
<form action="{{ url_for('delete_note', note_path=current_note) }}" method="post" style="display:inline;"> <form action="{{ url_for('delete_note', note_path=current_note) }}" method="post" style="display:inline;">
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Delete this note?');">Delete</button> <button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Delete this note?');">Delete</button>