summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Thompson <dthompson2@worcester.edu>2016-03-27 11:59:14 -0400
committerDavid Thompson <dthompson2@worcester.edu>2016-03-27 11:59:14 -0400
commit9934cc80b087ce9b71a87baaa77068fbd23445ce (patch)
tree0ad991cec55a7fe492f07e22c03d411fd7229a18
First commit!
The wonderful beginnings of a new blog powered by Haunt!
-rw-r--r--.gitignore1
-rw-r--r--css/dthompson.css202
-rw-r--r--css/fonts.css61
-rw-r--r--css/reset.css427
-rw-r--r--fonts/Inconsolata-Bold.woffbin0 -> 24608 bytes
-rw-r--r--fonts/Inconsolata-Normal.woffbin0 -> 38340 bytes
-rw-r--r--fonts/LinLibertine_R.woffbin0 -> 515460 bytes
-rw-r--r--fonts/LinLibertine_RB.woffbin0 -> 442244 bytes
-rw-r--r--fonts/LinLibertine_RI.woffbin0 -> 458368 bytes
-rw-r--r--fonts/linbio-r-subset.woffbin0 -> 30500 bytes
-rw-r--r--fonts/linbio-rb-subset.woffbin0 -> 45892 bytes
-rw-r--r--fonts/linbio-ri-subset.woffbin0 -> 37440 bytes
-rw-r--r--haunt.scm210
-rw-r--r--images/maine-2013/blues-festival.jpgbin0 -> 97231 bytes
-rw-r--r--images/maine-2013/katahdin-camp.jpgbin0 -> 129456 bytes
-rw-r--r--images/maine-2013/katahdin-fog.jpgbin0 -> 57828 bytes
-rw-r--r--images/maine-2013/katahdin-view-1.jpgbin0 -> 78432 bytes
-rw-r--r--images/maine-2013/katahdin-view-2.jpgbin0 -> 57371 bytes
-rw-r--r--images/maine-2013/katahdin-waterfall.jpgbin0 -> 164065 bytes
-rw-r--r--images/maine-2013/view-from-house.jpgbin0 -> 102235 bytes
-rw-r--r--images/maine-2013/zekes-lookout.jpgbin0 -> 120852 bytes
-rw-r--r--posts/2013-06-15-my-first-foss-contribution.skr39
-rw-r--r--posts/2013-06-30-find-me-on-diaspora.skr57
-rw-r--r--posts/2013-07-15-maine.skr34
-rw-r--r--posts/2013-07-20-stumpwm-on-debian-wheezy.skr62
-rw-r--r--posts/2013-08-07-guile-2d.skr48
-rw-r--r--posts/2013-08-08-angularjs-post-mortem.skr135
-rw-r--r--posts/2013-08-11-the-little-schemer.skr58
-rw-r--r--posts/2013-08-17-font-rendering-pango-cairo.skr328
-rw-r--r--posts/2013-09-22-liberating-a-thinkpad-x220.skr68
-rw-r--r--posts/2013-09-27-guile-2d-0.1.skr31
-rw-r--r--posts/2015-04-10-sxml-html-guile.skr235
-rw-r--r--posts/2015-08-30-ruby-on-guix.skr578
-rw-r--r--skribe-utils.scm40
34 files changed, 2614 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c9490a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/site
diff --git a/css/dthompson.css b/css/dthompson.css
new file mode 100644
index 0000000..563aa65
--- /dev/null
+++ b/css/dthompson.css
@@ -0,0 +1,202 @@
+body {
+ color: #000;
+ background-color: #fbfbfb;
+ font-size: 24px;
+ font-family: 'Linux Libertine',serif;
+ line-height: 140%;
+ text-rendering: optimizeLegibility;
+}
+
+h1, h2, h3 {
+ font-family: 'Linux Biolinum',sans;
+}
+
+h1 {
+ font-size: 150%;
+ line-height: 110%;
+}
+
+h2 {
+ font-size: 115%;
+}
+
+h3 {
+ font-size: 100%;
+}
+
+a {
+ color: #5b6ee1;
+ text-decoration: none;
+}
+
+a:hover {
+ color: #306082;
+ background-color: #f3f3f3;
+}
+
+a:visited {
+ color: #306082;
+}
+
+pre {
+ font-family: "Inconsolata",monospace;
+ line-height: 110%;
+ min-width: 100%;
+ padding: 1rem;
+ background-color: #222034;
+ color: #deeed6;
+ border-radius: 4px;
+ display: inline-block;
+}
+
+code {
+ font-family: "Inconsolata",monospace;
+}
+
+blockquote {
+ padding-left: 2rem;
+ border-left: 5px solid #eee;
+ font-style: italic;
+}
+
+/* Footer */
+
+footer {
+ border-top: 1px solid #ccc;
+ font-size: 80%;
+}
+
+.copyright {
+ text-align: center;
+}
+
+.cc-button {
+ margin-left: 1rem;
+}
+
+/* Navigation */
+
+.nav {
+ font-family: 'Linux Biolinum',sans;
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ margin-bottom: 3rem;
+ border-bottom: 1px solid #eee;
+}
+
+.nav ul {
+ list-style-type: none;
+ display: inline-block;
+ margin: 0;
+ padding: 0;
+}
+
+.nav li {
+ display: inline;
+ padding: 1rem;
+}
+
+.nav li:first-child {
+ padding-left: 0;
+}
+
+.nav a {
+ display: inline-block;
+ color: #000;
+}
+
+.nav a:hover {
+ color: #000;
+}
+
+.nav a:visited {
+ color: #000;
+}
+
+@media (min-width: 1140px) {
+ .container {
+ margin-left: 14rem;
+ margin-right: 4rem;
+ max-width: 52rem;
+ }
+}
+
+@media (min-width: 800px) and (max-width: 1140px) {
+ .container {
+ margin-left: 6rem;
+ margin-right: 4rem;
+ max-width: 52rem;
+ }
+}
+
+@media (max-width: 800px) {
+ .container {
+ margin: 1rem;
+ }
+}
+
+.fade-text {
+ color: #ddd;
+}
+
+/* Summaries */
+
+.summary {
+ margin-bottom: 3rem;
+}
+
+.summary .date {
+ margin-bottom: 0rem;
+}
+
+.summary p {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+}
+
+/* Posts */
+
+.date {
+ margin-top: -1rem;
+ margin-bottom: 2rem;
+ color: #666;
+}
+
+.post img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ margin: 0 auto;
+ padding: 0px;
+ border-radius: 4px;
+}
+
+.caption {
+ text-align: center;
+ margin: 0;
+ margin-left: 4rem;
+ margin-right: 4rem;
+}
+
+/* Syntax Highlighting */
+
+.syntax-special, .syntax-element {
+ color: #8ac6f2;
+ font-weight: bold;
+}
+
+.syntax-string {
+ color: #95e454;
+}
+
+.syntax-keyword, .syntax-attribute {
+ color: #e5786d;
+}
+
+.syntax-comment {
+ color: #999;
+}
+
+.syntax-open, .syntax-close {
+ color: #999;
+}
diff --git a/css/fonts.css b/css/fonts.css
new file mode 100644
index 0000000..b622993
--- /dev/null
+++ b/css/fonts.css
@@ -0,0 +1,61 @@
+/* Linux Libertine */
+
+@font-face {
+ font-family: 'Linux Libertine';
+ src: url('/fonts/LinLibertine_R.woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Linux Libertine';
+ src: url('/fonts/LinLibertine_RI.woff');
+ font-weight: normal;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Linux Libertine';
+ src: url('/fonts/LinLibertine_RB.woff');
+ font-weight: bold;
+ font-style: normal;
+}
+
+/* Linux Biolinum */
+
+@font-face {
+ font-family: 'Linux Biolinum';
+ src: url('/fonts/linbio-r-subset.woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Linux Biolinum';
+ src: url('/fonts/linbio-ri-subset.woff');
+ font-weight: normal;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Linux Biolinum';
+ src: url('/fonts/linbio-rb-subset.woff');
+ font-weight: bold;
+ font-style: normal;
+}
+
+/* Inconsolata */
+
+@font-face {
+ font-family: 'Inconsolata';
+ font-style: normal;
+ font-weight: 400;
+ src: url('/fonts/Inconsolata-Normal.woff');
+}
+
+@font-face {
+ font-family: 'Inconsolata';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('/fonts/Inconsolata-Bold.woff');
+}
diff --git a/css/reset.css b/css/reset.css
new file mode 100644
index 0000000..458eea1
--- /dev/null
+++ b/css/reset.css
@@ -0,0 +1,427 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
diff --git a/fonts/Inconsolata-Bold.woff b/fonts/Inconsolata-Bold.woff
new file mode 100644
index 0000000..3be93d9
--- /dev/null
+++ b/fonts/Inconsolata-Bold.woff
Binary files differ
diff --git a/fonts/Inconsolata-Normal.woff b/fonts/Inconsolata-Normal.woff
new file mode 100644
index 0000000..648defe
--- /dev/null
+++ b/fonts/Inconsolata-Normal.woff
Binary files differ
diff --git a/fonts/LinLibertine_R.woff b/fonts/LinLibertine_R.woff
new file mode 100644
index 0000000..0357ad4
--- /dev/null
+++ b/fonts/LinLibertine_R.woff
Binary files differ
diff --git a/fonts/LinLibertine_RB.woff b/fonts/LinLibertine_RB.woff
new file mode 100644
index 0000000..a0dbbbf
--- /dev/null
+++ b/fonts/LinLibertine_RB.woff
Binary files differ
diff --git a/fonts/LinLibertine_RI.woff b/fonts/LinLibertine_RI.woff
new file mode 100644
index 0000000..e2a0ae4
--- /dev/null
+++ b/fonts/LinLibertine_RI.woff
Binary files differ
diff --git a/fonts/linbio-r-subset.woff b/fonts/linbio-r-subset.woff
new file mode 100644
index 0000000..3ca298e
--- /dev/null
+++ b/fonts/linbio-r-subset.woff
Binary files differ
diff --git a/fonts/linbio-rb-subset.woff b/fonts/linbio-rb-subset.woff
new file mode 100644
index 0000000..eeca1dd
--- /dev/null
+++ b/fonts/linbio-rb-subset.woff
Binary files differ
diff --git a/fonts/linbio-ri-subset.woff b/fonts/linbio-ri-subset.woff
new file mode 100644
index 0000000..42321ea
--- /dev/null
+++ b/fonts/linbio-ri-subset.woff
Binary files differ
diff --git a/haunt.scm b/haunt.scm
new file mode 100644
index 0000000..5180304
--- /dev/null
+++ b/haunt.scm
@@ -0,0 +1,210 @@
+;;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;;
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define (add-to-load-path* directory)
+ (unless (member directory %load-path)
+ (add-to-load-path directory)))
+
+(add-to-load-path* "/home/dave/Code/guile-syntax-highlight")
+
+(use-modules (haunt asset)
+ (haunt builder blog)
+ (haunt builder atom)
+ (haunt builder assets)
+ (haunt html)
+ (haunt page)
+ (haunt post)
+ (haunt reader)
+ (haunt reader skribe)
+ (haunt reader texinfo)
+ (haunt site)
+ (haunt utils)
+ (syntax-highlight)
+ (syntax-highlight scheme)
+ (sxml match)
+ (sxml transform)
+ (texinfo)
+ (texinfo html)
+ (srfi srfi-19)
+ (ice-9 rdelim)
+ (ice-9 regex)
+ (ice-9 match)
+ (web uri))
+
+(define (stylesheet name)
+ `(link (@ (rel "stylesheet")
+ (href ,(string-append "/css/" name ".css")))))
+
+(define (anchor content uri)
+ `(a (@ (href ,uri)) ,content))
+
+(define %cc-by-sa-link
+ '(a (@ (href "https://creativecommons.org/licenses/by-sa/4.0/"))
+ "Creative Commons Attribution Share-Alike 4.0 International"))
+
+(define %cc-by-sa-button
+ '(a (@ (class "cc-button")
+ (href "https://creativecommons.org/licenses/by-sa/4.0/"))
+ (img (@ (src "https://licensebuttons.net/l/by-sa/4.0/80x15.png")))))
+
+(define %piwik-code
+ '((script (@ (type "text/javascript") (src "/js/piwik.js")))
+ (noscript
+ (p (img (@ (src "//stats.dthompson.us/piwik.php?idsite=3")
+ (style "border:0;")
+ (alt "")))))))
+
+(define (link name uri)
+ `(a (@ (href ,uri)) ,name))
+
+(define (first-paragraph post)
+ (let loop ((sxml (post-sxml post))
+ (result '()))
+ (match sxml
+ (() (reverse result))
+ ((or (('p ...) _ ...) (paragraph _ ...))
+ (reverse (cons paragraph result)))
+ ((head . tail)
+ (loop tail (cons head result))))))
+
+(define dthompson-theme
+ (theme #:name "dthompson"
+ #:layout
+ (lambda (site title body)
+ `((doctype "html")
+ (head
+ (meta (@ (charset "utf-8")))
+ (title ,(string-append title " — " (site-title site)))
+ ,(stylesheet "reset")
+ ,(stylesheet "fonts")
+ ,(stylesheet "dthompson"))
+ (body
+ (div (@ (class "container"))
+ (div (@ (class "nav"))
+ (ul (li ,(link "David Thompson" "/"))
+ (li (@ (class "fade-text")) "λ")
+ (li ,(link "About" "/about.html"))
+ (li ,(link "Blog" "/index.html"))
+ (li ,(link "Projects" "/projects.html"))))
+ ,body
+ (footer (@ (class "text-center"))
+ (p (@ (class "copyright"))
+ "© 2015 David Thompson"
+ ,%cc-by-sa-button)
+ (p "The text and images on this site are
+free culture works available under the " ,%cc-by-sa-link " license.")
+ (p "This website is built with "
+ (a (@ (href "http://haunt.dthompson.us"))
+ "Haunt")
+ ", a static site generator written in "
+ (a (@ (href "https://gnu.org/software/guile"))
+ "Guile Scheme")
+ "."))))))
+ #:post-template
+ (lambda (post)
+ `((h1 (@ (class "title")),(post-ref post 'title))
+ (div (@ (class "date"))
+ ,(date->string (post-date post)
+ "~B ~d, ~Y"))
+ (div (@ (class "post"))
+ ,(post-sxml post))))
+ #:collection-template
+ (lambda (site title posts prefix)
+ (define (post-uri post)
+ (string-append "/" (or prefix "")
+ (site-post-slug site post) ".html"))
+
+ `((h1 ,title)
+ ,(map (lambda (post)
+ (let ((uri (string-append "/"
+ (site-post-slug site post)
+ ".html")))
+ `(div (@ (class "summary"))
+ (h2 (a (@ (href ,uri))
+ ,(post-ref post 'title)))
+ (div (@ (class "date"))
+ ,(date->string (post-date post)
+ "~B ~d, ~Y"))
+ (div (@ (class "post"))
+ ,(first-paragraph post))
+ (a (@ (href ,uri)) "read more ➔"))))
+ posts)))))
+
+;; (define (static-page file theme reader)
+;; (lambda (site posts)
+;; (make-page "foo.html"
+;; (with-layout theme ))))
+
+(define %collections
+ `(("Recent Blog Posts" "index.html" ,posts/reverse-chronological)))
+
+(define parse-lang
+ (let ((rx (make-regexp "-*-[ ]+([a-z]*)[ ]+-*-")))
+ (lambda (port)
+ (let ((line (read-line port)))
+ (match:substring (regexp-exec rx line) 1)))))
+
+(define (maybe-highlight-code source)
+ (call-with-input-string source
+ (lambda (port)
+ (let ((lang (string->symbol (parse-lang port))))
+ (if lang
+ (highlights->sxml
+ (highlight (match lang
+ ('scheme lex-scheme)
+ ('xml lex-xml))
+ port))
+ source)))))
+
+(define (sxml-identity . args) args)
+
+(define (highlight-code . tree)
+ (sxml-match tree
+ ((pre (@ . ,attrs) ,source)
+ `(pre (@ ,@attrs)
+ ,(maybe-highlight-code source)))))
+
+(define %texi-rules
+ `((pre . ,highlight-code)
+ (*text* . ,(lambda (tag str) str))
+ (*default* . ,sxml-identity)))
+
+(define (texi->shtml port)
+ (let ((tree (stexi->shtml (texi-fragment->stexi port))))
+ (pre-post-order tree %texi-rules)))
+
+(define texinfo-reader
+ (make-reader (make-file-extension-matcher "texi")
+ (lambda (file)
+ (call-with-input-file file
+ (lambda (port)
+ (values (read-metadata-headers port)
+ (texi->shtml port)))))))
+
+(site #:title "dthompson"
+ #:domain "dthompson.us"
+ #:default-metadata
+ '((author . "David Thompson")
+ (email . "davet@gnu.org"))
+ #:readers (list (make-skribe-reader #:modules '((haunt skribe utils)
+ (skribe-utils)))
+ texinfo-reader)
+ #:builders (list (blog #:theme dthompson-theme #:collections %collections)
+ (atom-feed)
+ (atom-feeds-by-tag)
+ (static-directory "css")
+ (static-directory "fonts")
+ (static-directory "images")))
diff --git a/images/maine-2013/blues-festival.jpg b/images/maine-2013/blues-festival.jpg
new file mode 100644
index 0000000..ba3d922
--- /dev/null
+++ b/images/maine-2013/blues-festival.jpg
Binary files differ
diff --git a/images/maine-2013/katahdin-camp.jpg b/images/maine-2013/katahdin-camp.jpg
new file mode 100644
index 0000000..c024bf3
--- /dev/null
+++ b/images/maine-2013/katahdin-camp.jpg
Binary files differ
diff --git a/images/maine-2013/katahdin-fog.jpg b/images/maine-2013/katahdin-fog.jpg
new file mode 100644
index 0000000..0736e16
--- /dev/null
+++ b/images/maine-2013/katahdin-fog.jpg
Binary files differ
diff --git a/images/maine-2013/katahdin-view-1.jpg b/images/maine-2013/katahdin-view-1.jpg
new file mode 100644
index 0000000..601d5c4
--- /dev/null
+++ b/images/maine-2013/katahdin-view-1.jpg
Binary files differ
diff --git a/images/maine-2013/katahdin-view-2.jpg b/images/maine-2013/katahdin-view-2.jpg
new file mode 100644
index 0000000..1514764
--- /dev/null
+++ b/images/maine-2013/katahdin-view-2.jpg
Binary files differ
diff --git a/images/maine-2013/katahdin-waterfall.jpg b/images/maine-2013/katahdin-waterfall.jpg
new file mode 100644
index 0000000..e19bc6d
--- /dev/null
+++ b/images/maine-2013/katahdin-waterfall.jpg
Binary files differ
diff --git a/images/maine-2013/view-from-house.jpg b/images/maine-2013/view-from-house.jpg
new file mode 100644
index 0000000..9718276
--- /dev/null
+++ b/images/maine-2013/view-from-house.jpg
Binary files differ
diff --git a/images/maine-2013/zekes-lookout.jpg b/images/maine-2013/zekes-lookout.jpg
new file mode 100644
index 0000000..4eb42b9
--- /dev/null
+++ b/images/maine-2013/zekes-lookout.jpg
Binary files differ
diff --git a/posts/2013-06-15-my-first-foss-contribution.skr b/posts/2013-06-15-my-first-foss-contribution.skr
new file mode 100644
index 0000000..821bb02
--- /dev/null
+++ b/posts/2013-06-15-my-first-foss-contribution.skr
@@ -0,0 +1,39 @@
+(post
+ :title "My First Real FOSS Contribution"
+ :date (make-date* 2013 06 15)
+ :tags '("foss" "mediagoblin" "python" "federated" "decentralized" "wsu")
+ :summary "I added a small feature!"
+
+ (p [I spend a lot of my free time writing code. I usually work on my own
+personal projects that never really go anywhere. So, I decided to take
+a detour from my normal hacking routine and contribute to an existing
+free software project. My contribution was accepted awhile ago now,
+but I wasn’t blogging then so I’m rambling about it now.])
+
+ (p [It’s wise to find a project with a low barrier of entry. An
+active IRC channel and/or mailing list with people willing to help
+newcomers is ideal. I remembered hearing about
+,(anchor [GNU MediaGoblin] "http://mediagoblin.org")
+at LibrePlanet 2012, so I decided to check things out. MediaGoblin is
+a media sharing web application written in Python. Their bug tracker
+marks tickets that require little work and don’t require a deep
+understanding of MediaGoblin as “bitesized”.])
+
+ (p [I chose to work on
+,(anchor [this ticket] "http://issues.mediagoblin.org/ticket/453")
+because it didn’t require any complicated database migrations or
+knowledge of the media processing code. I added a new configuration
+option, ,(code [allow_comments]), and a small amount of code to enforce
+the setting.])
+
+ (p [Eventually, the ticket got reviewed and
+,(anchor [Christopher Webber] "http://dustycloud.org")
+(MediaGoblin’s friendly project leader) merged it: “Heya. Great
+branch, this works perfectly. Merged!”])
+
+ (p [It was a very small change, but I was happy to ,(em [finally])
+have some actual code of mine in a real free software project. I have
+a strong passion for free software and the GNU philosophy, so it’s
+really great to participate in the community. My job as a professional
+software developer eats up a lot of my time these days, but I hope to
+find the time to continue hacking and contributing.]))
diff --git a/posts/2013-06-30-find-me-on-diaspora.skr b/posts/2013-06-30-find-me-on-diaspora.skr
new file mode 100644
index 0000000..fc2410d
--- /dev/null
+++ b/posts/2013-06-30-find-me-on-diaspora.skr
@@ -0,0 +1,57 @@
+(post
+ :title "Find Me on Diaspora"
+ :date (make-date* 2013 06 30)
+ :tags '("foss" "diaspora" "federated" "decentralized" "rails" "wsu")
+ :summary "I have started using and contributing to Diaspora."
+
+ (p [With all of the recent news about the NSA’s widespread spying, I
+have decided to ween myself off of proprietary, centralized web
+services. Facebook, Google, and other such corporations hold onto
+massive amounts of our data that we’ve willingly given to them via
+status messages, “like” buttons, searches, and emails. Using and
+contributing to free (as in freedom), decentralized (like email) web
+services is a really great way to re-establish control of our data.
+These services rely on many small, interconnected nodes to operate,
+rather than a monolithic website that is under the control of one
+entity. If the distinction between centralized and decentralized
+isn’t clear, consider how email functions. There are many email
+providers to choose from. Users can communicate with others that
+choose to use a different email provider. This is how web services
+should work, but unfortunately very few work this way now.])
+
+ (p [The centralized web application that I spend too much time using
+is Facebook. I have knowingly given Facebook a “frontdoor” into my
+life for years now and I’m ready to move on. I think that the concept
+of a “social network” is fun, so I wanted a Facebook replacement.
+Fortunately, there is one:
+,(anchor [Diaspora] "http://diasporaproject.org/").])
+
+ (p [Diaspora is a
+,(anchor [free] "https://github.com/diaspora/diaspora"),
+distributed, social networking web application written in Ruby using
+the Rails framework. Diaspora is a community-run project. Its success
+depends upon users, developers, technical writers, user interface
+designers, etc. investing their time and/or money into making it
+better. The Diaspora network is broken up into many servers, known as
+,(anchor [pods] "http://podupti.me").
+Users have the choice of which pod to store their data on.
+Pods assert no ownership over their user’s data, unlike Facebook, and
+do not use that data for targeted advertisements. Diaspora is still a
+rather young project, but it does everything that I need it to
+do. Goodbye, Facebook!])
+
+ (p [Since I’m a programmer, I naturally wanted to hack on some code and
+contribute. The main developers are very friendly and give great
+guidance to newcomers that want to help out. Every Monday is a “Bug
+Mash Monday”, where a list of open issues is presented to entice
+contributors to resolve them. In the past few weeks, I have made two
+contributions to the Diaspora project: a
+,(anchor [bug fix] "https://github.com/diaspora/diaspora/issues/2948")
+and a
+,(anchor [small feature] "https://github.com/diaspora/diaspora/issues/2948").
+Diaspora is very hackable and I encourage other developers
+with Ruby/Rails and Javascript knowledge to join in.])
+
+ (p [TL\;R: Diaspora is great. Create an account. Check out my
+,(anchor [profile] "https://joindiaspora.com/u/davexunit").
+Start sharing. Happy hacking. :\)]))
diff --git a/posts/2013-07-15-maine.skr b/posts/2013-07-15-maine.skr
new file mode 100644
index 0000000..0c8e7e9
--- /dev/null
+++ b/posts/2013-07-15-maine.skr
@@ -0,0 +1,34 @@
+(post
+ :title "Maine!"
+ :date (make-date* 2013 07 15)
+ :tags '("maine" "vacation")
+ :summary "My vacation in Maine"
+
+ (p [Every summer, my friends and I go to Maine for a week. We stay in
+the Rockland area in an old house on a blueberry field. This year we
+hiked a mountain in Camden, hiked Mt. Katahdin, and went to the
+Rockland Blues Festival.])
+
+ (p [Here are some pictures taken from my not-so-great cellphone camera:])
+
+ (image/caption "/images/maine-2013/view-from-house.jpg"
+ [The view from the driveway of our house.])
+
+ (image/caption "/images/maine-2013/zekes-lookout.jpg"
+ [Sign at Zeke's Lookout. Somewhere on a mountain near Camden.])
+
+ (image/caption "/images/maine-2013/katahdin-camp.jpg"
+ [Mt. Katahdin as seen from the foot of the Hunt trail.])
+
+ (image/caption "/images/maine-2013/katahdin-waterfall.jpg"
+ [A beautiful waterfall that can be seen about a mile
+into the hike.])
+
+ (image/caption "/images/maine-2013/katahdin-view-1.jpg" [Katahdin])
+ (image/caption "/images/maine-2013/katahdin-view-2.jpg" [Katahdin again])
+
+ (image/caption "/images/maine-2013/katahdin-fog.jpg"
+ [Thick fog past the treeline.])
+
+ (image/caption "/images/maine-2013/blues-festival.jpg"
+ [Closed down street packed with people and bands in Rockland.]) )
diff --git a/posts/2013-07-20-stumpwm-on-debian-wheezy.skr b/posts/2013-07-20-stumpwm-on-debian-wheezy.skr
new file mode 100644
index 0000000..2268872
--- /dev/null
+++ b/posts/2013-07-20-stumpwm-on-debian-wheezy.skr
@@ -0,0 +1,62 @@
+(post
+ :title "StumpWM on Debian Wheezy"
+ :date (make-date* 2013 07 20)
+ :tags '("stumpwm" "common lisp" "debian" "wheezy" "wsu")
+ :summary "First steps with StumpWM on Debian Wheezy"
+
+ (p [Everyone that's ever talked to me about software development
+knows that I am in love with Emacs. Emacs has a wonderful keyboard
+driven interface and is almost infinitely customizable via Emacs Lisp.
+I've done a lot of programming in Emacs from my not-so-great laptop
+lately. My laptop has a rather poor 1280x800 resolution and low
+performing integrated graphics chip. Until today, I was running the
+GNOME 3 desktop environment on it. Unlike most people (or perhaps
+just a loud minority), I like GNOME 3. However, I wanted something
+that was both less graphics intensive and more keyboard driven than
+GNOME Shell and Mutter.])
+
+ (p [Someone on IRC told me about
+,(anchor [StumpWM] "http://www.nongnu.org/stumpwm/"),
+a window manager written entirely in Common Lisp. I had heard of
+StumpWM before, but back then I wasn't an Emacs user and I've never
+really stuck with any tiling window manager that I've tried (DWM,
+Awesome). Now that I know the power of a fully programmable
+environment thanks to Emacs, I decided to give StumpWM a try. After
+some initial pains trying to get it to run, I am now using it very
+happily.])
+
+ (p [Here is what I had to do to get StumpWM running on Debian Wheezy.])
+
+ (ol
+ (li [Install StumpWM] (source-code "sudo apt-get install stumpwm"))
+
+ (li [Create an ,(code [.xinitrc]) file in my home directory with the
+following text]
+ (source-code "exec stumpwm"))
+
+ (li (p [Workaround clisp "component not found" errors])
+ (p [I could not get StumpWM to start until I created the
+following symbolic links:])
+ (source-code
+ "ln -s /usr/share/common-lisp/source/stumpwm/stumpwm.asd \\
+ /usr/share/common-lisp/systems/stumpwm.asd
+ln -s /usr/share/common-lisp/source/cl-ppcre/cl-ppcre.asd \\
+ /usr/share/common-lisp/systems/cl-ppcre.asd"))
+
+ (li (p [Start the X server])
+ (source-code "startx")
+ (p [I use the GNOME Desktop Manager, so I also created a session
+file for StumpWM in ,(code [/usr/share/xsessions/stumpwm.desktop.])])
+ (source-code
+ "[Desktop Entry]
+Encoding=UTF-8
+Name=StumpWM
+Comment=This session logs you into StumpWM
+Exec=stumpwm
+TryExec=stumpwm
+Icon=
+Type=Application")))
+
+ (p [I hope this brief installation guide can be of use to one of you
+out there in Internet land. Perhaps in the future I will write an
+article about customizing StumpWM with Common Lisp.]))
diff --git a/posts/2013-08-07-guile-2d.skr b/posts/2013-08-07-guile-2d.skr
new file mode 100644
index 0000000..f612977
--- /dev/null
+++ b/posts/2013-08-07-guile-2d.skr
@@ -0,0 +1,48 @@
+(post
+ :title "guile-2d - A 2D Game Development Framework for GNU Guile"
+ :date (make-date* 2013 08 07)
+ :tags '("foss" "gnu" "guile" "scheme" "gamedev" "wsu")
+ :summary "I have started work on a 2D game framework"
+
+ (p [This is the very first devlog entry for my pet project,
+guile-2d. As the title suggests, guile-2d is a 2D game development
+framework for
+,(anchor [GNU Guile] "http://gnu.org/s/guile"),
+a Scheme implementation that has the honor of being the official
+extension language of the GNU project. Guile is a language with a
+growing number of features, but it still lacks a large assortment of
+libraries. I like to do 2D game programming, and I saw a niche that
+needed to be filled. Python has Pygame, Lua has Love, but there's no
+fun and accessible game programming library for Guile. Guile-2d is
+working to correct that.])
+
+ (p [The goal of Guile-2d is to create an easy to use 2D game
+programming framework. Libraries like SDL give the programmer a
+rather low-level set of tools that they can use to build a game,
+guile-2d will provide high-level tools over low-level SDL and OpenGL
+for commonly used elements of 2D games: tile maps, sprite animation,
+particle systems, collision detection, vector math, A* pathfinding,
+etc. Such features will allow a game developer to very quickly
+produce a working prototype with guile-2d.])
+
+ (p [Guile-2d is a framework, which means that it has some opinion
+about what the right way to do things is. The most apparent example
+of this is the game loop. The game loop runs at 60 frames-per-second
+and uses fixed timestep updates. Those that have read
+,(anchor [Fix Your Timestep]
+ "http://gafferongames.com/game-physics/fix-your-timestep/")
+will know that this decision is a good thing.])
+
+ (p [Perhaps the most important feature of guile-2d is the ability to
+do “live coding”. When the game loop starts, a REPL
+\(read-eval-print-loop\) server is started. Using the great
+,(anchor [Geiser] "http://geiser.nongnu.org/")
+extension for Emacs to connect to the REPL server, one can modify
+their game as it is running. This gives users the power to evaluate
+some new code and see the changes reflected immediately in the game
+window. No need to restart the game unless you crash it!])
+
+ (p [This has been a brief overview of some of the features and goals
+of guile-2d. If this project interests you, you can check out the
+source code on
+,(anchor [Github] "https://github.com/davexunit/guile-2d").]))
diff --git a/posts/2013-08-08-angularjs-post-mortem.skr b/posts/2013-08-08-angularjs-post-mortem.skr
new file mode 100644
index 0000000..2f7a804
--- /dev/null
+++ b/posts/2013-08-08-angularjs-post-mortem.skr
@@ -0,0 +1,135 @@
+(post
+ :title "AngularJS Post-mortem"
+ :date (make-date* 2013 08 08)
+ :tags '("web" "javascript" "angularjs" "wsu")
+ :summary "AngularJS likes/dislikes and what went right/wrong"
+
+ (p [,(anchor [AngularJS] "http://angularjs.org/")
+is the new popular client-side Javascript application framework
+developed by Google. We have recently adopted it at Vista Higher
+Learning for building our latest features that require a lot
+client-side logic. Now that I have a few applications under my belt,
+it’s time to talk about my experience.])
+
+ (p [If you want a quick TL\;DR: I think AngularJS is good, but it has
+a steep learning curve and there’s no well defined set of best
+practices.])
+
+ (p [Note: I will be using plenty of terms that will probably only
+make sense for people that have used AngularJS.])
+
+ (h2 [The Good Stuff])
+
+ (p [These are the things that went well. The things that made me glad
+that we chose to use AngularJS.])
+
+ (h3 [Easier Testing])
+
+ (p [Our Javascript test suite uses
+,(anchor [Jasmine.] "http://pivotal.github.io/jasmine/")
+AngularJS is built with test frameworks like Jasmine in mind.
+AngularJS could tends to be easily testable due to dependency
+injection. When the components of an application don’t rely on global
+state, it is easier to mock services for unit tests.])
+
+ (h3 [Separation of Concerns])
+
+ (p [AngularJS stresses separating the view from the data structures
+and logic behind it. I think everyone that’s written a somewhat
+complex JQuery application can relate to the mess of CSS selectors and
+click callbacks that the code quickly degenerates into.])
+
+ (p [AngularJS allows you to break up the DOM into logical chunks that
+are handled by separate controllers. Treating the application as many
+small pieces working together rather than one giant DOM blob keeps the
+code clean. Message passing via
+,(code [$emit]) and ,(code [$broadcast])
+keeps controllers loosely coupled to each other.])
+
+ (h3 [No More JQuery Spaghetti])
+
+ (p [Directives, the strategy for encapsulating DOM manipulation, are
+wonderful. It is an elegant solution to the mess that was JQuery
+selectors and event callbacks. AngularJS comes with a lot of
+directives out of the box to handle the most common stuff like
+showing/hiding elements, handling clicks, dynamically setting CSS
+classes.])
+
+ (h3 [More Maintainable Code])
+
+ (p [AngularJS is feature-rich. It does a lot on your behalf, which
+greatly reduces the amount of boilerplate code needed to get a
+prototype up and running. I had the opportunity to essentially
+rewrite an existing JQuery application using AngularJS. The results
+were clear: The AngularJS version had fewer lines of code, was more
+readable, and was easier to debug.])
+
+ (h2 [Bumps in the Road])
+
+ (p [These are the things that didn’t go smoothly. They boil down to
+AngularJS having a steep learning curve and ill-informed software
+design decisions on my part.])
+
+ (h3 [Understanding the Magic])
+
+ (p [A lot of things seem to happen by magic. For example, it is
+possible to make a asynchronous request and get a promise object in
+return. When the promise is assigned to a variable on ,(code [$scope]),
+AngularJS not only knows to ignore it while the request hasn’t
+finished, but it will re-assign to that variable the value of the
+asynchronous call when it completes. It is a nice feature, but it
+takes some digging to find out what is really going on.])
+
+ (h3 [Poor Documentation])
+
+ (p [I know I’m not the only one that hates the official AngularJS
+documentation. The documentation is getting more complete, but it’s
+still not very useful. Functions frequently have a blurb describing
+what they do, but no explanation of the parameter list. It’s hard to
+use a function that doesn’t describe what it expects for input.])
+
+ (p [When the documentation confused us, which it did frequently, we
+turned to the AngularJS book from
+,(anchor [O’Reilly] "http://shop.oreilly.com/product/0636920028055.do")
+for help. I need to get around to reading more of it.])
+
+ (h3 [RESTful Resources and Rails])
+
+ (p [AngularJS claims to be targeted at CRUD applications, but using
+the HTTP backend and the ,(code [Resource]) abstraction that sits on
+top of it was particularly difficult. A good amount of time was spent
+on trying to get the HTTP requests from resources to conform to what
+our Rails backend expects, like root-wrapping.])
+
+ (h3 [Bloated Controllers])
+
+ (p [I frequently made controllers that had too much state and logic
+that should have been extracted into services/factories/etc. A
+controller would start out slim but would quickly grow to several
+hundred lines of code as features were added. Controllers should be
+the middle man between the view and the model and that’s it.])
+
+ (p [Some tell-tale signs that a controller is becoming bloated:])
+
+ (ul
+ (li [There are a lot of private functions (not defined on
+,(code [$scope]))])
+ (li [Functions are defined on ,(code [$scope]) just so you can
+unit-test them, but are never used in the template]))
+
+ (p [I attribute controller bloat to a lack of knowing the appropriate uses
+for other AngularJS components. It was easy to just keep adding to the
+controller.])
+
+ (h2 [Conclusion])
+
+ (p [Overall, I think things went well, but I (and the rest of my
+team) made a lot of beginner mistakes. But that’s the learning
+process, isn’t it?])
+
+ (p [Now that I know more about AngularJS, it will be easier to make
+better design decisions moving forward.])
+
+ (p [I believe that as AngularJS continues to mature, some concensus
+in the community about best practices will emerge that will make
+things easier for beginners.]))
diff --git a/posts/2013-08-11-the-little-schemer.skr b/posts/2013-08-11-the-little-schemer.skr
new file mode 100644
index 0000000..d4e4211
--- /dev/null
+++ b/posts/2013-08-11-the-little-schemer.skr
@@ -0,0 +1,58 @@
+(post
+ :title "The Little Schemer"
+ :date (make-date* 2013 08 11)
+ :tags '("scheme" "books" "wsu")
+ :summary "I bought “The Little Schemer”"
+
+ (p [Yesterday, I took a trip to the MIT Press Bookstore and picked up
+a copy of
+,(anchor [The Little Schemer] "http://mitpress.mit.edu/books/little-schemer").
+I’ve only spent a few hours reading and coding along with it, but I’ve
+had a lot of fun. The following is a mini-review based on my
+experience thus far.])
+
+ (p [“The Little Schemer” teaches you to think recursively using an
+interesting and comedic writing style and the Scheme programming
+language. While Scheme is the language of choice, the real goal is to
+teach you problem solving rather than the details of a specific
+language. The book starts off simple, explaining what atoms, lists,
+and s-expressions are. Rather than providing the definition and then
+showing examples, it first gives examples in the form of a question
+and answer.])
+
+ (p [Example:])
+
+ (blockquote
+ (p [Is it true that this an atom?])
+ (p (strong [atom]))
+ (p [Yes, because ,(strong [atom]) is a string of characters
+beginning with a letter.]))
+
+ (p [From the examples given, a definition is created. In later
+examples, a Scheme procedure is written that produces the correct
+answers for all of the questions stated before it. It’s fun to build
+the procedure, verify that it works for all cases, and compare your
+implementation with the book’s.])
+
+ (p [“The Little Schemer” defines ten commandments that are essential
+to correctly solving the problems in the book. Some commandments are
+first given in an incomplete form, and expanded later when a greater
+level of understanding has been achieved. The problems that you solve
+reinforce the commandments. You might notice that you start writing
+procedures without thinking much about it, much like the muscle memory
+earned from using Emacs a lot. Gerald J. Sussman was right when he
+said that this book “can perform the same service that Hanon’s finger
+exercises or Czerny’s piano studies perform for the student of the
+piano.” I have no idea who Hanon and Czerny are, but I get it. For
+the drummers out there, you could liken this book to
+,(anchor [Stick Control]
+ "http://www.amazon.com/Stick-Control-For-Snare-Drummer/dp/1892764040").])
+
+ (p [The writing style is very informal, comedic, and food themed.
+Page 13 has a space reserved for jelly stains, and page 52 tells you
+to “go cons a piece of cake onto your mouth.” I have laughed a number
+of times while reading. Oh, and let’s not forget about the cute
+elephant drawings. This is definitely not your average boring, dry
+computer science book. If you are interested in a unique and
+enjoyable learning experience, then I highly recommend reading “The
+Little Schemer”.]))
diff --git a/posts/2013-08-17-font-rendering-pango-cairo.skr b/posts/2013-08-17-font-rendering-pango-cairo.skr
new file mode 100644
index 0000000..59e0f96
--- /dev/null
+++ b/posts/2013-08-17-font-rendering-pango-cairo.skr
@@ -0,0 +1,328 @@
+(define guile-2d-url "https://github.com/davexunit/guile-2d")
+(define madsy9-url "http://www.reddit.com/user/Madsy9")
+(define madsy9-comment-url
+ "http://www.reddit.com/r/scheme/comments/1k739l/guile_2d_game_programming_lib_for_scheme/cbmnyuk")
+(define pango-url "http://www.pango.org/")
+(define cairo-url "http://cairographics.org/")
+;; TODO: Move to files.dthompson.us
+(define tarball-url "/src/pangocairo.tar.gz")
+
+(post
+ :title "Font Rendering in OpenGL with Pango and Cairo"
+ :date (make-date* 2013 08 17)
+ :tags '("opengl" "pango" "cairo" "font" "wsu")
+ :summary "A brief tutorial for rendering fonts in OpenGL with libpangocairo"
+
+ (p [I am working towards a 0.1 release of my game development
+ framework for GNU Guile, ,(anchor "guile-2d" guile-2d-url). One of
+ the few remaining blockers on my to-do list is font rendering. A
+ reddit user, ,(anchor "Madsy9" madsy9-url), pointed me in the right
+ direction with this ,(anchor "comment" madsy9-comment-url). There are
+ two libraries needed to perform nice font rendering with proper
+ internationalization support: ,(anchor "Pango" pango-url), “a library
+ for laying out and rendering of text, with an emphasis on
+ internationalization,” and ,(anchor "Cairo" cairo-url), “a 2D graphics
+ library with support for multiple output devices.”])
+
+ (p [It took me awhile to put together all of the pieces and build a
+ working sample program. The goal of this post is to help others that
+ may be trying to accomplish a similar task that have no prior
+ knowledge of Pango and Cairo. I will assume basic knowledge of C,
+ SDL, and OpenGL throughout this post.])
+
+ (p [Let's get the basic SDL and OpenGL initialization out of the way:])
+
+ (source-code
+ (c-source
+ "#include <pango/pangocairo.h>
+#include <SDL.h>
+#include <SDL_opengl.h>
+
+#define WINDOW_WIDTH 800
+#define WINDOW_HEIGHT 600
+#define FONT \"Sans Bold 18\"
+#define TEXT \"The quick brown fox is so かわいい!\"
+
+void
+init_sdl ()
+{
+ SDL_Init (SDL_INIT_EVERYTHING);
+ SDL_SetVideoMode (WINDOW_WIDTH, WINDOW_HEIGHT, 0, SDL_OPENGL);
+}
+
+void
+init_gl ()
+{
+ glClearColor (0.0f, 0.0f, 0.0f, 0.0f);
+ glDisable (GL_DEPTH_TEST);
+ glEnable (GL_BLEND);
+ glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable (GL_TEXTURE_2D);
+ glViewport (0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
+ glMatrixMode (GL_PROJECTION);
+ glLoadIdentity ();
+ glOrtho (0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, -1, 1);
+ glMatrixMode (GL_MODELVIEW);
+ glLoadIdentity ();
+}"))
+
+ (p [,(code "create_texture") simply creates an OpenGL texture given an array of
+ pixel data and the texture dimensions. Our Cairo surface will use BGRA
+ color.])
+
+ (source-code
+ (c-source
+ "unsigned int
+create_texture (unsigned int width,
+ unsigned int height,
+ unsigned char *pixels)
+{
+ unsigned int texture_id;
+
+ glGenTextures (1, &texture_id);
+ glBindTexture (GL_TEXTURE_2D, texture_id);
+ glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexImage2D (GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ width,
+ height,
+ 0,
+ GL_BGRA,
+ GL_UNSIGNED_BYTE,
+ pixels);
+
+ return texture_id;
+}"))
+
+ (p [,(code "draw_texture") clears the screen, renders a simple textured quad
+ using OpenGL’s immediate mode, and then swaps buffers.])
+
+ (source-code
+ (c-source
+ "void
+draw_texture (int width,
+ int height,
+ unsigned int texture_id)
+{
+ /* Render a texture in immediate mode. */
+ glMatrixMode (GL_MODELVIEW);
+ glLoadIdentity ();
+ glClear (GL_COLOR_BUFFER_BIT);
+ glPushMatrix ();
+ glBindTexture (GL_TEXTURE_2D, texture_id);
+ glColor3f (1.f, 1.0f, 1.0f);
+
+ glBegin (GL_QUADS);
+ glTexCoord2f (0.0f, 0.0f);
+ glVertex2f (0.0f, 0.0f);
+ glTexCoord2f (1.0f, 0.0f);
+ glVertex2f (width, 0.0f);
+ glTexCoord2f (1.0f, 1.0f);
+ glVertex2f (width , height);
+ glTexCoord2f (0.0f, 1.0f);
+ glVertex2f (0.0f, height);
+ glEnd ();
+
+ glPopMatrix ();
+ SDL_GL_SwapBuffers();
+}"))
+
+ (p [,(code [create_cairo_context]) is used to make a new Cairo context that
+draws to a raw data surface. The return value, a ,(code [cairo_t]), is the
+main object in Cairo. All drawing is done via a ,(code [cairo_t]) object. A
+context needs a surface to draw on.
+,(code [cairo_image_surface_create_for_data]) creates a raw data surface for
+us. We will be translating the surface into a texture later on.])
+
+ (source-code
+ (c-source
+ "cairo_t*
+create_cairo_context (int width,
+ int height,
+ int channels,
+ cairo_surface_t** surf,
+ unsigned char** buffer)
+{
+ *buffer = calloc (channels * width * height, sizeof (unsigned char));
+ *surf = cairo_image_surface_create_for_data (*buffer,
+ CAIRO_FORMAT_ARGB32,
+ width,
+ height,
+ channels * width);
+ return cairo_create (*surf);
+}"))
+
+ (p [,(code [create_layout_context]) also makes a new Cairo context, but this
+context is for PangoLayout objects. In Pango, a layout describes the
+style of a paragraph of text. The layout needs a context in order to
+function. We use ,(code [cairo_image_surface_create]) with dimensions of 0x0
+because we won’t actually be rendering to this surface. Instead, we
+will layout our text and use ,(code [create_cairo_context]) to build a
+context with a surface that is the size of the rendered text. Cairo
+uses reference counting for dynamically allocated objects, so we need
+to call ,(code [cairo_surface_destroy]) when we’re done with the temporary
+surface. The context still maintains a reference to the surface, so
+the memory for the surface will not be freed until the context is.])
+
+ (source-code
+ (c-source
+ "cairo_t*
+create_layout_context ()
+{
+ cairo_surface_t *temp_surface;
+ cairo_t *context;
+
+ temp_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 0, 0);
+ context = cairo_create (temp_surface);
+ cairo_surface_destroy (temp_surface);
+
+ return context;
+}"))
+
+ (p [,(code [get_text_size]) tells us the size of the text that’s in the layout,
+in pixels. Pango’s units are not in pixels, so we must divide by
+,(code [PANGO_SCALE]) in order to get pixel units.])
+
+ (source-code
+ (c-source
+ "void
+get_text_size (PangoLayout *layout,
+ unsigned int *width,
+ unsigned int *height)
+{
+ pango_layout_get_size (layout, width, height);
+ /* Divide by pango scale to get dimensions in pixels. */
+ *width /= PANGO_SCALE;
+ *height /= PANGO_SCALE;
+}"))
+
+ (p [,(code [render_text]) is where all of the magic happens. First, we create a
+layout with a layout context and set the text that we will render with
+this layout. ,(code [TEXT]) is defined earlier in the program as “The quick
+brown fox is so かわいい!”])
+
+ (p [Then we create a ,(code [PangoFontDescription]) object. This object
+represents the font that we want to render. Earlier in the program,
+,(code [FONT]) is defined as “Sans Bold 18”. Pango is able to figure out
+how to load a font from a string in this format. Your system must be
+able to recognize the font family and font face, though. I haven’t
+yet figured out how to have Pango render an arbitrary font from a
+,(code [*.ttf]) file.])
+
+ (p [Next, we create a rendering context by getting the layout’s size and
+creating a context with a surface big enough to show all of the
+rendered text.])
+
+ (p [Finally, we set the font color to white, render the text to the
+surface with ,(code [pango_cairo_show_layout]), and create an OpenGL texture
+from the surface. We also clean up all the objects that we no longer
+need before returning.])
+
+ (source-code
+ (c-source
+ "unsigned int
+render_text (const char *text,
+ unsigned int *text_width,
+ unsigned int *text_height,
+ unsigned int *texture_id)
+{
+ cairo_t *layout_context;
+ cairo_t *render_context;
+ cairo_surface_t *temp_surface;
+ cairo_surface_t *surface;
+ unsigned char* surface_data = NULL;
+ PangoFontDescription *desc;
+ PangoLayout *layout;
+
+ layout_context = create_layout_context ();
+
+ /* Create a PangoLayout, set the font and text */
+ layout = pango_cairo_create_layout (layout_context);
+ pango_layout_set_text (layout, text, -1);
+
+ /* Load the font */
+ desc = pango_font_description_from_string (FONT);
+ pango_layout_set_font_description (layout, desc);
+ pango_font_description_free (desc);
+
+ /* Get text dimensions and create a context to render to */
+ get_text_size (layout, text_width, text_height);
+ render_context = create_cairo_context (*text_width,
+ *text_height,
+ 4,
+ &surface,
+ &surface_data);
+
+ /* Render */
+ cairo_set_source_rgba (render_context, 1, 1, 1, 1);
+ pango_cairo_show_layout (render_context, layout);
+ *texture_id = create_texture(*text_width, *text_height, surface_data);
+
+ /* Clean up */
+ free (surface_data);
+ g_object_unref (layout);
+ cairo_destroy (layout_context);
+ cairo_destroy (render_context);
+ cairo_surface_destroy (surface);
+}"))
+
+ (p [,(code [main]) is pretty simple. We initialize SDL and OpenGL, render text
+to a texture, and enter the rendering loop. The program will run
+until you click the close button, press “enter”, or press “q”.])
+
+ (source-code
+ (c-source
+ "int main (int argc, char **argv)
+{
+ SDL_Event event;
+ int keep_running = 1;
+ unsigned int texture_id;
+ unsigned int text_width = 0;
+ unsigned int text_height = 0;
+
+ init_sdl ();
+ init_gl ();
+ render_text(TEXT,
+ &texture_id,
+ &text_width,
+ &text_height);
+
+ /* Update/render loop */
+ while (keep_running) {
+ SDL_PollEvent (&event);
+
+ switch (event.type) {
+ case SDL_QUIT :
+ keep_running = 0;
+ break;
+
+ case SDL_KEYDOWN :
+ if (event.key.keysym.sym == SDLK_ESCAPE)
+ keep_running = 0;
+ if (event.key.keysym.sym == SDLK_q)
+ keep_running = 0;
+ break;
+ }
+
+ draw_texture (texture_id, text_width, text_height);
+ SDL_Delay (16);
+ }
+
+ /* Clean up */
+ glDeleteTextures (1, &texture_id);
+
+ SDL_Quit();
+
+ return 0;
+}"))
+
+ (p [And we’re done! You should now be able to render some text in an
+OpenGL context. I hope this brief tutorial was helpful. Font rendering
+isn’t easy, and it’s not really my area of interest. I’m glad that
+Pango exists to do all of the real work for me so that I can more
+quickly move on to the parts of graphics programming that I actually
+enjoy.])
+
+ (p [You can download the full source code ,(anchor [here] tarball-url).]))
diff --git a/posts/2013-09-22-liberating-a-thinkpad-x220.skr b/posts/2013-09-22-liberating-a-thinkpad-x220.skr
new file mode 100644
index 0000000..a527482
--- /dev/null
+++ b/posts/2013-09-22-liberating-a-thinkpad-x220.skr
@@ -0,0 +1,68 @@
+(post
+ :title "Liberating a Thinkpad X220"
+ :date (make-date* 2013 09 22)
+ :tags '("thinkpad" "free software" "wsu")
+ :summary "I bought a used Thinkpad X220 and made it more free
+software friendly."
+
+ (p [I had been looking for a suitable replacement to my old, slow
+Compaq laptop that I purchased during my freshman year of college when
+I had very little money. What I liked about my old laptop was that it
+played well with free software. I had no trouble getting all of my
+hardware to work out-of-the-box with fully free GNU/Linux
+distributions such as Trisquel, and I wanted any future laptops of
+mine to play nicely, too.])
+
+ (p [I have heard much praise for Thinkpads over the years. Solid
+build quality, utilitarian design, and cheap to buy used. However,
+upon further reading, I realized that most newer Thinkpads require
+nonfree software in order to the drive the Intel wireless chip.
+Furthermore, there was DRM present in the BIOS that would prevent the
+installation of PCIe wireless chips that weren't in the whitelist.])
+
+ (p [This really bummed me out, but I bought a Thinkpad anyway. I
+found a great deal on a used X220 on ebay for $400. In order to
+liberate it, I had to make a small deal with the devil: Use the
+pre-installed Windows 7 to flash a hacked BIOS that removes the
+whitelist. I could only find the needed BIOS as a Windows executable,
+so I didn't have much choice. This process left me hoping that
+coreboot gains wider adoption.])
+
+ (p [Once I had verified that I didn't brick my Thinkpad, I installed
+the new wireless card. I purchased a Wireless N, half-height, mini
+PCIe card from
+,(anchor [Thinkpenguin]
+ "https://www.thinkpenguin.com/gnu-linux/penguin-wireless-n-half-height-mini-pcie-card").
+It uses an Atheros chipset and is free software compatible. I met
+Chris, the owner of Thinkpenguin, at this year's Northeast GNU/Linux
+Fest at Harvard. He is doing some great work and I wanted to support
+his business. It was nice to buy from someone who could assure me
+that the hardware I purchased is fully supported on a libre GNU/Linux
+distribution.])
+
+ (p [Now that my Thinkpad was free (sans BIOS, of course), it was time
+for the final touch. I replaced the hard drive with a 128GB SSD and
+installed Debian testing. It takes roughly 9 seconds to get from GRUB
+to the GDM login screen. It feels very nice to have a device that
+boots so quickly.])
+
+ (p [Now that everything had been installed and configured, I was able
+to start hacking and get a feel for things. The keyboard is the
+nicest I've ever used on a laptop. The
+,(anchor [TrackPoint] "http://xkcd.com/243/")
+is quite a nice way to move around once you get used to it. The
+ThinkLight is pretty neat when you're in a dark area. The battery
+life is extremely impressive. I don't know exactly how long it lasts
+yet but I never have to charge it while I am using it. I was lucky if
+I got 2 hours of battery life out of my old laptop, which caused me to
+be constantly tethered to an AC adapter. The screen is matte, which
+is awesome because it's very difficult to use a laptop outdoors when
+the screen is glossy. 1366x768 is not an ideal resolution (16:9 be
+damned), but I can live with it on a 12.5\" screen. Last but not
+least, I honestly like the aesthetics. A lot of people are enamored
+with the brushed aluminum designs by that fruit company, but I love
+the flat black, functional design of the Thinkpad.])
+
+ (p [I hope to really break this thing in over the weekend at the
+,(anchor [GNU 30th Anniversary] "https://www.gnu.org/gnu30/")
+hackathon. :\)]))
diff --git a/posts/2013-09-27-guile-2d-0.1.skr b/posts/2013-09-27-guile-2d-0.1.skr
new file mode 100644
index 0000000..0a5f01f
--- /dev/null
+++ b/posts/2013-09-27-guile-2d-0.1.skr
@@ -0,0 +1,31 @@
+(post
+ :title "Guile-2D 0.1 Released"
+ :date (make-date* 2013 09 27)
+ :tags '("foss" "gnu" "guile" "scheme" "gamedev" "wsu")
+ :summary "Official release of Guile-2D Version 0.1"
+
+ (p [To celebrate the GNU Project's 30th anniversary, I have decided
+to make the very first release of my 2D game development framework for
+,(anchor [GNU Guile] "http://gnu.org/software/guile").
+GNU Guile is a Scheme implementation, and has the honor of being the
+official extension language of the GNU project. Guile-2D is a layer
+above SDL, OpenGL, FreeImage, and FTGL that provides abstractions for
+common 2D game programming requirements such as sprites, tilesets,
+animations, scripting, and collision detection.])
+
+ (p [There is a lot of work to do in order to get Guile-2D up to snuff
+with the game libraries for more popular languages like Python and
+Lua. I am looking for contributors who share my vision of creating a
+fully featured, easy to use game library in Scheme.])
+
+ (p [Guile-2D currently supports GNU/Linux distributions. I am
+looking for help to get it running on OS X and Windows.])
+
+ (p [Please refer to the ,(code [INSTALL.org]), ,(code [README.org]),
+and texinfo files to learn how to install Guile-2D, run example
+programs, and write your own games.])
+
+ ;; TODO: Move to files.dthompson.us
+ (p (anchor [Download the release tarball] "/src/guile-2d-0.1.tar.gz"))
+ (p (anchor [Browse the source code on GitHub]
+ "https://github.com/davexunit/guile-2d")))
diff --git a/posts/2015-04-10-sxml-html-guile.skr b/posts/2015-04-10-sxml-html-guile.skr
new file mode 100644
index 0000000..beea865
--- /dev/null
+++ b/posts/2015-04-10-sxml-html-guile.skr
@@ -0,0 +1,235 @@
+(post
+ :title "Rendering HTML with SXML and GNU Guile"
+ :date (make-date* 2015 04 10)
+ :tags '("gnu" "guile" "wsu")
+ :summary "With a little effort, SXML can be used for HTML templates"
+
+ (p [GNU Guile provides modules for working with XML documents called
+ SXML. SXML provides an elegant way of writing XML documents as
+ s-expressions that can be easily manipulated in Scheme. Here’s an
+ example:])
+
+ (source-code
+ (scheme-source
+ "(sxml->xml '(foo (bar (@ (attr \"something\")))))"))
+
+ (source-code
+ (xml-source
+ "<foo><bar attr=\"something\" /></foo>"))
+
+ (p [I don’t know about you, but I work with HTML documents much more
+ often than XML. Since HTML is very similar to XML, we should be able
+ to represent it with SXML, too!])
+
+ (source-code
+ (scheme-source
+ "(sxml->xml
+ '(html
+ (head
+ (title \"Hello, world!\")
+ (script (@ (src \"foo.js\"))))
+ (body
+ (h1 \"Hello!\"))))"))
+
+ (source-code
+ (xml-source
+ "<html>
+ <head>
+ <title>Hello, world!</title>
+ <script src=\"foo.js\" /> <!-- what? -->
+ </head>
+ <body>
+ <h1>Hello!</h1>
+ </body>
+</html>"))
+
+ (p [That ,(code [<script>]) tag doesn’t look right! Script tags
+ don’t close themselves like that. Well, we could hack around it:])
+
+ (source-code
+ (scheme-source
+ "(sxml->xml
+ '(html
+ (head
+ (title \"Hello, world!\")
+ (script (@ (src \"foo.js\")) \"\"))
+ (body
+ (h1 \"Hello!\"))))"))
+
+ (source-code
+ (xml-source
+ "<html>
+ <head>
+ <title>Hello, world!</title>
+ <script src=\"foo.js\"></script>
+ </head>
+ <body>
+ <h1>Hello!</h1>
+ </body>
+</html>"))
+
+ (p [There’s no bug. The improper rendering happens because HTML, while
+ similar to XML, has some different syntax rules. Instead of using
+ ,(code [sxml->xml]) a new procedure that is tailored to the HTML syntax
+ is needed. Introducing ,(code [sxml->html]):])
+
+ (source-code
+ (scheme-source
+ "(define* (sxml->html tree #:optional (port (current-output-port)))
+ \"Write the serialized HTML form of TREE to PORT.\"
+ (match tree
+ (() *unspecified*)
+ (('doctype type)
+ (doctype->html type port))
+ (((? symbol? tag) ('@ attrs ...) body ...)
+ (element->html tag attrs body port))
+ (((? symbol? tag) body ...)
+ (element->html tag '() body port))
+ ((nodes ...)
+ (for-each (cut sxml->html <> port) nodes))
+ ((? string? text)
+ (string->escaped-html text port))
+ ;; Render arbitrary Scheme objects, too.
+ (obj (object->escaped-html obj port))))"))
+
+ (p [In addition to being aware of void elements and escape
+characters, it can also render ,(code ['(doctype "html")]) as
+,(code [<!DOCTYPE html>]). If we replace ,(code [sxml->xml])
+with ,(code [sxml->html]) in the failing example above we can see
+that it does the right thing.])
+
+ (source-code
+ (scheme-source
+ "(sxml->html
+ '((script (@ (src \"foo.js\")))
+ \"Copyright © 2015 David Thompson <davet@gnu.org>\"))"))
+
+ (source-code
+ (xml-source
+ "<script src=\"foo.js\"></script>
+Copyright © 2015 David Thompson &lt;davet@gnu.org&gt;"))
+
+ (p [Here’s the full version of my ,(code "(sxml html)") module. It’s
+quite brief, which is a nice bonus. This code requires Guile 2.0.11
+or greater.])
+
+ (p [Happy hacking!])
+
+ (source-code
+ (scheme-source
+ ";;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;;
+;;; This library is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU Lesser General Public License
+;;; as published by the Free Software Foundation; either version 3 of
+;;; the License, or (at your option) any later version.
+;;;
+;;; This library is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with this library. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (sxml html)
+ #:use-module (sxml simple)
+ #:use-module (srfi srfi-26)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 hash-table)
+ #:export (sxml->html))
+
+(define %void-elements
+ '(area
+ base
+ br
+ col
+ command
+ embed
+ hr
+ img
+ input
+ keygen
+ link
+ meta
+ param
+ source
+ track
+ wbr))
+
+(define (void-element? tag)
+ \"Return #t if TAG is a void element.\"
+ (pair? (memq tag %void-elements)))
+
+(define %escape-chars
+ (alist->hash-table
+ '((#\\\" . \"quot\")
+ (#\\& . \"amp\")
+ (#\\' . \"apos\")
+ (#\\< . \"lt\")
+ (#\\> . \"gt\"))))
+
+(define (string->escaped-html s port)
+ \"Write the HTML escaped form of S to PORT.\"
+ (define (escape c)
+ (let ((escaped (hash-ref %escape-chars c)))
+ (if escaped
+ (format port \"&~a;\" escaped)
+ (display c port))))
+ (string-for-each escape s))
+
+(define (object->escaped-html obj port)
+ \"Write the HTML escaped form of OBJ to PORT.\"
+ (string->escaped-html
+ (call-with-output-string (cut display obj <>))
+ port))
+
+(define (attribute-value->html value port)
+ \"Write the HTML escaped form of VALUE to PORT.\"
+ (if (string? value)
+ (string->escaped-html value port)
+ (object->escaped-html value port)))
+
+(define (attribute->html attr value port)
+ \"Write ATTR and VALUE to PORT.\"
+ (format port \"~a=\\\"\" attr)
+ (attribute-value->html value port)
+ (display #\\\" port))
+
+(define (element->html tag attrs body port)
+ \"Write the HTML TAG to PORT, where TAG has the attributes in the
+list ATTRS and the child nodes in BODY.\"
+ (format port \"<~a\" tag)
+ (for-each (match-lambda
+ ((attr value)
+ (display #\\space port)
+ (attribute->html attr value port)))
+ attrs)
+ (if (and (null? body) (void-element? tag))
+ (display \" />\" port)
+ (begin
+ (display #\\> port)
+ (for-each (cut sxml->html <> port) body)
+ (format port \"</~a>\" tag))))
+
+(define (doctype->html doctype port)
+ (format port \"<!DOCTYPE ~a>\" doctype))
+
+(define* (sxml->html tree #:optional (port (current-output-port)))
+ \"Write the serialized HTML form of TREE to PORT.\"
+ (match tree
+ (() *unspecified*)
+ (('doctype type)
+ (doctype->html type port))
+ (((? symbol? tag) ('@ attrs ...) body ...)
+ (element->html tag attrs body port))
+ (((? symbol? tag) body ...)
+ (element->html tag '() body port))
+ ((nodes ...)
+ (for-each (cut sxml->html <> port) nodes))
+ ((? string? text)
+ (string->escaped-html text port))
+ ;; Render arbitrary Scheme objects, too.
+ (obj (object->escaped-html obj port))))")))
diff --git a/posts/2015-08-30-ruby-on-guix.skr b/posts/2015-08-30-ruby-on-guix.skr
new file mode 100644
index 0000000..5da4ab4
--- /dev/null
+++ b/posts/2015-08-30-ruby-on-guix.skr
@@ -0,0 +1,578 @@
+(post
+ :title "Ruby on Guix"
+ :date (make-date* 2015 08 30)
+ :tags '("gnu" "guix" "scheme" "guile" "ruby" "wsu")
+ :summary "How to use Guix + some elbow grease to replace RVM and
+Bundler on GNU/Linux"
+
+ (p [I’ve been working with Ruby professionally for over 3 years now
+and I’ve grown frustrated with two of its most popular development
+tools: RVM and Bundler. For those that may not know, RVM is the Ruby
+version manager and it allows unprivileged users to download, compile,
+install, and manage many versions of Ruby instead of being stuck with
+the one that is installed globally by your distro’s package manager.
+Bundler is the tool that allows developers to keep a version
+controlled “Gemfile” that specifies all of the project’s dependencies
+and provides utilities to install and update those gems. These tools
+are crucial because Ruby developers often work with many applications
+that use different versions of Ruby and/or different versions of gems
+such as Rails. Traditional GNU/Linux distributions install packages
+to the global ,(code [/usr]) directory, limiting users to a single
+version of Ruby and associated gems, if they are packaged at all.
+Traditional package management fails to meet the needs of a lot of
+users, so many niche package managers have been developed to
+supplement them.])
+
+ (source-code
+ (scheme-source
+ ";;; guile-syntax-highlight --- General-purpose syntax highlighter
+;;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;;
+;;; Guile-syntax-highlight is free software; you can redistribute it
+;;; and/or modify it under the terms of the GNU Lesser General Public
+;;; License as published by the Free Software Foundation; either
+;;; version 3 of the License, or (at your option) any later version.
+;;;
+;;; Guile-syntax-highlight is distributed in the hope that it will be
+;;; useful, but WITHOUT ANY WARRANTY; without even the implied
+;;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+;;; See the GNU Lesser General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU Lesser General Public
+;;; License along with guile-syntax-highlight. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Lexing utilities.
+;;
+;;; Code:
+
+(define-module (syntax-highlight lexers)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 regex)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-11)
+ #:use-module (srfi srfi-26)
+ #:export (make-cursor
+ cursor?
+ cursor-text
+ cursor-position
+ cursor-end?
+ move-cursor
+ move-cursor-by
+
+ lex-fail
+ lex-bind
+ lex-return
+ lex-lift
+ lex-map
+ lex-filter
+ lex-any*
+ lex-any
+ lex-all*
+ lex-all
+ lex-consume
+ lex-regexp
+ lex-string
+ lex-char-set
+ lex-delimited
+ lex-tag))
+
+(define (string-prefix?* s1 s2 start-s2)
+ (string-prefix? s1 s2 0 (string-length s1) start-s2))
+
+
+;;;
+;;; Cursor
+;;;
+
+(define-record-type <cursor>
+ (make-cursor text position)
+ cursor?
+ (text cursor-text)
+ (position cursor-position))
+
+(define (cursor-end? cursor)
+ \"Return #t if the cursor is at the end of the text.\"
+ (>= (cursor-position cursor) (string-length (cursor-text cursor))))
+
+(define (move-cursor cursor position)
+ \"Move CURSOR to the character at POSITION.\"
+ (make-cursor (cursor-text cursor) position))
+
+(define (move-cursor-by cursor offset)
+ \"Move CURSOR by OFFSET characters relative to its current
+position.\"
+ (move-cursor cursor (+ (cursor-position cursor) offset)))
+
+
+;;;
+;;; Lexers
+;;;
+
+(define (lex-fail cursor)
+ \"Always fail to lex STR without consuming any of it.\"
+ (values #f cursor))
+
+(define (lex-bind proc lexer)
+ \"Return a lexer that applies the result of LEXER to PROC, a
+procedure that returns a lexer, and then applies that new lexer.\"
+ (lambda (cursor)
+ (let-values (((result remainder) (lexer cursor)))
+ (if result
+ ((proc result) remainder)
+ (lex-fail cursor)))))
+
+(define (lex-return x)
+ \"Return a lexer that always yields X as the lex result.\"
+ (lambda (cursor)
+ (values x cursor)))
+
+(define (lex-lift proc)
+ \"Return a procedure that wraps the result of PROC in a lexer.\"
+ (lambda args
+ (lex-return (apply proc args))))
+
+(define (lex-map proc lexer)
+ \"Return a lexer that applies PROC to result of LEXER.\"
+ (lex-bind (lex-lift proc) lexer))
+
+(define (lex-any* lexers)
+ \"Return a lexer that succeeds with the result of the first
+successful lexer in LEXERS or fails if all lexers fail.\"
+ (define (either a b)
+ (lambda (cursor)
+ (let-values (((result remainder) (a cursor)))
+ (if result
+ (values result remainder)
+ (b cursor)))))
+
+ (fold-right either lex-fail lexers))
+
+(define (lex-any . lexers)
+ \"Return a lexer that succeeds with the result of the first
+successful lexer in LEXERS or fails if all lexers fail.\"
+ (lex-any* lexers))
+
+(define (lex-all* lexers)
+ \"Return a lexer that succeeds with the results of all LEXERS in
+order, or fails if any lexer fails.\"
+ (define (both a b)
+ (lambda (cursor)
+ (let-values (((result-a remainder-a) (a cursor)))
+ (if result-a
+ (let-values (((result-b remainder-b) (b remainder-a)))
+ (if result-b
+ (values (cons result-a result-b) remainder-b)
+ (lex-fail cursor)))
+ (lex-fail cursor)))))
+
+ (fold-right both (lex-return '()) lexers))
+
+(define (lex-all . lexers)
+ \"Return a lexer that succeeds with the results of all LEXERS in
+order, or fails if any lexer fails.\"
+ (lex-all* lexers))
+
+(define (lex-consume lexer)
+ \"Return a lexer that always succeeds with a list of as many
+consecutive successful applications of LEXER as possible, consuming
+the entire input text. Sections of text that could not be lexed are
+returned as plain strings.\"
+ (define (substring* cursor start)
+ (substring (cursor-text cursor) start (cursor-position cursor)))
+
+ (lambda (cursor)
+ (let loop ((cursor cursor)
+ (memo '())
+ (fail-start #f))
+ (if (cursor-end? cursor)
+ (values (reverse memo) cursor)
+ (let-values (((result remainder) (lexer cursor)))
+ (cond
+ ;; Regular successful result.
+ ((and result (not fail-start))
+ (loop remainder (cons result memo) #f))
+ ;; Successful result after some amount of unmatched
+ ;; characters.
+ ((and result fail-start)
+ (loop remainder
+ (cons* result (substring* cursor fail-start) memo)
+ #f))
+ ;; Consecutive failure.
+ (fail-start
+ (loop (move-cursor-by cursor 1)
+ memo
+ fail-start))
+ ;; First failure.
+ (else
+ (loop (move-cursor-by cursor 1)
+ memo
+ (cursor-position cursor)))))))))
+
+(define (lex-regexp pattern)
+ \"Return a lexer that succeeds with the matched substring when the
+input matches the string PATTERN.\"
+ (let ((rx (make-regexp (string-append \"^\" pattern))))
+ (lambda (cursor)
+ (if (cursor-end? cursor)
+ (lex-fail cursor)
+ (let ((result (regexp-exec rx (cursor-text cursor)
+ (cursor-position cursor))))
+ (if result
+ (let ((str (match:substring result 0)))
+ (values str (move-cursor-by cursor (string-length str))))
+ (lex-fail cursor)))))))
+
+(define (lex-string str)
+ \"Return a lexer that succeeds with STR when the input starts with
+STR.\"
+ (lambda (cursor)
+ (if (string-prefix?* str (cursor-text cursor) (cursor-position cursor))
+ (values str (move-cursor-by cursor (string-length str)))
+ (lex-fail cursor))))
+
+(define (lex-char-set char-set)
+ \"Return a lexer that succeeds with the nonempty input prefix that
+matches CHAR-SET, or fails if the first input character does not
+belong to CHAR-SET.\"
+ (define (char-set-substring str start)
+ (let ((len (string-length str)))
+ (let loop ((index start))
+ (cond
+ ((>= index len)
+ (substring str start len))
+ ((char-set-contains? char-set (string-ref str index))
+ (loop (1+ index)))
+ (else
+ (substring str start index))))))
+
+ (lambda (cursor)
+ (match (char-set-substring (cursor-text cursor) (cursor-position cursor))
+ (\"\" (lex-fail cursor))
+ (str (values str (move-cursor-by cursor (string-length str)))))))
+
+(define* (lex-delimited open #:key (until open) (escape #\\) nested?)
+ \"Return a lexer that succeeds with the string delimited by the
+opening string OPEN and the closing string UNTIL. Characters within
+the delimited expression may be escaped with the character ESCAPE. If
+NESTED?, allow for delimited expressions to be arbitrarily nested
+within.\"
+ (define (delimit str start)
+ (let ((len (string-length str)))
+ (let loop ((index start))
+ (cond
+ ;; Out of bounds.
+ ((>= index len)
+ len)
+ ;; Escape character.
+ ((eqv? escape (string-ref str index))
+ (loop (+ index 2)))
+ ;; Closing delimiter.
+ ((string-prefix?* until str index)
+ (+ index (string-length until)))
+ ;; Nested delimited string.
+ ((and nested? (string-prefix?* open str index))
+ (loop (delimit str (+ index (string-length open)))))
+ (else
+ (loop (1+ index)))))))
+
+ (lambda (cursor)
+ (let ((str (cursor-text cursor))
+ (pos (cursor-position cursor)))
+ (if (string-prefix?* open str pos)
+ (let ((end (delimit str (+ pos (string-length open)))))
+ (values (substring str pos end) (move-cursor cursor end)))
+ (lex-fail cursor)))))
+
+(define (lex-tag tag lexer)
+ \"Wrap the results of LEXER in a two-element tuple whose head is
+TAG.\"
+ (lex-map (cut list tag <>) lexer))
+"))
+
+ (p [Taking a step back, it becomes apparent that dependency isolation
+is a general problem that isn’t confined to software written in Ruby:
+Node has npm and nvm, Python has pip and virtualenv, and so on. A big
+limitation of all these language-specific package managers is that
+they cannot control what is outside of their language domain. In
+order to use RVM to successfully compile a version of Ruby, you need
+to make sure you have the GCC toolchain, OpenSSL, readline, libffi,
+etc. installed using the system package manager (note: I’ve seen RVM
+try to build prerequisites like OpenSSL before, which I then disabled
+to avoid duplication and security issues and I recommend you do the
+same.) In order to use Bundler to install Nokogiri, you need to make
+sure libxml2 has been installed using the system package manager. If
+you work with more than a single language, the number of different
+package management tools needed to get work done is staggering. For
+web applications, it’s not uncommon to use RVM, Bundler, NPM, Bower,
+and the system package manager simultaneously to get all of the
+necessary programs and libraries. Large web applications are
+notoriously difficult to deploy, and companies hire a bunch of
+operations folk like me to try to wrangle it all.])
+
+ (p [Anyway, let’s forget about Node, Python, etc. and just focus on
+Ruby. Have you or someone you work with encountered hard to debug
+issues and Git merge conflicts due to a problem with
+,(code [Gemfile.lock])? Bundler’s fast and loose versioning in the
+,(code [Gemfile]) (e.g. ,(code [rails >= 4.0])) causes headaches when
+different users update different gems at different times and check the
+resulting auto-generated ,(code [Gemfile.lock]) into the repository.
+Have you ever been frustrated that it’s difficult to deduplicate gems
+that are shared between multiple bundled gem sets? Have you looked at
+the ,(anchor [RVM home page] "https://rvm.io") and been frustrated
+that they recommend you to ,(code [curl bash]) to install their
+software? Have you been annoyed by RVM’s strange system of overriding
+shell built-ins in order to work its magic? I’m not sure how you
+feel, dear reader, but my Ruby environments feel like one giant,
+brittle hack, and I’m often enough involved in fixing issues with them
+on my own workstation, that of my colleagues, and on production
+servers.])
+
+ (p [So, if you’re still with me, what do we do about this? How can we
+work to improve upon the status quo? Just use Docker? Docker is
+helpful, and certainly much better than no isolation at all, but it
+hides the flaws of package management inside an opaque disk image and
+restricts the environments in which your application is built to
+function. The general problem of dependency isolation is orthogonal
+to the runtime environment, be it container, virtual machine, or “bare
+metal.” Enter functional package management. What does it mean for a
+package manager to be functional? GNU Guix, the functional package
+manager that I contribute to and recommend, has this to say:])
+
+ (blockquote
+ (p [GNU Guix is a functional package management tool for the GNU
+system. Package management consists of all activities that relate
+to building packages from sources, honoring their build-time and
+run-time dependencies, installing packages in user environments,
+upgrading installed packages to new versions or rolling back to a
+previous set, removing unused software packages, etc.])
+ (p [The term functional refers to a specific package management
+discipline. In Guix, the package build and installation process
+is seen as a function, in the mathematical sense. That function
+takes inputs, such as build scripts, a compiler, and libraries,
+and returns an installed package.]))
+
+ (p [Guix has a rich set of features, some of which you may find in
+other package managers, but not all of them (unless you use another
+functional package manager such as Nix.) Gem/Bundler can do
+unprivileged gem installation, but it cannot do transactional upgrades
+and rollbacks or install non-Ruby dependencies. Dpkg/yum/pacman can
+install all build-time and runtime dependencies, but it cannot do
+unprivileged package installation to isolated user environments. And
+none of them can precisely describe the ,(em [full]) dependency
+graph (all the way down to the C compiler’s compiler) but ,(em [Guix
+can]).])
+
+ (p [Guix is written in Guile, an implementation of the Scheme
+programming language. The upcoming release of Guix will feature a
+Ruby build system that captures the process of installing gems from
+,(code [.gem]) archives and a RubyGems import utility to make it
+easier to write Guix packages by using the metadata available on
+,(anchor "RubyGems.org" "https://rubygems.org"). Ruby developers
+interested in functional package management are encouraged to try
+packaging their gems (and dependencies) for Guix.])
+
+ (p [Now, how exactly can Guix replace RVM and Bundler? Guix uses an
+abstraction called a “profile” that represents a user-defined set of
+packages that should work together. Think of it as having many
+,(code [/usr]) file system trees that can be used in isolation from the
+others (without invoking virtualization technologies such as virtual
+machines or containers.) To install multiple versions of Ruby and
+various gems, the user need only create a separate profile for them:])
+
+ (source-code
+ "$ guix package --profile=project-1 --install ruby-2.2 ruby-rspec-3
+# Hypothetical packages:
+$ guix package --profile=project-2 --install ruby-1.9 ruby-rspec-2")
+
+ (p [A profile is a “symlink forest” that is the union of all the
+packages it includes, and files are deduplicated among all of them.
+To actually use the profile, the relevant environment variables must
+be configured. Guix is aware of such variables, and can tell you what
+to set by running the following:])
+
+ (source-code
+ "$ guix package --search-paths --profile=project-1")
+
+ (p [Additionally, you can also create ad-hoc development environments
+with the ,(code [guix environment]) tool. This tool will spawn a
+sub-shell (or another program of your choice) in an environment in
+which a set of specified packages are available. This is my preferred
+method as it automagically sets all of the environment variables for
+me and Guix is free to garbage collect the packages when I close the
+sub-shell:])
+
+ (source-code
+ "# Launch a Ruby REPL with ActiveSupport available.
+$ guix environment --ad-hoc ruby ruby-activesupport -E irb")
+
+ (p [In order to make this environment reproducible for others, I
+recommend keeping a ,(code [package.scm]) file in version control that
+describes the complete dependency graph for your project, as well as
+metadata such as the license, version, and description:])
+
+ (source-code
+ (scheme-source
+ "(use-modules (guix packages)
+ (guix licenses)
+ (guix build-system ruby)
+ (gnu packages)
+ (gnu packages version-control)
+ (gnu packages ssh)
+ (gnu packages ruby))
+
+(package
+ (name \"cool-ruby-project\")
+ (version \"1.0\")
+ (source #f) ; not needed just to create dev environment
+ (build-system ruby-build-system)
+ ;; These correspond roughly to \"development\" dependencies.
+ (native-inputs
+ `((\"git\" ,git)
+ (\"openssh\" ,openssh)
+ (\"ruby-rspec\" ,ruby-rspec)))
+ (propagated-inputs
+ `((\"ruby-pg\" ,ruby-pg)
+ (\"ruby-nokogiri\" ,ruby-nokogiri)
+ (\"ruby-i18n\" ,ruby-i18n)
+ (\"ruby-rails\" ,ruby-rails)))
+ (synopsis \"A cool Ruby project\")
+ (description \"This software does some cool stuff, trust me.\")
+ (home-page \"https://example.com\")
+ (license expat))"))
+
+ (p [With this package file, it is simple to an instantiate a
+development environment:])
+
+ (pre (code [$ guix environment -l package.scm]))
+
+ (p [I’m not covering it in this post, but properly filling out the
+blank ,(code [source]) field above would allow for building
+development snapshots, including running the test suite, in an
+isolated build container using the ,(code [guix build]) utility. This
+is very useful when composed with a continuous integration system.
+Guix itself uses ,(anchor "Hydra" "https://nixos.org/hydra/") as its
+CI system to perform all package builds.])
+
+ (p [As mentioned earlier, one of the big advantages of writing Guix
+package recipes is that the full dependency graph can be captured,
+including non-Ruby components. The pg gem provides a good example:])
+
+ (source-code
+ (scheme-source
+"(define-public ruby-pg
+ (package
+ (name \"ruby-pg\")
+ (version \"0.18.2\")
+ (source
+ (origin
+ (method url-fetch)
+ (uri (rubygems-uri \"pg\" version))
+ (sha256
+ (base32
+ \"1axxbf6ij1iqi3i1r3asvjc80b0py5bz0m2wy5kdi5xkrpr82kpf\"))))
+ (build-system ruby-build-system)
+ (arguments
+ '(#:test-target \"spec\"))
+ ;; Native inputs are used only at build and test time.
+ (native-inputs
+ `((\"ruby-rake-compiler\" ,ruby-rake-compiler)
+ (\"ruby-hoe\" ,ruby-hoe)
+ (\"ruby-rspec\" ,ruby-rspec)))
+ ;; Native extension links against PostgreSQL shared library.
+ (inputs
+ `((\"postgresql\" ,postgresql)))
+ (synopsis \"Ruby interface to PostgreSQL\")
+ (description \"Pg is the Ruby interface to the PostgreSQL RDBMS. It works
+ with PostgreSQL 8.4 and later.\")
+ (home-page \"https://bitbucket.org/ged/ruby-pg\")
+ (license license:ruby)))"))
+
+ (p [Note how the recipe specifies the PostgreSQL dependency. Below
+is the dependency graph for ruby-pg as produced by ,(code [guix
+graph]), excluding the GCC compiler toolchain and other low-level
+tools for brevity. Pretty neat, eh?])
+
+ (image/caption "/images/ruby-pg-graph.png"
+ "Abbreviated dependency graph for the pg gem")
+
+ (p [Given that Guix doesn’t yet have many gems packaged (help
+wanted), it can still be advantageous to use it for getting more
+up-to-date packages than many distros provide, but in conjuction with
+Bundler for fetching Ruby gems. This gets RVM out of your hair whilst
+creating a migration path away from Bundler at a later time once the
+required gems have been packaged:])
+
+ (source-code
+ "$ cd my-project/
+$ guix environment --ad-hoc ruby bundler libxml2 libxslt # etc.
+# A small bash script can be used to make these gem sets.
+$ mkdir .gems
+$ export GEM_HOME=$PWD/.gems
+$ export GEM_PATH=$GEM_HOME:$GEM_PATH
+$ export PATH=$GEM_HOME/bin:$PATH
+$ bundle install")
+
+ (p [As you’ve seen in the above package snippets, Guix package
+definitions are typically very short and rather easy to write
+yourself. The ,(code [guix import gem]) tool was made to lower ,(code
+ [foo bar]) the barrier even more by generating most of the boilerplate
+code. For example:])
+
+ (source-code "$ guix import gem pry")
+
+ (p [Produces this Scheme code:])
+
+(source-code
+ (scheme-source
+ [;;; hello there
+(+ 1 2 3)]))
+
+ (source-code
+ (scheme-source
+ "(package
+ (name \"ruby-pry\")
+ (version \"0.10.1\")
+ (source
+ (origin
+ (method url-fetch)
+ (uri (rubygems-uri \"pry\" version))
+ (sha256
+ (base32
+ \"1j0r5fm0wvdwzbh6d6apnp7c0n150hpm9zxpm5xvcgfqr36jaj8z\"))))
+ (build-system ruby-build-system)
+ (propagated-inputs
+ `((\"ruby-coderay\" ,ruby-coderay)
+ (\"ruby-method-source\" ,ruby-method-source)
+ (\"ruby-slop\" ,ruby-slop)))
+ (synopsis
+ \"An IRB alternative and runtime developer console\")
+ (description
+ \"An IRB alternative and runtime developer console\")
+ (home-page \"http://pryrepl.org\")
+ (license expat))"))
+
+ (p [One still has to package the propagated inputs if they aren’t
+yet available, add the necessary inputs for building native extensions
+if needed, and fiddle with the native inputs needed to run the test
+suite, but for most pure Ruby gems this gets you close to a working
+package quickly.])
+
+ (p [In conclusion, while support for Ruby in Guix is still in its
+early days, I hope that you have seen the benefits that using a
+general-purpose, functional package manager can bring to your Ruby
+environments (and all other environments, too.) For more information
+about Guix concepts, installation instructions, programming interface,
+and tools, please refer to the
+,(anchor [official manual] "https://gnu.org/software/guix/manual").
+
+Check out the ,(anchor [help] "https://gnu.org/software/guix/help")
+page for ways to contact the development team for help or to report
+bugs. If you are interested in getting your hands dirty, please
+,(anchor [contribute] "https://gnu.org/software/guix/contribute").
+Besides contributions of code, art, and docs, we also need ,(anchor
+[hardware donations] "https://gnu.org/software/guix/donate") to grow
+our build farm to meet the needs of all our users. Happy hacking!]))
diff --git a/skribe-utils.scm b/skribe-utils.scm
new file mode 100644
index 0000000..8120853
--- /dev/null
+++ b/skribe-utils.scm
@@ -0,0 +1,40 @@
+(define-module (skribe-utils)
+ #:use-module (ice-9 match)
+ #:use-module (syntax-highlight)
+ #:use-module (syntax-highlight scheme)
+ #:use-module (syntax-highlight xml)
+ #:use-module (syntax-highlight c)
+ #:export (image/caption
+ scheme-source
+ xml-source
+ c-source))
+
+(define (image/caption uri caption)
+ `((img (@ (class "centered rounded")
+ (src ,uri)
+ (alt ,caption)))
+ (div (@ (class "caption")) ,caption)))
+
+(define (scheme-source source)
+ (highlights->sxml
+ (highlight lex-scheme
+ (match source
+ ((source ...)
+ (string-concatenate source))
+ (_ source)))))
+
+(define (xml-source source)
+ (highlights->sxml
+ (highlight lex-xml
+ (match source
+ ((source ...)
+ (string-concatenate source))
+ (_ source)))))
+
+(define (c-source source)
+ (highlights->sxml
+ (highlight lex-c
+ (match source
+ ((source ...)
+ (string-concatenate source))
+ (_ source)))))