Compare commits

..

1 Commits
0.2.0 ... main

Author SHA1 Message Date
fc211edc77 start docs (AI) fix some caching logic
All checks were successful
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 9s
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 15s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 19s
Release / build (release) Successful in 39s
2025-03-15 15:55:40 -04:00
23 changed files with 496 additions and 17 deletions

185
README.md
View File

@ -0,0 +1,185 @@
# Foldsite
Foldsite is a dynamic site generator built with Python and Flask. It allows you to create and manage a website using Markdown content, HTML templates, and CSS styles.
## Table of Contents
- [Foldsite](#foldsite)
- [Table of Contents](#table-of-contents)
- [Configuration](#configuration)
- [Template Setup](#template-setup)
- [Site Setup](#site-setup)
- [Style Setup](#style-setup)
- [Template and Style Search](#template-and-style-search)
- [How a Template is Written](#how-a-template-is-written)
- [Jinja Primer](#jinja-primer)
- [Added Tools for the Template](#added-tools-for-the-template)
- [Tool Input and Return Types](#tool-input-and-return-types)
- [`get_sibling_content_files(path: str) -> list`](#get_sibling_content_filespath-str---list)
- [`get_text_document_preview(path: str) -> str`](#get_text_document_previewpath-str---str)
- [`get_sibling_content_folders(path: str) -> list`](#get_sibling_content_folderspath-str---list)
- [`get_folder_contents(path: str) -> list`](#get_folder_contentspath-str---list)
- [Example Usages for Tools and Types](#example-usages-for-tools-and-types)
- [Example Usage of `get_sibling_content_files`](#example-usage-of-get_sibling_content_files)
- [Example Usage of `get_text_document_preview`](#example-usage-of-get_text_document_preview)
- [Example Usage of `get_sibling_content_folders`](#example-usage-of-get_sibling_content_folders)
- [Example Usage of `get_folder_contents`](#example-usage-of-get_folder_contents)
- [Deployment](#deployment)
- [Docker Compose Example](#docker-compose-example)
## Configuration
The configuration file is written in TOML format and contains various settings for the application. Below is an example configuration file (`config.toml`):
```toml
[paths]
content_dir = "example/content"
templates_dir = "templates"
styles_dir = "styles"
[server]
listen_address = "127.0.0.1"
listen_port = 8080
debug = false
access_log = true
max_threads = 4
admin_browser = false
admin_password = "your_admin_password"
```
## Template Setup
Templates are HTML files that define the structure of your web pages. They are stored in the `templates` directory. Each template can include other templates and use Jinja2 syntax for dynamic content.
## Site Setup
The site content is stored in the `content` directory. Each Markdown file represents a page on your site. The directory structure of the `content` directory determines the URL structure of your site.
## Style Setup
Styles are CSS files that define the appearance of your web pages. They are stored in the `styles` directory. You can create specific styles for different types of content and categories.
## Template and Style Search
Templates and styles are searched in a specific order to apply the most specific styles first, followed by more general styles, and finally the base style.
## How a Template is Written
Templates are written in HTML and use Jinja2 syntax for dynamic content. Below is an example template (`base.html`):
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
{% for style in styles %}
<link rel="stylesheet" href="{{ style }}">
{% endfor %}
</head>
<body>
<div class="content">
{{ content }}
</div>
</body>
</html>
```
## Jinja Primer
Jinja2 is a templating engine for Python. It allows you to include dynamic content in your HTML templates. Below are some basic Jinja2 syntax examples:
- Variables: `{{ variable }}`
- Loops: `{% for item in list %} ... {% endfor %}`
- Conditionals: `{% if condition %} ... {% endif %}`
- Includes: `{% include 'template.html' %}`
## Added Tools for the Template
Foldsite provides additional tools for templates, such as functions to get sibling content files, text document previews, and folder contents.
## Tool Input and Return Types
### `get_sibling_content_files(path: str) -> list`
Returns a list of sibling content files in the specified directory.
### `get_text_document_preview(path: str) -> str`
Generates a preview of the text document located at the given path.
### `get_sibling_content_folders(path: str) -> list`
Returns a list of sibling content folders within a specified directory.
### `get_folder_contents(path: str) -> list`
Retrieves the contents of a folder and returns a list of `TemplateFile` objects.
## Example Usages for Tools and Types
### Example Usage of `get_sibling_content_files`
```html
<ul>
{% for file in get_sibling_content_files('path/to/directory') %}
<li>{{ file[0] }} - {{ file[1] }}</li>
{% endfor %}
</ul>
```
### Example Usage of `get_text_document_preview`
```html
<div>
{{ get_text_document_preview('path/to/document.md') }}
</div>
```
### Example Usage of `get_sibling_content_folders`
```html
<ul>
{% for folder in get_sibling_content_folders('path/to/directory') %}
<li>{{ folder[0] }} - {{ folder[1] }}</li>
{% endfor %}
</ul>
```
### Example Usage of `get_folder_contents`
```html
<ul>
{% for item in get_folder_contents('path/to/directory') %}
<li>{{ item.name }} - {{ item.path }}</li>
{% endfor %}
</ul>
```
## Deployment
To deploy Foldsite, you can use Docker. Below is an example Dockerfile:
```dockerfile
FROM python:3.13.2-bookworm
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
```
## Docker Compose Example
Below is an example `docker-compose.yml` file to deploy Foldsite using Docker Compose:
```yaml
version: '3.8'
services:
foldsite:
build: .
ports:
- "8080:8080"
volumes:
- .:/app
environment:
- CONFIG_PATH=config.toml
```

View File

@ -1,7 +1,7 @@
[paths]
content_dir = "/home/dubey/projects/foldsite/example/content"
templates_dir = "/home/dubey/projects/foldsite/example/templates"
styles_dir = "/home/dubey/projects/foldsite/example/styles"
content_dir = "/home/dubey/projects/foldsite/docs/content"
templates_dir = "/home/dubey/projects/foldsite/docs/templates"
styles_dir = "/home/dubey/projects/foldsite/docs/styles"
[server]
listen_address = "0.0.0.0"

View File

@ -0,0 +1,3 @@
# Added Tools for the Template
Foldsite provides additional tools for templates, such as functions to get sibling content files, text document previews, and folder contents.

View File

@ -0,0 +1,19 @@
# Configuration
The configuration file is written in TOML format and contains various settings for the application. Below is an example configuration file (`config.toml`):
```toml
[paths]
content_dir = "example/content"
templates_dir = "templates"
styles_dir = "styles"
[server]
listen_address = "127.0.0.1"
listen_port = 8080
debug = false
access_log = true
max_threads = 4
admin_browser = false
admin_password = "your_admin_password"
```

View File

@ -0,0 +1,12 @@
# Deployment
To deploy Foldsite, you can use Docker. Below is an example Dockerfile:
```dockerfile
FROM python:3.13.2-bookworm
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
```

View File

@ -0,0 +1,16 @@
# Docker Compose Example
Below is an example `docker-compose.yml` file to deploy Foldsite using Docker Compose:
```yaml
version: '3.8'
services:
foldsite:
build: .
ports:
- "8080:8080"
volumes:
- .:/app
environment:
- CONFIG_PATH=config.toml
```

View File

@ -0,0 +1,39 @@
# Example Usages for Tools and Types
### Example Usage of `get_sibling_content_files`
```html
<ul>
{% for file in get_sibling_content_files('path/to/directory') %}
<li>{{ file[0] }} - {{ file[1] }}</li>
{% endfor %}
</ul>
```
### Example Usage of `get_text_document_preview`
```html
<div>
{{ get_text_document_preview('path/to/document.md') }}
</div>
```
### Example Usage of `get_sibling_content_folders`
```html
<ul>
{% for folder in get_sibling_content_folders('path/to/directory') %}
<li>{{ folder[0] }} - {{ folder[1] }}</li>
{% endfor %}
</ul>
```
### Example Usage of `get_folder_contents`
```html
<ul>
{% for item in get_folder_contents('path/to/directory') %}
<li>{{ item.name }} - {{ item.path }}</li>
{% endfor %}
</ul>
```

View File

View File

@ -0,0 +1,23 @@
# How a Template is Written
Templates are written in HTML and use Jinja2 syntax for dynamic content. Below is an example template (`base.html`):
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
{% for style in styles %}
<link rel="stylesheet" href="{{ style }}">
{% endfor %}
</head>
<body>
<div class="content">
{{ content }}
</div>
</body>
</html>
```

20
docs/content/index.md Normal file
View File

@ -0,0 +1,20 @@
# Foldsite Documentation
Welcome to the Foldsite documentation. This site will guide you through the setup, configuration, and usage of Foldsite.
## Table of Contents
1. [Introduction](introduction.md)
2. [Configuration](configuration.md)
3. [Template Setup](template-setup.md)
4. [Site Setup](site-setup.md)
5. [Style Setup](style-setup.md)
6. [Template and Style Search](template-style-search.md)
7. [How a Template is Written](how-template-written.md)
8. [Jinja Primer](jinja-primer.md)
9. [Added Tools for the Template](added-tools.md)
10. [Tool Input and Return Types](tool-input-return-types.md)
11. [Example Usages for Tools and Types](example-usages.md)
12. [Deployment](deployment.md)
13. [Docker Compose Example](docker-compose-example.md)
14. [Folder Layout](folder-layout.md)

View File

@ -0,0 +1,5 @@
# Introduction
Foldsite is a dynamic site generator built with Python and Flask. It allows you to create and manage a website using Markdown content, HTML templates, and CSS styles.
This documentation will guide you through the setup, configuration, and usage of Foldsite.

View File

@ -0,0 +1,8 @@
# Jinja Primer
Jinja2 is a templating engine for Python. It allows you to include dynamic content in your HTML templates. Below are some basic Jinja2 syntax examples:
- Variables: `{{ variable }}`
- Loops: `{% for item in list %} ... {% endfor %}`
- Conditionals: `{% if condition %} ... {% endif %}`
- Includes: `{% include 'template.html' %}`

View File

@ -0,0 +1,3 @@
# Site Setup
The site content is stored in the `content` directory. Each Markdown file represents a page on your site. The directory structure of the `content` directory determines the URL structure of your site.

View File

@ -0,0 +1,3 @@
# Style Setup
Styles are CSS files that define the appearance of your web pages. They are stored in the `styles` directory. You can create specific styles for different types of content and categories.

View File

@ -0,0 +1,3 @@
# Template Setup
Templates are HTML files that define the structure of your web pages. They are stored in the `templates` directory. Each template can include other templates and use Jinja2 syntax for dynamic content.

View File

@ -0,0 +1,3 @@
# Template and Style Search
Templates and styles are searched in a specific order to apply the most specific styles first, followed by more general styles, and finally the base style.

View File

@ -0,0 +1,13 @@
# Tool Input and Return Types
### `get_sibling_content_files(path: str) -> list`
Returns a list of sibling content files in the specified directory.
### `get_text_document_preview(path: str) -> str`
Generates a preview of the text document located at the given path.
### `get_sibling_content_folders(path: str) -> list`
Returns a list of sibling content folders within a specified directory.
### `get_folder_contents(path: str) -> list`
Retrieves the contents of a folder and returns a list of `TemplateFile` objects.

24
docs/styles/__file.md.css Normal file
View File

@ -0,0 +1,24 @@
article {
max-width: 800px;
margin: 0 auto;
}
article h1, article h2, article h3, article h4, article h5, article h6 {
margin-top: 1.5rem;
}
article p {
margin: 1rem 0;
}
article pre {
background: #f4f4f4;
padding: 1rem;
overflow-x: auto;
}
article code {
background: #f4f4f4;
padding: 0.2rem 0.4rem;
border-radius: 3px;
}

50
docs/styles/base.css Normal file
View File

@ -0,0 +1,50 @@
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
}
header {
background: #333;
color: #fff;
padding: 1rem 0;
text-align: center;
}
header h1 {
margin: 0;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline;
margin-right: 1rem;
}
nav ul li a {
color: #fff;
text-decoration: none;
}
nav ul li a:hover {
text-decoration: underline;
}
main {
padding: 2rem;
}
footer {
background: #333;
color: #fff;
text-align: center;
padding: 1rem 0;
position: fixed;
bottom: 0;
width: 100%;
}

4
docs/templates/__file.md.html vendored Normal file
View File

@ -0,0 +1,4 @@
<article>
{{ content|safe }}
</article>

42
docs/templates/base.html vendored Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
{% for style in styles %}
<link rel="stylesheet" href="{{ style }}">
{% endfor %}
</head>
<body>
<header>
<h1>Foldsite Documentation</h1>
<nav>
<ul>
<li><a href="{{ url_for('default_route', path='index.md') }}">Home</a></li>
<li><a href="{{ url_for('default_route', path='introduction.md') }}">Introduction</a></li>
<li><a href="{{ url_for('default_route', path='configuration.md') }}">Configuration</a></li>
<li><a href="{{ url_for('default_route', path='template-setup.md') }}">Template Setup</a></li>
<li><a href="{{ url_for('default_route', path='site-setup.md') }}">Site Setup</a></li>
<li><a href="{{ url_for('default_route', path='style-setup.md') }}">Style Setup</a></li>
<li><a href="{{ url_for('default_route', path='template-style-search.md') }}">Template and Style Search</a></li>
<li><a href="{{ url_for('default_route', path='how-template-written.md') }}">How a Template is Written</a></li>
<li><a href="{{ url_for('default_route', path='jinja-primer.md') }}">Jinja Primer</a></li>
<li><a href="{{ url_for('default_route', path='added-tools.md') }}">Added Tools</a></li>
<li><a href="{{ url_for('default_route', path='tool-input-return-types.md') }}">Tool Input and Return Types</a></li>
<li><a href="{{ url_for('default_route', path='example-usages.md') }}">Example Usages</a></li>
<li><a href="{{ url_for('default_route', path='deployment.md') }}">Deployment</a></li>
<li><a href="{{ url_for('default_route', path='docker-compose-example.md') }}">Docker Compose Example</a></li>
<li><a href="{{ url_for('default_route', path='folder-layout.md') }}">Folder Layout</a></li>
</ul>
</nav>
</header>
<main>
{{ content|safe }}
</main>
<footer>
<p>&copy; 2023 Foldsite</p>
</footer>
</body>
</html>

View File

@ -1,12 +1,9 @@
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}"
from functools import lru_cache
@lru_cache(maxsize=512)
def generate_thumbnail(image_path, resize_percent, min_width, max_width):
# Open the image file
with Image.open(image_path) as img:
# Calculate the new size based on the resize percentage
@ -20,13 +17,19 @@ def generate_thumbnail(image_path, resize_percent, min_width):
new_width = min_width
new_height = int(new_height * scale_factor)
# Ensure the maximum width is not exceeded
if new_width > max_width:
scale_factor = max_width / new_width
new_width = max_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
exif = img.info['exif']
orientation = img._getexif().get(0x0112, 1) # 0x0112 is the EXIF orientation tag
if orientation == 3:
img = img.rotate(180, expand=True)
elif orientation == 6:
@ -35,12 +38,12 @@ def generate_thumbnail(image_path, resize_percent, min_width):
img = img.rotate(90, expand=True)
except (AttributeError, KeyError, IndexError):
# cases: image don't have getexif
pass
exif = None
# 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)
img.save(thumbnail_io, format=img_format, exif=exif)
thumbnail_io.seek(0)
return (thumbnail_io.getvalue(), img_format)

View File

@ -1,9 +1,8 @@
from pathlib import Path
from src.config.config import Configuration
from src.rendering.renderer import render_page, render_error_page
from flask import send_file
from flask import send_file, request
from src.rendering.image import generate_thumbnail
from functools import lru_cache
import os
@ -114,13 +113,15 @@ class RouteManager:
if file_path.exists():
# Check to see if the file is an image, if it is, render a thumbnail
if file_path.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif"]:
max_width = request.args.get("max_width", default=2048, type=int)
thumbnail_bytes, img_format = generate_thumbnail(
str(file_path), 10, 2048
str(file_path), 10, 2048, max_width
)
return (
thumbnail_bytes,
200,
{"Content-Type": f"image/{img_format.lower()}"},
{"Content-Type": f"image/{img_format.lower()}",
"cache-control": "public, max-age=31536000"},
)
return send_file(file_path)
else: