mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-23 16:32:04 +08:00
more
This commit is contained in:
@@ -34,21 +34,21 @@ jobs:
|
||||
DISCOURSE_CATEGORY_MAP: '{"getting-started": 133}'
|
||||
run: |
|
||||
uv run --python 3.12 python -c "
|
||||
import sys
|
||||
import os, sys
|
||||
sys.path.insert(0, 'docs_sp/tools')
|
||||
from pathlib import Path
|
||||
from converter import convert
|
||||
from discourse_client import DiscourseClient, DiscourseConfig
|
||||
|
||||
DOCS_BASE_URL = 'https://docs.sunnypilot.ai'
|
||||
DOC_PATH = 'getting-started/what-is-sunnypilot.md'
|
||||
GITHUB_BRANCH = os.environ.get('GITHUB_REF_NAME', 'master')
|
||||
|
||||
raw = (Path('docs_sp') / DOC_PATH).read_text()
|
||||
body = convert(raw, file_path=DOC_PATH, docs_base_url=DOCS_BASE_URL)
|
||||
body = convert(raw, file_path=DOC_PATH)
|
||||
|
||||
# Append sync metadata
|
||||
docs_url = f'{DOCS_BASE_URL}/{DOC_PATH.replace(\".md\", \"/\")}'
|
||||
body = body.rstrip('\n') + f'\n\n---\n:link: [View on docs site]({docs_url})\n\n<!-- docs-sync-id: {DOC_PATH} -->\n'
|
||||
# Append sync footer
|
||||
gh_url = f'https://github.com/sunnypilot/sunnypilot/blob/{GITHUB_BRANCH}/docs_sp/{DOC_PATH}'
|
||||
body = body.rstrip('\n') + f'\n\n---\n<small>This document is version-controlled. Suggest changes [on GitHub]({gh_url}).</small>\n\n<!-- docs-sync-id: {DOC_PATH} -->\n'
|
||||
|
||||
# Extract title from front matter or first heading
|
||||
title = None
|
||||
@@ -67,12 +67,7 @@ jobs:
|
||||
|
||||
config = DiscourseConfig.from_env()
|
||||
client = DiscourseClient(config)
|
||||
|
||||
def category_id_for(path: str) -> int:
|
||||
top_level = path.split('/')[0]
|
||||
return config.category_mapping.get(top_level, config.category_mapping.get('documentation', 114))
|
||||
|
||||
category_id = category_id_for(DOC_PATH)
|
||||
category_id = config.category_id_for(DOC_PATH)
|
||||
print(f'Category ID: {category_id}')
|
||||
|
||||
existing = client.find_topic_by_sync_id(DOC_PATH)
|
||||
|
||||
+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