init
This commit is contained in:
commit
ba26c8c1b6
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
build/
|
36
Makefile
Normal file
36
Makefile
Normal file
@ -0,0 +1,36 @@
|
||||
CONTENTDIR = content
|
||||
STATICDIR = static
|
||||
BUILDDIR = build
|
||||
|
||||
all: html
|
||||
|
||||
help:
|
||||
@echo 'Build site'
|
||||
@echo
|
||||
@echo 'Available targets:'
|
||||
@echo
|
||||
@echo ' html render HTML'
|
||||
@echo ' serve serve site'
|
||||
@echo ' css <theme> generate Pygments stylesheet'
|
||||
@echo
|
||||
@echo 'Run without target to build html'
|
||||
|
||||
html:
|
||||
test -d $(BUILDDIR) && rm -rf $(BUILDDIR) || true
|
||||
python rstw
|
||||
|
||||
serve:
|
||||
if hash http-server; \
|
||||
then \
|
||||
http-server $(BUILDDIR)/; \
|
||||
else \
|
||||
python3 -m http.server --directory $(BUILDDIR)/; \
|
||||
fi
|
||||
|
||||
css:
|
||||
mkdir -pv $(STATICDIR)/css/pygments
|
||||
pygmentize -f html -S $(filter-out $@,$(MAKECMDGOALS)) -a .highlight \
|
||||
> $(STATICDIR)/css/pygments/$(filter-out $@,$(MAKECMDGOALS)).css
|
||||
|
||||
%:
|
||||
@:
|
6
README
Normal file
6
README
Normal file
@ -0,0 +1,6 @@
|
||||
reStructuredWeb -- static site generator.
|
||||
|
||||
Docs:
|
||||
|
||||
* https://nixhacks.net/rstw/
|
||||
* https://git/nxhs.cloud/ge/rstw_docs/
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
docutils==0.17.1
|
||||
Jinja2==3.1.2
|
||||
Pygments==2.12.0
|
||||
toml==0.10.2
|
||||
colorlog==6.7.0
|
312
rstw
Executable file
312
rstw
Executable file
@ -0,0 +1,312 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = '0.1.0'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import datetime
|
||||
import logging
|
||||
import toml
|
||||
import jinja2
|
||||
import colorlog
|
||||
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------- #
|
||||
# Setup logger. #
|
||||
# ------------------------------------------------------------- #
|
||||
|
||||
LOGFORMAT = " %(log_color)s%(levelname)-8s%(reset)s \
|
||||
%(log_color)s%(message)s%(reset)s"
|
||||
log = logging.getLogger('reStructuredWeb')
|
||||
log.setLevel(logging.INFO)
|
||||
handler = logging.StreamHandler(stream=sys.stdout)
|
||||
handler.setFormatter(
|
||||
colorlog.ColoredFormatter(LOGFORMAT))
|
||||
log.addHandler(handler)
|
||||
|
||||
# ------------------------------------------------------------- #
|
||||
# Configuration. #
|
||||
# ------------------------------------------------------------- #
|
||||
|
||||
# Set defaults
|
||||
BUILD_DEFAULTS = {
|
||||
'build_dir': 'build',
|
||||
'content_dir': 'content',
|
||||
'templates_dir': 'layouts',
|
||||
'static_dir': 'static'
|
||||
}
|
||||
SITE_DEFAULTS = {'datetime_format': '%Y-%m-%d'}
|
||||
PYGMENTS_DEFAULTS = {'theme': 'default'}
|
||||
DOCUTILS_DEFAULTS = {}
|
||||
|
||||
# Read configuration file
|
||||
try:
|
||||
with open('settings.toml', 'r') as file:
|
||||
settings = toml.loads(file.read())
|
||||
except OSError as err:
|
||||
log.error('Cannot open \'settings.toml\'. ' +
|
||||
'Is the file actually in the current directory?')
|
||||
|
||||
# Fallback to default settings
|
||||
|
||||
# `build` section
|
||||
for dir in list(BUILD_DEFAULTS.keys()):
|
||||
try:
|
||||
settings['build'][dir]
|
||||
except KeyError:
|
||||
settings['build'][dir] = BUILD_DEFAULTS[dir]
|
||||
|
||||
# `site` section
|
||||
try:
|
||||
settings['site']['datetime_format']
|
||||
except KeyError:
|
||||
settings['site']['datetime_format'] = \
|
||||
SITE_DEFAULTS['datetime_format']
|
||||
|
||||
# `pygments` section
|
||||
try:
|
||||
settings['pygments']['theme']
|
||||
except KeyError:
|
||||
try:
|
||||
settings['pygments']
|
||||
except KeyError:
|
||||
settings['pygments'] = {}
|
||||
settings['pygments']['theme'] = PYGMENTS_DEFAULTS['theme']
|
||||
|
||||
# `docutils` section
|
||||
try:
|
||||
settings['docutils']
|
||||
except KeyError:
|
||||
settings['docutils'] = DOCUTILS_DEFAULTS
|
||||
|
||||
# ------------------------------------------------------------- #
|
||||
# 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 aggregate_lists(file_list: list) -> dict:
|
||||
posts = []
|
||||
pages = []
|
||||
for rst_file in file_list:
|
||||
with open(rst_file, 'r', encoding = 'utf-8') as rst:
|
||||
docinfo = parsedocinfo(rst.read())
|
||||
|
||||
# Validate date format
|
||||
try:
|
||||
dt = datetime.datetime.strptime(docinfo['date'],
|
||||
settings['site']['datetime_format'])
|
||||
except (KeyError, ValueError) as err:
|
||||
log.error('Wrong formatted or missing date in file' +
|
||||
'\'{}\': {}'.format(rst_file, err))
|
||||
|
||||
# Add path ot file
|
||||
docinfo['path'] = '/' + os.path.relpath(
|
||||
os.path.splitext(rst_file)[0] + '.html',
|
||||
settings['build']['content_dir'])
|
||||
try:
|
||||
if docinfo['type'] == 'post':
|
||||
posts.append(docinfo)
|
||||
else:
|
||||
pages.append(docinfo)
|
||||
except KeyError:
|
||||
pages.append(docinfo) # may 'default_article_type'
|
||||
|
||||
# Sort posts by date (newest in top)
|
||||
posts.sort(key=lambda date: datetime.datetime.strptime(date['date'],
|
||||
settings['site']['datetime_format']), reverse = True)
|
||||
|
||||
return {'posts': posts, 'pages': pages}
|
||||
|
||||
def build_site():
|
||||
# Prepare build directory
|
||||
os.makedirs(settings['build']['build_dir'], exist_ok = True)
|
||||
|
||||
log.info('Collecting data ...')
|
||||
rst_files = find_rst_files(settings['build']['content_dir'])
|
||||
lists = aggregate_lists(rst_files)
|
||||
|
||||
for rst_file in rst_files:
|
||||
with open(rst_file, 'r', encoding = 'utf-8') as rst:
|
||||
source = rst.read()
|
||||
|
||||
log.info('Parsing docinfo: %s' % rst_file)
|
||||
page_meta = parsedocinfo(source)
|
||||
|
||||
# Render HTML files
|
||||
html_file_path = os.path.join(settings['build']['build_dir'],
|
||||
os.path.relpath(os.path.splitext(rst_file)[0] + '.html',
|
||||
settings['build']['content_dir']))
|
||||
|
||||
log.info('Rendering page: %s' % html_file_path)
|
||||
|
||||
# Page template from docinfo
|
||||
try:
|
||||
template = page_meta['template']
|
||||
if not os.path.exists(os.path.join(
|
||||
settings['build']['templates_dir'], template)):
|
||||
log.error('{}: Template does not exist: {}'.format(
|
||||
rst_file, template))
|
||||
sys.exit(1)
|
||||
except KeyError:
|
||||
template = settings['build']['default_template']
|
||||
|
||||
# Render HTML
|
||||
html_body = render_html_body(source)
|
||||
|
||||
# Render template
|
||||
html_page = render_template(
|
||||
template,
|
||||
templates_dir = settings['build']['templates_dir'],
|
||||
pygments_theme = settings['pygments']['theme'],
|
||||
site = settings['site'],
|
||||
page = page_meta,
|
||||
aggr = lists,
|
||||
html = html_body
|
||||
)
|
||||
|
||||
# Save rendered page
|
||||
os.makedirs(os.path.dirname(html_file_path), exist_ok = True)
|
||||
write_to_file(html_file_path, html_page)
|
||||
|
||||
# Copy additional files to build_dir
|
||||
log.info('Copy static files from {}, {} to {}'.format(
|
||||
settings['build']['static_dir'],
|
||||
settings['build']['content_dir'],
|
||||
settings['build']['build_dir']))
|
||||
|
||||
copy_files(settings['build']['static_dir'],
|
||||
settings['build']['build_dir'])
|
||||
copy_files(settings['build']['content_dir'],
|
||||
settings['build']['build_dir'])
|
||||
|
||||
log.info('Success')
|
||||
|
||||
if __name__ == '__main__':
|
||||
build_site()
|
Loading…
Reference in New Issue
Block a user