init
This commit is contained in:
commit
9a9405cb82
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
build/
|
||||||
|
assets/
|
||||||
|
content/
|
||||||
|
layouts/
|
||||||
|
*.txt
|
21
Makefile
Normal file
21
Makefile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
all: html
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo 'Run without target to build all (html, rss)'
|
||||||
|
@echo Available targets:
|
||||||
|
@echo 'html render HTML'
|
||||||
|
@echo 'serve serve site on localhost'
|
||||||
|
@echo 'css <theme> generate Pygments stylesheet'
|
||||||
|
|
||||||
|
html:
|
||||||
|
python rst_blg.py
|
||||||
|
|
||||||
|
serve:
|
||||||
|
http-server ./build
|
||||||
|
|
||||||
|
css:
|
||||||
|
mkdir -p assets/css/pygments
|
||||||
|
pygmentize -f html -S $(filter-out $@,$(MAKECMDGOALS)) -a .highlight > assets/css/pygments/$(filter-out $@,$(MAKECMDGOALS)).css
|
||||||
|
|
||||||
|
%:
|
||||||
|
@:
|
107
README.rst
Normal file
107
README.rst
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
Static site generator from reStructuredText files.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
Run::
|
||||||
|
|
||||||
|
git clone ttps://git.nxhs.cloud/ge/blog.git && cd blog
|
||||||
|
python3 -m venv env
|
||||||
|
source env/bin/activate
|
||||||
|
pip intall -r requirements.txt
|
||||||
|
mkdir -p {layouts,content,assets/css/pygments}
|
||||||
|
touch settings.toml content/my_page.rst layout/{base,index,post}.j2
|
||||||
|
make css default
|
||||||
|
|
||||||
|
Put in settings.toml::
|
||||||
|
|
||||||
|
[site]
|
||||||
|
title = 'My site'
|
||||||
|
index_page_title = 'Home page'
|
||||||
|
datetime_format = '%d %b %Y'
|
||||||
|
|
||||||
|
[build]
|
||||||
|
build_dir = 'build'
|
||||||
|
content_dir = 'content'
|
||||||
|
templates_dir = 'layouts'
|
||||||
|
assets_dir = 'assets'
|
||||||
|
|
||||||
|
[pygments]
|
||||||
|
theme = 'default'
|
||||||
|
|
||||||
|
Create basic Jinja2 templates.
|
||||||
|
|
||||||
|
base.j2::
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="stylesheet" href="assets/css/pygments/{{ pygments_theme }}.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<p>
|
||||||
|
{% if posts %}
|
||||||
|
{{ site_title }}
|
||||||
|
{% else %}
|
||||||
|
<a href="/">{{ site_title }}</a> / {{ page_title }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
index.j2::
|
||||||
|
|
||||||
|
{% extends "base.j2" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<ul id="posts">
|
||||||
|
{% for post in posts %}
|
||||||
|
<li>
|
||||||
|
<a href="/{{ post['path'] }}">{{post['title'] }}</a>
|
||||||
|
<span class="meta"> — {{ post['date'] }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
post.j2::
|
||||||
|
|
||||||
|
{% extends "base.j2" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<article>
|
||||||
|
{{ post | safe }}
|
||||||
|
<article>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
Put in content/my_page.rst::
|
||||||
|
|
||||||
|
:title: My first page
|
||||||
|
:date: 01 Jan 1970
|
||||||
|
|
||||||
|
=============
|
||||||
|
My first page
|
||||||
|
=============
|
||||||
|
|
||||||
|
Hello, World!
|
||||||
|
|
||||||
|
Build site::
|
||||||
|
|
||||||
|
make
|
||||||
|
|
||||||
|
See HTML in build/ dir. Write your own CSS styles.
|
236
rst_blg.py
Normal file
236
rst_blg.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
__version__ = '0.1.0'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import toml
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from collections import namedtuple
|
||||||
|
from docutils.core import publish_parts
|
||||||
|
from docutils.core import publish_doctree
|
||||||
|
from docutils.writers import html5_polyglot
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.parsers.rst import directives
|
||||||
|
from pygments import highlight
|
||||||
|
from pygments.lexers import get_lexer_by_name
|
||||||
|
from pygments.formatters import HtmlFormatter
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# Read TOML configuration file. #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
with open('settings.toml', 'r') as file:
|
||||||
|
settings = toml.loads(file.read())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# Setup logger. #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
log = logging.getLogger('blog')
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
handler = logging.StreamHandler(stream=sys.stdout)
|
||||||
|
handler.setFormatter(
|
||||||
|
logging.Formatter(fmt = '[%(asctime)s: %(levelname)s] %(message)s'))
|
||||||
|
log.addHandler(handler)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# Parse docinfo from rST files. #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# Code below is copy-pasted from https://github.com/zeddee/parsedocinfo
|
||||||
|
# and modified a little bit. Zeddee, thank you!
|
||||||
|
# Original license SPDX identifier: Apache-2.0
|
||||||
|
# -- parsedocinfo BEGIN --
|
||||||
|
|
||||||
|
DocInfo = namedtuple("DocInfo", 'name body')
|
||||||
|
|
||||||
|
def _traverse_fields(field: List) -> DocInfo:
|
||||||
|
field_name = field.getElementsByTagName("field_name")[0]
|
||||||
|
field_body = field.getElementsByTagName("field_body")[0]
|
||||||
|
return DocInfo(field_name.firstChild.nodeValue,
|
||||||
|
" ".join(val.firstChild.nodeValue for val in field_body.childNodes))
|
||||||
|
|
||||||
|
def _traverse_docinfo(docinfo_list: List) -> List[DocInfo]:
|
||||||
|
out = []
|
||||||
|
for i in docinfo_list:
|
||||||
|
for node in i.childNodes:
|
||||||
|
if node.tagName == "field":
|
||||||
|
out.append(_traverse_fields(node))
|
||||||
|
else:
|
||||||
|
out.append(DocInfo(node.tagName,
|
||||||
|
" ".join(val.nodeValue for val in node.childNodes)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def parsedocinfo(data: str) -> dict:
|
||||||
|
docinfo = publish_doctree(data).asdom().getElementsByTagName("docinfo")
|
||||||
|
return dict(_traverse_docinfo(docinfo))
|
||||||
|
|
||||||
|
# -- parsedocinfo END --
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# Extra reStructuredText directives and roles #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# Pygments reST `code-block` directive.
|
||||||
|
# Source: https://docutils.sourceforge.io/sandbox/code-block-directive/
|
||||||
|
# `code-block` BEGIN
|
||||||
|
|
||||||
|
pygments_formatter = HtmlFormatter()
|
||||||
|
|
||||||
|
def pygments_directive(name, arguments, options, content, lineno,
|
||||||
|
content_offset, block_text, state, state_machine):
|
||||||
|
try:
|
||||||
|
lexer = get_lexer_by_name(arguments[0])
|
||||||
|
except ValueError:
|
||||||
|
# no lexer found - use the text one instead of an exception
|
||||||
|
lexer = get_lexer_by_name('text')
|
||||||
|
parsed = highlight(u'\n'.join(content), lexer, pygments_formatter)
|
||||||
|
return [nodes.raw('', parsed, format='html')]
|
||||||
|
|
||||||
|
pygments_directive.arguments = (1, 0, 1)
|
||||||
|
pygments_directive.content = 1
|
||||||
|
directives.register_directive('code-block', pygments_directive)
|
||||||
|
|
||||||
|
# `code-block` END
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# Jinja2 specific functions. #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def render_template(template: str, templates_dir = '.', **kwargs) -> str:
|
||||||
|
"""Render Jinja2 template from file. Usage::
|
||||||
|
|
||||||
|
render_template('index.j2',
|
||||||
|
templates_dir = './templates',
|
||||||
|
title = 'My title')
|
||||||
|
"""
|
||||||
|
env = jinja2.Environment(loader = jinja2.FileSystemLoader(templates_dir))
|
||||||
|
return env.get_template(template).render(**kwargs)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# Render HTML from reStructuredText. #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def render_html_body(text: str) -> str:
|
||||||
|
"""Return HTML body converted from reStructuredText.
|
||||||
|
See:
|
||||||
|
* help(docutils.core.publish_parts)
|
||||||
|
* https://docutils.sourceforge.io/docs/user/config.html
|
||||||
|
"""
|
||||||
|
html = publish_parts(source = text, writer = html5_polyglot.Writer(),
|
||||||
|
settings_overrides = settings['docutils']
|
||||||
|
)
|
||||||
|
return html['body']
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# File operations. #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def find_rst_files(directory: str) -> list:
|
||||||
|
"""Return the list of rST files from directory.
|
||||||
|
Scan subdirectories too.
|
||||||
|
"""
|
||||||
|
file_list = []
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
for name in files:
|
||||||
|
if os.path.splitext(name)[1] == '.rst':
|
||||||
|
file_list.append(os.path.join(root, name))
|
||||||
|
return file_list
|
||||||
|
|
||||||
|
def write_to_file(path: str, data: str):
|
||||||
|
with open(path, 'w', encoding = 'utf-8') as file:
|
||||||
|
file.write(data)
|
||||||
|
|
||||||
|
def copy_files(source_dir: str, destination_dir: str):
|
||||||
|
shutil.copytree(source_dir, destination_dir,
|
||||||
|
ignore = shutil.ignore_patterns('*.rst'),
|
||||||
|
dirs_exist_ok = True)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
# Build site! #
|
||||||
|
# ------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def validate_date_fmt(date: str):
|
||||||
|
try:
|
||||||
|
dt = datetime.datetime.strptime(date,
|
||||||
|
settings['site']['datetime_format'])
|
||||||
|
except ValueError as err:
|
||||||
|
log.error('Error: Wrong date format: %s' % err)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def build_site():
|
||||||
|
|
||||||
|
build_dir = settings['build']['build_dir']
|
||||||
|
content_dir = settings['build']['content_dir']
|
||||||
|
templates_dir = settings['build']['templates_dir']
|
||||||
|
assets_dir = settings['build']['assets_dir']
|
||||||
|
|
||||||
|
os.makedirs(build_dir, exist_ok = True)
|
||||||
|
files = find_rst_files(content_dir)
|
||||||
|
posts_list = []
|
||||||
|
|
||||||
|
for rst_file in files:
|
||||||
|
with open(rst_file, 'r', encoding = 'utf-8') as rst:
|
||||||
|
source = rst.read()
|
||||||
|
|
||||||
|
log.info('parse docinfo: %s' % rst_file)
|
||||||
|
meta = parsedocinfo(source)
|
||||||
|
meta_dict = meta
|
||||||
|
# TODO обработать ошибку при отсутствии :date: в файле
|
||||||
|
# TODO показывать ошибку при отсутствии :title:
|
||||||
|
validate_date_fmt(meta_dict['date'])
|
||||||
|
meta_dict['path'] = os.path.basename(rst_file).replace('.rst', '.html')
|
||||||
|
|
||||||
|
# Don't append single pages into posts list
|
||||||
|
try:
|
||||||
|
meta_dict['not_a_post']
|
||||||
|
except KeyError:
|
||||||
|
posts_list.append(meta_dict)
|
||||||
|
|
||||||
|
# Render HTML files
|
||||||
|
html_file = os.path.basename(rst_file).replace('.rst', '.html')
|
||||||
|
|
||||||
|
log.info('render html: %s' % html_file)
|
||||||
|
|
||||||
|
html_body = render_html_body(source)
|
||||||
|
html_page = render_template('post.j2',
|
||||||
|
templates_dir = templates_dir,
|
||||||
|
pygments_theme = settings['pygments']['theme'],
|
||||||
|
site_title = settings['site']['title'],
|
||||||
|
page_title = meta['title'],
|
||||||
|
post = html_body)
|
||||||
|
write_to_file(os.path.join(build_dir, html_file), html_page)
|
||||||
|
|
||||||
|
# Copy additional files to build_dir
|
||||||
|
log.info('copy assets and files from {}, {} to {}'.format(
|
||||||
|
assets_dir, content_dir, build_dir))
|
||||||
|
assets_dest_dir = os.path.join(build_dir,
|
||||||
|
os.path.basename(assets_dir))
|
||||||
|
copy_files(assets_dir, assets_dest_dir)
|
||||||
|
copy_files(content_dir, build_dir)
|
||||||
|
|
||||||
|
# Sort posts by date (newest in top)
|
||||||
|
posts_list.sort(
|
||||||
|
key=lambda date: datetime.datetime.strptime(date['date'], "%d %b %Y"),
|
||||||
|
reverse = True)
|
||||||
|
|
||||||
|
# Redner index page
|
||||||
|
log.info('generate index.html')
|
||||||
|
html_page = render_template('index.j2',
|
||||||
|
templates_dir = templates_dir,
|
||||||
|
pygments_theme = settings['pygments']['theme'],
|
||||||
|
site_title = settings['site']['title'],
|
||||||
|
page_title = settings['site']['index_page_title'],
|
||||||
|
posts = posts_list)
|
||||||
|
write_to_file(os.path.join(build_dir, 'index.html'), html_page)
|
||||||
|
log.info('success')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
build_site()
|
21
settings.toml
Normal file
21
settings.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[site]
|
||||||
|
title = 'Nixhacks'
|
||||||
|
index_page_title = 'Ещё один сайт про cисадмиство и программирование'
|
||||||
|
datetime_format = '%d %b %Y'
|
||||||
|
default_posts_template = 'post.j2' # not implemented
|
||||||
|
default_index_template = 'index.j2' # not implemented
|
||||||
|
|
||||||
|
[build]
|
||||||
|
build_dir = 'build'
|
||||||
|
content_dir = 'content'
|
||||||
|
templates_dir = 'layouts'
|
||||||
|
assets_dir = 'assets'
|
||||||
|
|
||||||
|
[pygments]
|
||||||
|
theme = 'solarized-light'
|
||||||
|
|
||||||
|
# https://docutils.sourceforge.io/docs/user/config.html
|
||||||
|
[docutils]
|
||||||
|
initial_header_level = 2
|
||||||
|
section_self_link = true
|
||||||
|
syntax_highlight = 'short'
|
Loading…
Reference in New Issue
Block a user