diff options
Diffstat (limited to 'makesite.py')
-rwxr-xr-x | makesite.py | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/makesite.py b/makesite.py new file mode 100755 index 0000000..e0caa0f --- /dev/null +++ b/makesite.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python + +# The MIT License (MIT) +# +# Copyright (c) 2018 Sunaina Pai +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +"""Make static website/blog with Python.""" + + +import os +import shutil +import re +import glob +import sys +import json + + +def fread(filename): + """Read file and close the file.""" + with open(filename, 'r') as f: + return f.read() + + +def fwrite(filename, text): + """Write content to file and close the file.""" + basedir = os.path.dirname(filename) + if not os.path.isdir(basedir): + os.makedirs(basedir) + + with open(filename, 'w') as f: + f.write(text) + + +def log(msg, *args): + """Log message with specified arguments.""" + sys.stderr.write(msg.format(*args) + '\n') + + +def truncate(text, words=25): + return ' '.join(re.sub('(?s)<.*?>', ' ', text).split()[:words]) + + +def read_headers(text): + """Parse headers in text and yield (key, value, end-index) tuples.""" + for match in re.finditer('\s*<!--\s*(.+?)\s*:\s*(.+?)\s*-->\s*|.+', text): + if not match.group(1): + break + yield match.group(1), match.group(2), match.end() + + +def read_content(filename): + """Read content and metadata from file into a dictionary.""" + # Read file content. + text = fread(filename) + + # Read metadata. + date_slug = os.path.basename(filename).split('.')[0] + match = re.search('^(?:(\d\d\d\d-\d\d-\d\d)-)?(.+)$', date_slug) + content = { + 'date': match.group(1) or '1970-01-01', + 'slug': match.group(2), + } + + # Read headers. + end = 0 + for key, val, end in read_headers(text): + content[key] = val + + # Separate content from headers. + text = text[end:] + + # Convert Markdown content to HTML. + if filename.endswith(('.md', '.mkd', '.mkdn', '.mdown', '.markdown')): + try: + if _test == 'ImportError': + raise ImportError('Error forced by test') + import CommonMark + text = CommonMark.commonmark(text) + except ImportError as e: + log('WARNING: Cannot render Markdown in {}: {}', filename, str(e)) + + content.update({ + 'content': text, + 'summary': truncate(text), + }) + + return content + + +def render(template, **params): + """Replace placeholders in template with values from params.""" + for key, val in params.items(): + template = re.sub(r'{{\s*' + key + '\s*}}', str(val), template) + return template + + +def make_pages(src, dst, layout, **params): + """Generate pages from page content.""" + items = [] + + for src_path in glob.glob(src): + content = read_content(src_path) + items.append(content) + + params.update(content) + + dst_path = render(dst, **params) + output = render(layout, **params) + + log('Rendering {} => {} ...', src_path, dst_path) + fwrite(dst_path, output) + + return sorted(items, key=lambda x: x['date'], reverse=True) + + +def make_list(posts, dst, list_layout, item_layout, **params): + """Generate list page for a blog.""" + items = [] + for post in posts: + item_params = dict(params, **post) + item = render(item_layout, **item_params) + items.append(item) + + params['content'] = ''.join(items) + dst_path = render(dst, **params) + output = render(list_layout, **params) + + log('Rendering list => {} ...', dst_path) + fwrite(dst_path, output) + + +def main(): + # Create a new _site directory from scratch. + if os.path.isdir('_site'): + shutil.rmtree('_site') + shutil.copytree('static', '_site') + + # Default parameters. + params = { + 'base_path': '', + 'subtitle': 'Lorem Ipsum', + 'author': 'Admin', + 'site_url': 'http://localhost:8000', + } + + # If params.json exists, load it. + if os.path.isfile('params.json'): + params.update(json.loads(fread('params.json'))) + + # Load layouts. + page_layout = fread('layout/page.html') + post_layout = fread('layout/post.html') + list_layout = fread('layout/list.html') + item_layout = fread('layout/item.html') + feed_xml = fread('layout/feed.xml') + item_xml = fread('layout/item.xml') + + # Combine layouts to form final layouts. + post_layout = render(page_layout, content=post_layout) + list_layout = render(page_layout, content=list_layout) + + # Create site pages. + make_pages('content/_index.html', '_site/index.html', + page_layout, **params) + make_pages('content/[!_]*.html', '_site/{{ slug }}/index.html', + page_layout, **params) + + # Create blogs. + blog_posts = make_pages('content/blog/*.md', + '_site/blog/{{ slug }}/index.html', + post_layout, blog='blog', **params) + news_posts = make_pages('content/news/*.html', + '_site/news/{{ slug }}/index.html', + post_layout, blog='news', **params) + + # Create blog list pages. + make_list(blog_posts, '_site/blog/index.html', + list_layout, item_layout, blog='blog', title='Blog', **params) + make_list(news_posts, '_site/news/index.html', + list_layout, item_layout, blog='news', title='News', **params) + + make_list(blog_posts, '_site/blog/rss.xml', + feed_xml, item_xml, blog='blog', title='Blog', **params) + make_list(news_posts, '_site/news/rss.xml', + feed_xml, item_xml, blog='news', title='News', **params) + + +# Test parameter to be set temporarily by unit tests. +_test = None + + +if __name__ == '__main__': + main() |