Fosi Fossil Service
Small teams. Open source. Built on Fossil.

CaaS / Lamina

Unified Management for Fossil Repositories

Public | Owner: CaaS | Default branch: trunk | Last update: 2026-10-25 00:41

Artifact e62bc17d776154de670de5d7acb3784cafb37c9a1738c2f12f7ba58615d5abd0:


;; 后台选择文档来源仓库和分支。
(defmodule lamina_docs_admin
  (export all))

(defun show (req)
  (page req (picked-id req) ""))

(defun save (req)
  (if (not (lamina_admin_pages:csrf-valid? req))
      (lamina_admin_pages:html 403 "CSRF token 无效")
      (let ((repo-id (param req "repo_id"))
            (branch (param req "branch")))
        (case (lamina_docs:save repo-id branch)
          ('ok (page req repo-id "Documentation source saved."))
          (`#(error ,message) (page req repo-id message))
          (_ (page req repo-id "Could not save the documentation source."))))))

(defun page (req repo-id message)
  (let* ((repos (lamina_docs:list-repos))
         (selected (selected-repo repos repo-id))
         (branch (current-branch req selected)))
    (lamina_admin_pages:html
     200
     (lamina_admin_pages:layout
      (lamina_admin_pages:current-username req)
      "Docs"
      (lamina_docs:encode-html (body req repos selected branch message))))))

(defun body (req repos selected branch message)
  (let* ((repo-id (if (=:= 'none selected) "" (to-str (maps:get 'id selected))))
         (names (branch-names selected branch)))
    (list
     "<div class=\"settings-page\"><h1>Docs</h1>"
     "<p class=\"lede\">Choose a repository and branch. The public Docs page reads Markdown from that check-in.</p>"
     (if (=:= "" message) "" (list "<div class=\"notice\">" (lamina_ui:escape message) "</div>"))
     "<form method=\"GET\" action=\"/admin/docs\" class=\"settings-card\">"
     "<h2>Repository</h2>"
     "<p class=\"hint\">Changing the repository reloads its branches. Save to publish the choice.</p>"
     "<label>Source repository<select name=\"repo_id\" onchange=\"this.form.submit()\">"
     "<option value=\"\">Select a repository</option>"
     (lists:map (lambda (repo) (repo-option repo repo-id)) repos)
     "</select></label></form>"
     "<form method=\"POST\" action=\"/admin/docs\">"
     (lamina_admin_pages:csrf-field req)
     "<input type=\"hidden\" name=\"repo_id\" value=\"" (lamina_ui:escape repo-id) "\">"
     "<section class=\"settings-card\"><h2>Branch</h2>"
     "<p class=\"hint\">Articles are read from this branch. The default is trunk.</p>"
     "<label>Branch"
     (if (=:= '() names)
         (list "<input name=\"branch\" value=\"" (lamina_ui:escape branch) "\">")
         (list "<select name=\"branch\">"
               (lists:map (lambda (name) (branch-option name branch)) names)
               "</select>"))
     "</label>"
     "<div class=\"settings-actions\"><button class=\"btn\" type=\"submit\">Save</button></div>"
     "</section></form>"
     (summary selected branch)
     "<section class=\"settings-card\"><h2>How files become pages</h2>"
     "<p class=\"hint\">A Markdown file in the repository root is an article under General. A folder name is a category. Nested folders are shown as Parent / Child, and the Markdown files inside are that category's articles. The first heading is the article title. README and index are listed first.</p>"
     "</section></div>")))

(defun summary (selected branch)
  (if (=:= 'none selected)
      "<section class=\"settings-card\"><h2>Bound repository</h2><p class=\"hint\">No repository is bound. Choose one above and save.</p></section>"
      (let* ((path (maps:get 'path selected))
             (url (lamina_site_mgr:repo-base-url (maps:get 'org selected "") (maps:get 'name selected)))
             (description (maps:get 'description selected ""))
             (counts (case (lamina_docs:list-files path branch)
                       (`#(ok ,files) `#(ok ,(lamina_docs:doc-counts files)))
                       (error error))))
        (list "<section class=\"settings-card\"><h2>Bound repository</h2>"
              "<p class=\"hint\">" (lamina_ui:escape (lamina_docs:repo-label selected))
              " · " (lamina_ui:escape branch) "</p>"
              (if (=:= "" description)
                  ""
                  (list "<p class=\"hint\">" (lamina_ui:escape description) "</p>"))
              (if (bound? selected)
                  ""
                  "<p class=\"hint\">Save to publish this repository on the Docs page.</p>")
              "<div class=\"meta-item\"><span>Address</span><a href=\""
              (lamina_ui:escape url) "\">" (lamina_ui:escape url) "</a></div>"
              (case counts
                (`#(ok ,stats)
                 (list "<div class=\"meta-row\">"
                       (fact "Articles" (integer_to_list (maps:get 'articles stats 0)))
                       (fact "Categories" (integer_to_list (maps:get 'categories stats 0)))
                       (fact "Repository size" (lamina_docs:format-bytes (lamina_docs:file-size path)))
                       (fact "Timeline" (integer_to_list (lamina_docs:timeline-count path)))
                       "</div>"))
                (`#(error ,message)
                 (list "<div class=\"err\">" (lamina_ui:escape message) "</div>"))
                (_ "<div class=\"err\">Could not read this repository.</div>"))
              "</section>"))))

(defun fact (label value)
  (list "<div class=\"meta-item\"><span>" label "</span>" (lamina_ui:escape value) "</div>"))

(defun bound? (selected)
  (=:= (to-str (maps:get 'id selected)) (lamina_docs:saved-repo-id)))

(defun repo-option (repo selected-id)
  (let ((id (to-str (maps:get 'id repo))))
    (list "<option value=\"" (lamina_ui:escape id) "\""
          (if (=:= id selected-id) " selected" "")
          ">" (lamina_ui:escape (lamina_docs:repo-label repo)) "</option>")))

(defun branch-option (name selected)
  (list "<option"
        (if (=:= name selected) " selected" "")
        ">" (lamina_ui:escape name) "</option>"))

(defun branch-names (selected fallback)
  (if (=:= 'none selected)
      '()
      (let ((names (lamina_docs:branches (maps:get 'path selected))))
        (if (lists:member fallback names)
            names
            (if (=:= "" fallback) names (lists:append names (list fallback)))))))

(defun selected-repo (repos repo-id)
  (let ((id (lamina_docs:to-int repo-id)))
    (case (lists:search (lambda (repo) (=:= id (maps:get 'id repo))) repos)
      (`#(value ,repo) repo)
      (_ 'none))))

(defun picked-id (req)
  (let ((from-query (param req "repo_id")))
    (if (=:= "" from-query)
        (lamina_docs:saved-repo-id)
        from-query)))

(defun current-branch (req selected)
  (let ((posted (param req "branch")))
    ;; LFE 的 and 两边都会算。selected 为 none 时不能再 maps:get。
    (if (/= "" posted)
        posted
        (if (=:= 'none selected)
            "trunk"
            (if (=:= (to-str (maps:get 'id selected)) (lamina_docs:saved-repo-id))
                (lamina_docs:saved-branch)
                "trunk")))))

(defun param (req name)
  (let ((params (maps:get 'params req #M()))
        (key (list_to_binary name)))
    (case (maps:find key params)
      (`#(ok ,value) (lamina_ui:to-str value))
      (_
       (case (lists:keyfind key 1 (cowboy_req:parse_qs req))
         (`#(,_ ,value) (lamina_ui:to-str value))
         (_ ""))))))

(defun to-str (value)
  (lamina_ui:to-str value))

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/CaaS/Lamina

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.