blog.rkt 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #lang racket
  2. (require yaml
  3. markdown/parse
  4. sha
  5. (prefix-in gregor: gregor)
  6. "../response.rkt"
  7. "blog-renderer.rkt"
  8. "post-renderer.rkt"
  9. "page-links-renderer.rkt"
  10. "unknown-page.rkt"
  11. "../utils/list-operations.rkt"
  12. "../utils/hash-procedures.rkt"
  13. "../utils/date-procedures.rkt"
  14. "../code-highlighting.rkt")
  15. (provide blog-app
  16. post-app
  17. tag-app)
  18. ;; =========
  19. ;; CONSTANTS
  20. ;; =========
  21. (define POSTS-DIRECTORY "../data/posts/")
  22. (define METADATA-FILE-ENDING "meta")
  23. (define CONFIG (file->yaml "config.yaml"))
  24. ;; ===
  25. ;; APP
  26. ;; ===
  27. (define (blog-app request [page 0])
  28. (send-success-response
  29. (let* ([posts (read-post-directory)]
  30. [page (if (> (* page (hash-ref CONFIG "posts-per-page" 10)) (length posts))
  31. 0
  32. page)])
  33. (let* ([blog-renderer
  34. (create-blog-renderer #:blog-title (hash-ref CONFIG "blog-title" #t)
  35. #:blog-language (hash-ref CONFIG "blog-language" "en")
  36. #:max-posts (let ([config-max-posts (hash-ref CONFIG "max-posts" +inf.0)])
  37. (if (eqv? config-max-posts 'null)
  38. +inf.0
  39. config-max-posts))
  40. #:min-date (let* ([default-datetime
  41. (gregor:->datetime/utc
  42. (gregor:with-timezone (gregor:datetime 2000)
  43. "Europe/Berlin"))]
  44. [config-datetime
  45. (hash-ref CONFIG "min-date" #f)])
  46. (if config-datetime
  47. (date->gregor-datetime config-datetime
  48. (hash-ref CONFIG "time-zone" "Europe/Berlin"))
  49. default-datetime))
  50. #:posts-per-page# (hash-ref CONFIG "posts-per-page" 10)
  51. #:page-number page
  52. #:render-separators (hash-ref CONFIG "render-post-separators" #t))]
  53. [post-renderer
  54. (create-post-renderer #:render-metadata (hash-ref CONFIG "render-post-metadata" #t)
  55. #:render-toc (hash-ref CONFIG "render-post-toc" #t)
  56. #:render-content (hash-ref CONFIG "render-post-content" #t))]
  57. [page-links-renderer (create-page-links-renderer)])
  58. (blog-renderer post-renderer
  59. page-links-renderer
  60. posts
  61. page)))))
  62. (define (post-app request post-id)
  63. (let* ([posts (filter (λ (a-post)
  64. (= (PostMetadata-id (Post-metadata a-post)) post-id))
  65. (read-post-directory))]
  66. [page 0])
  67. (cond [(empty? posts)
  68. (respond-unknown-file request)]
  69. [else (let* ([blog-renderer (create-blog-renderer)]
  70. [post-renderer (create-post-renderer)]
  71. [page-links-renderer (create-page-links-renderer)])
  72. (send-success-response (blog-renderer post-renderer
  73. page-links-renderer
  74. posts
  75. page)))])))
  76. (define (tag-app request tag-name [page 0])
  77. (let* ([posts (filter (λ (a-post)
  78. (member (string-downcase tag-name)
  79. (map string-downcase
  80. (PostMetadata-tags (Post-metadata a-post)))))
  81. (read-post-directory))]
  82. [page 0])
  83. (cond [(empty? posts) (respond-unknown-file request)]
  84. [else
  85. (let* ([blog-renderer (create-blog-renderer)]
  86. [post-renderer (create-post-renderer)]
  87. [page-links-renderer
  88. (create-page-links-renderer #:prefix-parts (list "tag" tag-name))])
  89. (send-success-response (blog-renderer post-renderer
  90. page-links-renderer
  91. posts
  92. page)))])))
  93. ;; =============
  94. ;; READING POSTS
  95. ;; =============
  96. ;; some global state for memoization
  97. ;; only want to render files again if their hash changed
  98. (define post-hashes (make-hash))
  99. (define metadata-hashes (make-hash))
  100. (define read-metadatas (make-hash))
  101. (define rendered-posts (make-hash))
  102. ;; access with
  103. ;; (hash-set! hash key v)
  104. ;; (hash-ref hash key [failure-result])
  105. ;; (bytes->hex-string (sha256 (string->bytes/utf-8 "test")))
  106. (define (concat-with-posts-base-path file-path)
  107. (build-path POSTS-DIRECTORY file-path))
  108. (define (read-post-directory)
  109. (let* ([filesystem-items (directory-list (string->path POSTS-DIRECTORY))]
  110. [files (filter file-exists? (map concat-with-posts-base-path filesystem-items))])
  111. (map read-post-from-file
  112. (filter-post-files files))))
  113. (define (filter-post-files list-of-paths)
  114. (filter (λ (a-path)
  115. (and (file-extension-markdown? a-path)
  116. (published-post? a-path)))
  117. list-of-paths))
  118. ;; path: a path to a file
  119. (define (file-extension-markdown? path)
  120. (or (path-has-extension? path "md")
  121. (path-has-extension? path "mdown")
  122. (path-has-extension? path "markdown")))
  123. (define (file-extension-metadata? path)
  124. (or (path-has-extension? path METADATA-FILE-ENDING)))
  125. (define (published-post? path)
  126. #t)
  127. (define (read-metadata-for-post metadata-path)
  128. (cond [(file-exists? metadata-path)
  129. ;; using `or` here because `file->yaml` returns `#f` for empty metadata files
  130. (PostMetadata-from-unserialized-yaml (or (file->yaml metadata-path)
  131. (hash)))]
  132. [else
  133. ;; supply empty hash because no metadata has been found
  134. (PostMetadata-from-unserialized-yaml (hash))]))
  135. (define (read-post-from-file path)
  136. ;; getting the paths straight ...
  137. (let* ([filename (path->string (file-name-from-path path))]
  138. [extension (bytes->string/utf-8 (path-get-extension path))]
  139. [extension-position (substring-position filename extension)]
  140. [filename-no-extension (substring filename 0 extension-position)]
  141. [metadata-path (concat-with-posts-base-path
  142. (string->path
  143. (string-append filename-no-extension "." "meta")))])
  144. ;; getting the hashes of metadata and posts ...
  145. (let* ([metadata-as-string (if (file-exists? metadata-path)
  146. (file->string metadata-path #:mode 'text)
  147. "")]
  148. [post-as-string (file->string path #:mode 'text)]
  149. [hash-of-metadata (bytes->hex-string
  150. (sha256
  151. (string->bytes/utf-8 metadata-as-string)))]
  152. [hash-of-post (bytes->hex-string
  153. (sha256
  154. (string->bytes/utf-8 post-as-string)))])
  155. ;; getting rendered metadata and content ...
  156. (let ([metadata (cond [(string=? (hash-ref metadata-hashes metadata-path "") hash-of-metadata)
  157. (hash-ref read-metadatas metadata-path)]
  158. [else
  159. (let ([read-metadata (read-metadata-for-post metadata-path)])
  160. (hash-set! metadata-hashes metadata-path hash-of-metadata)
  161. (hash-set! read-metadatas metadata-path read-metadata)
  162. read-metadata)])]
  163. [content (cond [(string=? (hash-ref post-hashes path "") hash-of-post)
  164. (hash-ref rendered-posts path)]
  165. [else
  166. (displayln "post hashes did not match, rendering post")
  167. (let ([rendered-post (highlight-code-xexprs (parse-markdown path))])
  168. (hash-set! post-hashes path hash-of-post)
  169. (hash-set! rendered-posts path rendered-post)
  170. rendered-post)])])
  171. ;; make the post
  172. #;(hash-pretty-print metadata-hashes (λ (a-path b-path)
  173. (string<? (path->string a-path)
  174. (path->string b-path))))
  175. #;(hash-pretty-print post-hashes (λ (a-path b-path)
  176. (string<? (path->string a-path)
  177. (path->string b-path))))
  178. (Post-from-content content metadata)))))