From 31e1db359d0fbdb44ffe9da45a3957e1ae5c9834 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 14 Jun 2022 00:32:35 +0200 Subject: [PATCH] Support for folders for pages/articles --- README.md | 10 ++++- madblog/_sorters.py | 49 ++++++++++++++++++++++++ madblog/app.py | 52 ++++++++++++++++--------- madblog/config.py | 2 +- madblog/routes.py | 18 +++++++-- madblog/static/css/home.css | 32 ++++++++++++---- madblog/templates/index.html | 74 ++++++++++++++++++++++-------------- 7 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 madblog/_sorters.py diff --git a/README.md b/README.md index 29a01d6..d059221 100644 --- a/README.md +++ b/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: diff --git a/madblog/_sorters.py b/madblog/_sorters.py new file mode 100644 index 0000000..bbb85bb --- /dev/null +++ b/madblog/_sorters.py @@ -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) + ) diff --git a/madblog/app.py b/madblog/app.py index 256fc5e..95f467c 100644 --- a/madblog/app.py +++ b/madblog/app.py @@ -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( - [ - { - '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 - ) + 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': 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__) diff --git a/madblog/config.py b/madblog/config.py index 755e654..963fc29 100644 --- a/madblog/config.py +++ b/madblog/config.py @@ -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__)) diff --git a/madblog/routes.py b/madblog/routes.py index e1f8b1c..117d5b7 100644 --- a/madblog/routes.py +++ b/madblog/routes.py @@ -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/', 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//
', methods=['GET']) +def article_with_path_route(path: str, article: str): + return app.get_page(os.path.join(path, article)) + + @app.route('/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([ ''' @@ -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') diff --git a/madblog/static/css/home.css b/madblog/static/css/home.css index ad56818..ca62743 100644 --- a/madblog/static/css/home.css +++ b/madblog/static/css/home.css @@ -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 { diff --git a/madblog/templates/index.html b/madblog/templates/index.html index 0315b56..feba856 100644 --- a/madblog/templates/index.html +++ b/madblog/templates/index.html @@ -1,41 +1,59 @@ {% 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 %}
-
+
+ {% set state = {} %} + {% for i, page in pages %} + {% if 'cur_folder' not in state or page.get('folder') != state.get('cur_folder') %} +
+ {% set folder = page.get('folder') %} + {% if folder %} + + {% endif %}
- {% for page in pages %} - - + + + {% if i == pages|length - 1 or pages[i+1][1].get('folder') != page.get('folder') %}
-
+
+ {% endif %} + {% endfor %} +
- {% include 'footer.html' %} + {% include 'footer.html' %}
{% include 'common-tail.html' %}