mvc #1

Merged
hjess merged 2 commits from mvc into main 2024-12-11 23:59:52 +01:00
29 changed files with 647 additions and 151 deletions

103
app.py
View File

@@ -1,100 +1,5 @@
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from contextlib import asynccontextmanager
import json
import os
from markdown_render import render_markdown_with_jinja
import uvicorn
from app.main import app
class App:
def __init__(self):
"""Initialize the FastAPI app."""
self.app = FastAPI(lifespan=self.lifespan)
self.templates = Jinja2Templates(directory="templates")
self.data = self.load_mock_data()
# Mount directories
self.app.mount("/data", StaticFiles(directory="data"), name="data")
self.app.mount("/static", StaticFiles(directory="static"), name="static")
# Add routes
self.add_routes()
# 1. Lifespan events
@asynccontextmanager
async def lifespan(self, app: FastAPI):
print("App startup: Processing Markdown files...")
self.process_markdown_files("./data", "./data") # Process all Markdown files
print("Markdown processing complete!")
yield # Allow the app to start
print("App shutdown: Cleanup complete.")
# 2. Load JSON data
def load_mock_data(self):
"""Load mock data from a JSON file."""
with open("mock_data.json") as file:
return json.load(file)
# 3. Markdown processing logic
@staticmethod
def process_markdown_files(input_dir: str, output_dir: str):
"""
Recursively process all Markdown files in the input directory,
render them to HTML, and save them in the output directory.
"""
for root, _, files in os.walk(input_dir):
for file in files:
if file.endswith(".md"):
input_file_path = os.path.join(root, file)
relative_path = os.path.relpath(input_file_path, input_dir)
output_file_path = os.path.join(output_dir, os.path.splitext(relative_path)[0] + ".html")
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
with open(input_file_path, "r", encoding="utf-8") as md_file:
markdown_content = md_file.read()
print(f"Processing: {input_file_path} -> {output_file_path}")
rendered_html = render_markdown_with_jinja(markdown_content)
with open(output_file_path, "w", encoding="utf-8") as html_file:
html_file.write(rendered_html)
# 4. Routes
def add_routes(self):
"""Add all routes to the FastAPI app."""
@self.app.get("/", response_class=HTMLResponse)
async def get_index(request: Request):
"""Index route."""
return self.templates.TemplateResponse(
"index.html",
{"request": request, "data": self.data, "page_title": "Forside", "author": "Henrik"},
)
@self.app.get("/category/{category_name}", response_class=HTMLResponse)
async def get_category(request: Request, category_name: str):
"""Category route."""
category = next((cat for cat in self.data["categories"] if cat["path"] == category_name), None)
if category:
category_file = f"data/{category_name}/index.html"
if os.path.exists(category_file):
with open(category_file) as file:
category_content = file.read()
return self.templates.TemplateResponse(
"category.html",
{
"request": request,
"data": self.data,
"page_title": category["name"],
"author": category["author"],
"content": category_content,
},
)
return HTMLResponse("Kategori ikke fundet", status_code=404)
# Create the app instance
app_instance = App()
app = app_instance.app
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)

0
app/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,56 @@
import os
import json
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
class CategoryController:
def __init__(self):
"""Initialize the controller."""
self.router = APIRouter()
self.templates = Jinja2Templates(directory="templates")
self.data = self._load_mock_data()
self._add_routes()
def _load_mock_data(self):
"""Load mock data from a JSON file."""
with open("mock_data.json") as file:
return json.load(file)
def _add_routes(self):
"""Add routes to the router."""
self.router.add_api_route("/", self.get_index, methods=["GET"], response_class=HTMLResponse)
self.router.add_api_route(
"/category/{category_name}",
self.get_category,
methods=["GET"],
response_class=HTMLResponse,
)
async def get_index(self, request: Request):
"""Index route."""
return self.templates.TemplateResponse(
"index.html",
{"request": request, "data": self.data, "page_title": "Forside", "author": "Henrik"},
)
async def get_category(self, request: Request, category_name: str):
"""Category route."""
category = next((cat for cat in self.data["categories"] if cat["path"] == category_name), None)
if category:
category_file = f"data/{category_name}/index.html"
if os.path.exists(category_file):
with open(category_file) as file:
category_content = file.read()
return self.templates.TemplateResponse(
"category.html",
{
"request": request,
"data": self.data,
"page_title": category["name"],
"author": category["author"],
"content": category_content,
},
)
return HTMLResponse("Kategori ikke fundet", status_code=404)

View File

@@ -0,0 +1,65 @@
import os
import json
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, Response
from fastapi.templating import Jinja2Templates
class DynamicController:
def __init__(self, data_dir: str):
"""Initialize the dynamic controller."""
self.router = APIRouter()
self.templates = Jinja2Templates(directory="templates")
self.data_dir = data_dir
self.data = self._load_mock_data()
self._add_dynamic_routes()
def _load_mock_data(self):
"""Load mock data from a JSON file."""
with open("mock_data.json") as file:
return json.load(file)
def _add_dynamic_routes(self):
"""Scan data directory and create dynamic routes."""
for root, dirs, files in os.walk(self.data_dir):
for directory in dirs:
route_path = f"/{directory}" # Create route based on directory name
directory_path = os.path.join(root, directory)
# Register route dynamically
self.router.add_api_route(
route_path,
self._serve_dynamic_template(directory, directory_path),
methods=["GET"],
response_class=HTMLResponse,
)
def _serve_dynamic_template(self, route_name: str, directory_path: str):
"""Closure to serve templates for each route."""
async def route_handler(request: Request):
# Look for index.html or render fallback content
index_html = os.path.join(directory_path, "index.html")
if os.path.exists(index_html):
with open(index_html, "r", encoding="utf-8") as file:
content = file.read()
# Find the author for this route from preloaded data
for category in self.data.get("categories", []):
if category["path"] == route_name:
author_name = category["author"]
break
# Pass required data to the template
return self.templates.TemplateResponse(
"category.html",
{
"request": request,
"page_title": route_name.capitalize(),
"content": content,
"author": author_name,
"data": self.data, # Pass additional data if needed
},
)
# Fallback: Return a 404 if no content is found
return Response(f"No content found for {route_name}", status_code=404)
return route_handler

53
app/main.py Normal file
View File

@@ -0,0 +1,53 @@
from fastapi import FastAPI
from contextlib import asynccontextmanager
from fastapi.staticfiles import StaticFiles
from app.services.markdown_processor import MarkdownProcessor
from app.services.metadata_processor import MetadataProcessor
from app.controllers.dynamic_controller import DynamicController
from app.controllers.category_controller import CategoryController
class Application:
def __init__(self):
"""Initialize the FastAPI app and configure it."""
self.app = FastAPI(lifespan=self._lifespan_event)
self._setup_static_files()
self._include_routers()
@asynccontextmanager
async def _lifespan_event(self, app: FastAPI):
"""Lifespan event for startup and shutdown logic."""
print("App startup: Processing Markdown files...")
# Process Markdown files into HTML
processor = MarkdownProcessor(input_dir="./data", templates_dir="./templates")
processor.run()
# Generate dynamic JSON data
metadata_processor = MetadataProcessor(input_dir="./data", output_file="generated_data.json")
metadata_processor.generate_json()
print("Generated dynamic data file.")
print("Markdown processing complete!")
yield
print("App shutdown: Cleanup complete.")
def _setup_static_files(self):
"""Mount static file directories."""
self.app.mount("/data", StaticFiles(directory="data"), name="data")
self.app.mount("/static", StaticFiles(directory="static"), name="static")
def _include_routers(self):
"""Include all route controllers."""
category_controller = CategoryController()
dynamic_controller = DynamicController("./data")
self.app.include_router(category_controller.router)
self.app.include_router(dynamic_controller.router)
def get_app(self):
"""Return the FastAPI app instance."""
return self.app
application = Application()
app = application.get_app()

0
app/services/__init__.py Normal file
View File

View File

@@ -0,0 +1,87 @@
import os
from app.services.markdown_render import render_markdown_with_jinja # Your custom renderer
from jinja2 import Environment, FileSystemLoader
class MarkdownProcessor:
"""
A class to process Markdown files, extract metadata, and generate a single
'index.html' per category directory using a custom rendering engine.
"""
def __init__(self, input_dir: str, templates_dir: str):
"""
Initialize the MarkdownProcessor.
Args:
input_dir (str): Root directory containing category subdirectories.
templates_dir (str): Directory containing Jinja2 templates.
"""
self.input_dir = input_dir
self.env = Environment(loader=FileSystemLoader(templates_dir))
def _process_markdown_files_in_directory(self, directory_path: str) -> list:
"""
Process all Markdown files in a directory using Markdown and Jinja2 custom tags.
Args:
directory_path (str): Path to the category directory.
Returns:
list: A list of processed sections containing metadata and rendered content.
"""
sections = []
for file in sorted(os.listdir(directory_path)):
if file.endswith(".md"):
file_path = os.path.join(directory_path, file)
with open(file_path, "r", encoding="utf-8") as md_file:
markdown_content = md_file.read()
# Process Markdown and Jinja2
rendered_content, metadata = render_markdown_with_jinja(markdown_content)
# Append the section to the list
sections.append({
"name": metadata.get("title", "Untitled"),
"content": rendered_content,
"summary": metadata.get("summary", ""),
"author": metadata.get("author", "Unknown"),
})
return sections
def _generate_index_html(self, directory_path: str, sections: list, output_file: str):
"""
Generate the index.html file for a category using the combined sections.
Args:
directory_path (str): Path to the category directory.
sections (list): List of processed Markdown content and metadata.
output_file (str): Path to save the generated index.html.
"""
# Render the template with the combined sections
template = self.env.get_template("combined_template.html")
rendered_html = template.render(
title=os.path.basename(directory_path).capitalize(),
sections=sections
)
# Write the rendered HTML to index.html
os.makedirs(directory_path, exist_ok=True)
with open(output_file, "w", encoding="utf-8") as output:
output.write(rendered_html)
print(f"Generated: {output_file}")
def run(self):
"""
Run the Markdown processing workflow: one 'index.html' per category.
"""
for root, dirs, _ in os.walk(self.input_dir):
for directory in dirs:
category_path = os.path.join(root, directory)
output_file = os.path.join(category_path, "index.html")
# Process all Markdown files in the current category directory
sections = self._process_markdown_files_in_directory(category_path)
if sections:
self._generate_index_html(category_path, sections, output_file)

View File

@@ -1,6 +1,7 @@
from jinja2 import Environment, FileSystemLoader
import markdown
from jinja2 import Environment, DictLoader
# Define Jinja2 custom functions
def img_left_overlay(src):
"""Render an image with overlay."""
return f'''
@@ -35,9 +36,9 @@ def warning(content):
</div>
'''
def create_jinja_environment(template_dir="templates"):
def create_jinja_environment():
"""Set up Jinja2 environment and register custom components."""
env = Environment(loader=FileSystemLoader(template_dir))
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
env.globals.update({
"img_left_overlay": img_left_overlay,
"box": box,
@@ -46,14 +47,24 @@ def create_jinja_environment(template_dir="templates"):
})
return env
def render_markdown_with_jinja(markdown_content, template_data=None):
"""Process Markdown first, then inject Jinja2 components."""
# Step 1: Convert Markdown to HTML
md_html = markdown.markdown(markdown_content, extensions=['extra', 'nl2br'])
def render_markdown_with_jinja(markdown_content: str):
"""
Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
# Step 2: Use Jinja2 to inject components into the already-rendered HTML
Args:
markdown_content (str): Raw Markdown content.
Returns:
tuple: Rendered HTML content and metadata as a dictionary.
"""
# Step 1: Convert Markdown to HTML and extract metadata
md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
html_content = md.convert(markdown_content)
metadata = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
# Step 2: Render the HTML with Jinja2 to apply custom tags
env = create_jinja_environment()
template = env.from_string(md_html)
rendered_html = template.render(template_data or {})
template = env.get_template("base_template")
final_html = template.render(content=html_content)
return rendered_html
return final_html, metadata

View File

@@ -0,0 +1,83 @@
import os
import markdown
import json
from typing import List, Dict
class MetadataProcessor:
"""
A class to scan Markdown files, extract front matter metadata,
and generate a structured JSON file.
"""
def __init__(self, input_dir: str, output_file: str):
"""
Initialize the MetadataProcessor.
Args:
input_dir (str): Directory containing Markdown files.
output_file (str): Path to save the generated JSON file.
"""
self.input_dir = input_dir
self.output_file = output_file
self.data = {"categories": [], "favorites": []}
def _extract_metadata(self, file_path: str) -> Dict:
"""
Extract front matter metadata using the 'markdown' package.
Args:
file_path (str): Path to the Markdown file.
Returns:
dict: A dictionary containing the extracted metadata.
"""
with open(file_path, "r", encoding="utf-8") as file:
markdown_content = file.read()
# Initialize Markdown with meta extension
md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
md.convert(markdown_content)
# Metadata is stored in md.Meta as a dictionary of lists
meta = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
return meta
def _process_directory(self):
"""
Recursively scan the input directory for Markdown files
and extract metadata to build the JSON structure.
"""
for root, _, files in os.walk(self.input_dir):
for file in files:
if file.endswith(".md"):
file_path = os.path.join(root, file)
metadata = self._extract_metadata(file_path)
if metadata:
# Add to 'categories'
self.data["categories"].append({
"name": metadata.get("name", "Unknown"),
"path": os.path.relpath(root, self.input_dir).replace(os.sep, "/"),
"author": metadata.get("author", "Unknown")
})
# Add to 'favorites' if 'favorite' is true
if metadata.get("favorite") and metadata["favorite"].lower() == "true":
self.data["favorites"].append({
"name": metadata.get("name", "Unknown"),
"image": metadata.get("image", "images/default.jpg"),
"description": metadata.get("summary", "No description provided")
})
def generate_json(self):
"""
Generate the JSON structure and save it to the output file.
"""
self._process_directory()
# Save JSON to file
with open(self.output_file, "w", encoding="utf-8") as json_file:
json.dump(self.data, json_file, indent=4, ensure_ascii=False)
print(f"Generated JSON saved to {self.output_file}")

View File

@@ -1 +1,16 @@
<P> Lidt om Bolig </P>
<main>
<section>
<h2>Untitled</h2>
<p><strong>Summary:</strong> Lidt omkring job situationen og hvordan det fungere</p>
<p><strong>Author:</strong> Henrik Jess</p>
<div>
<h1>Bolig Bolig Bolig Bolig - Hvor skal sengen placeres</h1>
<p>Nu bliver det spænde!</p>
<p>{{ note("Dette er stadig en test side") }}</p>
<p>{img-left-overlay: images/my-cat.png}</p>
</div>
<hr>
</section>
</main>

View File

@@ -1,10 +0,0 @@
<p>PortugalFAQ - Henriks og Erikas lille side</p>
<p></p>
<h1>BoligBolig Bolig Bolig - Hvor skal sengen placeres</h1>
<p>Nu bliver det spænde!</p>
<p>
<div class="note">
<p>Dette er stadig en test side</p>
</div>
</p>
<p></p>

View File

@@ -1,13 +1,17 @@
---
name: Generalt
description: Hvem, hvad og hvor
author: Henrik Jess
date: ons 11 dec 22:16:13 CET 2024
summary: Lidt omkring job situationen og hvordan det fungere
favorite: true
image: images/pic07.jpg
---
{% block title %}PortugalFAQ - Henriks og Erikas lille side{% endblock %}
{% block content %}
# BoligBolig Bolig Bolig - Hvor skal sengen placeres
# Bolig Bolig Bolig Bolig - Hvor skal sengen placeres
Nu bliver det spænde!
{{ note("Dette er stadig en test side") }}
{% endblock %}
{img-left-overlay: images/my-cat.png}

View File

@@ -1,7 +0,0 @@
<h1>Lidt mere info om job</h1>
<p>Der skal langt mere tekst her</p>
<p>
<div class="note">
<p>Husk alpha side</p>
</div>
</p>

View File

@@ -1,3 +1,13 @@
---
name: Job
description: Hvem, hvad og hvor
author: Henrik Jess
date: ons 11 dec 22:16:13 CET 2024
summary: Lidt omkring job situationen og hvordan det fungere
favorite: true
image: images/pic04.jpg
---
# Lidt mere info om job
Der skal langt mere tekst her

View File

@@ -1 +1,15 @@
<P> Lidt om job </P>
<main>
<section>
<h2>Untitled</h2>
<p><strong>Summary:</strong> Lidt omkring job situationen og hvordan det fungere</p>
<p><strong>Author:</strong> Henrik Jess</p>
<div>
<h1>Lidt mere info om job</h1>
<p>Der skal langt mere tekst her</p>
<p>{{ note("Husk alpha side") }}</p>
</div>
<hr>
</section>
</main>

15
data/kontor/index.html Normal file
View File

@@ -0,0 +1,15 @@
<main>
<section>
<h2>Untitled</h2>
<p><strong>Summary:</strong> Lad os snakke kontor fælleskaber</p>
<p><strong>Author:</strong> Henrik Jess</p>
<div>
<h1>Kontorfællesskab!</h1>
<p>Der skal langt mere tekst her</p>
<p>{{ note("Husk alpha side") }}</p>
</div>
<hr>
</section>
</main>

View File

@@ -0,0 +1,16 @@
---
name: Job
description: Kontorfælleskaber osv
author: Henrik Jess
date: today
summary: Lad os snakke kontor fælleskaber
favorite: false
image: images/pic05.jpg
---
# Kontorfællesskab!
Der skal langt mere tekst her
{{ note("Husk alpha side") }}

View File

@@ -1 +1,14 @@
<P> Lidt om skat </P>
<main>
<section>
<h2>Untitled</h2>
<p><strong>Summary:</strong> Jeg er langt fra expert, men her er lidt hvad jeg har indsamlet omkring skat</p>
<p><strong>Author:</strong> Henrik Jess</p>
<div>
<h1>Skat! - Det skal jo også være sjovt og leve</h1>
<p>dette er mere tekst omkring skat</p>
</div>
<hr>
</section>
</main>

View File

@@ -0,0 +1,13 @@
---
name: Skat
description: SKAT,SKAT, SKAT - Det skal betales den slags
author: Henrik Jess
date: today
summary: Jeg er langt fra expert, men her er lidt hvad jeg har indsamlet omkring skat
favorite: false
image: images/pic07.jpg
---
# Skat! - Det skal jo også være sjovt og leve
dette er mere tekst omkring skat

View File

@@ -1 +1,16 @@
<P> Lidt om skole </P>
<main>
<section>
<h2>Untitled</h2>
<p><strong>Summary:</strong> Nørj det er lidt spændende..</p>
<p><strong>Author:</strong> Erika Nielsen</p>
<div>
<h1>Skole start!</h1>
<p>dette er mere tekst omkring skole</p>
<h1>Skole efter lidt tid</h1>
<p>HA!</p>
</div>
<hr>
</section>
</main>

17
data/skole/skole.md Normal file
View File

@@ -0,0 +1,17 @@
---
name: Skole
description: Skole, ny kultur og menesker
author: Erika Nielsen
date: today
summary: Nørj det er lidt spændende..
favorite: false
image: images/pic07.jpg
---
# Skole start!
dette er mere tekst omkring skole
# Skole efter lidt tid
HA!

View File

@@ -1,5 +1,5 @@
import os
from markdown_render import render_markdown_with_jinja
from app.services.markdown_render import render_markdown_with_jinja
def process_markdown_files(input_dir: str, output_dir: str):
"""

View File

@@ -0,0 +1,25 @@
import os
from app.services.markdown_render import render_markdown_with_jinja
def process_markdown_files(input_dir: str, output_dir: str):
"""
Recursively process all Markdown files in the input directory,
render them to HTML, and save them in the output directory.
"""
for root, _, files in os.walk(input_dir):
for file in files:
if file.endswith(".md"):
input_file_path = os.path.join(root, file)
relative_path = os.path.relpath(input_file_path, input_dir)
output_file_path = os.path.join(output_dir, os.path.splitext(relative_path)[0] + ".html")
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
with open(input_file_path, "r", encoding="utf-8") as md_file:
markdown_content = md_file.read()
print(f"Processing: {input_file_path} -> {output_file_path}")
rendered_html = render_markdown_with_jinja(markdown_content)
with open(output_file_path, "w", encoding="utf-8") as html_file:
html_file.write(rendered_html)

13
depricated/mock_data.json Normal file
View File

@@ -0,0 +1,13 @@
{
"categories": [
{"name": "SKAT", "path": "skat", "author": "Henrik"},
{"name": "Skole", "path": "skole", "author": "Erika"},
{"name": "Bolig", "path": "bolig", "author": "Henrik"},
{"name": "Job", "path": "job", "author": "Henrik"}
],
"favorites": [
{"name": "SKAT", "image": "images/pic07.jpg", "description": "Favorit Kategori"},
{"name": "Skole", "image": "images/pic08.jpg", "description": "Skole information"},
{"name": "Bolig", "image": "images/pic09.jpg", "description": "Bolig detaljer"}
]
}

View File

@@ -1,4 +1,4 @@
from markdown_render import MarkdownRenderer
from app.services.markdown_render import MarkdownRenderer
# Initialize MarkdownRenderer
renderer = MarkdownRenderer()

41
generated_data.json Normal file
View File

@@ -0,0 +1,41 @@
{
"categories": [
{
"name": "Skole",
"path": "skole",
"author": "Erika Nielsen"
},
{
"name": "Generalt",
"path": "bolig",
"author": "Henrik Jess"
},
{
"name": "Job",
"path": "job",
"author": "Henrik Jess"
},
{
"name": "Skat",
"path": "skat",
"author": "Henrik Jess"
},
{
"name": "Job",
"path": "kontor",
"author": "Henrik Jess"
}
],
"favorites": [
{
"name": "Generalt",
"image": "images/pic07.jpg",
"description": "Lidt omkring job situationen og hvordan det fungere"
},
{
"name": "Job",
"image": "images/pic04.jpg",
"description": "Lidt omkring job situationen og hvordan det fungere"
}
]
}

View File

@@ -1,13 +1,41 @@
{
"categories": [
{"name": "SKAT", "path": "skat", "author": "Henrik"},
{"name": "Skole", "path": "skole", "author": "Erika"},
{"name": "Bolig", "path": "bolig", "author": "Henrik"},
{"name": "Job", "path": "job", "author": "Henrik"}
{
"name": "Skole",
"path": "skole",
"author": "Erika Nielsen"
},
{
"name": "Generalt",
"path": "bolig",
"author": "Henrik Jess"
},
{
"name": "Job",
"path": "job",
"author": "Henrik Jess"
},
{
"name": "Skat",
"path": "skat",
"author": "Henrik Jess"
},
{
"name": "Job",
"path": "kontor",
"author": "Henrik Jess"
}
],
"favorites": [
{"name": "SKAT", "image": "images/pic07.jpg", "description": "Favorit Kategori"},
{"name": "Skole", "image": "images/pic08.jpg", "description": "Skole information"},
{"name": "Bolig", "image": "images/pic09.jpg", "description": "Bolig detaljer"}
{
"name": "Generalt",
"image": "images/pic07.jpg",
"description": "Lidt omkring job situationen og hvordan det fungere"
},
{
"name": "Job",
"image": "images/pic04.jpg",
"description": "Lidt omkring job situationen og hvordan det fungere"
}
]
}

View File

@@ -0,0 +1,14 @@
<main>
{% for section in sections %}
<section>
<h2>{{ section.name }}</h2>
<p><strong>Summary:</strong> {{ section.summary }}</p>
<p><strong>Author:</strong> {{ section.author }}</p>
<div>
{{ section.content | safe }}
</div>
<hr>
</section>
{% endfor %}
</main>