Compare commits

..

15 Commits

Author SHA1 Message Date
779dab4f77 add bs4 in requirements 2025-07-13 07:17:49 -04:00
d872836624 fix: uri style page 2025-07-11 15:46:46 +02:00
df182fa7bb fix: readme 2025-07-11 15:44:24 +02:00
7d739ed777 add: doc photodown 2025-07-09 02:39:32 +02:00
6af8781aa2 add: bozodown doc 2025-07-09 02:20:58 +02:00
00bc97465f clean 2025-07-09 01:58:34 +02:00
74e99b96f0 clean: remove bulk_suffix
bulk_page -> page
bulk_category -> category
page -> collection
2025-07-09 01:57:52 +02:00
cbab99cc82 add: css to page 2025-07-09 00:03:03 +02:00
001a8c692e add: pictures menu in photo markdown 2025-07-08 22:08:24 +02:00
e1f08f0312 fix: indent and remove empty <p> 2025-07-08 22:05:41 +02:00
cf9cd77bcf add: use photodown to gen page 2025-07-08 18:35:58 +02:00
1563b3618a clean: create bozodown module 2025-07-08 16:31:53 +02:00
8c378492a3 bozodown: implement block 2025-07-08 15:10:17 +02:00
941e06e3d7 wip: implement page 2025-07-08 14:16:14 +02:00
6d6af200ab add: bozodown 2025-07-08 14:16:00 +02:00
36 changed files with 562 additions and 40 deletions

20
doc/bozodown/block.md Normal file
View File

@ -0,0 +1,20 @@
# Block
## Usage
```
-----
style
---
content
-----
```
| name | usage |
| --- | --- |
| style | Your custom css, multiline supported |
| content | Your bozodown |
## Effect
Create a div with custom css

13
doc/bozodown/checkbox.md Normal file
View File

@ -0,0 +1,13 @@
# Checkbox
## Usage
```
- [ ] your text
or
- [x] your text
```
## Effect
![](./checkbox.png)

BIN
doc/bozodown/checkbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

17
doc/bozodown/code.md Normal file
View File

@ -0,0 +1,17 @@
# Quote
## Usage
```
\```
your text
\```
```
## Effect
your text
to
```
your text
```

11
doc/bozodown/image.md Normal file
View File

@ -0,0 +1,11 @@
# Image
## Usage
```
![url]
```
## Effect
display the image

10
doc/bozodown/quote.md Normal file
View File

@ -0,0 +1,10 @@
# Quote
## Usage
```
`your text`
```
## Effect
your text -> `your text`

19
doc/bozodown/readme.md Normal file
View File

@ -0,0 +1,19 @@
# Bozodown
A markdown like, written in python, modulable
## Feature
### Text Formater
- [title](./title.md)
- [subtitle](./subtitle.md)
- [subsubtitle](./subsubtitle.md)
- [subsubsubtitle](./subsubsubtitle.md)
- [code](./code.md)
- [image](./image.md)
- [block](./block.md)
- [checkbox](./checkbox.md)
### Text Modifier
- [strong](./strong.md)
- [quote](./quote.md)

11
doc/bozodown/strong.md Normal file
View File

@ -0,0 +1,11 @@
# Strong
## Usage
```
**your text**
```
## Effect
your text -> **your text**

View File

@ -0,0 +1,13 @@
# Subsubsubtitle
## Usage
```
#### your text
```
## Effect
your text
to
#### your text

View File

@ -0,0 +1,13 @@
# Subsubtitle
## Usage
```
### your text
```
## Effect
your text
to
### your text

13
doc/bozodown/subtitle.md Normal file
View File

@ -0,0 +1,13 @@
# Subtitle
## Usage
```
## your text
```
## Effect
your text
to
## your text

13
doc/bozodown/title.md Normal file
View File

@ -0,0 +1,13 @@
# Title
## Usage
```
# your text
```
## Effect
your text
to
# your text

53
doc/photodown/gallery.md Normal file
View File

@ -0,0 +1,53 @@
# Block
## Usage
```
+++++
config
---
style
---
content
+++++
```
### Config
| type |
| --- |
| image-list |
| direction |
| --- |
| down |
| up |
| left |
| right |
### Style
Just css
### Content
```
![img1]
description 1
--
![img2]
description 2
--
...
```
| name | usage |
| --- | --- |
| style | Your custom css, multiline supported |
| content | Your bozodown |
## Effect
Create a group of image

16
doc/photodown/image.md Normal file
View File

@ -0,0 +1,16 @@
# Image
Permit you to use your own image without think about small, thumb, large, reference the bulk page. Always permit you to use external image with the url
## Usage
```
![./bulk/path_to_image/image_large_name_without_ext]
```
## Example
On my server I got this image `bulk/01613/01613.png`, so I will used put
```
![./bulk/01613/01613]
```
And the python will display an image clickable who link to the bulk page

11
doc/photodown/readme.md Normal file
View File

@ -0,0 +1,11 @@
# PhotoDown
Just a simple extension of [BozoDown](../bozodown/readme.md)
## Add
- [Pictures gallery]()
## Modify
- [image](./image.md)

6
doc/readme.md Normal file
View File

@ -0,0 +1,6 @@
# PhotoHUB
## Tools
### Generate page
To let user create his own page this project use [BozoDown](./bozodown/readme.md) with the [PhotoDown](./photodown/readme.md) extension

View File

@ -1,3 +1,5 @@
beautifulsoup4==4.13.4
bs4==0.0.2
exif==1.6.1 exif==1.6.1
imageio==2.37.0 imageio==2.37.0
Jinja2==3.1.6 Jinja2==3.1.6
@ -8,3 +10,5 @@ pillow==11.1.0
plum-py==0.8.7 plum-py==0.8.7
progress==1.6 progress==1.6
rawpy==0.24.0 rawpy==0.24.0
soupsieve==2.7
typing_extensions==4.14.1

1
src/bozodown/__init__.py Normal file
View File

@ -0,0 +1 @@
__all__ = ["Bozodown"]

69
src/bozodown/bozodown.py Normal file
View File

@ -0,0 +1,69 @@
from collections.abc import Callable
from . import default_converters
from bs4 import BeautifulSoup
class Bozodown():
def __init__(self):
self._converters: dict[str, dict[str, str]] = default_converters.converters.copy()
self._text_converter: dict[str, str] = default_converters.text_converter.copy()
self._specific_case_namespaces: dict[str, Callable[[object, str, str], str]] = {
"bozodown": self._bozodown_render,
}
def render(self, to_parse: str):
content: str = ""
while len(to_parse) > 0:
text, converter = self._get_first_converter(to_parse)
content += self._render_element(text, converter)
to_parse = to_parse[len(text):]
return BeautifulSoup(content, 'html.parser').prettify()
def _render_element(self, text: str, converter: dict[str, str]) -> str:
code: str = converter.get("code")
if (code is not None):
namespace, id = code.split(":")
func = self._specific_case_namespaces[namespace]
return func(id, text)
start: int = len(converter["from_prefix"])
stop: int = len(text) - len(converter.get("from_suffix", ""))
if (text[start:stop] == "\n" and converter is self._text_converter):
return ""
return f"{converter['to_prefix']}{text[start:stop]}{converter['to_suffix']}"
def _bozodown_render(self, id: str, text: str) -> str:
if (id == "list"):
print("error: list not supported")
return ""
if (id == "block"):
style, to_parse = text[5:-5].split("---")
style = style.replace("\n", "")
content: str = self.render(to_parse)
return f"<div style='{style}'>{content}</div>"
def _get_first_converter(self, text: str) -> tuple[str, dict] | None:
first_converter_found: dict[str, str] | None = None
start: int | None = None
for converter in self._converters.values():
matching_patern_pos: str = text.find(converter['from_prefix'])
if (matching_patern_pos != -1):
if (first_converter_found is None or matching_patern_pos < start):
first_converter_found = converter
start = matching_patern_pos
if (first_converter_found is None):
return text, self._text_converter
if (start != 0):
return text[:start], self._text_converter
suffix: int = first_converter_found.get("from_suffix", "")
prefix: int = first_converter_found['from_prefix']
stop: int = text.find(suffix, start + len(prefix))
if (stop == -1):
print(f"error: '{prefix}' was never finished by a '{suffix}'")
return
stop += len(suffix)
return text[start:stop], first_converter_found

View File

@ -0,0 +1,78 @@
converters: list[str, dict[str, str]] = {
"title": {
"from_prefix": "# ",
"from_suffix": "\n",
"to_prefix": "<h1>",
"to_suffix": "</h1>",
},
"subtitle": {
"from_prefix": "## ",
"from_suffix": "\n",
"to_prefix": "<h2>",
"to_suffix": "</h2>",
},
"subsubtitle": {
"from_prefix": "### ",
"from_suffix": "\n",
"to_prefix": "<h3>",
"to_suffix": "</h3>",
},
"subsubsubtitle": {
"from_prefix": "#### ",
"from_suffix": "\n",
"to_prefix": "<h4>",
"to_suffix": "</h4>",
},
"quote": {
"from_prefix": "`",
"from_suffix": "`",
"to_prefix": "<p><code>",
"to_suffix": "</code></p>",
},
"bold": {
"from_prefix": "**",
"from_suffix": "**",
"to_prefix": "<strong>",
"to_suffix": "</strong>",
},
"code": {
"from_prefix": "```",
"from_suffix": "```",
"to_prefix": "<pre><code>",
"to_suffix": "</code></pre>",
},
"image": {
"from_prefix": "![",
"from_suffix": "]",
"to_prefix": "<img src='",
"to_suffix": "'>",
},
"block": {
"from_prefix": "-----",
"from_suffix": "-----",
"code": "bozodown:block",
},
"checkbox_clear": {
"from_prefix": "- [ ] ",
"from_suffix": "\n",
"to_prefix": "<li><input type='checkbox' disabled> ",
"to_suffix": "</li>",
},
"checkbox_full": {
"from_prefix": "- [x] ",
"from_suffix": "\n",
"to_prefix": "<li><input type='checkbox' checked disabled> ",
"to_suffix": "</li>",
},
"newline": {
"from_prefix": "<br>",
"from_suffix": "",
"to_prefix": "<br>",
},
}
text_converter: dict[str, str] = {
"from_prefix": "",
"to_prefix": "<p>",
"to_suffix": "</p>",
}

View File

@ -10,9 +10,9 @@ if TYPE_CHECKING:
from path import Path from path import Path
env = Environment(loader=FileSystemLoader('src/templates')) env = Environment(loader=FileSystemLoader('src/templates'))
category_template = env.get_template('bulk_category.jinja') category_template = env.get_template('category.jinja')
class BulkCategory(): class Category():
def __init__(self, name: str, categorys_path: Path, pictures: list[Picture] = None, is_repertoried: bool = False): def __init__(self, name: str, categorys_path: Path, pictures: list[Picture] = None, is_repertoried: bool = False):
self.name: str = name self.name: str = name

35
src/collection.py Normal file
View File

@ -0,0 +1,35 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import config
from path import Path
if TYPE_CHECKING:
from photodown import Photodown
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('src/templates'))
page_template = env.get_template('collection.jinja')
class Collection:
def __init__(self, path: Path, markdown: Photodown):
self._path: Path = path
self._html: Path = Path(self._path.get_absolute_path()[:-len(config.COLLECTION_EXT)] + ".html")
self._markdown: Photodown = markdown
with open(path.get_absolute_path(), "r") as f:
self._raw_content: str = f.read()
def _get_content(self):
content = self._markdown.render(self._raw_content)
return content
def _to_html(self) -> str:
html: str = page_template.render(content=self._get_content())
return html
def create(self) -> Path:
with open(self._html.get_absolute_path(), "w") as f:
f.write(self._to_html())

View File

@ -2,4 +2,5 @@
CREATE_GENERAL_CATEGORY: bool = True CREATE_GENERAL_CATEGORY: bool = True
THUMB_DIMENSION: tuple[int, int] = (200, 200) THUMB_DIMENSION: tuple[int, int] = (200, 200)
MAX_THREADS: int = 50 MAX_THREADS: int = 50
COLLECTION_EXT: str = ".ph"

View File

@ -1,11 +1,12 @@
import sys
import os
from progress.bar import Bar from progress.bar import Bar
from path import Path from path import Path
from picture import Picture from picture import Picture
from bulk_page import BulkPage from page import Page
from bulk_category import BulkCategory from category import Category
from collection import Collection
from photodown import Photodown
import argparse import argparse
import config import config
@ -21,14 +22,14 @@ def argument_parsing():
args = parser.parse_args() args = parser.parse_args()
return args return args
def scan_pages(folders: list[Path]) -> list[BulkPage]: def scan_pages(folders: list[Path]) -> list[Page]:
pages: list[BulkPage] = [] pages: list[Page] = []
prev: BulkPage = None prev: Page = None
with Bar("Scanning Pages...", max=len(folders)) as bar: with Bar("Scanning Pages...", max=len(folders)) as bar:
for folder in folders: for folder in folders:
files: list[Path] = folder.get_files() files: list[Path] = folder.get_files()
page: BulkPage = BulkPage(folder, folder.get_name(), prev=prev) page: Page = Page(folder, folder.get_name(), prev=prev)
raw: Path = Path(folder, folder.get_name() + ".NEF") raw: Path = Path(folder, folder.get_name() + ".NEF")
id: int = 0 id: int = 0
@ -51,55 +52,69 @@ def scan_pages(folders: list[Path]) -> list[BulkPage]:
pages[0].prev = pages[-1] pages[0].prev = pages[-1]
return pages return pages
def create_pages(pages: list[BulkPage]) -> None: def create_pages(pages: list[Page]) -> None:
with Bar("Generating Pages...", max=len(pages)) as bar: with Bar("Generating Pages...", max=len(pages)) as bar:
for page in pages: for page in pages:
page.create() page.create()
bar.next() bar.next()
def scan_categories(pages: list[BulkPage], categories_path: Path) -> list[BulkCategory]: def scan_categories(pages: list[Page], categories_path: Path) -> list[Category]:
categories: dict[str, BulkCategory] = {} categories: dict[str, Category] = {}
with Bar("Scanning pages...", max=len(pages)) as bar: with Bar("Scanning pages...", max=len(pages)) as bar:
for page in pages: for page in pages:
for picture in page.get_pictures(): for picture in page.get_pictures():
for category_name in picture.get_categories_name(): for category_name in picture.get_categories_name():
category: BulkCategory | None = categories.get(category_name) category: Category | None = categories.get(category_name)
if (category is None): if (category is None):
category = BulkCategory(category_name, categories_path) category = Category(category_name, categories_path)
categories.update({category_name: category}) categories.update({category_name: category})
picture.categories.append(category) picture.categories.append(category)
category.add_picture(picture) category.add_picture(picture)
bar.next() bar.next()
return (categories.values()) return (categories.values())
def create_categories(categories: list[BulkCategory]) -> None: def create_categories(categories: list[Category]) -> None:
with Bar("Generating categories...", max=len(categories)) as bar: with Bar("Generating categories...", max=len(categories)) as bar:
for category in categories: for category in categories:
category.create() category.create()
bar.next() bar.next()
def gen_bulk(bulk_path: Path): def gen_bulk(bulk_path: Path, markdown: Photodown):
category_path: Path = Path(bulk_path, "categories") category_path: Path = Path(bulk_path, "categories")
pages: list[BulkPage] = scan_pages(bulk_path.get_dirs()) pages: list[Page] = scan_pages(bulk_path.get_dirs())
categories: list[BulkCategory] = scan_categories(pages, category_path) categories: list[Category] = scan_categories(pages, category_path)
bulk: list[Picture] = []
for page in pages:
for picture in page.get_pictures():
bulk.append(picture)
markdown.add_bulk(bulk)
if config.CREATE_GENERAL_CATEGORY: if config.CREATE_GENERAL_CATEGORY:
for category in categories: for category in categories:
if (category.name == "general"): if (category.name == "general"):
category.path = Path(bulk_path, "index.html") category.path = Path(bulk_path, "index.html")
Path("./src/templates/bulk_page.css").copy_to(Path(bulk_path, "bulk_page.css")) Path("./src/templates/page.css").copy_to(Path(bulk_path, "page.css"))
create_pages(pages) create_pages(pages)
Path("./src/templates/bulk_category.css").copy_to(Path(bulk_path, "bulk_category.css")) Path("./src/templates/category.css").copy_to(Path(bulk_path, "category.css"))
if (not category_path.exist()): if (not category_path.exist()):
category_path.create() category_path.create()
create_categories(categories) create_categories(categories)
def scan_collections(site_path: Path, markdown: Photodown):
for path in [f for f in site_path.get_files() if f.get_name().endswith(config.COLLECTION_EXT)]:
collection: Collection = Collection(path, markdown)
collection.create()
def regen(bulk_path: Path, is_thumb: bool, is_small: bool): def gen_collections(site_path: Path, markdown: Photodown):
pages: list[BulkPage] = scan_pages(bulk_path.get_dirs()) scan_collections(site_path, markdown)
Path("./src/templates/collection.css").copy_to(Path(site_path, "collection.css"))
def regen(_path: Path, is_thumb: bool, is_small: bool):
pages: list[Page] = scan_pages(_path.get_dirs())
with Bar("Regenerating assets...", max=len(pages)) as bar: with Bar("Regenerating assets...", max=len(pages)) as bar:
for page in pages: for page in pages:
for picture in page.get_pictures(): for picture in page.get_pictures():
@ -123,7 +138,9 @@ def main():
bulk_path: Path = Path(site_path, "bulk/") bulk_path: Path = Path(site_path, "bulk/")
if args.regen is not None: if args.regen is not None:
regen(bulk_path, 't' in args.regen, 's' in args.regen) regen(bulk_path, 't' in args.regen, 's' in args.regen)
gen_bulk(bulk_path) markdown = Photodown()
gen_bulk(bulk_path, markdown)
gen_collections(site_path, markdown)
gen_home(site_path) gen_home(site_path)

View File

@ -9,15 +9,14 @@ from path import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from bulk_page import Page
from picture import Picture from picture import Picture
env = Environment(loader=FileSystemLoader('src/templates')) env = Environment(loader=FileSystemLoader('src/templates'))
page_template = env.get_template('bulk_page.jinja') page_template = env.get_template('page.jinja')
class BulkPage(): class Page():
def __init__(self, path: Path, name: str, pictures: list[Picture] = None, prev: BulkPage|None = None, next: BulkPage|None = None): def __init__(self, path: Path, name: str, pictures: list[Picture] = None, prev: Page|None = None, next: Page|None = None):
self.name: str = name self.name: str = name
self._path: Path = path self._path: Path = path
self._pictures: list[Picture] = pictures or [] self._pictures: list[Picture] = pictures or []

View File

@ -71,4 +71,8 @@ class Path():
return self._absolute_path return self._absolute_path
def __repr__(self): def __repr__(self):
return f"Path({self._absolute_path})" return f"Path({self._absolute_path})"
def __eq__(self, value):
if (isinstance(value, Path)):
self = Path(value.get_absolute_path())

51
src/photodown.py Normal file
View File

@ -0,0 +1,51 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from bozodown.bozodown import Bozodown
if TYPE_CHECKING:
from picture import Picture
class Photodown(Bozodown):
def __init__(self):
super().__init__()
self._specific_case_namespaces.update({"photohub": self._photohub_render})
image_converter = self._converters.get("image")
image_converter.update({"code": "photohub:image"})
self._converters.update({"pictures": {"from_prefix": "+++++","from_suffix": "+++++","code": "photohub:pictures",}})
self._bulk: list[Picture] = []
def add_bulk(self, bulk: list[Picture]):
self._bulk = bulk.copy()
def _render_image(self, url: str) -> str:
if (url.startswith("./")):
for picture in self._bulk:
if url == "." + picture.get_large().get_url()[:-4]:
return f"""<a href={picture.get_page().html.get_url()}><img src='{picture.get_small().get_url()}'></a>"""
return f"<img src='{url}'>"
def _render_pictures(self, config: dict[str, str | int], to_parse: str):
segments: list[str] = to_parse.split("--")
content: str = ""
for segment in segments:
content += f"<div class='pictures-item'>{self.render(segment)}</div>"
return f"<div class='pictures-menu'>{content}</div>"
def _photohub_render(self, id: str, text: str) -> str:
if (id == "image"):
picture_url = f"{text[2:-1]}"
return self._render_image(picture_url)
if (id == "pictures"):
config, style, to_parse = text[5:-5].split("---")
style = style.replace("\n", "")
config = {
k: v for line in config.strip().splitlines()
if ": " in line
for k, v in [line.split(": ", 1)]
}
content: str = self._render_pictures(config, to_parse)
return f"<div style='{style}'>{content}</div>"

View File

@ -10,11 +10,11 @@ from typing import TYPE_CHECKING
import config import config
if TYPE_CHECKING: if TYPE_CHECKING:
from bulk_page import BulkPage from page import Page
from bulk_category import BulkCategory from category import Category
class Picture(): class Picture():
def __init__(self, picture_path: Path, id: int, page: BulkPage = None, raw: Path|None = None, is_repertoried: bool = True): def __init__(self, picture_path: Path, id: int, page: Page = None, raw: Path|None = None, is_repertoried: bool = True):
self._large: Path = picture_path self._large: Path = picture_path
self._small: Path = Path(picture_path.get_absolute_path()[:-4] + "_small.jpg") self._small: Path = Path(picture_path.get_absolute_path()[:-4] + "_small.jpg")
self._thumb: Path = Path(picture_path.get_absolute_path()[:-4] + "_thumb.jpg") self._thumb: Path = Path(picture_path.get_absolute_path()[:-4] + "_thumb.jpg")
@ -22,9 +22,9 @@ class Picture():
self._categories_file: Path = Path(picture_path.get_absolute_path()[:-4] + "_categories.txt") self._categories_file: Path = Path(picture_path.get_absolute_path()[:-4] + "_categories.txt")
self._raw: Path|None = raw self._raw: Path|None = raw
self.id: int = id self.id: int = id
self._page: BulkPage = page self._page: Page = page
self._categories_name: list[str] = [] self._categories_name: list[str] = []
self.categories: list[BulkCategory] = [] self.categories: list[Category] = []
if self._categories_file.exist(): if self._categories_file.exist():
with open(self._categories_file.get_absolute_path(), "r+") as f: with open(self._categories_file.get_absolute_path(), "r+") as f:
categories_name: list[str] = f.read() categories_name: list[str] = f.read()

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="/bulk/bulk_category.css"> <link rel="stylesheet" href="/bulk/category.css">
</head> </head>
<body> <body>

View File

@ -0,0 +1,6 @@
img {
width: 1000px;
height: 1000px;
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/page.css">
</head>
<body>
{{ content }}
</body>
</html>

0
src/templates/home.css Normal file
View File

View File

@ -1,7 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<body> <body>
<a href="/bulk/"><h1>See all pictures I take</h1></a>
<a href="/bulk/"><h1>Site WIP go to BULK</h1></a> <ul>
{% for collection in collections %}
<li>
<a href="{{collection.get_url() }}">{{ collection.get_name()}} </a>
</li>
{% endfor %}
</ul>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="/bulk/bulk_page.css"> <link rel="stylesheet" href="/bulk/page.css">
</head> </head>
<body> <body>