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