mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-08-22 00:43:44 +08:00
docs: replace zensical with dumb html template (#38462)
This commit is contained in:
@@ -35,8 +35,7 @@ jobs:
|
||||
- name: Build docs
|
||||
run: |
|
||||
git lfs pull
|
||||
pip install zensical
|
||||
python scripts/docs.py build
|
||||
python docs/serve.py --build
|
||||
|
||||
# Push to docs.comma.ai
|
||||
- uses: actions/checkout@v7
|
||||
@@ -57,7 +56,7 @@ jobs:
|
||||
git rm -rf .
|
||||
|
||||
# copy over docs
|
||||
cp -r ../docs_site/ docs/
|
||||
cp -r ../docs/_site/ docs/
|
||||
|
||||
# GitHub pages config
|
||||
touch docs/.nojekyll
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ compare_runtime*.html
|
||||
openpilot/selfdrive/modeld/models/tg_input_devices.json
|
||||
|
||||
# build artifacts
|
||||
docs_site/
|
||||
docs/_site/
|
||||
openpilot/selfdrive/pandad/pandad
|
||||
openpilot/cereal/services.h
|
||||
openpilot/cereal/gen
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Docs development
|
||||
|
||||
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
Those commands must be run in the root directory of openpilot, **not /docs**
|
||||
|
||||
**1. Install the docs dependencies**
|
||||
``` bash
|
||||
uv pip install .[docs]
|
||||
```
|
||||
|
||||
**2. Build the new site**
|
||||
``` bash
|
||||
docs build
|
||||
```
|
||||
|
||||
**3. Run the new site locally**
|
||||
``` bash
|
||||
docs serve
|
||||
```
|
||||
|
||||
References:
|
||||
* https://zensical.org/docs/
|
||||
@@ -0,0 +1,14 @@
|
||||
# Docs development
|
||||
|
||||
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
**1. Build the site**
|
||||
``` bash
|
||||
python docs/serve.py --build
|
||||
```
|
||||
|
||||
**2. Run the site locally** (rebuilds on change)
|
||||
``` bash
|
||||
python docs/serve.py
|
||||
```
|
||||
@@ -1 +1 @@
|
||||
../../selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
../../openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e019ed5af500e8820d05934d47e0380728e1b30e01a179f786dc4edb1eccbd7
|
||||
size 349
|
||||
@@ -1,3 +0,0 @@
|
||||
# openpilot glossary
|
||||
|
||||
{{GLOSSARY_DEFINITIONS}}
|
||||
@@ -1,216 +0,0 @@
|
||||
import posixpath
|
||||
import re
|
||||
import tomllib
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.preprocessors import Preprocessor
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
from zensical.extensions.links import LinksTreeprocessor
|
||||
|
||||
GlossaryTerm = tuple[str, re.Pattern[str], str]
|
||||
|
||||
GLOSSARY_FILE = Path(__file__).with_name("glossary.toml")
|
||||
GLOSSARY_PAGE = "concepts/glossary.md"
|
||||
GLOSSARY_PLACEHOLDER = "{{GLOSSARY_DEFINITIONS}}"
|
||||
|
||||
SKIP_TAGS = {
|
||||
"a",
|
||||
"code",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"kbd",
|
||||
"pre",
|
||||
"script",
|
||||
"style",
|
||||
}
|
||||
|
||||
def clean_tooltip(description: str) -> str:
|
||||
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
|
||||
text = re.sub(r"`([^`]+)`", r"\1", text)
|
||||
text = re.sub(r"[*_~]", "", text)
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def load_glossary() -> tuple[list[GlossaryTerm], str]:
|
||||
with GLOSSARY_FILE.open("rb") as f:
|
||||
glossary_data = tomllib.load(f).get("glossary", {})
|
||||
|
||||
glossary: list[GlossaryTerm] = []
|
||||
rendered = []
|
||||
for key, value in glossary_data.items():
|
||||
label = str(key).strip().replace("_", " ")
|
||||
description = str(value).strip()
|
||||
if not description:
|
||||
continue
|
||||
|
||||
slug = label.replace(" ", "-").replace("_", "-").lower()
|
||||
glossary.append((slug, re.compile(rf"(?<!\w){re.escape(label)}(?!\w)", re.IGNORECASE), clean_tooltip(description)))
|
||||
rendered.append(f'* <span id="{slug}"></span>**{label}**: {description}')
|
||||
|
||||
return glossary, "\n".join(rendered)
|
||||
|
||||
|
||||
class GlossaryPreprocessor(Preprocessor):
|
||||
def __init__(self, md, glossary: str):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
markdown = "\n".join(lines)
|
||||
if GLOSSARY_PLACEHOLDER not in markdown:
|
||||
return lines
|
||||
return markdown.replace(GLOSSARY_PLACEHOLDER, self.glossary).splitlines()
|
||||
|
||||
|
||||
class GlossaryTreeprocessor(Treeprocessor):
|
||||
def __init__(self, md, glossary: list[GlossaryTerm]):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
self.seen: set[str] = set()
|
||||
|
||||
def run(self, root: ET.Element) -> None:
|
||||
at = self.md.treeprocessors.get_index_for_name("zrelpath")
|
||||
processor = self.md.treeprocessors[at]
|
||||
if not isinstance(processor, LinksTreeprocessor):
|
||||
raise TypeError("Links processor not registered")
|
||||
if processor.path == GLOSSARY_PAGE:
|
||||
return
|
||||
|
||||
self.seen.clear()
|
||||
glossary_href = f"{posixpath.relpath(GLOSSARY_PAGE, posixpath.dirname(processor.path) or '.')}#"
|
||||
self._walk(root, glossary_href)
|
||||
|
||||
def _walk(self, element: ET.Element, glossary_href: str) -> None:
|
||||
if element.tag in SKIP_TAGS or element.attrib.get("data-glossary-skip") is not None:
|
||||
return
|
||||
|
||||
self._replace(element, glossary_href)
|
||||
|
||||
idx = 0
|
||||
while idx < len(element):
|
||||
child = element[idx]
|
||||
self._walk(child, glossary_href)
|
||||
idx = self._replace(element, glossary_href, idx) + 1
|
||||
|
||||
def _replace(self, parent: ET.Element, glossary_href: str, index: int | None = None) -> int:
|
||||
child = None if index is None else parent[index]
|
||||
text = parent.text if child is None else child.tail
|
||||
pieces = self._pieces(text or "", glossary_href)
|
||||
if not pieces:
|
||||
return -1 if index is None else index
|
||||
|
||||
if child is None:
|
||||
parent.text = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
# Insert replacements for parent.text before the first existing child.
|
||||
insert_at = -1
|
||||
else:
|
||||
assert index is not None
|
||||
child.tail = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
insert_at = index
|
||||
|
||||
start = 1 if isinstance(pieces[0], str) else 0
|
||||
previous = child
|
||||
|
||||
for piece in pieces[start:]:
|
||||
if isinstance(piece, str):
|
||||
previous.tail = (previous.tail or "") + piece
|
||||
continue
|
||||
|
||||
insert_at += 1
|
||||
parent.insert(insert_at, piece)
|
||||
previous = piece
|
||||
|
||||
return insert_at
|
||||
|
||||
def _pieces(self, text: str, glossary_href: str) -> list[str | ET.Element]:
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
pieces: list[str | ET.Element] = []
|
||||
cursor = 0
|
||||
|
||||
while True:
|
||||
best = None
|
||||
for slug, pattern, tooltip in self.glossary:
|
||||
if slug in self.seen:
|
||||
continue
|
||||
|
||||
found = pattern.search(text, cursor)
|
||||
if found is None:
|
||||
continue
|
||||
|
||||
candidate = (slug, tooltip, found.start(), found.end())
|
||||
if best is None:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
_, _, best_start, best_end = best
|
||||
_, _, current_start, current_end = candidate
|
||||
if current_start < best_start:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
if current_start == best_start and current_end - current_start > best_end - best_start:
|
||||
best = candidate
|
||||
|
||||
if best is None:
|
||||
break
|
||||
|
||||
slug, tooltip, start, end = best
|
||||
if start > cursor:
|
||||
pieces.append(text[cursor:start])
|
||||
|
||||
link = ET.Element(
|
||||
"a",
|
||||
{
|
||||
"class": "glossary-term",
|
||||
"data-glossary-term": "",
|
||||
"href": f"{glossary_href}{slug}",
|
||||
},
|
||||
)
|
||||
ET.SubElement(link, "span", {"class": "glossary-term__label"}).text = text[start:end]
|
||||
ET.SubElement(
|
||||
link,
|
||||
"span",
|
||||
{
|
||||
"class": "glossary-term__tooltip",
|
||||
"data-search-exclude": "",
|
||||
},
|
||||
).text = tooltip
|
||||
pieces.append(link)
|
||||
self.seen.add(slug)
|
||||
cursor = end
|
||||
|
||||
if not pieces:
|
||||
return []
|
||||
if cursor < len(text):
|
||||
pieces.append(text[cursor:])
|
||||
return pieces
|
||||
|
||||
|
||||
class GlossaryExtension(Extension):
|
||||
def extendMarkdown(self, md) -> None:
|
||||
md.registerExtension(self)
|
||||
glossary, rendered = load_glossary()
|
||||
|
||||
md.preprocessors.register(
|
||||
GlossaryPreprocessor(md, rendered),
|
||||
"docs-ext-glossary-preprocessor",
|
||||
27,
|
||||
)
|
||||
md.treeprocessors.register(
|
||||
GlossaryTreeprocessor(md, glossary),
|
||||
"docs-ext-glossary-treeprocessor",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def makeExtension(**kwargs) -> GlossaryExtension:
|
||||
return GlossaryExtension(**kwargs)
|
||||
@@ -1,8 +0,0 @@
|
||||
[glossary]
|
||||
onroad = "openpilot's system state while ignition is on."
|
||||
offroad = "openpilot's system state while ignition is off."
|
||||
route = "A route is a recording of an onroad session."
|
||||
segment = "Routes are split into one minute chunks called segments."
|
||||
"comma connect" = "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai)."
|
||||
panda = "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda)."
|
||||
"comma four" = "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four)."
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
import argparse
|
||||
import functools
|
||||
import html
|
||||
import http.server
|
||||
import json
|
||||
import posixpath
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
DOCS_DIR = Path(__file__).resolve().parent
|
||||
SITE_DIR = DOCS_DIR / "_site"
|
||||
TEMPLATE_FILE = DOCS_DIR / "template.html"
|
||||
EXCLUDE_DIRS = {"_site"}
|
||||
|
||||
REPO_URL = "https://github.com/commaai/openpilot/"
|
||||
|
||||
# (title, target) pairs. target is a page path or an absolute URL.
|
||||
# A None target marks a section header.
|
||||
NAV: list[tuple[str, str | None]] = [
|
||||
("What is openpilot?", "index.md"),
|
||||
("How-to", None),
|
||||
("Turn the speed blue", "how-to/turn-the-speed-blue.md"),
|
||||
("Connect to a comma 3X or four", "how-to/connect-to-comma.md"),
|
||||
("Add support for a car", "how-to/car-port.md"),
|
||||
("Concepts", None),
|
||||
("Logs", "concepts/logs.md"),
|
||||
("Safety", "concepts/safety.md"),
|
||||
("Glossary", "concepts/glossary.md"),
|
||||
("Contributing", None),
|
||||
("Feedback", "contributing/feedback.md"),
|
||||
("Roadmap", "contributing/roadmap.md"),
|
||||
("Contributing Guide →", "https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md"),
|
||||
("Links", None),
|
||||
("Blog →", "https://blog.comma.ai"),
|
||||
("Bounties →", "https://comma.ai/bounties"),
|
||||
("GitHub →", "https://github.com/commaai"),
|
||||
("Discord →", "https://discord.comma.ai"),
|
||||
("X →", "https://x.com/comma_ai"),
|
||||
]
|
||||
|
||||
GLOSSARY_DESCRIPTIONS = {
|
||||
"onroad": "openpilot's system state while ignition is on.",
|
||||
"offroad": "openpilot's system state while ignition is off.",
|
||||
"route": "A route is a recording of an onroad session.",
|
||||
"segment": "Routes are split into one minute chunks called segments.",
|
||||
"comma connect": "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).",
|
||||
"panda": "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).",
|
||||
"comma four": "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).",
|
||||
}
|
||||
GLOSSARY_PAGE = "concepts/glossary.md"
|
||||
GLOSSARY_ROUTE = GLOSSARY_PAGE.removesuffix(".md")
|
||||
GLOSSARY_SKIP = frozenset("a code h1 h2 h3 h4 h5 h6 kbd pre script style".split())
|
||||
|
||||
_ENTITY = re.compile(r"&(?:#x?[0-9a-fA-F]+|[a-zA-Z]+);")
|
||||
_LIST = re.compile(r"^(\s*)([*+-]|\d+\.)\s+(.*)$")
|
||||
_HEADING = re.compile(r"^(#{1,6})\s+(.*)$")
|
||||
_HR = re.compile(r"^(-{3,}|\*{3,}|_{3,})$")
|
||||
_ATTR_URL = re.compile(r"""(?P<pre>\b(?:href|src)=(?P<q>["']))(?P<url>.*?)(?P=q)""")
|
||||
_VOID = frozenset("br img hr meta link input".split())
|
||||
|
||||
def page_route(path: str) -> str:
|
||||
path = path.removesuffix(".md")
|
||||
return posixpath.dirname(path) or "." if posixpath.basename(path) == "index" else path
|
||||
|
||||
def page_href(current: str, target: str) -> str:
|
||||
route = posixpath.relpath(page_route(target), page_route(current))
|
||||
return ("." if route == "." else route) + "/"
|
||||
|
||||
def rewrite_relative_url(value: str, page: str) -> str | None:
|
||||
url = urllib.parse.urlparse(value)
|
||||
if value.startswith(("#", "/")) or url.scheme or url.netloc or not url.path:
|
||||
return None
|
||||
target = posixpath.normpath(posixpath.join(posixpath.dirname(page), url.path))
|
||||
if target == ".." or target.startswith("../"):
|
||||
return None
|
||||
path = page_href(page, target) if target.endswith(".md") else posixpath.relpath(target, page_route(page))
|
||||
return url._replace(path=path).geturl()
|
||||
|
||||
def rewrite_html_urls(fragment: str, page: str) -> str:
|
||||
def repl(m: re.Match[str]) -> str:
|
||||
r = rewrite_relative_url(m.group("url"), page)
|
||||
return m.group(0) if r is None else f'{m.group("pre")}{r}{m.group("q")}'
|
||||
return _ATTR_URL.sub(repl, fragment)
|
||||
|
||||
def esc(text: str, attr: bool = False) -> str:
|
||||
held: list[str] = []
|
||||
def hold(m: re.Match[str]) -> str:
|
||||
held.append(m.group(0))
|
||||
return f"\0{len(held) - 1}\0"
|
||||
return re.sub(r"\0(\d+)\0", lambda m: held[int(m.group(1))], html.escape(_ENTITY.sub(hold, text), quote=attr))
|
||||
|
||||
def clean_tooltip(description: str) -> str:
|
||||
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
|
||||
return re.sub(r"\s+", " ", re.sub(r"[*_~]", "", re.sub(r"`([^`]+)`", r"\1", text))).strip()
|
||||
|
||||
def glossary_slug(label: str) -> str:
|
||||
return label.replace(" ", "-").replace("_", "-").lower()
|
||||
|
||||
GLOSSARY_TERMS = [
|
||||
(glossary_slug(l), re.compile(rf"(?<!\w){re.escape(l)}(?!\w)", re.I), clean_tooltip(d))
|
||||
for l, d in GLOSSARY_DESCRIPTIONS.items()
|
||||
]
|
||||
GLOSSARY_DEFINITIONS = "\n".join(
|
||||
f'* <span id="{glossary_slug(l)}"></span>**{l}**: {d}' for l, d in GLOSSARY_DESCRIPTIONS.items()
|
||||
)
|
||||
|
||||
def inject_glossary(body: str, page: str) -> str:
|
||||
if page == GLOSSARY_PAGE:
|
||||
return body
|
||||
route = "." if page == "index.md" else page.removesuffix(".md")
|
||||
base, seen, out, skip, depth = f"{posixpath.relpath(GLOSSARY_ROUTE, route)}/#", set(), [], None, 0
|
||||
for part in re.split(r"(<[^>]+>)", body):
|
||||
if not part:
|
||||
continue
|
||||
if part.startswith("<"):
|
||||
out.append(part)
|
||||
if part.startswith("<!") or not (m := re.match(r"</?\s*([a-zA-Z0-9]+)", part)):
|
||||
continue
|
||||
tag = m.group(1).lower()
|
||||
if tag not in GLOSSARY_SKIP:
|
||||
continue
|
||||
closing, void = part.startswith("</"), part.endswith("/>") or tag in _VOID
|
||||
if closing and skip == tag and depth:
|
||||
depth -= 1
|
||||
skip = None if not depth else skip
|
||||
elif not closing and not void:
|
||||
skip, depth = (tag, 1) if skip is None else (skip, depth + (skip == tag))
|
||||
continue
|
||||
if depth:
|
||||
out.append(part); continue
|
||||
cur, text = 0, part
|
||||
while True:
|
||||
best = None
|
||||
for order, (slug, pat, tip) in enumerate(GLOSSARY_TERMS):
|
||||
if slug in seen or (found := pat.search(text, cur)) is None:
|
||||
continue
|
||||
cand = (found.start(), found.start() - found.end(), order, slug, tip, found.end(), found.group(0))
|
||||
if best is None or cand[:3] < best[:3]:
|
||||
best = cand
|
||||
if best is None:
|
||||
out.append(text[cur:]); break
|
||||
start, _, _, slug, tip, end, matched = best
|
||||
out.append(text[cur:start])
|
||||
out.append(
|
||||
f'<a class="glossary-term" data-glossary-term="" href="{base}{slug}">'
|
||||
f'<span class="glossary-term__label">{matched}</span>'
|
||||
f'<span class="glossary-term__tooltip" data-search-exclude="">{esc(tip)}</span></a>'
|
||||
)
|
||||
seen.add(slug); cur = end
|
||||
return "".join(out)
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
text = html.unescape(re.sub(r"<[^>]+>", "", text)).lower()
|
||||
return re.sub(r"[-\s]+", "-", re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE)).strip("-")
|
||||
|
||||
def _parse_link(text: str, start: int) -> tuple[str, str, int] | None:
|
||||
if start >= len(text) or text[start] != "[":
|
||||
return None
|
||||
depth, i = 0, start
|
||||
while i < len(text):
|
||||
depth += (text[i] == "[") - (text[i] == "]")
|
||||
if text[i] == "]" and depth == 0:
|
||||
label = text[start + 1:i]
|
||||
if i + 1 >= len(text) or text[i + 1] != "(":
|
||||
return None
|
||||
j, dp = i + 2, 1
|
||||
while j < len(text) and dp:
|
||||
dp += (text[j] == "(") - (text[j] == ")"); j += 1
|
||||
return None if dp else (label, text[i + 2:j - 1], j)
|
||||
i += 1
|
||||
return None
|
||||
|
||||
def render_inline(text: str, page: str) -> str:
|
||||
out, i, n = [], 0, len(text)
|
||||
while i < n:
|
||||
if text[i] == "\n" and i >= 2 and text[i-2:i] == " " and out and out[-1].endswith(" "):
|
||||
out[-1] = out[-1][:-2]; out.append("<br>\n"); i += 1; continue
|
||||
if text[i] == "`" and (end := text.find("`", i + 1)) != -1:
|
||||
out.append(f"<code>{esc(text[i+1:end])}</code>"); i = end + 1; continue
|
||||
if text[i] == "!" and i + 1 < n and text[i+1] == "[" and (p := _parse_link(text, i + 1)):
|
||||
label, url, end = p
|
||||
src = rewrite_relative_url(url, page) or url
|
||||
out.append(f'<img alt="{esc(label, True)}" src="{esc(src, True)}">'); i = end; continue
|
||||
if text[i] == "[" and (p := _parse_link(text, i)):
|
||||
label, url, end = p
|
||||
href = rewrite_relative_url(url, page) or url
|
||||
out.append(f'<a href="{esc(href, True)}">{render_inline(label, page)}</a>'); i = end; continue
|
||||
if text[i] == "<":
|
||||
if text.startswith("<!--", i):
|
||||
end = text.find("-->", i + 4); end = n if end < 0 else end + 3
|
||||
out.append(rewrite_html_urls(text[i:end], page)); i = end; continue
|
||||
if m := re.match(r"<[^>]+>", text[i:]):
|
||||
out.append(rewrite_html_urls(m.group(0), page)); i += len(m.group(0)); continue
|
||||
if (text.startswith("**", i) or text.startswith("__", i)) and (end := text.find(text[i:i+2], i+2)) != -1:
|
||||
out.append(f"<strong>{render_inline(text[i+2:end], page)}</strong>"); i = end + 2; continue
|
||||
if text[i] in "*_" and i + 1 < n and text[i+1] not in " \t\n" and (end := text.find(text[i], i+1)) > i + 1:
|
||||
out.append(f"<em>{render_inline(text[i+1:end], page)}</em>"); i = end + 1; continue
|
||||
j = i + 1
|
||||
while j < n and text[j] not in "`[<!*_":
|
||||
j += 1
|
||||
out.append(esc(text[i:j])); i = j
|
||||
return "".join(out)
|
||||
|
||||
def _trow(line: str) -> list[str]:
|
||||
return [c.strip() for c in line.strip().removeprefix("|").removesuffix("|").split("|")]
|
||||
|
||||
def _is_sep(line: str) -> bool:
|
||||
return "|" in line and all(re.fullmatch(r":?-{3,}:?", c.strip()) for c in _trow(line))
|
||||
|
||||
def _align(sep: str) -> str:
|
||||
s = sep.strip()
|
||||
if s.startswith(":") and s.endswith(":"): return ' style="text-align: center;"'
|
||||
if s.endswith(":"): return ' style="text-align: right;"'
|
||||
if s.startswith(":"): return ' style="text-align: left;"'
|
||||
return ""
|
||||
|
||||
def _list_info(line: str) -> tuple[int, str, str] | None:
|
||||
m = _LIST.match(line)
|
||||
return None if not m else (len(m.group(1)) // 4, "ol" if m.group(2)[-1] == "." else "ul", m.group(3))
|
||||
|
||||
def render_markdown(text: str, page: str) -> str:
|
||||
lines, out, i, n = text.splitlines(), [], 0, 0
|
||||
n = len(lines)
|
||||
while i < n:
|
||||
line, s = lines[i], lines[i].strip()
|
||||
if not s:
|
||||
i += 1; continue
|
||||
|
||||
if s.startswith("```"):
|
||||
lang, body = s[3:].strip(), []
|
||||
i += 1
|
||||
while i < n and not lines[i].strip().startswith("```"):
|
||||
body.append(lines[i]); i += 1
|
||||
if i < n: i += 1
|
||||
code, cls = html.escape("\n".join(body) + ("\n" if body else "")), (f' class="language-{html.escape(lang)}"' if lang else "")
|
||||
out.append(f"<pre><code{cls}>{code}</code></pre>"); continue
|
||||
|
||||
if m := _HEADING.match(s):
|
||||
content, level = m.group(2).rstrip("#").strip(), len(m.group(1))
|
||||
sid = slugify(content)
|
||||
out.append(f'<h{level} id="{sid}">{render_inline(content, page)}<a class="headerlink" href="#{sid}" title="Permanent link">#</a></h{level}>')
|
||||
i += 1; continue
|
||||
|
||||
if _HR.fullmatch(s):
|
||||
out.append("<hr>"); i += 1; continue
|
||||
|
||||
if "|" in line and i + 1 < n and _is_sep(lines[i + 1]):
|
||||
headers, aligns = _trow(line), [_align(c) for c in _trow(lines[i + 1])]
|
||||
i += 2; rows = []
|
||||
while i < n and "|" in lines[i] and lines[i].strip():
|
||||
rows.append(_trow(lines[i])); i += 1
|
||||
parts = ["<table>", "<thead>", "<tr>"] + [
|
||||
f"<th{aligns[j] if j < len(aligns) else ''}>{render_inline(h, page)}</th>" for j, h in enumerate(headers)
|
||||
] + ["</tr>", "</thead>", "<tbody>"]
|
||||
for row in rows:
|
||||
parts.append("<tr>")
|
||||
for j in range(len(headers)):
|
||||
parts.append(f"<td{aligns[j] if j < len(aligns) else ''}>{render_inline(row[j] if j < len(row) else '', page)}</td>")
|
||||
parts.append("</tr>")
|
||||
out.append("\n".join(parts + ["</tbody>", "</table>"])); continue
|
||||
|
||||
if _list_info(line):
|
||||
items: list[tuple[int, str, list[str]]] = []
|
||||
while i < n:
|
||||
if not lines[i].strip():
|
||||
if i + 1 < n and _list_info(lines[i + 1]):
|
||||
i += 1; continue
|
||||
break
|
||||
info = _list_info(lines[i])
|
||||
if not info: break
|
||||
level, kind, body = info
|
||||
chunk = [body]; i += 1
|
||||
while i < n and lines[i].strip() and _list_info(lines[i]) is None:
|
||||
t = lines[i].strip()
|
||||
if t.startswith("```") or _HEADING.match(t) or _HR.fullmatch(t): break
|
||||
chunk.append(lines[i]); i += 1
|
||||
items.append((level, kind, chunk))
|
||||
|
||||
def render_list(start: int, min_level: int) -> tuple[str, int]:
|
||||
if start >= len(items) or items[start][0] < min_level:
|
||||
return "", start
|
||||
kind, chunks, idx = items[start][1], [f"<{items[start][1]}>"], start
|
||||
while idx < len(items) and items[idx][0] >= min_level:
|
||||
level, ikind, body_lines = items[idx]
|
||||
if level > min_level:
|
||||
nested, idx = render_list(idx, level)
|
||||
chunks[-1] = (chunks[-1][:-5] + nested + "</li>") if chunks[-1].endswith("</li>") else chunks[-1] + nested
|
||||
continue
|
||||
if ikind != kind:
|
||||
chunks += [f"</{kind}>", f"<{ikind}>"]; kind = ikind
|
||||
idx += 1
|
||||
body = render_inline("\n".join(body_lines), page)
|
||||
nested = ""
|
||||
if idx < len(items) and items[idx][0] > min_level:
|
||||
nested, idx = render_list(idx, min_level + 1)
|
||||
chunks.append(f"<li>{body}{nested}\n</li>" if nested else f"<li>{body}</li>")
|
||||
chunks.append(f"</{kind}>")
|
||||
return "\n".join(chunks), idx
|
||||
|
||||
out.append(render_list(0, items[0][0])[0]); continue
|
||||
|
||||
if s.startswith(">"):
|
||||
q = []
|
||||
while i < n and lines[i].strip().startswith(">"):
|
||||
q.append(re.sub(r"^>\s?", "", lines[i].strip())); i += 1
|
||||
out.append(f"<blockquote>\n<p>{render_inline(chr(10).join(q), page)}</p>\n</blockquote>"); continue
|
||||
|
||||
if s.startswith("<!--") or s.startswith("<!---"):
|
||||
out.append(rewrite_html_urls(line, page)); i += 1
|
||||
# Preserve a blank line after HTML comments (python-markdown does).
|
||||
if i < n and not lines[i].strip():
|
||||
out.append("")
|
||||
while i < n and not lines[i].strip():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
buf = [line]; i += 1
|
||||
while i < n and lines[i].strip():
|
||||
t = lines[i].strip()
|
||||
if t.startswith("```") or _HEADING.match(t) or _HR.fullmatch(t) or t.startswith(">"): break
|
||||
if "|" in lines[i] and i + 1 < n and _is_sep(lines[i + 1]): break
|
||||
buf.append(lines[i]); i += 1
|
||||
out.append(f"<p>{render_inline(chr(10).join(buf), page)}</p>")
|
||||
|
||||
return inject_glossary("\n".join(out), page)
|
||||
|
||||
def page_title(source: str) -> str:
|
||||
for line in source.splitlines():
|
||||
if line.startswith("# "):
|
||||
return line[2:].strip()
|
||||
return "openpilot docs"
|
||||
|
||||
def write_html_redirect(rel: Path) -> None:
|
||||
if rel.name == "index.md":
|
||||
return
|
||||
target = f"{rel.stem}/"
|
||||
out = SITE_DIR / rel.with_suffix(".html")
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text("\n".join([
|
||||
"<!doctype html>",
|
||||
f'<meta http-equiv="refresh" content="0; url={html.escape(target)}">',
|
||||
f'<link rel="canonical" href="{html.escape(target)}">',
|
||||
f"<script>location.replace({json.dumps(target)} + location.search + location.hash)</script>",
|
||||
]))
|
||||
|
||||
def copy_assets() -> None:
|
||||
for src in DOCS_DIR.rglob("*"):
|
||||
if not src.is_file():
|
||||
continue
|
||||
rel = src.relative_to(DOCS_DIR)
|
||||
if any(part in EXCLUDE_DIRS for part in rel.parts):
|
||||
continue
|
||||
if src.suffix == ".md" or src in (Path(__file__).resolve(), TEMPLATE_FILE):
|
||||
continue
|
||||
dest = SITE_DIR / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dest)
|
||||
|
||||
def render_nav_html(current_page: str) -> str:
|
||||
parts: list[str] = []
|
||||
for title, target in NAV:
|
||||
if target is None:
|
||||
parts.append(f'<div class="nav-section">{html.escape(title)}</div>')
|
||||
elif target.startswith(("http://", "https://")):
|
||||
parts.append(f'<a href="{html.escape(target)}">{html.escape(title)}</a>')
|
||||
else:
|
||||
active = ' class="active"' if target == current_page else ""
|
||||
parts.append(f'<a href="{html.escape(page_href(current_page, target))}"{active}>{html.escape(title)}</a>')
|
||||
return "\n".join(parts)
|
||||
|
||||
def build() -> None:
|
||||
template = TEMPLATE_FILE.read_text()
|
||||
pages = [
|
||||
(path.relative_to(DOCS_DIR), path.read_text()) for path in sorted(DOCS_DIR.rglob("*.md"))
|
||||
if path != DOCS_DIR / "README.md"
|
||||
and not any(part in EXCLUDE_DIRS for part in path.relative_to(DOCS_DIR).parts)
|
||||
]
|
||||
pages.append((Path(GLOSSARY_PAGE), f"# openpilot glossary\n\n{GLOSSARY_DEFINITIONS}"))
|
||||
pages.sort()
|
||||
|
||||
if SITE_DIR.exists():
|
||||
shutil.rmtree(SITE_DIR)
|
||||
SITE_DIR.mkdir(parents=True)
|
||||
copy_assets()
|
||||
|
||||
for rel_path, source in pages:
|
||||
rel = rel_path.as_posix()
|
||||
body = render_markdown(source, rel)
|
||||
title = page_title(source)
|
||||
route = page_route(rel)
|
||||
root = "../" * (0 if route == "." else len(route.split("/")))
|
||||
edit_path = "serve.py" if rel == GLOSSARY_PAGE else rel
|
||||
page_html = template
|
||||
for name, value in {
|
||||
"TITLE": html.escape(title),
|
||||
"ROOT": root,
|
||||
"HOME_HREF": page_href(rel, "index.md"),
|
||||
"NAV": render_nav_html(rel),
|
||||
"BODY": body,
|
||||
"EDIT_URL": html.escape(f"{REPO_URL}blob/master/docs/{edit_path}"),
|
||||
}.items():
|
||||
page_html = page_html.replace(f"{{{{{name}}}}}", value)
|
||||
out = SITE_DIR / ("" if route == "." else route) / "index.html"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(page_html)
|
||||
write_html_redirect(rel_path)
|
||||
|
||||
print(f"docs: built {len(pages)} pages into {SITE_DIR}")
|
||||
|
||||
def serve() -> None:
|
||||
build()
|
||||
mtimes = {p: p.stat().st_mtime for p in DOCS_DIR.rglob("*") if p.is_file()}
|
||||
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(SITE_DIR))
|
||||
httpd = http.server.ThreadingHTTPServer(("", 0), handler)
|
||||
print(f"docs: serving on http://localhost:{httpd.server_port}/ (watching for changes)")
|
||||
try:
|
||||
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
new_mtimes = {p: p.stat().st_mtime for p in DOCS_DIR.rglob("*") if p.is_file()}
|
||||
if new_mtimes != mtimes:
|
||||
mtimes = new_mtimes
|
||||
print("docs: change detected, rebuilding...")
|
||||
try:
|
||||
build()
|
||||
except Exception as e:
|
||||
print(f"docs: build failed: {e}")
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Build or serve the openpilot documentation site.")
|
||||
parser.add_argument("--build", action="store_true", help="Build the site and exit.")
|
||||
args = parser.parse_args()
|
||||
if args.build:
|
||||
build()
|
||||
else:
|
||||
serve()
|
||||
@@ -1,42 +0,0 @@
|
||||
.md-logo img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.glossary-term {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-term__label {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
}
|
||||
|
||||
.glossary-term__tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(30rem, 80vw);
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 0.6rem;
|
||||
background: rgb(26 26 26 / 96%);
|
||||
color: white;
|
||||
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(-0.15rem);
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
visibility: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.glossary-term:hover .glossary-term__tooltip,
|
||||
.glossary-term:focus-visible .glossary-term__tooltip,
|
||||
.glossary-term:focus-within .glossary-term__tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}} · openpilot docs</title>
|
||||
<link rel="icon" href="{{ROOT}}assets/favicon.svg">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fff;
|
||||
--bg-elev: #f5f5f5;
|
||||
--bg-hover: #eef0ff;
|
||||
--fg: #262626;
|
||||
--fg-dim: #666;
|
||||
--accent: #4051b5;
|
||||
--border: #e5e5e5;
|
||||
--max-width: 76rem;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { scrollbar-gutter: stable; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
header.site {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
min-height: 3rem;
|
||||
padding: 0.5rem max(1.25rem, calc((100% - var(--max-width)) / 2));
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
}
|
||||
header.site img { display: block; height: 24px; filter: invert(1); }
|
||||
header.site .brand { display: flex; align-items: center; gap: 0.75rem; color: var(--fg); }
|
||||
header.site .brand:hover { text-decoration: none; }
|
||||
header.site .site-name { font-weight: 600; font-size: 1.05rem; }
|
||||
header.site .spacer { flex: 1; }
|
||||
header.site .social { display: flex; gap: 1rem; }
|
||||
header.site .social a { display: flex; color: var(--fg); }
|
||||
header.site .social a:hover { color: var(--accent); }
|
||||
header.site .social svg { width: 1.75rem; height: 1.75rem; fill: currentColor; }
|
||||
header.site .social a[aria-label="Discord"] svg { transform: scale(1.15); }
|
||||
|
||||
.layout { display: flex; width: 100%; max-width: var(--max-width); margin: 0 auto; }
|
||||
|
||||
nav.sidebar {
|
||||
width: 14rem; flex-shrink: 0;
|
||||
padding: 3.3rem 1.25rem 2rem 0;
|
||||
position: sticky; top: 3rem; align-self: start;
|
||||
max-height: calc(100vh - 3rem); overflow-y: auto;
|
||||
}
|
||||
nav.sidebar .nav-section { font-weight: 600; margin: 1.15rem 0.8rem 0.45rem; }
|
||||
nav.sidebar a {
|
||||
display: block; padding: 0.28rem 0.8rem; border-radius: 0.45rem;
|
||||
color: var(--fg); line-height: 1.35;
|
||||
}
|
||||
nav.sidebar a:hover { background: var(--bg-hover); text-decoration: none; }
|
||||
nav.sidebar a.active { background: var(--bg-hover); color: var(--accent); font-weight: 600; }
|
||||
|
||||
main.content { width: min(100%, 46.5rem); min-width: 0; padding: 2.7rem 2rem 5rem; }
|
||||
main.content h1, main.content h2, main.content h3 { line-height: 1.25; margin-top: 1.8rem; }
|
||||
main.content h1 { margin-top: 0; }
|
||||
main.content img { max-width: 100%; }
|
||||
main.content table { border-collapse: collapse; display: block; overflow-x: auto; }
|
||||
main.content th, main.content td { border: 1px solid var(--border); padding: 0.4rem 0.6rem; text-align: left; }
|
||||
main.content th { background: var(--bg-elev); }
|
||||
main.content pre {
|
||||
position: relative;
|
||||
background: var(--bg-elev);
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
main.content code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
main.content :not(pre) > code { background: var(--bg-elev); padding: 0.1rem 0.35rem; border-radius: 0.2rem; font-size: 0.88em; }
|
||||
main.content blockquote { border-left: 3px solid var(--border); margin: 1rem 0; padding: 0.2rem 1rem; color: var(--fg-dim); }
|
||||
main.content details { margin: 0.5rem 0; }
|
||||
main.content hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
||||
.headerlink { margin-left: 0.25rem; opacity: 0; font-size: 0.7em; }
|
||||
h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink { opacity: 1; }
|
||||
|
||||
.edit-link { margin-top: 3rem; font-size: 0.85rem; }
|
||||
|
||||
.copy-btn {
|
||||
position: absolute; top: 0.4rem; right: 0.4rem;
|
||||
background: var(--bg); color: var(--fg-dim);
|
||||
border: 1px solid var(--border); border-radius: 0.35rem;
|
||||
padding: 0.2rem 0.5rem; font-size: 0.78rem; cursor: pointer;
|
||||
opacity: 0; transition: opacity 120ms;
|
||||
}
|
||||
main.content pre:hover .copy-btn { opacity: 1; }
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
nav.sidebar { display: none; }
|
||||
main.content { padding: 2rem 1.25rem 4rem; }
|
||||
}
|
||||
|
||||
.glossary-term {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.glossary-term__label {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
}
|
||||
|
||||
.glossary-term__tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(30rem, 80vw);
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 0.6rem;
|
||||
background: rgb(26 26 26 / 96%);
|
||||
color: white;
|
||||
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(-0.15rem);
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
visibility: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.glossary-term:hover .glossary-term__tooltip,
|
||||
.glossary-term:focus-visible .glossary-term__tooltip,
|
||||
.glossary-term:focus-within .glossary-term__tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site">
|
||||
<a class="brand" href="{{HOME_HREF}}">
|
||||
<img src="{{ROOT}}assets/comma-logo.png" alt="">
|
||||
<span class="site-name">openpilot docs</span>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<div class="social">
|
||||
<a href="https://github.com/commaai" aria-label="GitHub">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
||||
M12 .7A11.5 11.5 0 0 0 8.4 23c.6.1.8-.2.8-.5v-2c-3.3.7-4-1.4-4-1.4-.5-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7
|
||||
1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.7 0-1.3.5-2.3 1.2-3.1
|
||||
-.1-.3-.5-1.6.1-3.1 0 0 1-.3 3.2 1.2a11 11 0 0 1 5.8 0C15.8 6 16.8 6.3 16.8 6.3c.6 1.5.2 2.8.1 3.1
|
||||
.8.8 1.2 1.8 1.2 3.1 0 4.4-2.8 5.4-5.5 5.7.4.4.8 1.1.8 2.2v2.1c0 .3.2.6.8.5A11.5 11.5 0 0 0 12 .7Z"/></svg>
|
||||
</a>
|
||||
<a href="https://discord.comma.ai" aria-label="Discord">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
||||
M20.3 4.4A16 16 0 0 0 16.3 3l-.5 1.1a15 15 0 0 0-7.6 0L7.7 3a16 16 0 0 0-4 1.4C1.1 8.2.4 11.9.8 15.5
|
||||
a16 16 0 0 0 4.9 2.6l1.2-1.7-1.8-.9.4-.3c3.5 1.6 9.4 1.6 13 0l.4.3-1.8.9 1.2 1.7a16 16 0 0 0 4.9-2.6
|
||||
c.5-4.2-.8-7.8-2.9-11.1ZM8.3 13.3c-1.1 0-1.9-1-1.9-2.2 0-1.3.8-2.3 1.9-2.3s2 1 1.9 2.3c0 1.2-.8 2.2-1.9 2.2Z
|
||||
m7.4 0c-1.1 0-1.9-1-1.9-2.2 0-1.3.8-2.3 1.9-2.3s2 1 1.9 2.3c0 1.2-.8 2.2-1.9 2.2Z"/></svg>
|
||||
</a>
|
||||
<a href="https://x.com/comma_ai" aria-label="X">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
||||
M18.2 2.3h3.3l-7.2 8.3 8.5 11.2h-6.7l-5.2-6.8-6 6.8H1.6l7.8-8.9L1.2 2.3h6.8l4.7 6.2 5.5-6.2Z
|
||||
m-1.2 17.5h1.8L7 4.2H5Z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
{{NAV}}
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
{{BODY}}
|
||||
<div class="edit-link">
|
||||
<a href="{{EDIT_URL}}">Edit this page on GitHub</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('pre').forEach(function (el) {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.textContent = 'copy';
|
||||
btn.addEventListener('click', function () {
|
||||
var code = el.querySelector('code');
|
||||
if (!code) return;
|
||||
navigator.clipboard.writeText(code.innerText).then(function () {
|
||||
btn.textContent = 'copied';
|
||||
setTimeout(function () { btn.textContent = 'copy'; }, 1200);
|
||||
});
|
||||
});
|
||||
el.appendChild(btn);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -50,10 +50,6 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
docs = [
|
||||
"zensical",
|
||||
]
|
||||
|
||||
testing = [
|
||||
"coverage", # line coverage
|
||||
"ty", # type checking
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""
|
||||
wrapper that materializes symlinks in docs/ before build
|
||||
|
||||
we can delete this once zensical supports symlinks:
|
||||
https://github.com/zensical/backlog/issues/55
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DOCS_DIR = REPO_ROOT / "docs"
|
||||
SITE_DIR = REPO_ROOT / "docs_site"
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
# Local docs build helpers live under docs/ so they stay near the content
|
||||
# source. The wrapper prunes them from docs_site/ after build.
|
||||
sys.path.insert(0, str(DOCS_DIR))
|
||||
|
||||
|
||||
def _materialize(docs: Path) -> dict[Path, str]:
|
||||
originals: dict[Path, str] = {}
|
||||
for link in docs.rglob("*"):
|
||||
if not link.is_symlink():
|
||||
continue
|
||||
target = link.resolve()
|
||||
if not target.is_file():
|
||||
continue
|
||||
originals[link] = os.readlink(link)
|
||||
link.unlink()
|
||||
shutil.copy2(target, link)
|
||||
return originals
|
||||
|
||||
|
||||
def _restore(originals: dict[Path, str]) -> None:
|
||||
for link, target in originals.items():
|
||||
link.unlink(missing_ok=True)
|
||||
os.symlink(target, link)
|
||||
|
||||
|
||||
def _raise_interrupt(*_):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def _prune_site_output() -> None:
|
||||
shutil.rmtree(SITE_DIR / "ext", ignore_errors=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
signal.signal(signal.SIGTERM, _raise_interrupt)
|
||||
originals = _materialize(DOCS_DIR)
|
||||
try:
|
||||
from zensical.main import cli
|
||||
cli(standalone_mode=False)
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "build":
|
||||
_prune_site_output()
|
||||
finally:
|
||||
_restore(originals)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -62,18 +62,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespell"
|
||||
version = "2.4.2"
|
||||
@@ -350,15 +338,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/19/31aa63ab719b2e1eea5f200a8a54e2591dc1966a6b75e3de30ef8be9bc2c/cython-3.2.8-py3-none-any.whl", hash = "sha256:f635e113677666de13a2ec2979e9b1d5b90617cdfd1a691d3559be81e2dd6cb9", size = 1258688, upload-time = "2026-06-30T07:41:55.624Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deepmerge"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "2.1.2"
|
||||
@@ -430,18 +409,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.5.0"
|
||||
@@ -520,25 +487,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.11.0"
|
||||
@@ -690,7 +638,7 @@ dev = [
|
||||
{ name = "matplotlib" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "zensical" },
|
||||
{ name = "markdown" },
|
||||
]
|
||||
submodules = [
|
||||
{ name = "msgq" },
|
||||
@@ -739,6 +687,7 @@ requires-dist = [
|
||||
{ name = "coverage", marker = "extra == 'testing'" },
|
||||
{ name = "inputs" },
|
||||
{ name = "jeepney" },
|
||||
{ name = "markdown", marker = "extra == 'docs'" },
|
||||
{ name = "matplotlib", marker = "extra == 'dev'" },
|
||||
{ name = "msgq", marker = "extra == 'submodules'", editable = "msgq_repo" },
|
||||
{ name = "numpy", specifier = ">=2.0" },
|
||||
@@ -763,7 +712,6 @@ requires-dist = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "ty", marker = "extra == 'testing'" },
|
||||
{ name = "websocket-client" },
|
||||
{ name = "zensical", marker = "extra == 'docs'" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
provides-extras = ["docs", "testing", "dev", "tools", "submodules"]
|
||||
@@ -908,19 +856,6 @@ crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "11.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.3.2"
|
||||
@@ -971,24 +906,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyzmq"
|
||||
version = "27.1.0"
|
||||
@@ -1261,24 +1178,6 @@ requires-dist = [
|
||||
]
|
||||
provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs", "mesa"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.68.3"
|
||||
@@ -1334,36 +1233,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zensical"
|
||||
version = "0.0.46"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "deepmerge" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markdown" },
|
||||
{ name = "pygments" },
|
||||
{ name = "pymdown-extensions" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tomli" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/57/c7bbb71f943e1e0ba5ce460f4930ec836ead7286969e7fd742f7a6c049ab/zensical-0.0.46.tar.gz", hash = "sha256:3ec21f4fb1e78cd7c0d6b07ae336b04770e27ba020dabc457b2790e5d34f1978", size = 3973968, upload-time = "2026-06-21T18:52:40.368Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/bd/bbc499ee35ac9ec5459dbfec7bb7231556689e97eaa13a5eddbe1f0443b5/zensical-0.0.46-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d91af81ab058c8693dfd75f2f77b4c73bcba4125681d1d276f38624291820bd2", size = 12796482, upload-time = "2026-06-21T18:52:07.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/1b/7acc273184d59b8e894d15ebe3cf1c5e81b3a822fde1792ea3e33be37a2e/zensical-0.0.46-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d9221264a9a87409900a47e29985607b0c9245dacb89077e87c8e16e31edc167", size = 12660030, upload-time = "2026-06-21T18:52:10.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/df/bd0a68de98a19fc6050c58be11f36d05ea72a213b6a7ff7395d33c793747/zensical-0.0.46-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec43018d5343ca2e1d71aa352eeddd560fef504effd03025840a5a783abefa4f", size = 13057130, upload-time = "2026-06-21T18:52:12.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/db/e27635f5787a42245f900e658340698a6654e165d466f9a3b640efced2cd/zensical-0.0.46-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26e98fb8ab7ab50cdd20a73e2c7d4d9aae0b46cf2d8691e6bb22f9c261b8a60a", size = 13022345, upload-time = "2026-06-21T18:52:15.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9d/6ce2ba11c97154870b458a8dae4637ade93b7097912f0102f5ea7fe8cf5b/zensical-0.0.46-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46fe578f26963f8ee89567983e62737b6fadc9197d4742e1020b522e092d7baa", size = 13377445, upload-time = "2026-06-21T18:52:18.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/06/9930d43cd9d2f899b648d63491007c1b4f9716cf118b0c98e867b933069c/zensical-0.0.46-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aef03fa186a5589148e10b62610500989c6b075a2c08e1554233adbf91b2a3dc", size = 13086749, upload-time = "2026-06-21T18:52:21.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/ed/2342cf860fbb02314938b0d1f1b02344935801b04d185ff3151ef1812898/zensical-0.0.46-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bc7446cdf97a8dea390f20ed2bd6b030cddc1bd36a8ce113ea3efef6fa61c573", size = 13231120, upload-time = "2026-06-21T18:52:24.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/b0/d2ece02f63cd767fcf10fd7608dc8e0a995f87dc5261209b1dbc296fd57b/zensical-0.0.46-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:bbee37801f1ed500f158dc0992c569282950f780ae353c37fe6969f99983d701", size = 13295035, upload-time = "2026-06-21T18:52:26.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/b2/cb0048a612e63e615399fc507472a557d1c5b7c2f74065c5bf11998fd597/zensical-0.0.46-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:9487c147c9cceb50c04d0ad70b024821a6eab1629dafd70ab6d1e86ec841e623", size = 13437191, upload-time = "2026-06-21T18:52:29.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/16/515f81db8055b109a510063be481e60a657c4fad1a883680b2ee4aa9a424/zensical-0.0.46-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f42a4683c762f026878d19ede4bcf7bfbb84dbecb5ad923949abb77806ed88a5", size = 13369382, upload-time = "2026-06-21T18:52:32.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/5c/da54ee65b642eb7d88dd4a3db35845d0765915638e05d5d434a10b42f1c3/zensical-0.0.46-cp310-abi3-win32.whl", hash = "sha256:85f018f2a7ee76a83915c87ddb12b58cf343fd6154081d33ac95b6751b011dd7", size = 12354298, upload-time = "2026-06-21T18:52:34.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/26/fc7ef081acbdada8436825221cb728ee84a81d4d78a7bb79aa58bd150d31/zensical-0.0.46-cp310-abi3-win_amd64.whl", hash = "sha256:1543a693a160de60e86ca589592401b584670e7e12c5ae30e3c2ba76786f7ec3", size = 12599687, upload-time = "2026-06-21T18:52:37.913Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstandard"
|
||||
version = "0.25.0"
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
[project]
|
||||
site_name = "openpilot docs"
|
||||
site_url = "https://docs.comma.ai"
|
||||
repo_url = "https://github.com/commaai/openpilot/"
|
||||
|
||||
docs_dir = "docs"
|
||||
site_dir = "docs_site/"
|
||||
|
||||
extra_css = ["stylesheets/extra.css"]
|
||||
|
||||
nav = [
|
||||
{ "What is openpilot?" = "index.md" },
|
||||
{ "How-to" = [
|
||||
{ "Turn the speed blue" = "how-to/turn-the-speed-blue.md" },
|
||||
{ "Connect to a comma 3X or four" = "how-to/connect-to-comma.md" },
|
||||
{ "Add support for a car" = "how-to/car-port.md" },
|
||||
] },
|
||||
{ "Concepts" = [
|
||||
{ "Logs" = "concepts/logs.md" },
|
||||
{ "Safety" = "concepts/safety.md" },
|
||||
{ "Glossary" = "concepts/glossary.md" },
|
||||
] },
|
||||
{ "Contributing" = [
|
||||
{ "Feedback" = "contributing/feedback.md" },
|
||||
{ "Roadmap" = "contributing/roadmap.md" },
|
||||
{ "Contributing Guide →" = "https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md" },
|
||||
] },
|
||||
{ "Links" = [
|
||||
{ "Blog →" = "https://blog.comma.ai" },
|
||||
{ "Bounties →" = "https://comma.ai/bounties" },
|
||||
{ "GitHub →" = "https://github.com/commaai" },
|
||||
{ "Discord →" = "https://discord.comma.ai" },
|
||||
{ "X →" = "https://x.com/comma_ai" },
|
||||
] },
|
||||
]
|
||||
|
||||
[project.theme]
|
||||
logo = "assets/comma-logo.png"
|
||||
features = [
|
||||
"navigation.expand",
|
||||
"navigation.sections",
|
||||
"navigation.instant",
|
||||
"navigation.instant.prefetch",
|
||||
"content.code.copy",
|
||||
"content.action.edit",
|
||||
"content.action.view",
|
||||
]
|
||||
|
||||
[[project.extra.social]]
|
||||
icon = "fontawesome/brands/github"
|
||||
link = "https://github.com/commaai"
|
||||
|
||||
[[project.extra.social]]
|
||||
icon = "fontawesome/brands/discord"
|
||||
link = "https://discord.comma.ai"
|
||||
|
||||
[[project.extra.social]]
|
||||
icon = "fontawesome/brands/x-twitter"
|
||||
link = "https://x.com/comma_ai"
|
||||
|
||||
[project.markdown_extensions.attr_list]
|
||||
|
||||
[project.markdown_extensions.admonition]
|
||||
|
||||
[project.markdown_extensions.md_in_html]
|
||||
|
||||
[project.markdown_extensions.pymdownx.highlight]
|
||||
anchor_linenums = true
|
||||
line_spans = "__span"
|
||||
pygments_lang_class = true
|
||||
|
||||
[project.markdown_extensions.pymdownx.inlinehilite]
|
||||
|
||||
[project.markdown_extensions.pymdownx.magiclink]
|
||||
|
||||
[project.markdown_extensions.pymdownx.superfences]
|
||||
custom_fences = [{ name = "mermaid", class = "mermaid" }]
|
||||
|
||||
[project.markdown_extensions.pymdownx.details]
|
||||
|
||||
[project.markdown_extensions."ext.glossary"]
|
||||
Reference in New Issue
Block a user