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:
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)))))