ssg/__init__.py

Summary

Maintainability
B
5 hrs
Test Coverage
"""
Main routines for the Static Site Generator.

Version 0.0.5:
 
* Parse metadata using string templates.

"""

import logging
import os
import codecs
import markdown
from string import Template
from ssg import writer
from jinja2 import Environment, FileSystemLoader, TemplateSyntaxError
from jinja2 import TemplateError
from ssg.log import logger, init_file_log, init_console_log, close_log
from ssg.metaext import parsers_run
from ssg import settings
from ssg.settings import SETTINGS, write_config
from ssg.tools import get_files, get_datetime, die
from ssg.context import CONTEXT
from ssg import generators
from ssg import contentfilters
# Markdown extensions
from ssg.markdownext.figure import FigureExtension


__version__ = '0.0.5'
configs = {}
# Instantiate Markdown extensions
figure = FigureExtension(configs=configs)

# Extra extensions
# Meta-data extension
# Table of Contents extension
# Figure extension
MARKDOWN_EXTENSIONS = ['markdown.extensions.extra',
                       'markdown.extensions.meta',
                       'markdown.extensions.toc',
                       figure]
"""Markdown extension to use."""

DEBUG = False


class ContentParserError(RuntimeError):
    """
    The client has received an unexpected response
    """
    pass


def init(debug=False, root=None):
    """
    Initialise ssg

    :param debug: True enables debugging to console
    :type debug: bool
    :param root: Root directory of the site
    :type root: string
    """
    global DEBUG
    # Init logging to file
    init_file_log(logging.DEBUG)
    # Set the console logging level
    if debug:
        init_console_log(logging.DEBUG)
        DEBUG = True
    else:
        init_console_log(logging.INFO)
    # Init settings placeholder
    settings.init(root)


def _get_html_file_name(metadata):
    """
    Return the html file name from the source file name in the metadata.

    :param metadata: Meta data to use. *src_file* key must be present.
    :type metadata: dict
    :return: HTML file name.
    """
    html_file_name = os.path.basename(metadata['src_file'])
    # Strip old extension
    html_file_name, _ = os.path.splitext(html_file_name)
    # Add new
    html_file_name += '.html'
    return html_file_name


def _get_url(metadata, file_name):
    """
    Get the final URL of the rendered html on the site.

    :param metadata: Meta data to use. *src_file* key must be present.
    :type metadata: dict
    """
    logger.debug('Generating URL from: ' + str(metadata) + ' + ' + file_name)
    # Start with the site URL
    site_url = SETTINGS['SITEURL'] + '/'
    # Get path starting from content
    content_path = os.path.join(SETTINGS['ROOTDIR'],
                                SETTINGS['CONTENTDIR'])
    # Get relative path to source file directory.
    source_relative_path = os.path.dirname(
        os.path.relpath(metadata['src_file'], content_path)
    )

    # Add the filename to the URL
    url = os.path.join(site_url, source_relative_path, file_name)
    logger.debug('URL: ' + url)
    return url


def _new_metadata(filename, md):
    """
    Create new meta data from a Markdown file.

    :param filename: The full path of the source Markdown file.
    :type filename: string
    :param md: An instance of the Markdown preprocessor
    :type md: markdown.Markdown
    """

    logger.debug("Creating new metadata.")
    # Create a dictionary for meta data
    metadata = dict()
    metadata['src_file'] = filename
    # Create output file path and name
    # Get path starting from content
    content_path = os.path.join(SETTINGS['ROOTDIR'],
                                SETTINGS['CONTENTDIR'])
    output_filename = os.path.relpath(metadata['src_file'],
                                      content_path)
    # Strip old extension
    output_filename, _ = os.path.splitext(output_filename)
    # Add new
    output_filename += '.html'
    # Make an absolute path
    output_filename = os.path.join(SETTINGS['ROOTDIR'],
                                   SETTINGS['OUTPUTDIR'],
                                   output_filename)
    # Save output file name
    metadata['dst_file'] = output_filename
    metadata['URL'] = _get_url(
        metadata,
        _get_html_file_name(metadata)
    )
    # Check for meta data
    if len(md.Meta) == 0:
        raise ContentParserError('No meta data found.')
    # Splice the lines together
    for key, item in md.Meta.items():
        item = ''.join(item)
        # Add meta data from the meta data markdown extension
        try:
            tmpl = Template(item)
            item = tmpl.substitute(LOCALURL=_get_url(metadata, ''))
        except KeyError as exception:
            logger.error('Could not find key ' + str(exception) + '.')
            logger.error('Maybe a missing $ character.')
            raise exception

        metadata[key] = item
    # Get python datetime from the one in the content meta data
    if 'date' in metadata.keys():
        metadata['date'] = get_datetime(metadata['date'])
    # Run through extra meta data parsers.
    metadata.update(parsers_run(filename))
    logger.debug('Metadata: ' + str(metadata))
    return metadata


def process_content(path, context):
    """
    Process all contents, converting it to HTML.

    *Metadata need to start at the first line of the file, and to have ONE
    newline before the content.*

    :param path: Where the content files are at.
    :type path: string
    :returns: A list of contexts
    """
    logger.info("Processing content.")
    # Get list of files
    content_files = get_files(path, '.md')

    # Run through the files
    for filename in content_files:
        try:
            # Open it
            with codecs.open(filename, encoding='utf-8') as markdown_file:
                logger.info("Reading: " + filename)
                # Create an instance of the Markdown processor
                md = markdown.Markdown(extensions=MARKDOWN_EXTENSIONS,
                                       output_format='html5')
                # Convert file to html
                html_content = md.convert(markdown_file.read())
                # Create meta data
                metadata = _new_metadata(filename, md)
                # Create content
                content = dict()
                # Add meta data
                content['metadata'] = metadata
                # Add content
                content['content'] = html_content
                # Run content filters on content
                contentfilters.run(content)
                # Append the content to the list
                context.contents.append(content)
        except Exception as exception:
            if DEBUG:
                logger.exception('Exception reading file: ' + filename)
            else:
                logger.error('Exception reading file: ' + filename)
            raise exception
    return(context)


def sanity_checks(context):
    """Checks to see if the templates and content actually parses.

    :param context:
    :type context:
    """
    logger.debug("Running sanity checks on input.")
    for content in context.contents:
        # Check if template is set
        if 'template' not in content['metadata']:
            raise ContentParserError('Missing template in: ' +
                                     content['metadata']['src_file'])
        if 'title' not in content['metadata']:
            raise ContentParserError('Missing title in: ' +
                                     content['metadata']['src_file'])


def apply_templates(path, context):
    """
    Apply jinja2 templates to content, and write the files.

    :param path: Path to templates
    :type path: string
    :param contents: A list of metadata, content tuples
    :type contents: list
    :returns: List of contexts
    """
    logger.info('Applying templates.')
    env = Environment(loader=FileSystemLoader(path))
    # Run generator extensions
    generators.run(context)
    # Run through all content
    try:
        for content in context.contents:
            # Use specified template or index.html
            if 'template' in content['metadata'].keys():
                template = content['metadata']['template'] + '.html'
            else:
                template = 'page.html'
            logger.debug('Using "' + template + '" as template.')
            # Get template
            tpl = env.get_template(template)
            # Use default context if none is set
            if 'context' in content.keys():
                local_context = content['context']
            else:
                local_context = {'context': context, 'content': content}
            # Render template
            if content['metadata']['src_file'] != '':
                logger.info("Rendering " + content['metadata']['src_file'])
            logger.debug('Rendering template "' + template +
                         '" with "' + content['metadata']['src_file'] + '"')
            content['html'] = tpl.render(local_context)
    except TemplateSyntaxError as exception:
        logger.error('Jinja2 syntax error:')
        logger.error('In ' + exception.name + ' line number :' +
                     str(exception.lineno))
        logger.error(exception.filename)
        raise exception
    except TemplateError as exception:
        logger.error('Jinja2 syntax error:')
        logger.error('Template: ' + content['metadata']['template'])
        logger.error('Destination: ' + content['metadata']['dst_file'])
        raise exception
    return(context)


def create_empty_site(path=None):
    """
    Create the skeleton and config file for a new site.

    :param path: Root path of the new site. Default is current directory.
    :type path: string
    """
    # Defaults to current directory
    if path is None:
        path = os.getcwd()
    # Write a shiny new configuration file
    write_config(path)

    # Create directories
    os.mkdir(os.path.join(SETTINGS['ROOTDIR'], SETTINGS['CONTENTDIR']))
    os.mkdir(os.path.join(SETTINGS['ROOTDIR'], SETTINGS['TEMPLATEDIR']))
    os.mkdir(os.path.join(SETTINGS['ROOTDIR'], SETTINGS['OUTPUTDIR']))


def run(update):
    """Process everything and create output files.

    :param update: Only write updated files.
    :type update: bool
    """
    global CONTEXT, DEBUG
    # Add settings to global context
    CONTEXT.settings = SETTINGS
    try:
        # Process the input files
        CONTEXT = process_content(os.path.join(SETTINGS['ROOTDIR'],
                                               SETTINGS['CONTENTDIR']),
                                  CONTEXT)
        # Template and content sanity checks
        sanity_checks(CONTEXT)
        # Apply the templates
        CONTEXT = apply_templates(os.path.join(SETTINGS['ROOTDIR'],
                                               SETTINGS['TEMPLATEDIR']),
                                  CONTEXT)
        # Copy and write the output files
        writer.write(os.path.join(SETTINGS['ROOTDIR'], SETTINGS['CONTENTDIR']),
                     CONTEXT,
                     update)
    except Exception as exception:
        logger.error(str(exception))
        if DEBUG:
            raise exception
        die()


def close():
    """
    Perform cleanup.
    """
    close_log()