text: #!/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
  import datetime
  
  
  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):
      """Remove tags and truncate text to the specified number of words."""
      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 and save it in a dictionary.
      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))
  
      # Update the dictionary with content text and summary text.
      content.update({
          'content': text,
          'summary': truncate(text),
      })
  
      return content
  
  
  def render(template, **params):
      """Replace placeholders in template with values from params."""
      return re.sub(r'{{\s*([^}\s]+)\s*}}',
                    lambda match: str(params.get(match.group(1), match.group(0))),
                    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',
          'current_year': datetime.datetime.now().year
      }
  
      # 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)
  
      # Create RSS feeds.
      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()
  