First commit
This commit is contained in:
commit
a91b564305
30 changed files with 1233 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
__pycache__
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info
|
22
LICENSE.txt
Normal file
22
LICENSE.txt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021, 2022 Fabio Manganiello
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
recursive-include madblog/static/css *
|
||||||
|
recursive-include madblog/static/fonts *
|
||||||
|
recursive-include madblog/static/img *
|
||||||
|
recursive-include madblog/templates *
|
103
README.md
Normal file
103
README.md
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# mdblog
|
||||||
|
|
||||||
|
This project provides a minimal blogging platform based on Markdown files.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ python setup.py install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# The application will listen on port 8000 and it will
|
||||||
|
# serve the current folder
|
||||||
|
$ madness
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: madblog [-h] [--host HOST] [--port PORT] [--debug] [path]
|
||||||
|
|
||||||
|
Serve a Markdown folder as a web blog.
|
||||||
|
|
||||||
|
The folder should have the following structure:
|
||||||
|
|
||||||
|
.
|
||||||
|
-> markdown
|
||||||
|
-> article-1.md
|
||||||
|
-> article-2.md
|
||||||
|
-> ...
|
||||||
|
-> img [recommended]
|
||||||
|
-> favicon.ico
|
||||||
|
-> icon.png
|
||||||
|
-> image-1.png
|
||||||
|
-> image-2.png
|
||||||
|
-> ...
|
||||||
|
-> css [optional]
|
||||||
|
-> custom-1.css
|
||||||
|
-> custom-2.css
|
||||||
|
-> ...
|
||||||
|
-> fonts [optional]
|
||||||
|
-> custom-1.ttf
|
||||||
|
-> custom-1.css
|
||||||
|
-> ...
|
||||||
|
-> templates [optional]
|
||||||
|
-> index.html [for a custom index template]
|
||||||
|
-> article.html [for a custom article template]
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
path Base path for the blog
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--host HOST Bind host/address
|
||||||
|
--port PORT Bind port (default: 8000)
|
||||||
|
--debug Enable debug mode (default: False)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Markdown files
|
||||||
|
|
||||||
|
Articles are Markdown files stored under `pages`. For an article to be correctly rendered,
|
||||||
|
you need to start the Markdown file with the following metadata header:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[//]: # (title: Title of the article)
|
||||||
|
[//]: # (description: Short description of the content)
|
||||||
|
[//]: # (image: /img/some-header-image.png)
|
||||||
|
[//]: # (author: Author Name <email@author.me>)
|
||||||
|
[//]: # (published: 2022-01-01)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
Images are stored under `img`. You can reference them in your articles through the following syntax:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
![image description](/img/image.png)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also drop your `favicon.ico` under this folder.
|
||||||
|
|
||||||
|
## LaTeX support
|
||||||
|
|
||||||
|
LaTeX support is built-in as long as you have the `latex` executable installed on your server.
|
||||||
|
|
||||||
|
Syntax for inline LaTeX:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
And we can therefore prove that \( c^2 = a^2 + b^2 \)
|
||||||
|
```
|
||||||
|
|
||||||
|
Syntax for LaTeX expression on a new line:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
$$
|
||||||
|
c^2 = a^2 + b^2
|
||||||
|
$$
|
||||||
|
```
|
||||||
|
|
||||||
|
## RSS syndacation
|
||||||
|
|
||||||
|
RSS feeds for the blog are provided under the `/rss` URL.
|
||||||
|
|
0
madblog/__init__.py
Normal file
0
madblog/__init__.py
Normal file
4
madblog/__main__.py
Normal file
4
madblog/__main__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from .cli import run
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
103
madblog/app.py
Normal file
103
madblog/app.py
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from glob import glob
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from flask import Flask, abort, render_template
|
||||||
|
from markdown import markdown
|
||||||
|
|
||||||
|
from .config import config
|
||||||
|
from .latex import MarkdownLatex
|
||||||
|
|
||||||
|
|
||||||
|
class BlogApp(Flask):
|
||||||
|
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.img_dir = config.default_img_dir
|
||||||
|
self.css_dir = config.default_css_dir
|
||||||
|
self.fonts_dir = config.default_fonts_dir
|
||||||
|
|
||||||
|
if not os.path.isdir(self.pages_dir):
|
||||||
|
raise FileNotFoundError(self.pages_dir)
|
||||||
|
|
||||||
|
img_dir = os.path.join(config.content_dir, 'img')
|
||||||
|
if os.path.isdir(img_dir):
|
||||||
|
self.img_dir = img_dir
|
||||||
|
|
||||||
|
css_dir = os.path.join(config.content_dir, 'css')
|
||||||
|
if os.path.isdir(css_dir):
|
||||||
|
self.css_dir = css_dir
|
||||||
|
|
||||||
|
fonts_dir = os.path.join(config.content_dir, 'fonts')
|
||||||
|
if os.path.isdir(fonts_dir):
|
||||||
|
self.fonts_dir = fonts_dir
|
||||||
|
|
||||||
|
templates_dir = os.path.join(config.content_dir, 'templates')
|
||||||
|
if os.path.isdir(templates_dir):
|
||||||
|
self.template_folder = templates_dir
|
||||||
|
|
||||||
|
def get_page_metadata(self, page: str) -> dict:
|
||||||
|
if not page.endswith('.md'):
|
||||||
|
page = page + '.md'
|
||||||
|
|
||||||
|
if not os.path.isfile(os.path.join(self.pages_dir, page)):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
with open(os.path.join(self.pages_dir, page), 'r') as f:
|
||||||
|
metadata['uri'] = '/article/' + page[:-3]
|
||||||
|
|
||||||
|
for line in f.readlines():
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
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))
|
||||||
|
else:
|
||||||
|
metadata[m.group(1)] = m.group(2)
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def get_page(self, page: str, title: Optional[str] = None, skip_header: bool = False):
|
||||||
|
if not page.endswith('.md'):
|
||||||
|
page = page + '.md'
|
||||||
|
|
||||||
|
metadata = self.get_page_metadata(page)
|
||||||
|
with open(os.path.join(self.pages_dir, page), 'r') as f:
|
||||||
|
return render_template(
|
||||||
|
'article.html',
|
||||||
|
config=config,
|
||||||
|
title=title if title else metadata.get('title', config.title),
|
||||||
|
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') else None),
|
||||||
|
content=markdown(f.read(), extensions=['fenced_code', 'codehilite', MarkdownLatex()]),
|
||||||
|
skip_header=skip_header
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_pages(self, with_content: bool = False, skip_header: bool = False) -> list:
|
||||||
|
return sorted([
|
||||||
|
{
|
||||||
|
'path': path,
|
||||||
|
'content': self.get_page(path, 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'), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
app = BlogApp(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
from .routes import *
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
55
madblog/cli.py
Normal file
55
madblog/cli.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def get_args():
|
||||||
|
parser = argparse.ArgumentParser(description='''Serve a Markdown folder as a web blog.
|
||||||
|
|
||||||
|
The folder should have the following structure:
|
||||||
|
|
||||||
|
.
|
||||||
|
-> config.yaml [recommended]
|
||||||
|
-> markdown
|
||||||
|
-> article-1.md
|
||||||
|
-> article-2.md
|
||||||
|
-> ...
|
||||||
|
-> img [recommended]
|
||||||
|
-> favicon.ico
|
||||||
|
-> icon.png
|
||||||
|
-> image-1.png
|
||||||
|
-> image-2.png
|
||||||
|
-> ...
|
||||||
|
-> css [optional]
|
||||||
|
-> custom-1.css
|
||||||
|
-> custom-2.css
|
||||||
|
-> ...
|
||||||
|
-> fonts [optional]
|
||||||
|
-> custom-1.ttf
|
||||||
|
-> custom-1.css
|
||||||
|
-> ...
|
||||||
|
-> templates [optional]
|
||||||
|
-> index.html [for a custom index template]
|
||||||
|
-> article.html [for a custom article template]
|
||||||
|
|
||||||
|
''', formatter_class=argparse.RawTextHelpFormatter)
|
||||||
|
parser.add_argument('dir', nargs='?', default='.', help='Base path for the blog (default: current directory)')
|
||||||
|
parser.add_argument('--config', dest='config', default='config.yaml', required=False, help='Path to a configuration file (default: config.yaml in the blog root directory)')
|
||||||
|
parser.add_argument('--host', dest='host', required=False, default='0.0.0.0', help='Bind host/address')
|
||||||
|
parser.add_argument('--port', dest='port', required=False, type=int, default=8000, help='Bind port (default: 8000)')
|
||||||
|
parser.add_argument('--debug', dest='debug', required=False, action='store_true', default=False,
|
||||||
|
help='Enable debug mode (default: False)')
|
||||||
|
|
||||||
|
return parser.parse_known_args(sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
from .config import init_config
|
||||||
|
opts, _ = get_args()
|
||||||
|
config_file = os.path.join(opts.dir, 'config.yaml')
|
||||||
|
init_config(config_file=config_file, content_dir=opts.dir)
|
||||||
|
|
||||||
|
from .app import app
|
||||||
|
app.run(host=opts.host, port=opts.port, debug=opts.debug)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
54
madblog/config.py
Normal file
54
madblog/config.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
title = 'Blog'
|
||||||
|
description = ''
|
||||||
|
link = '/'
|
||||||
|
home_link = '/'
|
||||||
|
language = 'en-US'
|
||||||
|
logo = '/img/icon.png'
|
||||||
|
content_dir = None
|
||||||
|
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_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'):
|
||||||
|
cfg = {}
|
||||||
|
config.content_dir = content_dir
|
||||||
|
|
||||||
|
if os.path.isfile(config_file):
|
||||||
|
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'):
|
||||||
|
config.logo = cfg['logo']
|
||||||
|
if cfg.get('language'):
|
||||||
|
config.language = cfg['language']
|
||||||
|
|
||||||
|
config.categories = cfg.get('categories', [])
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
248
madblog/latex.py
Normal file
248
madblog/latex.py
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
"""
|
||||||
|
Licensed under Public Domain Mark 1.0.
|
||||||
|
See https://creativecommons.org/publicdomain/mark/1.0/
|
||||||
|
Author: Justin Bruce Van Horne <justinvh@gmail.com>
|
||||||
|
|
||||||
|
Python-Markdown LaTeX Extension
|
||||||
|
Adds support for $math mode$ and %text mode%. This plugin supports
|
||||||
|
multiline equations/text.
|
||||||
|
The actual image generation is done via LaTeX/DVI output.
|
||||||
|
It encodes data as base64 so there is no need for images directly.
|
||||||
|
All the work is done in the preprocessor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
from subprocess import call as rawcall, PIPE
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
|
||||||
|
def call(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Proxy to subprocess.call(), removes timeout argument in case of
|
||||||
|
Python2 because that was only implemented in Python3.
|
||||||
|
"""
|
||||||
|
return rawcall(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Defines our basic inline image
|
||||||
|
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" alt="%s" id="%s" src="data:image/png;base64,%s"></div>'''
|
||||||
|
|
||||||
|
# Base CSS template
|
||||||
|
img_css = """<style scoped>
|
||||||
|
.multiline-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.latex.multiline {
|
||||||
|
height: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.latex.inline {
|
||||||
|
height: .9em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>"""
|
||||||
|
|
||||||
|
# Cache and temp file paths
|
||||||
|
tmpdir = tempfile.gettempdir() + '/markdown-latex'
|
||||||
|
cache_file = tmpdir + '/latex.cache'
|
||||||
|
|
||||||
|
|
||||||
|
class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
||||||
|
# These are our cached expressions that are stored in latex.cache
|
||||||
|
cached = {}
|
||||||
|
|
||||||
|
# Basic LaTex Setup as well as our list of expressions to parse
|
||||||
|
tex_preamble = r"""\documentclass[14pt]{article}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amsthm}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
\usepackage{bm}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage[usenames,dvipsnames]{color}
|
||||||
|
\pagestyle{empty}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Math TeX extraction regex
|
||||||
|
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)
|
||||||
|
|
||||||
|
def __init__(self, *_, **__):
|
||||||
|
if not os.path.isdir(tmpdir):
|
||||||
|
os.makedirs(tmpdir)
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
self.cached = json.load(f)
|
||||||
|
except (IOError, json.JSONDecodeError):
|
||||||
|
self.cached = {}
|
||||||
|
|
||||||
|
self.config = {
|
||||||
|
("general", "preamble"): "",
|
||||||
|
("dvipng", "args"): "-q -T tight -bg Transparent -z 9 -D 200",
|
||||||
|
("delimiters", "text"): "%",
|
||||||
|
("delimiters", "math"): "$",
|
||||||
|
("delimiters", "preamble"): "%%"}
|
||||||
|
|
||||||
|
def _latex_to_base64(self, tex):
|
||||||
|
"""Generates a base64 representation of TeX string"""
|
||||||
|
|
||||||
|
# Generate the temporary file
|
||||||
|
tmp_file_fd, path = tempfile.mkstemp(dir=tmpdir)
|
||||||
|
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}')
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Run dvipng on the generated DVI file. Use tight bounding box.
|
||||||
|
# Magnification is set to 1200
|
||||||
|
dvi = "%s.dvi" % path
|
||||||
|
png = "%s.png" % path
|
||||||
|
|
||||||
|
# Extract the image
|
||||||
|
cmd = "dvipng %s %s -o %s" % (self.config[("dvipng", "args")], dvi, png)
|
||||||
|
status = call(cmd.split(), stdout=PIPE)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Read the png and encode the data
|
||||||
|
try:
|
||||||
|
with open(png, "rb") as png:
|
||||||
|
data = png.read()
|
||||||
|
return base64.b64encode(data)
|
||||||
|
finally:
|
||||||
|
self._cleanup(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cleanup(path, err=False):
|
||||||
|
# don't clean up the log if there's an error
|
||||||
|
extensions = ["", ".aux", ".dvi", ".png", ".log"]
|
||||||
|
if err:
|
||||||
|
extensions.pop()
|
||||||
|
|
||||||
|
# now do the actual cleanup, passing on non-existent files
|
||||||
|
for extension in extensions:
|
||||||
|
try:
|
||||||
|
os.remove("%s%s" % (path, extension))
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self, lines):
|
||||||
|
"""Parses the actual page"""
|
||||||
|
# Checks for the LaTeX header
|
||||||
|
use_latex = any(line == '[//]: # (latex: 1)' for line in lines)
|
||||||
|
if not use_latex:
|
||||||
|
return lines
|
||||||
|
|
||||||
|
# Re-creates the entire page so we can parse in a multiline env.
|
||||||
|
page = "\n".join(lines)
|
||||||
|
|
||||||
|
# Adds a preamble mode
|
||||||
|
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)
|
||||||
|
|
||||||
|
# No sense in doing the extra work
|
||||||
|
if not len(tex_expr):
|
||||||
|
return page.split("\n")
|
||||||
|
|
||||||
|
# Parse the expressions
|
||||||
|
new_cache = {}
|
||||||
|
new_page = ''
|
||||||
|
n_multiline_expressions = 0
|
||||||
|
|
||||||
|
while page:
|
||||||
|
m = self.math_extract_regex.match(page)
|
||||||
|
if not m:
|
||||||
|
new_page += page
|
||||||
|
break
|
||||||
|
|
||||||
|
new_page += m.group(1)
|
||||||
|
math_match = self.math_match_regex.match(m.group(2))
|
||||||
|
if not math_match:
|
||||||
|
new_page += m.group(2)
|
||||||
|
else:
|
||||||
|
expr = m.group(2)
|
||||||
|
is_multiline = math_match.group(2) is not None
|
||||||
|
tex_hash = self.hash(expr)
|
||||||
|
if tex_hash in self.cached:
|
||||||
|
data = self.cached[tex_hash]
|
||||||
|
else:
|
||||||
|
data = self._latex_to_base64(expr).decode()
|
||||||
|
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', expr, tex_hash, data)
|
||||||
|
|
||||||
|
if is_multiline:
|
||||||
|
new_page += '<p>'
|
||||||
|
n_multiline_expressions += 1
|
||||||
|
|
||||||
|
page = m.group(5)
|
||||||
|
|
||||||
|
if n_multiline_expressions > 0:
|
||||||
|
new_page += '</p>'
|
||||||
|
|
||||||
|
# Cache our data
|
||||||
|
self.cached.update(new_cache)
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
json.dump(self.cached, f)
|
||||||
|
|
||||||
|
# Make sure to re-split the lines
|
||||||
|
return new_page.split("\n")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hash(tex: str) -> str:
|
||||||
|
return hashlib.sha1(tex.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class LaTeXPostprocessor(markdown.postprocessors.Postprocessor):
|
||||||
|
"""This post processor extension just allows us to further
|
||||||
|
refine, if necessary, the document after it has been parsed."""
|
||||||
|
|
||||||
|
# noinspection PyMethodMayBeStatic
|
||||||
|
def run(self, text):
|
||||||
|
# Inline a style for default behavior
|
||||||
|
text = img_css + text
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownLatex(markdown.Extension):
|
||||||
|
"""Wrapper for LaTeXPreprocessor"""
|
||||||
|
|
||||||
|
def extendMarkdown(self, md):
|
||||||
|
# Our base LaTeX extension
|
||||||
|
md.preprocessors.add('latex',
|
||||||
|
LaTeXPreprocessor(self), ">html_block")
|
||||||
|
# Our cleanup postprocessing extension
|
||||||
|
md.postprocessors.add('latex',
|
||||||
|
LaTeXPostprocessor(self), ">amp_substitute")
|
96
madblog/routes.py
Normal file
96
madblog/routes.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from flask import Response, send_from_directory as send_from_directory_, render_template
|
||||||
|
|
||||||
|
from .app import app
|
||||||
|
from .config import config
|
||||||
|
|
||||||
|
|
||||||
|
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'])
|
||||||
|
def home_route():
|
||||||
|
return render_template('index.html', pages=app.get_pages(), config=config)
|
||||||
|
|
||||||
|
|
||||||
|
@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'])
|
||||||
|
def favicon_route():
|
||||||
|
return img_route('favicon.ico')
|
||||||
|
|
||||||
|
|
||||||
|
@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'])
|
||||||
|
def fonts_route(file: str):
|
||||||
|
return send_from_directory(app.fonts_dir, file, config.default_fonts_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/article/<article>', methods=['GET'])
|
||||||
|
def article_route(article: str):
|
||||||
|
return app.get_page(article)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/rss', methods=['GET'])
|
||||||
|
def rss_route():
|
||||||
|
pages = app.get_pages(with_content=True, skip_header=True)
|
||||||
|
|
||||||
|
return Response('''<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<channel>
|
||||||
|
<title>{title}</title>
|
||||||
|
<link>{link}</link>
|
||||||
|
<description>{description}</description>
|
||||||
|
<category>{",".join(categories)}</category>
|
||||||
|
<image>
|
||||||
|
<url>{link}/img/icon.png</url>
|
||||||
|
<title>{title}</title>
|
||||||
|
<link>{link}</link>
|
||||||
|
</image>
|
||||||
|
<pubDate>{last_pub_date}</pubDate>
|
||||||
|
<language>{language}</language>
|
||||||
|
|
||||||
|
{items}
|
||||||
|
</channel>
|
||||||
|
</rss>'''.format(
|
||||||
|
title=config.title,
|
||||||
|
description=config.description,
|
||||||
|
link=config.link,
|
||||||
|
categories=config.categories,
|
||||||
|
language=config.language,
|
||||||
|
last_pub_date=pages[0]['published'].strftime('%a, %d %b %Y %H:%M:%S GMT'),
|
||||||
|
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="{base_link}{image}" width="200" height="150" />
|
||||||
|
</item>
|
||||||
|
'''.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('content', ''),
|
||||||
|
image=page.get('image', ''),
|
||||||
|
)
|
||||||
|
for page in pages
|
||||||
|
]),
|
||||||
|
), mimetype='application/rss+xml')
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
50
madblog/static/css/blog.css
Normal file
50
madblog/static/css/blog.css
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
main .content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .content p,
|
||||||
|
main .content ul {
|
||||||
|
font-family: Avenir, Palatino, charter, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
|
text-align: justify;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .content code, .codehilite {
|
||||||
|
font-size: .85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description h3 {
|
||||||
|
font-weight: normal;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin: -.5em auto .5em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.published-date {
|
||||||
|
font-size: 0.75em;
|
||||||
|
opacity: .75;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
main .container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
main .container {
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.codehilite {
|
||||||
|
padding: 0 .5em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
74
madblog/static/css/code.css
Normal file
74
madblog/static/css/code.css
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
pre { line-height: 125%; }
|
||||||
|
td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
.codehilite .hll { background-color: #ffffcc }
|
||||||
|
.codehilite { background: #f8f8f8; }
|
||||||
|
.codehilite .c { color: #408080; font-style: italic } /* Comment */
|
||||||
|
.codehilite .err { border: 1px solid #FF0000 } /* Error */
|
||||||
|
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
|
||||||
|
.codehilite .o { color: #666666 } /* Operator */
|
||||||
|
.codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
|
||||||
|
.codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||||
|
.codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||||
|
.codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
|
||||||
|
.codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||||
|
.codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||||
|
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
|
||||||
|
.codehilite .ge { font-style: italic } /* Generic.Emph */
|
||||||
|
.codehilite .gr { color: #FF0000 } /* Generic.Error */
|
||||||
|
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||||
|
.codehilite .gi { color: #00A000 } /* Generic.Inserted */
|
||||||
|
.codehilite .go { color: #888888 } /* Generic.Output */
|
||||||
|
.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||||
|
.codehilite .gs { font-weight: bold } /* Generic.Strong */
|
||||||
|
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||||
|
.codehilite .gt { color: #0044DD } /* Generic.Traceback */
|
||||||
|
.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||||
|
.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||||
|
.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
||||||
|
.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
|
||||||
|
.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||||
|
.codehilite .kt { color: #B00040 } /* Keyword.Type */
|
||||||
|
.codehilite .m { color: #666666 } /* Literal.Number */
|
||||||
|
.codehilite .s { color: #BA2121 } /* Literal.String */
|
||||||
|
.codehilite .na { color: #7D9029 } /* Name.Attribute */
|
||||||
|
.codehilite .nb { color: #008000 } /* Name.Builtin */
|
||||||
|
.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||||
|
.codehilite .no { color: #880000 } /* Name.Constant */
|
||||||
|
.codehilite .nd { color: #AA22FF } /* Name.Decorator */
|
||||||
|
.codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||||
|
.codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||||
|
.codehilite .nf { color: #0000FF } /* Name.Function */
|
||||||
|
.codehilite .nl { color: #A0A000 } /* Name.Label */
|
||||||
|
.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||||
|
.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||||
|
.codehilite .nv { color: #19177C } /* Name.Variable */
|
||||||
|
.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||||
|
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
|
||||||
|
.codehilite .mb { color: #666666 } /* Literal.Number.Bin */
|
||||||
|
.codehilite .mf { color: #666666 } /* Literal.Number.Float */
|
||||||
|
.codehilite .mh { color: #666666 } /* Literal.Number.Hex */
|
||||||
|
.codehilite .mi { color: #666666 } /* Literal.Number.Integer */
|
||||||
|
.codehilite .mo { color: #666666 } /* Literal.Number.Oct */
|
||||||
|
.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
|
||||||
|
.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||||
|
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
|
||||||
|
.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
|
||||||
|
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||||
|
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
|
||||||
|
.codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||||
|
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||||
|
.codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||||
|
.codehilite .sx { color: #008000 } /* Literal.String.Other */
|
||||||
|
.codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||||
|
.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
|
||||||
|
.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
|
||||||
|
.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||||
|
.codehilite .fm { color: #0000FF } /* Name.Function.Magic */
|
||||||
|
.codehilite .vc { color: #19177C } /* Name.Variable.Class */
|
||||||
|
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
|
||||||
|
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
|
||||||
|
.codehilite .vm { color: #19177C } /* Name.Variable.Magic */
|
||||||
|
.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
|
107
madblog/static/css/common.css
Normal file
107
madblog/static/css/common.css
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited {
|
||||||
|
color: #555;
|
||||||
|
border-bottom: 1px dashed #999;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 3em;
|
||||||
|
padding: 0 .5em;
|
||||||
|
font-size: .9em;
|
||||||
|
box-shadow: 1px 3px 3px 0 #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a {
|
||||||
|
color: initial;
|
||||||
|
opacity: .8;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .left a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: initial !important;
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a:hover {
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .title {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
margin-left: .333em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .left,
|
||||||
|
header .right {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .right {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: right;
|
||||||
|
text-align: right;
|
||||||
|
direction: rtl;
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .icon {
|
||||||
|
background-size: 40px !important;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .left .icon {
|
||||||
|
width: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .right .icon {
|
||||||
|
background: url(/img/rss.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
header .title {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: calc(100% - 3em);
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: 1.1em;
|
||||||
|
}
|
||||||
|
|
112
madblog/static/css/home.css
Normal file
112
madblog/static/css/home.css
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
main {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articles {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 80%;
|
||||||
|
box-shadow: 0 1px 3px 1px #ccc;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article:hover {
|
||||||
|
box-shadow: 0 1px 4px 3px #989898;
|
||||||
|
opacity: 1;
|
||||||
|
border-radius: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 767px) {
|
||||||
|
.article {
|
||||||
|
max-height: 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 640px) and (max-width: 767px) {
|
||||||
|
.article {
|
||||||
|
padding: 0 calc(1em + 7vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) and (max-width: 979px) {
|
||||||
|
.article {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 980px) and (max-width: 1279px) {
|
||||||
|
.article {
|
||||||
|
width: 33.33%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1280px) and (max-width: 1599px) {
|
||||||
|
.article {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1600px) {
|
||||||
|
.article {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article .container {
|
||||||
|
height: 100%;
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
height: 35%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article .title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: .4em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.published-date {
|
||||||
|
font-size: .8em;
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
margin: .5em 0 2em 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 40px 0 0;
|
||||||
|
}
|
||||||
|
|
BIN
madblog/static/fonts/Poppins.ttf
Normal file
BIN
madblog/static/fonts/Poppins.ttf
Normal file
Binary file not shown.
BIN
madblog/static/fonts/fira-sans-200.ttf
Normal file
BIN
madblog/static/fonts/fira-sans-200.ttf
Normal file
Binary file not shown.
BIN
madblog/static/fonts/fira-sans-300.ttf
Normal file
BIN
madblog/static/fonts/fira-sans-300.ttf
Normal file
Binary file not shown.
14
madblog/static/fonts/fira-sans.css
Normal file
14
madblog/static/fonts/fira-sans.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./fira-sans-200.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./fira-sans-300.ttf) format('truetype');
|
||||||
|
}
|
7
madblog/static/fonts/poppins.css
Normal file
7
madblog/static/fonts/poppins.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(./Poppins.ttf) format('truetype');
|
||||||
|
}
|
BIN
madblog/static/img/favicon.ico
Normal file
BIN
madblog/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
madblog/static/img/icon.png
Normal file
BIN
madblog/static/img/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
madblog/static/img/rss.png
Normal file
BIN
madblog/static/img/rss.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
43
madblog/templates/article.html
Normal file
43
madblog/templates/article.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% with title=title or 'Platypush blog', skip_header=skip_header, styles=['/css/blog.css', '/css/code.css'] %}
|
||||||
|
{% include 'common-head.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
{% if not skip_header %}
|
||||||
|
<div class="title">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if description %}
|
||||||
|
<div class="description">
|
||||||
|
<h3>{{ description }}</h3>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if published %}
|
||||||
|
<div class="published-date">
|
||||||
|
Published
|
||||||
|
{% if author %} by
|
||||||
|
{% if author_email %}
|
||||||
|
<a href="mailto:{{ author_email }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ author }}
|
||||||
|
|
||||||
|
{% if author_email %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
on {{ published }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{{ content | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% include 'common-tail.html' %}
|
37
madblog/templates/common-head.html
Normal file
37
madblog/templates/common-head.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="{{ config.title }}" href="/rss" />
|
||||||
|
<link rel="stylesheet" href="/fonts/poppins.css">
|
||||||
|
<link rel="stylesheet" href="/fonts/fira-sans.css">
|
||||||
|
<link rel="stylesheet" href="/css/common.css">
|
||||||
|
|
||||||
|
{% if styles %}
|
||||||
|
{% for style in styles %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ style }}">
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% if not skip_header %}
|
||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<a href="{{ config.home_link }}" title="Home">
|
||||||
|
<img src="{{ config.logo }}" class="icon" alt=".">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ config.link }}" title="{{ config.title }}">
|
||||||
|
<span class="title">{{ config.title }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right">
|
||||||
|
<a href="/rss" target="_blank" title="RSS feeds">
|
||||||
|
<div class="icon"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% endif %}
|
2
madblog/templates/common-tail.html
Normal file
2
madblog/templates/common-tail.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
</body>
|
||||||
|
</html>
|
37
madblog/templates/index.html
Normal file
37
madblog/templates/index.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{% with title=title or 'Platypush blog', skip_header=False, styles=['/css/home.css'] %}
|
||||||
|
{% include 'common-head.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="articles">
|
||||||
|
{% for page in pages %}
|
||||||
|
<a class="article" href="{{ page['uri'] }}">
|
||||||
|
<div class="container">
|
||||||
|
{% if page['image'] %}
|
||||||
|
<div class="image">
|
||||||
|
<img src="{{ page['image'] }}" alt="">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
{{ page['title'] }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page['published'] %}
|
||||||
|
<div class="published-date">
|
||||||
|
{{ page['published'].strftime('%b %d, %Y') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page['description'] %}
|
||||||
|
<div class="description">
|
||||||
|
{{ page['description'] }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% include 'common-tail.html' %}
|
11
madblog/uwsgi.py
Normal file
11
madblog/uwsgi.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from .config import init_config
|
||||||
|
|
||||||
|
init_config()
|
||||||
|
|
||||||
|
from .app import app
|
||||||
|
|
||||||
|
# For gunicorn/uWSGI compatibility
|
||||||
|
application = app
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
flask
|
||||||
|
markdown
|
||||||
|
pygments
|
||||||
|
pyyaml
|
42
setup.py
Executable file
42
setup.py
Executable file
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
def readfile(file):
|
||||||
|
with open(file, 'r') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='madblog',
|
||||||
|
version='0.1.0',
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
)
|
Loading…
Reference in a new issue