Initial commit
This commit is contained in:
commit
41c80533cd
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
/public
|
13
Makefile
Normal file
13
Makefile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
all: build
|
||||||
|
|
||||||
|
@PHONY: build
|
||||||
|
build:
|
||||||
|
./build.py
|
||||||
|
|
||||||
|
@PHONY: watch
|
||||||
|
watch:
|
||||||
|
./watch.sh
|
||||||
|
|
||||||
|
@PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf public
|
142
build.py
Executable file
142
build.py
Executable file
|
@ -0,0 +1,142 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
log = logging.getLogger("build")
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
class App(object):
|
||||||
|
# Global template variables.
|
||||||
|
def vars(self):
|
||||||
|
return {
|
||||||
|
"site": {
|
||||||
|
"name": "noah.is",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Jinja environment.
|
||||||
|
self.env = jinja2.Environment(
|
||||||
|
loader=jinja2.FileSystemLoader("./templates"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Collect all the templates and all the static files.
|
||||||
|
self.templates = self.crawl("./templates") # TODO: remove list() wrapper
|
||||||
|
self.static = self.crawl("./static")
|
||||||
|
|
||||||
|
# Copy all of the static files into the public root.
|
||||||
|
if not os.path.isdir("./public"):
|
||||||
|
self.mkdir("./public")
|
||||||
|
for static in self.static:
|
||||||
|
public = static.replace("./static/", "./public/")
|
||||||
|
self.mkdir(public.rsplit("/", 1)[0]) # ensure the path will exist
|
||||||
|
if self.should_copy_static(static, public):
|
||||||
|
log.info("COPY STATIC FILE %s -> %s", static, public)
|
||||||
|
copyfile(static, public)
|
||||||
|
|
||||||
|
# And render all of the templates.
|
||||||
|
for template in self.templates:
|
||||||
|
if template.startswith("./templates/layout"): # don't create these
|
||||||
|
continue
|
||||||
|
public = self.template_to_public(template)
|
||||||
|
self.mkdir(public.rsplit("/", 1)[0]) # ensure the path will exist
|
||||||
|
log.info("RENDER TEMPLATE: %s -> %s", template, public)
|
||||||
|
self.render_template(template, public)
|
||||||
|
|
||||||
|
def render_template(self, template, public):
|
||||||
|
"""
|
||||||
|
Render a template to the public path.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
template (str): path in ./templates
|
||||||
|
public (str): path in ./public
|
||||||
|
"""
|
||||||
|
tpl = self.env.get_template(template.replace("./templates/", ""))
|
||||||
|
html = tpl.render(self.vars())
|
||||||
|
|
||||||
|
with codecs.open(public, "w", "utf-8") as fh:
|
||||||
|
fh.write(html)
|
||||||
|
|
||||||
|
|
||||||
|
def crawl(self, path):
|
||||||
|
"""
|
||||||
|
Crawl a directory recursively and return a list of file paths.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
path (str)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
filepaths (list[str])
|
||||||
|
"""
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
yield os.path.join(root, file)
|
||||||
|
|
||||||
|
def should_copy_static(self, static_file, public_file):
|
||||||
|
"""
|
||||||
|
Determine if a static file should be copied over to the public folder.
|
||||||
|
|
||||||
|
This means the public file either does not exist yet, or is older
|
||||||
|
than the static file and so the new static file should be copied over it.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
static_file (str)
|
||||||
|
public_file (str)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if public_file doesn't exist or is older than static_file.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(public_file):
|
||||||
|
return True
|
||||||
|
return os.stat(static_file).st_mtime > os.stat(public_file).st_mtime
|
||||||
|
|
||||||
|
def template_to_public(self, template_filename):
|
||||||
|
"""
|
||||||
|
Convert a template filename into a public name, preferring clean URL
|
||||||
|
paths without file extension suffixes.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
* /index.html -> /index.html
|
||||||
|
* /about.html -> /about/index.html
|
||||||
|
* /photos/index.html -> /photos/index.html
|
||||||
|
* /photos/2018.html -> /photos/2018/index.html
|
||||||
|
|
||||||
|
The `.html` and `.md` file types will suppress suffixes in this way.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
template_filename: like ``./templates/about.html``
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
public_filename: like ``./public/about/index.html``
|
||||||
|
"""
|
||||||
|
public = template_filename.replace("./templates/", "./public/")
|
||||||
|
path, filename = public.rsplit("/", 1)
|
||||||
|
basename, ext = filename.rsplit(".", 1)
|
||||||
|
if ext not in ["html", "md"]:
|
||||||
|
# Not a web page so just keep the path literal.
|
||||||
|
return public
|
||||||
|
|
||||||
|
print("template_to_public:", template_filename, public)
|
||||||
|
|
||||||
|
# See if it's already an index page or if we need to create one.
|
||||||
|
if filename in ["index.html", "index.md"]:
|
||||||
|
return public # already a good name
|
||||||
|
else:
|
||||||
|
print("HERE:", os.path.join(path, basename, "index."+ext))
|
||||||
|
log.info("basename=%r path=%r filename=%s", basename, path, filename)
|
||||||
|
return os.path.join(path, basename, "index."+ext)
|
||||||
|
|
||||||
|
def mkdir(self, path):
|
||||||
|
"""Create a directory and log it."""
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
log.info("mkdir: %s", path)
|
||||||
|
os.makedirs(path, mode=0o755)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
App()
|
BIN
static/images/photo.jpg
Normal file
BIN
static/images/photo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 117 KiB |
28
templates/contact.html
Normal file
28
templates/contact.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "layout/base.html" %}
|
||||||
|
{% block title %}Contact Me{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="first">
|
||||||
|
<h1>Contact Me</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Below are some places you can find me on the Internet and ways you can
|
||||||
|
contact me.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="right">Email:</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://www.kirsle.net/contact">Send me an e-mail here</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>External Links</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://www.kirsle.net/">Kirsle.net</a>, my web blog.</a></li>
|
||||||
|
<li><a href="https://github.com/kirsle">GitHub</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
41
templates/index.html
Normal file
41
templates/index.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends "layout/base.html" %}
|
||||||
|
{% block title %}Welcome{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="first">
|
||||||
|
<img src="/images/photo.jpg" class="circle center" width="320">
|
||||||
|
|
||||||
|
<h1>Hello world!</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is the personal homepage of <strong>Noah Petherbridge</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
I'm a software developer in California. I write about random stuff on
|
||||||
|
my <a href="https://www.kirsle.net/" target="_blank">web blog</a> and
|
||||||
|
maintain a handful of open source projects on
|
||||||
|
<a href="https://github.com/kirsle" target="_blank">GitHub</a>
|
||||||
|
and <a href="https://git.kirsle.net/" target="_blank">git.kirsle.net</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Software Development</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
I'm a full stack web developer and I program in Go, Python, and
|
||||||
|
JavaScript. One of my largest personal projects is
|
||||||
|
<a href="https://www.rivescript.com/" target="_blank">RiveScript</a>,
|
||||||
|
a scripting language for programming chat bots — matching a user's
|
||||||
|
message to a certain response.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
My web blog, <a href="https://www.kirsle.net/" target="_blank">Kirsle.net</a>,
|
||||||
|
is written in Go and I keep that source code <a href="https://github.com/kirsle/blog" target="_blank">here</a>
|
||||||
|
for now. Previously it was written in
|
||||||
|
<a href="https://git.kirsle.net/apps/rophako" target="_blank">Python</a>
|
||||||
|
and its <a href="https://git.kirsle.net/kirsle/kirsle.net" target="_blank">HTML pages</a>
|
||||||
|
are readable too. The site you're reading is a simple Python-generated
|
||||||
|
static site <a href="https://git.kirsle.net/kirsle/noah.is">here</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
25
templates/layout/base.html
Normal file
25
templates/layout/base.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}Untitled{% endblock %} - {{ site.name }}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="/theme/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/contact">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
<h1><a href="/">{{ site.name }}</a></h1>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
100
templates/theme/style.css
Normal file
100
templates/theme/style.css
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #0099FF;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: medium;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
color: #222;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 950px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link, a:visited {
|
||||||
|
color: #006699;
|
||||||
|
}
|
||||||
|
a:hover, a:active {
|
||||||
|
color: #0099FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
img.circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
line-height: 3rem;
|
||||||
|
background-color: rgba(255, 153, 255, 0.8);
|
||||||
|
box-shadow: 0px 2px 2px #000;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: larger;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
header nav {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
header nav ul {
|
||||||
|
list-style: none;
|
||||||
|
float: right;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
header nav ul li {
|
||||||
|
display: inline;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
header nav a {
|
||||||
|
color: #000 !important;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0 24px;
|
||||||
|
background-color: #FFF;
|
||||||
|
box-shadow: 0px 0px 4px #000;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
section.first {
|
||||||
|
padding-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
max-width: 860px;
|
||||||
|
padding: 0 4rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
color: #036;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
14
watch.sh
Executable file
14
watch.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function die() {
|
||||||
|
echo >&2 $1
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Before we crash and burn, make sure necessary programs are installed!
|
||||||
|
command -v inotifywait || die "I need the inotifywait command (apt install inotify-tools)"
|
||||||
|
|
||||||
|
make
|
||||||
|
while inotifywait -r -e modify,move,create,delete static templates; do
|
||||||
|
make
|
||||||
|
done
|
Loading…
Reference in New Issue
Block a user