git.m455.casa

pancake

clone url: git://git.m455.casa/pancake


src/pancake.rkt

1 #lang racket/base
2
3 (require racket/file
4 racket/match
5 racket/string
6 racket/system
7 web-server/dispatchers/dispatch
8 web-server/servlet-env)
9
10 ;; -----------------------------------------------------------
11 ;; Path helpers
12 ;; -----------------------------------------------------------
13 (define directories
14 (hash
15 'pages (build-path "pages")
16 'posts (build-path "posts")
17 'output (build-path "output")
18 'drafts (build-path "drafts")
19 'layout (build-path "layout")
20 'pdfs (build-path "pdfs")
21 'css (build-path "css")
22 'images (build-path "images")
23 'misc (build-path "misc")))
24
25 (define (directories-ref a-key)
26 (hash-ref directories a-key))
27
28 (define (build-path-to-md-file-ref directories-key file-string)
29 (build-path (directories-ref directories-key)
30 (path-add-extension file-string #".md")))
31
32 (define files
33 (hash
34 'head (build-path-to-md-file-ref 'layout "head")
35 'nav (build-path-to-md-file-ref 'layout "nav")
36 'index (build-path-to-md-file-ref 'pages "index")
37 'footer (build-path-to-md-file-ref 'layout "footer")))
38
39 (define (files-ref a-key)
40 (hash-ref files a-key))
41
42 ;; -----------------------------------------------------------
43 ;; File contents
44 ;; -----------------------------------------------------------
45 (define head-contents
46 #<<string-block
47 ---
48 title: '[your blog title](/)'
49 lang: en
50 subtitle: 'your pronouns'
51 header-includes:
52 <meta name="author" content="Jesse Laprade" />
53 <meta name="description" content="m455's blog" />
54 <meta name="keywords" content="programming, documentation, personal homepages" />
55 <meta name="0" content=" "/>
56 <meta name="0" content=" "/>
57 <meta name="0" content=" built with "/>
58 <meta name="0" content=" p a n c a k e "/>
59 <meta name="0" content=" "/>
60 <meta name="0" content=" "/>
61 <meta name="0" content=" __//\ "/>
62 <meta name="0" content=" ,,------/__\\ \-----,,,_ "/>
63 <meta name="0" content=" ,'` , -```|___|\\/`-, ,`, "/>
64 <meta name="0" content=" ; `, ____,-` '' ,; "/>
65 <meta name="0" content=" :; `` ; ;` , ' ;;;` "/>
66 <meta name="0" content=" ';,;,,,; ;,,,,,,,,,,,;;;;`` "/>
67 <meta name="0" content=" ```-; ;``````````` "/>
68 <meta name="0" content=" ,''`` `;______________ "/>
69 <meta name="0" content=" ; ------------ `, "/>
70 <meta name="0" content=" `'--------------------` "/>
71 <meta name="0" content=" "/>
72 <meta name="0" content=" "/>
73 ---
74 string-block
75 )
76
77 (define nav-contents
78 #<<string-block
79 ::: {#nav}
80 [Home](index.html) - [I also link to home](index.html)
81 :::
82 <hr/>
83 ::: {#main}
84 string-block
85 )
86
87 (define index-contents
88 #<<string-block
89 # Welcome!
90
91 I am default text!
92 string-block
93 )
94
95 ;; The ":::" below ends the "::: {#main}" id from head-contents
96 (define footer-contents
97 #<<string-block
98 :::
99 <hr/>
100 ::: {#footer}
101 This website was built with [pancake](https://git.m455.casa/pancake.git)
102 :::
103 string-block
104 )
105
106 ;; -----------------------------------------------------------
107 ;; Display helpers
108 ;; -----------------------------------------------------------
109 (define-syntax-rule (displayln-format a-string ...)
110 (displayln (format a-string ...)))
111
112 (define (displayln-for . strings)
113 (for ([message strings])
114 (displayln message)))
115
116 (define (displayln-creating-file file-string)
117 (displayln-format "Creating file: ~a ..." file-string))
118
119 (define (displayln-creating-directory directory-string)
120 (displayln-format "Creating directory: ~a ..." directory-string))
121
122 (define (displayln-error error-type item-found)
123 (case error-type
124 ['arguments-invalid (displayln-format "Error: Found '~a', but expected 'help', 'initialize', 'preview', 'build', or 'clean'." item-found)]
125 ['arguments-length (displayln-format "Error: Found ~a arguments, but expected 1 argument." (number->string item-found))]
126 ['cannot-create-directory (begin (displayln-format "Error: Cannot create ~a." item-found)
127 (displayln-format "Reason: ~a is a file. Try moving this somewhere else and try again." item-found)
128 (exit))]
129 ['cannot-create-file (begin (displayln-format "Error: Cannot create ~a." item-found)
130 (displayln-format "Reason: ~a is a directory. Try moving this somewhere else and try again." item-found)
131 (exit))]
132 ['y-n (displayln-format "Error: Found '~a', but expected 'y' or 'n'." item-found)]))
133
134 ;; -----------------------------------------------------------
135 ;; Directory helpers
136 ;; -----------------------------------------------------------
137 (define (directories-exist?)
138 (andmap directory-exists? (hash-values directories)))
139
140 (define (path->directory-path-string directory-path)
141 (path->string (path->directory-path directory-path)))
142
143 (define (directory-create directory-path)
144 (if (file-exists? directory-path)
145 (displayln-error 'cannot-create-directory (path->string directory-path))
146 (begin
147 (let ([directory-string-with-slash (path->directory-path-string directory-path)])
148 (when (not (directory-exists? directory-path))
149 (displayln-creating-directory directory-string-with-slash)
150 (make-directory* directory-path))))))
151
152 ;; -----------------------------------------------------------
153 ;; File helpers
154 ;; -----------------------------------------------------------
155 (define (files-exist?)
156 (andmap file-exists? (hash-values files)))
157
158 (define (filter-extensions extension-string listof-file-paths)
159 (let ([listof-file-strings (map path->string listof-file-paths)])
160 (filter (lambda (file-string) (string-suffix? file-string extension-string))
161 listof-file-strings)))
162
163 ;; Lists all files inside of directories
164 (define (file-list)
165 (for/list ([file (in-directory)]) file))
166
167 (define (file-create file-path)
168 (if (directory-exists? file-path)
169 (displayln-error 'cannot-create-file)
170 (when (not (file-exists? file-path))
171 (displayln-creating-file (path->string file-path))
172 (close-output-port (open-output-file file-path #:exists 'truncate)))))
173
174 (define (file-populate file-path contents)
175 (displayln-format "Populating file with default values: ~a ..." (path->string file-path))
176 (display-to-file contents file-path #:exists 'truncate))
177
178 (define (file-populate-ref key contents)
179 (file-populate (files-ref key) contents))
180
181 (define (file-create-populate file-path contents)
182 (file-create file-path)
183 (file-populate file-path contents))
184
185 ;; -----------------------------------------------------------
186 ;; Repair - Checks for and creates missing directories and files
187 ;; -----------------------------------------------------------
188 (define (repair/cancel)
189 (displayln "Cancelled directory and file repairs.")
190 (exit))
191
192 (define (repair/elements-missing files-required files-to-check)
193 (if (null? files-required)
194 null
195 (if (member (car files-required) files-to-check)
196 (repair/elements-missing (cdr files-required) files-to-check)
197 (cons (car files-required)
198 (repair/elements-missing (cdr files-required) files-to-check)))))
199
200 (define (repair/create-missing-directories missing-directory-paths)
201 (when (not (null? missing-directory-paths))
202 (for ([directory missing-directory-paths])
203 (directory-create directory))))
204
205 (define (repair/create-missing-files missing-file-paths)
206 (when (not (null? missing-file-paths))
207 (for ([file missing-file-paths])
208 (file-create file))))
209
210 (define (repair/populate-files missing-file-paths)
211 (when (not (null? missing-file-paths))
212 (let ([head (files-ref 'head)]
213 [nav (files-ref 'nav)]
214 [index (files-ref 'index)]
215 [footer (files-ref 'footer)])
216 (for ([file missing-file-paths])
217 (match (path->string file)
218 [head (file-populate-ref 'head head-contents)]
219 [nav (file-populate-ref 'nav nav-contents)]
220 [index (file-populate-ref 'index index-contents)]
221 [footer (file-populate-ref 'footer footer-contents)]
222 [_ 'else])))))
223
224 (define (repair/start missing-directory-paths missing-file-paths)
225 (repair/create-missing-directories missing-directory-paths)
226 (repair/create-missing-files missing-file-paths)
227 (repair/populate-files missing-file-paths))
228
229 ;; missing-directory-paths and missing-file-paths will be null if
230 ;; all required directories and files exist
231 ;;
232 ;; the (filter directory-exists? ...) removes files from the list of paths
233 (define (repair/gather-missing-directory-paths)
234 (let* ([listof-required-paths (hash-values directories)]
235 [listof-current-paths (filter directory-exists? (directory-list))]
236 [listof-missing-paths (repair/elements-missing listof-required-paths listof-current-paths)])
237 ;; return missing directories.
238 ;; path->directory-path appeneds a "/" at the end
239 listof-missing-paths))
240
241 ;; The (filter file-exists? ...) removes directories from the list of paths
242 (define (repair/gather-missing-file-paths)
243 (let* ([listof-required-paths (hash-values files)]
244 [listof-current-paths (filter file-exists? (file-list))])
245 (repair/elements-missing listof-required-paths listof-current-paths)))
246
247 (define (repair/display-missing-paths listof-paths)
248 (when (not (null? listof-paths))
249 (let* ([listof-strings (map path->string listof-paths)]
250 [listas-string (string-join listof-strings "\n")])
251 (displayln listas-string))))
252
253 ;; The (map path->directory-path ...) adds a "/" to each path in the list, which is
254 ;; only for displaying to the user
255 (define (repair/prompt)
256 (let ([missing-directory-paths (repair/gather-missing-directory-paths)]
257 [missing-file-paths (repair/gather-missing-file-paths)])
258 (displayln "The following directories or files are missing:")
259 (repair/display-missing-paths (map path->directory-path missing-directory-paths))
260 (repair/display-missing-paths missing-file-paths)
261 (displayln "Do you want to create them? [y/n]")
262 (display "> ")
263 (flush-output)
264 (let ([user-choice (read-line)])
265 (case (string->symbol user-choice)
266 [(y Y yes Yes) (repair/start missing-directory-paths missing-file-paths)]
267 [(n N no No) (repair/cancel)]
268 [else (displayln-error 'y-n user-choice)]))))
269
270 (define (repair)
271 (displayln "Checking for missing directories or files ...")
272 (if (not (and (directories-exist?) (files-exist?)))
273 (repair/prompt)
274 (displayln "No missing directories or files found!")))
275
276 ;; -----------------------------------------------------------
277 ;; Help
278 ;; -----------------------------------------------------------
279 (define (help)
280 (displayln-for
281 "pancake help - Displays this help message"
282 "pancake initialize - Generates the directories and files in your current directory, so pancake can operate"
283 "pancake build - Builds your website in the output/ directory"
284 "pancake clean - Deletes the contents of the output/ directory"
285 "pancake preview - Starts a local HTTP server in your output/ directory to view your website."))
286
287 ;; -----------------------------------------------------------
288 ;; Clean - Deletes content in ./output/, except for ".git"
289 ;; -----------------------------------------------------------
290 (define (clean/cancel)
291 (displayln "Cancelled cleaning of directories and files.")
292 (exit))
293
294 (define (clean/delete-files)
295 (let* ([output-directory (directories-ref 'output)]
296 [listof-paths (directory-list output-directory)])
297 (for ([path listof-paths])
298 (when (not (equal? path (string->path ".git")))
299 (delete-directory/files (build-path output-directory path))))
300 (displayln "Cleaning complete!")))
301
302 (define (clean)
303 (repair)
304 (displayln-for "Caution: The clean command will delete all contents of the output/ directory, except for any .git directories found."
305 "Do you want to continue? [y/n]")
306 (display "> ")
307 (flush-output)
308 (let ([user-choice (read-line)])
309 (case (string->symbol user-choice)
310 [(y Y yes Yes) (clean/delete-files)]
311 [(n N no No) (clean/cancel)]
312 [else (displayln-error 'y-n user-choice)])))
313
314 ;; -----------------------------------------------------------
315 ;; Build - Builds website in ./output/
316 ;; -----------------------------------------------------------
317 (define (build/copy directories-key) ;; directories-key is 'css, 'pdfs, or 'images
318 (let* ([css-directory (directories-ref directories-key)]
319 [output-directory (build-path (directories-ref 'output)
320 (directories-ref directories-key))]
321 [listof-file-paths (directory-list (directories-ref directories-key))])
322 (directory-create output-directory)
323 (for ([file-path listof-file-paths])
324 (copy-file (build-path css-directory file-path)
325 (build-path output-directory file-path) #t))))
326
327 (define (build/create-output-args directories-key file-string)
328 (let ([directory (case directories-key
329 ['posts (build-path (directories-ref 'output)
330 (directories-ref directories-key))]
331 ['pages (build-path (directories-ref 'output))])]
332 [file-name (path-replace-extension file-string ".html")])
333 (path->string (build-path directory file-name))))
334
335 (define (build/create-input-args directories-key file-string)
336 (let* ([listof-input-paths (list (files-ref 'head)
337 (files-ref 'nav)
338 (build-path (directories-ref directories-key) file-string)
339 (files-ref 'footer))]
340 [listof-input-strings (map path->string listof-input-paths)])
341 (string-join listof-input-strings)))
342
343 (define (build/prepend-pandoc-prefix css-file)
344 (let* ([pandoc-flag "-c /"]
345 [css-directory (directories-ref 'css)]
346 [path-to-css-file (build-path css-directory css-file)]
347 [path-to-css-file-string (path->string path-to-css-file)])
348 (string-append pandoc-flag path-to-css-file-string)))
349
350 (define (build/create-css-args)
351 (let* ([css-directory (directories-ref 'css)]
352 [listof-css-file-strings (filter-extensions ".css" (directory-list css-directory))]
353 [listof-css-file-strings-prefixed (map build/prepend-pandoc-prefix listof-css-file-strings)])
354 (string-join listof-css-file-strings-prefixed)))
355
356 (define (build/markdown->html directories-key file-string)
357 (let ([css-args (build/create-css-args)]
358 [input-args (build/create-input-args directories-key file-string)]
359 [output-args (build/create-output-args directories-key file-string)])
360 ;; In the future, I will need to change `--base-head-level=2` to `--shift-heading-level-by=NUMBER`,
361 ;; because Debian is using an older version of Pandoc.
362 (system (format "pandoc -s ~a ~a -o ~a --base-header-level=2"
363 css-args
364 input-args
365 output-args))))
366
367 (define (build/convert-files directories-key)
368 (let* ([listof-file-strings (directory-list (directories-ref directories-key))]
369 [listof-md-file-strings (filter-extensions ".md" listof-file-strings)])
370 (for ([file-string listof-md-file-strings])
371 (build/markdown->html directories-key file-string))))
372
373 (define (build/generate directories-key)
374 (case directories-key
375 ['posts (directory-create (build-path (directories-ref 'output)
376 (directories-ref directories-key)))]
377 ['pages (directory-create (build-path (directories-ref 'output)))])
378 (build/convert-files directories-key))
379
380 (define (build)
381 (let ([output-directory (path->directory-path (build-path (directories-ref 'output)))])
382 (repair)
383 (displayln "Building your website ...")
384 (for ([a '(posts pages)]) (build/generate a))
385 (for ([b '(css pdfs images misc)]) (build/copy b))
386 (displayln "Website build complete!")
387 (displayln-format "Your website was built in ~a" output-directory)))
388
389 ;; -----------------------------------------------------------
390 ;; Preview
391 ;; -----------------------------------------------------------
392 ;; I referenced Greg Hendershott's servlet for this.
393 ;; Source: https://github.com/greghendershott/blog/blob/master/rkt/make-preview.rkt
394 (define (preview)
395 (repair)
396 ;; serve/servlet expects a procedure that expects a non-keyword argument
397 ;; as its first parameter. (next-dispatcher) makes a response, which the
398 ;; servlet expects.
399 (serve/servlet (lambda (x) (next-dispatcher))
400 #:servlet-path "/index.html"
401 #:extra-files-paths (list "output/")
402 #:listen-ip "127.0.0.1"
403 #:port 8000
404 #:launch-browser? #t))
405
406 ;; -----------------------------------------------------------
407 ;; Initialize
408 ;; -----------------------------------------------------------
409 (define (initialize/files-populate)
410 (file-populate-ref 'head head-contents)
411 (file-populate-ref 'nav nav-contents)
412 (file-populate-ref 'index index-contents)
413 (file-populate-ref 'footer footer-contents))
414
415 (define (initialize/files-create)
416 (for ([file (hash-values files)])
417 (file-create file)))
418
419 (define (initialize/directories-create)
420 (for ([directory (hash-values directories)])
421 (directory-create directory)))
422
423 (define (initialize/start)
424 (initialize/directories-create)
425 (initialize/files-create)
426 (initialize/files-populate)
427 (displayln "Initialization complete!"))
428
429 (define (initialize/cancel)
430 (displayln "Cancelled directory creation.")
431 (exit))
432
433 ;; The path must be built here, instead of in the directory definitions
434 ;; themselves, because the directories do not have the path-building
435 ;; procedure built into them in order to avoid problems when joining with
436 ;; the file paths, such as:
437 ;; /home/username/directory/home/username/file.txt
438 (define (initialize/display-directories)
439 (for ([directory (hash-values directories)])
440 (let* ([directory-path (path->directory-path-string directory)])
441 (displayln directory-path))))
442
443 (define (initialize/display-files)
444 (for ([path (hash-values files)])
445 (displayln path)))
446
447 (define (initialize/prompt)
448 (displayln-for "Caution: If any of the files above exist, then they will be overwritten with default values."
449 "Note: If any of the directories above exist, then they will remain unmodified."
450 "Do you want to continue? [y/n]")
451 (display "> ")
452 (flush-output)
453 (let ([user-choice (read-line)])
454 (case (string->symbol user-choice)
455 [(y Y yes Yes) (initialize/start)]
456 [(n N no No) (initialize/cancel)]
457 [else (displayln-error 'y-n user-choice)])))
458
459 (define (initialize)
460 (displayln "pancake will create the following directories and files in your current directory:")
461 (initialize/display-directories)
462 (initialize/display-files)
463 (initialize/prompt))
464
465 ;; -----------------------------------------------------------
466 ;; Argument handling
467 ;; -----------------------------------------------------------
468 (define (process-args vectorof-args)
469 (match vectorof-args
470 [(vector "initialize") (initialize)]
471 [(vector "preview") (preview)]
472 [(vector "build") (build)]
473 [(vector "clean") (clean)]
474 [(or '#("-h")
475 '#("--help")
476 '#("help")) (help)]
477 [(vector) (displayln-error 'arguments-length (vector-length vectorof-args))]
478 [(vector _ ...) (displayln-error 'arguments-invalid (vector-ref vectorof-args 0))]))
479
480 (define (main vectorof-args)
481 (process-args vectorof-args))
482
483 (main (current-command-line-arguments))