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

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
notes/**
notes/**/
__pycache__/
*.py[cod]
*.pyo

6
README.md Normal file
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.

219
app.py
View File

@@ -1,21 +1,31 @@
# 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 mistune
from pathlib import Path
import uuid
import re
from urllib.parse import quote
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
from app_settings_loader import ensure_settings_ini, FLASK_HOST, FLASK_PORT, SECRET_KEY, DEBUG, NOTES_DIR, MAX_CONTENT_LENGTH
app = Flask(__name__)
app.config['NOTES_DIR'] = Path('notes') # Store Markdown files here
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
ensure_settings_ini()
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
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
crumbs.append({
'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 title:
crumbs.append({'name': title, 'url': None})
else:
crumbs.append({'name': parts[-1][:-3], 'url': None}) # Remove .md extension
name = parts[-1][:-3] if parts[-1].endswith('.md') else parts[-1]
url = None if parts[-1].endswith('.md') else url_for('folder', folder_path=current_path + '/' + parts[-1] if current_path else parts[-1])
crumbs.append({'name': name, 'url': url})
return crumbs
@app.route('/')
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'])
return render_template('index.html',
return render_template('folder.html',
folder_path='',
folder_contents=folder_contents,
notes_tree=notes_tree,
active_path=[],
current_note=None,
@@ -105,9 +135,6 @@ def note(note_path):
try:
with open(full_path, 'r', encoding='utf-8') as f:
content = f.read()
title = full_path.stem
if '##' in content:
title = content.split('##')[1].split('\n')[0].strip()
# Convert markdown to HTML
renderer = mistune.HTMLRenderer()
@@ -117,6 +144,8 @@ def note(note_path):
# Build tree and get path components
notes_tree = build_tree_structure(app.config['NOTES_DIR'])
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)
return render_template('note.html',
@@ -149,9 +178,6 @@ def edit_note(note_path):
with open(full_path, 'r', encoding='utf-8') as f:
content = f.read()
title = full_path.stem
if '##' in content:
title = content.split('##')[1].split('\n')[0].strip()
notes_tree = build_tree_structure(app.config['NOTES_DIR'])
active_path = get_path_components(note_path)
breadcrumbs = generate_breadcrumbs(note_path, f"Edit {title}")
@@ -166,32 +192,96 @@ def edit_note(note_path):
except Exception as e:
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'])
def create_note():
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()
path = request.form.get('path', '').strip()
# Generate filename
filename = f"{generate_slug(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
# Use the title as the file name
filename = f"{title}.md"
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(f"## {title}\n\n{content}")
return redirect(url_for('note', note_path=str(file_path.relative_to(app.config['NOTES_DIR']))))
except Exception as e:
return f"Error creating note: {str(e)}", 500
# Safely resolve the full path to ensure it's within NOTES_DIR
if path:
dir_path = (Path(app.config['NOTES_DIR']) / path).resolve()
if not str(dir_path).startswith(str(app.config['NOTES_DIR'].resolve())):
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'])
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'])
def delete_note(note_path):
@@ -222,10 +312,7 @@ def search():
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if query in content.lower():
title = file_path.stem
if '##' in content:
title = content.split('##')[1].split('\n')[0].strip()
title = file_path.stem # Always use file name without .md
# Find context for the match
pos = content.lower().find(query)
start = max(0, pos - 50)
@@ -242,5 +329,43 @@ def search():
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__':
app.run(debug=True)
app.run(debug=True, host=FLASK_HOST, port=FLASK_PORT)

196
app_settings_loader.py Normal file
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
notes/hello-world.md Normal file
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
settings.ini Normal file
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

View File

@@ -233,4 +233,7 @@ color: #6c757d;
}
.file-tree-children {
margin-left: 0;
}
}
#sidebar {
padding-bottom: 2.5rem !important;
}

View File

@@ -1,10 +1,55 @@
{% extends 'base.html' %}
{% 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 %}
<form method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input id="title" name="title" class="form-control" required>
<div class="mb-3 position-relative">
<label for="note-path" class="form-label">Note path</label>
<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 class="mb-3">
<label for="content" class="form-label">Content</label>
@@ -14,11 +59,163 @@
<a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
</form>
{% endblock %}
{% 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>
const easyMDE = new EasyMDE({
element: document.getElementById('content')
});
// Make folders data available to JavaScript
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>
{% endblock %}

View File

@@ -2,6 +2,10 @@
{% 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 %}
<form method="post">
<div class="mb-3">
@@ -14,10 +18,60 @@
{% endblock %}
{% 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>
const easyMDE = new EasyMDE({
element: document.getElementById('content')
// 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;
}
});
</script>
{% endblock %}

17
templates/error.html Normal file
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
templates/folder.html Normal file
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 %}

View File

@@ -4,8 +4,7 @@
{% block content %}
<div class="mb-3">
<h1>{{ title }}</h1>
<div class="mb-4">
<div class="mb-3">
<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;">
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Delete this note?');">Delete</button>