summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSunaina Pai <sunainapai.in@gmail.com>2018-03-17 14:45:21 +0530
committerSunaina Pai <sunainapai.in@gmail.com>2018-03-17 14:45:21 +0530
commit62d0aa159fc046a27bed47e337d787a08f4687d0 (patch)
treec1162f14377890c290d2ad114b3892cbb9848975
Add makesite: A simple static site generator
-rw-r--r--.coveragerc3
-rw-r--r--.gitignore8
-rw-r--r--.travis.yml19
-rw-r--r--LICENSE.md22
-rw-r--r--Makefile73
-rwxr-xr-xREADME.md376
-rw-r--r--content/_index.html28
-rw-r--r--content/about.html23
-rw-r--r--content/blog/2018-01-01-proin-quam.md29
-rw-r--r--content/blog/2018-01-03-sed-finibus.md29
-rw-r--r--content/contact.html21
-rw-r--r--content/news/2018-01-02-vivamus-purus.html30
-rw-r--r--content/news/2018-01-04-mauris-tempor.html37
-rw-r--r--layout/feed.xml12
-rw-r--r--layout/item.html10
-rw-r--r--layout/item.xml13
-rw-r--r--layout/list.html5
-rw-r--r--layout/page.html42
-rw-r--r--layout/post.html5
-rwxr-xr-xmakesite.py213
-rw-r--r--static/css/style.css136
-rw-r--r--test/__init__.py0
-rw-r--r--test/path.py16
-rw-r--r--test/test_content.py114
-rw-r--r--test/test_file_io.py39
-rw-r--r--test/test_headers.py43
-rw-r--r--test/test_list.py46
-rw-r--r--test/test_main.py73
-rw-r--r--test/test_pages.py63
-rw-r--r--test/test_path.py78
-rw-r--r--test/test_render.py25
31 files changed, 1631 insertions, 0 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..4ed6f59
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,3 @@
+[report]
+exclude_lines =
+ if __name__ == '__main__':
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..77cf371
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+_site
+*.pyc
+__pycache__
+.coverage
+htmlcov
+*.sw?
+.DS_Store
+venv
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ef70b91
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,19 @@
+language: python
+
+python:
+ - "2.7"
+ - "3.3"
+ - "3.4"
+ - "3.5"
+ - "3.6"
+
+install:
+ - pip install commonmark coverage coveralls
+
+script:
+ - python -m unittest discover -bv
+ - coverage run --branch --source=. -m unittest discover -bv
+ - coverage report -m
+
+after_success:
+ - coveralls
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..3129e86
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,22 @@
+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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c00a218
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,73 @@
+site:
+ ./makesite.py
+
+serve: site
+ cd _site && python -m SimpleHTTPServer 2> /dev/null || python3 -m http.server
+
+venv2:
+ virtualenv ~/.venv/makesite
+ echo . ~/.venv/makesite/bin/activate > venv
+ . ./venv && pip install commonmark coverage
+
+venv: FORCE
+ python3 -m venv ~/.venv/makesite
+ echo . ~/.venv/makesite/bin/activate > venv
+ . ./venv && pip install commonmark coverage
+
+test: FORCE
+ . ./venv && python -m unittest -bv
+
+coverage:
+ . ./venv && coverage run --branch --source=. -m unittest discover -bv; :
+ . ./venv && coverage report -m
+ . ./venv && coverage html
+
+clean:
+ find . -name "__pycache__" -exec rm -r {} +
+ find . -name "*.pyc" -exec rm {} +
+ rm -rf .coverage htmlcov
+
+REV = cat /tmp/rev.txt
+example:
+ #
+ # Remove existing output directories.
+ rm -rf _site /tmp/_site
+ #
+ # Create params.json for makesite-demo.
+ echo '{ "base_path": "/makesite-demo", "site_url":' \
+ '"https://tmug.github.io/makesite-demo" }' > params.json
+ #
+ # Generate the website.
+ . ./venv && ./makesite.py
+ rm params.json
+ #
+ # Get current commit ID.
+ git rev-parse --short HEAD > /tmp/rev.txt
+ #
+ # Write a README for makesite-demo repository.
+ echo makesite.py demo > _site/README.md
+ echo ================ >> _site/README.md
+ echo This is the HTML/CSS source of an example static >> _site/README.md
+ echo website auto-generated with [sunainapai/makesite][makesite] >> _site/README.md
+ echo "([$$($(REV))][commit])". >> _site/README.md
+ echo >> _site/README.md
+ echo Visit "<https://tmug.github.io/makesite-demo>" to >> _site/README.md
+ echo view the example website. >> _site/README.md
+ echo >> _site/README.md
+ echo [makesite]: https://github.com/sunainapai/makesite >> _site/README.md
+ echo [commit]: https://github.com/sunainapai/makesite/commit/$$($(COMMIT)) >> _site/README.md
+ echo [demo]: https://tmug.github.io/makesite-demo >> _site/README.md
+ #
+ # Publish makesite-demo.
+ mv _site /tmp
+ cd /tmp/_site && git init
+ cd /tmp/_site && git add .
+ cd /tmp/_site && git commit -m "Auto-generated with sunainapai/makesite - $$($(REV))"
+ cd /tmp/_site && git remote add origin https://github.com/tmug/makesite-demo.git
+ cd /tmp/_site && git log
+ cd /tmp/_site && git push -f origin master
+
+loc:
+ grep -vE '^[[:space:]]*#|^[[:space:]]*$$' makesite.py | wc -l
+
+FORCE:
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..0f625a9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,376 @@
+makesite.py
+===========
+Take full control of your static website/blog generation by writing your
+own simple, lightweight, and magic-free static site generator in
+Python. That's right! Reinvent the wheel, fellas!
+
+[![View Source][SOURCE-BADGE]](makesite.py)
+[![View Demo][DEMO-BADGE]](https://tmug.github.io/makesite-demo)
+[![MIT License][LICENSE-BADGE]](LICENSE.md)
+
+[SOURCE-BADGE]: https://img.shields.io/badge/view-source-brightgreen.svg
+[DEMO-BADGE]: https://img.shields.io/badge/view-demo-brightgreen.svg
+[LICENSE-BADGE]: https://img.shields.io/badge/license-MIT-blue.svg
+
+
+Contents
+--------
+* [Introduction](#introduction)
+* [But Why?](#but-why)
+* [Get Started](#get-started)
+* [The Code](#the-code)
+* [Layout](#layout)
+* [Content](#content)
+* [Credits](#credits)
+* [License](#license)
+* [Support](#support)
+
+
+Introduction
+------------
+This repository contains the source code of an example website
+containing two static blogs and a few static pages. The website can be
+generated by running [makesite.py](makesite.py). The output looks like
+[this](https://tmug.github.io/makesite-demo). That's it!
+
+So go ahead, fork this repository, replace the [content](content) with
+your own, and generate your static website. It's that simple!
+
+You are [free](LICENSE.md) to copy, use, and modify this project for
+your blog or website, so go ahead and fork this repository and make it
+your own project. Change the [layout](layout) if you wish to, improve
+the [stylesheet](static/css/style.css) to suit your taste, enhance
+[makesite.py](makesite.py) if you need to, and develop your website/blog
+just the way you want it.
+
+
+But Why?
+--------
+For fun and profit! Okay, maybe not for profit, but hopefully for fun.
+
+Have you used a popular static site generator like Jekyll to generate
+your blog? I have too. It is simple and great. But then did you yearn
+to use something even simpler to generate your blog? Do you like Python?
+Perhaps the thought of writing your own static site generator crossed
+your mind but you thought it would be too much work? If you answered
+"yes" to these questions, then this project is for you.
+
+With this project, you are in full control. There is no hidden magic!
+Everything is laid out in [makesite.py](makesite.py) as plain and simple
+Python code. It is just 125 lines of code (excluding blank lines
+and comments). It gets you off the ground pretty quickly. You can have a
+decent website/blog generated within a few minutes and then you can
+begin tinkering with the [source code](makesite.py), the
+[layout](layout), and the [css](static/css/style.css) to customize the
+look and feel of your website to your satisfaction.
+
+
+Get Started
+-----------
+This section provides some quick steps to get you off the ground as
+quickly as possible.
+
+ 1. For a quick demo on your local system, just enter this command:
+
+ make serve
+
+ If you don't have `make` but have Python 3.x, enter this command:
+
+ python3 makesite.py
+ cd _site
+ python3 -m http.server
+
+ Note: In some environments, you may need to use `python` instead of
+ `python3` to invoke Python 3.x.
+
+ If you only have Python 2.7, enter this command:
+
+ python makesite.py
+ cd _site
+ python -m SimpleHTTPServer
+
+ Then visit http://localhost:8000/. It should look like
+ [this](https://tmug.github.io/makesite-demo).
+
+ Note: You can run [makesite.py](makesite.py) with Python 2.7 or
+ Python 3.x.
+
+ 2. You may see a few `Cannot render Markdown` warning messages in the
+ output of the previous command. This is due to the fact that an
+ example [blog](content/blog) in this project has a few posts written
+ in Markdown. To render them correctly, install the `commonmark`
+ package with this command:
+
+ pip install commonmark
+
+ Then try the previous step again.
+
+ 3. For an Internet-facing website, you would be hosting the static
+ website/blog on a hosting service and/or with a web server such as
+ Apache HTTP Server, Nginx, etc. You probably only need to generate
+ the static files and know where the static files are and move them
+ to your hosting location.
+
+ If you have the `make` command, enter this command to generate your
+ website:
+
+ make site
+
+ If you don't have `make` but have `python3`, enter this command:
+
+ python3 makesite.py
+
+ Note: In some environments, you may need to use `python` instead of
+ `python3` to invoke Python 3.x.
+
+ If you only have `python`, enter this command:
+
+ python makesite.py
+
+ The `_site` directory contains the entire generated website. The
+ content of this directory may be copied to your website hosting
+ location.
+
+
+The Code
+--------
+Now that you know how to generate the static website that comes with
+this project, it is time to see what [makesite.py](makesite.py) does.
+You probably don't really need to read the entire section. The source
+code is pretty self-explanatory but just in case, you need a detailed
+overview of what it does, here are the details:
+
+ 1. The `main()` function is the starting point of website generation.
+ It calls the other functions necessary to get the website generation
+ done.
+
+ 2. First it creates a fresh new `_site` directory from scratch. All
+ files in the [static directory](static) are copied to this
+ directory. Later the static website is generated and written to this
+ directory.
+
+ 3. Then it creates a `params` dictionary with some default parameters.
+ This dictionary is passed around to other functions. These other
+ functions would pick values from this dictionary to populate
+ placeholders in the layout template files.
+
+ Let us take the `subtitle` parameter for example. It is set
+ to our example website's fictitious brand name: "Lorem Ipsum". We
+ want each page to include this brand name as a suffix in the title.
+ For example, the [about page](https://tmug.github.io/makesite-demo/about/)
+ has "About - Lorem Ipsum" in its title. Now take a look at the
+ [page layout template](layout/page.html) that is used as the layout
+ for all pages in the static website. This layout file uses the
+ `{{ subtitle }}` syntax to denote that it is a placeholder that
+ should be populated while rendering the template.
+
+ Another interesting thing to note is that a content file can
+ override these parameters by defining its own parameters in the
+ content header. For example, take a look at the content file for
+ the [home page](content/_index.html). In its content header, i.e.,
+ the HTML comments at the top with key-value pairs, it defines a new
+ parameter named `title` and overrides the `subtitle` parameter.
+
+ We will discuss the syntax for placeholders and content headers
+ later. It is quite simple.
+
+ 4. It then loads all the layout templates. There are 6 of them in this
+ project.
+
+ - [layout/page.html](layout/page.html): It contains the base
+ template that applies to all pages. It begins with
+ `<!DOCTYPE html>` and `<html>`, and ends with `</html>`. The
+ `{{ content }}` placeholder in this template is replaced with
+ the actual content of the page. For example, for the about page,
+ the `{{ content }}` placeholder is replaced with the the entire
+ content from [content/about.html](content/about.html). This is
+ done with the `make_pages()` calls further down in the code.
+
+ - [layout/post.html](layout/post.html): It contains the template
+ for the blog posts. Note that it does not begin with `<!DOCTYPE
+ html>` and does not contain the `<html>` and `</html>` tags.
+ This is not a complete standalone template. This template
+ defines only a small portion of the blog post pages that are
+ specific to blog posts. It contains the HTML code and the
+ placeholders to display the title, publication date, and author
+ of blog posts.
+
+ This template must be combined with the
+ [page layout template](layout/page.html) to create the final
+ standalone template. To do so, we replace the `{{ content }}`
+ placeholder in the [page layout template](layout/page.html) with
+ the HTML code in the [post layout template](layout/post.html) to
+ get a final standalone template. This is done with the
+ `render()` calls further down in the code.
+
+ The resulting standalone template still has a `{{ content }}`
+ placeholder from the [post layout template](layout/post.html)
+ template. This `{{ content }}` placeholder is then replaced
+ with the actual content from the [blog posts](content/blog).
+
+ - [layout/list.html](layout/list.html): It contains the template
+ for the blog listing page, the page that lists all the posts in
+ a blog in reverse chronological order. This template does not do
+ much except provide a title at the top and an RSS link at the
+ bottom. The `{{ content }}` placeholder is populated with the
+ list of blog posts in reverse chronological order.
+
+ Just like the [post layout template](layout/post.html) , this
+ template must be combined with the
+ [page layout template](layout/page.html) to arrive at the final
+ standalone template.
+
+ - [layout/item.html](layout/item.html): It contains the template
+ for each blog post item in the blog listing page. The
+ `make_list()` function renders each blog post item with this
+ template and inserts them into the
+ [list layout template](layout/list.html) to create the blog
+ listing page.
+
+ - [layout/feed.xml](layout/feed.xml): It contains the XML template
+ for RSS feeds. The `{{ content }}` placeholder is populated with
+ the list of feed items.
+
+ - [layout/item.xml](layout/item.xml): It contains the XML template for
+ each blog post item to be included in the RSS feed. The
+ `make_list()` function renders each blog post item with this
+ template and inserts them into the
+ [layout/feed.xml](layout/feed.xml) template to create the
+ complete RSS feed.
+
+ 5. After loading all the layout templates, it makes a `render()` call
+ to combine the [post layout template](layout/post.html) with the
+ [page layout template](layout/page.html) to form the final
+ standalone post template.
+
+ Similarly, it combines the [list layout template](layout/list.html)
+ template with the [page layout template](layout/page.html) to form
+ the final list template.
+
+ 6. Then it makes two `make_pages()` calls to render the home page and a
+ couple of other site pages: the [contact page](content/contact.html)
+ and the [about page](content/about.html).
+
+ 7. Then it makes two more `make_pages()` calls to render two blogs: one
+ that is named simply [blog](content/blog) and another that is named
+ [news](content/news).
+
+ Note that the `make_pages()` call accepts three positional
+ arguments:
+
+ - Path to content source files provided as a glob pattern.
+ - Output path template as a string.
+ - Layout template code as a string.
+
+ These three positional arguments are then followed by keyword
+ arguments. These keyword arguments are used as template parameters
+ in the output path template and the layout template to replace the
+ placeholders with their corresponding values.
+
+ As described in point 2 above, a content file can override these
+ parameters in its content header.
+
+ 8. Then it makes two `make_list()` calls to render the blog listing
+ pages for the two blogs. These calls are very similar to the
+ `make_pages()` calls. There are only two things that are different
+ about the `make_list()` calls:
+
+ - There is no point in reading the same blog posts again that were
+ read by `make_pages()`, so instead of passing the path to
+ content source files, we feed a chronologically reverse-sorted
+ index of blog posts returned by `make_pages()` to `make_list()`.
+ - There is an additional argument to pass the
+ [item layout template](layout/item.html) as a string.
+
+ 9. Finally it makes two more `make_list()` calls to generate the RSS
+ feeds for the two blogs. There is nothing different about these
+ calls than the previous ones except that we use the feed XML
+ templates here to generate RSS feeds.
+
+To recap quickly, we create a `_site` directory to write the static site
+generated, define some default parameters, load all the layout
+templates, and then call `make_pages()` to render pages and blog posts
+with these templates, call `make_list()` to render blog listing pages
+and RSS feeds. That's all!
+
+Take a look at how the `make_pages()` and `make_list()` functions are
+implemented. They are very simple with less than 20 lines of code each.
+Once you are comfortable with this code, you can begin modifying it to
+add more blogs or reduce them. For example, you probably don't need a
+news blog, so you may delete the `make_pages()` and `make_list()` calls
+for `'news'` along with its content at [content/news](content/news).
+
+
+Layout
+------
+In this project, the layout template files are located in the [layout
+directory](layout). But they don't necessarily have to be there. You can
+place the layout files wherever you want and update
+[makesite.py](makesite.py) accordingly.
+
+The source code of [makesite.py](makesite.py) that comes with this
+project understands the notion of placeholders in the layout templates.
+The template placeholders have the following syntax:
+
+ {{ <key> }}
+
+Any whitespace before `{{`, around `<key>`, and after `}}` is ignored.
+The `<key>` should be a valid Python identifier. Here is an example of
+template placeholder:
+
+ {{ title }}
+
+This is a very simple template mechanism that is implemented already in
+the [makesite.py](makesite.py). For a simple website or blog, this
+should be sufficient. If you need a more sophisticated template engine
+such as [Jinja2](http://jinja.pocoo.org/) or
+[Cheetah](https://pythonhosted.org/Cheetah/), you need to modify
+[makesite.py](makesite.py) to add support for it.
+
+
+Content
+-------
+In this project, the content files are located in the [content
+directory](content). Most of the content files are written in HTML.
+However, the content files for the blog named [blog](content/blog) are
+written in Markdown.
+
+The notion of headers in the content files is supported by
+[makesite.py](makesite.py). Each content file may begin with one or more
+consecutive HTML comments that contain headers. Each header has the
+following syntax:
+
+ <!-- <key>: <value> -->
+
+Any whitespace before, after, and around the `<!--`, `<key>`, `:`,
+`<value>`, and `-->` tokens are ignored. Here are some example headers:
+
+ <!-- title: About -->
+ <!-- subtitle: Lorem Ipsum -->
+ <!-- author: Admin -->
+
+It looks for the headers at the top of every content file. As soon as
+some non-header text is encountered, the rest of the content from that
+point is not checked for headers.
+
+
+Credits
+-------
+Thanks to [Susam](https://github.com/susam) for writing the
+documentation and the unit tests.
+
+
+License
+-------
+This is free and open source software. You can use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell copies of it,
+under the terms of the [MIT License](LICENSE.md).
+
+This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
+express or implied. See the [MIT License](LICENSE.md) for details.
+
+
+Support
+-------
+To report bugs, suggest improvements, or ask questions, please visit
+<https://github.com/sunainapai/makesite/issues>.
diff --git a/content/_index.html b/content/_index.html
new file mode 100644
index 0000000..8cc489a
--- /dev/null
+++ b/content/_index.html
@@ -0,0 +1,28 @@
+<!-- title: Lorem Ipsum -->
+<!-- subtitle: Dolor Sit -->
+<h1>Lorem Ipsum</h1>
+<p>
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc nibh
+tellus, vehicula ut maximus sed, fringilla a justo. Nunc vitae efficitur
+nisl. Ut sapien erat, pretium et commodo nec, rutrum bibendum magna. Sed
+ut massa massa. Etiam euismod neque lacus, id tincidunt risus iaculis a.
+Aliquam porta venenatis bibendum. Nam id varius nulla. Sed vitae purus
+ac odio ornare vestibulum vitae tempor arcu. Nunc non venenatis purus.
+Duis a augue at nulla congue egestas. Morbi mattis felis sit amet tortor
+euismod, fringilla viverra est elementum.
+</p>
+<p>
+Suspendisse vestibulum sed massa eu tincidunt. Pellentesque cursus, nisl
+at gravida suscipit, odio quam placerat mi, in iaculis nunc risus eu
+sapien. Suspendisse potenti. Nullam fermentum, tellus vel faucibus
+dictum, arcu ante rutrum nisi, ut iaculis eros felis a lectus. Etiam vel
+maximus nulla, sed mattis lectus. Aliquam commodo est massa, at
+vestibulum diam commodo vehicula. Nullam et tristique tortor. Praesent
+luctus, leo id mattis mattis, ex dui dapibus dolor, nec ultrices turpis
+nibh in sem. In efficitur, velit ut bibendum interdum, libero turpis
+mattis odio, non pharetra metus leo nec arcu. Maecenas auctor laoreet
+maximus. Donec metus massa, scelerisque a lacinia et, viverra eget
+metus. Aenean vitae tellus vehicula, mattis metus in, facilisis purus.
+In purus erat, fringilla ut diam et, convallis convallis nunc. Morbi
+sagittis interdum ipsum sit amet fringilla.
+</p>
diff --git a/content/about.html b/content/about.html
new file mode 100644
index 0000000..f51d17d
--- /dev/null
+++ b/content/about.html
@@ -0,0 +1,23 @@
+<!-- title: About -->
+<h1>About</h1>
+<p>
+Quisque quam nisl, egestas nec convallis vitae, fringilla nec mauris.
+Sed et cursus lacus, a pharetra ex. Pellentesque rhoncus malesuada elit
+at sodales. In ut elit lectus. Phasellus et hendrerit odio, ac hendrerit
+ante. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sem nibh, auctor vel dictum
+eu, pharetra sit amet nunc. Integer suscipit suscipit dapibus.
+Suspendisse vulputate sed mauris eget tempus. Etiam rhoncus, leo nec
+cursus elementum, massa lorem fermentum nisi, non convallis nisl dolor
+vel ipsum.
+</p>
+<p>
+Aliquam imperdiet vel purus sed facilisis. Mauris condimentum vel nulla
+ac tempor. In non venenatis arcu. Nam in sapien purus. Suspendisse
+faucibus, erat et fringilla vestibulum, ligula nisi porta odio, ut
+tristique dui ante eu nisi. Mauris vitae vulputate lorem. Proin tortor
+nisl, vehicula sed justo sed, volutpat bibendum purus. Phasellus luctus
+fringilla augue ac sodales. Aenean ac nisi sit amet neque pulvinar
+tincidunt ut nec ipsum. Aliquam purus tellus, dignissim a augue
+placerat, aliquet semper turpis. Fusce id lacinia quam, vel porta quam.
+</p>
diff --git a/content/blog/2018-01-01-proin-quam.md b/content/blog/2018-01-01-proin-quam.md
new file mode 100644
index 0000000..540a467
--- /dev/null
+++ b/content/blog/2018-01-01-proin-quam.md
@@ -0,0 +1,29 @@
+<!-- title: Proin Quam -->
+Proin quam urna, pulvinar id ipsum ac, mattis consectetur ante. Praesent
+non justo lectus. Duis egestas arcu libero, quis laoreet dolor volutpat
+ut. Donec facilisis orci sit amet sem blandit elementum. Vestibulum
+suscipit consectetur diam, ac posuere metus condimentum in. Integer
+vehicula vitae enim id gravida. Vestibulum ut eros vitae risus porttitor
+porta in eget felis. Nulla lorem erat, mattis eget lacus eget, interdum
+aliquet lectus. Fusce non felis diam. Mauris sagittis porttitor est et
+vestibulum. Duis faucibus commodo est. Maecenas elit purus, auctor a
+consectetur eu, suscipit nec metus. Nam gravida id massa quis faucibus.
+Sed non consectetur eros. Nullam iaculis sit amet ex eget ultrices. Sed
+ligula arcu, vehicula vel ipsum nec, ultrices pulvinar ante.
+
+Vivamus egestas justo sed nulla condimentum iaculis. Pellentesque
+eleifend elementum turpis sed tempus. Aliquam erat volutpat. In hac
+habitasse platea dictumst. Integer elementum sed diam at vulputate.
+Donec maximus, lacus a vulputate sagittis, felis turpis vestibulum
+massa, nec tincidunt libero felis eget arcu. Morbi eget velit vulputate,
+pellentesque odio laoreet, consequat orci. Aliquam erat volutpat. Nulla
+a vulputate mi, sed laoreet ipsum. Nulla nunc ipsum, ultricies at
+suscipit id, consectetur id erat. Suspendisse scelerisque vehicula felis
+quis luctus.
+
+Donec posuere ante a nibh dictum suscipit. Mauris interdum dolor nulla,
+sit amet fermentum quam porta a. Ut metus ipsum, venenatis a lorem
+pellentesque, finibus commodo turpis. Cras mollis dui quis varius
+placerat. Phasellus nec nisi nec quam tincidunt luctus. Sed nec
+vulputate enim, vel dapibus enim. Praesent mollis maximus enim in
+tempus. Phasellus a arcu lorem. Sed dictum rhoncus tempus.
diff --git a/content/blog/2018-01-03-sed-finibus.md b/content/blog/2018-01-03-sed-finibus.md
new file mode 100644
index 0000000..783129c
--- /dev/null
+++ b/content/blog/2018-01-03-sed-finibus.md
@@ -0,0 +1,29 @@
+<!-- title: Sed Finibus -->
+Sed finibus fermentum convallis. Sed consequat, lacus a pellentesque
+suscipit, lorem libero egestas dui, sit amet volutpat mi dolor sit amet
+nisl. Maecenas faucibus iaculis nibh id gravida. Aenean ac nulla
+efficitur, pulvinar massa eget, viverra enim. Ut vulputate velit nisl,
+id volutpat nibh rhoncus a. Phasellus pretium finibus tempus.
+Suspendisse placerat nibh sem, sit amet vehicula urna porta quis.
+Suspendisse ac nisi maximus, porttitor sem id, dignissim ipsum. Sed
+suscipit dolor sed velit viverra, quis viverra dui placerat. Vestibulum
+dapibus molestie dapibus.
+
+Praesent fringilla dapibus enim quis consectetur. Donec eget nibh nisl.
+Proin venenatis interdum nunc, sed venenatis orci suscipit in. Morbi sed
+lacinia tellus, iaculis condimentum tellus. Pellentesque habitant morbi
+tristique senectus et netus et malesuada fames ac turpis egestas.
+Maecenas sem mi, commodo ut facilisis pharetra, aliquam id nisi. Morbi
+nec nisi sed tortor faucibus pretium nec non eros. Nullam rhoncus
+pharetra elementum. Ut tincidunt fermentum metus, at fermentum mi
+scelerisque id. Aenean sed odio auctor, placerat velit sed, consectetur
+felis. Donec volutpat id lorem non ornare. Phasellus convallis mi magna,
+vel laoreet ex efficitur fermentum.
+
+Phasellus a interdum odio, vitae finibus leo. Pellentesque porta quis
+massa non suscipit. Mauris finibus vel nibh quis scelerisque.
+Suspendisse eget molestie lacus, eu mattis felis. Proin ex tellus,
+ultrices eu facilisis vel, faucibus eget enim. Integer sit amet magna
+ligula. Ut massa nisl, sodales vel eros ac, dignissim maximus metus.
+Donec odio nulla, sollicitudin quis dolor in, varius sodales lectus. Sed
+non lacinia ligula, eu pulvinar eros.
diff --git a/content/contact.html b/content/contact.html
new file mode 100644
index 0000000..f868ad9
--- /dev/null
+++ b/content/contact.html
@@ -0,0 +1,21 @@
+<!-- title: Contact -->
+<h1>Contact</h1>
+<p>
+In hac habitasse platea dictumst. Suspendisse purus leo, laoreet ac
+scelerisque vitae, gravida vitae turpis. Etiam lacinia justo in pharetra
+tincidunt. Donec id mi in elit euismod feugiat. Fusce eget velit nec
+nunc fermentum ultrices ut auctor tellus. Suspendisse convallis lacus a
+mollis volutpat. Donec maximus eros lorem, non faucibus sapien tristique
+a. Proin ut magna eget nunc sagittis sodales ac suscipit dolor.
+Vestibulum sit amet velit nunc. Nam euismod fermentum neque ac
+facilisis. Phasellus imperdiet arcu a lorem pulvinar accumsan. Sed
+maximus neque tristique, sollicitudin risus sed, interdum enim.
+</p>
+<p>
+Curabitur vel augue mattis, blandit libero rhoncus, fringilla augue.
+Aenean condimentum ex justo. In hac habitasse platea dictumst. Etiam
+ullamcorper finibus enim, nec cursus dui tristique nec. Phasellus et
+tortor libero. Vivamus viverra euismod pulvinar. Fusce maximus, ante
+quis lobortis facilisis, lectus mi consequat purus, sed vestibulum ipsum
+mi sit amet dui.
+</p>
diff --git a/content/news/2018-01-02-vivamus-purus.html b/content/news/2018-01-02-vivamus-purus.html
new file mode 100644
index 0000000..42845b1
--- /dev/null
+++ b/content/news/2018-01-02-vivamus-purus.html
@@ -0,0 +1,30 @@
+<!-- title: Vivamus Purus -->
+<p>
+Vivamus purus tellus, facilisis in sapien quis, ullamcorper lacinia
+neque. Morbi tincidunt ac leo sit amet auctor. Donec dolor mauris,
+lobortis eget faucibus sit amet, egestas non ante. Sed quam erat,
+consectetur eget magna in, pretium aliquet ligula. Proin id nunc ex.
+Curabitur posuere lectus neque, ut blandit diam scelerisque sed. In
+semper lacinia ipsum a malesuada. Nam accumsan consequat sem, eu mollis
+diam iaculis commodo.
+</p>
+<p>
+Fusce arcu turpis, blandit lacinia augue et, dignissim imperdiet lorem.
+Donec fermentum dui eu fermentum accumsan. Nunc porta tellus sit amet
+nulla tempor varius. Ut id dolor velit. Aenean et dolor ac nisl mattis
+iaculis. Phasellus at convallis lectus. Curabitur volutpat purus sed
+purus feugiat, in elementum orci ullamcorper. Nam sollicitudin pretium
+lacus, quis convallis massa suscipit et. Fusce ac elit tristique,
+efficitur est at, pellentesque tellus.
+</p>
+<p>
+Suspendisse vel eleifend nunc. Vivamus ac sem luctus, luctus velit ac,
+pretium urna. Sed eu diam quis odio euismod sollicitudin. Ut faucibus
+pharetra sem, ut malesuada sapien maximus at. Quisque et eleifend augue.
+Sed dignissim urna ac justo venenatis vestibulum. Vivamus sodales, dui
+sed luctus porttitor, ante purus pretium velit, at dapibus enim felis a
+tortor. Fusce sit amet ipsum odio. Etiam eget felis sed risus efficitur
+rutrum at a libero. Praesent finibus pellentesque tellus sed maximus.
+Morbi sit amet turpis eros. Vivamus rhoncus libero ut lacinia luctus. Ut
+nec ex dui.
+</p>
diff --git a/content/news/2018-01-04-mauris-tempor.html b/content/news/2018-01-04-mauris-tempor.html
new file mode 100644
index 0000000..b7dd769
--- /dev/null
+++ b/content/news/2018-01-04-mauris-tempor.html
@@ -0,0 +1,37 @@
+<!-- title: Mauris Tempor -->
+<p>
+Mauris tempor nulla odio, vel tempus metus bibendum sit amet. Vivamus
+consequat fringilla tristique. Nullam a congue ligula, eget rutrum
+lectus. In pulvinar at nunc ac finibus. Sed cursus consequat sem et
+egestas. Cras eu eros vel mi eleifend efficitur eu dapibus orci.
+Phasellus non bibendum purus. Nam facilisis laoreet massa tempus
+vehicula. Donec a hendrerit nisi.
+</p>
+<p>
+Vestibulum blandit dui a lorem pulvinar, et auctor eros elementum.
+Vestibulum tortor ex, pharetra id metus vel, tincidunt faucibus nisl.
+Suspendisse potenti. Nam maximus est mi, sit amet tristique nisi pretium
+sed. Maecenas in nulla at nibh volutpat lacinia. Aliquam erat volutpat.
+Donec odio magna, imperdiet quis risus at, ultricies faucibus lorem.
+Quisque tempus enim purus. Integer interdum mauris lorem, in accumsan
+lorem tempor quis. Praesent facilisis, velit vitae aliquam mattis,
+turpis justo malesuada odio, nec hendrerit tortor sapien sit amet diam.
+Praesent aliquam malesuada feugiat. Proin vitae efficitur massa.
+Vestibulum quam enim, finibus et magna ac, pellentesque aliquet leo.
+Nullam condimentum neque nec ex viverra, sit amet ullamcorper lectus
+sodales. Suspendisse non est et velit malesuada ullamcorper maximus
+porta quam.
+</p>
+<p>
+Cras ornare maximus augue nec varius. Aliquam tincidunt sodales ipsum.
+Sed viverra ut nibh sit amet porta. Cras sit amet condimentum est.
+Suspendisse quis tristique ipsum. Etiam vel lorem bibendum, laoreet dui
+vitae, rutrum leo. Nullam cursus dui a augue lobortis euismod.
+Suspendisse finibus id neque ut imperdiet. Pellentesque vel suscipit
+est, vel vulputate neque. Curabitur eu eros auctor, laoreet nunc quis,
+laoreet erat. Fusce rhoncus mattis lorem, et imperdiet nisi lacinia sit
+amet. Integer et cursus lacus. Nullam id arcu et libero condimentum
+tristique. Pellentesque arcu magna, aliquam sed lectus in, tempor mattis
+erat. Curabitur vel massa sit amet nunc suscipit pulvinar. In tincidunt
+diam metus, eu sollicitudin velit commodo a.
+</p>
diff --git a/layout/feed.xml b/layout/feed.xml
new file mode 100644
index 0000000..388c39e
--- /dev/null
+++ b/layout/feed.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0">
+
+<channel>
+<title>{{ title }}</title>
+<link>{{ site_url }}/</link>
+<description>Grow with Technology</description>
+
+{{ content }}
+
+</channel>
+</rss>
diff --git a/layout/item.html b/layout/item.html
new file mode 100644
index 0000000..b16a097
--- /dev/null
+++ b/layout/item.html
@@ -0,0 +1,10 @@
+<article>
+<h2><a href="{{ base_path }}/{{ blog }}/{{ slug }}/">{{ title }}</a></h2>
+<p class="meta">Published on {{ date }} by <b>{{ author }}</b></p>
+<p class="summary">
+{{ summary }}&nbsp;<a class="more" href="{{ base_path }}/blog/{{ slug }}/">...</a>
+</p>
+<div>
+<a class="more" href="{{ base_path }}/{{ blog }}/{{ slug }}/">Read More</a>
+</div>
+</article>
diff --git a/layout/item.xml b/layout/item.xml
new file mode 100644
index 0000000..65b9679
--- /dev/null
+++ b/layout/item.xml
@@ -0,0 +1,13 @@
+<item>
+<title>{{ title }}</title>
+<link>{{ site_url }}/{{ blog }}/{{ slug }}/</link>
+<description>
+<![CDATA[
+<p>
+{{ summary }}&nbsp;<a href="{{ site_url }}/{{ blog }}/{{ slug }}/">...</a>
+</p>
+<p><a href="{{ site_url }}/{{ blog }}/{{ slug }}/">Read More</a></p>
+]]>
+</description>
+<pubDate>{{ date }}</pubDate>
+</item>
diff --git a/layout/list.html b/layout/list.html
new file mode 100644
index 0000000..79a6aab
--- /dev/null
+++ b/layout/list.html
@@ -0,0 +1,5 @@
+<h1>{{ title }}</h1>
+{{ content }}
+<section>
+<a class="rss" href="{{ base_path }}/{{ blog }}/rss.xml">RSS</a>
+</section>
diff --git a/layout/page.html b/layout/page.html
new file mode 100644
index 0000000..16482e1
--- /dev/null
+++ b/layout/page.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>{{ title }} - {{ subtitle }}</title>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width">
+ <link rel="stylesheet" type="text/css" href="{{ base_path }}/css/style.css">
+</head>
+
+<body id="{{ slug }}">
+
+<nav>
+<section>
+ <span class="home">
+ <a href="{{ base_path }}/">Home</a>
+ </span>
+ <span class="links">
+ <a href="{{ base_path }}/blog/">Blog</a>
+ <a href="{{ base_path }}/news/">News</a>
+ <a href="{{ base_path }}/contact/">Contact</a>
+ <a href="{{ base_path }}/about/">About</a>
+ </span>
+</section>
+</nav>
+
+<main>
+{{ content }}
+</main>
+
+<footer>
+<section>
+<p>&copy; 2018 Lorem Ipsum</p>
+<p>
+ <a href="https://twitter.com/sunainapai">Twitter</a>
+ <a href="https://github.com/sunainapai">GitHub</a>
+ <a href="https://www.example.com/">Example</a>
+</p>
+</section>
+</footer>
+
+</body>
+</html>
diff --git a/layout/post.html b/layout/post.html
new file mode 100644
index 0000000..f0a42be
--- /dev/null
+++ b/layout/post.html
@@ -0,0 +1,5 @@
+<article>
+<h1><a href="{{ base_path }}/{{ blog }}/{{ slug }}/">{{ title }}</a></h1>
+<p class="meta">Published on {{ date }} by <b>{{ author }}</b></p>
+{{ content }}
+</article>
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()
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..b87bbe8
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,136 @@
+/* General */
+body {
+ margin: 0;
+ line-height: 1.5em;
+ color: #333333;
+ font-family: helvetica, arial, sans-serif;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: 0;
+ line-height: 1.2em;
+ margin-top: 1em;
+}
+
+a:link, a:visited {
+ color: #0000e0;
+ text-decoration: none
+}
+
+a:hover, a:active {
+ color: #0000ff;
+ text-decoration: underline
+}
+
+
+/* Single column layout */
+nav section, header section, main, footer section {
+ max-width: 40em;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 0 0.5em;
+}
+
+
+/* Navigation panel */
+nav {
+ background: #333333;
+ border-bottom: thin solid #111111;
+ line-height: 3em;
+}
+
+nav a {
+ margin-right: 1em;
+}
+
+nav a:link, nav a:visited {
+ color: #cccccc;
+}
+
+nav a:hover, nav a:active {
+ color: #ffffff;
+ text-decoration: none;
+}
+
+@media screen and (min-width: 600px) {
+ nav .links {
+ float: right;
+ }
+}
+
+
+/* Footer */
+footer {
+ background: #f0f0f0;
+ border-top: thin solid #e0e0e0;
+ margin-top: 2em;
+ padding: 1em 0;
+ box-shadow: 0 100vh 0 100vh #f0f0f0;
+ text-align: center;
+}
+
+footer section p {
+ font-size: 0.8em;
+}
+
+footer a {
+ margin-right: 1em;
+}
+
+
+/* Posts */
+article {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+article h2 a:link, article h2 a:visited,
+article h1 a:link, article h1 a:visited,
+.more:link, .more:visited {
+ color: #333333;
+}
+
+article h2 a:hover, article h2 a:active,
+article h1 a:hover, article h1 a:active,
+.more:hover, .more:active {
+ color: #777777;
+}
+
+p.meta {
+ margin-top: 0;
+ font-size: 0.8em;
+ color: #777777;
+ font-style: italic;
+}
+
+p.summary {
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
+}
+
+div .more {
+ margin-top: 0;
+ font-weight: bold;
+}
+
+
+/* RSS */
+.rss {
+ padding: 0.3em 0.35em;
+ border-radius: 0.25em;
+ font-size: 0.75em;
+ font-weight: bold;
+}
+
+.rss:link, .rss:visited, .rss:hover, .rss:active {
+ color: #ffffff;
+ text-decoration: none;
+}
+
+.rss:link, .rss:visited {
+ background: #ff6600;
+}
+
+.rss:hover, .rss:active {
+ background: #ff8822;
+}
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/__init__.py
diff --git a/test/path.py b/test/path.py
new file mode 100644
index 0000000..38c0991
--- /dev/null
+++ b/test/path.py
@@ -0,0 +1,16 @@
+import os
+import tempfile
+import shutil
+
+
+def temppath(*paths):
+ return os.path.join(tempfile.gettempdir(), *paths)
+
+
+def move(src, dst):
+ if os.path.isfile(dst):
+ os.remove(dst)
+ elif os.path.isdir(dst):
+ shutil.rmtree(dst)
+ if os.path.exists(src):
+ os.rename(src, dst)
diff --git a/test/test_content.py b/test/test_content.py
new file mode 100644
index 0000000..06ec97d
--- /dev/null
+++ b/test/test_content.py
@@ -0,0 +1,114 @@
+import unittest
+import shutil
+import os
+
+import makesite
+from test import path
+
+
+class ContentTest(unittest.TestCase):
+ def setUp(self):
+ self.blog_path = path.temppath('blog')
+ self.undated_path = os.path.join(self.blog_path, 'foo.txt')
+ self.dated_path = os.path.join(self.blog_path, '2018-01-01-foo.txt')
+ self.long_post_path = os.path.join(self.blog_path, 'bar.txt')
+ self.normal_post_path = os.path.join(self.blog_path, 'baz.txt')
+ self.md_post_path = os.path.join(self.blog_path, 'qux.md')
+ self.no_md_post_path = os.path.join(self.blog_path, 'qux.txt')
+
+ os.makedirs(self.blog_path)
+
+ with open(self.undated_path, 'w') as f:
+ f.write('hello world')
+
+ with open(self.dated_path, 'w') as f:
+ f.write('hello world')
+
+ with open(self.long_post_path, 'w') as f:
+ self.long_text = ' \n'.join('word' + str(i) for i in range(50))
+ f.write(self.long_text)
+
+ with open(self.normal_post_path, 'w') as f:
+ f.write('<!-- a: 1 -->\n<!-- b: 2 -->\nFoo')
+
+ with open(self.md_post_path, 'w') as f:
+ f.write('*Foo*')
+
+ with open(self.no_md_post_path, 'w') as f:
+ f.write('*Foo*')
+
+ def tearDown(self):
+ shutil.rmtree(self.blog_path)
+
+ # Rudimentary mock because unittest.mock is unavailable in Python 2.7.
+ def mock(self, *args):
+ self.mock_args = args
+
+ def test_content_content(self):
+ content = makesite.read_content(self.long_post_path)
+ self.assertEqual(content['content'], self.long_text)
+
+ def test_content_summary(self):
+ content = makesite.read_content(self.long_post_path)
+ expected_text = ' '.join('word' + str(i) for i in range(25))
+ self.assertEqual(content['summary'], expected_text)
+
+ def test_content_date(self):
+ content = makesite.read_content(self.dated_path)
+ self.assertEqual(content['date'], '2018-01-01')
+
+ def test_content_date_missing(self):
+ content = makesite.read_content(self.undated_path)
+ self.assertEqual(content['date'], '1970-01-01')
+
+ def test_content_slug_dated(self):
+ content = makesite.read_content(self.dated_path)
+ self.assertEqual(content['slug'], 'foo')
+
+ def test_content_slug_undated(self):
+ content = makesite.read_content(self.undated_path)
+ self.assertEqual(content['slug'], 'foo')
+
+ def test_content_headers(self):
+ content = makesite.read_content(self.normal_post_path)
+ self.assertEqual(content['a'], '1')
+ self.assertEqual(content['b'], '2')
+ self.assertEqual(content['content'], 'Foo')
+
+ def test_markdown_rendering(self):
+ content = makesite.read_content(self.md_post_path)
+ self.assertEqual(content['content'], '<p><em>Foo</em></p>\n')
+
+ def test_markdown_import_error(self):
+ makesite._test = 'ImportError'
+ original_log = makesite.log
+
+ makesite.log = self.mock
+ self.mock_args = None
+ content = makesite.read_content(self.md_post_path)
+
+ makesite._test = None
+ makesite.log = original_log
+
+ self.assertEqual(content['content'], '*Foo*')
+ self.assertEqual(self.mock_args,
+ ('WARNING: Cannot render Markdown in {}: {}',
+ self.md_post_path, 'Error forced by test'))
+
+ def test_no_markdown_rendering(self):
+ content = makesite.read_content(self.no_md_post_path)
+ self.assertEqual(content['content'], '*Foo*')
+
+ def test_no_markdown_import_error(self):
+ makesite._test = 'ImportError'
+ original_log = makesite.log
+
+ makesite.log = self.mock
+ self.mock_args = None
+ content = makesite.read_content(self.no_md_post_path)
+
+ makesite._test = None
+ makesite.log = original_log
+
+ self.assertEqual(content['content'], '*Foo*')
+ self.assertIsNone(self.mock_args)
diff --git a/test/test_file_io.py b/test/test_file_io.py
new file mode 100644
index 0000000..3760956
--- /dev/null
+++ b/test/test_file_io.py
@@ -0,0 +1,39 @@
+import unittest
+import os
+import shutil
+
+import makesite
+from test import path
+
+
+class FileIOTest(unittest.TestCase):
+ """Tests for file I/O functions."""
+
+ def test_fread(self):
+ text = 'foo\nbar\n'
+ filepath = path.temppath('foo.txt')
+ with open(filepath, 'w') as f:
+ f.write(text)
+ text_read = makesite.fread(filepath)
+ os.remove(filepath)
+ self.assertEqual(text_read, text)
+
+ def test_fwrite(self):
+ text = 'baz\nqux\n'
+ filepath = path.temppath('foo.txt')
+ makesite.fwrite(filepath, text)
+ with open(filepath) as f:
+ text_read = f.read()
+ os.remove(filepath)
+ self.assertEqual(text_read, text)
+
+ def test_fwrite_makedir(self):
+ text = 'baz\nqux\n'
+ dirpath = path.temppath('foo', 'bar')
+ filepath = os.path.join(dirpath, 'foo.txt')
+ makesite.fwrite(filepath, text)
+ with open(filepath) as f:
+ text_read = f.read()
+ self.assertTrue(os.path.isdir(dirpath))
+ shutil.rmtree(path.temppath('foo'))
+ self.assertEqual(text_read, text)
diff --git a/test/test_headers.py b/test/test_headers.py
new file mode 100644
index 0000000..56eeb4a
--- /dev/null
+++ b/test/test_headers.py
@@ -0,0 +1,43 @@
+import unittest
+import makesite
+
+
+class HeaderTest(unittest.TestCase):
+ """Tests for read_headers() function."""
+
+ def test_single_header(self):
+ text = '<!-- key1: val1 -->'
+ headers = list(makesite.read_headers(text))
+ self.assertEqual(headers, [('key1', 'val1', 19)])
+
+ def test_multiple_headers(self):
+ text = '<!-- key1: val1 -->\n<!-- key2: val2-->'
+ headers = list(makesite.read_headers(text))
+ self.assertEqual(headers, [('key1', 'val1', 20), ('key2', 'val2', 38)])
+
+ def test_headers_and_text(self):
+ text = '<!-- a: 1 -->\n<!-- b: 2 -->\nFoo\n<!-- c: 3 -->'
+ headers = list(makesite.read_headers(text))
+ self.assertEqual(headers, [('a', '1', 14), ('b', '2', 28)])
+
+ def test_headers_and_blank_line(self):
+ text = '<!-- a: 1 -->\n<!-- b: 2 -->\n\n<!-- c: 3 -->\n'
+ headers = list(makesite.read_headers(text))
+ self.assertEqual(headers, [('a', '1', 14),
+ ('b', '2', 29),
+ ('c', '3', 43)])
+
+ def test_multiline_header(self):
+ text = '<!--\na: 1 --><!-- b:\n2 -->\n<!-- c: 3\n-->'
+ headers = list(makesite.read_headers(text))
+ self.assertEqual(headers, [('a', '1', 13),
+ ('b', '2', 27),
+ ('c', '3', 40)])
+
+ def test_no_header(self):
+ headers = list(makesite.read_headers('Foo'))
+ self.assertEqual(headers, [])
+
+ def test_empty_string(self):
+ headers = list(makesite.read_headers(''))
+ self.assertEqual(headers, [])
diff --git a/test/test_list.py b/test/test_list.py
new file mode 100644
index 0000000..8acb2ef
--- /dev/null
+++ b/test/test_list.py
@@ -0,0 +1,46 @@
+import unittest
+import shutil
+import os
+import makesite
+from test import path
+
+class PagesTest(unittest.TestCase):
+ def setUp(self):
+ self.site_path = path.temppath('site')
+
+ def tearDown(self):
+ shutil.rmtree(self.site_path)
+
+ def test_list(self):
+ posts = [{'content': 'Foo'}, {'content': 'Bar'}]
+ dst = os.path.join(self.site_path, 'list.txt')
+ list_layout = '<div>{{ content }}</div>'
+ item_layout = '<p>{{ content }}</p>'
+ makesite.make_list(posts, dst, list_layout, item_layout)
+ with open(os.path.join(self.site_path, 'list.txt')) as f:
+ self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')
+
+ def test_list_params(self):
+ posts = [{'content': 'Foo', 'title': 'foo'},
+ {'content': 'Bar', 'title': 'bar'}]
+ dst = os.path.join(self.site_path, 'list.txt')
+ list_layout = '<div>{{ key }}:{{ title }}:{{ content }}</div>'
+ item_layout = '<p>{{ key }}:{{ title }}:{{ content }}</p>'
+ makesite.make_list(posts, dst, list_layout, item_layout,
+ key='val', title='lorem')
+ with open(os.path.join(self.site_path, 'list.txt')) as f:
+ text = f.read()
+ self.assertEqual(text,
+ '<div>val:lorem:<p>val:foo:Foo</p><p>val:bar:Bar</p></div>')
+
+ def test_dst_params(self):
+ posts = [{'content': 'Foo'}, {'content': 'Bar'}]
+ dst = os.path.join(self.site_path, '{{ key }}.txt')
+ list_layout = '<div>{{ content }}</div>'
+ item_layout = '<p>{{ content }}</p>'
+ makesite.make_list(posts, dst, list_layout, item_layout, key='val')
+
+ expected_path = os.path.join(self.site_path, 'val.txt')
+ self.assertTrue(os.path.isfile(expected_path))
+ with open(expected_path) as f:
+ self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')
diff --git a/test/test_main.py b/test/test_main.py
new file mode 100644
index 0000000..50f1cc5
--- /dev/null
+++ b/test/test_main.py
@@ -0,0 +1,73 @@
+import unittest
+import makesite
+import os
+import shutil
+import json
+
+from test import path
+
+
+class MainTest(unittest.TestCase):
+ def setUp(self):
+ path.move('_site', '_site.backup')
+ path.move('params.json', 'params.json.backup')
+
+ def tearDown(self):
+ path.move('_site.backup', '_site')
+ path.move('params.json.backup', 'params')
+
+ def test_site_missing(self):
+ makesite.main()
+
+ def test_site_exists(self):
+ os.mkdir('_site')
+ with open('_site/foo.txt', 'w') as f:
+ f.write('foo')
+
+ self.assertTrue(os.path.isfile('_site/foo.txt'))
+ makesite.main()
+ self.assertFalse(os.path.isfile('_site/foo.txt'))
+
+ def test_default_params(self):
+ makesite.main()
+
+ with open('_site/blog/proin-quam/index.html') as f:
+ s1 = f.read()
+
+ with open('_site/blog/rss.xml') as f:
+ s2 = f.read()
+
+ shutil.rmtree('_site')
+
+ self.assertIn('<a href="/">Home</a>', s1)
+ self.assertIn('<title>Proin Quam - Lorem Ipsum</title>', s1)
+ self.assertIn('Published on 2018-01-01 by <b>Admin</b>', s1)
+
+ self.assertIn('<link>http://localhost:8000/</link>', s2)
+ self.assertIn('<link>http://localhost:8000/blog/proin-quam/</link>', s2)
+
+ def test_json_params(self):
+ params = {
+ 'base_path': '/base',
+ 'subtitle': 'Foo',
+ 'author': 'Bar',
+ 'site_url': 'http://localhost/base'
+ }
+ with open('params.json', 'w') as f:
+ json.dump(params, f)
+ makesite.main()
+
+ with open('_site/blog/proin-quam/index.html') as f:
+ s1 = f.read()
+
+ with open('_site/blog/rss.xml') as f:
+ s2 = f.read()
+
+ shutil.rmtree('_site')
+
+ self.assertIn('<a href="/base/">Home</a>', s1)
+ self.assertIn('<title>Proin Quam - Foo</title>', s1)
+ self.assertIn('Published on 2018-01-01 by <b>Bar</b>', s1)
+
+ self.assertIn('<link>http://localhost/base/</link>', s2)
+ self.assertIn('<link>http://localhost/base/blog/proin-quam/</link>', s2)
diff --git a/test/test_pages.py b/test/test_pages.py
new file mode 100644
index 0000000..5fe7a4d
--- /dev/null
+++ b/test/test_pages.py
@@ -0,0 +1,63 @@
+import unittest
+import os
+import shutil
+import makesite
+from test import path
+
+class PagesTest(unittest.TestCase):
+ def setUp(self):
+ self.blog_path = path.temppath('blog')
+ self.site_path = path.temppath('site')
+ os.makedirs(self.blog_path)
+
+ with open(os.path.join(self.blog_path, 'foo.txt'), 'w') as f:
+ f.write('Foo')
+ with open(os.path.join(self.blog_path, 'bar.txt'), 'w') as f:
+ f.write('Bar')
+ with open(os.path.join(self.blog_path, '2018-01-01-foo.txt'), 'w') as f:
+ f.write('Foo')
+ with open(os.path.join(self.blog_path, '2018-01-02-bar.txt'), 'w') as f:
+ f.write('Bar')
+
+ def tearDown(self):
+ shutil.rmtree(self.blog_path)
+ shutil.rmtree(self.site_path)
+
+ def test_pages_undated(self):
+ src = os.path.join(self.blog_path, '[fb]*.txt')
+ dst = os.path.join(self.site_path, '{{ slug }}.txt')
+ tpl = '<div>{{ content }}</div>'
+ makesite.make_pages(src, dst, tpl)
+ with open(os.path.join(self.site_path, 'foo.txt')) as f:
+ self.assertEqual(f.read(), '<div>Foo</div>')
+ with open(os.path.join(self.site_path, 'bar.txt')) as f:
+ self.assertEqual(f.read(), '<div>Bar</div>')
+
+ def test_pages_dated(self):
+ src = os.path.join(self.blog_path, '2*.txt')
+ dst = os.path.join(self.site_path, '{{ slug }}.txt')
+ tpl = '<div>{{ content }}</div>'
+ makesite.make_pages(src, dst, tpl)
+ with open(os.path.join(self.site_path, 'foo.txt')) as f:
+ self.assertEqual(f.read(), '<div>Foo</div>')
+ with open(os.path.join(self.site_path, 'bar.txt')) as f:
+ self.assertEqual(f.read(), '<div>Bar</div>')
+
+ def test_pages_layout_params(self):
+ src = os.path.join(self.blog_path, '2*.txt')
+ dst = os.path.join(self.site_path, '{{ slug }}.txt')
+ tpl = '<div>{{ slug }}:{{ title }}:{{ date }}:{{ content }}</div>'
+ makesite.make_pages(src, dst, tpl, title='Lorem')
+ with open(os.path.join(self.site_path, 'foo.txt')) as f:
+ self.assertEqual(f.read(), '<div>foo:Lorem:2018-01-01:Foo</div>')
+ with open(os.path.join(self.site_path, 'bar.txt')) as f:
+ self.assertEqual(f.read(), '<div>bar:Lorem:2018-01-02:Bar</div>')
+
+ def test_pages_return_value(self):
+ src = os.path.join(self.blog_path, '2*.txt')
+ dst = os.path.join(self.site_path, '{{ slug }}.txt')
+ tpl = '<div>{{ content }}</div>'
+ posts = makesite.make_pages(src, dst, tpl)
+ self.assertEqual(len(posts), 2)
+ self.assertEqual(posts[0]['date'], '2018-01-02')
+ self.assertEqual(posts[1]['date'], '2018-01-01')
diff --git a/test/test_path.py b/test/test_path.py
new file mode 100644
index 0000000..91e76e8
--- /dev/null
+++ b/test/test_path.py
@@ -0,0 +1,78 @@
+import unittest
+import os
+import shutil
+
+from test import path
+
+class PathTest(unittest.TestCase):
+ def test_temppath(self):
+ self.assertTrue(path.temppath())
+
+ def test_move_existing_file(self):
+ src = os.path.join(path.temppath(), 'foo.txt')
+ dst = os.path.join(path.temppath(), 'bar.txt')
+ with open(src, 'w') as f:
+ f.write('foo')
+
+ path.move(src, dst)
+ self.assertFalse(os.path.isfile(src))
+ self.assertTrue(os.path.isfile(dst))
+
+ with open(dst) as f:
+ text = f.read()
+
+ os.remove(dst)
+
+ self.assertEqual(text, 'foo')
+
+ def test_move_missing_file(self):
+ src = os.path.join(path.temppath(), 'foo.txt')
+ dst = os.path.join(path.temppath(), 'bar.txt')
+ path.move(src, dst)
+ self.assertFalse(os.path.isfile(src))
+ self.assertFalse(os.path.isfile(dst))
+
+ def test_move_file_cleanup(self):
+ src = os.path.join(path.temppath(), 'foo.txt')
+ dst = os.path.join(path.temppath(), 'bar.txt')
+ with open(dst, 'w') as f:
+ f.write('foo')
+ path.move(src, dst)
+ self.assertFalse(os.path.isfile(src))
+ self.assertFalse(os.path.isfile(dst))
+
+ def test_move_existing_dir(self):
+ src = os.path.join(path.temppath(), 'foo')
+ srcf = os.path.join(src, 'foo.txt')
+ dst = os.path.join(path.temppath(), 'bar')
+ dstf = os.path.join(dst, 'foo.txt')
+
+ os.makedirs(src)
+ with open(srcf, 'w') as f:
+ f.write('foo')
+
+ path.move(src, dst)
+ self.assertFalse(os.path.isdir(src))
+ self.assertTrue(os.path.isdir(dst))
+
+ with open(dstf) as f:
+ text = f.read()
+
+ shutil.rmtree(dst)
+
+ self.assertEqual(text, 'foo')
+
+ def test_move_missing_dir(self):
+ src = os.path.join(path.temppath(), 'foo')
+ dst = os.path.join(path.temppath(), 'bar')
+ path.move(src, dst)
+ self.assertFalse(os.path.isdir(src))
+ self.assertFalse(os.path.isdir(dst))
+
+ def test_move_dir_cleanup(self):
+ src = os.path.join(path.temppath(), 'foo')
+ dst = os.path.join(path.temppath(), 'bar')
+ os.makedirs(dst)
+ path.move(src, dst)
+ self.assertFalse(os.path.isdir(src))
+ self.assertFalse(os.path.isdir(dst))
diff --git a/test/test_render.py b/test/test_render.py
new file mode 100644
index 0000000..e036b70
--- /dev/null
+++ b/test/test_render.py
@@ -0,0 +1,25 @@
+import unittest
+import makesite
+
+class RenderTest(unittest.TestCase):
+ """Tests for render() function."""
+
+ def test_oneline_template(self):
+ tpl = 'foo {{ key1 }} baz {{ key2 }}'
+ out = makesite.render(tpl, key1='bar', key2='qux')
+ self.assertEqual(out, 'foo bar baz qux')
+
+ def test_multiline_template(self):
+ tpl = 'foo {{ key1 }}\nbaz {{ key1 }}'
+ out = makesite.render(tpl, key1='bar')
+ self.assertEqual(out, 'foo bar\nbaz bar')
+
+ def test_repeated_key(self):
+ tpl = 'foo {{ key1 }} baz {{ key1 }}'
+ out = makesite.render(tpl, key1='bar')
+ self.assertEqual(out, 'foo bar baz bar')
+
+ def test_multiline_placeholder(self):
+ tpl = 'foo {{\nkey1\n}} baz {{\nkey2\n}}'
+ out = makesite.render(tpl, key1='bar', key2='qux')
+ self.assertEqual(out, 'foo bar baz qux')