main
py 171 lines 6.64 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """MkDocs hook + markdown extension to enable the following links to render correctly,
4 including inside content included via pymdownx.snippets:
5
6 - Relative file links outside of the `docs/` directory, e.g.:
7 - [Text](../some_file.py)
8 - [Directory](../../some_directory/)
9 - GitHub URLs for issues, pull requests, and projects, e.g.:
10 - Adds GitHub icon before links
11 - Replaces raw links with descriptive text,
12 e.g. <...pull/123> -> [Pull Request #123](.../pull/123)
13 - Works for external repos too by including the `owner/repo` in the link title
14
15 The link replacement runs as a markdown preprocessor (priority 25) so that it executes
16 after pymdownx.snippets (priority 32) has expanded all included content.
17 The on_page_markdown hook passes the current page context to the preprocessor before
18 each page is converted.
19 """
20
21 import posixpath
22 from pathlib import Path
23
24 import regex as re
25 from markdown import Extension
26 from markdown.preprocessors import Preprocessor
27 from mkdocs.config.defaults import MkDocsConfig
28 from mkdocs.structure.files import Files
29 from mkdocs.structure.pages import Page
30
31 ROOT_DIR = Path(__file__).parent.parent.parent.parent.resolve()
32 DOC_DIR = ROOT_DIR / "docs"
33
34 gh_icon = ":octicons-mark-github-16:"
35
36 # Regex pieces
37 TITLE = r"(?P<title>[^\[\]<>]+?)"
38 REPO = r"(?P<repo>.+?/.+?)"
39 TYPE = r"(?P<type>issues|pull|projects)"
40 NUMBER = r"(?P<number>\d+)"
41 VERSION = r"[^/\s]+"
42 PATH = r"(?P<path>[^\s]+?)"
43 FRAGMENT = r"(?P<fragment>#[^\s]+)?"
44 URL_GITHUB = f"https://github.com/{REPO}/{TYPE}/{NUMBER}{FRAGMENT}"
45 RELATIVE = rf"(?!(https?|ftp)://|#){PATH}{FRAGMENT}"
46 URL_DOCS = f"https://docs.vllm.ai/en/{VERSION}/{PATH}{FRAGMENT}"
47
48 # Common titles to use for GitHub links when none is provided in the link.
49 TITLES = {"issues": "Issue ", "pull": "Pull Request ", "projects": "Project "}
50
51 # Regex to match GitHub issue, PR, and project links with optional titles.
52 github_link = re.compile(rf"(\[{TITLE}\]\(|<){URL_GITHUB}(\)|>)")
53 # Regex to match relative file links with optional titles.
54 relative_link = re.compile(rf"\[{TITLE}\]\({RELATIVE}\)")
55 # Regex to match absolute docs.vllm.ai links (should only exist in CLI).
56 docs_link = re.compile(rf"\[{TITLE}\]\({URL_DOCS}\)")
57
58
59 class UrlSchemesPreprocessor(Preprocessor):
60 """Preprocessor that runs after pymdownx.snippets to process all links."""
61
62 def __init__(self, md, ext):
63 super().__init__(md)
64 self.ext = ext
65
66 def run(self, lines):
67 page = self.ext.page
68 files = self.ext.files
69 if page is None:
70 return lines
71
72 def replace_relative_link(match: re.Match) -> str:
73 """Replace relative file links pointing outside the docs dir with URLs."""
74 title = match.group("title")
75 path = match.group("path")
76 path = ((DOC_DIR / page.file.src_uri).parent / path).resolve()
77 fragment = match.group("fragment") or ""
78
79 # Check if the path exists and is outside the docs dir
80 if not path.exists() or path.is_relative_to(DOC_DIR):
81 return match.group(0)
82
83 # Files and directories have different URL schemes on GitHub
84 slug = "tree/main" if path.is_dir() else "blob/main"
85
86 path = path.relative_to(ROOT_DIR)
87 url = f"https://github.com/vllm-project/vllm/{slug}/{path}{fragment}"
88 return f"[{gh_icon} {title}]({url})"
89
90 def replace_github_link(match: re.Match) -> str:
91 """Replace GitHub issue, PR, and project links with rich Markdown links."""
92 repo = match.group("repo")
93 type = match.group("type")
94 number = match.group("number")
95 # Title and fragment could be None
96 title = match.group("title") or ""
97 fragment = match.group("fragment") or ""
98
99 # Use default titles for raw links
100 if not title:
101 title = TITLES[type]
102 if "vllm-project" not in repo:
103 title += repo
104 title += f"#{number}"
105
106 url = f"https://github.com/{repo}/{type}/{number}{fragment}"
107 return f"[{gh_icon} {title}]({url})"
108
109 def replace_docs_link(match: re.Match) -> str:
110 """Rewrite absolute docs.vllm.ai links as doc-relative links."""
111 title = match.group("title")
112 path = match.group("path").rstrip("/")
113 fragment = match.group("fragment") or ""
114
115 # vllm.config.<Class> API reference -> mkdocstrings cross-reference
116 if path == "api/vllm/config" and re.fullmatch(
117 r"#vllm\.config\.\w+", fragment
118 ):
119 ident = fragment[1:]
120 return f"[`{ident}`][{ident}]"
121
122 # Other docs pages -> link relative to the current page, but only
123 # when the target is a known docs page (real or generated); leave
124 # unknown/external URLs untouched. This is correct even when the same
125 # docstring is also rendered on its API reference page.
126 src = f"{path.removesuffix('.html')}.md"
127 if files.get_file_from_path(src) is None:
128 return match.group(0)
129 rel = posixpath.relpath(src, posixpath.dirname(page.file.src_uri))
130 # Auto-wrapped bare URLs use the URL as their title; make it readable.
131 if title.startswith("http"):
132 title = path.removesuffix(".html")
133 return f"[{title}]({rel}{fragment})"
134
135 markdown = "\n".join(lines)
136 markdown = github_link.sub(replace_github_link, markdown)
137 markdown = relative_link.sub(replace_relative_link, markdown)
138 markdown = docs_link.sub(replace_docs_link, markdown)
139 return markdown.split("\n")
140
141
142 class UrlSchemesExtension(Extension):
143 """Markdown extension that registers the URL schemes preprocessor."""
144
145 def __init__(self, **kwargs):
146 self.page = None
147 self.files = None
148 super().__init__(**kwargs)
149
150 def extendMarkdown(self, md):
151 # Priority 25 runs after pymdownx.snippets (priority 32)
152 md.preprocessors.register(UrlSchemesPreprocessor(md, self), "url_schemes", 25)
153
154
155 # Singleton extension instance shared between the hook and the preprocessor.
156 _ext = UrlSchemesExtension()
157
158
159 def on_config(config: MkDocsConfig) -> MkDocsConfig:
160 """Register the URL schemes markdown extension."""
161 config["markdown_extensions"].append(_ext)
162 return config
163
164
165 def on_page_markdown(
166 markdown: str, *, page: Page, config: MkDocsConfig, files: Files
167 ) -> str:
168 """Pass the current page context to the preprocessor."""
169 _ext.page = page
170 _ext.files = files
171 return markdown