Support for folders for pages/articles
This commit is contained in:
parent
2d18dd5bd6
commit
31e1db359d
7 changed files with 179 additions and 58 deletions
10
README.md
10
README.md
|
@ -81,7 +81,7 @@ categories:
|
|||
|
||||
## 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:
|
||||
|
||||
```markdown
|
||||
|
@ -92,6 +92,14 @@ you need to start the Markdown file with the following metadata header:
|
|||
[//]: # (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 are stored under `img`. You can reference them in your articles through the following syntax:
|
||||
|
|
49
madblog/_sorters.py
Normal file
49
madblog/_sorters.py
Normal 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)
|
||||
)
|
|
@ -1,14 +1,14 @@
|
|||
import datetime
|
||||
import os
|
||||
import re
|
||||
from glob import glob
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Tuple, Type
|
||||
|
||||
from flask import Flask, abort
|
||||
from markdown import markdown
|
||||
|
||||
from .config import config
|
||||
from .latex import MarkdownLatex
|
||||
from ._sorters import PagesSorter, PagesSortByTime
|
||||
|
||||
|
||||
class BlogApp(Flask):
|
||||
|
@ -46,11 +46,11 @@ class BlogApp(Flask):
|
|||
if not page.endswith('.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)
|
||||
|
||||
metadata = {}
|
||||
md_file = os.path.join(self.pages_dir, page)
|
||||
with open(md_file, 'r') as f:
|
||||
metadata['uri'] = '/article/' + page[:-3]
|
||||
|
||||
|
@ -123,19 +123,37 @@ class BlogApp(Flask):
|
|||
skip_header=skip_header
|
||||
)
|
||||
|
||||
def get_pages(self, with_content: bool = False, skip_header: bool = False) -> list:
|
||||
return sorted(
|
||||
[
|
||||
def get_pages(
|
||||
self,
|
||||
with_content: bool = False,
|
||||
skip_header: bool = False,
|
||||
sorter: Type[PagesSorter] = PagesSortByTime,
|
||||
reverse: bool = True,
|
||||
) -> List[Tuple[int, dict]]:
|
||||
pages_dir = app.pages_dir.rstrip('/')
|
||||
pages = [
|
||||
{
|
||||
'path': path[len(app.pages_dir)+1:],
|
||||
'content': self.get_page(path[len(app.pages_dir)+1:], skip_header=skip_header) if with_content else '',
|
||||
**self.get_page_metadata(os.path.basename(path)),
|
||||
}
|
||||
for path in glob(os.path.join(app.pages_dir, '*.md'))
|
||||
],
|
||||
key=lambda page: page.get('published', datetime.date.fromtimestamp(0)),
|
||||
reverse=True
|
||||
'path': os.path.join(root[len(pages_dir)+1:], f),
|
||||
'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__)
|
||||
|
|
|
@ -13,7 +13,7 @@ class Config:
|
|||
language = 'en-US'
|
||||
logo = '/img/icon.png'
|
||||
header = True
|
||||
content_dir = None
|
||||
content_dir = '.'
|
||||
categories = None
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
|
|
@ -5,6 +5,7 @@ from flask import request, Response, send_from_directory as send_from_directory_
|
|||
|
||||
from .app import app
|
||||
from .config import config
|
||||
from ._sorters import PagesSortByTimeGroupedByFolder
|
||||
|
||||
|
||||
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'])
|
||||
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'])
|
||||
|
@ -38,9 +43,14 @@ def fonts_route(file: str):
|
|||
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'])
|
||||
def article_route(article: str):
|
||||
return app.get_page(article)
|
||||
return article_with_path_route('', article)
|
||||
|
||||
|
||||
@app.route('/rss', methods=['GET'])
|
||||
|
@ -71,7 +81,7 @@ def rss_route():
|
|||
link=config.link,
|
||||
categories=','.join(config.categories),
|
||||
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([
|
||||
'''
|
||||
<item>
|
||||
|
@ -89,7 +99,7 @@ def rss_route():
|
|||
content=page.get('description', '') if short_description else page.get('content', ''),
|
||||
image=page.get('image', ''),
|
||||
)
|
||||
for page in pages
|
||||
for _, page in pages
|
||||
]),
|
||||
), mimetype='application/rss+xml')
|
||||
|
||||
|
|
|
@ -8,7 +8,26 @@ main {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
|
@ -16,7 +35,6 @@ main {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
@ -36,11 +54,11 @@ main {
|
|||
border-radius: .5em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 767px) {
|
||||
.article {
|
||||
max-height: 55%;
|
||||
}
|
||||
}
|
||||
/* @media screen and (min-width: 767px) { */
|
||||
/* .article { */
|
||||
/* max-height: 55%; */
|
||||
/* } */
|
||||
/* } */
|
||||
|
||||
@media screen and (min-width: 640px) and (max-width: 767px) {
|
||||
.article {
|
||||
|
|
|
@ -4,8 +4,22 @@
|
|||
|
||||
<main>
|
||||
<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">
|
||||
{% for page in pages %}
|
||||
{% endif %}
|
||||
{% if state.update({'cur_folder': page.get('folder')}) %}{% endif %}
|
||||
|
||||
<a class="article" href="{{ page['uri'] }}">
|
||||
<div class="container">
|
||||
{% if page['image'] %}
|
||||
|
@ -31,9 +45,13 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
{% if i == pages|length - 1 or pages[i+1][1].get('folder') != page.get('folder') %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
</main>
|
||||
|
|
Loading…
Reference in a new issue