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 69cf347c59d853b163bdd64f7f704c8b6997e5c68171c658251e2db7276c85ee:


;; 公开文档页。/docs 打开第一篇文章,其余路径按仓库里的文件解析。
(defmodule lamina_docs_pages
  (export all))

(defun show (req)
  (if (=:= 'true (lamina_setup:installed?))
      (render req (request-path req))
      `#(redirect "/setup")))

(defun render (req path)
  (let ((session (lamina_auth:verify-request req))
        (fragment (fragment? req)))
    (case (lamina_docs:serve path)
      (`#(file ,mime ,bytes)
       `#(status 200
                 #M(#"content-type" ,(list_to_binary mime)
                    #"x-content-type-options" #"nosniff"
                    #"content-security-policy" #"default-src 'none'")
                 ,bytes))
      (`#(article ,tree ,article)
       (if fragment
           (html 200 (article-fragment article))
           (html 200 (article-page session tree article))))
      ('unconfigured
       (html 200 (message-page session "Docs" "Documentation has not been configured yet.")))
      (`#(empty ,_tree)
       (html 200 (message-page session "Docs" "This repository has no Markdown files on the selected branch.")))
      (`#(not-found ,tree)
       (if fragment
           (html 404 (not-found-fragment))
           (html 404 (not-found-page session tree))))
      (`#(error ,message)
       (html 200 (message-page session "Docs" message)))
      (_
       (html 200 (message-page session "Docs" "The documentation could not be read."))))))

(defun fragment? (req)
  (=:= "fragment" (string:lowercase (lamina_ui:to-str (cowboy_req:header #"x-lamina-docs" req #"")))))

(defun html (code body)
  (let ((bin (lamina_docs:encode-html body)))
    `#(status ,code
              #M(#"content-type" #"text/html; charset=utf-8"
                 #"vary" #"x-lamina-docs")
              ,bin)))

(defun request-path (req)
  (let ((path (lamina_docs:decode-url-path (decode-path (cowboy_req:path req)))))
    (cond
      ((lists:prefix "/docs/" path) (lists:nthtail 6 path))
      ((=:= path "/docs") "")
      ('true ""))))

(defun decode-path (path)
  (case (unicode:characters_to_list path)
    (`#(error ,_ ,_) (lamina_ui:to-str path))
    (`#(incomplete ,_ ,_) (lamina_ui:to-str path))
    (chars chars)))

(defun article-page (session tree article)
  (let ((site (lamina_site_mgr:all)))
    (page site session (maps:get 'title article "")
          (list (sidebar (maps:get 'categories tree '()) (maps:get 'path article))
                (article-html article)))))

(defun article-fragment (article)
  (let ((site-name (maps:get 'site_name (lamina_site_mgr:all) "lamina"))
        (title (maps:get 'title article "")))
    (list "<title>" (lamina_ui:escape (lists:flatten (list title " - Docs - " site-name))) "</title>"
          (article-html article))))

(defun article-html (article)
  (list "<article class=\"docs-body\"><h1>" (lamina_ui:escape (maps:get 'title article "")) "</h1>"
        (lamina_docs:render-markdown
         (maps:get 'body article "")
         (tuple (maps:get 'prefix article "/docs") (maps:get 'dir article "")))
        "</article>"))

(defun not-found-page (session tree)
  (let ((site (lamina_site_mgr:all)))
    (page site session "Not found"
          (list (sidebar (maps:get 'categories tree '()) "")
                (not-found-html)))))

(defun not-found-fragment ()
  (let ((site-name (maps:get 'site_name (lamina_site_mgr:all) "lamina")))
    (list "<title>" (lamina_ui:escape (lists:flatten (list "Not found - Docs - " site-name))) "</title>"
          (not-found-html))))

(defun not-found-html ()
  "<article class=\"docs-body\"><h1>Page not found</h1><p>That document is not in the selected repository.</p></article>")

(defun message-page (session title message)
  (page (lamina_site_mgr:all) session title
        (list "<article class=\"docs-body\"><h1>" (lamina_ui:escape title) "</h1><p>"
              (lamina_ui:escape message) "</p></article>")))

(defun page (site session title body)
  (let ((site-name (maps:get 'site_name site "lamina")))
    (lists:flatten
     (list
      (lamina_ui:public-head (lists:flatten (list title " - Docs - " site-name)) "shell docs-shell")
      (lamina_ui:public-nav site-name session 'docs)
      (lamina_ui:public-tagline (maps:get 'site_tagline site ""))
      "<main class=\"docs\">" body "</main>"
      (nav-script)
      (lamina_ui:public-footer site)
      "</div></body></html>"))))

(defun sidebar (categories current-path)
  (if (=:= '() categories)
      ""
      (list "<aside class=\"docs-side\">"
            (lists:map
             (lambda (group)
               (list "<h2>" (lamina_ui:escape (maps:get 'label group)) "</h2><ul>"
                     (lists:map (lambda (article) (article-link article current-path))
                                (maps:get 'articles group '()))
                     "</ul>"))
             categories)
            "</aside>")))

(defun article-link (article current-path)
  (list "<li><a"
        (if (=:= current-path (maps:get 'path article)) " class=\"active\"" "")
        " href=\"" (lamina_ui:escape (maps:get 'url article)) "\">"
        (lamina_ui:escape (maps:get 'title article)) "</a></li>"))

(defun nav-script ()
  "<script>(function(){
var main=document.querySelector('main.docs');
if(!main) return;
function asset(path){
  var clean=path.split('?')[0].split('#')[0];
  var slash=clean.lastIndexOf('/');
  var name=slash<0?clean:clean.slice(slash+1);
  var dot=name.lastIndexOf('.');
  if(dot<0) return false;
  var ext=name.slice(dot+1).toLowerCase();
  return ext==='png'||ext==='jpg'||ext==='jpeg'||ext==='gif'||ext==='webp'||ext==='svg'||ext==='pdf'||ext==='css'||ext==='txt';
}
function docsPath(path){return path==='/docs'||path.indexOf('/docs/')===0||path.indexOf('/library/')===0;}
function setActive(path){
  var links=main.querySelectorAll('.docs-side a');
  for(var i=0;i<links.length;i++){
    if(links[i].pathname===path) links[i].classList.add('active');
    else links[i].classList.remove('active');
  }
}
function swap(html,url,push,restoreY){
  var doc=new DOMParser().parseFromString(html,'text/html');
  var next=doc.querySelector('article.docs-body');
  var current=main.querySelector('article.docs-body');
  if(!next||!current){location.assign(url);return;}
  var dest=new URL(url,location.href);
  if(push) history.replaceState({y:window.scrollY},'',location.href);
  current.replaceWith(document.importNode(next,true));
  var title=doc.querySelector('title');
  if(title&&title.textContent) document.title=title.textContent;
  setActive(dest.pathname);
  if(push){
    history.pushState({y:0},'',dest.pathname+dest.search+dest.hash);
    if(dest.hash===''){
      var placed=main.querySelector('article.docs-body');
      var y=placed.getBoundingClientRect().top+window.scrollY-8;
      if(y<window.scrollY) window.scrollTo(0,Math.max(0,y));
    }
    history.replaceState({y:window.scrollY},'',location.href);
  }else if(typeof restoreY==='number'){
    window.scrollTo(0,restoreY);
  }
}
var pending=0;
function load(url,push,restoreY){
  var seq=++pending;
  fetch(url,{headers:{'x-lamina-docs':'fragment'},credentials:'same-origin'})
    .then(function(res){
      var type=res.headers.get('content-type')||'';
      if(type.indexOf('text/html')<0){location.assign(url);return null;}
      return res.text();
    })
    .then(function(html){
      if(html==null||seq!==pending) return;
      swap(html,url,push,restoreY);
    })
    .catch(function(){if(seq===pending) location.assign(url);});
}
if(!history.state) history.replaceState({y:window.scrollY},'',location.href);
main.addEventListener('click',function(ev){
  if(ev.defaultPrevented||ev.button!==0||ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.altKey) return;
  var node=ev.target;
  while(node&&node!==main&&(!node.tagName||node.tagName!=='A')) node=node.parentNode;
  if(!node||node===main||node.tagName!=='A') return;
  if(node.target&&node.target!=='_self') return;
  var dest;
  try{dest=new URL(node.href,location.href);}catch(e){return;}
  if(dest.origin!==location.origin||!docsPath(dest.pathname)||asset(dest.pathname)) return;
  if(dest.pathname===location.pathname&&dest.search===location.search) return;
  ev.preventDefault();
  load(dest.pathname+dest.search+dest.hash,true);
});
window.addEventListener('popstate',function(ev){
  if(!docsPath(location.pathname)) return;
  var y=ev.state&&typeof ev.state.y==='number'?ev.state.y:0;
  load(location.pathname+location.search+location.hash,false,y);
});
})();</script>")

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.