(defmodule nemoptera_fossil_skin
(export all))
;; Classic repository skin: serif title, pipe menu, status line,
;; and a two-pane file browser on the Files and file pages.
(defun apply (repo-path)
(let* ((name (lists:flatten
(io_lib:format "skin-~w.conf"
(list (erlang:unique_integer '(positive))))))
(path (filename:join (skin-dir) name))
(body (lists:flatten
(list "# nemoptera repository skin\n"
(card "css" (css))
(card "header" (header))
(card "footer" (footer))
(card "details" (details))
(card "js" (js))))))
(filelib:ensure_dir path)
(case (file:write_file path body)
('ok
(let ((result (nemoptera_fossil:run
(list "configuration" "import" path "-R" repo-path))))
(catch (file:delete path))
result))
(error error))))
(defun skin-dir ()
(filename:join (filename:dirname (nemoptera_config:db-path)) "skin"))
(defun card (name value)
(let ((line (lists:flatten
(io_lib:format "~p '~s' value '~s'"
(list (erlang:system_time 'second)
name
(fossil-quote value))))))
(lists:flatten
(list "config /config "
(integer_to_list (byte_size (list_to_binary line)))
"\n" line "\n"))))
(defun fossil-quote (value)
(lists:flatten (string:replace (to-str value) "'" "''" 'all)))
(defun to-str (value)
(cond
((is_binary value) (binary_to_list value))
((is_list value) value)
('true (lists:flatten (io_lib:format "~p" (list value))))))
(defun join (lines)
(string:join lines "\n"))
(defun header ()
(join
(list
"<div class=\"banner\">"
" <h1 class=\"title\">$<project_name></h1>"
" <p class=\"tagline\">$<project_description></p>"
" <p class=\"navlinks\">"
" <a href=\"$home/home\">Home</a> |"
" <a href=\"$home/timeline\">Timeline</a> |"
" <a href=\"$home/dir?ci=tip&type=tree\">Files</a> |"
" <a href=\"$home/brlist\">Branches</a> |"
" <a href=\"$home/taglist\">Tags</a> |"
" <a href=\"$home/ticket\">Tickets</a> |"
" <a href=\"$home/wiki\">Wiki</a> |"
" <a href=\"$home/forum\">Forum</a> |"
" <a href=\"$home/search\">Search</a> |"
" <th1>if {[info exists login]} {"
" html \"<a href='$home/logout'>Logout</a>\""
" } else {"
" html \"<a href='$home/login'>Login</a>\""
" }</th1>"
" </p>"
" <p class=\"repobar\"><th1>"
"set ncheckin 0"
"set last none"
"query {SELECT count(*) AS n FROM event WHERE type='ci'} {set ncheckin $n}"
"query {SELECT cast(strftime('%m',mtime) as int) AS mo, cast(strftime('%d',mtime) as int) AS d, strftime('%Y',mtime) AS y, strftime('%H:%M',mtime) AS hm FROM event ORDER BY mtime DESC LIMIT 1} {"
" set names [list January February March April May June July August September October November December]"
" set mname [lindex $names [expr {$mo-1}]]"
" set last \"$mname $d, $y $hm UTC\""
"}"
"html \"Repository: <a href='$home/dir?ci=tip&type=tree'>\""
"html [htmlize $project_name]"
"html \"</a> | Project: <a href='$home/home'>\""
"html [htmlize $project_name]"
"html \"</a> | Check-ins: \""
"html [htmlize $ncheckin]"
"html \" | Last change: \""
"html [htmlize $last]"
"</th1></p>"
"</div>")))
(defun footer ()
(join
(list
"<div class=\"footer\">"
"This page was generated in about"
"<th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by"
"Fossil $manifest_version $manifest_date"
"</div>"
"<script nonce=\"$nonce\">"
"<th1>styleScript</th1>"
"</script>")))
(defun details ()
(join
(list
"timeline-arrowheads: 1"
"timeline-circle-nodes: 1"
"timeline-color-graph-lines: 0"
"white-foreground: 0")))
(defun css ()
(join
(list
"body{margin:0;background:#fff;color:#000;font:16px/1.45 Georgia,\"Times New Roman\",Times,serif}"
"a:link,a:visited{color:#00e;text-decoration:underline}"
"a:hover{color:#00e}"
"div.banner{text-align:center;margin:22px 16px 0}"
"div.banner h1.title{margin:0;font:700 46px/1.05 Georgia,\"Times New Roman\",Times,serif;color:#000}"
"div.banner p.tagline{margin:8px auto 2px;font-size:16px;color:#000}"
"div.banner p.navlinks{margin:10px 0 12px}"
"div.banner p.navlinks a{display:inline;margin:0 2px;padding:0;background:transparent;border:0;font-weight:400;color:#00e;text-decoration:underline}"
"div.banner p.repobar{border-top:1px solid #000;border-bottom:1px solid #000;margin:0;padding:7px 12px;font-size:15px}"
"h1.page-title{max-width:1080px;margin:16px auto 0;padding:0 16px;text-align:left;font:700 22px Georgia,\"Times New Roman\",Times,serif}"
"body.rpage-file h1.page-title,body.rpage-dir h1.page-title{display:none}"
"div.content{max-width:1080px;margin:14px auto 28px;padding:0 16px}"
"body.rpage-file div.content,body.rpage-dir div.content{max-width:none;margin:0;padding:0 0 28px}"
"div.footer{text-align:center;color:#333;font-size:13px;margin:18px 12px 28px}"
"div.submenu{text-align:center;margin:8px 0}"
"div.submenu a.label{display:inline;background:transparent;border:0;border-radius:0;box-shadow:none;padding:0;margin:0 0 0 8px;color:#00e;font-weight:400;text-decoration:underline}"
"div.filesplit{display:flex;align-items:stretch;min-height:70vh}"
"aside.filepane-tree{flex:0 0 300px;width:300px;box-sizing:border-box;border-right:1px solid #222;padding:12px 16px 28px;overflow:auto}"
"aside.filepane-tree h2.filestitle{margin:6px 0 14px;font:700 28px Georgia,\"Times New Roman\",Times,serif}"
"div.filepane-view{flex:1;min-width:0;padding:14px 28px 32px}"
"div.filebar{display:flex;justify-content:space-between;align-items:baseline;gap:18px;border-bottom:1px solid #000;margin:0 0 16px;padding:2px 0 8px}"
"h2.filepath{margin:0;padding:0;border:0;font:18px/1.4 Georgia,\"Times New Roman\",Times,serif;font-weight:400}"
"div.filebar div.submenu{margin:0;text-align:right;white-space:nowrap}"
"div.filebar div.submenu a.label::before{content:\"[\"}"
"div.filebar div.submenu a.label::after{content:\"]\"}"
"p.filehint{margin:18px 0;color:#333;font-size:18px}"
"body.rpage-dir div.filepane-view>h2,body.rpage-dir div.filepane-view>p:not(.filehint){display:none}"
"div.filetree{font-size:15px;line-height:1.7}"
"div.filetree>ul{display:block}"
"div.filetree ul{display:block;list-style:none;margin:0;padding:0}"
"div.filetree ul.collapsed{display:none}"
"div.filetree ul ul{margin:0 0 0 18px}"
"div.filetree li{display:block;position:static;clear:both;margin:0;padding:0}"
"div.filetree li li:before,div.filetree li>ul:before{content:none;display:none;border:0}"
"div.filetreeline{display:block;width:auto;white-space:nowrap;padding:1px 0;line-height:1.7}"
"div.filetree div.filetreeage,div.filetree div.filetreesize{display:none}"
"div.filetree a{display:inline;position:static;min-height:0;padding:0 2px;background-image:none;background-repeat:no-repeat;color:#00e;text-decoration:underline;z-index:auto;vertical-align:middle;line-height:1.7}"
"div.filetree li.dir>div.filetreeline>a,div.filetree li.file>div.filetreeline>a{display:inline;background-image:none;background-repeat:no-repeat;background-position:0 0;padding:0 2px;min-height:0}"
"div.filetree li.dir>div.filetreeline>a::before{content:\"▶\";display:inline-block;box-sizing:border-box;width:36px;height:16px;margin-right:4px;padding:0;border:0;color:#222;font-size:10px;line-height:16px;text-align:left;text-decoration:none;vertical-align:middle;background-image:url(\"data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiIv///wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo+jUs6b5Z/K4siDu5RPUFADs=\");background-repeat:no-repeat;background-position:18px center}"
"div.filetree li.dir:has(>ul:not(.collapsed))>div.filetreeline>a::before{content:\"▼\"}"
"div.filetree li.dir>div.filetreeline>a::after{content:\"/\"}"
"div.filetree li.file>div.filetreeline>a::before{content:\"\";display:inline-block;box-sizing:border-box;width:36px;height:16px;margin-right:4px;padding:0;border:0;vertical-align:middle;background:transparent url(\"data:image/gif;base64,R0lGODlhEAAQAJEAAP///yEhIf///wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14GqFXImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw==\") no-repeat 18px center}"
"div.filetree a.current{background-color:#e6e0f5;background-image:none}"
"div.filetreeline:has(>a.current){background:#e6e0f5}"
"div.filetreeline:hover{background:transparent}"
"div.filetreeline:has(>a.current):hover{background:#e6e0f5}"
"div.markdown h1,div.markdown h2,div.content h1,div.content h2{font-family:Georgia,\"Times New Roman\",Times,serif;color:#000}"
"div.markdown h1{font-size:32px;font-weight:700;border-bottom:1px solid #000;padding-bottom:4px;margin:18px 0 12px}"
"div.markdown h2{font-size:24px;font-weight:700;border-bottom:1px solid #000;padding-bottom:3px;margin:20px 0 10px}"
"div.markdown p,div.markdown li{font-size:16px}"
"div.content pre,div.markdown pre{background:#f2f2f2;border:0;border-radius:0;padding:12px 14px;overflow:auto;font:14px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}"
"table.timelineTable,div.content table{border-collapse:collapse}"
"input,select,textarea{font:inherit}"
"@media(max-width:800px){div.filesplit{display:block}aside.filepane-tree{width:auto;flex:none;border-right:0;border-bottom:1px solid #222}div.filebar{display:block}div.filebar div.submenu{text-align:left;white-space:normal;margin-top:8px}}"
)))
(defun js ()
(join
(list
"(function () {"
" var home = \"$home\";"
" if (window.fossil && fossil.rootPath) home = String(fossil.rootPath);"
" if (home.length && home.charAt(home.length - 1) === \"/\") home = home.substring(0, home.length - 1);"
" var body = document.body;"
" if (!body) return;"
" var isFile = body.classList.contains(\"rpage-file\");"
" var isDir = body.classList.contains(\"rpage-dir\");"
" if (!isFile && !isDir) return;"
" var content = document.querySelector(\"div.content\");"
" if (!content) return;"
" var split = document.createElement(\"div\");"
" split.className = \"filesplit\";"
" var treePane = document.createElement(\"aside\");"
" treePane.className = \"filepane-tree\";"
" var heading = document.createElement(\"h2\");"
" heading.className = \"filestitle\";"
" heading.textContent = \"Files\";"
" treePane.appendChild(heading);"
" var view = document.createElement(\"div\");"
" view.className = \"filepane-view\";"
" var tree = content.querySelector(\"div.filetree\");"
" if (tree) treePane.appendChild(tree);"
" var submenu = document.querySelector(\"div.submenu\");"
" if (isFile && submenu) view.appendChild(submenu);"
" while (content.firstChild) view.appendChild(content.firstChild);"
" if (isDir) {"
" var hint = document.createElement(\"p\");"
" hint.className = \"filehint\";"
" hint.textContent = \"Select a file.\";"
" view.insertBefore(hint, view.firstChild);"
" }"
" split.appendChild(treePane);"
" split.appendChild(view);"
" content.appendChild(split);"
" if (isFile) {"
" rewritePath(view);"
" loadTree(treePane);"
" }"
" function currentName() {"
" var params = new URLSearchParams(window.location.search);"
" var name = params.get(\"name\") || \"\";"
" if (name) return name;"
" var path = window.location.pathname;"
" var marker = \"/file/\";"
" var at = path.indexOf(marker);"
" if (at < 0) return \"\";"
" return decodeURIComponent(path.substring(at + marker.length));"
" }"
" function currentCi() {"
" var params = new URLSearchParams(window.location.search);"
" return params.get(\"ci\") || \"tip\";"
" }"
" function addLink(parent, href, text) {"
" var anchor = document.createElement(\"a\");"
" anchor.href = href;"
" anchor.textContent = text;"
" parent.appendChild(anchor);"
" }"
" function rewritePath(pane) {"
" var h2 = pane.querySelector(\"h2\");"
" var name = currentName();"
" if (!h2 || !name) return;"
" var ci = currentCi();"
" var project = (window.fossil && fossil.config && fossil.config.projectName) || \"Repository\";"
" h2.className = \"filepath\";"
" h2.textContent = \"\";"
" addLink(h2, home + \"/dir?ci=\" + encodeURIComponent(ci) + \"&type=tree\", project);"
" var parts = name.split(\"/\");"
" var acc = \"\";"
" for (var i = 0; i < parts.length; i++) {"
" h2.appendChild(document.createTextNode(\" / \"));"
" acc = acc ? (acc + \"/\" + parts[i]) : parts[i];"
" if (i + 1 === parts.length) addLink(h2, home + \"/file?name=\" + encodeURIComponent(name) + \"&ci=\" + encodeURIComponent(ci), parts[i]);"
" else addLink(h2, home + \"/dir?ci=\" + encodeURIComponent(ci) + \"&type=tree&name=\" + encodeURIComponent(acc), parts[i]);"
" }"
" var bar = document.createElement(\"div\");"
" bar.className = \"filebar\";"
" h2.parentNode.insertBefore(bar, h2);"
" bar.appendChild(h2);"
" var menu = pane.querySelector(\"div.submenu\");"
" if (menu) bar.appendChild(menu);"
" }"
" function markCurrent(pane) {"
" var name = currentName();"
" if (!name) return;"
" var links = pane.querySelectorAll(\"a\");"
" for (var i = 0; i < links.length; i++) {"
" var href = links[i].getAttribute(\"href\") || \"\";"
" if (href.indexOf(\"/file\") < 0) continue;"
" var query = href.split(\"?\")[1] || \"\";"
" if ((new URLSearchParams(query)).get(\"name\") !== name) continue;"
" links[i].classList.add(\"current\");"
" var node = links[i];"
" while (node) {"
" if (node.classList && node.classList.contains(\"collapsed\")) node.classList.remove(\"collapsed\");"
" node = node.parentNode;"
" }"
" }"
" }"
" function enableToggle(tree) {"
" var outer = tree.querySelector(\"ul\");"
" if (!outer || outer.getAttribute(\"data-toggle\")) return;"
" outer.setAttribute(\"data-toggle\", \"1\");"
" outer.addEventListener(\"click\", function (event) {"
" var target = event.target;"
" if (!target || target.nodeName !== \"A\") return;"
" var line = target.parentNode;"
" var list = line ? line.nextSibling : null;"
" while (list && list.nodeType !== 1) list = list.nextSibling;"
" if (!list || list.nodeName !== \"UL\") return;"
" event.preventDefault();"
" if (list.classList.contains(\"collapsed\")) list.classList.remove(\"collapsed\");"
" else list.classList.add(\"collapsed\");"
" });"
" }"
" function loadTree(pane) {"
" var url = home + \"/dir?ci=\" + encodeURIComponent(currentCi()) + \"&type=tree\";"
" var xhr = new XMLHttpRequest();"
" xhr.open(\"GET\", url);"
" xhr.onload = function () {"
" if (xhr.status !== 200) return;"
" var doc = new DOMParser().parseFromString(xhr.responseText, \"text/html\");"
" var fetched = doc.querySelector(\"div.filetree\");"
" if (!fetched) return;"
" var node = document.importNode(fetched, true);"
" pane.appendChild(node);"
" markCurrent(pane);"
" enableToggle(node);"
" };"
" xhr.send();"
" }"
"})();"
)))