1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-07-02 16:58:30 +00:00

Initial version of documentation generator

This commit is contained in:
Unrud 2020-02-27 02:34:49 +01:00
parent 3f032e00b0
commit b2c3f38766
12 changed files with 890 additions and 0 deletions

View file

@ -0,0 +1,17 @@
name: Generate documentation
on:
push:
paths:
- 'documentation-tools/**'
- DOCUMENTATION.md
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: gh-pages
- name: Run generator
run: documentation-tools/generate.py

107
beta/assets/default.css Normal file
View file

@ -0,0 +1,107 @@
/* https://www.w3.org/TR/CSS22/ */
html, address,
blockquote,
body, dd, div,
dl, dt, fieldset, form,
frame, frameset,
h1, h2, h3, h4,
h5, h6, noframes,
ol, p, ul, center,
dir, hr, menu, pre { display: block; unicode-bidi: embed }
li { display: list-item }
head { display: none }
table { display: table }
tr { display: table-row }
thead { display: table-header-group }
tbody { display: table-row-group }
tfoot { display: table-footer-group }
col { display: table-column }
colgroup { display: table-column-group }
td, th { display: table-cell }
caption { display: table-caption }
th { font-weight: bolder; text-align: center }
caption { text-align: center }
body { margin: 8px }
h1 { font-size: 2em; margin: .67em 0 }
h2 { font-size: 1.5em; margin: .75em 0 }
h3 { font-size: 1.17em; margin: .83em 0 }
h4, p,
blockquote, ul,
fieldset, form,
ol, dl, dir,
menu { margin: 1.12em 0 }
h5 { font-size: .83em; margin: 1.5em 0 }
h6 { font-size: .75em; margin: 1.67em 0 }
h1, h2, h3, h4,
h5, h6, b,
strong { font-weight: bolder }
blockquote { margin-left: 40px; margin-right: 40px }
i, cite, em,
var, address { font-style: italic }
pre, tt, code,
kbd, samp { font-family: monospace }
pre { white-space: pre }
button, textarea,
input, select { display: inline-block }
big { font-size: 1.17em }
small, sub, sup { font-size: .83em }
sub { vertical-align: sub }
sup { vertical-align: super }
table { border-spacing: 2px; }
thead, tbody,
tfoot { vertical-align: middle }
td, th, tr { vertical-align: inherit }
s, strike, del { text-decoration: line-through }
hr { border: 1px inset }
ol, ul, dir,
menu, dd { margin-left: 40px }
ol { list-style-type: decimal }
ol ul, ul ol,
ul ul, ol ol { margin-top: 0; margin-bottom: 0 }
u, ins { text-decoration: underline }
br:before { content: "\A"; white-space: pre-line }
center { text-align: center }
:link, :visited { text-decoration: underline }
:focus { outline: thin dotted invert }
/* Begin bidirectionality settings (do not change) */
BDO[DIR="ltr"] { direction: ltr; unicode-bidi: bidi-override }
BDO[DIR="rtl"] { direction: rtl; unicode-bidi: bidi-override }
*[DIR="ltr"] { direction: ltr; unicode-bidi: embed }
*[DIR="rtl"] { direction: rtl; unicode-bidi: embed }
@media print {
h1 { page-break-before: always }
h1, h2, h3,
h4, h5, h6 { page-break-after: avoid }
ul, ol, dl { page-break-before: avoid }
}
/* END: https://www.w3.org/TR/CSS22/ */
@media not screen {
header select.documentBranch, nav, .headerlink {
display: none;
}
p.heading {
font-size: .67em;
margin: 2em 0;
font-weight: bolder;
}
nav {
/* Override changes made by JS to HTMLElement */
max-height: none !important;
}
}
@media print {
h1 {
page-break-before: auto;
}
p.heading {
page-break-after: avoid;
}
}

View file

@ -0,0 +1,20 @@
window.addEventListener("load", function() {
let select = document.querySelector("header select.documentBranch");
while (select.firstChild) {
select.removeChild(select.firstChild);
}
for (let branch of documentBranches) {
let option = document.createElement("option");
option.textContent = branch;
if (branch === documentBranch) {
option.setAttribute("selected", "");
}
select.appendChild(option);
}
select.addEventListener("change", function() {
if (select.value !== documentBranch) {
location.assign(select.value + ".html");
select.value = documentBranch;
}
});
});

BIN
beta/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

10
beta/assets/logo.svg Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="200" height="300" xmlns="http://www.w3.org/2000/svg">
<path fill="#a40000" d="M 186,188 C 184,98 34,105 47,192 C 59,279 130,296 130,296 C 130,296 189,277 186,188 z" />
<path fill="#ffffff" d="M 73,238 C 119,242 140,241 177,222 C 172,270 131,288 131,288 C 131,288 88,276 74,238 z" />
<g fill="none" stroke="#4e9a06" stroke-width="15">
<path d="M 103,137 C 77,69 13,62 13,62" />
<path d="M 105,136 C 105,86 37,20 37,20" />
<path d="M 105,135 C 112,73 83,17 83,17" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 564 B

67
beta/assets/navigation.js Normal file
View file

@ -0,0 +1,67 @@
window.addEventListener("load", function() {
function findActiveSection(sections) {
let result = sections[0];
for (let [section, link] of sections) {
if (section.getBoundingClientRect().y > 10) {
break;
}
result = [section, link];
}
return result;
}
let nav = document.querySelector("nav");
let sections = new Array();
sections.push([document.querySelector("main"), null]);
for (let section of document.querySelectorAll("section")) {
let id = section.getAttribute("id");
let link = nav.querySelector("a[href=\\#" + id.replace(/\//g, "\\/") + "]");
if (link !== null) {
link = link.parentElement;
link.classList.remove("active")
sections.push([section, link]);
}
}
let oldLink = null;
function updateLink() {
let [section, link] = findActiveSection(sections);
while (oldLink) {
if (oldLink.tagName === "LI") {
oldLink.classList.remove("active");
if (!oldLink.classList.contains("level4")) {
break;
}
}
oldLink = oldLink.parentElement;
}
oldLink = link;
while (link) {
if (link.tagName === "LI") {
link.classList.add("active");
if (!link.classList.contains("level4")) {
break;
}
}
link = link.parentElement;
}
if (link === null) {
nav.scrollTop = 0;
} else {
let topLink = link.getBoundingClientRect().y;
let topNav = nav.getBoundingClientRect().y;
let y = nav.scrollTop + topLink - topNav - 10;
nav.scrollTo(0, y);
}
}
function resizeNav() {
let height = window.innerHeight - nav.getBoundingClientRect().y;
nav.style.maxHeight = height > 0 ? height + "px" : "none";
}
function updateNav() {
resizeNav();
updateLink();
}
document.addEventListener("scroll", updateNav);
window.addEventListener("resize", updateNav);
updateNav();
});

View file

@ -0,0 +1,27 @@
header select.documentBranch {
display: none;
}
header span.documentBranch {
display: inline;
}
nav {
position: relative;
height: auto;
/* Override changes made by JS to HTMLElement */
max-height: none !important;
}
nav .level2.active > a, nav .level3.active > a {
background: none;
border-left-color: transparent;
}
nav .level4 {
display: initial;
}
nav .level4 > a::after {
display: none !important;
}

349
beta/assets/screen.css Normal file
View file

@ -0,0 +1,349 @@
html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
line-height: 1.4;
background-color: #E4E9F6;
color: #424247;
}
body {
display: flex;
flex-direction: column;
align-items: center;
}
body > * {
width: 100%;
}
a {
color: #a40000;
}
a:link {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
header {
display: flex;
flex-direction: column;
background-color: #050a02;
color: #efdddd;
align-items: center;
}
header > * {
width: 100%;
}
header .logoContainer {
max-width: 1400px;
padding: 2em 10px;
padding-left: calc(210px + 2em);
box-sizing: border-box;
background: url(logo.svg);
background-repeat: no-repeat;
background-position: 10px 50%;
min-height: calc(300px + 4em);
display: flex;
flex-direction: column;
justify-content: center;
}
header p {
margin: 0;
font-size: 1.5em;
}
header h1 {
font-weight: lighter;
font-size: 3em;
margin-top: 0;
margin-bottom: 2rem;
}
header span.documentBranch {
border: 1px dashed #efdddd;
padding: 0 5px;
display: none;
}
header select.documentBranch {
background: none;
color: #efdddd;
border: none;
font-size: 1em;
font-weight: lighter;
font-family: sans-serif;
-webkit-appearance: none;
-moz-appearance: none;
border: 1px dashed #efdddd;
cursor: pointer;
padding: 0 5px;
}
header select.documentBranch:hover {
text-decoration: underline;
}
header select.documentBranch option {
color: initial;
font-size: initial;
font-weight: initial;
font-family: initial;
cursor: initial;
}
header .linkContainer {
order: -1;
background-color: #a40000;
flex-direction: column;
display: flex;
align-items: center;
}
header ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
max-width: 1400px;
width: 100%;
justify-content: space-between;
}
header a {
color: white;
padding: 10px;
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
transition: color .2s ease;
height: 100%;
box-sizing: border-box;
}
header a:link {
text-decoration: none;
}
header a:hover {
color: #050a02;
}
main {
flex: 1;
max-width: 1400px;
display: flex;
}
/* Shift headers by 1 */
h2 {
font-size: 2em;
margin: .67em 0;
}
h3 {
font-size: 1.5em;
margin: .75em 0;
}
h4 {
font-size: 1.17em;
margin: .83em 0;
}
h5 {
font-size: 1em;
margin: 1.12em 0;
}
h6 {
font-size: .83em;
margin: 1.5em 0;
}
p.heading {
font-size: .75em;
margin: 1.67em 0;
font-weight: bolder;
}
img, .tableContainer {
max-width: 100%;
}
.tableContainer {
overflow-x: scroll;
}
blockquote {
border-radius: 3px;
background-color: #ECE7D5;
padding: 10px;
border-left: 3px solid #F6E39A;
margin-left: 0;
margin-right: 0;
}
blockquote > *:first-child {
margin-top: 0;
}
blockquote > *:last-child {
margin-bottom: 0;
}
div.sourceCode {
border-radius: 3px;
background-color: #D2D7E3;
padding: 10px;
border-left: 3px solid #a40000;
}
:not(pre) > code {
border-radius: 2px;
background-color: #D2D7E3;
padding: 0 2px;
}
section.last {
min-height: 100vh;
}
nav h2 {
font-weight: normal;
padding: 0 10px;
font-size: 1.5em;
margin: .75em 0;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
}
nav {
padding: 10px 0;
margin-right: 2rem;
flex: 1;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
scrollbar-width: none;
}
nav::-webkit-scrollbar {
display: none;
}
nav .level2 {
padding-bottom: 20px;
}
nav a {
display: block;
padding: 5px 10px;
padding-left: 7px;
border-left: 3px solid transparent;
border-radius: 3px;
}
nav .level2 > a {
font-weight: bolder;
}
nav .level4.active > a::after {
content: "▷";
color: #424247;
position: absolute;
top: 0;
left: 10px;
height: 100%;
width: 20px;
font-size: 0.8em;
display: flex;
justify-content: center;
align-items: center;
}
nav .level4 > a {
color: #424247;
padding-left: 30px;
position: relative;
}
nav .level2.active > a, nav .level3.active > a {
background: #D2D7E3;
border-left-color: #a40000;
}
nav .level4 {
display: none;
}
nav .active > ul > .level4 {
display: initial;
}
main p, main ul, main ol {
max-width: 42em;
box-sizing: border-box;
margin-left: 0;
}
.documentContainer {
padding: 10px;
box-sizing: border-box;
width: 75%;
}
table, tr, td, th, thead {
border: 1px solid #424247;
border-collapse: collapse;
}
thead {
background-color: #D2D7E3;
}
td, th {
padding: 5px 10px;
}
.headerlink {
display: none;
}
section > *:first-child:hover .headerlink {
display: initial;
}
@media all and (max-width: 50em) {
nav {
display: none;
}
.documentContainer {
width: 100%;
}
header .logoContainer {
padding: 2em 10px;
background-position: 50%;
min-height: 320px;
text-align: center;
}
header .logoContainer, header select.documentBranch {
text-shadow: 0 0 3px #050a02, 0 0 3px #050a02;
}
}

55
documentation-tools/filter.py Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Filter program for transforming the Pandoc AST
"""
import json
import re
import sys
from build import SHIFT_HEADING
def text_content(content):
text = ""
for block in content:
if block["t"] == "Space":
text += " "
elif block["t"] == "Str":
text += block["c"]
return text
def convert_framgent(*titles):
titles = list(titles)
for i, title in enumerate(titles):
title = re.sub(r"\s", "-", title)
title = re.sub(r"[^\w-]", "", title)
titles[i] = title.lower()
return "/".join(titles)
def main():
data = json.load(sys.stdin)
# Use hierachical link fragments (e.g. #heading/subheading)
headings = []
for block in data["blocks"]:
if block["t"] != "Header":
continue
level, (attr_id, attr_class, attr_name), content = block["c"]
shifted_level = level - SHIFT_HEADING
title = text_content(content)
headings = headings[:shifted_level - 1]
while len(headings) < shifted_level - 1:
headings.append("")
headings.append(title)
full_attr_id = convert_framgent(*headings)
block["c"] = [level, [full_attr_id, attr_class, attr_name], content]
json.dump(data, sys.stdout)
if __name__ == "__main__":
main()

136
documentation-tools/generate.py Executable file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
Documentation generator
Generates the documentation for every Git branch and commits it.
Gracefully handles conflicting commits.
"""
import contextlib
import glob
import os
import re
import shutil
import subprocess
import sys
from tempfile import TemporaryDirectory
REMOTE = "origin"
GIT_CONFIG = {"protocol.version": "2",
"user.email": "<>",
"user.name": "Github Actions"}
COMMIT_MESSAGE = "Generate documentation"
DOCUMENTATION_SRC = "DOCUMENTATION.md"
TARGET_DIR = "beta"
SHIFT_HEADING = 1
TOOLS_PATH = os.path.dirname(__file__)
TEMPLATE_PATH = os.path.join(TOOLS_PATH, "template.html")
FILTER_EXE = os.path.join(TOOLS_PATH, "filter.py")
POSTPROCESSOR_EXE = os.path.join(TOOLS_PATH, "postprocessor.py")
PANDOC_EXE = "pandoc"
PANDOC_DOWNLOAD = "https://github.com/jgm/pandoc/releases/download/2.9.2/pandoc-2.9.2-1-amd64.deb"
def convert_doc(src_path, to_path, branch, branches):
subprocess.run([
PANDOC_EXE,
"--from=gfm",
"--to=html5",
os.path.abspath(src_path),
"--toc",
"--template=%s" % os.path.basename(TEMPLATE_PATH),
"--output=%s" % os.path.abspath(to_path),
"--section-divs",
"--shift-heading-level-by=%d" % SHIFT_HEADING,
"--toc-depth=4",
"--filter=%s" % os.path.abspath(FILTER_EXE),
"--variable=branch=%s" % branch,
*["--variable=branches=%s" % b for b in branches]],
check=True, cwd=os.path.dirname(TEMPLATE_PATH))
with open(to_path, "rb+") as f:
data = subprocess.run([POSTPROCESSOR_EXE], input=f.read(),
stdout=subprocess.PIPE, check=True).stdout
f.seek(0)
f.truncate()
f.write(data)
def install_dependencies():
subprocess.run([sys.executable, "-m", "pip", "install", "beautifulsoup4"],
check=True)
with TemporaryDirectory() as temp:
subprocess.run(["curl", "--location", "--output", "pandoc.deb",
PANDOC_DOWNLOAD], check=True, cwd=temp)
subprocess.run(["apt", "install", "--assume-yes", "./pandoc.deb"],
check=True, cwd=temp)
def natural_sort_key(s):
# https://stackoverflow.com/a/16090640
return [int(part) if part.isdigit() else part.lower()
for part in re.split(r"(\d+)", s)]
def run_git(*args):
config_args = []
for key, value in GIT_CONFIG.items():
config_args.extend(["-c", "%s=%s" % (key, value)])
output = subprocess.run(["git", *config_args, *args],
stdout=subprocess.PIPE, check=True,
universal_newlines=True).stdout
return tuple(filter(None, output.split("\n")))
def checkout(branch):
run_git("checkout", "--progress", "--force", "-B", branch,
"refs/remotes/%s/%s" % (REMOTE, branch))
def run_git_fetch_and_restart_if_changed(remote_commits, target_branch):
run_git("fetch", "--no-tags", "--prune", "--progress",
"--no-recurse-submodules", "--depth=1", REMOTE,
"+refs/heads/*:refs/remotes/%s/*" % REMOTE)
if remote_commits != run_git("rev-parse", "--remotes=%s" % REMOTE):
checkout(target_branch)
print("Remote changed, restarting", file=sys.stderr)
os.execv(__file__, sys.argv)
def main():
install_dependencies()
target_branch, = run_git("rev-parse", "--abbrev-ref", "HEAD")
remote_commits = run_git("rev-parse", "--remotes=%s" % REMOTE)
run_git_fetch_and_restart_if_changed(remote_commits, target_branch)
branches = [ref[len("refs/remotes/%s/" % REMOTE):] for ref in run_git(
"rev-parse", "--symbolic-full-name", "--remotes=%s" % REMOTE)]
branches.sort(key=natural_sort_key, reverse=True)
os.makedirs(TARGET_DIR, exist_ok=True)
for path in glob.iglob(os.path.join(TARGET_DIR, "*.html")):
run_git("rm", "--", path)
with TemporaryDirectory() as temp:
branch_docs = {}
for branch in branches:
checkout(branch)
if os.path.exists(DOCUMENTATION_SRC):
branch_docs[branch] = os.path.join(temp, "%s.md" % branch)
shutil.copy(DOCUMENTATION_SRC, branch_docs[branch])
checkout(target_branch)
for branch, src_path in branch_docs.items():
to_path = os.path.join(TARGET_DIR, "%s.html" % branch)
convert_doc(src_path, to_path, branch, branches)
run_git("add", "--", to_path)
with contextlib.suppress(subprocess.CalledProcessError):
run_git("diff", "--cached", "--quiet")
print("No changes", file=sys.stderr)
return
run_git("commit", "-m", COMMIT_MESSAGE)
try:
run_git("push", REMOTE, "HEAD:%s" % target_branch)
except subprocess.CalledProcessError:
run_git_fetch_and_restart_if_changed(remote_commits, target_branch)
raise
if __name__ == "__main__":
main()

View file

@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""
Postprocessor program for the HTML output of Pandoc
"""
import sys
from bs4 import BeautifulSoup
def add_class(element, class_):
element["class"] = element.get("class", []) + [class_]
def main():
soup = BeautifulSoup(sys.stdin.buffer, "html.parser")
# Mark the hierachical levels in the navigation
for section in soup.select("section"):
link = soup.find("a", href="#" + section["id"])
if link is None:
continue
add_class(link.parent, section["class"][0])
# Mark last section
add_class(soup.select("section")[-1], "last")
# Wrap tables in a div container (for scrolling)
for table in soup.select("main table"):
container = soup.new_tag("div")
add_class(container, "tableContainer")
table.wrap(container)
# Add a link with the fragment to every header
for header in soup.select("section > *:first-child"):
section = header.parent
link = soup.new_tag("a")
add_class(link, "headerlink")
link["href"] = "#" + section["id"]
link.string = ""
header.append(" ")
header.append(link)
sys.stdout.buffer.write(soup.encode(formatter="html5") + b"\n")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<meta name="generator" content="pandoc">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<style>
$styles.html()$
</style>
$if(math)$
$math$
$endif$
<link href="assets/default.css" media="all" rel="stylesheet">
<link href="assets/screen.css" media="screen" rel="stylesheet">
<noscript><link href="assets/screen-noscript.css" media="screen" rel="stylesheet"></noscript>
<link href="https://github.com/Kozea/Radicale/releases.atom" type="application/atom+xml" rel="alternate" title="Radicale Releases">
<link href="assets/icon.png" type="image/png" rel="shortcut icon">
<title>Radicale - Free and Open-Source CalDAV and CardDAV Server</title>
<meta name="description" content="Free and Open-Source CalDAV and CardDAV Server">
<script src="assets/navigation.js"></script>
<script src="assets/document-branches.js"></script>
<script>
const documentBranch = "$branch$";
const documentBranches = ["$for(branches)$$branches$$sep$", "$endfor$"];
</script>
<header>
<div class="logoContainer">
<h1>
Radicale
<span class="documentBranch">$branch$</span>
<select class="documentBranch"></select>
</h1>
<p>Free and Open-Source CalDAV and CardDAV Server</p>
</div>
<div class="linkContainer">
<ul>
<li><a href="https://community.kozea.fr">Made with ❤ by Kozea Community</a></li>
<li><a href="https://github.com/Kozea/Radicale">Fork me on GitHub</a></li>
</ul>
</div>
</header>
<main>
<nav>
<h2>Contents</h2>
$table-of-contents$
</nav>
<div class="documentContainer">
$body$
</div>
</main>