Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/docs/web/main.py
428331 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2010-2026 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file main.py
15
# @author Angelo Banse
16
# @date 2026-02-13
17
18
"""
19
This script is used by the mkdocs-macros-plugin to list all markdown files inside a directory (even with subfolders)
20
21
Usage (only when called from a page inside the folder whose contents we want to list):
22
{{ list_pages("Netedit") }}
23
{{ list_pages("Contributed", recursive=true) }}
24
"""
25
26
from pathlib import Path
27
import re
28
29
30
def define_env(env):
31
docs_dir = Path(env.conf["docs_dir"]).resolve()
32
use_dir_urls = env.conf.get("use_directory_urls", True)
33
34
FRONTMATTER_RE = re.compile(r"(?s)\A---\s*\n(.*?)\n---\s*\n")
35
TITLE_RE = re.compile(r"(?m)^\s*title\s*:\s*(.+?)\s*$")
36
37
def human_title(stem: str) -> str:
38
return re.sub(r"[-_]+", " ", stem).strip().title()
39
40
def strip_quotes(s: str) -> str:
41
s = s.strip()
42
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
43
return s[1:-1].strip()
44
return s
45
46
def extract_title(md_text: str, fallback_stem: str) -> str:
47
# 1) frontmatter title
48
m = FRONTMATTER_RE.search(md_text)
49
if m:
50
fm = m.group(1)
51
t = TITLE_RE.search(fm)
52
if t:
53
return strip_quotes(t.group(1))
54
55
# 2) first H1
56
for line in md_text.splitlines():
57
if line.startswith("# "):
58
return line[2:].strip()
59
60
# 3) filename
61
return human_title(fallback_stem)
62
63
def md_to_link(rel_md_path: Path) -> str:
64
rel = rel_md_path.as_posix().replace("\\", "/")
65
66
# index/readme -> folder root
67
if rel.lower() in ("index.md", "readme.md"):
68
rel = ""
69
70
if rel.endswith(".md"):
71
rel = rel[:-3]
72
73
if use_dir_urls:
74
return (rel.rstrip("/") + "/") if rel else "./"
75
else:
76
return (rel + ".md") if rel else "index.md"
77
78
def current_page_dir() -> Path | None:
79
page = getattr(env, "page", None)
80
src_path = getattr(getattr(page, "file", None), "src_path", None) if page else None
81
if not src_path:
82
return None
83
return (docs_dir / src_path).resolve().parent
84
85
def build_tree(md_files: list[Path], base: Path) -> dict:
86
root = {"files": [], "dirs": {}}
87
88
for p in md_files:
89
rel = p.relative_to(base)
90
parts = rel.parts
91
if len(parts) == 1:
92
root["files"].append(p)
93
continue
94
95
node = root
96
for d in parts[:-1]:
97
node = node["dirs"].setdefault(d, {"files": [], "dirs": {}})
98
node["files"].append(p)
99
100
# sort deterministically
101
def sort_node(node):
102
node["files"].sort(key=lambda p: p.as_posix().lower())
103
for k in sorted(list(node["dirs"].keys()), key=lambda s: s.lower()):
104
sort_node(node["dirs"][k])
105
106
sort_node(root)
107
return root
108
109
def render_tree(node: dict, base: Path, indent: int = 0, rel_prefix: Path = Path(".")) -> list[str]:
110
lines: list[str] = []
111
112
# files at this level
113
for p in node["files"]:
114
rel_from_base = p.relative_to(base)
115
title = None
116
try:
117
text = p.read_text(encoding="utf-8")
118
title = extract_title(text, p.stem)
119
except Exception:
120
title = human_title(p.stem)
121
122
url = md_to_link(rel_from_base)
123
lines.append(f"{' '*indent}- [{title}]({url})")
124
125
# subdirs
126
for dirname, child in node["dirs"].items():
127
folder_title = human_title(dirname)
128
lines.append(f"{' '*indent}- **{folder_title}**")
129
lines.extend(render_tree(child, base, indent=indent + 1))
130
131
return lines
132
133
@env.macro
134
def list_pages(
135
folder: str,
136
recursive: bool = False,
137
exclude: str = r"^(index|README)$",
138
):
139
base = (docs_dir / folder).resolve()
140
if not base.is_dir():
141
return f"> **Error:** folder `{folder}` not found under `{env.conf['docs_dir']}`."
142
143
# macro must be used from within that folder
144
page_dir = current_page_dir()
145
if page_dir is None:
146
return (
147
f"> **Error:** cannot detect current page path; `list_pages('{folder}')` "
148
f"must be used from a page inside `{folder}/`."
149
)
150
151
try:
152
page_dir.relative_to(base)
153
except Exception:
154
try:
155
where = page_dir.relative_to(docs_dir).as_posix()
156
except Exception:
157
where = str(page_dir)
158
return (
159
f"> **Error:** `list_pages('{folder}')` must be used from a page inside `{folder}/`.\n"
160
f"> Current page is in `{where}/`."
161
)
162
163
pattern = "**/*.md" if recursive else "*.md"
164
ex = re.compile(exclude)
165
166
md_files = sorted(
167
(p for p in base.glob(pattern) if p.is_file() and not ex.search(p.stem)),
168
key=lambda p: p.as_posix().lower(),
169
)
170
171
if not md_files:
172
return "> *(No pages found)*"
173
174
if not recursive:
175
lines = []
176
for p in md_files:
177
try:
178
text = p.read_text(encoding="utf-8")
179
title = extract_title(text, p.stem)
180
except Exception:
181
title = human_title(p.stem)
182
183
rel_from_base = p.relative_to(base)
184
url = md_to_link(rel_from_base)
185
lines.append(f"- [{title}]({url})")
186
return "\n".join(lines)
187
188
tree = build_tree(md_files, base)
189
lines = render_tree(tree, base)
190
191
return "\n".join(lines)
192
193