mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-10-01 03:53:45 +08:00
more
This commit is contained in:
+24
-33
@@ -1,12 +1,12 @@
|
||||
"""MkDocs Material -> Discourse-compatible Markdown converter.
|
||||
"""Zensical -> Discourse-compatible Markdown converter.
|
||||
|
||||
Converts MkDocs Material syntax to Discourse-friendly markdown:
|
||||
Converts Zensical/MkDocs Material syntax to Discourse-friendly markdown:
|
||||
1. Strip YAML front matter
|
||||
2. Convert admonitions (!!!/???/???+) to Discourse callouts (> [!TYPE])
|
||||
3. Convert Material tabs (=== "Tab Name") to bold headings + ---
|
||||
4. Strip grid card HTML (<div class="grid cards" markdown>)
|
||||
5. Convert Material emoji shortcodes (:material-*:) to Unicode or strip
|
||||
6. Resolve internal .md links to docs site URLs
|
||||
6. Resolve internal .md links to Discourse search URLs (via docs-sync-id)
|
||||
7. Clean up excessive blank lines
|
||||
"""
|
||||
|
||||
@@ -14,9 +14,12 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
DOCS_BASE_URL = os.environ.get("DOCS_BASE_URL", "https://docs.sunnypilot.ai")
|
||||
DISCOURSE_FORUM_URL = os.environ.get(
|
||||
"DISCOURSE_URL", "https://community.sunnypilot.ai"
|
||||
).rstrip("/")
|
||||
|
||||
ADMONITION_MAP: dict[str, str] = {
|
||||
"note": "NOTE",
|
||||
@@ -64,19 +67,18 @@ _EMOJI_SHORTCODE_RE = re.compile(r":material-[\w-]+:")
|
||||
_EXCESSIVE_BLANKS_RE = re.compile(r"\n{4,}")
|
||||
|
||||
|
||||
def convert(content: str, *, file_path: str, docs_base_url: str | None = None) -> str:
|
||||
"""Convert MkDocs Material markdown to Discourse-compatible markdown.
|
||||
def convert(content: str, *, file_path: str) -> str:
|
||||
"""Convert Zensical markdown to Discourse-compatible markdown.
|
||||
|
||||
Args:
|
||||
content: Raw markdown content.
|
||||
file_path: Path to the source file (relative to docs_sp/ or absolute).
|
||||
file_path: Path to the source file relative to docs_sp/
|
||||
(e.g. "getting-started/what-is-sunnypilot.md").
|
||||
Used for resolving relative internal links.
|
||||
docs_base_url: Base URL for the docs site. Defaults to DOCS_BASE_URL env var.
|
||||
|
||||
Returns:
|
||||
Converted markdown string.
|
||||
"""
|
||||
base_url = docs_base_url or DOCS_BASE_URL
|
||||
result = content
|
||||
|
||||
result = strip_front_matter(result)
|
||||
@@ -84,7 +86,7 @@ def convert(content: str, *, file_path: str, docs_base_url: str | None = None) -
|
||||
result = convert_tabs(result)
|
||||
result = convert_grid_cards(result)
|
||||
result = convert_emoji_shortcodes(result)
|
||||
result = resolve_internal_links(result, file_path=file_path, docs_base_url=base_url)
|
||||
result = resolve_internal_links(result, file_path=file_path)
|
||||
result = clean_blank_lines(result)
|
||||
|
||||
return result.strip() + "\n"
|
||||
@@ -235,13 +237,13 @@ def convert_emoji_shortcodes(content: str) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def resolve_internal_links(
|
||||
content: str, *, file_path: str, docs_base_url: str
|
||||
) -> str:
|
||||
"""Resolve internal .md links to docs site URLs.
|
||||
def resolve_internal_links(content: str, *, file_path: str) -> str:
|
||||
"""Resolve internal .md links to Discourse search URLs via docs-sync-id.
|
||||
|
||||
Converts: [text](../features/icbm.md) -> [text](https://docs.sunnypilot.ai/features/icbm/)
|
||||
Converts: [text](../features/icbm.md#section) -> [text](https://docs.sunnypilot.ai/features/icbm/#section)
|
||||
Converts: [text](../features/icbm.md) ->
|
||||
[text](https://community.sunnypilot.ai/search?q=%22docs-sync-id%3A+features/icbm.md%22)
|
||||
|
||||
Anchors are stripped since Discourse search cannot target sections.
|
||||
"""
|
||||
current_dir = str(Path(file_path).parent)
|
||||
|
||||
@@ -251,27 +253,16 @@ def resolve_internal_links(
|
||||
if raw_path.startswith("http"):
|
||||
return match.group(0)
|
||||
|
||||
# Split path and optional anchor
|
||||
anchor = ""
|
||||
# Strip anchor (Discourse search cannot target sections)
|
||||
if "#" in raw_path:
|
||||
raw_path, anchor = raw_path.rsplit("#", 1)
|
||||
anchor = f"#{anchor}"
|
||||
raw_path = raw_path.rsplit("#", 1)[0]
|
||||
|
||||
# Resolve relative path from current file's directory
|
||||
resolved = Path(current_dir, raw_path).resolve()
|
||||
resolved_str = str(resolved)
|
||||
resolved = os.path.normpath(os.path.join(current_dir, raw_path))
|
||||
|
||||
# Extract docs-relative path
|
||||
docs_sp_idx = resolved_str.find("docs_sp/")
|
||||
if docs_sp_idx >= 0:
|
||||
docs_relative = resolved_str[docs_sp_idx + len("docs_sp/"):]
|
||||
else:
|
||||
# Fallback: use the resolved path relative to file
|
||||
docs_relative = raw_path
|
||||
|
||||
# Convert to URL: remove .md, add trailing slash
|
||||
url_path = re.sub(r"\.md$", "/", docs_relative)
|
||||
return f"]({docs_base_url}/{url_path}{anchor})"
|
||||
# Build Discourse search URL for the sync ID
|
||||
query = urllib.parse.quote(f'"docs-sync-id: {resolved}"', safe="")
|
||||
return f"]({DISCOURSE_FORUM_URL}/search?q={query})"
|
||||
|
||||
return _INTERNAL_LINK_RE.sub(_replace_link, content)
|
||||
|
||||
|
||||
@@ -353,11 +353,13 @@ def test_internal_link_relative():
|
||||
input_text = "See [ICBM](../features/cruise/icbm.md) for details."
|
||||
result = resolve_internal_links(
|
||||
input_text,
|
||||
file_path="docs_sp/settings/cruise/speed-limit.md",
|
||||
docs_base_url="https://docs.sunnypilot.ai",
|
||||
file_path="settings/cruise/speed-limit.md",
|
||||
)
|
||||
assert "/search?q=" in result, f"FAIL internal_link missing search URL:\n{result!r}"
|
||||
assert "docs-sync-id" in result, f"FAIL internal_link missing sync-id:\n{result!r}"
|
||||
assert "features%2Fcruise%2Ficbm.md" in result or "features/cruise/icbm.md" in result, (
|
||||
f"FAIL internal_link wrong path:\n{result!r}"
|
||||
)
|
||||
assert "https://docs.sunnypilot.ai/" in result, f"FAIL internal_link:\n{result!r}"
|
||||
assert ".md" not in result.split("](")[1], f"FAIL internal_link still has .md:\n{result!r}"
|
||||
print(" PASS: internal_link_relative")
|
||||
|
||||
|
||||
@@ -365,11 +367,12 @@ def test_internal_link_with_anchor():
|
||||
input_text = "See [section](./safety.md#driver-responsibility)."
|
||||
result = resolve_internal_links(
|
||||
input_text,
|
||||
file_path="docs_sp/safety/index.md",
|
||||
docs_base_url="https://docs.sunnypilot.ai",
|
||||
file_path="safety/index.md",
|
||||
)
|
||||
assert "#driver-responsibility" in result, f"FAIL anchor preserved:\n{result!r}"
|
||||
assert ".md" not in result.split("](")[1].split("#")[0], f"FAIL link has .md:\n{result!r}"
|
||||
# Anchors are stripped — Discourse search cannot target sections
|
||||
assert "#driver-responsibility" not in result, f"FAIL anchor should be stripped:\n{result!r}"
|
||||
assert "docs-sync-id" in result, f"FAIL missing sync-id:\n{result!r}"
|
||||
assert "safety" in result, f"FAIL wrong path:\n{result!r}"
|
||||
print(" PASS: internal_link_with_anchor")
|
||||
|
||||
|
||||
@@ -377,8 +380,7 @@ def test_external_link_untouched():
|
||||
input_text = "Visit [GitHub](https://github.com/sunnypilot/sunnypilot)."
|
||||
result = resolve_internal_links(
|
||||
input_text,
|
||||
file_path="docs_sp/index.md",
|
||||
docs_base_url="https://docs.sunnypilot.ai",
|
||||
file_path="index.md",
|
||||
)
|
||||
assert result == input_text, f"FAIL external_link:\n{result!r}"
|
||||
print(" PASS: external_link_untouched")
|
||||
@@ -425,16 +427,16 @@ See [safety info](../safety/safety.md) for more.
|
||||
"""
|
||||
result = convert(
|
||||
input_text,
|
||||
file_path="docs_sp/features/index.md",
|
||||
docs_base_url="https://docs.sunnypilot.ai",
|
||||
file_path="features/index.md",
|
||||
)
|
||||
# Front matter stripped
|
||||
assert "---\ntitle:" not in result, f"FAIL front matter not stripped:\n{result!r}"
|
||||
# Admonition converted
|
||||
assert "> [!WARNING] Important" in result, f"FAIL admonition:\n{result!r}"
|
||||
assert "> Pay attention to the road." in result, f"FAIL admonition content:\n{result!r}"
|
||||
# Link resolved
|
||||
assert ".md" not in result.split("](")[1].split(")")[0], f"FAIL link:\n{result!r}"
|
||||
# Link resolved to Discourse search
|
||||
assert "/search?q=" in result, f"FAIL link not converted to search:\n{result!r}"
|
||||
assert "docs-sync-id" in result, f"FAIL link missing sync-id:\n{result!r}"
|
||||
# Emoji converted
|
||||
assert ":material-check:" not in result, f"FAIL emoji:\n{result!r}"
|
||||
assert "Y Feature supported" in result, f"FAIL emoji replacement:\n{result!r}"
|
||||
|
||||
Reference in New Issue
Block a user