Compare commits

..

37 commits

Author SHA1 Message Date
5505d074f0 Bump version: 0.2.34 → 0.2.35 2025-03-25 18:57:08 +01:00
279e527736 Updated CHANGELOG 2025-03-25 18:56:48 +01:00
39de4ed593 Use Lora font by default for article content. 2025-03-25 18:55:25 +01:00
68b7516f98 Bump version: 0.2.33 → 0.2.34 2025-02-16 11:59:09 +01:00
b38048e57b Migrated from setup.cfg to pyproject.toml 2025-02-16 11:56:35 +01:00
574cde8c2d Better font-family for multiple devices and better padding on mobile. 2025-02-16 11:15:31 +01:00
Fabio Manganiello
6d3cdc7fb0 Bump version: 0.2.32 → 0.2.33 2025-01-30 15:29:42 +01:00
Fabio Manganiello
0979f8dad6 Increased post container max-width for large screens 2025-01-30 15:29:25 +01:00
Fabio Manganiello
982c596d71 Bump version: 0.2.31 → 0.2.32 2025-01-30 15:27:49 +01:00
Fabio Manganiello
3f47f3a656 Migrated font-family for posts content from serif to sans 2025-01-30 15:27:08 +01:00
Fabio Manganiello
45a5371074 Bump version: 0.2.30 → 0.2.31 2024-10-25 10:05:09 +02:00
Fabio Manganiello
79555fb14f Better padding and font-size 2024-10-25 10:04:18 +02:00
Fabio Manganiello
620086c17a Bumped to version 0.2.30 2024-10-25 02:15:49 +02:00
Fabio Manganiello
2458d2cc0c Default font 2024-10-25 02:06:56 +02:00
ba801995a8 Bumped to version 0.2.29 2024-08-19 11:43:11 +02:00
7bea868899 More font-size fixes 2024-08-19 11:42:52 +02:00
6de5db8485 Bumped to version 0.2.28 2024-08-19 11:38:32 +02:00
aec603f2e8 Some fixes on font size 2024-08-19 11:37:52 +02:00
cece2bee68 Bumped to version 0.2.27 2024-08-19 11:30:21 +02:00
e11c3b2688 Use sans-serif font for content 2024-08-19 11:28:52 +02:00
50207d7e25 Bump version: 0.2.25 → 0.2.26 2024-06-03 13:15:40 +02:00
8d74cb0c26 Added tables to the list of markdown extensions 2024-06-03 13:14:29 +02:00
90d85fedb4 Bump version: 0.2.24 → 0.2.25 2024-06-01 01:08:27 +02:00
279d885478 Updated CHANGELOG 2024-06-01 01:08:22 +02:00
56417c6763 Keep a sans-serif font for the index and for the titles 2024-06-01 01:07:34 +02:00
d78393f1b3 Bump version: 0.2.23 → 0.2.24 2024-06-01 01:02:03 +02:00
bf80f73950 Switch from a sans-serif to a serif default font 2024-06-01 01:01:39 +02:00
38b079d42d Fixed base_link 2024-04-11 02:21:09 +02:00
43897cc961 Don't prepend images with the blog base URL if they are already full URLs 2024-04-11 02:18:56 +02:00
bf714a30bc Bump version: 0.2.22 → 0.2.23 2024-04-11 01:24:52 +02:00
Fabio Manganiello
89ea18a805 Allow parentheses in post headers values 2024-04-11 01:24:38 +02:00
e3b30e6a98 Bump version: 0.2.21 → 0.2.22 2024-04-11 01:10:38 +02:00
Fabio Manganiello
817b23ac69 Always cast post datetimes to dates on render 2024-04-11 01:10:27 +02:00
d517416077 Bump version: 0.2.20 → 0.2.21 2024-04-11 01:05:13 +02:00
Fabio Manganiello
390ca758b9 More resilient logic to handle both date and datetime timestamps in a post metadata 2024-04-11 01:04:13 +02:00
609dd14d90 Bump version: 0.2.19 → 0.2.20 2023-05-08 12:11:16 +02:00
958f4106cc Fixed broken format strings in RSS feed route 2023-05-08 12:10:15 +02:00
23 changed files with 218 additions and 119 deletions

View file

@ -1,5 +1,14 @@
# Changelog
## 0.2.35
- Use _Lora_ font for the article body.
## 0.2.24
- Better default fonts - `sans-serif` style for the index and the titles,
`serif` for the articles' body.
## 0.2.19
- Added `short_feed` configuration flag to permanently disable returning the

View file

@ -1 +1 @@
__version__ = '0.2.19'
__version__ = "0.2.35"

View file

@ -12,11 +12,11 @@ from ._sorters import PagesSorter, PagesSortByTime
class BlogApp(Flask):
_title_header_regex = re.compile(r'^#\s*((\[(.*)\])|(.*))')
_title_header_regex = re.compile(r"^#\s*((\[(.*)\])|(.*))")
def __init__(self, *args, **kwargs):
super().__init__(*args, template_folder=config.templates_dir, **kwargs)
self.pages_dir = os.path.join(config.content_dir, 'markdown')
self.pages_dir = os.path.join(config.content_dir, "markdown")
self.img_dir = config.default_img_dir
self.css_dir = config.default_css_dir
self.js_dir = config.default_js_dir
@ -27,73 +27,77 @@ class BlogApp(Flask):
# `config.content_dir` is treated as the root for markdown files.
self.pages_dir = config.content_dir
img_dir = os.path.join(config.content_dir, 'img')
img_dir = os.path.join(config.content_dir, "img")
if os.path.isdir(img_dir):
self.img_dir = os.path.abspath(img_dir)
else:
self.img_dir = config.content_dir
css_dir = os.path.join(config.content_dir, 'css')
css_dir = os.path.join(config.content_dir, "css")
if os.path.isdir(css_dir):
self.css_dir = os.path.abspath(css_dir)
js_dir = os.path.join(config.content_dir, 'js')
js_dir = os.path.join(config.content_dir, "js")
if os.path.isdir(js_dir):
self.js_dir = os.path.abspath(js_dir)
fonts_dir = os.path.join(config.content_dir, 'fonts')
fonts_dir = os.path.join(config.content_dir, "fonts")
if os.path.isdir(fonts_dir):
self.fonts_dir = os.path.abspath(fonts_dir)
templates_dir = os.path.join(config.content_dir, 'templates')
templates_dir = os.path.join(config.content_dir, "templates")
if os.path.isdir(templates_dir):
self.template_folder = os.path.abspath(templates_dir)
def get_page_metadata(self, page: str) -> dict:
if not page.endswith('.md'):
page = page + '.md'
if not page.endswith(".md"):
page = page + ".md"
md_file = os.path.join(self.pages_dir, page)
if not os.path.isfile(md_file):
abort(404)
metadata = {}
with open(md_file, 'r') as f:
metadata['uri'] = '/article/' + page[:-3]
with open(md_file, "r") as f:
metadata["uri"] = "/article/" + page[:-3]
for line in f.readlines():
for line in f:
if not line:
continue
if not (m := re.match(r'^\[//]: # \(([^:]+):\s*([^)]+)\)\s*$', line)):
if not (m := re.match(r"^\[//]: # \(([^:]+):\s*(.*)\)\s*$", line)):
break
if m.group(1) == 'published':
metadata[m.group(1)] = datetime.date.fromisoformat(m.group(2))
if m.group(1) == "published":
metadata[m.group(1)] = datetime.datetime.fromisoformat(
m.group(2)
).date()
else:
metadata[m.group(1)] = m.group(2)
if not metadata.get('title'):
if not metadata.get("title"):
# If the `title` header isn't available in the file,
# infer it from the first line of the file
with open(md_file, 'r') as f:
header = ''
with open(md_file, "r") as f:
header = ""
for line in f.readlines():
header = line
break
metadata['title_inferred'] = True
metadata["title_inferred"] = True
m = self._title_header_regex.search(header)
if m:
metadata['title'] = m.group(3) or m.group(1)
metadata["title"] = m.group(3) or m.group(1)
else:
metadata['title'] = os.path.basename(md_file)
metadata["title"] = os.path.basename(md_file)
if not metadata.get('published'):
if not metadata.get("published"):
# If the `published` header isn't available in the file,
# infer it from the file's creation date
metadata['published'] = datetime.date.fromtimestamp(os.stat(md_file).st_ctime)
metadata['published_inferred'] = True
metadata["published"] = datetime.date.fromtimestamp(
os.stat(md_file).st_ctime
)
metadata["published_inferred"] = True
return metadata
@ -102,37 +106,42 @@ class BlogApp(Flask):
page: str,
title: Optional[str] = None,
skip_header: bool = False,
skip_html_head: bool = False
skip_html_head: bool = False,
):
if not page.endswith('.md'):
page = page + '.md'
if not page.endswith(".md"):
page = page + ".md"
metadata = self.get_page_metadata(page)
# Don't duplicate the page title if it's been inferred
if not (title or metadata.get('title_inferred')):
title = metadata.get('title', config.title)
if not (title or metadata.get("title_inferred")):
title = metadata.get("title", config.title)
with open(os.path.join(self.pages_dir, page), 'r') as f:
with open(os.path.join(self.pages_dir, page), "r") as f:
return render_template(
'article.html',
"article.html",
config=config,
title=title,
image=metadata.get('image'),
description=metadata.get('description'),
image=metadata.get("image"),
description=metadata.get("description"),
author=(
re.match(r'(.+?)\s+<([^>]+>)', metadata['author'])[1]
if 'author' in metadata else None
),
author_email=(
re.match(r'(.+?)\s+<([^>]+)>', metadata['author'])[2]
if 'author' in metadata else None
),
published=(
metadata['published'].strftime('%b %d, %Y')
if metadata.get('published') and not metadata.get('published_inferred')
re.match(r"(.+?)\s+<([^>]+>)", metadata["author"])[1]
if "author" in metadata
else None
),
content=markdown(f.read(), extensions=['fenced_code', 'codehilite', MarkdownLatex()]),
author_email=(
re.match(r"(.+?)\s+<([^>]+)>", metadata["author"])[2]
if "author" in metadata
else None
),
published=(
metadata["published"].strftime("%b %d, %Y")
if metadata.get("published")
and not metadata.get("published_inferred")
else None
),
content=markdown(
f.read(), extensions=["fenced_code", "codehilite", "tables", MarkdownLatex()]
),
skip_header=skip_header,
skip_html_head=skip_html_head,
)
@ -145,26 +154,25 @@ class BlogApp(Flask):
sorter: Type[PagesSorter] = PagesSortByTime,
reverse: bool = True,
) -> List[Tuple[int, dict]]:
pages_dir = app.pages_dir.rstrip('/')
pages_dir = app.pages_dir.rstrip("/")
pages = [
{
'path': os.path.join(root[len(pages_dir)+1:], f),
'folder': root[len(pages_dir)+1:],
'content': (
"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,
skip_html_head=skip_html_head,
)
if with_content else ''
),
**self.get_page_metadata(
os.path.join(root[len(pages_dir)+1:], f)
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')
if f.endswith(".md")
]
sorter_func = sorter(pages)

View file

@ -1,5 +1,7 @@
import os
import re
from typing import Optional
from urllib.parse import urljoin
from flask import (
jsonify,
@ -115,8 +117,12 @@ def article_route(article: str):
@app.route("/rss", methods=["GET"])
def rss_route():
pages = app.get_pages(with_content=True, skip_header=True, skip_html_head=True)
short_description = "short" in request.args
short_description = "short" in request.args or config.short_feed
pages = app.get_pages(
with_content=not short_description,
skip_header=True,
skip_html_head=True,
)
return Response(
"""<?xml version="1.0" encoding="UTF-8" ?>
@ -149,32 +155,36 @@ def rss_route():
),
items="\n\n".join(
[
"""
(
"""
<item>
<title>{title}</title>
<link>{base_link}{link}</link>
<pubDate>{published}</pubDate>"""
+ (
"<description><![CDATA[{content}]]></description>"
if not config.short_feed
else ""
)
+ """
<media:content medium="image" url="{base_link}{image}" width="200" height="150" />
<pubDate>{published}</pubDate>
<description><![CDATA[{content}]]></description>
<media:content medium="image" url="{image}" width="200" height="150" />
</item>
""".format(
"""
).format(
base_link=config.link,
title=page.get("title", "[No Title]"),
link=page.get("uri", ""),
published=page["published"].strftime(
"%a, %d %b %Y %H:%M:%S GMT"
)
if "published" in page
else "",
content=page.get("description", "")
if short_description
else page.get("content", ""),
image=page.get("image", ""),
published=(
page["published"].strftime("%a, %d %b %Y %H:%M:%S GMT")
if "published" in page
else ""
),
content=(
page.get("description", "")
if short_description
else page.get("content", "")
),
image=(
urljoin(config.link, page["image"])
if page.get("image")
and not re.search(r"^https?://", page["image"])
else page.get("image", "")
),
)
for _, page in pages
]

View file

@ -2,22 +2,31 @@ main .content {
display: flex;
flex-direction: column;
line-height: 1.5em;
font-size: 0.95em;
}
main .content p,
main .content ul {
main .content ul,
main .content ol {
font-family: Lora, "Palatino Linotype", "Book Antiqua", "New York", "DejaVu serif", serif;
text-align: justify;
overflow-wrap: break-word;
word-break: break-word;
line-height: 1.5;
}
main .content code, .codehilite {
font-size: .85em;
font-size: 0.9em;
}
@media screen and (max-width: 767px) {
main {
font-size: 0.9em;
font-size: 0.95em;
padding: 0.25em 0.5em;
}
main p {
padding: 0.25em;
}
}
@ -45,13 +54,14 @@ a:hover {
@media screen and (min-width: 1024px) {
main .container {
max-width: 35em;
max-width: 40em;
}
}
.codehilite {
padding: 0 .5em;
overflow: auto;
border: 1px solid #ccc;
}
.main-image-container {

View file

@ -2,7 +2,8 @@ html {
height: -webkit-fill-available;
height: -moz-available;
font-size: 20px;
font-family: -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Open Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
/* font-family: Lora, "Palatino Linotype", "Book Antiqua", "New York", "DejaVu serif", serif; */
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-weight: 400;
text-rendering: optimizeLegibility;
}
@ -95,18 +96,7 @@ main {
display: flex;
flex-direction: column;
align-items: center;
}
@media screen and (max-width: 767px) {
main {
padding: 0 0.75em;
}
}
@media screen and (min-width: 768px) {
main {
padding: 0 2em;
}
padding: 0 0.25em;
}
h1 {
@ -119,6 +109,10 @@ h2 {
line-height: 1.1em;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Open Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
footer {
width: 100%;
font-size: .65em;
@ -129,3 +123,7 @@ footer {
text-align: center;
box-shadow: 1px -2px 2px 0 #bbb;
}
.index {
font-family: -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Open Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,36 @@
/* lora-regular - latin */
@font-face {
font-family: 'Lora';
font-style: normal;
font-weight: 400;
src: url('/fonts/Lora-Regular.eot'); /* IE9 Compat Modes */
src: local('Lora'), local('Lora-Regular'),
url('/fonts/Lora-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/Lora-Regular.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/Lora-Regular.woff') format('woff'), /* Modern Browsers */
url('/fonts/Lora-Regular.ttf') format('truetype'); /* Safari, Android, iOS */
}
/* lora-700 - latin */
@font-face {
font-family: 'Lora';
font-style: normal;
font-weight: 700;
src: url('/fonts/Lora-Bold.eot'); /* IE9 Compat Modes */
src: local('Lora Bold'), local('Lora-Bold'),
url('/fonts/Lora-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/Lora-Bold.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/Lora-Bold.woff') format('woff'), /* Modern Browsers */
url('/fonts/Lora-Bold.ttf') format('truetype'); /* Safari, Android, iOS */
}
@font-face {
font-family: 'Lora';
font-style: italic;
font-weight: 400;
src: url('/fonts/Lora-Italic.eot'); /* IE9 Compat Modes */
src: local('Lora Italic'), local('Lora-Italic'),
url('/fonts/Lora-Italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/Lora-Italic.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/Lora-Italic.woff') format('woff'), /* Modern Browsers */
url('/fonts/Lora-Italic.ttf') format('truetype'); /* Safari, Android, iOS */
}

View file

@ -21,6 +21,7 @@
<!-- RSS feed -->
<link rel="alternate" type="application/rss+xml" title="{{ config.title }}" href="/rss" />
<!-- Fonts & Styles -->
<link rel="stylesheet" href="/fonts/lora.css">
<link rel="stylesheet" href="/fonts/poppins.css">
<link rel="stylesheet" href="/fonts/fira-sans.css">
<link rel="stylesheet" href="/css/common.css">

35
pyproject.toml Normal file
View file

@ -0,0 +1,35 @@
[project]
name = "madblog"
description = "A general-purpose framework for automation"
dynamic = ["version", "dependencies", "optional-dependencies", "entry-points", "license"]
authors = [
{name = "Fabio Manganiello", email = "fabio@manganiello.tech"},
]
classifiers=[
"Topic :: Utilities",
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
]
readme = "README.md"
requires-python = '>= 3.8'
keywords = ["blog", "markdown"]
[tool.setuptools.dynamic]
version = {attr = "madblog.__version__"}
dependencies = {file = "requirements.txt"}
[project.scripts]
madblog = 'madblog:__main__'
[tool.bumpversion]
current_version = "0.2.35"
commit = true
tag = true
[[tool.bumpversion.files]]
filename = "madblog/__init__.py"
[[tool.bumpversion.files]]
filename = "setup.py"

View file

@ -1,7 +0,0 @@
[bumpversion]
current_version = 0.2.19
commit = True
tag = True
[metadata]
description-file = README.md

View file

@ -1,42 +1,41 @@
#!/usr/bin/env python
import os
from setuptools import setup, find_packages
def readfile(file):
with open(file, 'r') as f:
with open(file, "r") as f:
return f.read()
setup(
name='madblog',
version='0.2.19',
author='Fabio Manganiello',
author_email='info@fabiomanganiello.com',
description='A minimal platform for Markdown-based blogs',
license='MIT',
python_requires='>= 3.8',
keywords='blog markdown',
url='https://git.platypush.tech/blacklight/madblog',
packages=find_packages(include=['madblog']),
name="madblog",
version="0.2.35",
author="Fabio Manganiello",
author_email="info@fabiomanganiello.com",
description="A minimal platform for Markdown-based blogs",
license="MIT",
python_requires=">= 3.8",
keywords="blog markdown",
url="https://git.platypush.tech/blacklight/madblog",
packages=find_packages(include=["madblog"]),
include_package_data=True,
entry_points={
'console_scripts': [
'madblog=madblog.cli:run',
"console_scripts": [
"madblog=madblog.cli:run",
],
},
long_description=readfile('README.md'),
long_description_content_type='text/markdown',
long_description=readfile("README.md"),
long_description_content_type="text/markdown",
classifiers=[
"Topic :: Utilities",
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
],
install_requires=[
'flask',
'markdown',
'pygments',
'pyyaml',
"flask",
"markdown",
"pygments",
"pyyaml",
],
)