Support for folders for pages/articles

This commit is contained in:
Fabio Manganiello 2022-06-14 00:32:35 +02:00
parent 2d18dd5bd6
commit 31e1db359d
7 changed files with 179 additions and 58 deletions

View file

@ -81,7 +81,7 @@ categories:
## Markdown files ## Markdown files
Articles are Markdown files stored under `pages`. For an article to be correctly rendered, Articles are Markdown files stored under `markdown`. For an article to be correctly rendered,
you need to start the Markdown file with the following metadata header: you need to start the Markdown file with the following metadata header:
```markdown ```markdown
@ -92,6 +92,14 @@ you need to start the Markdown file with the following metadata header:
[//]: # (published: 2022-01-01) [//]: # (published: 2022-01-01)
``` ```
If no `markdown` folder exists in the base directory, then the base directory itself will be treated as a root for
Markdown files.
### Folders
You can organize Markdown files in folders. If multiple folders are present, pages on the home will be grouped by
folders.
## Images ## Images
Images are stored under `img`. You can reference them in your articles through the following syntax: Images are stored under `img`. You can reference them in your articles through the following syntax:

49
madblog/_sorters.py Normal file
View file

@ -0,0 +1,49 @@
from abc import ABC, abstractmethod
from datetime import datetime, date
from typing import Any, Iterable, Tuple
class PagesSorter(ABC):
_default_published = date.fromtimestamp(0)
def __init__(self, pages: Iterable[dict]):
self.pages = pages
@abstractmethod
def __call__(self, page: dict) -> Any:
raise NotImplemented()
class PagesSortByTime(PagesSorter):
def __call__(self, page: dict) -> datetime:
return page.get('published', self._default_published)
class PagesSortByFolderAndTime(PagesSorter):
def __call__(self, page: dict) -> Tuple:
return (
page.get('folder'),
date.today() - page.get(
'published', self._default_published
)
)
class PagesSortByTimeGroupedByFolder(PagesSorter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
st = {}
for page in self.pages:
folder = page.get('folder', '')
published = page.get('published', self._default_published)
st[folder] = st.get(folder, published)
st[folder] = max(st[folder], published)
self._max_date_by_folder = st
def __call__(self, page: dict) -> Tuple:
return (
self._max_date_by_folder[page.get('folder', '')],
page.get('published', self._default_published)
)

View file

@ -1,14 +1,14 @@
import datetime import datetime
import os import os
import re import re
from glob import glob from typing import Optional, List, Tuple, Type
from typing import Optional
from flask import Flask, abort from flask import Flask, abort
from markdown import markdown from markdown import markdown
from .config import config from .config import config
from .latex import MarkdownLatex from .latex import MarkdownLatex
from ._sorters import PagesSorter, PagesSortByTime
class BlogApp(Flask): class BlogApp(Flask):
@ -46,11 +46,11 @@ class BlogApp(Flask):
if not page.endswith('.md'): if not page.endswith('.md'):
page = page + '.md' page = page + '.md'
if not os.path.isfile(os.path.join(self.pages_dir, page)): md_file = os.path.join(self.pages_dir, page)
if not os.path.isfile(md_file):
abort(404) abort(404)
metadata = {} metadata = {}
md_file = os.path.join(self.pages_dir, page)
with open(md_file, 'r') as f: with open(md_file, 'r') as f:
metadata['uri'] = '/article/' + page[:-3] metadata['uri'] = '/article/' + page[:-3]
@ -123,19 +123,37 @@ class BlogApp(Flask):
skip_header=skip_header skip_header=skip_header
) )
def get_pages(self, with_content: bool = False, skip_header: bool = False) -> list: def get_pages(
return sorted( self,
[ with_content: bool = False,
{ skip_header: bool = False,
'path': path[len(app.pages_dir)+1:], sorter: Type[PagesSorter] = PagesSortByTime,
'content': self.get_page(path[len(app.pages_dir)+1:], skip_header=skip_header) if with_content else '', reverse: bool = True,
**self.get_page_metadata(os.path.basename(path)), ) -> List[Tuple[int, dict]]:
} pages_dir = app.pages_dir.rstrip('/')
for path in glob(os.path.join(app.pages_dir, '*.md')) pages = [
], {
key=lambda page: page.get('published', datetime.date.fromtimestamp(0)), 'path': os.path.join(root[len(pages_dir)+1:], f),
reverse=True 'folder': root[len(pages_dir)+1:],
) 'content': (
self.get_page(
os.path.join(root, f),
skip_header=skip_header
)
if with_content else ''
),
**self.get_page_metadata(
os.path.join(root[len(pages_dir)+1:], f)
),
}
for root, _, files in os.walk(pages_dir, followlinks=True)
for f in files
if f.endswith('.md')
]
sorter_func = sorter(pages)
pages.sort(key=sorter_func, reverse=reverse)
return [(i, page) for i, page in enumerate(pages)]
app = BlogApp(__name__) app = BlogApp(__name__)

View file

@ -13,7 +13,7 @@ class Config:
language = 'en-US' language = 'en-US'
logo = '/img/icon.png' logo = '/img/icon.png'
header = True header = True
content_dir = None content_dir = '.'
categories = None categories = None
basedir = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))

View file

@ -5,6 +5,7 @@ from flask import request, Response, send_from_directory as send_from_directory_
from .app import app from .app import app
from .config import config from .config import config
from ._sorters import PagesSortByTimeGroupedByFolder
def send_from_directory(path: str, file: str, alternative_path: Optional[str] = None, *args, **kwargs): def send_from_directory(path: str, file: str, alternative_path: Optional[str] = None, *args, **kwargs):
@ -15,7 +16,11 @@ def send_from_directory(path: str, file: str, alternative_path: Optional[str] =
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def home_route(): def home_route():
return render_template('index.html', pages=app.get_pages(), config=config) return render_template(
'index.html',
pages=app.get_pages(sorter=PagesSortByTimeGroupedByFolder),
config=config
)
@app.route('/img/<img>', methods=['GET']) @app.route('/img/<img>', methods=['GET'])
@ -38,9 +43,14 @@ def fonts_route(file: str):
return send_from_directory(app.fonts_dir, file, config.default_fonts_dir) return send_from_directory(app.fonts_dir, file, config.default_fonts_dir)
@app.route('/article/<path:path>/<article>', methods=['GET'])
def article_with_path_route(path: str, article: str):
return app.get_page(os.path.join(path, article))
@app.route('/article/<article>', methods=['GET']) @app.route('/article/<article>', methods=['GET'])
def article_route(article: str): def article_route(article: str):
return app.get_page(article) return article_with_path_route('', article)
@app.route('/rss', methods=['GET']) @app.route('/rss', methods=['GET'])
@ -71,7 +81,7 @@ def rss_route():
link=config.link, link=config.link,
categories=','.join(config.categories), categories=','.join(config.categories),
language=config.language, language=config.language,
last_pub_date=pages[0]['published'].strftime('%a, %d %b %Y %H:%M:%S GMT'), last_pub_date=pages[0][1]['published'].strftime('%a, %d %b %Y %H:%M:%S GMT'),
items='\n\n'.join([ items='\n\n'.join([
''' '''
<item> <item>
@ -89,7 +99,7 @@ def rss_route():
content=page.get('description', '') if short_description else page.get('content', ''), content=page.get('description', '') if short_description else page.get('content', ''),
image=page.get('image', ''), image=page.get('image', ''),
) )
for page in pages for _, page in pages
]), ]),
), mimetype='application/rss+xml') ), mimetype='application/rss+xml')

View file

@ -8,7 +8,26 @@ main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
overflow: hidden; overflow: auto;
}
.folder {
width: 100%;
display: flex;
flex-direction: column;
}
.folder .folder-title {
width: calc(100% - 0.66em);
display: flex;
background: linear-gradient(45deg, #f0f0f0, #fafaffe0);
box-shadow: 1px 1px 1px 1px #e0e0e080;
font-size: 0.8em;
padding: 0.33em;
}
.folder .folder-title a {
width: 100%;
} }
.articles { .articles {
@ -16,7 +35,6 @@ main {
height: 100%; height: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
overflow: auto;
padding: 0; padding: 0;
} }
@ -36,11 +54,11 @@ main {
border-radius: .5em; border-radius: .5em;
} }
@media screen and (min-width: 767px) { /* @media screen and (min-width: 767px) { */
.article { /* .article { */
max-height: 55%; /* max-height: 55%; */
} /* } */
} /* } */
@media screen and (min-width: 640px) and (max-width: 767px) { @media screen and (min-width: 640px) and (max-width: 767px) {
.article { .article {

View file

@ -1,41 +1,59 @@
{% with title=title or config.title or 'Blog', skip_header=not config.header, styles=['/css/home.css'] %} {% with title=title or config.title or 'Blog', skip_header=not config.header, styles=['/css/home.css'] %}
{% include 'common-head.html' %} {% include 'common-head.html' %}
{% endwith %} {% endwith %}
<main> <main>
<div class="index"> <div class="index">
{% set state = {} %}
{% for i, page in pages %}
{% if 'cur_folder' not in state or page.get('folder') != state.get('cur_folder') %}
<div class="folder">
{% set folder = page.get('folder') %}
{% if folder %}
<div id="{{ folder.replace('/', '-') }}" class="folder-title">
<a href="#{{ folder.replace('/', '-') }}">
{{ folder }}
</a>
</div>
{% endif %}
<div class="articles"> <div class="articles">
{% for page in pages %} {% endif %}
<a class="article" href="{{ page['uri'] }}"> {% if state.update({'cur_folder': page.get('folder')}) %}{% endif %}
<div class="container">
{% if page['image'] %}
<div class="image">
<img src="{{ page['image'] }}" alt="">
</div>
{% endif %}
<div class="title"> <a class="article" href="{{ page['uri'] }}">
{{ page['title'] }} <div class="container">
</div> {% if page['image'] %}
<div class="image">
<img src="{{ page['image'] }}" alt="">
</div>
{% endif %}
{% if page['published'] %} <div class="title">
<div class="published-date"> {{ page['title'] }}
{{ page['published'].strftime('%b %d, %Y') }} </div>
</div>
{% endif %}
{% if page['description'] %} {% if page['published'] %}
<div class="description"> <div class="published-date">
{{ page['description'] }} {{ page['published'].strftime('%b %d, %Y') }}
</div> </div>
{% endif %} {% endif %}
</div>
</a> {% if page['description'] %}
{% endfor %} <div class="description">
{{ page['description'] }}
</div>
{% endif %}
</div>
</a>
{% if i == pages|length - 1 or pages[i+1][1].get('folder') != page.get('folder') %}
</div> </div>
</div> </div>
{% endif %}
{% endfor %}
</div>
{% include 'footer.html' %} {% include 'footer.html' %}
</main> </main>
{% include 'common-tail.html' %} {% include 'common-tail.html' %}