about summary refs log tree commit diff
path: root/users/sterni/mblog/mblog.lisp
blob: 7823bde2034367b19d51479ca295291b2e86e878 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
;; SPDX-License-Identifier: GPL-3.0-only
;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni
;; SPDX-FileCopyrightText: Copyright (C) 2006-2010 by Walter C. Pelissero

(in-package :mblog)

;; util

;; Taken from SCLF, written by Walter C. Pelissero
(defun pathname-as-directory (pathname)
  "Converts PATHNAME to directory form and return it."
  (setf pathname (pathname pathname))
  (if (pathname-name pathname)
      (make-pathname :directory (append (or (pathname-directory pathname)
                                            '(:relative))
                                        (list (file-namestring pathname)))
                     :name nil
                     :type nil
                     :defaults pathname)
      pathname))

(defmacro with-overwrite-file ((&rest args) &body body)
  "Like WITH-OPEN-FILE, but creates/supersedes the given file for writing."
  `(with-open-file (,@args :direction :output
                           :if-exists :supersede
                           :if-does-not-exist :create)
     ,@body))

;; CSS

(defvar *style* "
header, main {
  width: 100%;
  max-width: 800px;
}

main img {
  max-width: 100%;
}

a:link, a:visited {
  color: blue;
}
")

;; Templating

(eval-when (:compile-toplevel :load-toplevel)
  (setf (who:html-mode) :html5))

(defmacro render-page ((stream title &key root) &body body)
  "Surround BODY with standard mblog document skeleton and render it to STREAM
  using CL-WHO. If :ROOT is T, assume that the page is the top level index page.
  Otherwise it is assumed to be one level below the index page."
  `(who:with-html-output (,stream nil :prologue t)
    (:html
     (:head
      (:meta :charset "utf-8")
      (:meta :viewport "width=device-width")
      (:title (who:esc ,title))
      (:link :rel "stylesheet"
             :type "text/css"
             :href ,(concatenate 'string (if root "" "../") "style.css")))
     (:body
      (:header
       (:nav
        (:a :href ,(who:escape-string (if root "" "..")) "index")))
      (:main ,@body)))))

;; Build Logic

(defun build-note-page (note note-dir)
  "Convert NOTE to HTML and write it to index.html in NOTE-DIR alongside any
  extra attachments NOTE contains."
  (with-overwrite-file (html-stream (merge-pathnames "index.html" note-dir))
    (render-page (html-stream (apple-note-subject note))
      (:article
       (apple-note-html-fragment note html-stream))))

  (mime:do-parts (part note)
    (unless (string= (mime:mime-id part)
                     (mime:mime-id (note:apple-note-text-part note)))
      (let ((attachment-in (mime:mime-body-stream part))
            (attachment-dst (merge-pathnames
                             (mime:mime-part-file-name part)
                             note-dir)))

        (format *error-output* "Writing attachment ~A~%" attachment-dst)

        (with-overwrite-file (attachment-out attachment-dst
                              :element-type
                              (stream-element-type attachment-in))
          (redirect-stream attachment-in attachment-out
                           :buffer-size *general-buffer-size*)))))

  (values))

(defun build-index-page (notes-list destination)
  "Write an overview page linking all notes in NOTE-LIST in the given order to
  DESTINATION. The notes are assumed to be in a sibling directory named like the
  each note's UUID."
  (with-overwrite-file (listing-stream destination)
    (render-page (listing-stream "mblog" :root t)
      (:h1 "mblog")
      (:table
       (dolist (note notes-list)
         (who:htm
          (:tr
           (:td (:a :href (who:escape-string (apple-note-uuid note))
                    (who:esc (apple-note-subject note))))
           (:td (who:esc
                 (klatre:format-dottime
                  (universal-to-timestamp (apple-note-time note)))))))))))
  (values))

(defun build-mblog (notes-dir html-dir)
  "Take MIME messages from maildir NOTES-DIR and build a complete mblog in HTML-DIR."
  (setf notes-dir (pathname-as-directory notes-dir))
  (setf html-dir (pathname-as-directory html-dir))

  ;; TODO(sterni): avoid rewriting if nothing was updated
  ;; TODO(sterni): clean up deleted things
  ;; TODO(sterni): atom feed

  (let ((all-notes '()))
    (dolist (message-path (maildir:list notes-dir))
      (let* ((note (make-apple-note (mime:mime-message message-path)))
             (note-dir  (merge-pathnames (make-pathname
                                          :directory
                                          `(:relative ,(apple-note-uuid note)))
                                         html-dir)))

        (format *error-output* "Writing note message ~A to ~A~%"
                message-path note-dir)
        (ensure-directories-exist note-dir)
        (build-note-page note note-dir)
        (push note all-notes)))

    ;; reverse sort the entries by time for the index page
    (setf all-notes (sort all-notes #'> :key #'apple-note-time))

    (build-index-page all-notes (merge-pathnames "index.html" html-dir))

    (with-overwrite-file (css-stream (merge-pathnames "style.css" html-dir))
      (write-string *style* css-stream))

    (values)))