summaryrefslogtreecommitdiff
path: root/makesite.py
diff options
context:
space:
mode:
Diffstat (limited to 'makesite.py')
-rwxr-xr-xmakesite.py213
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()