commit
41c80533cd
10 changed files with 369 additions and 0 deletions
@ -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,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() |
After Width: | Height: | Size: 117 KiB |
@ -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 |
Loading…
Reference in new issue