260 lines
8.2 KiB
Python
260 lines
8.2 KiB
Python
"""
|
|
Source Link Extension
|
|
######################
|
|
|
|
This Sphinx extension automatically adds source code repository links to documentation pages.
|
|
It's especially useful for sample documentation, allowing readers to easily locate the source
|
|
code in the repository.
|
|
|
|
Features
|
|
========
|
|
|
|
- Automatically injects a "View Source Code" link at the top of specified documents
|
|
- Supports GitHub, GitLab, and local path displays
|
|
- Configurable patterns to match which documents should get source links
|
|
- Minimal configuration required
|
|
|
|
Configuration options
|
|
=====================
|
|
|
|
- ``source_link_base_url``: Base URL for the source repository (e.g.,
|
|
'https://github.com/your-org/repo/tree/master'). If not set, only shows local paths.
|
|
- ``source_link_patterns``: List of glob patterns for files that should have source links
|
|
(defaults to ['samples/**/*.md', 'samples/**/*.rst'])
|
|
- ``source_link_show_local_path``: Whether to show the local file path (default: True)
|
|
- ``source_link_label``: Label text for the link (default: '📁 源码位置')
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List
|
|
from docutils import nodes
|
|
from docutils.parsers.rst import Directive
|
|
from sphinx.application import Sphinx
|
|
from sphinx.util.docutils import SphinxDirective
|
|
|
|
|
|
__version__ = "0.1.0"
|
|
|
|
|
|
def get_source_path(app: Sphinx, docname: str) -> str:
|
|
"""
|
|
Get the source file path relative to SDK_BASE.
|
|
|
|
Args:
|
|
app: Sphinx application instance
|
|
docname: Document name (without extension)
|
|
|
|
Returns:
|
|
Relative path from SDK_BASE to the source file, or empty string if not applicable
|
|
"""
|
|
srcdir = Path(app.srcdir).resolve()
|
|
sdk_base = Path(os.environ.get("SDK_BASE", srcdir.parent)).resolve()
|
|
|
|
# Try to find the actual source file (.md or .rst)
|
|
for ext in ['.md', '.rst']:
|
|
source_file = srcdir / f"{docname}{ext}"
|
|
if source_file.exists():
|
|
try:
|
|
rel_path = source_file.relative_to(srcdir)
|
|
# Get directory path (remove filename if it's README.md or similar)
|
|
if source_file.name.lower() in ['readme.md', 'readme.rst', 'index.md', 'index.rst']:
|
|
return str(rel_path.parent)
|
|
else:
|
|
return str(rel_path.parent)
|
|
except ValueError:
|
|
pass
|
|
|
|
return ""
|
|
|
|
|
|
def should_add_source_link(app: Sphinx, docname: str) -> bool:
|
|
"""
|
|
Check if a document should have a source link based on configured patterns.
|
|
|
|
Args:
|
|
app: Sphinx application instance
|
|
docname: Document name
|
|
|
|
Returns:
|
|
True if the document matches any of the configured patterns
|
|
"""
|
|
patterns = app.config.source_link_patterns
|
|
|
|
for pattern in patterns:
|
|
# Convert glob pattern to simple matching (could be enhanced with fnmatch)
|
|
if pattern.endswith('**/*.md') or pattern.endswith('**/*.rst'):
|
|
prefix = pattern.rsplit('/', 1)[0].replace('**', '')
|
|
if docname.startswith(prefix):
|
|
return True
|
|
elif pattern in docname:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_version_branch(app: Sphinx) -> str:
|
|
"""
|
|
Get the Git branch name based on the current documentation version.
|
|
|
|
Args:
|
|
app: Sphinx application instance
|
|
|
|
Returns:
|
|
Branch name to use in repository URLs
|
|
"""
|
|
# Get current version from html_context
|
|
current_version = app.config.html_context.get('current_version', 'latest')
|
|
|
|
# Get version-to-branch mapping from config
|
|
version_branch_map = getattr(app.config, 'source_link_version_branches', {})
|
|
|
|
# Return mapped branch, or construct default branch name
|
|
if current_version in version_branch_map:
|
|
return version_branch_map[current_version]
|
|
elif current_version == 'latest':
|
|
return 'master'
|
|
elif current_version.startswith('v'):
|
|
# Convert v0.1.0 to release/v0.1.0
|
|
return f"release/{current_version}"
|
|
else:
|
|
return 'master'
|
|
|
|
|
|
def create_source_link_node(app: Sphinx, source_path: str) -> nodes.container:
|
|
"""
|
|
Create a docutils node containing the source link information.
|
|
|
|
Args:
|
|
app: Sphinx application instance
|
|
source_path: Relative path to the source directory
|
|
|
|
Returns:
|
|
A docutils container node with the source link
|
|
"""
|
|
container = nodes.container()
|
|
container['classes'].append('source-link-box')
|
|
|
|
paragraph = nodes.paragraph()
|
|
|
|
# Add emoji/icon
|
|
icon = nodes.inline(text=app.config.source_link_label.split()[0] + ' ')
|
|
icon['classes'].append('source-link-icon')
|
|
paragraph += icon
|
|
|
|
# Add label
|
|
label_text = ' '.join(app.config.source_link_label.split()[1:]) + ': '
|
|
label = nodes.strong(text=label_text)
|
|
paragraph += label
|
|
|
|
# Add local path
|
|
if app.config.source_link_show_local_path:
|
|
path_text = nodes.literal(text=source_path)
|
|
path_text['classes'].append('source-link-path')
|
|
paragraph += path_text
|
|
|
|
# Add repository link if configured
|
|
if app.config.source_link_base_url:
|
|
if app.config.source_link_show_local_path:
|
|
paragraph += nodes.Text(' ')
|
|
|
|
# Get version-specific branch
|
|
branch = get_version_branch(app)
|
|
|
|
# Create clickable link with version-specific branch
|
|
base_url = app.config.source_link_base_url.rstrip('/')
|
|
# Replace the branch/tag in the URL
|
|
# Handle both GitHub style (/tree/BRANCH) and GitLab style (/-/tree/BRANCH)
|
|
if '/-/tree/' in base_url:
|
|
# GitLab style
|
|
base_parts = base_url.rsplit('/-/tree/', 1)
|
|
ref_url = f"{base_parts[0]}/-/tree/{branch}/{source_path}"
|
|
elif '/tree/' in base_url:
|
|
# GitHub style
|
|
base_parts = base_url.rsplit('/tree/', 1)
|
|
ref_url = f"{base_parts[0]}/tree/{branch}/{source_path}"
|
|
else:
|
|
# Fallback: append branch and path
|
|
ref_url = f"{base_url}/{branch}/{source_path}"
|
|
|
|
reference = nodes.reference('', '查看源码', refuri=ref_url)
|
|
reference['classes'].append('source-link-button')
|
|
paragraph += reference
|
|
|
|
container += paragraph
|
|
|
|
return container
|
|
|
|
|
|
def inject_source_link(app: Sphinx, doctree, docname: str) -> None:
|
|
"""
|
|
Inject source link at the beginning of the document.
|
|
|
|
Args:
|
|
app: Sphinx application instance
|
|
doctree: Document tree
|
|
docname: Document name
|
|
"""
|
|
if not should_add_source_link(app, docname):
|
|
return
|
|
|
|
source_path = get_source_path(app, docname)
|
|
if not source_path:
|
|
return
|
|
|
|
# Create the source link node
|
|
source_link_node = create_source_link_node(app, source_path)
|
|
|
|
# Insert at the beginning of the document
|
|
# Find the first section or insert at the very beginning
|
|
if len(doctree.children) > 0:
|
|
# Insert after the title but before the first section content
|
|
for i, child in enumerate(doctree.children):
|
|
if isinstance(child, nodes.section) and len(child.children) > 0:
|
|
# Insert after the section title
|
|
child.insert(1, source_link_node)
|
|
break
|
|
else:
|
|
# No section found, insert at the beginning
|
|
doctree.insert(0, source_link_node)
|
|
|
|
|
|
def add_source_link_css(app: Sphinx, config) -> None:
|
|
"""
|
|
Add CSS for source link styling.
|
|
|
|
Args:
|
|
app: Sphinx application instance
|
|
config: Sphinx config
|
|
"""
|
|
# CSS will be added via static files
|
|
app.add_css_file('source-link.css')
|
|
|
|
|
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
|
"""
|
|
Setup function for the Sphinx extension.
|
|
|
|
Args:
|
|
app: Sphinx application instance
|
|
|
|
Returns:
|
|
Extension metadata
|
|
"""
|
|
# Add configuration values
|
|
app.add_config_value("source_link_base_url", "", "html")
|
|
app.add_config_value("source_link_patterns", ["samples/**/*.md", "samples/**/*.rst"], "html")
|
|
app.add_config_value("source_link_show_local_path", True, "html")
|
|
app.add_config_value("source_link_label", "📁 源码位置", "html")
|
|
app.add_config_value("source_link_version_branches", {}, "html")
|
|
|
|
# Connect event handlers
|
|
app.connect("doctree-resolved", inject_source_link)
|
|
app.connect("config-inited", add_source_link_css)
|
|
|
|
return {
|
|
"version": __version__,
|
|
"parallel_read_safe": True,
|
|
"parallel_write_safe": True,
|
|
}
|