summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Xu (Hello71) <alex_y_xu@yahoo.ca>2021-10-03 15:03:42 -0400
committerAlex Xu (Hello71) <alex_y_xu@yahoo.ca>2021-10-03 15:03:42 -0400
commit7dccf6e8684f767b6e377d627b36ccf493947f1e (patch)
tree4859d4e1a24ea2028de1366bc59cd28e6782a6d1
downloadalxu.ca-7dccf6e8684f767b6e377d627b36ccf493947f1e.tar.xz
alxu.ca-7dccf6e8684f767b6e377d627b36ccf493947f1e.zip
Initial commit
-rw-r--r--.gitignore5
-rw-r--r--Makefile82
-rw-r--r--analytics/analytics.go117
-rw-r--r--analytics/analytics.service28
-rw-r--r--analytics/analytics.socket8
-rw-r--r--analytics/go.mod18
-rw-r--r--analytics/go.sum181
-rwxr-xr-xinliner.py50
-rw-r--r--resume/resume.css241
-rw-r--r--resume/resume.html230
-rw-r--r--resume/resume.js51
11 files changed, 1011 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..93cd0b9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/analytics/analytics
+/out
+/static
+*.woff2
+*.otf
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e813d30
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,82 @@
+MAKEFLAGS += -R -r
+HTML_MINIFIER_FLAGS := \
+ --collapse-boolean-attributes \
+ --collapse-whitespace \
+ --decode-entities \
+ --minify-css \
+ --minify-js \
+ --remove-attribute-quotes \
+ --remove-comments \
+ --remove-optional-tags \
+ --remove-redundant-attributes \
+ --remove-tag-whitespace \
+ --sort-attributes \
+ --sort-class-name \
+ --ignore-custom-fragments 'clamp([^;]*);' \
+ --trim-custom-fragments
+
+WFS ?= ~/wfs/wfs.py
+WFS_FLAGS := \
+ --desubroutinize \
+ --layout-scripts=latn \
+ --layout-features-=frac,locl \
+ --name-IDs= \
+ --drop-tables+=gasp,prep
+
+deployhosts := pink red blue
+
+staticall := $(patsubst static/%,out/%,$(shell find static))
+staticdirs := $(sort $(dir $(staticall)))
+staticfiles := $(filter-out $(staticdirs),$(staticall))
+
+tocomp := $(filter %.txt %.html %.js %.css,$(staticfiles)) out/resume/index.html
+
+fonts := resume/EBGaramond-Italic.otf resume/EBGaramond-Regular.otf resume/EBGaramond-Medium.otf
+fontssubset := $(fonts:.otf=.subset.woff2)
+
+all: $(staticall) out/resume $(tocomp:=.br) $(tocomp:=.gz)
+
+out/%: static/%
+ install -Dpm644 $< $@
+
+%.br: %
+ brotli -Zc $< > $@
+
+%.gz: %
+ zopfli -c $< > $@
+
+$(staticdirs) out/resume &:
+ mkdir -p $@
+
+out/%.html: static/%.html | $(@D)
+ html-minifier $(HTML_MINIFIER_FLAGS) $< -o $@
+
+out/resume/index.html: resume/resume.html resume/resume.js resume/resume.css $(fontssubset) | out/resume
+ ./inliner.py $< | html-minifier $(HTML_MINIFIER_FLAGS) > $@
+
+$(fontssubset) &: resume/resume.html $(fonts)
+ cd $(<D) && $(WFS) \
+ --font 'EBGaramond-Italic.otf:EB Garamond:400:italic' \
+ --font 'EBGaramond-Regular.otf:EB Garamond:400:normal' \
+ --font 'EBGaramond-Medium.otf:EB Garamond:500:normal' \
+ $(WFS_FLAGS) $(<F)
+
+deploy: $(patsubst %,deploy-%,$(deployhosts))
+
+deploy-%:
+ rsync -e 'ssh -oVisualHostKey=no' -av --delete out/ $*.alxu.ca:public_html/
+
+define check
+check-$(subst :,_,$(1)):
+ curl -sS --compressed --resolve www.alxu.ca:443:$(1) https://www.alxu.ca/resume/ | cmp - out/resume/index.html
+check: check-$(subst :,_,$(1))
+endef
+
+$(foreach ip,$(shell getent ahosts www.alxu.ca | awk '/STREAM/{print $$1}'),$(eval $(call check,$(ip))))
+
+clean:
+ rm -rf out
+ rm -f $(fontssubset)
+
+.DELETE_ON_ERROR:
+.PHONY: all clean deploy
diff --git a/analytics/analytics.go b/analytics/analytics.go
new file mode 100644
index 0000000..16d12c5
--- /dev/null
+++ b/analytics/analytics.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/http/fcgi"
+ "time"
+
+ "blitiri.com.ar/go/systemd"
+ "github.com/jackc/pgx/v4"
+ "github.com/jackc/pgx/v4/pgxpool"
+)
+
+type Event struct {
+ Created_ts int64
+ Kind string
+ Details string
+}
+
+func getListener() (net.Listener, error) {
+ listeners, err := systemd.Listeners()
+ if err != nil {
+ return nil, fmt.Errorf("unable to get systemd listeners: %w", err)
+ }
+ count := 0
+ for _, listeners := range listeners {
+ count += len(listeners)
+ if count == 1 {
+ return listeners[0], nil
+ }
+ }
+ return nil, fmt.Errorf("expected 1 systemd fd passed, got %v", count)
+}
+
+func httpEnd(w http.ResponseWriter, r *http.Request, content string, code int) {
+ log.Printf("%v %d %d %s", r.RemoteAddr, r.ContentLength, code, content)
+ http.Error(w, content, code)
+}
+
+func main() {
+ conn, err := pgxpool.Connect(context.Background(), "")
+ if err != nil {
+ log.Fatal("Unable to connect to database: ", err)
+ }
+ defer conn.Close()
+
+ http.HandleFunc("/analytics", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "GET" {
+ if url, ok := r.URL.Query()["url"]; ok {
+ if len(url) > 1 {
+ httpEnd(w, r, "too many urls", 400)
+ } else {
+ http.Redirect(w, r, url[0], 303)
+ _, err = conn.Exec(context.Background(), "insert into events(created_ts, ip_addr, kind, details) values ($1, $2, $3, $4)", time.Now(), r.RemoteAddr, "r", url[0])
+ if err != nil {
+ log.Printf("%v 0 303 db error: %s", r.RemoteAddr, err)
+ return
+ }
+ log.Printf("%v 0 303 ok", r.RemoteAddr)
+ }
+ } else {
+ httpEnd(w, r, "missing url for GET", 400)
+ }
+ return
+ }
+ if r.Method != "POST" {
+ httpEnd(w, r, "method not allowed: "+r.Method, 405)
+ return
+ }
+ if r.ContentLength <= 0 {
+ httpEnd(w, r, "length required", 411)
+ return
+ }
+ if r.ContentLength > 1048576 {
+ httpEnd(w, r, "too much data", 413)
+ return
+ }
+
+ data, err := io.ReadAll(r.Body)
+ if err != nil {
+ httpEnd(w, r, "read error", 400)
+ return
+ }
+ var events []Event
+ if err := json.Unmarshal(data, &events); err != nil {
+ log.Print(err)
+ httpEnd(w, r, "bad json", 400)
+ return
+ }
+
+ _, err = conn.CopyFrom(
+ r.Context(),
+ pgx.Identifier{"events"},
+ []string{"created_ts", "ip_addr", "kind", "details"},
+ pgx.CopyFromSlice(len(events), func(i int) ([]interface{}, error) {
+ return []interface{}{time.Unix(events[i].Created_ts/1000, events[i].Created_ts%1000*1000000), r.RemoteAddr, events[i].Kind, events[i].Details}, nil
+ }),
+ )
+ if err != nil {
+ log.Printf("%v %d 500 db error: %s", r.RemoteAddr, r.ContentLength, err)
+ http.Error(w, "db error", 500)
+ } else {
+ httpEnd(w, r, "ok", 200)
+ }
+ })
+
+ l, err := getListener()
+ if err != nil {
+ log.Fatal(err)
+ }
+ fcgi.Serve(l, nil)
+}
diff --git a/analytics/analytics.service b/analytics/analytics.service
new file mode 100644
index 0000000..461a940
--- /dev/null
+++ b/analytics/analytics.service
@@ -0,0 +1,28 @@
+[Unit]
+Description=analytics server
+
+[Service]
+Type=simple
+ExecStart=/usr/sbin/analytics
+
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+CapabilityBoundingSet=
+PrivateDevices=yes
+PrivateNetwork=yes
+PrivateTmp=yes
+ProtectClock=yes
+ProtectControlGroups=yes
+ProtectHome=yes
+ProtectHostname=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectKernelLogs=yes
+ProtectSystem=strict
+RestrictAddressFamilies=AF_UNIX
+RestrictNamespaces=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
diff --git a/analytics/analytics.socket b/analytics/analytics.socket
new file mode 100644
index 0000000..072ac0b
--- /dev/null
+++ b/analytics/analytics.socket
@@ -0,0 +1,8 @@
+[Unit]
+Description=analytics socket
+
+[Socket]
+ListenStream=/run/analytics.sock
+
+[Install]
+WantedBy=sockets.target
diff --git a/analytics/go.mod b/analytics/go.mod
new file mode 100644
index 0000000..7c8aaf2
--- /dev/null
+++ b/analytics/go.mod
@@ -0,0 +1,18 @@
+module alxu.ca/analytics
+
+go 1.17
+
+require (
+ blitiri.com.ar/go/systemd v1.1.0
+ github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+ github.com/jackc/pgconn v1.10.0 // indirect
+ github.com/jackc/pgio v1.0.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgproto3/v2 v2.1.1 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
+ github.com/jackc/pgtype v1.8.1 // indirect
+ github.com/jackc/pgx/v4 v4.13.0
+ github.com/jackc/puddle v1.1.4 // indirect
+ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
+ golang.org/x/text v0.3.7 // indirect
+)
diff --git a/analytics/go.sum b/analytics/go.sum
new file mode 100644
index 0000000..92d6ee0
--- /dev/null
+++ b/analytics/go.sum
@@ -0,0 +1,181 @@
+blitiri.com.ar/go/systemd v1.1.0 h1:AMr7Ce/5CkvLZvGxsn/ZOagzFf3zU13rcgWdlbWMQ+Y=
+blitiri.com.ar/go/systemd v1.1.0/go.mod h1:0D9Ttrh+TX+WuKQ/dJpdhFND7NYy505v6jhsWrihmPY=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
+github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
+github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
+github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
+github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
+github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
+github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
+github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
+github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
+github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU=
+github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
+github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
+github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
+github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
+github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
+github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
+github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs=
+github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
+github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
+github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
+github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
+github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0=
+github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.4 h1:5Ey/o5IfV7dYX6Znivq+N9MdK1S18OJI5OJq6EAAADw=
+github.com/jackc/puddle v1.1.4/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
diff --git a/inliner.py b/inliner.py
new file mode 100755
index 0000000..b37c275
--- /dev/null
+++ b/inliner.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+import base64
+import mimetypes
+import os.path
+import re
+import sys
+
+ABSURL_RE = re.compile(r'''^(?:[a-z0-9+.-]+:|/)''')
+def repl(func, base):
+ def g(m):
+ urlstr = m.group(1)
+ if urlstr[0] in ('"', "'"):
+ urlstr = urlstr[1:-1]
+ if ABSURL_RE.match(urlstr):
+ return m.group(0)
+ try:
+ return func(os.path.join(base, urlstr))
+ except FileNotFoundError:
+ return m.group(0)
+ return g
+
+SCRIPT_RE = re.compile(r'''<script src=("[^"]+"|'[^']+')>\s*</script>''')
+def scriptrepl(urlstr):
+ with open(urlstr, 'r') as f:
+ script = f.read()
+ if '</script>' in script:
+ raise Exception('</script> in script')
+ return f'<script>{script}</script>'
+
+CSS_RE = re.compile(r'''<link rel="stylesheet" href=("[^"]+"|'[^']+')>''')
+def cssrepl(urlstr):
+ with open(urlstr, 'r') as f:
+ return '<style>' + f.read() + '</style>'
+
+URL_RE = re.compile(r'''\burl\(("[^"]+"|'[^']+'|[^)]+)\)''')
+def urlrepl(urlstr):
+ with open(urlstr, 'rb') as f:
+ return ('url(data:' + mimetypes.guess_type(urlstr)[0] +
+ ';base64,' + base64.b64encode(f.read()).decode('ascii') + ')')
+
+if __name__ == '__main__':
+ mimetypes.add_type('font/woff2', '.woff2')
+ base = os.path.dirname(sys.argv[1])
+ with open(sys.argv[1], 'r') as f:
+ buf = f.read()
+ buf = SCRIPT_RE.sub(repl(scriptrepl, base), buf)
+ buf = CSS_RE.sub(repl(cssrepl, base), buf)
+ buf = URL_RE.sub(repl(urlrepl, base), buf)
+ sys.stdout.write(buf)
diff --git a/resume/resume.css b/resume/resume.css
new file mode 100644
index 0000000..04c8431
--- /dev/null
+++ b/resume/resume.css
@@ -0,0 +1,241 @@
+@font-face {
+ font-family: "EB Garamond";
+ src: url(EBGaramond-Regular.subset.woff2);
+}
+
+@font-face {
+ font-family: "EB Garamond";
+ font-weight: 500;
+ src: url(EBGaramond-Medium.subset.woff2);
+}
+
+@font-face {
+ font-family: "EB Garamond";
+ font-style: italic;
+ src: url(EBGaramond-Italic.subset.woff2);
+}
+
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: .8em auto;
+ max-width: 90%;
+ width: 50em;
+ position: relative;
+ left: clamp(-2em,calc(25em - 45%),0em);
+ display: grid;
+ grid-template-columns: auto auto;
+
+ font-family: "EB Garamond", serif;
+ font-size: 16px;
+ line-height: 1.38;
+ overflow-wrap: break-word;
+ text-rendering: optimizeLegibility;
+ background: #fff;
+ color: #333;
+}
+
+address, .right {
+ align-self: center;
+}
+.left {
+ padding-right: .9rem;
+ text-align: right;
+ color: #246d84;
+}
+.right {
+ border-left: solid 3px #246d84;
+ padding: .2rem 0 0 1rem;
+}
+/* firefox prints grid margin wrong */
+.gap {
+ margin-top: .8rem;
+ grid-column: 1/-1;
+}
+.entry-header > :nth-child(1) {
+ flex: 60%;
+ max-width: max-content;
+ margin-right: auto;
+}
+.taddr {
+ grid-row: 1;
+ align-self: center;
+}
+b {
+ font-weight: 500;
+ font-size: 1.1em;
+}
+.date {
+ text-align: right;
+ margin-left: 1.2em;
+ max-width: max-content;
+ flex: 10 0 auto;
+ width: min-content;
+}
+.title {
+ line-height: 1.22;
+}
+.entry-header {
+ display: flex;
+}
+section + section {
+ margin-top: .7em;
+}
+address, .h1s {
+ color: #666;
+}
+address {
+ font-size: .9em;
+}
+.icon {
+ vertical-align: middle;
+ margin-bottom: .5ex;
+}
+h1, h2, h3, h4, p, ul {
+ margin: 0;
+}
+h1, h2, h3, h4 {
+ font-weight: normal;
+}
+h1 {
+ font-size: 3em;
+ margin-top: -.5ex;
+}
+h4 {
+ display: inline;
+}
+ul {
+ padding: 0;
+ list-style: none;
+}
+ul:not(.fakelist) > li {
+ margin-left: .1em;
+ /* separate list-style-type for safari <14.1 support */
+ list-style-type: '◦ ';
+ list-style-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='7.1' width='7.1' fill='none' stroke='%233873b3' stroke-width='1.1'><circle cx='3.55' cy='3.55' r='3'/></svg>");
+ list-style-position: inside;
+}
+a {
+ color: #06c;
+ text-decoration: none;
+}
+a:visited {
+ color: #0f00b0;
+}
+.date-punct, .hide, .printcol {
+ display: none;
+}
+.nb {
+ display: inline-block;
+}
+abbr {
+ text-decoration: none;
+}
+.h1s {
+ font-size: 1.4em;
+ font-style: italic;
+}
+@media (min-width: 25em) {
+ .left-heavy-columns {
+ columns: 2 14em;
+ margin-right: -1.4em;
+ }
+}
+@media (max-width: 30em) {
+ body {
+ display: block;
+ }
+ h1 {
+ margin-bottom: -.2ex;
+ }
+ .h1s {
+ margin-bottom: .6ex;
+ }
+ .title, .left address {
+ text-align: center;
+ }
+ .left address {
+ columns: 2;
+ }
+ .left {
+ text-align: center;
+ padding: 0;
+ min-width: 0;
+ }
+ .left:not(.taddr) {
+ border-bottom: solid;
+ margin-bottom: .2rem;
+ }
+ .gap {
+ margin-bottom: 1rem;
+ }
+ .right {
+ border-left: none;
+ padding: 0;
+ }
+ .title {
+ padding: 0;
+ }
+ .nb {
+ display: initial;
+ }
+}
+@media (max-width: 21em) {
+ header, .entry-header {
+ display: block;
+ }
+ .date {
+ text-align: left;
+ margin: 0;
+ width: auto;
+ }
+ .date-punct {
+ display: inline;
+ }
+}
+@media print {
+ .noprint {
+ display: none;
+ }
+ body {
+ position: static;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ max-width: 100%;
+ font-size: 13px;
+ line-height: 1.28;
+ color: #000;
+ -webkit-print-color-adjust: exact;
+ }
+ /* workaround: safari doesn't support css columns in print */
+ .left-heavy-columns {
+ display: none;
+ }
+ .printcol {
+ display: inline-block;
+ }
+ section + section {
+ margin-top: .6em;
+ }
+ /* override print color for firefox */
+ address, .h1s {
+ color: transparent;
+ text-shadow: 0 0 #555;
+ }
+ address svg {
+ color: #555;
+ }
+}
+@media (prefers-contrast: high) {
+ .h1s, address {
+ color: #555;
+ }
+}
+@media (prefers-contrast: low) {
+ body {
+ background: #ccc;
+ }
+}
+@page { margin: 7mm 26mm 7mm 18mm; }
diff --git a/resume/resume.html b/resume/resume.html
new file mode 100644
index 0000000..982ded8
--- /dev/null
+++ b/resume/resume.html
@@ -0,0 +1,230 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Alex Xu's Resume</title>
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="Generalist software developer with experience in Linux, C, Python, networking.">
+ <link rel="stylesheet" href="resume.css">
+</head>
+<body>
+ <div id="ie" style="font-size:2em;display:none">
+ Internet Explorer does not support the web standards used on this page. Please upgrade to a modern browser such as <a href="https://firefox.com">Mozilla Firefox</a>, <a href="https://microsoft.com/edge">Microsoft Edge</a>, or <a href="https://google.com/chrome">Google Chrome</a>.
+ </div>
+ <div class="right title">
+ <h1>Alex Xu</h1>
+ <p class="h1s">Generalist software developer</p>
+ </div>
+ <div class="left taddr">
+ <address>
+ <ul class="fakelist">
+ <li>
+ <svg class="icon" role="img" height="12" width="12" fill="none" stroke="currentcolor" stroke-linejoin="round" stroke-width="1.1">
+ <title>Location:</title>
+ <path aria-hidden="true" d="m1.5 4.4999967 4.5-3.50000004 4.5 3.50000004v5.5a1 1 0 0 1 -1 1.0000003h-7a1 1 0 0 1 -1-1.0000003z"/>
+ <path aria-hidden="true" d="m4.5 11v-5h3v5"/>
+ </svg>
+ Toronto, Canada<br>(open to remote, reloc)
+ </li>
+ <li>
+ <svg class="icon" role="img" height="12" width="12" fill="none" stroke="currentcolor" stroke-width="1.1">
+ <title>Email:</title>
+ <path aria-hidden="true" d="m2 2h8c.55 0 1 .45 1 1v6c0 .55-.45 1-1 1h-8c-.55 0-1-.45-1-1v-6c0-.55.45-1 1-1z"/>
+ <path aria-hidden="true" d="m11 3-5 3.5-5-3.5"/>
+ </svg>
+ <!-- remember to update print js below -->
+ <a href="javascript:location=['ca','.','alxu','@','alex',':','mailto'].reverse().join('');void 0">
+ <span style="display:inline">alex</span>&#64;alxu<span class="hide"> DELETE ME </span>.ca
+ </a>
+ </li>
+ </ul>
+ </address>
+ </div>
+ <div class="gap"></div>
+ <h2 class="left">Education</h2>
+ <div class="right">
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b>Lassonde School of Engineering, B.Sc., <abbr title="Honours">Hons.</abbr> Computer Science</b>, <i class="nb">York University</i>, GPA: 7.7/9.0 (A).</h4></div>
+ <div class="date"><b>Sept.&nbsp;2016&ndash;May&nbsp;2020</b><span class="date-punct">:</span></div>
+ </div>
+ <ul class="left-heavy-columns">
+ <li>Advanced Object Oriented Programming (A+)</li>
+ <li>Design and Analysis of Algorithms (A+)</li>
+ <li>Fundamentals of Data Structures (A+)</li>
+ <li>Computer Architecture (A)</li>
+ <li>Database Management Systems (A+)</li>
+ <li>Applied Cryptography (A+)</li>
+ <li>Mathematics of Cryptography (A)</li>
+ <li>Network Security (A+)</li>
+ </ul>
+ <ul class="printcol" style="width:21em">
+ <li>Advanced Object Oriented Programming (A+)</li>
+ <li>Design and Analysis of Algorithms (A+)</li>
+ <li>Fundamentals of Data Structures (A+)</li>
+ <li>Computer Architecture (A)</li>
+ </ul>
+ <ul class="printcol" style="width:16em">
+ <li>Database Management Systems (A+)</li>
+ <li>Applied Cryptography (A+)</li>
+ <li>Mathematics of Cryptography (A)</li>
+ <li>Network Security (A+)</li>
+ </ul>
+ </section>
+ </div>
+ <div class="gap"></div>
+ <h2 class="left">Work</h2>
+ <div class="right">
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b>Ethica Channel Enablement Inc</b>, <i>Network Engineer</i>.</h4></div>
+ <div class="date"><b>Dec.&nbsp;2018&ndash;Oct.&nbsp;2019</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Developed several core projects and provided extensive consulting on C, Linux, git, and networking.</p>
+ <ul>
+ <li>built a Buildroot-based minimal Linux infrastructure for the launch of a multi-link VPN product</li>
+ <li>created a fully automated high-speed operating system installer for x86 systems using BusyBox sh</li>
+ <li>created a tool for remote Linux in-place replacement using POSIX sh and BusyBox</li>
+ <li>developed a GitLab CI process for generating the deployable image</li>
+ </ul>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b>York University</b>, <i>Undergraduate Student Research Award researcher</i>.</h4></div>
+ <div class="date"><b>May&ndash;Aug. 2018</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Co-developed a Django web platform for worldwide crowdsourced hydrographic data collaboration.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b>York University</b>, <i>Teaching assistant</i>.</h4></div>
+ <div class="date"><b>June&ndash;July 2017</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Taught lab sessions for 48-student Android/web development course.</p>
+ </section>
+ </div>
+ <div class="gap"></div>
+ <div class="left">
+ <h2>Community</h2>
+ <address>
+ <ul class="fakelist">
+ <li>
+ <svg class="icon" role="img" height="12" width="12" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-width="1.1">
+ <title>cgit:</title>
+ <path aria-hidden="true" d="m8 9 3-3-3-3 M4 3 l-3 3 3 3"/>
+ </svg>
+ <a href="https://cgit.alxu.ca/">cgit.alxu.ca</a>
+ </li>
+ <li>
+ <svg class="icon" role="img" height="12" width="12" fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.1">
+ <title>GitHub:</title>
+ <path aria-hidden="true" d="m4.625 9.7566971c-2.5.75-2.5-1.25-3.5-1.5m7 3v-1.935a1.685 1.685 0 0 0 -.47-1.305c1.57-.175 3.22-.77 3.22-3.5a2.72 2.72 0 0 0 -.75-1.875 2.535 2.535 0 0 0 -.045-1.885s-.59-.175-1.955.74a6.69 6.69 0 0 0 -3.5 0c-1.365-.915-1.955-.74-1.955-.74a2.535 2.535 0 0 0 -.045 1.885 2.72 2.72 0 0 0 -.75 1.89c0 2.71 1.65 3.305 3.22 3.5a1.685 1.685 0 0 0 -.47 1.29v1.935"/>
+ </svg>
+ <a href="https://github.com/Hello71">gh/Hello71</a> <span style="font-style: normal">[</span><a href="https://github.com/search?q=type%3Apr+author%3AHello71&amp;s=created">PRs</a><span style="font-style: normal">]</span>
+ </li>
+ </ul>
+ </address>
+ </div>
+ <div class="right">
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://alpinelinux.org/">Alpine Linux</a></b></h4></div>
+ <div class="date"><b>March 2020&ndash;present</b><span class="date-punct">:</span></div>
+ </div>
+ <p><a href="https://gitlab.alpinelinux.org/groups/alpine/-/issues?state=all&amp;author_username=alxu">Reported</a>, documented, and <a href="https://gitlab.alpinelinux.org/groups/alpine/-/merge_requests?author_username=alxu&amp;state=all">contributed fixes</a> for issues including <a href="https://wiki.alpinelinux.org/wiki/Release_Notes_for_Alpine_3.14.0#faccessat2">Operation not permitted in Docker</a>, <a href="https://github.com/alpinelinux/docker-alpine/issues/134">sh: write error: Invalid argument</a>, and <a href="https://gitlab.alpinelinux.org/alpine/aports/-/issues/12368">Raspberry Pi doesn't boot, 7 blinks</a>. <a href="https://lists.alpinelinux.org/~alpine/devel/?search=from%3A%22Alex+Xu%22">Proposed changes</a> including <a href="https://lists.alpinelinux.org/~alpine/devel/%3C1628515011.zujvcn248v.none%40localhost%3E">-fno-plt for x86 and x86_64</a>, <a href="https://lists.alpinelinux.org/~alpine/devel/%3C1593702164.2nw55qdomr.none%40localhost%3E">compressing debuginfo</a>, and <a href="https://lists.alpinelinux.org/~alpine/devel/%3C1593625212.dirkptm3b0.none%40localhost%3E">reconsidering -Os</a>.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://gitlab.freedesktop.org/glvnd/libglvnd">libglvnd</a></b></h4></div>
+ <div class="date"><b>September 2021</b><span class="date-punct">:</span></div>
+ </div>
+ <p><a href="https://gitlab.freedesktop.org/glvnd/libglvnd/-/merge_requests/249">Implemented correct global-dynamic TLS support, fixing musl compatibility.</a></p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://www.winehq.org/">Wine</a></b></h4></div>
+ <div class="date"><b>July 2021</b><span class="date-punct">:</span></div>
+ </div>
+ <p><a href="https://www.winehq.org/pipermail/wine-devel/2021-July/191267.html">Implemented copy_file_range support, shrinking Wine prefixes from 200 MB to less than 1 MB.</a></p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b>Linux kernel</b></h4></div>
+ <div class="date"><b>2012&ndash;present</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Reported issues and submitted fixes: <a href="https://lore.kernel.org/lkml/?q=f%3A%22Alex+Xu%22">lkml</a>, <a href="https://linuxlists.cc/profile/47355/Alex_Xu">linuxlists</a>. Diagnosed <a href="https://github.com/rust-lang/cargo/issues/9739">Cargo issue 9739</a> to <a href="https://lore.kernel.org/lkml/1628086770.5rn8p04n6j.none@localhost/">a long-standing kernel bug</a>, and <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=46c4c9d1beb7f5b4cec4dd90e7728720583ee348">fixed the underlying issue</a>, resolving a deadlock in GNU Make and similar jobservers.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://qemu.org/">QEMU</a></b></h4></div>
+ <div class="date"><b>December 2020</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Discovered and reported <a href="https://nvd.nist.gov/vuln/detail/CVE-2020-35517">an issue allowing full host device access from guests with virtiofsd enabled</a>.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://cgit.alxu.ca/wfs.git/">wfs</a></b></h4></div>
+ <div class="date"><b>June 2020</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Built a concurrent Python web font subsetter with automatic pixel-perfect verification.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://cgit.alxu.ca/cgit-syntax-highlighting.git">cgit-syntax-highlighting</a></b></h4></div>
+ <div class="date"><b>March 2020</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Built a concurrent Python pygments microservice, reducing TTFB from 1.1s to 0.15s on <a href="https://cgit.alxu.ca/">cgit.alxu.ca</a>. Improved portability and security and reduced LOC by 66% by <a href="https://cgit.alxu.ca/cgit-syntax-highlighting.git/commit/?id=bbbbafd21b1cade042d57c90cc70df682df28e6d">switching from http.server to aiohttp</a>.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://cgit.alxu.ca/udpastcp.git">udpastcp</a></b></h4></div>
+ <div class="date"><b>July&nbsp;2016</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Built a C tunnel to simulate datagrams using TCP packets, fixing TCP-over-TCP overhead.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://github.com/Dwarf-Therapist/Dwarf-Therapist">Dwarf Therapist</a></b></h4></div>
+ <div class="date"><b>Aug.&nbsp;2014&ndash;Sept.&nbsp;2017</b><span class="date-punct">:</span></div>
+ </div>
+ <p><a href="https://github.com/Dwarf-Therapist/Dwarf-Therapist/commits?author=Hello71">Implemented cross-bit ptrace, migrated from qmake to CMake, and refactored code</a>.</p>
+ </section>
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b><a href="https://sourceforge.net/projects/simpregedit/">Simple Registry Editor</a></b></h4></div>
+ <div class="date"><b>Sept.&nbsp;2008</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Built a minimal .NET registry editor.</p>
+ </section>
+ </div>
+ <div class="gap"></div>
+ <h2 class="left noprint">Personal</h2>
+ <div class="right noprint">
+ <section class="entry">
+ <div class="entry-header">
+ <div><h4><b>Resume</b></h4></div>
+ <div class="date"><b>June&nbsp;2020</b><span class="date-punct">:</span></div>
+ </div>
+ <p>Rewrote my resume from LaTeX to modern web standards:</p>
+ <ul>
+ <li>significantly improved mobile-friendliness and blind accessibility using standard HTML and CSS</li>
+ <li>reduced transfer size from 390 kB to 30 kB using aggressive inlining and font subsetting</li>
+ <li>optimized loading time by minimizing size and reducing round trips using HTTP/2 server push</li>
+ </ul>
+ </section>
+ </div>
+ <div class="gap noprint"></div>
+ <h2 class="left" style="font-size: 1.4em">Skills</h2>
+ <div class="right">
+ <p>Alpine, Debian Linux; FreeBSD; Docker; Git; NGINX; WireGuard; TCP/IP; HTTP(S); DNS; SSH</p>
+ </div>
+ <h3 class="left">Programming</h3>
+ <div class="right">
+ <p>C; C++; Go; Python; bash, POSIX sh; GNU make; HTML; CSS; JavaScript; SQL</p>
+ </div>
+ <script src="resume.js"></script>
+</body>
+</html>
diff --git a/resume/resume.js b/resume/resume.js
new file mode 100644
index 0000000..3f33976
--- /dev/null
+++ b/resume/resume.js
@@ -0,0 +1,51 @@
+(function(w, d, a){
+ 'use strict';
+ if (d.documentMode)
+ ie.style.display = "block";
+ else
+ ie.parentNode.removeChild(ie);
+ const css = d.styleSheets[0];
+ if (w.safari) {
+ css.insertRule("@page{margin:10mm}", css.cssRules.length);
+ css.insertRule("@media print{body{margin:-2mm 0 -2mm 0;padding:0 15mm 0 7mm}}", css.cssRules.length);
+ }
+ else if (w.chrome)
+ css.insertRule("@page{margin-top:auto;margin-bottom:auto}", css.cssRules.length);
+ let t, p = [];
+ const f = () => {
+ clearTimeout(t);
+ navigator.sendBeacon('/analytics', new Blob([JSON.stringify(p)], {type: 'application/json'}));
+ p = [];
+ };
+ const q = (kind, details) => {
+ clearTimeout(t);
+ p.push({created_ts: Date.now(), kind: kind, details: details});
+ t = setTimeout(f, 2000);
+ };
+ q('l', '');
+ d[a]('visibilitychange', () => {
+ const vs = d.visibilityState;
+ q('v', vs);
+ if (vs === 'hidden')
+ f();
+ });
+ d[a]('click', e => {
+ const a = e.target.closest('a');
+ if (a)
+ q('c', a.href);
+ });
+ w[a]('beforeprint', () => {
+ for (let el of d.getElementsByTagName('a')) {
+ const h = el.href.replace(/javascript:location=(.*);void 0/, '$1');
+ el.setAttribute('data-href', el.href);
+ if (h != el.href) el.href = eval(h);
+ else el.href = "https://alxu.ca/analytics?url=" + el.href.replace(/%/g, '%25').replace(/&/g, '%26').replace(/;/g, '%3B');
+ }
+ });
+ w[a]('afterprint', function () {
+ for (let el of d.getElementsByTagName('a')) {
+ el.href = el.getAttribute('data-href');
+ el.removeAttribute('data-href');
+ }
+ });
+}(window, document, 'addEventListener'));