Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
blacklight committed Jan 11, 2022
0 parents commit a91b564
Show file tree
Hide file tree
Showing 30 changed files with 1,233 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__
build/
dist/
*.egg-info
22 changes: 22 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
recursive-include madblog/static/css *
recursive-include madblog/static/fonts *
recursive-include madblog/static/img *
recursive-include madblog/templates *
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 protected]>)
[//]: # (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.

Empty file added madblog/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions madblog/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .cli import run

if __name__ == '__main__':
run()
103 changes: 103 additions & 0 deletions madblog/app.py
Original file line number Diff line number Diff line change
@@ -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 changes: 55 additions & 0 deletions madblog/cli.py
Original file line number Diff line number Diff line change
@@ -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 changes: 54 additions & 0 deletions madblog/config.py
Original file line number Diff line number Diff line change
@@ -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:
Loading

0 comments on commit a91b564

Please sign in to comment.