37ef2922e8
this lets us use ,(when ...) when constructing sxml for optionally-present elements.
293 lines
10 KiB
Scheme
Executable file
293 lines
10 KiB
Scheme
Executable file
#!/usr/bin/csi -s
|
|
|
|
(import utf8
|
|
lowdown
|
|
(chicken string)
|
|
(chicken port)
|
|
(chicken io)
|
|
(chicken process)
|
|
(chicken process-context)
|
|
(chicken format)
|
|
(chicken pathname)
|
|
(chicken file)
|
|
sxml-transforms
|
|
(clojurian syntax)
|
|
srfi-1 ;; list utils
|
|
srfi-13 ;; string utils
|
|
srfi-14 ;; charsets
|
|
symbol-utils ;; (unspecified-value)
|
|
)
|
|
|
|
(define CLONE-URL (or (get-environment-variable "REPO2HTML_CLONE_URL") "git://git.example.com"))
|
|
(define TITLE (or (get-environment-variable "REPO2HTML_TITLE") "my git repositories"))
|
|
(define DESCRIPTION (or (get-environment-variable "REPO2HTML_DESCRIPTION") "my git repositories"))
|
|
(define H1 (or (get-environment-variable "REPO2HTML_H1") "git.example.com"))
|
|
|
|
;; small utilities ---------------------------------
|
|
|
|
;; (bail [message [exit-status]])
|
|
;; end the program immediately.
|
|
;; if a message is provided, print it to the screen.
|
|
;; exit-status defaults to 1.
|
|
(define (bail . args)
|
|
(let-optionals args ((msg "") (status 1))
|
|
(unless (equal? "" msg) (print msg))
|
|
(exit status)))
|
|
|
|
;; decompose a path s into its constituent parts. returns values:
|
|
;;
|
|
;; root: "/" if it's an absolute path, "" if relative
|
|
;; directory-elements: a list of each directory from root, () if none
|
|
;; basename: the filename with extension removed like "readme" or ".bashrc"
|
|
;; extension: the file extension with the dot, like ".txt" or "" if none
|
|
;; relative-root: the relative path from the given path to the root
|
|
;; e.g foo/bar/baz.html -> ../../
|
|
;;
|
|
;; this is intended to provide default values that make for easier reassembly
|
|
;; into filenames.
|
|
;;
|
|
;; typical use:
|
|
;; (->> source-file
|
|
;; (pathparts)
|
|
;; (define-values (root elements basename extension relative-root)))
|
|
;;
|
|
(define (pathparts s)
|
|
(define-values (dirname basename extension)
|
|
(decompose-pathname s))
|
|
(define-values (origin root directory-elements)
|
|
(decompose-directory (or dirname "")))
|
|
;; discarding origin because idgaf about windows
|
|
(values (or root "")
|
|
(or directory-elements '())
|
|
basename
|
|
(if extension (string-append "." extension) "")
|
|
(->>
|
|
(or directory-elements '())
|
|
(map (constantly "../"))
|
|
(apply string-append))))
|
|
|
|
;; main code ---------------------------------
|
|
|
|
(define css "
|
|
body {
|
|
margin: 0 auto;
|
|
max-width: 700px;
|
|
}
|
|
pre, code {
|
|
background-color: #ffd9df;
|
|
}
|
|
pre {
|
|
padding: 15px 20px;
|
|
white-space: pre;
|
|
overflow: scroll;
|
|
}
|
|
a { color: blue; }
|
|
nav a { margin-right: 10px; }
|
|
hr {
|
|
border:0;
|
|
border-bottom: 1px solid black;
|
|
margin-top: 16px;
|
|
}
|
|
td {
|
|
padding: 0em .5em;
|
|
vertical-align: top;
|
|
}
|
|
footer {
|
|
text-align: right;
|
|
font-size: small;
|
|
}
|
|
#file-path {
|
|
/* change this to your liking */
|
|
}")
|
|
|
|
(define (make-sxml-template-wrapper repository-name source-files-list)
|
|
(let ((readme-file
|
|
(find (lambda (x) (member x source-files-list))
|
|
'("README" "README.md" "README.txt")))
|
|
(license-file
|
|
(find (lambda (x) (member x source-files-list))
|
|
'("LICENSE" "LICENSE.md" "LICENSE.txt")))
|
|
(issues-present?
|
|
(find (lambda (x) (string-prefix? "ISSUES/" x)) source-files-list)))
|
|
|
|
(lambda (source-file body-sxml)
|
|
(define-values (_ _ _ _ relative-root) (pathparts source-file))
|
|
`(html (@ lang en)
|
|
(head
|
|
(title ,TITLE)
|
|
(meta (@ charset utf-8))
|
|
(meta (@ (name viewport) (content "width=device-width, initial-scale-1.0, user-scalable=yes")))
|
|
(link (@ (rel icon) (href "data:,")))
|
|
(meta (@ (name description) (content ,DESCRIPTION)))
|
|
(style ,css))
|
|
(body
|
|
(h1 ,H1)
|
|
(h2 ,repository-name)
|
|
(p "clone url:" ,CLONE-URL "/" ,repository-name)
|
|
(nav
|
|
,(when readme-file
|
|
`(a (@ href ,relative-root "index.html") "about"))
|
|
(a (@ href ,relative-root "files.html") "files")
|
|
,(when license-file
|
|
`(a (@ href ,relative-root ,license-file ".html") "license"))
|
|
,(when issues-present?
|
|
`(a (@ href ,relative-root "ISSUES.html") "issues"))
|
|
(a (@ href ,relative-root "commits.html") "commits")
|
|
(a (@ href ,relative-root "contributors.html") "contributors")))
|
|
(hr)
|
|
,body-sxml
|
|
(hr)
|
|
(footer (p "Generated by " (a (@ href "https://git.m455.casa/repo2html/") "repo2html")))))))
|
|
|
|
(define (in-git-directory?)
|
|
(not (eof-object? (call-with-input-pipe "git rev-parse --git-dir" read-line))))
|
|
|
|
(define (git-file-is-text? path)
|
|
(not (equal?
|
|
"-\t-\t"
|
|
(call-with-input-pipe
|
|
(string-append "git diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 --numstat HEAD -- " path)
|
|
(lambda (port) (read-line port 4))))))
|
|
|
|
(define (git-repository->paths-list)
|
|
(call-with-input-pipe "git ls-tree -r --name-only HEAD" read-lines))
|
|
|
|
(define (git-file->string path)
|
|
(->
|
|
(format "git show HEAD:~a" path)
|
|
(call-with-input-pipe read-lines)
|
|
(string-intersperse "\n")))
|
|
|
|
(define (source->sxml source-file) ;; src/main.scm
|
|
(define-values (_ _ basename extension _)
|
|
(pathparts source-file))
|
|
(define (image-link)
|
|
`(p (img (@ src ,basename ,extension))))
|
|
(define (plaintext)
|
|
`(pre ,(git-file->string source-file)))
|
|
(define (binary)
|
|
'(p "(Binary file)"))
|
|
`((p (@ id "file-path") ,source-file)
|
|
,(case (string->symbol extension)
|
|
((.md .markdown)
|
|
(handle-exceptions exn
|
|
(begin
|
|
(format (current-error-port) "Error parsing ~a\n" source-file)
|
|
`((p (b "There was an error parsing this file as Markdown."))
|
|
,(plaintext)))
|
|
(markdown->sxml (git-file->string source-file))))
|
|
((.jpg .jpeg .png .gif .webp .webm .apng .avif .svgz .ico)
|
|
(image-link))
|
|
((.svg)
|
|
(list (image-link) (plaintext)))
|
|
((.gz .pack .idx)
|
|
(binary))
|
|
(else
|
|
(if (git-file-is-text? source-file)
|
|
(plaintext)
|
|
(binary))))))
|
|
|
|
(define (filelist->sxml source-files-list)
|
|
`((h1 "Files")
|
|
((ul
|
|
,(map
|
|
(lambda (source-file)
|
|
`(li (a (@ href ,source-file ".html") ,source-file)))
|
|
source-files-list)))))
|
|
|
|
(define (commits->sxml)
|
|
`((h1 "Commits")
|
|
(table
|
|
(tr (th "Date") (th "Ref") (th "Log") (th "Author"))
|
|
,(map
|
|
(lambda (line)
|
|
(let-values (((date ref title author) (apply values (string-split line "\t"))))
|
|
`(tr (td ,date) (td ,ref) (td ,title) (td ,author))))
|
|
(call-with-input-pipe "git log --format=format:%as%x09%h%x09%s%x09%aN HEAD" read-lines)))))
|
|
|
|
(define (contributors->sxml)
|
|
`((h1 "Contributors")
|
|
(table
|
|
(tr (th "Author") (th "Commits"))
|
|
,(map
|
|
(lambda (line)
|
|
(let-values (((commits author) (apply values (string-split line "\t"))))
|
|
`(tr (td ,author) (td ,commits))))
|
|
(call-with-input-pipe "git shortlog -ns HEAD" read-lines)))))
|
|
|
|
(define (issueslist->sxml source-files-list)
|
|
`((h1 "Issues")
|
|
((ul
|
|
,(filter-map
|
|
(lambda (source-file)
|
|
(and
|
|
(string-prefix? "ISSUES/" source-file)
|
|
`(li (a (@ href ,source-file ".html")
|
|
,(->
|
|
source-file
|
|
((flip format) "git show HEAD:~a")
|
|
(call-with-input-pipe read-line)
|
|
(string-trim (string->char-set "# ")))))))
|
|
source-files-list)))))
|
|
|
|
(define (generate-html-files html-repo-path)
|
|
(let* ((source-files-list (git-repository->paths-list))
|
|
(repository-name (pathname-strip-directory (string-chomp html-repo-path "/")))
|
|
(template-wrap->sxml (make-sxml-template-wrapper repository-name source-files-list)))
|
|
|
|
(define (write-with-template filename sxml)
|
|
(let ((destination-directory (pathname-directory filename)))
|
|
(when destination-directory
|
|
(create-directory (make-pathname html-repo-path destination-directory) #t)))
|
|
(with-output-to-file (make-pathname html-repo-path filename)
|
|
(lambda ()
|
|
(display "<!DOCTYPE html>\n")
|
|
(SXML->HTML
|
|
(pre-post-order*
|
|
(template-wrap->sxml filename sxml)
|
|
`((*text* . ,(lambda (trigger str)
|
|
(if (equal? str (unspecified-value))
|
|
""
|
|
((alist-ref '*text* alist-conv-rules*) trigger str))))
|
|
,@alist-conv-rules*))))))
|
|
|
|
(create-directory html-repo-path #t)
|
|
;; special files
|
|
(write-with-template "files.html" (filelist->sxml source-files-list))
|
|
(write-with-template "contributors.html" (contributors->sxml))
|
|
(write-with-template "commits.html" (commits->sxml))
|
|
;; htmlified repo contents
|
|
(for-each
|
|
(lambda (source-file)
|
|
(write-with-template
|
|
(string-append source-file ".html")
|
|
(source->sxml source-file))
|
|
(case (string->symbol (or (pathname-extension source-file) ""))
|
|
((jpg jpeg png gif webp webm svg apng avif svgz ico)
|
|
(system (format "git show HEAD:~a > ~a"
|
|
source-file
|
|
(make-pathname html-repo-path source-file))))))
|
|
source-files-list)
|
|
;; if README.md, README, or README.txt exists, copy that to index.html.
|
|
;; otherwise copy files.html to index.html.
|
|
(->>
|
|
'("README.md.html" "README.html" "README.txt.html" "files.html")
|
|
(map (lambda (x) (make-pathname html-repo-path x)))
|
|
(find file-exists?)
|
|
((lambda (x) (copy-file x (make-pathname html-repo-path "index.html") #t))))
|
|
|
|
(when (file-exists? (make-pathname html-repo-path "ISSUES"))
|
|
(write-with-template "ISSUES.html" (issueslist->sxml source-files-list)))))
|
|
|
|
(define (main args)
|
|
(let-optionals args ((html-repo-path ""))
|
|
|
|
(when (equal? html-repo-path "")
|
|
(bail "please specify a destination directory for html files"))
|
|
|
|
(unless (in-git-directory?)
|
|
(bail "woops this isn't a git directory"))
|
|
|
|
(generate-html-files html-repo-path)))
|
|
|
|
(main (command-line-arguments))
|