summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--haunt.scm625
-rw-r--r--highlight.scm31
-rw-r--r--markdown.scm53
-rw-r--r--projects.scm369
-rw-r--r--theme.scm194
-rw-r--r--utils.scm51
6 files changed, 704 insertions, 619 deletions
diff --git a/haunt.scm b/haunt.scm
index 807d652..ed7358f 100644
--- a/haunt.scm
+++ b/haunt.scm
@@ -1,4 +1,4 @@
-;;; Copyright © 2018 David Thompson <davet@gnu.org>
+;;; Copyright © 2018-2021 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
@@ -18,288 +18,16 @@
(haunt builder blog)
(haunt builder atom)
(haunt builder assets)
- (haunt html)
- (haunt page)
(haunt post)
- (haunt reader)
- (haunt reader commonmark)
(haunt site)
- (haunt utils)
- (commonmark)
- (syntax-highlight)
- (syntax-highlight scheme)
- (syntax-highlight xml)
- (syntax-highlight c)
- (sxml match)
- (sxml transform)
- (texinfo)
- (texinfo html)
- (srfi srfi-1)
- (srfi srfi-19)
- (ice-9 rdelim)
- (ice-9 regex)
- (ice-9 match)
- (web uri))
-
-(define (date year month day)
- "Create a SRFI-19 date for the given YEAR, MONTH, DAY"
- (let ((tzoffset (tm:gmtoff (localtime (time-second (current-time))))))
- (make-date 0 0 0 0 day month year tzoffset)))
-
-(define (stylesheet name)
- `(link (@ (rel "stylesheet")
- (href ,(string-append "/css/" name ".css")))))
-
-(define* (anchor content #:optional (uri content))
- `(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=1")
- (style "border:0;")
- (alt "")))))))
-
-(define (link name uri)
- `(a (@ (href ,uri)) ,name))
-
-(define* (centered-image url #:optional alt)
- `(img (@ (class "centered-image")
- (src ,url)
- ,@(if alt
- `((alt ,alt))
- '()))))
-
-(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"))
- "© 2020 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 "https://dthompson.us/projects/haunt.html"))
- "Haunt")
- ", a static site generator written in "
- (a (@ (href "https://gnu.org/software/guile"))
- "Guile Scheme")
- ".")))
- ,%piwik-code)))
- #: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
- (a (@ (href "/feed.xml"))
- (img (@ (src "images/feed.png")))))
- ,(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)))))
+ (markdown)
+ (projects)
+ (theme)
+ (utils))
(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 lang source)
- (let ((lexer (match lang
- ('scheme lex-scheme)
- ('xml lex-xml)
- ('c lex-c)
- (_ #f))))
- (if lexer
- (highlights->sxml (highlight lexer source))
- source)))
-
-(define (sxml-identity . args) args)
-
-(define (highlight-code . tree)
- (sxml-match tree
- ((code (@ (class ,class) . ,attrs) ,source)
- (let ((lang (string->symbol
- (string-drop class (string-length "language-")))))
- `(code (@ ,@attrs)
- ,(maybe-highlight-code lang source))))
- (,other other)))
-
-(define (highlight-scheme code)
- `(pre (code ,(highlights->sxml (highlight lex-scheme code)))))
-
-(define (raw-snippet code)
- `(pre (code ,(if (string? code) code (read-string code)))))
-
-;; Markdown doesn't support video, so let's hack around that! Find
-;; <img> tags with a ".webm" source and substitute a <video> tag.
-(define (media-hackery . tree)
- (sxml-match tree
- ((img (@ (src ,src) . ,attrs) . ,body)
- (if (string-suffix? ".webm" src)
- `(video (@ (src ,src) (controls "true"),@attrs) ,@body)
- tree))))
-
-(define %commonmark-rules
- `((code . ,highlight-code)
- (img . ,media-hackery)
- (*text* . ,(lambda (tag str) str))
- (*default* . ,sxml-identity)))
-
-(define (post-process-commonmark sxml)
- (pre-post-order sxml %commonmark-rules))
-
-(define commonmark-reader*
- (make-reader (make-file-extension-matcher "md")
- (lambda (file)
- (call-with-input-file file
- (lambda (port)
- (values (read-metadata-headers port)
- (post-process-commonmark
- (commonmark->sxml port))))))))
-
-(define (static-page title file-name body)
- (lambda (site posts)
- (make-page file-name
- (with-layout dthompson-theme site title body)
- sxml->html)))
-
-(define* (project-page #:key name file-name description usage requirements
- installation manual? license repo releases guix-package)
- (define (tarball-url version)
- (string-append "https://files.dthompson.us/"
- repo "/" repo "-" version
- ".tar.gz"))
- (define body
- `((h1 ,name)
- ,description
- ,@(if usage
- `((h2 "Usage")
- ,usage)
- '())
- ,@(if manual?
- `((h2 "Documentation")
- (p ,(anchor "View the reference manual"
- (string-append "/manuals/" repo "/index.html"))))
- '())
- (h2 "Releases")
- (ul ,(map (match-lambda
- ((version date)
- (let ((url (tarball-url version)))
- `(li ,(date->string date "~Y-~m-~d")
- " — " ,version " — "
- ,(anchor (string-append repo "-" version ".tar.gz")
- url)
- " — "
- ,(anchor "GPG signature"
- (string-append url ".asc"))))))
- releases))
- (h2 "Requirements")
- (ul ,(map (lambda (requirement)
- `(li ,requirement))
- requirements))
- (h2 "Installation")
- ,@(if installation
- (list installation)
- (match (car releases)
- ((version _)
- `(,@(if guix-package
- `((p "To install " ,name " with the GNU Guix package manager, run:")
- (pre "guix install " ,guix-package))
- '())
- (p "To build and install " ,name " from source, run:")
- (pre "wget "
- ,(tarball-url version)
- "
-tar xf "
- ,repo "-" ,version ".tar.gz"
- "
-cd "
- ,repo "-" ,version
- "
-./configure
-make
-make install")))))
- (h2 "License")
- (p ,license)
- (h2 "Git Repository")
- ,@(let ((url (string-append "https://git.dthompson.us/" repo ".git")))
- `((p ,name " is developed using the Git version control
-system. The official repository is hosted at "
- ,(anchor url url) ".")
- (p "To clone the repository, run:")
- (pre "git clone " ,url)))
- (h2 "Community")
- (p "Real-time discussion for " ,name " can be found on the "
- (code "#guile")
- " channel on the Freenode IRC network.")
- (h2 "Contributing")
- (p "Send patches and bug reports to "
- ,(anchor "davet@gnu.org" "mailto:davet@gnu.org")
- ".")))
-
- (static-page name (string-append "projects/" file-name) body))
-
(define about-page
(static-page
"About Me"
@@ -307,7 +35,7 @@ system. The official repository is hosted at "
`((h2 "Hi.")
(p "I am a professional software developer, hobbyist gardener,
woodworker, and drummer from Worcester, Massachusetts.")
- (p "I am currently a DevOps engineer at " ,(anchor "Vista Higher
+ (p "I am currently a Senior DevOps engineer at " ,(anchor "Vista Higher
Learning" "http://vistahigherlearning.com")
", and formerly a web developer at the "
,(anchor "Free Software Foundation." "https://fsf.org"))
@@ -336,347 +64,6 @@ Learning" "http://vistahigherlearning.com")
(p ,(anchor "srt2vtt" "projects/srt2vtt.html")
" — SRT to WebVTT subtitle converter"))))
-(define sly-page
- (project-page
- #:name "Sly"
- #:file-name "sly.html"
- #:description
- `((p (strong "note: this project is no longer being developed!"))
- (p "Sly is a fun, free software 2D/3D game engine written in "
- ,(anchor "Guile Scheme" "https://gnu.org/s/guile") ".")
- ,(centered-image "/images/sly/logo.png" "Sly fox mascot")
- (p "Sly differentiates itself from most other game engines by encouraging
-and enabling the use of "
- ,(anchor "live coding"
- "http://toplap.org/about/")
- " and "
- ,(anchor "functional reactive programming"
- "https://en.wikipedia.org/wiki/Functional_reactive_programming")
- " techniques. Sly provides a dynamic live coding environment
-that allows games to be built interactively and iteratively without
-ever stopping the running program. A data structure called a “signal”
-provides a method of modeling time-varying state that is declarative,
-functional, and reactive.")
- (p ,(centered-image "/images/sly/2048.png"))
- (p ,(centered-image "/images/sly/mines.png")))
- #:requirements '("GNU Guile >= 2.0.11"
- "guile-opengl_ >= 0.1.0"
- "guile-sdl_ >= 0.5.0"
- "SDL 1.2.x"
- "FreeImage >= 3.0"
- "GNU Scientific Library (GSL)")
- #:license "GNU GPLv3+"
- #:repo "sly"
- #:manual? #t
- #:releases
- `(("0.1" ,(date 2015 11 12)))))
-
-(define guile-sdl2-page
- (project-page
- #:name "Guile-SDL2"
- #:file-name "guile-sdl2.html"
- #:repo "guile-sdl2"
- #:guix-package "guile-sdl2"
- #:description
- `((p "Guile-SDL2 provides "
- ,(anchor "Guile Scheme" "https://gnu.org/s/guile")
- " bindings for the "
- ,(anchor "SDL2" "http://libsdl.org")
- " C shared library. The bindings are written in pure Scheme
-using Guile's foreign function interface."))
- #:usage
- `((p "Guile-SDL2 provides modules in the "
- (code "(sdl2 ...)")
- " namespace, roughly organized how the SDL2 C header files are
-organized. Low-level bindings are available in the"
- (code "(sdl2 bindings ...)")
- " namespace, but these are not recommended for normal usage.")
- (p "Additionally, SDL2 extension library bindings are available in the
-following modules:")
- (ul (li "SDL2_image: " (code "(sdl2 image)"))
- (li "SDL2_mixer: " (code "(sdl2 mixer)"))
- (li "SDL2_ttf: " (code "(sdl2 ttf)")))
- (p "Here is a short “hello, world” example program:")
- ,(highlight-scheme
- "(use-modules (sdl2)
- (sdl2 render)
- (sdl2 surface)
- (sdl2 video))
-
-(define (draw ren)
- (let* ((surface (load-bmp \"hello.bmp\"))
- (texture (surface->texture ren surface)))
- (clear-renderer ren)
- (render-copy ren texture)
- (present-renderer ren)
- (sleep 2)))
-
-(sdl-init)
-
-(call-with-window (make-window)
- (lambda (window)
- (call-with-renderer (make-renderer window) draw)))
-
-(sdl-quit)"))
- #:requirements '("GNU Guile >= 2.0.9"
- "SDL2 >= 2.0.0"
- "SDL2_image >= 2.0.0"
- "SDL2_mixer >= 2.0.0"
- "SDL2_ttf >= 2.0.0"
- "GNU Make"
- "GNU pkg-config")
- #:license "GNU LGPLv3+"
- #:manual? #t
- #:releases
- `(("0.5.0" ,(date 2020 04 07))
- ("0.4.0" ,(date 2019 06 02))
- ("0.3.1" ,(date 2018 10 16))
- ("0.3.0" ,(date 2018 07 10))
- ("0.2.0" ,(date 2017 01 20))
- ("0.1.2" ,(date 2016 08 10))
- ("0.1.1" ,(date 2016 01 01))
- ("0.1.0" ,(date 2015 12 22)))))
-
-(define guile-syntax-highlight-page
- (project-page
- #:name "guile-syntax-highlight"
- #:file-name "guile-syntax-highlight.html"
- #:repo "guile-syntax-highlight"
- #:description
- `((p "guile-syntax-highlight is a general-purpose syntax
-highlighting library for GNU Guile. It can parse code written in
-various programming languages into a simple s-expression that can be
-easily converted to HTML (via SXML) or any other format for
-rendering."))
- #:usage
- (highlight-scheme
- "(use-modules (syntax-highlight)
- (syntax-highlight scheme)
- (sxml simple))
-
-(define code
- \"(define (square x) \\\"Return the square of X.\\\" (* x x))\")
-
-;; Get raw highlights list.
-(define highlighted-code
- (highlight scheme-highlighter code))
-
-;; Convert to SXML.
-(define highlighted-sxml
- (highlights->sxml highlighted-code))
-
-;; Write HTML to stdout.
-(display (sxml->xml highlighted-sxml))
-(newline)
-")
- #:requirements '("GNU Guile >= 2.0"
- "GNU Make"
- "GNU pkg-config")
- #:license "GNU LGPLv3+"
- #:releases
- `(("0.1" ,(date 2018 03 10)))))
-
-(define haunt-page
- (project-page
- #:name "Haunt"
- #:file-name "haunt.html"
- #:repo "haunt"
- #:guix-package "haunt"
- #:manual? #t
- #:description
- `((p "Haunt is a simple, functional, hackable static site generator
-that gives authors the ability to treat websites as Scheme programs.")
- ,(centered-image "/images/haunt/logo.png" "crudely drawn ghost")
- (p "By giving authors the full expressive power of Scheme, they
-are able to control every aspect of the site generation process.
-Haunt provides a simple, functional build system that can be easily
-extended for this purpose.")
- (p "Haunt has no opinion about what markup language authors
-should use to write posts, though it comes with support for the
-popular Markdown format. Likewise, Haunt has no opinion about how
-authors structure their sites. Though it comes with support for
-building simple blogs or Atom feeds, authors should feel empowered to
-tweak, replace, or create builders to do things that aren't provided
-out-of-the-box."))
- #:usage
- `((p "Here's what a simple Haunt configuration looks like:")
- ,(call-with-input-file "snippets/haunt.scm" highlight-scheme)
- (p "In a new directory, save the above to a file named "
- (code "haunt.scm") ".")
- (p "Create a subdirectory named " (code "posts") ".")
- (p "Add the following to a new file named " (code "posts/hello.md") ":")
- ,(call-with-input-file "snippets/haunt-hello.md" raw-snippet)
- (p "Run " (code "haunt build") " to build the site.")
- (p "Run " (code "haunt serve") " to a launch a web server to
-preview your work.")
- (p "Open " ,(anchor "http://localhost:8080") " in your web browser
-and smile, because you've just generated your first Haunt site!"))
- #:requirements
- '("GNU Guile >= 2.0"
- "guile-commonmark (for Markdown support, optional)"
- "guile-reader (for Skribe support, optional)")
- #:license "GNU GPLv3+"
- #:releases
- `(("0.2.4" ,(date 2018 11 29))
- ("0.2.3" ,(date 2018 11 25))
- ("0.2.2" ,(date 2018 03 10))
- ("0.2.1" ,(date 2017 01 23))
- ("0.2" ,(date 2016 04 24))
- ("0.1" ,(date 2015 08 08)))))
-
-(define chickadee-page
- (project-page
- #:name "Chickadee"
- #:file-name "chickadee.html"
- #:repo "chickadee"
- #:guix-package "guile-chickadee"
- #:manual? #t
- #:description
- `((p "Chickadee is a game development toolkit for "
- ,(anchor "Guile Scheme" "https://gnu.org/s/guile") ".")
- ,(centered-image "/images/chickadee/logo.png" "Chickadee logo")
- (p "Chickadee provides all the essential tools that
-parenthetically inclined game developers need to make games in
-Scheme.")
- (p "Here is the obligatory “Hello, world” program:")
- ,(highlight-scheme
- "(use-modules (chickadee)
- (chickadee math vector)
- (chickadee graphics font))
-
-(define (draw alpha)
- (draw-text \"Hello, world!\" (vec2 260.0 240.0)))
-
-(run-game #:draw draw)
-")
- (p "And here's how to draw a sprite:")
- ,(highlight-scheme
- "(use-modules (chickadee)
- (chickadee math vector)
- (chickadee graphics sprite)
- (chickadee graphics texture))
-
-(define sprite #f)
-
-(define (load)
- (set! sprite (load-image \"images/chickadee.png\")))
-
-(define (draw alpha)
- (draw-sprite sprite (vec2 256.0 176.0)))
-
-(run-game #:load load #:draw draw)
-")
- (p "Features include:")
- (ul
- (li "extensible, fixed-timestep game loop")
- (li "OpenGL-based rendering engine")
- (li "keyboard, mouse, controller input events")
- (li "vectors, matrices, bounding boxes, easing functions, etc.")
- (li "asynchronous scripting"))
- (p (small ,(anchor "chickadee sprite by Refuzzle, CC0"
- "http://opengameart.org/content/winter-birds"))))
- #:requirements '("GNU Guile >= 2.1.4"
- "Guile-SDL2 >= 0.2.0"
- "Guile-OpenGL >= 0.1.0")
- #:license "GNU GPLv3+"
- #:releases
- `(("0.6.0" ,(date 2020 11 19))
- ("0.5.0" ,(date 2020 04 08))
- ("0.4.0" ,(date 2019 06 04))
- ("0.3.0" ,(date 2018 10 03))
- ("0.2.0" ,(date 2017 01 26))
- ("0.1.0" ,(date 2017 01 23)))))
-
-(define shroud-page
- (project-page
- #:name "Shroud"
- #:file-name "shroud.html"
- #:repo "shroud"
- #:description
- `((p "Shroud is a simple secret manager with a command line interface.")
- (p "The password database is stored as a Scheme s-expression
-and encrypted with a "
- ,(anchor "GnuPG" "https://gnupg.org")
- " key. Secrets consist of an arbitrary number of key/value
-pairs, making Shroud suitable for more than just password storage.
-For copying and pasting secrets into web browsers and other graphical
-applications, there is xclip integration."))
- #:requirements '("GNU Guile >= 2.0.9"
- "GnuPG >= 1.4"
- "GNU Make"
- "GNU pkg-config"
- ("optional: xclip is needed for the "
- (code "-c")
- " flag of "
- (code "shroud show")
- " to work"))
- #:usage
- `((p "First, create a " (code ".shroud")
- " file in your home directory to hold your
-configuration settings. All you really need to set here is your GPG
-user ID i.e. your email address:")
- ,(highlight-scheme
- "'((user-id . \"foo@example.com\"))")
- (p "The "
- (code ".shroud")
- " file is Scheme source code, so any expression that evaluates
-to an alist of valid configuration settings is usable by Shroud.")
- (p "Once Shroud is configured, try out the following commands to
-get a feel for how things work:")
- (pre
- "# Add a new secret:
-shroud hide bank-account username=foobar password=hackme
-
-# Edit an existing secret:
-shroud hide --edit bank-account password=hackmepls
-
-# List all secrets:
-shroud list
-
-# Show all key/value pairs for a saved secret:
-shroud show bank-account
-
-# Show a single value in a secret:
-shroud show bank-account password
-
-# Copy a password directly to X clipboard:
-shroud show -c bank-account password
-
-# Delete a secret:
-shroud remove bank-account")
- (p "Happy shrouding!"))
- #:license "GNU GPLv3+"
- #:releases
- `(("0.1.1" ,(date 2015 10 01))
- ("0.1.0" ,(date 2015 09 29)))))
-
-(define srt2vtt-page
- (project-page
- #:name "srt2vtt"
- #:file-name "srt2vtt.html"
- #:repo "srt2vtt"
- #:description
- `((p "Convert SRT formatted subtitles to WebVTT format for use with
-the HTML5 "
- (code "<track>") " tag."))
- #:requirements '("GNU Guile >= 2.0.5")
- #:usage
- `((pre
- "$ srt2vtt --help
-Usage: srt2vtt [OPTIONS]
-Convert SubRip formatted subtitles to WebVTT format.
-
- -h, --help display this help and exit
- -v, --version display version and exit
- -i, --input=FILE-NAME read input from FILE-NAME
- -o, --output=FILE-NAME write output to FILE-NAME")
- (p "If " (code "--input")
- " or " (code "--output")
- " is ommitted, read from stdin or stdout, respectively."))
- #:license "GNU GPLv3+"
- #:releases
- `(("0.1" ,(date 2015 02 7)))))
-
(site #:title "dthompson"
#:domain "dthompson.us"
#:default-metadata
diff --git a/highlight.scm b/highlight.scm
new file mode 100644
index 0000000..51f2b02
--- /dev/null
+++ b/highlight.scm
@@ -0,0 +1,31 @@
+(define-module (highlight)
+ #:use-module (ice-9 match)
+ #:use-module (sxml match)
+ #:use-module (syntax-highlight)
+ #:use-module (syntax-highlight scheme)
+ #:use-module (syntax-highlight xml)
+ #:use-module (syntax-highlight c)
+ #:export (highlight-code
+ highlight-scheme))
+
+(define (maybe-highlight-code lang source)
+ (let ((lexer (match lang
+ ('scheme lex-scheme)
+ ('xml lex-xml)
+ ('c lex-c)
+ (_ #f))))
+ (if lexer
+ (highlights->sxml (highlight lexer source))
+ source)))
+
+(define (highlight-code . tree)
+ (sxml-match tree
+ ((code (@ (class ,class) . ,attrs) ,source)
+ (let ((lang (string->symbol
+ (string-drop class (string-length "language-")))))
+ `(code (@ ,@attrs)
+ ,(maybe-highlight-code lang source))))
+ (,other other)))
+
+(define (highlight-scheme code)
+ `(pre (code ,(highlights->sxml (highlight lex-scheme code)))))
diff --git a/markdown.scm b/markdown.scm
new file mode 100644
index 0000000..d1a70b4
--- /dev/null
+++ b/markdown.scm
@@ -0,0 +1,53 @@
+;;; Copyright © 2018-2021 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-module (markdown)
+ #:use-module (commonmark)
+ #:use-module (haunt post)
+ #:use-module (haunt reader)
+ #:use-module (highlight)
+ #:use-module (sxml match)
+ #:use-module (sxml transform)
+ #:export (commonmark-reader*))
+
+(define (sxml-identity . args) args)
+
+;; Markdown doesn't support video, so let's hack around that! Find
+;; <img> tags with a ".webm" source and substitute a <video> tag.
+(define (media-hackery . tree)
+ (sxml-match tree
+ ((img (@ (src ,src) . ,attrs) . ,body)
+ (if (string-suffix? ".webm" src)
+ `(video (@ (src ,src) (controls "true"),@attrs) ,@body)
+ tree))))
+
+(define %commonmark-rules
+ `((code . ,highlight-code)
+ (img . ,media-hackery)
+ (*text* . ,(lambda (tag str) str))
+ (*default* . ,sxml-identity)))
+
+(define (post-process-commonmark sxml)
+ (pre-post-order sxml %commonmark-rules))
+
+(define commonmark-reader*
+ (make-reader (make-file-extension-matcher "md")
+ (lambda (file)
+ (call-with-input-file file
+ (lambda (port)
+ (values (read-metadata-headers port)
+ (post-process-commonmark
+ (commonmark->sxml port))))))))
diff --git a/projects.scm b/projects.scm
new file mode 100644
index 0000000..e2b672e
--- /dev/null
+++ b/projects.scm
@@ -0,0 +1,369 @@
+;;; Copyright © 2018-2021 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-module (projects)
+ #:use-module (highlight)
+ #:use-module (theme)
+ #:use-module (utils)
+ #:export (chickadee-page
+ sly-page
+ guile-sdl2-page
+ guile-syntax-highlight-page
+ haunt-page
+ shroud-page
+ srt2vtt-page))
+
+(define sly-page
+ (project-page
+ #:name "Sly"
+ #:file-name "sly.html"
+ #:description
+ `((p (strong "note: this project is no longer being developed!"))
+ (p "Sly is a fun, free software 2D/3D game engine written in "
+ ,(anchor "Guile Scheme" "https://gnu.org/s/guile") ".")
+ ,(centered-image "/images/sly/logo.png" "Sly fox mascot")
+ (p "Sly differentiates itself from most other game engines by encouraging
+and enabling the use of "
+ ,(anchor "live coding"
+ "http://toplap.org/about/")
+ " and "
+ ,(anchor "functional reactive programming"
+ "https://en.wikipedia.org/wiki/Functional_reactive_programming")
+ " techniques. Sly provides a dynamic live coding environment
+that allows games to be built interactively and iteratively without
+ever stopping the running program. A data structure called a “signal”
+provides a method of modeling time-varying state that is declarative,
+functional, and reactive.")
+ (p ,(centered-image "/images/sly/2048.png"))
+ (p ,(centered-image "/images/sly/mines.png")))
+ #:requirements '("GNU Guile >= 2.0.11"
+ "guile-opengl_ >= 0.1.0"
+ "guile-sdl_ >= 0.5.0"
+ "SDL 1.2.x"
+ "FreeImage >= 3.0"
+ "GNU Scientific Library (GSL)")
+ #:license "GNU GPLv3+"
+ #:repo "sly"
+ #:manual? #t
+ #:releases
+ `(("0.1" ,(date 2015 11 12)))))
+
+(define guile-sdl2-page
+ (project-page
+ #:name "Guile-SDL2"
+ #:file-name "guile-sdl2.html"
+ #:repo "guile-sdl2"
+ #:guix-package "guile-sdl2"
+ #:description
+ `((p "Guile-SDL2 provides "
+ ,(anchor "Guile Scheme" "https://gnu.org/s/guile")
+ " bindings for the "
+ ,(anchor "SDL2" "http://libsdl.org")
+ " C shared library. The bindings are written in pure Scheme
+using Guile's foreign function interface."))
+ #:usage
+ `((p "Guile-SDL2 provides modules in the "
+ (code "(sdl2 ...)")
+ " namespace, roughly organized how the SDL2 C header files are
+organized. Low-level bindings are available in the"
+ (code "(sdl2 bindings ...)")
+ " namespace, but these are not recommended for normal usage.")
+ (p "Additionally, SDL2 extension library bindings are available in the
+following modules:")
+ (ul (li "SDL2_image: " (code "(sdl2 image)"))
+ (li "SDL2_mixer: " (code "(sdl2 mixer)"))
+ (li "SDL2_ttf: " (code "(sdl2 ttf)")))
+ (p "Here is a short “hello, world” example program:")
+ ,(highlight-scheme
+ "(use-modules (sdl2)
+ (sdl2 render)
+ (sdl2 surface)
+ (sdl2 video))
+
+(define (draw ren)
+ (let* ((surface (load-bmp \"hello.bmp\"))
+ (texture (surface->texture ren surface)))
+ (clear-renderer ren)
+ (render-copy ren texture)
+ (present-renderer ren)
+ (sleep 2)))
+
+(sdl-init)
+
+(call-with-window (make-window)
+ (lambda (window)
+ (call-with-renderer (make-renderer window) draw)))
+
+(sdl-quit)"))
+ #:requirements '("GNU Guile >= 2.0.9"
+ "SDL2 >= 2.0.0"
+ "SDL2_image >= 2.0.0"
+ "SDL2_mixer >= 2.0.0"
+ "SDL2_ttf >= 2.0.0"
+ "GNU Make"
+ "GNU pkg-config")
+ #:license "GNU LGPLv3+"
+ #:manual? #t
+ #:releases
+ `(("0.5.0" ,(date 2020 04 07))
+ ("0.4.0" ,(date 2019 06 02))
+ ("0.3.1" ,(date 2018 10 16))
+ ("0.3.0" ,(date 2018 07 10))
+ ("0.2.0" ,(date 2017 01 20))
+ ("0.1.2" ,(date 2016 08 10))
+ ("0.1.1" ,(date 2016 01 01))
+ ("0.1.0" ,(date 2015 12 22)))))
+
+(define guile-syntax-highlight-page
+ (project-page
+ #:name "guile-syntax-highlight"
+ #:file-name "guile-syntax-highlight.html"
+ #:repo "guile-syntax-highlight"
+ #:description
+ `((p "guile-syntax-highlight is a general-purpose syntax
+highlighting library for GNU Guile. It can parse code written in
+various programming languages into a simple s-expression that can be
+easily converted to HTML (via SXML) or any other format for
+rendering."))
+ #:usage
+ (highlight-scheme
+ "(use-modules (syntax-highlight)
+ (syntax-highlight scheme)
+ (sxml simple))
+
+(define code
+ \"(define (square x) \\\"Return the square of X.\\\" (* x x))\")
+
+;; Get raw highlights list.
+(define highlighted-code
+ (highlight scheme-highlighter code))
+
+;; Convert to SXML.
+(define highlighted-sxml
+ (highlights->sxml highlighted-code))
+
+;; Write HTML to stdout.
+(display (sxml->xml highlighted-sxml))
+(newline)
+")
+ #:requirements '("GNU Guile >= 2.0"
+ "GNU Make"
+ "GNU pkg-config")
+ #:license "GNU LGPLv3+"
+ #:releases
+ `(("0.1" ,(date 2018 03 10)))))
+
+(define haunt-page
+ (project-page
+ #:name "Haunt"
+ #:file-name "haunt.html"
+ #:repo "haunt"
+ #:guix-package "haunt"
+ #:manual? #t
+ #:description
+ `((p "Haunt is a simple, functional, hackable static site generator
+that gives authors the ability to treat websites as Scheme programs.")
+ ,(centered-image "/images/haunt/logo.png" "crudely drawn ghost")
+ (p "By giving authors the full expressive power of Scheme, they
+are able to control every aspect of the site generation process.
+Haunt provides a simple, functional build system that can be easily
+extended for this purpose.")
+ (p "Haunt has no opinion about what markup language authors
+should use to write posts, though it comes with support for the
+popular Markdown format. Likewise, Haunt has no opinion about how
+authors structure their sites. Though it comes with support for
+building simple blogs or Atom feeds, authors should feel empowered to
+tweak, replace, or create builders to do things that aren't provided
+out-of-the-box."))
+ #:usage
+ `((p "Here's what a simple Haunt configuration looks like:")
+ ,(call-with-input-file "snippets/haunt.scm" highlight-scheme)
+ (p "In a new directory, save the above to a file named "
+ (code "haunt.scm") ".")
+ (p "Create a subdirectory named " (code "posts") ".")
+ (p "Add the following to a new file named " (code "posts/hello.md") ":")
+ ,(call-with-input-file "snippets/haunt-hello.md" raw-snippet)
+ (p "Run " (code "haunt build") " to build the site.")
+ (p "Run " (code "haunt serve") " to a launch a web server to
+preview your work.")
+ (p "Open " ,(anchor "http://localhost:8080") " in your web browser
+and smile, because you've just generated your first Haunt site!"))
+ #:requirements
+ '("GNU Guile >= 2.0"
+ "guile-commonmark (for Markdown support, optional)"
+ "guile-reader (for Skribe support, optional)")
+ #:license "GNU GPLv3+"
+ #:releases
+ `(("0.2.4" ,(date 2018 11 29))
+ ("0.2.3" ,(date 2018 11 25))
+ ("0.2.2" ,(date 2018 03 10))
+ ("0.2.1" ,(date 2017 01 23))
+ ("0.2" ,(date 2016 04 24))
+ ("0.1" ,(date 2015 08 08)))))
+
+(define chickadee-page
+ (project-page
+ #:name "Chickadee"
+ #:file-name "chickadee.html"
+ #:repo "chickadee"
+ #:guix-package "guile-chickadee"
+ #:manual? #t
+ #:description
+ `((p "Chickadee is a game development toolkit for "
+ ,(anchor "Guile Scheme" "https://gnu.org/s/guile") ".")
+ ,(centered-image "/images/chickadee/logo.png" "Chickadee logo")
+ (p "Chickadee provides all the essential tools that
+parenthetically inclined game developers need to make games in
+Scheme.")
+ (p "Here is the obligatory “Hello, world” program:")
+ ,(highlight-scheme
+ "(use-modules (chickadee)
+ (chickadee math vector)
+ (chickadee graphics font))
+
+(define (draw alpha)
+ (draw-text \"Hello, world!\" (vec2 260.0 240.0)))
+
+(run-game #:draw draw)
+")
+ (p "And here's how to draw a sprite:")
+ ,(highlight-scheme
+ "(use-modules (chickadee)
+ (chickadee math vector)
+ (chickadee graphics sprite)
+ (chickadee graphics texture))
+
+(define sprite #f)
+
+(define (load)
+ (set! sprite (load-image \"images/chickadee.png\")))
+
+(define (draw alpha)
+ (draw-sprite sprite (vec2 256.0 176.0)))
+
+(run-game #:load load #:draw draw)
+")
+ (p "Features include:")
+ (ul
+ (li "extensible, fixed-timestep game loop")
+ (li "OpenGL-based rendering engine")
+ (li "keyboard, mouse, controller input events")
+ (li "vectors, matrices, bounding boxes, easing functions, etc.")
+ (li "asynchronous scripting"))
+ (p (small ,(anchor "chickadee sprite by Refuzzle, CC0"
+ "http://opengameart.org/content/winter-birds"))))
+ #:requirements '("GNU Guile >= 2.1.4"
+ "Guile-SDL2 >= 0.2.0"
+ "Guile-OpenGL >= 0.1.0")
+ #:license "GNU GPLv3+"
+ #:releases
+ `(("0.6.0" ,(date 2020 11 19))
+ ("0.5.0" ,(date 2020 04 08))
+ ("0.4.0" ,(date 2019 06 04))
+ ("0.3.0" ,(date 2018 10 03))
+ ("0.2.0" ,(date 2017 01 26))
+ ("0.1.0" ,(date 2017 01 23)))))
+
+(define shroud-page
+ (project-page
+ #:name "Shroud"
+ #:file-name "shroud.html"
+ #:repo "shroud"
+ #:description
+ `((p "Shroud is a simple secret manager with a command line interface.")
+ (p "The password database is stored as a Scheme s-expression
+and encrypted with a "
+ ,(anchor "GnuPG" "https://gnupg.org")
+ " key. Secrets consist of an arbitrary number of key/value
+pairs, making Shroud suitable for more than just password storage.
+For copying and pasting secrets into web browsers and other graphical
+applications, there is xclip integration."))
+ #:requirements '("GNU Guile >= 2.0.9"
+ "GnuPG >= 1.4"
+ "GNU Make"
+ "GNU pkg-config"
+ ("optional: xclip is needed for the "
+ (code "-c")
+ " flag of "
+ (code "shroud show")
+ " to work"))
+ #:usage
+ `((p "First, create a " (code ".shroud")
+ " file in your home directory to hold your
+configuration settings. All you really need to set here is your GPG
+user ID i.e. your email address:")
+ ,(highlight-scheme
+ "'((user-id . \"foo@example.com\"))")
+ (p "The "
+ (code ".shroud")
+ " file is Scheme source code, so any expression that evaluates
+to an alist of valid configuration settings is usable by Shroud.")
+ (p "Once Shroud is configured, try out the following commands to
+get a feel for how things work:")
+ (pre
+ "# Add a new secret:
+shroud hide bank-account username=foobar password=hackme
+
+# Edit an existing secret:
+shroud hide --edit bank-account password=hackmepls
+
+# List all secrets:
+shroud list
+
+# Show all key/value pairs for a saved secret:
+shroud show bank-account
+
+# Show a single value in a secret:
+shroud show bank-account password
+
+# Copy a password directly to X clipboard:
+shroud show -c bank-account password
+
+# Delete a secret:
+shroud remove bank-account")
+ (p "Happy shrouding!"))
+ #:license "GNU GPLv3+"
+ #:releases
+ `(("0.1.1" ,(date 2015 10 01))
+ ("0.1.0" ,(date 2015 09 29)))))
+
+(define srt2vtt-page
+ (project-page
+ #:name "srt2vtt"
+ #:file-name "srt2vtt.html"
+ #:repo "srt2vtt"
+ #:description
+ `((p "Convert SRT formatted subtitles to WebVTT format for use with
+the HTML5 "
+ (code "<track>") " tag."))
+ #:requirements '("GNU Guile >= 2.0.5")
+ #:usage
+ `((pre
+ "$ srt2vtt --help
+Usage: srt2vtt [OPTIONS]
+Convert SubRip formatted subtitles to WebVTT format.
+
+ -h, --help display this help and exit
+ -v, --version display version and exit
+ -i, --input=FILE-NAME read input from FILE-NAME
+ -o, --output=FILE-NAME write output to FILE-NAME")
+ (p "If " (code "--input")
+ " or " (code "--output")
+ " is ommitted, read from stdin or stdout, respectively."))
+ #:license "GNU GPLv3+"
+ #:releases
+ `(("0.2" ,(date 2021 03 19))
+ ("0.1" ,(date 2015 02 7)))))
diff --git a/theme.scm b/theme.scm
new file mode 100644
index 0000000..16f0c41
--- /dev/null
+++ b/theme.scm
@@ -0,0 +1,194 @@
+;;; Copyright © 2018-2021 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-module (theme)
+ #:use-module (haunt builder blog)
+ #:use-module (haunt html)
+ #:use-module (haunt page)
+ #:use-module (haunt post)
+ #:use-module (haunt site)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-19)
+ #:use-module (utils)
+ #:export (dthompson-theme
+ static-page
+ project-page))
+
+(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 (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"))
+ "© 2020 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 "https://dthompson.us/projects/haunt.html"))
+ "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
+ (a (@ (href "/feed.xml"))
+ (img (@ (src "images/feed.png")))))
+ ,(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 title file-name body)
+ (lambda (site posts)
+ (make-page file-name
+ (with-layout dthompson-theme site title body)
+ sxml->html)))
+
+(define* (project-page #:key name file-name description usage requirements
+ installation manual? license repo releases guix-package)
+ (define (tarball-url version)
+ (string-append "https://files.dthompson.us/"
+ repo "/" repo "-" version
+ ".tar.gz"))
+ (define body
+ `((h1 ,name)
+ ,description
+ ,@(if usage
+ `((h2 "Usage")
+ ,usage)
+ '())
+ ,@(if manual?
+ `((h2 "Documentation")
+ (p ,(anchor "View the reference manual"
+ (string-append "/manuals/" repo "/index.html"))))
+ '())
+ (h2 "Releases")
+ (ul ,(map (match-lambda
+ ((version date)
+ (let ((url (tarball-url version)))
+ `(li ,(date->string date "~Y-~m-~d")
+ " — " ,version " — "
+ ,(anchor (string-append repo "-" version ".tar.gz")
+ url)
+ " — "
+ ,(anchor "GPG signature"
+ (string-append url ".asc"))))))
+ releases))
+ (h2 "Requirements")
+ (ul ,(map (lambda (requirement)
+ `(li ,requirement))
+ requirements))
+ (h2 "Installation")
+ ,@(if installation
+ (list installation)
+ (match (car releases)
+ ((version _)
+ `(,@(if guix-package
+ `((p "To install " ,name " with the GNU Guix package manager, run:")
+ (pre "guix install " ,guix-package))
+ '())
+ (p "To build and install " ,name " from source, run:")
+ (pre "wget "
+ ,(tarball-url version)
+ "
+tar xf "
+ ,repo "-" ,version ".tar.gz"
+ "
+cd "
+ ,repo "-" ,version
+ "
+./configure
+make
+make install")))))
+ (h2 "License")
+ (p ,license)
+ (h2 "Git Repository")
+ ,@(let ((url (string-append "https://git.dthompson.us/" repo ".git")))
+ `((p ,name " is developed using the Git version control
+system. The official repository is hosted at "
+ ,(anchor url url) ".")
+ (p "To clone the repository, run:")
+ (pre "git clone " ,url)))
+ (h2 "Community")
+ (p "Real-time discussion for " ,name " can be found on the "
+ (code "#guile")
+ " channel on the Freenode IRC network.")
+ (h2 "Contributing")
+ (p "Send patches and bug reports to "
+ ,(anchor "davet@gnu.org" "mailto:davet@gnu.org")
+ ".")))
+
+ (static-page name (string-append "projects/" file-name) body))
diff --git a/utils.scm b/utils.scm
new file mode 100644
index 0000000..7b64cdc
--- /dev/null
+++ b/utils.scm
@@ -0,0 +1,51 @@
+;;; Copyright © 2018-2021 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-module (utils)
+ #:use-module (ice-9 rdelim)
+ #:use-module (srfi srfi-19)
+ #:export (date
+ stylesheet
+ anchor
+ link
+ centered-image
+ raw-snippet)
+ #:replace (link))
+
+(define (date year month day)
+ "Create a SRFI-19 date for the given YEAR, MONTH, DAY"
+ (let ((tzoffset (tm:gmtoff (localtime (time-second (current-time))))))
+ (make-date 0 0 0 0 day month year tzoffset)))
+
+(define (stylesheet name)
+ `(link (@ (rel "stylesheet")
+ (href ,(string-append "/css/" name ".css")))))
+
+(define* (anchor content #:optional (uri content))
+ `(a (@ (href ,uri)) ,content))
+
+(define (link name uri)
+ `(a (@ (href ,uri)) ,name))
+
+(define* (centered-image url #:optional alt)
+ `(img (@ (class "centered-image")
+ (src ,url)
+ ,@(if alt
+ `((alt ,alt))
+ '()))))
+
+(define (raw-snippet code)
+ `(pre (code ,(if (string? code) code (read-string code)))))