grug

A static website generator written for Guile Scheme
Log | Files | Refs | README | LICENSE

commit 4fb352c507697ba4410211f4080952215fcabcaa
parent c3491fa94db4ccf37ee0398a9f35790221e6b1f5
Author: Luke Willis <lukejw@loquat.dev>
Date:   Fri,  4 Jul 2025 17:02:21 -0400

Add metadata parsing to blog, move readers to another file, and more

Diffstat:
MMakefile.am | 1+
MREADME.md | 5-----
Aexample/pages/about.md | 2++
Dexample/pages/err/404.md | 1-
Dexample/pages/foo.md | 2--
Mexample/posts/hello-world.md | 3++-
Aexample/posts/update-situation.md | 3+++
Dexample/posts/update_situation.md | 2--
Mgrug/builders.scm | 99+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Agrug/readers.scm | 25+++++++++++++++++++++++++
10 files changed, 95 insertions(+), 48 deletions(-)

diff --git a/Makefile.am b/Makefile.am @@ -26,6 +26,7 @@ bin_SCRIPTS = \ SOURCES = \ grug/ui.scm \ grug/utils.scm \ + grug/readers.scm \ grug/builders.scm EXTRA_DIST += \ diff --git a/README.md b/README.md @@ -3,8 +3,3 @@ A simple static site generator, written with [Guile](https://www.gnu.org/softwar ## Why? I am learning to work with Guile. I originally had a simple shell script that used cmark to generate static pages for my website, but I quickly reached limitations. I started to use haunt as my generator of choice, and I greatly enjoyed using it, but I struggled to understand how it actually worked on the inside. I figured that making a Guile program of my own would be a good step for me to take as a programmer. - -## TODO -- Make a framework for defining sites -- Make a framework for defining builders -- Make a framework for using different readers diff --git a/example/pages/about.md b/example/pages/about.md @@ -0,0 +1,2 @@ +# About Me +This is some information about me. diff --git a/example/pages/err/404.md b/example/pages/err/404.md @@ -1 +0,0 @@ -You failed. diff --git a/example/pages/foo.md b/example/pages/foo.md @@ -1,2 +0,0 @@ -# Welcome to FOO PAGE -Truly the best alternate page diff --git a/example/posts/hello-world.md b/example/posts/hello-world.md @@ -1,2 +1,3 @@ -# Hello, post! +`((title . "Hello, world!") (description . "You'll never guess what this post says")) +# GRUG Is Finally Working!!! This is a test post. diff --git a/example/posts/update-situation.md b/example/posts/update-situation.md @@ -0,0 +1,3 @@ +`((title . "Update Situation") (description . "In this post, I talk about another update.")) +# The Situation +Boys, we did it. There's another situation. diff --git a/example/posts/update_situation.md b/example/posts/update_situation.md @@ -1,2 +0,0 @@ -# The Update Situation -Things are changing, and it might be good or bad. diff --git a/grug/builders.scm b/grug/builders.scm @@ -2,20 +2,17 @@ #:use-module (ice-9 popen) #:use-module (ice-9 textual-ports) #:use-module (ice-9 pretty-print) + #:use-module (ice-9 rdelim) + #:use-module (ice-9 eval-string) #:use-module (htmlprag) + #:use-module (grug readers) #:use-module (grug utils) #:export (copy-directory simple-pages blog)) -;; Reads the markdown file at path and converts it into sxml representing html ('shtml') -;; TODO: Cleanup and move to its own module -(define (markdown->shtml path) - (let* ((cmd (string-append "cmark --to html --nobreaks < " path)) - (port (open-input-pipe cmd)) - (html (get-string-all port))) - (close-port port) - (delete '*TOP* (html->shtml (string-delete #\newline html))))) +;; TODO: Make a function that reads a file from path, parses it using the given reader, and +;; returns both the resulting shtml and the metadata (if any) in order to deduplicate code. ;; Copy the given directory to the site. ;; This is good for things like CSS folders. @@ -33,7 +30,6 @@ (copy-file path output-path))) (reverse (ls-recursive directory)))) -;; TODO: Cleanup and move to its own module (define (basic-template body) `(*TOP* (*DECL* DOCTYPE html) (html @@ -56,10 +52,7 @@ ;; Iterate through files in directory (for-each (lambda (path) - (let* ((base-shtml (markdown->shtml path)) - (built-shtml (template base-shtml)) - (output (shtml->html built-shtml)) - (output-dir + (let* ((output-dir (string-append prefix (substring (dirname path) (string-length directory)))) @@ -69,26 +62,40 @@ ".html"))) (unless (file-exists? output-dir) (mkdir output-dir)) (format #t "\t~A -> ~A\n" path output-path) - (call-with-output-file output-path + ;; Build html (no metadata parsing) + (call-with-input-file path (lambda (port) - (display output port))))) - ;; The list is reversed so that the shortest paths are listed first. + (let* ((input (get-string-all port)) + (base-shtml (cmark input)) + (built-shtml (template base-shtml)) + (output (shtml->html built-shtml))) + (call-with-output-file + output-path + (lambda (port) + (display output port)))))))) (reverse (ls-recursive directory)))) -;; TODO: Cleanup and move to its own module (define (basic-collection-template posts) `((h1 "Posts") ,@(map (lambda (post) - `(article (h2 ,post) - (p "Description..."))) - posts))) + `(article (h2 (a (@ (href ,(assoc-ref post 'uri))) + ,(assoc-ref post 'title))) + (p ,(assoc-ref post 'description)))) + posts))) -;; TODO: Figure out how to get title, date and description information from posts -;; This would likely require some kind of parsing system for the first line of the file. -;; Perhaps it could be written in scheme? `((title . "My Update") (date . "07/10/2007 18:00")) -;; If I were to modify the text by removing the first line, I would have to change the parsing -;; process. I'd have to do it the hard way using ports after cutting out the string. -;; TODO: Cleanup and move to its own module + +;; Build a blog using the posts in the given directory. +;; +;; Builds prefix/index.html and posts in prefix/post-prefix/. +;; Post files should define metadata on the first in the form of an a-list. +;; +;; Example: `((title . "Hello, world!") (description . "Foo bar baz...")) +;; +;; This will be parsed and a list of all the collected metadata will be passed to +;; collection-template, where you can process it any way you like. +;; A single entry titled uri will be added. +;; +;; TODO: Don't assume a markdown reader (define* (blog directory #:key (prefix "site") @@ -99,25 +106,43 @@ (let ((posts (map (lambda (path) - (let* ((base-shtml (markdown->shtml path)) - (built-shtml (template base-shtml)) - (output (shtml->html built-shtml)) - (output-dir - (string-append prefix "/" post-prefix + (let* ((output-name + (string-append (basename path ".md") + ".html")) + (relative-output-dir + (string-append post-prefix (substring (dirname path) (string-length directory)))) + (relative-output-path + (string-append relative-output-dir "/" + output-name)) + (output-dir + (string-append prefix "/" relative-output-dir)) (output-path (string-append output-dir "/" - (basename path ".md") - ".html"))) + output-name))) (unless (file-exists? output-dir) (mkdir output-dir)) (format #t "\t~A -> ~A\n" path output-path) - (call-with-output-file output-path + + (call-with-input-file path (lambda (port) - (display output port))) - output-path)) + ;; Strip and parse post metadata from the first line of the file + (define metadata (eval-string (read-line port))) + + ;; Build post html from the rest of the file + (let* ((input (get-string-all port)) + (base-shtml (cmark input)) + (built-shtml (template base-shtml)) + (output (shtml->html built-shtml))) + (call-with-output-file + output-path + (lambda (port) + (display output port)))) + + ;; Collect metadata with uri added + (acons 'uri relative-output-path metadata))))) (reverse (ls-recursive directory))))) - ;; Build index.html + ;; Build index.html (let ((output (shtml->html (template (collection-template posts)))) (output-path (string-append prefix "/index.html"))) (format #t "\t~A\n" output-path) diff --git a/grug/readers.scm b/grug/readers.scm @@ -0,0 +1,25 @@ +(define-module (grug readers) + #:use-module (ice-9 textual-ports) + #:use-module (htmlprag) + #:export (cmark)) + +;;; A reader is just a function that takes a string and outputs shtml. + +;; Reader that uses cmark to parse markdown into html. +(define (cmark md-string) + (let* ((md-pipe (pipe)) + (cmark-pipe (pipe)) + (pid (spawn "cmark" '("cmark" "--nobreaks") + #:input (car md-pipe) + #:output (cdr cmark-pipe)))) + (put-string (cdr md-pipe) md-string) + (close-port (cdr md-pipe)) + + (close-port (cdr cmark-pipe)) + (define html (get-string-all (car cmark-pipe))) + (close-port (car cmark-pipe)) + + (close-port (car md-pipe)) + (waitpid pid) + + (delete '*TOP* (html->shtml (string-delete #\newline html)))))