root/directory-view.scm

(define-module (directory-view)
  #:use-module (srfi srfi-1)  ;; map/fold
  #:use-module (srfi srfi-19) ;; date/time
  #:use-module (srfi srfi-26) ;; partial application
  #:use-module (ice-9 optargs)
  #:use-module (ice-9 regex)
  #:use-module (sxml simple)
  ;; local
  #:use-module (path-utils)
  #:use-module (system-utils)
  #:use-module (utils)
  #:use-module (html-wrapper)
  #:use-module (file-view)
  #:use-module (favicon)
  #:export (directory-view
            directory-view-head
            directory-view-dir
            directory-view-readme
            find-with-suffix
            find-with-prefix
            get-files-list
            make-readme))

(define (list-files dir)
  (define (step stream acc)
    (let ((item (readdir stream)))
      (if (eof-object? item)
          acc
          (step stream (cons item acc)))))
  (let ((stream (opendir dir)))
    (step stream '())))

(define (filter-files a)
  (if (string-match "~$|^\\.$|^\\.\\.$|^\\.git$" a ) #f #t))

(define (sort-files prefix a b)
  (let ((a-is-dir (eq? 'directory (stat:type (lstat (path-append prefix a)))))
        (b-is-dir (eq? 'directory (stat:type (lstat (path-append prefix b))))))
    (if (or (and a-is-dir
                 b-is-dir)
            (and (not a-is-dir)
                 (not b-is-dir)))
        (string<? a b)
        a-is-dir)))

(define (get-files-list path)
  (sort
   (filter filter-files (list-files path))
   (cut sort-files path <> <>)))

(define (make-readme prefix-data items)
  (let ((match (find (lambda (s)
                       (regexp-exec (make-regexp "^README" regexp/icase) s))
                     items)))
    (if match
        `(div (@ (class "README"))
              ,(file-view (path-append prefix-data match) match))
        '())))

(define (find-with-suffix items suffix)
  (define match (find (lambda (s)
                        (regexp-exec (make-regexp
                                      (string-append suffix "$")
                                      regexp/icase) s))
                      items))
  match)

(define (find-with-prefix items prefix)
  (define match (find (lambda (s)
                        (regexp-exec (make-regexp
                                      (string-append "^" prefix)
                                      regexp/icase) s))
                      items))
  match)

(define (raw-prefix path)
  (let* ((parts (length (string-split path #\/)))
         (steps-up parts)
         (prefix (repeat steps-up "..")))
    (if (string=? "raw" path)
        "." ;; don't go up when in /index.html
        (string-join prefix "/"))))

(define (href-to-raw prefix item)
  (string-append (raw-prefix prefix) "/" prefix "/" item))

(define* (make-anchor-target prefix-data prefix-html prefix-a item #:key (favicon #f))
  (let* ((href-data (path-append prefix-data item))
         (item-stat (lstat href-data))
         (is-dir (eq? 'directory (stat:type item-stat)))
         (is-symlink (eq? 'symlink (stat:type item-stat)))
         (href-html (path-append prefix-html
                                 (if is-dir
                                     (string-append item "/index.html")
                                     (string-append item ".html")))))
    ;; if (file-exists? href-html)
    (when is-dir
      (mkdir-p (dirname href-html)))
    (with-output-to-file href-html
      (lambda ()
        (define title (if is-dir
                          (string-append "Directory listing: " (dirname href-html))
                          (string-append "File view: " (basename href-html))))
        (set-port-encoding! (current-output-port) "utf-8")
        (sxml->xml
         (html-wrapper
          title
          (cond (is-dir
                 (directory-view href-data (dirname href-html) "." (get-files-list href-data)
                                 #:favicon favicon))
                (is-symlink
                 `("[symlink]"))
                (else (file-view href-data)))
          #:favicon (if favicon
                        ;; for every dir in prefix-html add ..
                        (let* ((parts (length (string-split prefix-html #\/)))
                               (steps-up (if is-dir
                                             (+ 1 parts)
                                             parts))
                               (prefix-lst (repeat steps-up ".."))
                               (prefix-str (string-join prefix-lst "/"))
                               (icon-path (favicon-href favicon prefix-str)))
                          ;; (warn "does this go into the root dir? above raw" steps-up href-html prefix-str icon-path)
                          icon-path)
                        #f)))))))

(define (make-anchor prefix-data prefix-html item)
  (let* ((data (path-append prefix-data item))
         (item-stat (lstat data))
         (is-dir (eq? 'directory (stat:type item-stat)))
         (is-symlink (eq? 'symlink (stat:type item-stat)))
         (href-html (path-append (basename prefix-html) (if is-dir
                                                            (string-append item "/index.html")
                                                            (string-append item ".html"))))
         (time (make-time 'time-monotonic 0 (stat:ctime item-stat)))
         (date (time-monotonic->date time)))
    `(tr
      (td (span (@ (class "icon")
                   (aria-label ,(if is-dir "Directory" "File")))
                ,(if is-dir "📁 " "🖹 ")) ;; 📁 🖹
          (a (@ (href ,href-html)) ,item))
         ;; "  |  "
      (td ,(if (and (not is-dir) (not is-symlink))
            `(a (@
                 (class "raw-link")
                 (href  ,(href-to-raw prefix-data item))) "[raw]")
            `()))
      (td (@ (class "date-row")) ,(date->string date "~Y-~m-~d ~H:~M"))
      )))

;; TODO this is mostly redundant with file-view
(define (directory-view-head prefix-html)
  (define parts (string-split prefix-html #\/))
  (define final (last parts))
  (define is-root (equal? "." (car parts)))
  (define mid (if is-root
                  (cdr (cdr parts))
                  (cdr parts)))
  (define offset 0)
  (define links
    (cdr (fold (lambda (value acc)
                 (define path (if (<= (car acc) 0)
                                  "./"
                                  (repeat-str (car acc) "../")))
                 (define file-path (string-append path "index.html"))
                 (define link `((a (@ (href ,file-path)) ,value) "/"))
                 (define links (append (cdr acc) link))
                 (cons (- (car acc) 1) links))
               (cons (- (length mid) 1) '())
               mid)))
  `(h4
    (@ (aria-description "Current directory"))
    ,(append `((a (@ (href
                         ,(if is-root
                              "index.html"
                              (string-append
                               (repeat-str (length mid) "../")
                               "../index.html"))))
                     "root")  "/")
                links)
       "    "))

(define* (directory-view-dir prefix-data prefix-html prefix-a files-list
                             #:key (favicon #f))
  (mkdir-p prefix-html)
  (map (lambda (item)
         (make-anchor-target prefix-data prefix-html prefix-a item
                             #:favicon favicon)) files-list)
  `(table
    (@ (aria-label "Directory listing"))
    ,(map (cut make-anchor prefix-data prefix-a <>) files-list)))

(define (directory-view-readme prefix-data files-list)
  (make-readme prefix-data files-list))

(define* (directory-view prefix-data prefix-html prefix-a files-list #:key (favicon #f))
  (list (directory-view-head prefix-html)
        (directory-view-dir prefix-data prefix-html prefix-a files-list
                            #:favicon favicon)
        (directory-view-readme prefix-data files-list)))