Skip to content

Commit 156b15b

Browse files
committed
Add a user manual documentation tree
1 parent 4db5e40 commit 156b15b

File tree

5 files changed

+116
-2
lines changed

5 files changed

+116
-2
lines changed

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.PHONY: build build-alpine build-playwright build-ts build-ubuntu certs \
22
check check-extra check-light commit docs generate-version ipython \
3-
run-playwright run-playwright-slow serve serve-local sync sync-all \
4-
update-deps
3+
manual run-playwright run-playwright-slow serve serve-local sync \
4+
sync-all update-deps
55

66
BIND ?= 127.0.0.1:8080
77
IMAGE ?= tooling-trusted-release
@@ -62,6 +62,12 @@ generate-version:
6262
ipython:
6363
uv run --frozen --with ipython ipython
6464

65+
manual:
66+
for fn in atr/manual/*.md; \
67+
do \
68+
cmark "$$fn" > "$${fn%.md}.html"; \
69+
done
70+
6571
run-playwright:
6672
docker run --net=host -it atr-playwright python3 test.py --skip-slow
6773

atr/manual/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<h1>Apache Trusted Releases user manual</h1>
2+
<p>Welcome to the user manual for the <strong>Apache Trusted Releases</strong> (ATR) platform.</p>
3+
<p>This user manual is a work in progress.</p>

atr/manual/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Apache Trusted Releases user manual
2+
3+
Welcome to the user manual for the **Apache Trusted Releases** (ATR) platform.
4+
5+
This user manual is a work in progress.

atr/routes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import atr.routes.finish as finish
2727
import atr.routes.ignores as ignores
2828
import atr.routes.keys as keys
29+
import atr.routes.manual as manual
2930
import atr.routes.preview as preview
3031
import atr.routes.projects as projects
3132
import atr.routes.published as published
@@ -53,6 +54,7 @@
5354
"finish",
5455
"ignores",
5556
"keys",
57+
"manual",
5658
"preview",
5759
"projects",
5860
"published",

atr/routes/manual.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
import pathlib
19+
from html.parser import HTMLParser
20+
21+
import aiofiles
22+
import aiofiles.os
23+
import markupsafe
24+
import quart
25+
26+
import atr.config as config
27+
import atr.route as route
28+
import atr.template as template
29+
30+
31+
class H1Parser(HTMLParser):
32+
def __init__(self) -> None:
33+
super().__init__()
34+
self.h1_content: str | None = None
35+
self._in_h1 = False
36+
37+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
38+
if (tag == "h1") and (self.h1_content is None):
39+
self._in_h1 = True
40+
41+
def handle_endtag(self, tag: str) -> None:
42+
if tag == "h1":
43+
self._in_h1 = False
44+
45+
def handle_data(self, data: str) -> None:
46+
if self._in_h1 and (self.h1_content is None):
47+
self.h1_content = data.strip()
48+
49+
50+
@route.public("/manual/")
51+
async def index(session: route.CommitterSession | None) -> str:
52+
return await _serve_manual_page("index")
53+
54+
55+
@route.public("/manual/<path:page>")
56+
async def page(session: route.CommitterSession | None, page: str) -> str:
57+
return await _serve_manual_page(page)
58+
59+
60+
async def _serve_manual_page(page: str) -> str:
61+
manual_dir = pathlib.Path(config.get().PROJECT_ROOT) / "atr" / "manual"
62+
63+
if not page.endswith(".html"):
64+
page = f"{page}.html"
65+
66+
file_path = manual_dir / page
67+
68+
manual_root = manual_dir.resolve()
69+
try:
70+
resolved_file = file_path.resolve()
71+
except FileNotFoundError:
72+
quart.abort(404)
73+
try:
74+
resolved_file.relative_to(manual_root)
75+
except ValueError:
76+
quart.abort(404)
77+
78+
if not await aiofiles.os.path.exists(resolved_file):
79+
quart.abort(404)
80+
81+
if not await aiofiles.os.path.isfile(resolved_file):
82+
quart.abort(404)
83+
84+
async with aiofiles.open(resolved_file, encoding="utf-8") as handle:
85+
html_content = await handle.read()
86+
87+
safe_content = markupsafe.Markup(html_content)
88+
89+
filename_title = resolved_file.stem.replace("-", " ").replace("_", " ").title()
90+
try:
91+
parser = H1Parser()
92+
first_lines = "\n".join(html_content.split("\n")[:8])
93+
parser.feed(first_lines)
94+
title = parser.h1_content or filename_title
95+
except Exception:
96+
title = filename_title
97+
98+
return await template.blank(title=title, content=safe_content)

0 commit comments

Comments
 (0)