Mostly works! Image loading and caching seems slow
This commit is contained in:
@ -0,0 +1,10 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
GENERIC_FILE_MAPPING = OrderedDict()
|
||||
GENERIC_FILE_MAPPING["document"] = ["md", "txt", "html"]
|
||||
GENERIC_FILE_MAPPING["image"] = ["png", "jpg", "jpeg", "gif", "svg"]
|
||||
GENERIC_FILE_MAPPING["directory"] = [None]
|
||||
GENERIC_FILE_MAPPING["other"] = [None]
|
||||
|
||||
# Combinations of Groups
|
||||
GENERIC_FILE_MAPPING["multimedia"] = GENERIC_FILE_MAPPING["image"]
|
116
src/rendering/helpers.py
Normal file
116
src/rendering/helpers.py
Normal file
@ -0,0 +1,116 @@
|
||||
from dataclasses import dataclass
|
||||
from src.config.config import Configuration
|
||||
from src.rendering import GENERIC_FILE_MAPPING
|
||||
from enum import Enum
|
||||
|
||||
from thumbhash import image_to_thumbhash
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageMetadata:
|
||||
path: str
|
||||
width: int
|
||||
height: int
|
||||
alt: str
|
||||
thumbhash: str
|
||||
# exif attributes
|
||||
exif: dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileMetadata:
|
||||
path: str
|
||||
first_hundred_chars: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateFile:
|
||||
name: str
|
||||
extension: str
|
||||
categories: list[str]
|
||||
date_modified: str
|
||||
date_created: str
|
||||
size_kb: int
|
||||
metadata: ImageMetadata | FileMetadata | None
|
||||
dir_item_count: int
|
||||
is_dir: bool
|
||||
|
||||
|
||||
class TemplateHelpers:
|
||||
def __init__(self, config: Configuration):
|
||||
self.config: Configuration = config
|
||||
|
||||
def get_folder_contents(self, path: str = ""):
|
||||
"""Returns the contents of a folder as a list of TemplateFile objects
|
||||
|
||||
The metadata field is populated with the appropriate metadata object
|
||||
"""
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
ret = []
|
||||
for f in files:
|
||||
t = TemplateFile(
|
||||
name=f.name,
|
||||
extension=f.suffix.lower(),
|
||||
categories=[],
|
||||
date_modified=f.stat().st_mtime,
|
||||
date_created=f.stat().st_ctime,
|
||||
size_kb=f.stat().st_size / 1024,
|
||||
metadata=None,
|
||||
dir_item_count=len(list(f.glob("*"))) if f.is_dir() else 0,
|
||||
is_dir=f.is_dir(),
|
||||
)
|
||||
if f.is_file():
|
||||
# Build metadata depending on the mapping in GENERIC_FILE_MAPPING
|
||||
for k, v in GENERIC_FILE_MAPPING.items():
|
||||
if f.suffix[1:].lower() in v:
|
||||
t.categories.append(k)
|
||||
if k == "image":
|
||||
img = Image.open(f)
|
||||
exif = img._getexif()
|
||||
orientation = exif.get(274, 1) if exif else 1
|
||||
width, height = img.width, img.height
|
||||
if orientation in [5, 6, 7, 8]:
|
||||
width, height = height, width
|
||||
t.metadata = ImageMetadata(
|
||||
path=str(f.relative_to(self.config.content_dir)),
|
||||
width=width,
|
||||
height=height,
|
||||
alt=f.name,
|
||||
thumbhash=image_to_thumbhash(img),
|
||||
exif=img.info,
|
||||
)
|
||||
elif k == "document":
|
||||
with open(f, "r") as fdoc:
|
||||
t.metadata = FileMetadata(
|
||||
path=str(f.relative_to(self.config.content_dir)),
|
||||
first_hundred_chars=fdoc.read(100),
|
||||
)
|
||||
ret.append(t)
|
||||
return ret
|
||||
|
||||
|
||||
def get_sibling_content_files(self, path: str = ""):
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
return [
|
||||
(file.name, str(file.relative_to(self.config.content_dir)))
|
||||
for file in files
|
||||
if file.is_file()
|
||||
]
|
||||
|
||||
def get_text_document_preview(self, path: str):
|
||||
file_path = self.config.content_dir / path
|
||||
with open(file_path, "r") as f:
|
||||
content = f.read(100)
|
||||
return content
|
||||
|
||||
def get_sibling_content_folders(self, path: str = ""):
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
return [
|
||||
(file.name, str(file.relative_to(self.config.content_dir)))
|
||||
for file in files
|
||||
if file.is_dir()
|
||||
]
|
46
src/rendering/image.py
Normal file
46
src/rendering/image.py
Normal file
@ -0,0 +1,46 @@
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def generate_thumbnail(image_path, resize_percent, min_width):
|
||||
# Generate a unique key based on the image path, resize percentage, and minimum width
|
||||
key = f"{image_path}_{resize_percent}_{min_width}"
|
||||
|
||||
# Open the image file
|
||||
with Image.open(image_path) as img:
|
||||
# Calculate the new size based on the resize percentage
|
||||
width, height = img.size
|
||||
new_width = int(width * resize_percent / 100)
|
||||
new_height = int(height * resize_percent / 100)
|
||||
|
||||
# Ensure the minimum width is maintained
|
||||
if new_width < min_width:
|
||||
scale_factor = min_width / new_width
|
||||
new_width = min_width
|
||||
new_height = int(new_height * scale_factor)
|
||||
|
||||
# Resize the image while maintaining the aspect ratio
|
||||
img.thumbnail((new_width, new_height))
|
||||
|
||||
# Rotate the image based on the EXIF orientation tag
|
||||
try:
|
||||
exif = img._getexif()
|
||||
orientation = exif.get(0x0112, 1) # 0x0112 is the EXIF orientation tag
|
||||
if orientation == 3:
|
||||
img = img.rotate(180, expand=True)
|
||||
elif orientation == 6:
|
||||
img = img.rotate(270, expand=True)
|
||||
elif orientation == 8:
|
||||
img = img.rotate(90, expand=True)
|
||||
except (AttributeError, KeyError, IndexError):
|
||||
# cases: image don't have getexif
|
||||
pass
|
||||
|
||||
# Save the thumbnail to a BytesIO object
|
||||
thumbnail_io = BytesIO()
|
||||
img_format = img.format if img.format in ["JPEG", "JPG", "PNG"] else "JPEG"
|
||||
img.save(thumbnail_io, format=img_format)
|
||||
thumbnail_io.seek(0)
|
||||
|
||||
return (thumbnail_io, img_format)
|
16
src/rendering/markdown.py
Normal file
16
src/rendering/markdown.py
Normal file
@ -0,0 +1,16 @@
|
||||
import frontmatter
|
||||
import mistune
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
mistune_render = mistune.create_markdown(
|
||||
renderer=mistune.HTMLRenderer(escape=False),
|
||||
plugins=["strikethrough", "footnotes", "table", "task_lists", "abbr", "math"],
|
||||
)
|
||||
|
||||
|
||||
def render_markdown(path: Path) -> str:
|
||||
with open(path, "r") as f:
|
||||
obj = frontmatter.load(f)
|
||||
res = mistune_render(obj.content)
|
||||
return res
|
188
src/rendering/renderer.py
Normal file
188
src/rendering/renderer.py
Normal file
@ -0,0 +1,188 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from pprint import pprint as pp
|
||||
from flask import render_template_string, send_file
|
||||
|
||||
from src.rendering import GENERIC_FILE_MAPPING
|
||||
from src.rendering.markdown import render_markdown
|
||||
|
||||
|
||||
def count_file_extensions(path):
|
||||
files = path.glob("*")
|
||||
files_map = {}
|
||||
for file in files:
|
||||
if file.is_file():
|
||||
extension = file.suffix.lower()
|
||||
if extension in files_map:
|
||||
files_map[extension] += 1
|
||||
else:
|
||||
files_map[extension] = 1
|
||||
return files_map
|
||||
|
||||
|
||||
def determine_type(path: Path) -> tuple[str, str, Optional[str]]:
|
||||
if path.is_file():
|
||||
# Find extension in GENERIC_FILE_MAPPING, it may match multiple
|
||||
generic_mapping = None
|
||||
for file_type, extensions in GENERIC_FILE_MAPPING.items():
|
||||
if path.suffix[1:] in extensions:
|
||||
if generic_mapping is None:
|
||||
generic_mapping = [file_type]
|
||||
else:
|
||||
generic_mapping.append(file_type)
|
||||
generic_mapping.reverse()
|
||||
if generic_mapping is None:
|
||||
generic_mapping = ["other"]
|
||||
extension = path.suffix[1:]
|
||||
return "file", generic_mapping, extension
|
||||
# Directory
|
||||
files_map = count_file_extensions(path)
|
||||
if files_map:
|
||||
most_seen_extension = max(files_map, key=files_map.get)
|
||||
generic_mapping = None
|
||||
for file_type, extensions in GENERIC_FILE_MAPPING.items():
|
||||
if most_seen_extension[1:] in extensions:
|
||||
if generic_mapping is None:
|
||||
generic_mapping = [file_type]
|
||||
else:
|
||||
generic_mapping.append(file_type)
|
||||
generic_mapping.reverse()
|
||||
if generic_mapping is None:
|
||||
generic_mapping = ["other"]
|
||||
return "folder", generic_mapping, most_seen_extension[1:]
|
||||
else:
|
||||
return "folder", [], None
|
||||
|
||||
|
||||
DEFAULT_ERROR_TEMPLATE = """
|
||||
<div class="content">
|
||||
<h1>Error: {{ code }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
<p>{{ description }}</p>
|
||||
<p>PS: If you are a theme developer, or the owner of this site, you can style this page with a __error.html file</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def render_error_page(
|
||||
error_code: int,
|
||||
error_message: str,
|
||||
error_description: str,
|
||||
template_path: Path = Path("./"),
|
||||
):
|
||||
inp = DEFAULT_ERROR_TEMPLATE
|
||||
if (template_path / "__error.html").exists():
|
||||
inp = (template_path / "__error.html").read_text()
|
||||
content = render_template_string(
|
||||
inp,
|
||||
error_code=error_code,
|
||||
error_message=error_message,
|
||||
error_description=error_description,
|
||||
)
|
||||
return (
|
||||
render_template_string(
|
||||
(template_path / "base.html").read_text(),
|
||||
content=content,
|
||||
styles=["/base.css", "/__error.css"],
|
||||
),
|
||||
error_code,
|
||||
)
|
||||
|
||||
|
||||
def render_page(
|
||||
path: Path,
|
||||
base_path: Path = Path("./"),
|
||||
template_path: Path = Path("./"),
|
||||
style_path: Path = Path("./"),
|
||||
):
|
||||
if not path.exists():
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
"The requested resource was not found on this server.",
|
||||
template_path,
|
||||
)
|
||||
target_path = path
|
||||
target_file = path
|
||||
if path.is_file():
|
||||
target_path = path.parent
|
||||
type, category, extension = determine_type(target_file)
|
||||
|
||||
relative_path = target_file.relative_to(base_path)
|
||||
relative_dir = target_path.relative_to(base_path)
|
||||
|
||||
# Generate the possible paths for style
|
||||
styles = []
|
||||
styles.append("/" + str(relative_path) + ".css")
|
||||
|
||||
search_path = style_path / relative_dir
|
||||
while search_path >= style_path:
|
||||
if (search_path / f"__{type}.{extension}.css").exists():
|
||||
styles.append(
|
||||
"/"
|
||||
+ str(search_path.relative_to(style_path))
|
||||
+ f"/__{type}.{extension}.css"
|
||||
)
|
||||
for c in reversed(category):
|
||||
if (search_path / f"__{type}.{c}.css").exists():
|
||||
styles.append(
|
||||
"/"
|
||||
+ str(search_path.relative_to(style_path))
|
||||
+ f"/__{type}.{c}.css"
|
||||
)
|
||||
search_path = search_path.parent
|
||||
|
||||
styles.append("/base.css")
|
||||
|
||||
styles = [t for t in styles if (style_path / t[1:]).exists()]
|
||||
|
||||
templates = []
|
||||
if type == "folder":
|
||||
if (template_path / relative_dir / "__folder.html").exists():
|
||||
templates.append(relative_dir / "__folder.html")
|
||||
else:
|
||||
if (template_path / (str(relative_path) + ".html")).exists():
|
||||
templates.append(template_path / (str(relative_path) + ".html"))
|
||||
|
||||
if len(templates) == 0:
|
||||
search_path = template_path / relative_dir
|
||||
while search_path >= template_path:
|
||||
if (search_path / f"__{type}.{extension}.html").exists():
|
||||
templates.append(search_path / f"__{type}.{extension}.html")
|
||||
break
|
||||
for c in reversed(category):
|
||||
if (search_path / f"__{type}.{c}.html").exists():
|
||||
templates.append(search_path / f"__{type}.{c}.html")
|
||||
break
|
||||
search_path = search_path.parent
|
||||
|
||||
if len(templates) == 0:
|
||||
if type == "file":
|
||||
return send_file(target_file)
|
||||
else:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
"The requested resource was not found on this server.",
|
||||
template_path,
|
||||
)
|
||||
|
||||
content = ""
|
||||
if "document" in category and type == "file":
|
||||
content = render_markdown(target_file)
|
||||
|
||||
if not (template_path / "base.html").exists():
|
||||
raise Exception("Base template not found")
|
||||
|
||||
templates.append(template_path / "base.html")
|
||||
|
||||
# Filter templates to only those that exist
|
||||
for template in templates:
|
||||
content = render_template_string(
|
||||
template.read_text(),
|
||||
content=content,
|
||||
styles=styles,
|
||||
currentPath=str(relative_path),
|
||||
)
|
||||
|
||||
return content
|
Reference in New Issue
Block a user