Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled

This commit is contained in:
2024-12-29 04:34:15 +01:00
parent 95b6c1fa05
commit 0121662530
26 changed files with 341 additions and 547 deletions

View File

@@ -7,29 +7,30 @@ from app.services.markdown_processor import MarkdownProcessor
from app.services.metadata_processor import MetadataProcessor
from app.controllers.category_controller import CategoryController
from fastapi.middleware.gzip import GZipMiddleware
from app.services.image_controller import ImageHandler
from app.services.image_service import ImageService
class Application:
def __init__(self):
"""Initialize the FastAPI app and configure it."""
self.app = FastAPI(lifespan=self._lifespan_event)
self._set_image_sizes()
self._setup_static_files()
self._include_routers()
self._include_middelware()
@asynccontextmanager
async def _lifespan_event(self, app: FastAPI):
"""Lifespan event for startup and shutdown logic."""
print("App startup: Processing Markdown files...")
# Generate dynamic JSON data
metadata_processor = MetadataProcessor(input_dir="./data", output_file="generated_data.json")
metadata_processor = MetadataProcessor(input_dir="./data", output_file="generated_data.json",app=self.app)
metadata_processor.generate_json()
print("Generated dynamic data file.")
print("Markdown processing complete!")
# Process Markdown files into HTML
processor = MarkdownProcessor(input_dir="./data", templates_dir="./templates")
processor = MarkdownProcessor(input_dir="./data", templates_dir="./templates",app=self.app)
processor.run()
yield
print("App shutdown: Cleanup complete.")
@@ -38,24 +39,30 @@ class Application:
"""Mount static file directories."""
self.app.mount("/data", StaticFiles(directory="data"), name="data")
self.app.mount("/static", StaticFiles(directory="static"), name="static")
self.app.mount( "/images", StaticFiles( directory = "static/images" ), name = "images" )
def _include_routers(self):
"""Include all route controllers."""
category_controller = CategoryController()
#dynamic_controller = DynamicController( "./data" )
image_service = ImageService(self.app)
route_to_web = RouteToWeb(self.app)
self.app.include_router( category_controller.router )
#self.app.include_router( dynamic_controller.router )
self.app.include_router(route_to_web.router)
self.app.include_router( image_service.router )
def _include_middelware(self):
self.app.add_middleware( GZipMiddleware, minimum_size = 500 )
def _set_image_sizes(self):
self.app.state.IMAGE_SIZES = {
'thumbnails': {'width': 100, 'height': 100},
'large': {'width': 800, 'height': 600},
'small': {'width': 300, 'height': 200},
'original': {'width': None, 'height': None}, # Original størrelse
}
def get_app(self):
"""Return the FastAPI app instance."""
return self.app

View File

@@ -1,72 +0,0 @@
import os
from PIL import Image
class ImageHandler:
def __init__(self, base_dir: str):
"""
Initialize the ImageHandler.
:param base_dir: Base directory for storing and retrieving images.
"""
self.base_dir = base_dir
def get_image_path(self, filename: str) -> str:
"""
Construct the full path for a given image file.
:param filename: Relative filename of the image.
:return: Full path to the image.
"""
return os.path.join(self.base_dir, filename)
def get_resized_image_path(self, filename: str, width: int, height: int) -> str:
"""
Construct the path for a resized image.
:param filename: Original image filename.
:param width: Desired width.
:param height: Desired height.
:return: Path to the resized image.
"""
return os.path.join(self.base_dir, f"resized_{width}x{height}_{filename}")
def resize_and_save(self, original_path: str, resized_path: str, width: int, height: int):
"""
Resize and save the image if it doesn't already exist.
:param original_path: Path to the original image file.
:param resized_path: Path to save the resized image.
:param width: Desired width.
:param height: Desired height.
"""
if not os.path.exists(resized_path):
with Image.open(original_path) as img:
img_resized = img.resize((width, height))
img_resized.save(resized_path, format="JPEG")
def generate_image_tag(self, src: str, width: int, height: int, css_class: str = "", alt: str = "") -> str:
"""
Generate an HTML <img> tag and ensure the image exists with the specified dimensions.
:param src: Relative path to the original image.
:param width: Desired width of the image.
:param height: Desired height of the image.
:param css_class: Optional CSS class to add to the <img> tag.
:param alt: Alternative text for the image.
:return: HTML <img> tag.
"""
original_path = self.get_image_path(src)
if not os.path.isfile(original_path):
raise FileNotFoundError(f"Image not found: {src}")
# Construct resized image path
resized_filename = f"resized_{width}x{height}_{os.path.basename(src)}"
resized_path = self.get_resized_image_path(src, width, height)
# Resize and save the image if necessary
self.resize_and_save(original_path, resized_path, width, height)
# Return the <img> tag
class_attr = f' class="{css_class}"' if css_class else ""
alt_attr = f' alt="{alt}"' if alt else ""
return f'<img src="/{resized_path}" width="{width}" height="{height}"{alt_attr}{class_attr}>'

View File

@@ -0,0 +1,156 @@
import os
from fastapi import HTTPException
from fastapi.responses import FileResponse
from fastapi import APIRouter, Request, FastAPI
from PIL import Image
class FileHandler:
def __init__(self, category, image_type, filename):
self.filename = filename
self.category = category
self.image_type = image_type
@property
def src_file(self) -> str:
src_path = "data/{category}/images/{filename}"
return src_path.format( category = self.category, filename = self.filename )
@property
def dest_file(self) -> str:
base_url = "/images/{category}/{filename}"
return base_url.format( category = self.category, filename = self.filename )
@property
def dest_filename(self) -> str:
base_url = "static/images/{category}/{image_type}/{filename}"
return base_url.format( category = self.category, image_type = self.image_type, filename = self.filename )
@property
def dest_path(self) -> str:
base_url = "static/images/{category}/{image_type}"
return base_url.format( category = self.category, image_type = self.image_type )
def __str__(self):
return (
f"FileHandler(\n"
f" filename='{self.filename}',\n"
f" category='{self.category}',\n"
f" image_type='{self.image_type}',\n"
f" src_file='{self.src_file}',\n"
f" dest_file='{self.dest_file}',\n"
f" dest_filename='{self.dest_filename}'\n"
f")"
)
class ImageService:
def __init__(self,app: FastAPI):
self.router = APIRouter()
self.app = app
self.IMAGE_SIZES = self.app.state.IMAGE_SIZES
#self._ensure_directories_exist()
self._add_routes()
def __str__(self):
"""
Provides a string representation of the class instance.
"""
base_paths_str = "\n".join(
[f"{key}: {value}" for key, value in self.base_paths.items()]
)
image_sizes_str = "\n".join(
[
f"{key}: width={value['width']}, height={value['height']}"
for key, value in self.image_sizes.items()
]
)
return f"<Class:ImageService Base Paths:{base_paths_str} Image Sizes:\n{image_sizes_str}"
def get_image_size(self, image_type: str) -> dict:
"""
Retrieve the width and height for a given image type from the app state.
Args:
request (Request): FastAPI request object.
image_type (str): The type of the image (e.g., 'thumbnails').
Returns:
dict: A dictionary with 'width' and 'height'.
"""
image_sizes = self.app.state.IMAGE_SIZES
if image_type not in image_sizes:
raise ValueError( f"Invalid image type: {image_type}. Must be one of {list( image_sizes.keys() )}" )
return image_sizes[image_type]
def _add_routes(self):
self.router.add_api_route(
"/image/{category}/{type}/{filename}",
self.get_image,
methods=["GET"],
response_class=FileResponse,
)
async def get_image(self, category: str, type: str, filename: str):
"""
Retrieve an image file from the specified category and type.
"""
file_path = self._resolve_path(category, type, filename)
return FileResponse(file_path)
def validate_image(self, file_path:FileHandler=None, width:int=None, height:int=None, overwrite = True ) -> bool:
if not os.path.exists( file_path.dest_filename ):
with Image.open( file_path.src_file ) as img:
print(file_path.src_file)
self._resize_image( img, file_path, width, height )
return True
with Image.open( file_path.dest_filename ) as img:
if img.width != width or img.height != height:
if overwrite:
self._resize_image( img, file_path, width, height )
return False
return True
def _resize_image(self, img: Image.Image, file_path: FileHandler, width: int, height: int):
resized_img = img.resize( (width, height), Image.Resampling.LANCZOS )
os.makedirs(file_path.dest_path,exist_ok = True)
resized_img.save( file_path.dest_filename )
async def get_image(self, category: str, type: str, filename: str):
file_path = self._resolve_path( category, type, filename )
return FileResponse( file_path )
def image_tag(self, category: str, image_type: str, filename: str, alt: str = "", width: int = None,
height: int = None) -> str:
"""
Generate an HTML <img> tag with default sizes if dimensions are not provided.
"""
# Use default sizes if none are provided
default_size = self.get_image_size( image_type)
width = width or default_size.get( "width" )
height = height or default_size.get( "height" )
#file_path = self._resolve_path( category, image_type, filename )
file_path = FileHandler(category = category,image_type = image_type,filename = filename)
self.validate_image( file_path, width = width,height=height, overwrite = True )
tag = f'<img src="{file_path.dest_filename}" alt="{alt}"'
if width:
tag += f' width="{width}"'
if height:
tag += f' height="{height}"'
tag += ">"
return tag

View File

@@ -1,8 +1,7 @@
import os
from bs4 import BeautifulSoup
from app.services.markdown_render import render_markdown_with_jinja # Your custom renderer
from fastapi import FastAPI
from app.services.markdown_render import MarkdownRenderer
from jinja2 import Environment, FileSystemLoader
@@ -12,7 +11,7 @@ class MarkdownProcessor:
'index.html' per category directory using a custom rendering engine.
"""
def __init__(self, input_dir: str, templates_dir: str):
def __init__(self, input_dir: str, templates_dir: str,app:FastAPI=None):
"""
Initialize the MarkdownProcessor.
@@ -22,6 +21,8 @@ class MarkdownProcessor:
"""
self.input_dir = input_dir
self.env = Environment(loader=FileSystemLoader(templates_dir))
self.app = app
def _process_markdown_files_in_directory(self, directory_path: str) -> list:
"""
@@ -33,15 +34,21 @@ class MarkdownProcessor:
Returns:
list: A list of processed sections containing metadata and rendered content.
"""
from pathlib import Path
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()
markdown_render = MarkdownRenderer(file_path=file_path,app=self.app)
# Process Markdown and Jinja2
rendered_content, metadata = render_markdown_with_jinja(markdown_content)
rendered_content, metadata = markdown_render.render_markdown_with_jinja ( markdown_content )
# Append the section to the list
sections.append({

View File

@@ -1,123 +1,168 @@
from pathlib import Path
import sys
import markdown
from fastapi import FastAPI
from jinja2 import Environment, DictLoader
from .image_controller import ImageHandler
# Define Jinja2 custom functions
def img_left_overlay(src):
"""Render an image with overlay."""
return f'''
<div class="img-left-overlay">
<img src="{src}" alt="Overlay Image" loading="lazy">
<div class="overlay-text">Overlay Text</div>
</div>
'''
from markupsafe import Markup
from .image_service import ImageService
def box(title, content):
"""Render a box component."""
return f'''
<div class="box">
<strong>{title}</strong>
<p>{content}</p>
</div>
'''
def note(content):
"""Render a note component."""
return f'''
<div class="note">
<p>{content}</p>
</div>
'''
def link_to(title, url):
"""Render a box component."""
return f'''
<a href="{url}" target="_blank" rel="noopener noreferrer">{title}</a>
'''
def warning(content):
"""Render a warning component."""
return f'''
<div class="warning">
⚠️ <p>{content}</p>
</div>
'''
class MarkdownRenderer:
def __init__(self, file_path: str = None, app: FastAPI=None):
"""
Initialize the MarkdownRenderer with a Jinja2 environment and custom functions.
"""
self.app = app
self.image_service = ImageService(self.app)
self.jinja_env = self._create_jinja_environment()
self.file_path = file_path
def slider(options, images):
"""Render a slider using the provided HTML structure."""
import uuid
modal_id = uuid.uuid4().hex.upper()[0:6] # Lets create some uniq modals
width = options.get("width", 500)
height = options.get("height", 375)
def _create_jinja_environment(self) -> Environment:
"""
Create and configure the Jinja2 environment with custom functions.
html_content = []
html_content.append('<div class="button-stack">')
for i, val in enumerate(images):
modal_id = f"{modal_id}_{i}"
modal_id_next = f"{modal_id}_{i+1}"
if int(len(images))<=int(i+1):
modal_id_next = f"{modal_id}_0"
if i % 2 == 0:
html_content.append(f"""<button onclick="openModal('modal{modal_id}')" class="stacked-button"> <img src="{val}" alt="Lets do better" class="thumbnail" loading="lazy"></button>""".strip())
else:
html_content.append(f"""<button onclick="openModal('modal{modal_id}')" class="stacked-button"> <img src="{val}" alt="Lets do better" class="thumbnail" loading="lazy"></button>""".strip())
html_content.append(f"""<div class="modal" id="modal{modal_id}">
<div class="modal-content">
<h2>Modal {i}</h2>
<img src="{val}" alt="Lets do better" loading="lazy">
<div class="modal-buttons">
<button onclick="closeModal('modal{modal_id}')">Close</button>
<button class="next-btn" onclick="nextModal('modal{modal_id}', 'modal{modal_id_next}')">Next</button>
</div>
</div>
</div>""")
Returns:
Environment: A configured Jinja2 environment.
"""
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
html_content.append( '</div>' )
html = '\n'.join( html_content )
env.globals.update({
"img_left_overlay": self.img_left_overlay,
"box": self.box,
"note": self.note,
"warning": self.warning,
"link_to": self.link_to,
"slider": self.slider,
"image": self.get_image, # Add image handler function
})
return env
return html
def img_left_overlay(self, src: str) -> str:
"""Render an image with overlay."""
return f'''
<div class="img-left-overlay">
<img src="{src}" alt="Overlay Image" loading="lazy">
<div class="overlay-text">Overlay Text</div>
</div>
'''
def box(self, title: str, content: str) -> str:
"""Render a box component."""
return f'''
<div class="box">
<strong>{title}</strong>
<p>{content}</p>
</div>
'''
def note(self, content: str) -> str:
"""Render a note component."""
return f'''
<div class="note">
<p>{content}</p>
</div>
'''
def link_to(self, title: str, url: str) -> str:
"""Render a link component."""
return f'''
<a href="{url}" target="_blank" rel="noopener noreferrer">{title}</a>
'''
def warning(self, content: str) -> str:
"""Render a warning component."""
return f'''
<div class="warning">
⚠️ <p>{content}</p>
</div>
'''
def slider(self, options: dict, images: list) -> str:
"""Render a slider component."""
import uuid
modal_id = uuid.uuid4().hex.upper()[0:6]
def create_jinja_environment():
"""Create and configure the Jinja2 environment."""
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
image_handler = ImageHandler(base_dir="static/images")
html_content = []
html_content.append('<div class="button-stack">')
for i, val in enumerate(images):
self.image_service = ImageService( self.app )
print(val)
modal_id_current = f"{modal_id}_{i}"
modal_id_next = f"{modal_id}_{i + 1}" if i + 1 < len(images) else f"{modal_id}_0"
env.globals.update({
"img_left_overlay": img_left_overlay,
"box": box,
"note": note,
"warning": warning,
"link_to": link_to,
"slider": slider,
"image": image_handler.generate_image_tag, # Add image handler function
html_content.append(f"""
<button onclick="openModal('modal{modal_id_current}')" class="stacked-button">
<img src="{val}" alt="Image {i}" class="thumbnail" loading="lazy">
</button>
<div class="modal" id="modal{modal_id_current}">
<div class="modal-content">
<h2>Modal {i}</h2>
<img src="{val}" alt="Image {i}" loading="lazy">
<div class="modal-buttons">
<button onclick="closeModal('modal{modal_id_current}')">Close</button>
<button class="next-btn" onclick="nextModal('modal{modal_id_current}', 'modal{modal_id_next}')">Next</button>
</div>
</div>
</div>
""")
html_content.append('</div>')
return '\n'.join(html_content)
})
return env
def _get_category(self):
if isinstance(self.file_path, str):
this_path = Path(self.file_path)
return this_path.parent.name
def render_markdown_with_jinja(markdown_content: str):
"""
Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
return True
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"])
intermediate_html = md.convert(markdown_content)
metadata = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
def get_image(self, image_type: str, filename: str, alt: str = "", width: int = None, height: int = None) -> Markup:
"""
Generate a dynamic HTML <img> tag for an image using ImageService's image_tag method.
"""
valid_types = ['thumbnails', 'large', 'small', 'original']
if image_type not in valid_types:
sys.tracebacklimit = 0
raise ValueError( f"Invalid image type: {image_type}. Must be one of {valid_types}" )
# Step 2: Pass the resulting HTML with Jinja2 custom tags through Jinja2
env = create_jinja_environment()
tag = self.image_service.image_tag(
category=self._get_category(),
image_type=image_type,
filename=filename,
alt=alt,
width=width,
height=height
)
my_tag = Markup(tag)
print(my_tag)
return my_tag
template = env.get_template("base_template")
final_html = template.render(content=intermediate_html)
# Step 3: Re-render final_html in Jinja2 for embedded tags like {{ image(...) }}
final_output = env.from_string(final_html).render()
return final_output, metadata
def render_markdown_with_jinja(self, markdown_content: str):
"""
Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
Args:
markdown_content (str): Raw Markdown content.
Returns:
tuple: Rendered HTML content and metadata as a dictionary.
"""
md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
intermediate_html = md.convert(markdown_content)
metadata = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
# Step 3: Pass the resulting HTML with Jinja2 custom tags through Jinja2
template = self.jinja_env.get_template("base_template")
final_html = template.render(content=intermediate_html)
# Step 4: Re-render final_html in Jinja2 for embedded tags like {{ image(...) }}
final_output = self.jinja_env.from_string(final_html).render()
return final_output, metadata

View File

@@ -3,6 +3,8 @@ import markdown
import json
from typing import List, Dict
from fastapi import FastAPI
class MetadataProcessor:
"""
@@ -10,7 +12,7 @@ class MetadataProcessor:
and generate a structured JSON file.
"""
def __init__(self, input_dir: str, output_file: str):
def __init__(self, input_dir: str, output_file: str,app:FastAPI=None):
"""
Initialize the MetadataProcessor.