λ +documentation - auto docs generator

Author: lispcat 187922791+lispcat@users.noreply.github.com

Automatically generates documentation using el2markdown. Converts elisp files into markdown docs.

% make package accessible

(add-to-list 'load-path
             (file-name-concat +emacs-submodules-dir
                               "el2markdown-fork"))

(require 'el2markdown-fork)
(require 'dash)

(defun +remove-prefix-or-err (prefix str)
  (if (string-prefix-p prefix str)
      (string-remove-prefix prefix str)
    (error "%s is not a prefix of %s" prefix str)
    nil))

(defun +zip-lists (list1 list2)
  (let ((len1 (length list1))
        (len2 (length list2)))
    (unless (eq len1 len2)
      (error "Lists to zip differ in length (%s %s): %s %s"
             len1 len2 list1 list2))
    (-zip-pair list1 list2)))

(defun +assert-f-dir-p (path)
  (if (f-dir-p path)
      path
    (error (format "Path not exist: %s" path))
    nil))

(defun +create-docs (root-directory infiles-directory outfiles-directory
                                      &optional extra-infiles)
  (interactive
   (let* ((root-directory
           (read-file-name "Root dir: " default-directory
                           `,(buffer-file-name) 'confirm))
          (infiles-directory
           (read-file-name "Infiles dir: " root-directory nil 'confirm))
          (outfiles-directory
           (read-file-name "Outfiles dir: " root-directory nil 'confirm)))
     ;; ret
     (list root-directory infiles-directory outfiles-directory)))

  ;; expand
  (setq root-directory (expand-file-name root-directory))
  (setq infiles-directory (expand-file-name infiles-directory))
  (setq outfiles-directory (expand-file-name outfiles-directory))
  (setq extra-infiles (mapcar #'expand-file-name extra-infiles))

  ;; fs checks
  (+remove-prefix-or-err root-directory infiles-directory)
  (+remove-prefix-or-err root-directory outfiles-directory)

  (+assert-f-dir-p root-directory)
  (+assert-f-dir-p infiles-directory)

  ;; proceed?
  (when (y-or-n-p
         (format
          "Prefix: %s\nInfiles: %s\nOutfiles: %s\n> Proceed? "
          root-directory infiles-directory outfiles-directory))

    ;; body
    (let* (;; collect all infiles
           (infiles-all
            (append
             (reverse
              (-filter #'file-regular-p
                       (directory-files-recursively infiles-directory emacs-lisp-file-regexp)))
             extra-infiles))

           ;; derive all outfiles for each infile
           ;; - given infile:
           ;;   - remove prefix infiles-directory, but keep the dirname
           ;;   - replace extension .el -> .md
           ;;   - prepend with prefix outfiles-directory
           (outfiles-all
            (mapcar (lambda (infile)
                      (-as-> infile x
                             (+remove-prefix-or-err (file-name-directory infiles-directory) x)
                             (file-name-sans-extension x)
                             (file-name-with-extension x ".md")
                             (expand-file-name x outfiles-directory)))
                    infiles-all))

           ;; create alist from infiles-all and outfiles-all
           (alist
            (+zip-lists infiles-all outfiles-all))

           (alist-uniq-parts
            (mapcar
             (lambda (pair)
               (let* ((infile (car pair))
                      (outfile (cdr pair))
                      (infile-uniq (+remove-prefix-or-err
                                    (file-name-directory infiles-directory)
                                    infile))
                      (outfile-uniq (+remove-prefix-or-err
                                     outfiles-directory
                                     outfile))
                      (infile-uniq-no-slash (replace-regexp-in-string
                                             "^/+" "" infile-uniq))
                      (outfile-uniq-no-slash (replace-regexp-in-string
                                              "^/+" "" outfile-uniq)))
                 (cons infile-uniq-no-slash
                       outfile-uniq-no-slash)))
             alist)))

                                        ; checks ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      ;; check: ensure unique part (minus extension) is matching:
      (dolist (pair alist-uniq-parts)
        (unless (equal (file-name-sans-extension (car pair))
                       (file-name-sans-extension (cdr pair)))
          (error "Found pair with non-equal uniq part: %s"
                 pair)))
      ;; check: ensure all infiles exist
      (dolist (infile infiles-all)
        (unless (file-exists-p infile)
          (error "Found an infile that doesn't exist: %s"
                 infile)))
      ;; check: ensure all targets are under the outfiles dir:
      (dolist (outfile outfiles-all)
        (unless (+remove-prefix-or-err outfiles-directory outfile)
          (error "outfile not a prefix of outfiles-directory: %s"
                 outfile)))

      ;; delete outfiles-directory
      (when (f-dir-p outfiles-directory)
        (when (y-or-n-p (format "Trash outfiles-directory (%s)? " outfiles-directory))
          (move-file-to-trash outfiles-directory)))

      ;; action
      (with-output-to-temp-buffer "*File Copy Preview*"
        (princ "Files to be copied:\n")
        (princ "==================\n\n")
        (dolist (pair alist)
          (princ (format "%s\n%s\n"
                         (car pair)
                         (cdr pair)))))
      ;; make all parent dirs?
      (when (y-or-n-p (format "Create all parent dirs?"))
        (dolist (path outfiles-all)
          (message "DEBUG: path: %s" path)
          (let ((dir (file-name-directory path)))
            (message "DEBUG: dir: %s" dir)
            (unless (f-dir-p dir)
              (make-directory dir t)))))

      ;; proceed?
      (when (y-or-n-p "Proceed?")
        (mapcar (lambda (pair)
                  (let ((infile (car pair))
                        (outfile (cdr pair)))
                    (message "%s" pair)
                    (with-temp-buffer
                      (insert-file-contents infile)
                      (el2markdown-write-file outfile t))))
                alist)))))

(defun +create-docs-default ()
  (interactive)
  (+create-docs "~/.emacs.d/"
                  "~/.emacs.d/src"
                  "~/.emacs.d/docs/docs_src/build"
                  '("~/.emacs.d/init.el" "~/.emacs.d/early-init.el")))

% end

(provide '+documentation)

% +documentation.el ends here


Last updated: August 22, 2025