@@ -0,0 +1,3 @@ | |||
__pycache__ | |||
*.pyc | |||
/public |
@@ -0,0 +1,13 @@ | |||
all: build | |||
@PHONY: build | |||
build: | |||
./build.py | |||
@PHONY: watch | |||
watch: | |||
./watch.sh | |||
@PHONY: clean | |||
clean: | |||
rm -rf public |
@@ -0,0 +1,3 @@ | |||
all: build | |||
build: |
@@ -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() |
@@ -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 %} |
@@ -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 %} |
@@ -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> |
@@ -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; | |||
} |
@@ -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 |