-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2b3b32f
Showing
8 changed files
with
390 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
build | ||
dist | ||
*egg-info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) [year] [fullname] | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
# Easybake | ||
|
||
Easybake is a tool for making static websites that lets you focus on your content and not on software. It generates your website based on simple yaml configuration files and jinja templates. | ||
|
||
## The Site File | ||
|
||
The basic component of a website is a site file, which is a json or yaml file specifying the pages of your site. This example yaml file defines a single content object that generates a single page at the root of the website: | ||
|
||
``` | ||
content: | ||
- url: / | ||
template: home.html | ||
assets: | ||
- picture-of-me.jpg | ||
``` | ||
|
||
The `template` key references a template file called `home.html`. This is a jinja template that might look like this: | ||
|
||
``` | ||
<html> | ||
<head> | ||
<title>My Website</title> | ||
</head> | ||
<body> | ||
<h1>Hello World!</h1> | ||
<img src="/assets/picture-of-me.jpg" /> | ||
</body> | ||
</html> | ||
``` | ||
|
||
When you generate this site, you'll end up with the following files in your `build` directory: | ||
|
||
``` | ||
/build/ | ||
index.html | ||
/assets/ | ||
picture-of-me.jpg | ||
``` | ||
|
||
This can be published to any web host (Amazon S3, Github Pages, Azure Storage, or Google Cloud Storage all offer popular ways to do this). | ||
|
||
## Generating your site | ||
|
||
Generating the site from the example above is simple. Using the example above, you might have the following files: | ||
|
||
``` | ||
site.yaml | ||
/templates/ | ||
home.html | ||
/assets/ | ||
picture-of-me.jpg | ||
``` | ||
|
||
From this directory, run `python -m easybake build --site=site.yaml` and your site will be generated in a new subdirectory called `build`: | ||
|
||
``` | ||
site.yaml | ||
/templates/ | ||
home.html | ||
/assets/ | ||
picture-of-me.jpg | ||
/build/ | ||
index.html | ||
/assets/ | ||
picture-of-me.jpg | ||
``` | ||
|
||
To see your site, you can run `python -m easybake serve` and your site will be viewable at http://127.0.0.1:8000. | ||
|
||
## More complex content | ||
|
||
Content objects can do more than just specify a template and related assets. Variables can be defined and used in your templates, and the rendered page itself can be stored in a variable instead of in a file. Here's a full list of usable keys in a content object: | ||
|
||
- `template` - Render the data in this content object into this template | ||
- `assets` - A list of extra files to make available to the website | ||
- `url` - If given, the content will be saved in a file available at this url | ||
- `name` - If given, the context variable name where this rendered content will be available. If a name is repeated, both values will be kept in a list. | ||
- `data` - An object of data to add to the template's rendering context | ||
- `datafile` - A file of data to add to the template's rendering context | ||
|
||
Content objects are rendered in order, so content stored in a variable will be available to use in subsequently rendered content. | ||
|
||
Any keys defined in a datafile that aren't used in a template are ignored, so a single datafile can be used in multiple content objects to render one piece of content in multiple ways (for example, to render a article preview with a short summary that links to the full article). | ||
|
||
In a datafile, variables can be defined in markdown for richer content. | ||
|
||
``` | ||
title: My Article | ||
summary: > | ||
This is a summary of the content in my article. | ||
I've split it across multiple lines for readability. | ||
body: | ||
_language: markdown | ||
content: | | ||
## An H2 in markdown | ||
This is the full content of my article, fully written | ||
in markdown. It will be in the 'body' variable as html. | ||
- a list | ||
- of items | ||
- in markdown | ||
``` | ||
|
||
Here's an example of a site using more of these features: | ||
|
||
**site.yaml** | ||
``` | ||
content: | ||
- url: /contact/ | ||
template: page.html | ||
data: | ||
title: Contact Me | ||
body: | ||
_language: | ||
content: | | ||
Drop me a line at [[email protected]](mailto://[email protected])! | ||
- name: articles | ||
template: card.html | ||
datafile: my-first-article.yaml | ||
- url: /my-first-article/ | ||
template: page.html | ||
datafile: my-first-article.yaml | ||
- name: articles | ||
template: card.html | ||
datafile: my-second-article.yaml | ||
- url: /my-second-article/ | ||
template: page.html | ||
datafile: my-second-article.yaml | ||
- url: / | ||
template: home.html | ||
assets: | ||
- picture-of-me.jpg | ||
``` | ||
|
||
**home.html** | ||
``` | ||
<html> | ||
<head> | ||
<title>My Website</title> | ||
</head> | ||
<body> | ||
<h1>This is my website</h1> | ||
<img src="/assets/picture-of-me.jpg" /> | ||
<p>Articles:</p> | ||
{% for article in articles %} | ||
{{article}} <!-- injects a rendered card.html for this article --> | ||
{% endfor %} | ||
</body> | ||
</html> | ||
``` | ||
|
||
**card.html** | ||
``` | ||
<div class="card"> | ||
<h2>{{title}}</h2> | ||
<p>{{summary}}</p> | ||
</div> | ||
``` | ||
|
||
**page.html** | ||
``` | ||
<html> | ||
<head> | ||
<title>My Website</title> | ||
</head> | ||
<body> | ||
<h1>{{title}}</h1> | ||
{{body}} <!-- no containing tag since this variable is markdown rendered to html --> | ||
</body> | ||
</html> | ||
``` |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import argparse | ||
from .easybake import SiteBuilder, serve, clean | ||
|
||
parser = argparse.ArgumentParser(prog="easybake", description="Generate a static website") | ||
parser.add_argument( | ||
"command", type=str, help="The action to take (e.g. build, serve)", | ||
) | ||
parser.add_argument("--site", type=str, help="The definition of the site to build") | ||
parser.add_argument( | ||
"--templates", type=str, default="templates", help="the template directory" | ||
) | ||
parser.add_argument( | ||
"--content", type=str, default="content", help="the content directory" | ||
) | ||
|
||
args = parser.parse_args() | ||
|
||
if args.command.lower() == "build": | ||
sb = SiteBuilder( | ||
args.site, template_dir=args.templates, content_dir=args.content | ||
) | ||
sb.build() | ||
elif args.command.lower() == "serve": | ||
serve() | ||
elif args.command.lower() == "clean": | ||
clean() | ||
else: | ||
print("Unknown command '{}'".format(args.command)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import copy | ||
import http.server | ||
import json | ||
import os | ||
import shutil | ||
import socketserver | ||
import sys | ||
import yaml | ||
import markdown | ||
from jinja2 import Environment, FileSystemLoader | ||
|
||
|
||
class SiteBuilder: | ||
def __init__( | ||
self, | ||
sitefile_path, | ||
template_dir="templates", | ||
content_dir="content", | ||
asset_dir="assets", | ||
base_dir=os.getcwd() | ||
): | ||
self.BASE_DIR = base_dir | ||
self.sitefile_path = sitefile_path | ||
self.template_dir = template_dir | ||
self.content_dir = content_dir | ||
self.asset_dir = asset_dir | ||
self.env = Environment(loader=FileSystemLoader(self.BASE_DIR)) | ||
|
||
def load_datafile(self, datafile): | ||
with open(os.path.join(self.content_dir, datafile)) as f: | ||
if datafile.endswith("json"): | ||
data = json.load(f) | ||
elif datafile.endswith("yaml"): | ||
data = yaml.safe_load(f) | ||
else: | ||
data = {} | ||
data = self.process_data(data) | ||
return data | ||
|
||
def process_data(self, data): | ||
if isinstance(data, list): | ||
data = [self.process_data(i) for i in data] | ||
elif isinstance(data, dict): | ||
if set(data.keys()) == set(("_language", "content")): | ||
if data["_language"] == "markdown": | ||
data = markdown.markdown(data["content"]) | ||
else: | ||
data = {key: self.process_data(value) for key, value in data.items()} | ||
return data | ||
|
||
def load_sitefile(self, sitefile): | ||
try: | ||
with open(self.sitefile_path) as f: | ||
if self.sitefile_path.endswith("json"): | ||
data = json.load(f) | ||
elif self.sitefile_path.endswith("yaml"): | ||
data = yaml.load(f) | ||
else: | ||
data = None | ||
except (TypeError, FileNotFoundError): | ||
print("Can't load sitefile '{}'".format(self.sitefile_path)) | ||
sys.exit(1) | ||
except json.decoder.JSONDecodeError: | ||
print("Provided sitefile '{}' is not valid JSON".format(self.sitefile_path)) | ||
sys.exit(1) | ||
data = self.process_data(data) | ||
return data | ||
|
||
def load_template(self, template): | ||
return self.env.get_template(os.path.join(self.template_dir, template)) | ||
|
||
def render(self, obj): | ||
template = self.load_template(obj["template"]) | ||
data = copy.deepcopy(self.context) | ||
if "datafile" in obj: | ||
data.update(self.load_datafile(obj["datafile"])) | ||
if "data" in obj: | ||
data.update(obj["data"]) | ||
content = template.render(**data) | ||
for a in obj.get("assets", []): | ||
shutil.copy(os.path.join(self.asset_dir, a), os.path.join("build", "assets", a)) | ||
return content | ||
|
||
def write_page(self, content): | ||
url = content["url"] | ||
if url.startswith("/"): | ||
url = url[1:] | ||
dirpath = os.path.join("build", url) | ||
os.makedirs(dirpath, exist_ok=True) | ||
with open(dirpath + "index.html", "w") as f: | ||
f.write(content["rendered"]) | ||
|
||
def build(self): | ||
clean() | ||
os.makedirs(os.path.join("build", "assets")) | ||
site = self.load_sitefile(self.sitefile_path) | ||
self.context = {} | ||
for content in site["content"]: | ||
content["rendered"] = self.render(content) | ||
if "name" in content: | ||
cname = content["name"] | ||
if cname in self.context: | ||
if isinstance(self.context[cname], list): | ||
self.context[cname].append(content["rendered"]) | ||
else: | ||
self.context[cname] = [self.context[cname], content["rendered"]] | ||
else: | ||
self.context[cname] = content["rendered"] | ||
if "url" in content: | ||
self.write_page(content) | ||
|
||
|
||
def clean(): | ||
if os.path.exists("build"): | ||
shutil.rmtree("build") | ||
|
||
|
||
def get_handler(directory): | ||
def handler(*args, **kwargs): | ||
kwargs["directory"] = directory | ||
return http.server.SimpleHTTPRequestHandler(*args, **kwargs) | ||
|
||
return handler | ||
|
||
|
||
def serve(): | ||
try: | ||
PORT = 8000 | ||
Handler = get_handler("build") | ||
with socketserver.TCPServer(("", PORT), Handler) as httpd: | ||
print("serving at http://0.0.0.0:{}/".format(PORT)) | ||
httpd.serve_forever() | ||
except KeyboardInterrupt: | ||
print("Stopping...") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Jinja2 | ||
markdown | ||
PyYAML |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import setuptools | ||
|
||
with open("README.md", "r") as fh: | ||
long_description = fh.read() | ||
|
||
setuptools.setup( | ||
name="easybake", | ||
version="0.0.1", | ||
author="Tim Saylor", | ||
author_email="[email protected]", | ||
description="A static site builder", | ||
long_description=long_description, | ||
long_description_content_type="text/markdown", | ||
url="https://github.com/tsaylor/easybake", | ||
packages=setuptools.find_packages(), | ||
classifiers=[ | ||
"Programming Language :: Python :: 3", | ||
"License :: OSI Approved :: MIT License", | ||
"Operating System :: OS Independent", | ||
], | ||
python_requires='>=3.6', | ||
install_requires=[ | ||
"Jinja2", | ||
"markdown", | ||
"PyYAML", | ||
], | ||
) |