git.m455.casa

repo2html

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


repo2html.scm

1 #!/usr/bin/csi -s
2
3 (import
4 (chicken file)
5 (chicken format) ;; format
6 (chicken io) ;; read-line
7 (chicken pathname)
8 (chicken port)
9 (chicken process) ;; call-with-input-pipe
10 (chicken process-context)
11 (chicken string) ;; string-intersperse
12 (clojurian syntax)
13 ersatz
14 lowdown
15 scss
16 srfi-1 ;; list utils
17 srfi-13 ;; string utils
18 srfi-14 ;; charsets
19 sxml-transforms
20 symbol-utils ;; (unspecified-value)
21 utf8
22 utils
23 utils-git
24 )
25
26 ;; auto-apply ids to headings ---------------------------------
27 (define (slugify inner)
28 (->
29 inner
30 (pre-post-order*
31 `((*text* .
32 ,(lambda (_ str)
33 (if (string? str)
34 (->
35 str
36 (string-downcase)
37 (string-translate "/,:;\"[]{}()=+")
38 (string-translate " _." "---"))
39 str)))
40 ,@alist-conv-rules*))
41 (flatten)
42 ((flip map) ->string)
43 (string-intersperse "")
44 (substring* 0 40)))
45
46 (define (enumerate-tag inner)
47 (let ((slug (slugify inner)))
48 `((@ (id ,slug))
49 ,inner
50 (a (@ ((title "Permalink to this section")
51 (href "#" ,slug)))))))
52
53 ;; a relative link to a file within our own repo should get .html added to the
54 ;; target, since we make that filename change when rendering files for the web.
55 ;;
56 ;; thought it might also be good to apply that same treatment to any absolute
57 ;; links into our repo (or other repos on the same forge?) but that gets a bit
58 ;; messy, would need to drag variables holding current site, path, repo name all
59 ;; the way into here
60 ;;
61 ;; if adjust-relative is not false, it is a prefix to be added to relative
62 ;; links, to make the top-level readme link correctly into the site.
63 (define (adjust-relative-link adjust-relative inner)
64 (let* ((linkurl (alist-ref-in '(@ href) inner equal?))
65 (linkurl-startswith (cute string-prefix? <> (car linkurl))))
66 (if
67 (any linkurl-startswith '("#" "mailto:" "gemini:" "http://" "https://"))
68 inner
69 (alist-update-in '(@ href) (cons adjust-relative (append linkurl '(".html"))) inner equal?))))
70
71 ;; TODO FIXME for some reason, lowdown renders links differently than images:
72 ;; (markdown->sxml "[x](x)") => ((p (a (@ (href "x")) "x")))
73 ;; (markdown->sxml "![x](x)") => ((p (img (@ (src ("x")) (alt "x")))))
74
75 (define (adjust-relative-src adjust-relative inner)
76 (let* ((srcurl
77 (->
78 ;; ugh why
79 (alist-ref-in '(@ src) inner equal?)
80 (car)
81 ((lambda (x) (if (list? x) (car x) x)))))
82 (srcurl-startswith (cute string-prefix? <> srcurl)))
83 (if
84 (or (not adjust-relative)
85 (not srcurl)
86 (any srcurl-startswith '("/" "http://" "https://")))
87 inner
88 (alist-update-in '(@ src) `((,(string-append adjust-relative srcurl))) inner equal?))))
89
90 (define (sxml-html-rules adjust-relative)
91 `(;; assign all headings an id so you can link to them
92 (h1 . ,(lambda (t i) (cons t (enumerate-tag i))))
93 (h2 . ,(lambda (t i) (cons t (enumerate-tag i))))
94 (h3 . ,(lambda (t i) (cons t (enumerate-tag i))))
95 (h4 . ,(lambda (t i) (cons t (enumerate-tag i))))
96 (h5 . ,(lambda (t i) (cons t (enumerate-tag i))))
97 ;; if adjust-relative is true, all relative links should get prefixed with
98 ;; the relative-root
99 (a . ,(lambda (t i) (cons t (adjust-relative-link adjust-relative i))))
100 (img . ,(lambda (t i) (cons t (adjust-relative-src adjust-relative i))))
101 ;; this copied from lowdown's html-serialization-rules* because it is for
102 ;; some reason not exported, so i can't just import it??
103 (*COMMENT* . ,(lambda (_t i) (list #\< "!--" i "--" #\>)))
104 ;; ignore any #<unspecified> values in the tree
105 (*text* . ,(lambda (_t i) (if (unspecified? i) "" i)))
106 ,@alist-conv-rules*))
107
108 ;; environment always takes precedence over git-config
109 (define (config key)
110 (or
111 (get-environment-variable (string-append "REPO2HTML_" (string-upcase key)))
112 (git-config->string (string-append "repo2html." (string-downcase key)))))
113
114 ;; sxml generators for constructed pages ---------------------------------
115
116 (define (lines->string xs) (string-intersperse xs "\n"))
117
118 (define (lines->numbered-sxml lines)
119 `(table
120 (@ (id "file-contents"))
121 ,@(map (lambda (number line)
122 `(tr (@ ((class "line")
123 (id ,number)))
124 (td (@ (class "line-number"))
125 (a (@ (href "#" ,number)) ,number))
126 (td (@ (class "line-contents"))
127 (code ,line))))
128 (map number->string (iota (length lines) 1))
129 lines)))
130
131 (define (source->sxml source-file) ;; src/main.scm
132 (define-values (_ _ basename extension _)
133 (pathparts source-file))
134 (define (image-link)
135 `(p (img (@ (src (,(string-append basename extension)))))))
136 (define (plaintext)
137 `(pre ,(git-file->lines source-file)))
138 (define (numbered-sxml)
139 (-> source-file
140 git-file->lines
141 lines->numbered-sxml))
142 (define (binary)
143 '(p "(Binary file)"))
144 (case (string->symbol extension)
145 ((.md .markdown)
146 (handle-exceptions exn
147 (begin
148 (format (current-error-port) "Error parsing ~a\n" source-file)
149 `((p (b "There was an error parsing this file as Markdown."))
150 ,(plaintext)))
151 (-> source-file
152 git-file->lines
153 lines->string
154 markdown->sxml)))
155 ((.jpg .jpeg .png .gif .webp .webm .apng .avif .svgz .ico)
156 (image-link))
157 ((.svg)
158 (list (image-link) (plaintext)))
159 ((.gz .pack .idx)
160 (binary))
161 (else
162 (if (git-file-is-text? source-file)
163 (numbered-sxml)
164 (binary)))))
165
166 (define (filelist->sxml source-files-list relative-root)
167 `((h1 "Files")
168 ((ul
169 ,(map
170 (lambda (source-file)
171 `(li (a (@ (href ,(make-pathname relative-root source-file))) ,source-file)))
172 source-files-list)))))
173
174 (define (commits->sxml)
175 `((h1 "Commits")
176 (table
177 (tr ,@(map (lambda x `(th ,x)) '("Date" "Ref" "Log" "Author")))
178 ,(map
179 (lambda (commit) `(tr ,@(map (lambda x `(td ,x)) commit)))
180 (git-commits)))))
181
182 (define (contributors->sxml)
183 `((h1 "Contributors")
184 (table
185 (tr (th "Author") (th "Commits"))
186 ,(map
187 (lambda (line)
188 (let-values (((commits author) (apply values (string-split line "\t"))))
189 `(tr (td ,author) (td ,commits))))
190 (git-contributors)))))
191
192
193 (define (issueslist->sxml source-files-list)
194 `((h1 "Issues")
195 ((ul
196 ,(filter-map
197 (lambda (source-file)
198 (and
199 (string-prefix? "ISSUES/" source-file)
200 `(li (a (@ (href ,source-file))
201 ,(->
202 source-file
203 git-file->lines
204 ((lambda (x) (if (or (eof-object? x) (null-list? x)) (list (pathname-strip-directory source-file)) x)))
205 car
206 (string-trim (string->char-set "# ")))))))
207 source-files-list)))))
208
209 ;; used by ersatz writer
210 (define (alist->tvals vars)
211 (map (lambda (pair)
212 `(,(car pair) . ,(sexpr->tvalue (cdr pair)))) vars))
213
214 ;; this version uses a jinja-style template via ersatz
215 (define (make-template-writer-ersatz templates-directory #!optional vars)
216 (define template (statements-from-file (template-std-env search-path: (list templates-directory)) "default.html"))
217 (lambda (output-filename body-sxml #!optional newvars)
218 ;; create destination directory if needed
219 (if-let (destination-directory (pathname-directory output-filename))
220 (create-directory destination-directory #t)
221 '())
222
223 (let* (;; vars = global vars + file-specific vars
224 (vars (alist-merge vars (or newvars '())))
225 (adjust-relative (unless-equals (alist-ref 'relative_root vars) "html/"))
226 ;; render the sxml to a html string that we can hand to the template
227 (body-html
228 (with-output-to-string
229 (lambda ()
230 (SXML->HTML (pre-post-order* body-sxml (sxml-html-rules adjust-relative))))))
231 ;; vars = vars + body k/v pair
232 (vars (alist-cons 'content body-html vars)))
233
234 (with-output-to-file output-filename
235 (lambda ()
236 (display (eval-statements template models: (alist->tvals vars))))))))
237
238 ;; main program ------------------------------------------------------------------------------
239
240 (define (generate-html-files html-repo-path templates-directory)
241 ;; git automatically updates this hash when you checkout/pull/etc.
242 (let* ((version-ident "$Id$")
243 (source-files-list (git-repository->paths-list))
244 (forge-root (string-append (string-chomp (or (config "forgeroot") "") "/") "/"))
245 (repository-path (or (config "path")
246 (and (not (equal? forge-root "/"))
247 (string-prefix? forge-root html-repo-path)
248 (string-drop html-repo-path (string-length forge-root)))
249 (pathname-strip-directory html-repo-path)))
250 (template-alist
251 `(;; variables provided to template at all times. beware: ersatz
252 ;; templates break if you attempt to use a variable with a hyphen.
253
254 ;; the list of all files in the git repo
255 (source_files_list . ,source-files-list)
256 ;; the description of the repo, taken from: env, config, cgit-like
257 ;; description file
258 (repository_description . ,(or (config "description")
259 (if-let (f (file-exists? "description"))
260 (with-input-from-file f read-lines) #f)
261 ""))
262 ;; the name of the repo, which is usually but not necessarily the
263 ;; same as its directory name (and last path element of the url)
264 (repository_name . ,(or (config "name")
265 (-> html-repo-path
266 (string-chomp ".git")
267 (pathname-strip-directory))))
268 ;; the path from the forge root to the repository
269 (repository_path . ,repository-path)
270 ;; the repository_path with the last path element removed
271 (repository_path_parent . ,(or (pathname-directory repository-path) ""))
272 ;; the repository_path_parent as a list of path components
273 (repository_ancestors . ,(or (string-split (or (pathname-directory repository-path) "") "/") '()))
274 ;; the first README file found among these, if any.
275 (readme_file . ,(find (cut member <> source-files-list)
276 '("README.md" "README" "README.txt")))
277 ;; the first LICENSE file found among these, if any.
278 (license_file . ,(find (cut member <> source-files-list)
279 '("LICENSE.md" "LICENSE" "LICENSE.txt")))
280 ;; the string "ISSUES" if any files exist in ISSUES/
281 (issues_file . ,(and (find (cut string-prefix? "ISSUES/" <>) source-files-list) "ISSUES"))
282 (repo2html_version . ,(if (equal? version-ident (list->string '(#\$ #\I #\d #\$)))
283 ""
284 (substring* version-ident 5 12)))
285 ))
286 (write-with-template
287 (make-template-writer-ersatz templates-directory template-alist)))
288 (define html-path (make-pathname html-repo-path "html"))
289
290 (create-directory html-repo-path #t)
291 ;; special files
292 (write-with-template (make-pathname html-path "files" "html") (filelist->sxml source-files-list ""))
293 (write-with-template (make-pathname html-path "contributors" "html") (contributors->sxml))
294 (write-with-template (make-pathname html-path "commits" "html") (commits->sxml))
295 ;; htmlified repo contents
296 (for-each
297 (lambda (source-file)
298 (->> source-file
299 (pathparts)
300 (define-values (root elements basename extension relative-root)))
301 (write-with-template
302 (make-pathname html-path source-file "html")
303 (source->sxml source-file)
304 `(;; additional per-page variables provided to template
305 (source_file . ,source-file)
306 (root . ,root)
307 (elements . ,elements)
308 (basename . ,basename)
309 (extension . ,extension)
310 (relative_root . ,relative-root)
311 ))
312 ;; if it's an image, also write it verbatim to output directory
313 (case (string->symbol (or (pathname-extension source-file) ""))
314 ((jpg jpeg png gif webp webm svg apng avif svgz ico)
315 (git-copy source-file (make-pathname html-path source-file)))))
316 source-files-list)
317 ;; if README.md, README, or README.txt exists, regenerate it as index.html.
318 ;; otherwise regenerate files.html as index.html.
319 (write-with-template
320 (make-pathname html-repo-path "index" "html")
321 (if-let (readme-file
322 (alist-ref 'readme_file template-alist))
323 (source->sxml readme-file)
324 (filelist->sxml source-files-list ""))
325 ;; TODO: do we need the full set of template variables defined here?
326 ;; if so maybe this and the set above should be lifted out somewhere
327 `((relative_root . "html/")))
328
329 ;; if the ISSUES directory got created, write out an index file for the
330 ;; stuff in there.
331 (when (file-exists? (make-pathname html-path "ISSUES"))
332 (write-with-template (make-pathname html-path "ISSUES" "html") (issueslist->sxml source-files-list)))))
333
334 (define (main #!optional html-repo-path templates-directory)
335
336 (unless html-repo-path
337 (bail "please specify a destination directory for html files"))
338
339 (unless (in-git-directory?)
340 (bail "woops this isn't a git directory"))
341
342 (unless templates-directory
343 (bail "please specify the directory containing the templates.\nnote: built-in sxml templates have been removed."))
344
345 (generate-html-files (string-chomp html-repo-path "/") templates-directory))
346
347 (apply main (command-line-arguments))