Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 37d2e2d8eb4f3c18560bdd2d011d59e24194c643 0 parents
@Anomareh authored
5 .gitignore
@@ -0,0 +1,5 @@
+dist/
+mynt.egg-info/
+*.pyc
+*.sublime-project
+*.sublime-workspace
26 LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2011, Andrew Fricke
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* The names of its contributors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1  MANIFEST.in
@@ -0,0 +1 @@
+include LICENSE README.md
42 README.md
@@ -0,0 +1,42 @@
+# mynt
+
+_Another static site generator?_
+
+With the ever growing population of static site generators, more often than not I found that they either had very simplistic support for blogs or used template engines that for one reason or another irked me.
+
+After not finding a solution I was happy with, just as any other programmer would do, I decided to roll my own and wrote mynt with the hope that others would find it useful as well.
+
+
+### Install
+
+From PyPI:
+`pip install mynt`
+
+Latest trunk:
+`pip install git+https://github.com/Anomareh/mynt.git`
+
+
+### Getting Started
+
+After installing mynt head on over and give the [docs][1] a read.
+
+
+### Dependencies
+
++ [Jinja2][2]
++ [misaka][3]
++ [Pygments][4]
++ [PyYAML][5]
+
+
+### Support
+
+If you run into any issues or have any questions, either open an [issue][6] or hop in #mynt on irc.freenode.net.
+
+
+[1]: http://mynt.mirroredwhite.com/
+[2]: http://jinja.pocoo.org/
+[3]: http://misaka.61924.nl/
+[4]: http://pygments.org/
+[5]: http://pyyaml.org/
+[6]: https://github.com/Anomareh/mynt/issues
23 mynt/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function, unicode_literals
+
+import sys
+
+from mynt.core import Mynt
+from mynt.exceptions import MyntException
+
+
+def main():
+ try:
+ Mynt().generate()
+ except MyntException as e:
+ print(e)
+
+ return e.code
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
38 mynt/base.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+
+class Parser(object):
+ def __init__(self, options):
+ self.options = options
+
+ self.setup()
+
+
+ def parse(self, content):
+ raise NotImplementedError('A parser must implement parse.')
+
+ def setup(self):
+ pass
+
+class Renderer(object):
+ def __init__(self, path, options, globals_ = {}):
+ self.path = path
+ self.options = options
+ self.globals = globals_
+
+ self.setup()
+
+
+ def from_string(self, source, vars_ = {}):
+ raise NotImplementedError('A renderer must implement from_string.')
+
+ def register(self, key, value):
+ raise NotImplementedError('A renderer must implement register.')
+
+ def render(self, template, vars_ = {}):
+ raise NotImplementedError('A renderer must implement render.')
+
+ def setup(self):
+ pass
86 mynt/containers.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from collections import OrderedDict
+from datetime import datetime
+import re
+
+import yaml
+
+from mynt.exceptions import ConfigException, PostException
+from mynt.fs import File
+from mynt.utils import get_logger
+
+
+yaml.add_constructor('tag:yaml.org,2002:str', lambda loader, node: loader.construct_scalar(node))
+
+logger = get_logger('mynt')
+
+
+class Archives(object):
+ posts = OrderedDict()
+
+
+ def __init__(self, posts):
+ for post in posts:
+ year, month = datetime.utcfromtimestamp(post['timestamp']).strftime('%Y %B').split()
+
+ if year not in self.posts:
+ self.posts[year] = OrderedDict({month: [post]})
+ elif month not in self.posts[year]:
+ self.posts[year][month] = [post]
+ else:
+ self.posts[year][month].append(post)
+
+
+ def __iter__(self):
+ for year, months in self.posts.iteritems():
+ yield (year, months.items())
+
+class Config(dict):
+ def __init__(self, string):
+ super(Config, self).__init__()
+
+ try:
+ self.update(yaml.load(string))
+ except yaml.YAMLError:
+ raise ConfigException('Config contains unsupported YAML.')
+ except:
+ raise ConfigException('Invalid config format.')
+
+class Page(File):
+ pass
+
+class Post(object):
+ def __init__(self, post):
+ self.path = post.path
+ self.root = post.root
+ self.name = post.name
+ self.extension = post.extension
+
+ logger.debug('.. {0}.{1}'.format(self.name, self.extension))
+
+ try:
+ date, self.slug = re.match(r'(\d{4}-\d{2}-\d{2})-(.+)', self.name).groups()
+ self.date = datetime.strptime(date, '%Y-%m-%d')
+ except (AttributeError, ValueError):
+ raise PostException('Invalid post filename.', 'src: {0}'.format(self.path), 'must be of the format \'YYYY-MM-DD-Post-title.md\'')
+
+ try:
+ frontmatter, self.bodymatter = re.search(r'\A---\s+^(.+?)$\s+---\s*(.*)\Z', post.content, re.M | re.S).groups()
+ except AttributeError:
+ raise PostException('Invalid post format.', 'src: {0}'.format(self.path), 'frontmatter must not be empty')
+
+ try:
+ self.frontmatter = Config(frontmatter)
+ except ConfigException as e:
+ raise ConfigException('Invalid post frontmatter.', 'src: {0}'.format(self.path), e.message.lower().replace('.', ''))
+
+ if 'layout' not in self.frontmatter:
+ raise PostException('Invalid post frontmatter.', 'src: {0}'.format(self.path), 'layout must be set')
+
+class Tags(OrderedDict):
+ def __iter__(self):
+ for name in super(Tags, self).__iter__():
+ yield (name, self[name])
316 mynt/core.py
@@ -0,0 +1,316 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from argparse import ArgumentParser
+from calendar import timegm
+import logging
+import re
+from time import time
+
+from pkg_resources import load_entry_point
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name
+from pygments.util import ClassNotFound
+
+from mynt.containers import Archives, Config, Page, Post, Tags
+from mynt.exceptions import ConfigException, OptionException, RendererException
+from mynt.fs import Directory, File
+from mynt.utils import get_logger, normpath
+
+
+logger = get_logger('mynt')
+
+
+class Mynt(object):
+ config = {
+ 'assets_url': '/assets',
+ 'base_url': '/',
+ 'date_format': '%A, %B %d, %Y',
+ 'markup': 'markdown',
+ 'parser': 'misaka',
+ 'posts_url': '/<year>/<month>/<day>/<title>/',
+ 'pygmentize': True,
+ 'renderer': 'jinja',
+ 'tags_url': '/'
+ }
+
+ _parser = None
+ _renderer = None
+
+ archives = []
+ pages = []
+ posts = []
+ tags = Tags()
+
+
+ def __init__(self, args = None):
+ self._start = time()
+
+ self.opts = self._get_opts(args)
+ self.src = Directory(self.opts['src'])
+ self.dest = Directory(self.opts['dest'])
+
+ logger.setLevel(getattr(logging, self.opts['level'], logging.INFO))
+ logger.debug('>> Initializing\n.. src: {0}\n.. dest: {1}'.format(self.src, self.dest))
+
+ if self.src == self.dest:
+ raise OptionException('Source and destination must differ.')
+ elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'):
+ raise OptionException('Root is not a valid source or destination.')
+
+ logger.debug('>> Searching for config')
+
+ for ext in ('.yml', '.yaml'):
+ f = File(normpath(self.src.path, 'config' + ext))
+
+ if f.exists:
+ logger.debug('.. found: {0}'.format(f.path))
+
+ try:
+ self.config.update(Config(f.content))
+ except ConfigException as e:
+ raise ConfigException(e.message, 'src: {0}'.format(f.path))
+
+ break
+ else:
+ logger.debug('.. no config file found')
+
+ for opt in ('base_url',):
+ if opt in self.opts:
+ self.config[opt] = self.opts[opt]
+
+ self.renderer.register({'site': self.config})
+
+
+ def _get_opts(self, args):
+ opts = {}
+
+ parser = ArgumentParser(description = 'A static blog generator.')
+
+ parser.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The location %(prog)s looks for source files.')
+ parser.add_argument('dest', metavar = 'destination', help = 'The location %(prog)s outputs to.')
+
+ level = parser.add_mutually_exclusive_group()
+
+ level.add_argument('-l', '--level', default = b'INFO', type = str.upper, choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR'], help = 'Sets %(prog)s\'s log level.')
+ level.add_argument('-q', '--quiet', action = 'store_const', const = 'error', dest = 'level', help = 'Sets %(prog)s\'s log level to ERROR.')
+ level.add_argument('-v', '--verbose', action = 'store_const', const = 'debug', dest = 'level', help = 'Sets %(prog)s\'s log level to DEBUG.')
+
+ parser.add_argument('--base-url', help = 'Sets the site\'s base URL.')
+ parser.add_argument('-f', '--force', action = 'store_true', help = 'Forces generation deleting the destination if it already exists.')
+
+ for option, value in vars(parser.parse_args(args)).iteritems():
+ if value is not None:
+ if isinstance(option, str):
+ option = option.decode('utf-8')
+
+ if isinstance(value, str):
+ value = value.decode('utf-8')
+
+ opts[option] = value
+
+ return opts
+
+ def _get_parser(self):
+ return load_entry_point('mynt', 'mynt.parsers.{0}'.format(self.config['markup']), self.config['parser'])
+
+ def _get_path(self, url):
+ parts = [self.dest.path] + url.split('/')
+
+ if url.endswith('/'):
+ parts.append('index.html')
+
+ return normpath(*parts)
+
+ def _get_post_url(self, date, slug):
+ subs = {
+ '<year>': '%Y',
+ '<month>': '%m',
+ '<day>': '%d',
+ '<i_month>': str(date.month).decode('utf-8'),
+ '<i_day>': str(date.day).decode('utf-8'),
+ '<title>': slug
+ }
+
+ link = self.config['posts_url'].replace('%', '%%')
+
+ for match, replace in subs.iteritems():
+ link = link.replace(match, replace)
+
+ return date.strftime(link)
+
+ def _get_tag_url(self, name):
+ end = '/' if self.config['tags_url'].endswith('/') else '.html'
+
+ return '{0}/{1}{2}'.format(self.config['tags_url'], name, end)
+
+ def _get_renderer(self):
+ return load_entry_point('mynt', 'mynt.renderers', self.config['renderer'])
+
+ def _highlight(self, match):
+ language, code = match.groups()
+ formatter = HtmlFormatter(linenos = 'table')
+
+ for pattern, replace in [('&amp;', '&'), ('&gt;', '>'), ('&lt;', '<'), ('&quot;', '"')]:
+ code = code.replace(pattern, replace)
+
+ try:
+ code = highlight(code, get_lexer_by_name(language), formatter)
+ except ClassNotFound:
+ code = highlight(code, get_lexer_by_name('text'), formatter)
+
+ return '<div class="code"><div>{0}</div></div>'.format(code)
+
+ def _pygmentize(self, html):
+ if not self.config['pygmentize']:
+ return html
+
+ return re.sub(r'<pre[^>]+lang="([^>]+)"[^>]*><code>(.+?)</code></pre>', self._highlight, html, flags = re.S)
+
+
+ def _parse(self):
+ logger.info('>> Parsing')
+
+ path = Directory(normpath(self.src.path, '_posts'))
+
+ logger.debug('.. src: {0}'.format(path))
+
+ for f in path:
+ post = Post(f)
+
+ content = self.renderer.from_string(self.parser.parse(post.bodymatter), post.frontmatter)
+ excerpt = re.search(r'\A.*?(<p>.+?</p>)?', content, re.M | re.S).group(1)
+
+ data = {
+ 'content': content,
+ 'date': post.date.strftime(self.config['date_format']),
+ 'excerpt': excerpt,
+ 'slug': post.slug,
+ 'tags': [],
+ 'timestamp': timegm(post.date.utctimetuple()),
+ 'url': self._get_post_url(post.date, post.slug)
+ }
+
+ data.update(post.frontmatter)
+ data['tags'].sort(key = unicode.lower)
+
+ self.posts.append(data)
+
+ for tag in data['tags']:
+ if tag not in self.tags:
+ self.tags[tag] = []
+
+ self.tags[tag].append(data)
+
+ if self.posts:
+ self.posts.sort(key = lambda post: post['timestamp'], reverse = True)
+
+ self.archives = Archives(self.posts)
+
+ sorting = []
+
+ for name, posts in self.tags:
+ posts.sort(key = lambda post: post['timestamp'], reverse = True)
+
+ sorting.append({
+ 'name': name,
+ 'count': len(posts),
+ 'posts': posts,
+ 'url': self._get_tag_url(name)
+ })
+
+ sorting.sort(key = lambda tag: tag['name'].lower())
+ sorting.sort(key = lambda tag: tag['count'], reverse = True)
+
+ self.tags.clear()
+
+ for tag in sorting:
+ self.tags[tag.pop('name')] = tag
+ else:
+ logger.debug('.. no posts found')
+
+ def _render(self):
+ self._parse()
+
+ logger.info('>> Rendering')
+
+ self.renderer.register({
+ 'posts': self.posts,
+ 'tags': self.tags
+ })
+
+ logger.debug('.. posts')
+
+ for post in self.posts:
+ try:
+ self.pages.append(Page(
+ self._get_path(post['url']),
+ self._pygmentize(self.renderer.render(post['layout'], {'post': post}))
+ ))
+ except RendererException as e:
+ raise RendererException(e.message, '{0} in post \'{1}\''.format(post['layout'], post['title']))
+
+ logger.debug('.. pages')
+
+ for f in self.src:
+ if f.extension not in ('html', 'htm', 'xml'):
+ continue
+
+ template = f.path.replace(self.src.path, '')
+
+ self.pages.append(Page(
+ normpath(self.dest.path, template),
+ self._pygmentize(self.renderer.render(template))
+ ))
+
+ if 'tag_layout' in self.config and self.tags:
+ logger.debug('.. tags')
+
+ for name, data in self.tags:
+ self.pages.append(Page(
+ self._get_path(data['url']),
+ self._pygmentize(self.renderer.render(self.config['tag_layout'], {'tag': data}))
+ ))
+
+
+ def generate(self):
+ self._render()
+
+ logger.info('>> Generating')
+
+ assets_src = Directory(normpath(self.src.path, '_assets'))
+ assets_dest = Directory(normpath(self.dest.path, *self.config['assets_url'].split('/')))
+
+ if self.dest.exists:
+ if not self.opts['force']:
+ raise OptionException('Destination already exists.', 'the -f option must be used to force generation by deleting the destination')
+
+ self.dest.rm()
+
+ self.dest.mk()
+
+ for page in self.pages:
+ page.mk()
+
+ if assets_src.exists:
+ for asset in assets_src:
+ asset.cp(asset.path.replace(assets_src.path, assets_dest.path))
+
+ logger.info('Completed in {0:.3f}s'.format(time() - self._start))
+
+
+ @property
+ def parser(self):
+ if self._parser is None:
+ self._parser = self._get_parser()(self.config.get(self.config['parser'], {}))
+
+ return self._parser
+
+ @property
+ def renderer(self):
+ if self._renderer is None:
+ self._renderer = self._get_renderer()(self.src.path, self.config.get(self.config['renderer'], {}))
+
+ return self._renderer
40 mynt/exceptions.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+
+class MyntException(Exception):
+ code = 1
+
+
+ def __init__(self, message, *args):
+ self.message = message
+ self.debug = args
+
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __unicode__(self):
+ message = '!! {0}'.format(self.message)
+
+ for d in self.debug:
+ message += '\n.. {0}'.format(d)
+
+ return message
+
+
+class OptionException(MyntException):
+ code = 2
+
+class ConfigException(MyntException):
+ pass
+
+class ParserException(MyntException):
+ pass
+
+class PostException(MyntException):
+ pass
+
+class RendererException(MyntException):
+ pass
117 mynt/fs.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from codecs import open
+from os import makedirs, path as op, walk
+import shutil
+
+from mynt.utils import abspath, get_logger, normpath
+
+
+logger = get_logger('mynt')
+
+
+class Directory(object):
+ def __init__(self, path):
+ self.path = abspath(path)
+
+
+ def mk(self):
+ if not self.exists:
+ logger.debug('.. mk: {0}'.format(self.path))
+
+ makedirs(self.path)
+
+ def rm(self):
+ if self.exists:
+ logger.debug('.. rm: {0}'.format(self.path))
+
+ shutil.rmtree(self.path)
+
+
+ @property
+ def exists(self):
+ return op.exists(self.path) and op.isdir(self.path)
+
+
+ def __eq__(self, other):
+ return self.path == other
+
+ def __iter__(self):
+ for root, dirs, files in walk(self.path):
+ for d in dirs[:]:
+ if d.startswith(('.', '_')):
+ dirs.remove(d)
+
+ for f in files:
+ if f.startswith(('.', '_')):
+ continue
+
+ yield File(normpath(root, f))
+
+ def __ne__(self, other):
+ return self.path != other
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __unicode__(self):
+ return self.path
+
+class File(object):
+ def __init__(self, path, content = None):
+ self.path = abspath(path)
+ self.root = Directory(op.dirname(self.path))
+ self.name, self.extension = op.basename(self.path).rsplit('.', 1)
+ self.content = content
+
+
+ def cp(self, dest):
+ dest = File(dest)
+
+ logger.debug('.. cp: {0}.{1}\n.. src: {2}\n.. dest: {3}'.format(self.name, self.extension, self.root, dest.root))
+
+ if dest.exists:
+ dest.rm()
+ elif not dest.root.exists:
+ dest.root.mk()
+
+ shutil.copyfile(self.path, dest.path)
+
+ def mk(self):
+ if not self.exists:
+ logger.debug('.. mk: {0}'.format(self.path))
+
+ if not self.root.exists:
+ self.root.mk()
+
+ with open(self.path, 'w', encoding = 'utf-8') as f:
+ if self.content is None:
+ self.content = ''
+
+ f.write(self.content)
+
+
+ @property
+ def content(self):
+ if self._content is None and self.exists:
+ with open(self.path, 'r', encoding = 'utf-8') as f:
+ self._content = f.read()
+
+ return self._content
+
+ @content.setter
+ def content(self, content):
+ self._content = content
+
+ @property
+ def exists(self):
+ return op.exists(self.path) and op.isfile(self.path)
+
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __unicode__(self):
+ return self.path
0  mynt/parsers/__init__.py
No changes.
0  mynt/parsers/markdown/__init__.py
No changes.
68 mynt/parsers/markdown/misaka.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, unicode_literals
+
+import misaka as m
+
+from mynt.base import Parser as _Parser
+
+
+class Parser(_Parser):
+ lookup = {
+ 'extensions': {
+ 'autolink': m.EXT_AUTOLINK,
+ 'fenced_code': m.EXT_FENCED_CODE,
+ 'lax_html_blocks': m.EXT_LAX_HTML_BLOCKS,
+ 'no_intra_emphasis': m.EXT_NO_INTRA_EMPHASIS,
+ 'space_headers': m.EXT_SPACE_HEADERS,
+ 'strikethrough': m.EXT_STRIKETHROUGH,
+ 'superscript': m.EXT_SUPERSCRIPT,
+ 'tables': m.EXT_TABLES
+ },
+ 'render_flags': {
+ 'expand_tabs': m.HTML_EXPAND_TABS,
+ 'github_blockcode': m.HTML_GITHUB_BLOCKCODE,
+ 'hard_wrap': m.HTML_HARD_WRAP,
+ 'safelink': m.HTML_SAFELINK,
+ 'skip_html': m.HTML_SKIP_HTML,
+ 'skip_images': m.HTML_SKIP_IMAGES,
+ 'skip_links': m.HTML_SKIP_LINKS,
+ 'skip_style': m.HTML_SKIP_STYLE,
+ 'smartypants': m.HTML_SMARTYPANTS,
+ 'toc': m.HTML_TOC,
+ 'toc_tree': m.HTML_TOC_TREE,
+ 'use_xhtml': m.HTML_USE_XHTML
+ }
+ }
+
+ config = {
+ 'extensions': {
+ 'autolink': True,
+ 'fenced_code': True,
+ 'no_intra_emphasis': True,
+ 'strikethrough': True
+ },
+ 'render_flags': {
+ 'github_blockcode': True,
+ 'hard_wrap': True,
+ 'smartypants': True
+ }
+ }
+
+ flags = {
+ 'extensions': 0,
+ 'render_flags': 0
+ }
+
+
+ def parse(self, markdown):
+ return m.html(markdown.encode('utf-8'), **self.flags).decode('utf-8')
+
+ def setup(self):
+ for k, v in self.options.iteritems():
+ self.config[k].update(v)
+
+ for group, options in self.config.iteritems():
+ for option, value in options.iteritems():
+ if value:
+ self.flags[group] |= self.lookup[group][option]
0  mynt/renderers/__init__.py
No changes.
95 mynt/renderers/jinja.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from collections import OrderedDict
+from datetime import datetime
+from math import ceil
+
+from jinja2 import Environment, FileSystemLoader, PrefixLoader
+from jinja2.exceptions import TemplateNotFound
+
+from mynt.base import Renderer as _Renderer
+from mynt.exceptions import RendererException
+from mynt.utils import absurl, normpath
+
+
+class _PrefixLoader(PrefixLoader):
+ def get_source(self, environment, template):
+ try:
+ if not self.delimiter:
+ for prefix in self.mapping:
+ if template.startswith(prefix):
+ name = template.replace(prefix, '', 1)
+ loader = self.mapping[prefix]
+
+ break
+ else:
+ raise TemplateNotFound(template)
+ else:
+ prefix, name = template.split(self.delimiter, 1)
+ loader = self.mapping[prefix]
+ except (KeyError, ValueError):
+ raise TemplateNotFound(template)
+
+ try:
+ return loader.get_source(environment, name)
+ except TemplateNotFound:
+ raise TemplateNotFound(template)
+
+
+class Renderer(_Renderer):
+ config = {}
+
+
+ def _date(self, ts, format = '%A, %B %d, %Y'):
+ if ts is None:
+ return datetime.utcnow().strftime(format)
+
+ return datetime.utcfromtimestamp(ts).strftime(format)
+
+ def _get_asset(self, asset):
+ return absurl(self.globals['site']['base_url'], self.globals['site']['assets_url'], asset)
+
+ def _get_url(self, url = ''):
+ return absurl(self.globals['site']['base_url'], url)
+
+ def _needed(self, iterable, multiple):
+ length = float(len(iterable))
+ multiple = float(multiple)
+
+ return int((ceil(length / multiple) * multiple) - length)
+
+
+ def from_string(self, source, vars_ = {}):
+ template = self.environment.from_string(source)
+
+ return template.render(**vars_)
+
+ def register(self, vars_):
+ self.globals.update(vars_)
+ self.environment.globals.update(vars_)
+
+ def render(self, template, vars_ = {}):
+ try:
+ template = self.environment.get_template(template)
+ except TemplateNotFound:
+ raise RendererException('Template not found.')
+
+ return template.render(**vars_)
+
+ def setup(self):
+ self.config.update(self.options)
+ self.config['loader'] = _PrefixLoader(OrderedDict([
+ ('/', FileSystemLoader(self.path)),
+ ('', FileSystemLoader(normpath(self.path, '_templates')))
+ ]), None)
+
+ self.environment = Environment(**self.config)
+
+ self.environment.filters['date'] = self._date
+ self.environment.filters['needed'] = self._needed
+
+ self.environment.globals.update(self.globals)
+ self.environment.globals['get_asset'] = self._get_asset
+ self.environment.globals['get_url'] = self._get_url
49 mynt/utils.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+import logging
+from os import path as op
+from re import match, sub
+
+
+def _cleanpath(*args):
+ parts = [args[0].strip()]
+
+ for arg in args[1:]:
+ parts.append((arg.replace(op.sep, '', 1) if arg.startswith(op.sep) else arg).strip())
+
+ return parts
+
+
+def abspath(*args):
+ return op.realpath(
+ op.expanduser(
+ op.join(
+ *_cleanpath(*args)
+ )
+ )
+ )
+
+def absurl(*args):
+ if match('.+://', args[0]):
+ return sub(r'(?<!:)//+', '/', '/'.join(args))
+ else:
+ return sub(r'//+', '/', '/' + '/'.join(args))
+
+def get_logger(name):
+ logger = logging.getLogger(name)
+
+ if not logger.handlers:
+ handler = logging.StreamHandler()
+
+ logger.addHandler(handler)
+
+ return logger
+
+def normpath(*args):
+ return op.normpath(
+ op.join(
+ *_cleanpath(*args)
+ )
+ )
94 setup.py
@@ -0,0 +1,94 @@
+'''
+mynt
+====
+
+*Another static site generator?*
+
+With the ever growing population of static site generators, more often
+than not I found that they either had very simplistic support for blogs
+or used template engines that for one reason or another irked me.
+
+After not finding a solution I was happy with, just as any other
+programmer would do, I decided to roll my own and wrote mynt with the
+hope that others would find it useful as well.
+
+Install
+-------
+
+| From PyPI:
+| ``pip install mynt``
+
+| Latest trunk:
+| ``pip install git+https://github.com/Anomareh/mynt.git``
+
+Getting Started
+---------------
+
+After installing mynt head on over and give the `docs`_ a read.
+
+Dependencies
+------------
+
+- `Jinja2`_
+- `misaka`_
+- `Pygments`_
+- `PyYAML`_
+
+Support
+-------
+
+If you run into any issues or have any questions, either open an
+`issue`_ or hop in #mynt on irc.freenode.net.
+
+.. _docs: http://mynt.mirroredwhite.com/
+.. _Jinja2: http://jinja.pocoo.org/
+.. _misaka: http://misaka.61924.nl/
+.. _Pygments: http://pygments.org/
+.. _PyYAML: http://pyyaml.org/
+.. _issue: https://github.com/Anomareh/mynt/issues
+'''
+from setuptools import find_packages, setup
+
+
+setup(
+ name = 'mynt',
+ version = '0.1',
+ author = 'Andrew Fricke',
+ author_email = 'andrew@mirroredwhite.com',
+ url = 'http://mynt.mirroredwhite.com/',
+ description = 'A static blog generator.',
+ long_description = __doc__,
+ license = 'BSD',
+ packages = find_packages(),
+ entry_points = {
+ 'mynt.parsers.markdown': [
+ 'misaka = mynt.parsers.markdown.misaka:Parser'
+ ],
+ 'mynt.renderers': [
+ 'jinja = mynt.renderers.jinja:Renderer'
+ ],
+ 'console_scripts': 'mynt = mynt:main'
+ },
+ install_requires = [
+ 'Jinja2',
+ 'misaka',
+ 'Pygments',
+ 'PyYAML'
+ ],
+ platforms = 'any',
+ zip_safe = False,
+ classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Console',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: End Users/Desktop',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Internet',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Text Processing',
+ 'Topic :: Utilities'
+ ]
+)
Please sign in to comment.
Something went wrong with that request. Please try again.