init
This commit is contained in:
		
							
								
								
									
										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()
 | 
			
		||||
		Reference in New Issue
	
	Block a user