Compare commits
No commits in common. "main" and "v0.2.12" have entirely different histories.
35 changed files with 201 additions and 418 deletions
CHANGELOG.mdREADME.md
madblog
__init__.pyapp.pyconfig.pylatex.pyroutes.py
pyproject.tomlsetup.cfgsetup.pystatic
css
fonts
Lora-Bold.eotLora-Bold.ttfLora-Bold.woffLora-Bold.woff2Lora-Italic.eotLora-Italic.ttfLora-Italic.woffLora-Italic.woff2Lora-Regular.eotLora-Regular.ttfLora-Regular.woffLora-Regular.woff2lora.css
img
templates
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -1,28 +1,5 @@
|
|||
# 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
|
||||
full content of the articles in the RSS feed.
|
||||
|
||||
## 0.2.16
|
||||
|
||||
- Removed `alt` attribute from LaTeX rendered `<img>` tags. It may generate
|
||||
non-standard Unicode characters that break the RSS feed.
|
||||
|
||||
## 0.2.14
|
||||
|
||||
- Better support for PWA tags and added a default config-generated `/manifest.json`.
|
||||
|
||||
## 0.2.3
|
||||
|
||||
- Fix for broken RSS feed URLs when a blog has no pages.
|
||||
|
|
|
@ -72,8 +72,6 @@ logo: /path/or/url/here
|
|||
language: en-US
|
||||
# Show/hide the header (default: true)
|
||||
header: true
|
||||
# Enable/disable the short RSS feed (default: false)
|
||||
short_feed: false
|
||||
|
||||
categories:
|
||||
- category1
|
||||
|
@ -135,9 +133,4 @@ $$
|
|||
RSS feeds for the blog are provided under the `/rss` URL.
|
||||
|
||||
By default, the whole HTML-rendered content of an article is returned under `rss.channel.item.description`.
|
||||
|
||||
If you only want to include the short description of an article in the feed, use `/rss?short` instead.
|
||||
|
||||
If you want the short feed (i.e. without the fully rendered article as a
|
||||
description) to be always returned, then you can specify `short_feed=true` in
|
||||
your configuration.
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "0.2.35"
|
||||
__version__ = '0.2.12'
|
||||
|
|
106
madblog/app.py
106
madblog/app.py
|
@ -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,77 +27,73 @@ 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:
|
||||
for line in f.readlines():
|
||||
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.datetime.fromisoformat(
|
||||
m.group(2)
|
||||
).date()
|
||||
if m.group(1) == 'published':
|
||||
metadata[m.group(1)] = datetime.date.fromisoformat(m.group(2))
|
||||
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
|
||||
|
||||
|
@ -106,42 +102,37 @@ 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
|
||||
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
|
||||
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")
|
||||
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()]
|
||||
),
|
||||
content=markdown(f.read(), extensions=['fenced_code', 'codehilite', MarkdownLatex()]),
|
||||
skip_header=skip_header,
|
||||
skip_html_head=skip_html_head,
|
||||
)
|
||||
|
@ -154,25 +145,26 @@ 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 ""
|
||||
if with_content else ''
|
||||
),
|
||||
**self.get_page_metadata(
|
||||
os.path.join(root[len(pages_dir)+1:], f)
|
||||
),
|
||||
**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)
|
||||
|
|
|
@ -1,61 +1,57 @@
|
|||
import os
|
||||
from typing import List
|
||||
import yaml
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
title = "Blog"
|
||||
description = ""
|
||||
link = "/"
|
||||
home_link = "/"
|
||||
language = "en-US"
|
||||
logo = "/img/icon.png"
|
||||
title = 'Blog'
|
||||
description = ''
|
||||
link = '/'
|
||||
home_link = '/'
|
||||
language = 'en-US'
|
||||
logo = '/img/icon.png'
|
||||
header = True
|
||||
content_dir = "."
|
||||
categories: List[str] = field(default_factory=list)
|
||||
short_feed = False
|
||||
content_dir = '.'
|
||||
categories = None
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
templates_dir = os.path.join(basedir, "templates")
|
||||
static_dir = os.path.join(basedir, "static")
|
||||
default_css_dir = os.path.join(static_dir, "css")
|
||||
default_js_dir = os.path.join(static_dir, "js")
|
||||
default_fonts_dir = os.path.join(static_dir, "fonts")
|
||||
default_img_dir = os.path.join(static_dir, "img")
|
||||
templates_dir = os.path.join(basedir, 'templates')
|
||||
static_dir = os.path.join(basedir, 'static')
|
||||
default_css_dir = os.path.join(static_dir, 'css')
|
||||
default_js_dir = os.path.join(static_dir, 'js')
|
||||
default_fonts_dir = os.path.join(static_dir, 'fonts')
|
||||
default_img_dir = os.path.join(static_dir, 'img')
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
|
||||
def init_config(content_dir=".", config_file="config.yaml"):
|
||||
def init_config(content_dir='.', config_file='config.yaml'):
|
||||
cfg = {}
|
||||
config.content_dir = content_dir
|
||||
|
||||
if os.path.isfile(config_file):
|
||||
with open(config_file, "r") as f:
|
||||
with open(config_file, 'r') as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
if cfg.get("title"):
|
||||
config.title = cfg["title"]
|
||||
if cfg.get("description"):
|
||||
config.description = cfg["description"]
|
||||
if cfg.get("link"):
|
||||
config.link = cfg["link"]
|
||||
if cfg.get("home_link"):
|
||||
config.home_link = cfg["home_link"]
|
||||
if cfg.get("logo") is not None:
|
||||
config.logo = cfg["logo"]
|
||||
if cfg.get("language"):
|
||||
config.language = cfg["language"]
|
||||
if cfg.get("header") is False:
|
||||
if cfg.get('title'):
|
||||
config.title = cfg['title']
|
||||
if cfg.get('description'):
|
||||
config.description = cfg['description']
|
||||
if cfg.get('link'):
|
||||
config.link = cfg['link']
|
||||
if cfg.get('home_link'):
|
||||
config.home_link = cfg['home_link']
|
||||
if cfg.get('logo') is not None:
|
||||
config.logo = cfg['logo']
|
||||
if cfg.get('language'):
|
||||
config.language = cfg['language']
|
||||
if cfg.get('header') is False:
|
||||
config.header = False
|
||||
if cfg.get("short_feed"):
|
||||
config.short_feed = True
|
||||
|
||||
config.categories = cfg.get("categories", [])
|
||||
config.categories = cfg.get('categories', [])
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -31,11 +31,11 @@ def call(*args, **kwargs):
|
|||
|
||||
|
||||
# Defines our basic inline image
|
||||
img_expr = '<img class="latex inline math-%s" id="%s" src="data:image/png;base64,%s">'
|
||||
img_expr = '<img class="latex inline math-%s" alt="%s" id="%s" src="data:image/png;base64,%s">'
|
||||
|
||||
# Defines multiline expression image
|
||||
multiline_img_expr = """<div class="multiline-wrapper">
|
||||
<img class="latex multiline math-%s" id="%s" src="data:image/png;base64,%s"></div>"""
|
||||
multiline_img_expr = '''<div class="multiline-wrapper">
|
||||
<img class="latex multiline math-%s" alt="%s" id="%s" src="data:image/png;base64,%s"></div>'''
|
||||
|
||||
# Base CSS template
|
||||
img_css = """<style scoped>
|
||||
|
@ -55,8 +55,8 @@ img.latex.inline {
|
|||
</style>"""
|
||||
|
||||
# Cache and temp file paths
|
||||
tmpdir = tempfile.gettempdir() + "/markdown-latex"
|
||||
cache_file = tmpdir + "/latex.cache"
|
||||
tmpdir = tempfile.gettempdir() + '/markdown-latex'
|
||||
cache_file = tmpdir + '/latex.cache'
|
||||
|
||||
|
||||
class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
||||
|
@ -75,20 +75,16 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
"""
|
||||
|
||||
# Math TeX extraction regex
|
||||
math_extract_regex = re.compile(
|
||||
r"(.+?)((\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n))(.*)", re.MULTILINE | re.DOTALL
|
||||
)
|
||||
math_extract_regex = re.compile(r'(.+?)((\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n))(.*)', re.MULTILINE | re.DOTALL)
|
||||
|
||||
# Math TeX matching regex
|
||||
math_match_regex = re.compile(
|
||||
r"\s*(\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n)\s*", re.MULTILINE | re.DOTALL
|
||||
)
|
||||
math_match_regex = re.compile(r'\s*(\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n)\s*', re.MULTILINE | re.DOTALL)
|
||||
|
||||
def __init__(self, *_, **__):
|
||||
if not os.path.isdir(tmpdir):
|
||||
os.makedirs(tmpdir)
|
||||
try:
|
||||
with open(cache_file, "r") as f:
|
||||
with open(cache_file, 'r') as f:
|
||||
self.cached = json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
self.cached = {}
|
||||
|
@ -98,8 +94,7 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
("dvipng", "args"): "-q -T tight -bg Transparent -z 9 -D 200",
|
||||
("delimiters", "text"): "%",
|
||||
("delimiters", "math"): "$",
|
||||
("delimiters", "preamble"): "%%",
|
||||
}
|
||||
("delimiters", "preamble"): "%%"}
|
||||
|
||||
def _latex_to_base64(self, tex):
|
||||
"""Generates a base64 representation of TeX string"""
|
||||
|
@ -109,24 +104,18 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
with os.fdopen(tmp_file_fd, "w") as tmp_file:
|
||||
tmp_file.write(self.tex_preamble)
|
||||
tmp_file.write(tex)
|
||||
tmp_file.write("\n\\end{document}")
|
||||
tmp_file.write('\n\\end{document}')
|
||||
|
||||
# compile LaTeX document. A DVI file is created
|
||||
status = call(
|
||||
(
|
||||
"latex -halt-on-error -output-directory={:s} {:s}".format(tmpdir, path)
|
||||
).split(),
|
||||
stdout=PIPE,
|
||||
timeout=10,
|
||||
)
|
||||
status = call(('latex -halt-on-error -output-directory={:s} {:s}'
|
||||
.format(tmpdir, path)).split(),
|
||||
stdout=PIPE, timeout=10)
|
||||
|
||||
# clean up if the above failed
|
||||
if status:
|
||||
self._cleanup(path, err=True)
|
||||
raise Exception(
|
||||
"Couldn't compile LaTeX document."
|
||||
+ "Please read '%s.log' for more detail." % path
|
||||
)
|
||||
raise Exception("Couldn't compile LaTeX document." +
|
||||
"Please read '%s.log' for more detail." % path)
|
||||
|
||||
# Run dvipng on the generated DVI file. Use tight bounding box.
|
||||
# Magnification is set to 1200
|
||||
|
@ -140,10 +129,8 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
# clean up if we couldn't make the above work
|
||||
if status:
|
||||
self._cleanup(path, err=True)
|
||||
raise Exception(
|
||||
"Couldn't convert LaTeX to image."
|
||||
+ "Please read '%s.log' for more detail." % path
|
||||
)
|
||||
raise Exception("Couldn't convert LaTeX to image." +
|
||||
"Please read '%s.log' for more detail." % path)
|
||||
|
||||
# Read the png and encode the data
|
||||
try:
|
||||
|
@ -170,7 +157,7 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
def run(self, lines):
|
||||
"""Parses the actual page"""
|
||||
# Checks for the LaTeX header
|
||||
use_latex = any(line == "[//]: # (latex: 1)" for line in lines)
|
||||
use_latex = any(line == '[//]: # (latex: 1)' for line in lines)
|
||||
if not use_latex:
|
||||
return lines
|
||||
|
||||
|
@ -178,9 +165,7 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
page = "\n".join(lines)
|
||||
|
||||
# Adds a preamble mode
|
||||
self.tex_preamble += (
|
||||
self.config[("general", "preamble")] + "\n\\begin{document}\n"
|
||||
)
|
||||
self.tex_preamble += self.config[("general", "preamble")] + "\n\\begin{document}\n"
|
||||
|
||||
# Figure out our text strings and math-mode strings
|
||||
tex_expr = self.math_extract_regex.findall(page)
|
||||
|
@ -191,7 +176,7 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
|
||||
# Parse the expressions
|
||||
new_cache = {}
|
||||
new_page = ""
|
||||
new_page = ''
|
||||
n_multiline_expressions = 0
|
||||
|
||||
while page:
|
||||
|
@ -215,25 +200,21 @@ class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
new_cache[tex_hash] = data
|
||||
|
||||
if is_multiline and n_multiline_expressions > 0:
|
||||
new_page += "</p>"
|
||||
new_page += (multiline_img_expr if is_multiline else img_expr) % (
|
||||
"true",
|
||||
tex_hash,
|
||||
data,
|
||||
)
|
||||
new_page += '</p>'
|
||||
new_page += (multiline_img_expr if is_multiline else img_expr) % ('true', expr, tex_hash, data)
|
||||
|
||||
if is_multiline:
|
||||
new_page += "<p>"
|
||||
new_page += '<p>'
|
||||
n_multiline_expressions += 1
|
||||
|
||||
page = m.group(5)
|
||||
|
||||
if n_multiline_expressions > 0:
|
||||
new_page += "</p>"
|
||||
new_page += '</p>'
|
||||
|
||||
# Cache our data
|
||||
self.cached.update(new_cache)
|
||||
with open(cache_file, "w") as f:
|
||||
with open(cache_file, 'w') as f:
|
||||
json.dump(self.cached, f)
|
||||
|
||||
# Make sure to re-split the lines
|
||||
|
@ -250,6 +231,6 @@ class MarkdownLatex(markdown.Extension):
|
|||
def extendMarkdown(self, md):
|
||||
md.preprocessors.register(
|
||||
LaTeXPreprocessor(self),
|
||||
"latex",
|
||||
'latex',
|
||||
1,
|
||||
)
|
||||
|
|
|
@ -1,131 +1,84 @@
|
|||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from flask import (
|
||||
jsonify,
|
||||
request,
|
||||
Response,
|
||||
send_from_directory as send_from_directory_,
|
||||
render_template,
|
||||
)
|
||||
from flask import request, Response, send_from_directory as send_from_directory_, render_template
|
||||
|
||||
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
|
||||
):
|
||||
def send_from_directory(path: str, file: str, alternative_path: Optional[str] = None, *args, **kwargs):
|
||||
if not os.path.exists(os.path.join(path, file)) and alternative_path:
|
||||
path = alternative_path
|
||||
return send_from_directory_(path, file, *args, **kwargs)
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
@app.route('/', methods=['GET'])
|
||||
def home_route():
|
||||
return render_template(
|
||||
"index.html",
|
||||
'index.html',
|
||||
pages=app.get_pages(sorter=PagesSortByTimeGroupedByFolder),
|
||||
config=config,
|
||||
config=config
|
||||
)
|
||||
|
||||
|
||||
@app.route("/img/<img>", methods=["GET"])
|
||||
@app.route('/img/<img>', methods=['GET'])
|
||||
def img_route(img: str):
|
||||
return send_from_directory(app.img_dir, img, config.default_img_dir)
|
||||
|
||||
|
||||
@app.route("/favicon.ico", methods=["GET"])
|
||||
@app.route('/favicon.ico', methods=['GET'])
|
||||
def favicon_route():
|
||||
return img_route("favicon.ico")
|
||||
return img_route('favicon.ico')
|
||||
|
||||
|
||||
@app.route("/js/<file>", methods=["GET"])
|
||||
@app.route('/js/<file>', methods=['GET'])
|
||||
def js_route(file: str):
|
||||
return send_from_directory(app.js_dir, file, config.default_js_dir)
|
||||
|
||||
|
||||
@app.route("/pwabuilder-sw.js", methods=["GET"])
|
||||
@app.route('/pwabuilder-sw.js', methods=['GET'])
|
||||
def pwa_builder_route():
|
||||
return send_from_directory(app.js_dir, "pwabuilder-sw.js", config.default_js_dir)
|
||||
return send_from_directory(app.js_dir, 'pwabuilder-sw.js', config.default_js_dir)
|
||||
|
||||
|
||||
@app.route("/pwabuilder-sw-register.js", methods=["GET"])
|
||||
@app.route('/pwabuilder-sw-register.js', methods=['GET'])
|
||||
def pwa_builder_register_route():
|
||||
return send_from_directory(
|
||||
app.js_dir, "pwabuilder-sw-register.js", config.default_js_dir
|
||||
)
|
||||
return send_from_directory(app.js_dir, 'pwabuilder-sw-register.js', config.default_js_dir)
|
||||
|
||||
|
||||
@app.route("/css/<style>", methods=["GET"])
|
||||
@app.route('/css/<style>', methods=['GET'])
|
||||
def css_route(style: str):
|
||||
return send_from_directory(app.css_dir, style, config.default_css_dir)
|
||||
|
||||
|
||||
@app.route("/fonts/<file>", methods=["GET"])
|
||||
@app.route('/fonts/<file>', methods=['GET'])
|
||||
def fonts_route(file: str):
|
||||
return send_from_directory(app.fonts_dir, file, config.default_fonts_dir)
|
||||
|
||||
|
||||
@app.route("/manifest.json", methods=["GET"])
|
||||
@app.route('/manifest.json', methods=['GET'])
|
||||
def manifest_route():
|
||||
# If there is a manifest.json in the content directory, use it
|
||||
manifest_file = os.path.join(config.content_dir, "manifest.json")
|
||||
if os.path.isfile(manifest_file):
|
||||
return send_from_directory(config.content_dir, "manifest.json")
|
||||
|
||||
# Otherwise, generate a default manifest.json
|
||||
return jsonify(
|
||||
{
|
||||
"name": config.title,
|
||||
"short_name": config.title,
|
||||
"icons": [
|
||||
{"src": "/img/icon-48.png", "sizes": "48x48", "type": "image/png"},
|
||||
{"src": "/img/icon-72.png", "sizes": "72x72", "type": "image/png"},
|
||||
{"src": "/img/icon-96.png", "sizes": "96x96", "type": "image/png"},
|
||||
{"src": "/img/icon-144.png", "sizes": "144x144", "type": "image/png"},
|
||||
{"src": "/img/icon-168.png", "sizes": "168x168", "type": "image/png"},
|
||||
{"src": "/img/icon-192.png", "sizes": "192x192", "type": "image/png"},
|
||||
{"src": "/img/icon-256.png", "sizes": "256x256", "type": "image/png"},
|
||||
{"src": "/img/icon-512.png", "sizes": "512x512", "type": "image/png"},
|
||||
],
|
||||
"gcm_sender_id": "",
|
||||
"gcm_user_visible_only": True,
|
||||
"start_url": "/",
|
||||
"permissions": ["gcm"],
|
||||
"scope": "",
|
||||
"orientation": "portrait",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff",
|
||||
}
|
||||
)
|
||||
return send_from_directory(config.content_dir, 'manifest.json')
|
||||
|
||||
|
||||
@app.route("/article/<path:path>/<article>", methods=["GET"])
|
||||
@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):
|
||||
return article_with_path_route("", article)
|
||||
return article_with_path_route('', article)
|
||||
|
||||
|
||||
@app.route("/rss", methods=["GET"])
|
||||
@app.route('/rss', methods=['GET'])
|
||||
def rss_route():
|
||||
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,
|
||||
)
|
||||
pages = app.get_pages(with_content=True, skip_header=True, skip_html_head=True)
|
||||
short_description = 'short' in request.args
|
||||
|
||||
return Response(
|
||||
"""<?xml version="1.0" encoding="UTF-8" ?>
|
||||
return Response('''<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
|
||||
<channel>
|
||||
<title>{title}</title>
|
||||
|
@ -142,56 +95,36 @@ def rss_route():
|
|||
|
||||
{items}
|
||||
</channel>
|
||||
</rss>""".format(
|
||||
</rss>'''.format(
|
||||
title=config.title,
|
||||
description=config.description,
|
||||
link=config.link,
|
||||
categories=",".join(config.categories),
|
||||
categories=','.join(config.categories),
|
||||
language=config.language,
|
||||
last_pub_date=(
|
||||
pages[0][1]["published"].strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
if pages
|
||||
else ""
|
||||
pages[0][1]['published'].strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||
if pages else ''
|
||||
),
|
||||
items="\n\n".join(
|
||||
[
|
||||
(
|
||||
"""
|
||||
items='\n\n'.join([
|
||||
'''
|
||||
<item>
|
||||
<title>{title}</title>
|
||||
<link>{base_link}{link}</link>
|
||||
<pubDate>{published}</pubDate>
|
||||
<description><![CDATA[{content}]]></description>
|
||||
<media:content medium="image" url="{image}" width="200" height="150" />
|
||||
<media:content medium="image" url="{base_link}{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=(
|
||||
urljoin(config.link, page["image"])
|
||||
if page.get("image")
|
||||
and not re.search(r"^https?://", page["image"])
|
||||
else page.get("image", "")
|
||||
),
|
||||
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', ''),
|
||||
)
|
||||
for _, page in pages
|
||||
]
|
||||
),
|
||||
),
|
||||
mimetype="application/xml",
|
||||
)
|
||||
]),
|
||||
), mimetype='application/rss+xml')
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -2,31 +2,22 @@ main .content {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5em;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
main .content p,
|
||||
main .content ul,
|
||||
main .content ol {
|
||||
font-family: Lora, "Palatino Linotype", "Book Antiqua", "New York", "DejaVu serif", serif;
|
||||
main .content ul {
|
||||
text-align: justify;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
main .content code, .codehilite {
|
||||
font-size: 0.9em;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
main {
|
||||
font-size: 0.95em;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
main p {
|
||||
padding: 0.25em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,14 +45,13 @@ a:hover {
|
|||
|
||||
@media screen and (min-width: 1024px) {
|
||||
main .container {
|
||||
max-width: 40em;
|
||||
max-width: 35em;
|
||||
}
|
||||
}
|
||||
|
||||
.codehilite {
|
||||
padding: 0 .5em;
|
||||
overflow: auto;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.main-image-container {
|
||||
|
|
|
@ -2,8 +2,7 @@ html {
|
|||
height: -webkit-fill-available;
|
||||
height: -moz-available;
|
||||
font-size: 20px;
|
||||
/* 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-family: -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Open Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
@ -96,7 +95,18 @@ main {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
main {
|
||||
padding: 0 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
main {
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -109,10 +119,6 @@ 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;
|
||||
|
@ -123,7 +129,3 @@ 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.
|
@ -1,36 +0,0 @@
|
|||
/* 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 */
|
||||
}
|
||||
|
Binary file not shown.
Before ![]() (image error) Size: 1.3 KiB |
Binary file not shown.
Before ![]() (image error) Size: 1.4 KiB |
Binary file not shown.
Before ![]() (image error) Size: 1.7 KiB |
Binary file not shown.
Before ![]() (image error) Size: 2.1 KiB |
Binary file not shown.
Before ![]() (image error) Size: 774 B |
Binary file not shown.
Before ![]() (image error) Size: 5.8 KiB |
Binary file not shown.
Before ![]() (image error) Size: 840 B |
Binary file not shown.
Before ![]() (image error) Size: 1 KiB |
|
@ -1 +0,0 @@
|
|||
icon-512.png
|
Before ![]() (image error) Size: 12 B After ![]() (image error) Size: 5.8 KiB ![]() ![]() |
BIN
madblog/static/img/icon.png
Normal file
BIN
madblog/static/img/icon.png
Normal file
Binary file not shown.
Before ![]() (image error) Size: 12 B After ![]() (image error) Size: 5.8 KiB ![]() ![]() |
|
@ -2,30 +2,12 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<!-- PWA & Viewport -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="description" content="{{ config.description }}">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<!-- Android PWA -->
|
||||
<meta name="theme-color" content="white">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="{{ config.title }}">
|
||||
<!-- iOS PWA -->
|
||||
<meta name="apple-mobile-web-app-title" content="{{ config.title }}">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<!-- Orientation -->
|
||||
<meta name="screen-orientation" content="portrait">
|
||||
<!-- 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">
|
||||
<!-- PWA builder -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<script type="module" src="/pwabuilder-sw-register.js"></script>
|
||||
|
||||
{% if styles %}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
[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"
|
7
setup.cfg
Normal file
7
setup.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[bumpversion]
|
||||
current_version = 0.2.12
|
||||
commit = True
|
||||
tag = True
|
||||
|
||||
[metadata]
|
||||
description-file = README.md
|
39
setup.py
39
setup.py
|
@ -1,41 +1,42 @@
|
|||
#!/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.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"]),
|
||||
name='madblog',
|
||||
version='0.2.12',
|
||||
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',
|
||||
],
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue