diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 23a89de1c..f618c1bdf 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -29,9 +29,9 @@ jobs: # Build - name: Build docs run: | - # TODO: can we install just the "docs" dependency group without the normal deps? - pip install mkdocs - mkdocs build + git lfs pull + pip install zensical + python scripts/docs.py build # Push to docs.comma.ai - uses: actions/checkout@v6 diff --git a/.gitignore b/.gitignore index c4022a865..689d1d6c2 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ compare_runtime*.html selfdrive/modeld/models/tg_compiled_flags.json # build artifacts +docs_site/ selfdrive/pandad/pandad cereal/services.h cereal/gen diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 000000000..e803a3fb8 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,24 @@ +# 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/ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 12d0b6f5d..000000000 --- a/docs/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# openpilot docs - -This 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). - -## Development -NOTE: 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 -mkdocs build -``` - -**3. Run the new site locally** -``` bash -mkdocs serve -``` - -References: -* https://www.mkdocs.org/getting-started/ -* https://github.com/ntno/mkdocs-terminal diff --git a/docs/assets/comma-logo.png b/docs/assets/comma-logo.png new file mode 120000 index 000000000..2838d92bf --- /dev/null +++ b/docs/assets/comma-logo.png @@ -0,0 +1 @@ +../../selfdrive/assets/icons_mici/settings/comma_icon.png \ No newline at end of file diff --git a/docs/concepts/glossary.md b/docs/concepts/glossary.md index 3bfe71bcb..4f4dd5475 100644 --- a/docs/concepts/glossary.md +++ b/docs/concepts/glossary.md @@ -1,9 +1,3 @@ # openpilot 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**: this is 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_DEFINITIONS}} diff --git a/docs/css/tooltip.css b/docs/css/tooltip.css deleted file mode 100644 index b9a54f793..000000000 --- a/docs/css/tooltip.css +++ /dev/null @@ -1,44 +0,0 @@ -[data-tooltip] { - position: relative; - display: inline-block; - border-bottom: 1px dotted black; -} - -[data-tooltip] .tooltip-content { - width: max-content; - max-width: 25em; - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - background-color: white; - color: #404040; - box-shadow: 0 4px 14px 0 rgba(0,0,0,.2), 0 0 0 1px rgba(0,0,0,.05); - padding: 10px; - font: 14px/1.5 Lato, proxima-nova, Helvetica Neue, Arial, sans-serif; - text-decoration: none; - opacity: 0; - visibility: hidden; - transition: opacity 0.1s, visibility 0s; - z-index: 1000; - pointer-events: none; /* Prevent accidental interaction */ -} - -[data-tooltip]:hover .tooltip-content { - opacity: 1; - visibility: visible; - pointer-events: auto; /* Allow interaction when visible */ -} - -.tooltip-content .tooltip-glossary-link { - display: inline-block; - margin-top: 8px; - font-size: 12px; - color: #007bff; - text-decoration: none; -} - -.tooltip-content .tooltip-glossary-link:hover { - color: #0056b3; - text-decoration: underline; -} diff --git a/docs/ext/glossary.py b/docs/ext/glossary.py new file mode 100644 index 000000000..d4c3ebd8f --- /dev/null +++ b/docs/ext/glossary.py @@ -0,0 +1,215 @@ +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 LinksProcessor + +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"(?**{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, LinksProcessor): + 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_at = 0 if isinstance(pieces[0], str) else -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) diff --git a/docs/ext/glossary.toml b/docs/ext/glossary.toml new file mode 100644 index 000000000..62408d9dd --- /dev/null +++ b/docs/ext/glossary.toml @@ -0,0 +1,8 @@ +[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)." diff --git a/docs/glossary.toml b/docs/glossary.toml deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/hooks/glossary.py b/docs/hooks/glossary.py deleted file mode 100644 index e2fa3d51e..000000000 --- a/docs/hooks/glossary.py +++ /dev/null @@ -1,68 +0,0 @@ -import re -import tomllib - -def load_glossary(file_path="docs/glossary.toml"): - with open(file_path, "rb") as f: - glossary_data = tomllib.load(f) - return glossary_data.get("glossary", {}) - -def generate_anchor_id(name): - return name.replace(" ", "-").replace("_", "-").lower() - -def format_markdown_term(name, definition): - anchor_id = generate_anchor_id(name) - markdown = f"* [**{name.replace('_', ' ').title()}**](#{anchor_id})" - if definition.get("abbreviation"): - markdown += f" *({definition['abbreviation']})*" - if definition.get("description"): - markdown += f": {definition['description']}\n" - return markdown - -def glossary_markdown(vocabulary): - markdown = "" - for category, terms in vocabulary.items(): - markdown += f"## {category.replace('_', ' ').title()}\n\n" - for name, definition in terms.items(): - markdown += format_markdown_term(name, definition) - return markdown - -def format_tooltip_html(term_key, definition, html): - display_term = term_key.replace("_", " ").title() - clean_description = re.sub(r"\[(.+)]\(.+\)", r"\1", definition["description"]) - glossary_link = ( - f"GlossaryπŸ”—" - ) - return re.sub( - re.escape(display_term), - lambda - match: f"{match.group(0)}{clean_description} {glossary_link}", - html, - flags=re.IGNORECASE, - ) - -def apply_tooltip(_term_key, _definition, pattern, html): - return re.sub( - pattern, - lambda match: format_tooltip_html(_term_key, _definition, match.group(0)), - html, - flags=re.IGNORECASE, - ) - -def tooltip_html(vocabulary, html): - for _category, terms in vocabulary.items(): - for term_key, definition in terms.items(): - if definition.get("description"): - pattern = rf"(?)(?!\([^)]*\))" - html = apply_tooltip(term_key, definition, pattern, html) - return html - -# Page Hooks -def on_page_markdown(markdown, **kwargs): - glossary = load_glossary() - return markdown.replace("{{GLOSSARY_DEFINITIONS}}", glossary_markdown(glossary)) - -def on_page_content(html, **kwargs): - if kwargs.get("page").title == "Glossary": - return html - glossary = load_glossary() - return tooltip_html(glossary, html) diff --git a/docs/index.md b/docs/index.md deleted file mode 120000 index 74ea27aee..000000000 --- a/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -getting-started/what-is-openpilot.md \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 000000000..36ce354af --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,42 @@ +.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; +} diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 550f807ac..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,44 +0,0 @@ -site_name: openpilot docs -repo_url: https://github.com/commaai/openpilot/ -site_url: https://docs.comma.ai - -exclude_docs: README.md - -strict: true -docs_dir: docs -site_dir: docs_site/ - -hooks: - - docs/hooks/glossary.py -extra_css: - - css/tooltip.css -theme: - name: readthedocs - navigation_depth: 3 - -nav: - - Getting Started: - - What is openpilot?: getting-started/what-is-openpilot.md - - How-to: - - Turn the speed blue: how-to/turn-the-speed-blue.md - - Connect to a comma 3X: how-to/connect-to-comma.md - # - Make your first pull request: how-to/make-first-pr.md - #- Replay a drive: how-to/replay-a-drive.md - - Concepts: - - Logs: concepts/logs.md - - Safety: concepts/safety.md - - Glossary: concepts/glossary.md - - Car Porting: - - What is a car port?: car-porting/what-is-a-car-port.md - - Porting a car brand: car-porting/brand-port.md - - Porting a car model: car-porting/model-port.md - - Contributing: - - Roadmap: contributing/roadmap.md - #- Architecture: contributing/architecture.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 diff --git a/pyproject.toml b/pyproject.toml index e595106b5..d7af83d40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ dependencies = [ [project.optional-dependencies] docs = [ "Jinja2", - "mkdocs", + "zensical", ] testing = [ diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100644 index 000000000..d60bfb791 --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,63 @@ +""" + 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() diff --git a/uv.lock b/uv.lock index cde094a85..f8278c136 100644 --- a/uv.lock +++ b/uv.lock @@ -359,6 +359,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, ] +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -434,18 +443,6 @@ name = "gcc-arm-none-eabi" version = "13.2.1" source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi#0e1ae2548977f6cd78c51d4d0c16ebd1863241b8" } -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - [[package]] name = "git-lfs" version = "3.6.1" @@ -655,15 +652,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, ] -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - [[package]] name = "metadrive-simulator" version = "0.4.2.3" @@ -674,44 +662,6 @@ dependencies = [ { name = "panda3d-gltf" }, ] -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, -] - [[package]] name = "mpmath" version = "1.3.0" @@ -849,7 +799,7 @@ dev = [ ] docs = [ { name = "jinja2" }, - { name = "mkdocs" }, + { name = "zensical" }, ] testing = [ { name = "codespell" }, @@ -899,7 +849,6 @@ requires-dist = [ { name = "libyuv", git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" }, - { name = "mkdocs", marker = "extra == 'docs'" }, { name = "ncurses", git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses" }, { name = "numpy", specifier = ">=2.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, @@ -932,6 +881,7 @@ requires-dist = [ { name = "ty", marker = "extra == 'testing'" }, { name = "websocket-client" }, { name = "xattr" }, + { name = "zensical", marker = "extra == 'docs'" }, { name = "zeromq", git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq" }, { name = "zstandard" }, { name = "zstd", git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd" }, @@ -986,15 +936,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/5d/3744c6550dddf933785a37cdd4a9921fe13284e6d115b5a2637fe390f158/panda3d_simplepbr-0.13.1-py3-none-any.whl", hash = "sha256:cda41cb57cff035b851646956cfbdcc408bee42511dabd4f2d7bd4fbf48c57a9", size = 2457097, upload-time = "2025-03-30T16:57:39.729Z" }, ] -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - [[package]] name = "pillow" version = "12.2.0" @@ -1014,15 +955,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, ] -[[package]] -name = "platformdirs" -version = "4.9.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1186,6 +1118,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + [[package]] name = "pyopenssl" version = "26.0.0" @@ -1322,18 +1267,6 @@ wheels = [ { 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 = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - [[package]] name = "pyzmq" version = "27.1.0" @@ -1589,27 +1522,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - [[package]] name = "websocket-client" version = "1.9.0" @@ -1669,6 +1581,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] +[[package]] +name = "zensical" +version = "0.0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/c2/dea4b86dc1ca2a7b55414017f12cfb12b5cfdf3a1ed7c77a04c271eb523b/zensical-0.0.33.tar.gz", hash = "sha256:05209cb4f80185c533e0d37c25d084ddc2050e3d5a4dd1b1812961c2ee0c3380", size = 3892278, upload-time = "2026-04-14T11:08:19.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/5f/45d5200405420a9d8ac91cf9e7826622ea12f3198e8e6ac4ffb481eb53bf/zensical-0.0.33-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f658e3c241cfbb560bd8811116a9486cff7e04d7d5aed73569dd533c74187450", size = 12416748, upload-time = "2026-04-14T11:07:43.246Z" }, + { url = "https://files.pythonhosted.org/packages/33/1e/aadaf31d6e4d20419ecedaf0b1c804e359ec23dcdb44c8d2bf6d8407080c/zensical-0.0.33-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f9813ac3256c28e2e2f1ba5c9fab1b4bca62bbe0e0f8e85ac22d33b068b1b08a", size = 12293372, upload-time = "2026-04-14T11:07:46.569Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/838be8451ea8b2aecec39fbec3971060fc705e17f5741249740d9b6a6824/zensical-0.0.33-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bad7ac71028769c5d1f3f84f448dbb7352db28d77095d1b40a8d1b0aa34ec30", size = 12659832, upload-time = "2026-04-14T11:07:50.754Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5c/dd957d7c83efc13a70a6058d4190a3afcf29942aefb391120bca5466347d/zensical-0.0.33-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06bb039daf044547c9400a52f9493b3cd486ba9baef3324fdcffd2e26e61105f", size = 12603847, upload-time = "2026-04-14T11:07:53.698Z" }, + { url = "https://files.pythonhosted.org/packages/b7/99/dd6ccc392ece1f34fb20ea339a01717badbbeb2fba1d4f3019a5028d0bcc/zensical-0.0.33-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:260238062b3139ece0edab93f4dbe7a12923453091f5aa580dfd73e799388076", size = 12956236, upload-time = "2026-04-14T11:07:56.728Z" }, + { url = "https://files.pythonhosted.org/packages/f4/76/e0a1b884eadf6afa7e2d56c90c268eec36836ac27e96ef250c0129e55417/zensical-0.0.33-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dff0f4afda7b8586bc4ab2a5684bce5b282232dd4e0cad3be4c73fedd264425", size = 12701944, upload-time = "2026-04-14T11:07:59.928Z" }, + { url = "https://files.pythonhosted.org/packages/38/38/e1ff13461e406864fa2b23fc828822659a7dbac5c79398f724d17f088540/zensical-0.0.33-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:207b4d81b208d75b97dc7bd318804550b886a3e852ef67429ef0e6b9442839d1", size = 12835444, upload-time = "2026-04-14T11:08:02.998Z" }, + { url = "https://files.pythonhosted.org/packages/41/04/7d24d52d6903fc5c511633afe8b5716fef19da09685327665cc127f61648/zensical-0.0.33-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:06d2f57f7bc8cc8fd904386020ea1365eebc411e8698a871e9525c885abca574", size = 12878419, upload-time = "2026-04-14T11:08:06.054Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ec/87fc9e360c694ab006363c7834639eccafd0d26a487cd63dd609bd68f36a/zensical-0.0.33-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:c2851b82d83aa0b2ae4f8e99731cfeedeecebfa04e6b3fc4d375deca629fa240", size = 13022474, upload-time = "2026-04-14T11:08:09.007Z" }, + { url = "https://files.pythonhosted.org/packages/10/b3/0bf174ab6ceedb31d9af462073b5339c894b2084a27d42cb9f0906050d76/zensical-0.0.33-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90daaf512b0429d7b9147ad5e6085b455d24803eff18b508aed738ca65444683", size = 12975233, upload-time = "2026-04-14T11:08:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a9/27/7cc3c2d284698647f60f3b823e0101e619c87edf158d47ee11bf4bfb6228/zensical-0.0.33-cp310-abi3-win32.whl", hash = "sha256:2701820597fe19361a12371129927c58c19633dcaa5f6986d610dce58cecd8c4", size = 12012664, upload-time = "2026-04-14T11:08:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/25/0b/6be5c2fdaf9f1600577e7ba5e235d86b72a26f6af389efb146f978f76ac3/zensical-0.0.33-cp310-abi3-win_amd64.whl", hash = "sha256:a5a0911b4247708a55951b74c459f4d5faec5daaf287d23a2e1f0d96be1e647f", size = 12206255, upload-time = "2026-04-14T11:08:17.375Z" }, +] + [[package]] name = "zeromq" version = "4.3.5" diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 000000000..f08ee05c6 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,67 @@ +[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 = [ + { "Home" = "index.md" }, + { "Getting Started" = [ + { "What is openpilot?" = "getting-started/what-is-openpilot.md" }, + ] }, + { "How-to" = [ + { "Turn the speed blue" = "how-to/turn-the-speed-blue.md" }, + { "Connect to a comma 3X" = "how-to/connect-to-comma.md" }, + ] }, + { "Concepts" = [ + { "Logs" = "concepts/logs.md" }, + { "Safety" = "concepts/safety.md" }, + { "Glossary" = "concepts/glossary.md" }, + ] }, + { "Car Porting" = [ + { "What is a car port?" = "car-porting/what-is-a-car-port.md" }, + { "Porting a car brand" = "car-porting/brand-port.md" }, + { "Porting a car model" = "car-porting/model-port.md" }, + ] }, + { "Contributing" = [ + { "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."ext.glossary"]