github.com/cilium/ebpf@v0.15.1-0.20240517100537-8079b37aa138/docs/macros.py (about) 1 """Macro definitions for documentation.""" 2 3 # Use built-in 'list' type when upgrading to Python 3.9. 4 5 import glob 6 import os 7 import re 8 import textwrap 9 from io import TextIOWrapper 10 from typing import List 11 from urllib.parse import ParseResult, urlparse 12 13 from mkdocs_macros.plugin import MacrosPlugin 14 15 16 def define_env(env: MacrosPlugin): 17 """ 18 Define the mkdocs-macros-plugin environment. 19 20 This function is called on setup. 'env' can be interacted with 21 for defining variables, macros and filters. 22 23 - variables: the dictionary that contains the environment variables 24 - macro: a decorator function, to declare a macro. 25 - filter: a function with one or more arguments, used to perform a 26 transformation 27 """ 28 # Values can be overridden in mkdocs.yml:extras. 29 go_examples_path: str = env.variables.get( 30 "go_examples_path", "examples/**/*.go" 31 ) 32 godoc_url: ParseResult = urlparse( 33 env.variables.get( 34 "godoc_url", "https://pkg.go.dev/github.com/cilium/ebpf" 35 ) 36 ) 37 38 c_examples_path: str = env.variables.get("c_examples_path", "examples/**/*.c") 39 40 @env.macro 41 def godoc(sym: str, short: bool = False): 42 """ 43 Generate a godoc link based on the configured godoc_url. 44 45 `sym` is the symbol to link to. A dot '.' separator means it's a method 46 on another type. Forward slashes '/' can be used to navigate to symbols 47 in subpackages. 48 49 For example: 50 - CollectionSpec.LoadAndAssign 51 - link/Link 52 - btf/Spec.TypeByID 53 54 `short` renders only the symbol name. 55 """ 56 if len(godoc_url) == 0: 57 raise ValueError("Empty godoc url") 58 59 # Support referring to symbols in subpackages. 60 subpkg = os.path.dirname(sym) 61 # Symbol name including dots for struct methods. (e.g. Map.Get) 62 name = os.path.basename(sym) 63 64 # Python's urljoin() expects the base path to have a trailing slash for 65 # it to correctly append subdirs. Use urlparse instead, and interact 66 # with the URL's components individually. 67 url = godoc_url._replace( 68 path=os.path.join(godoc_url.path, subpkg), 69 # Anchor token appearing after the # in the URL. 70 fragment=name, 71 ).geturl() 72 73 text = name 74 if short: 75 text = text.split(".")[-1] 76 77 return f"[:fontawesome-brands-golang: `{text}`]({url})" 78 79 @env.macro 80 def go_example(*args, **kwargs): 81 """ 82 Include the body of a Go code example. 83 84 See docstring of code_example() for details. 85 """ 86 return code_example( 87 *args, **kwargs, language="go", path=go_examples_path 88 ) 89 90 @env.macro 91 def c_example(*args, **kwargs): 92 """ 93 Include the body of a C code example. 94 95 See docstring of `code_example` for details. 96 """ 97 return code_example( 98 *args, **kwargs, language="c", path=c_examples_path 99 ) 100 101 102 def code_example( 103 symbol: str, 104 title: str = None, 105 language: str = "", 106 lines: bool = True, 107 signature: bool = False, 108 path: str = "", 109 ) -> str: 110 """ 111 Include the body of a code example. 112 113 `symbol` takes the name of the function or snippet to include. 114 `title` is rendered as a title at the top of the snippet. 115 `language` is the name of the programming language passed to pygments. 116 `lines` controls rendering line numbers. 117 `signature` controls whether or not the function signature and brackets are 118 included. 119 `path` specifies the include path that may contain globs. 120 """ 121 opts: List[str] = [] 122 if lines: 123 opts.append("linenums='1'") 124 if title: 125 opts.append(f"title='{title}'") 126 127 if signature: 128 body = full_body(path, symbol) 129 else: 130 body = inner_body(path, symbol) 131 132 out = f"``` {language} {' '. join(opts)}\n{body}```" 133 134 return out 135 136 137 def inner_body(path: str, sym: str) -> str: 138 """ 139 Get the inner body of sym, using default delimiters. 140 141 First and last lines (so, function signature and closing bracket) are 142 stripped, the remaining body dedented. 143 """ 144 out = _search_body(path, sym) 145 if len(out) < 2: 146 raise ValueError( 147 f"Need at least two lines to get inner body for symbol {sym}" 148 ) 149 150 return textwrap.dedent("".join(out[1:-1])) 151 152 153 def full_body(path: str, sym: str) -> str: 154 """Get the full body of sym, using default delimiters, dedented.""" 155 out = _search_body(path, sym) 156 157 return textwrap.dedent("".join(out)) 158 159 160 def _get_body( 161 f: TextIOWrapper, sym: str, start: str = "{", end: str = "}" 162 ) -> List[str]: 163 """ 164 Extract a body of text between sym and start/end delimiters. 165 166 Tailored to finding function bodies of C-family programming languages with 167 curly braces. 168 169 The starting line of the body must contain sym prefixed by a space, with 170 'start' appearing on the same line, for example " Foo() {". Further 171 occurrences of "{" and its closing counterpart "}" are tracked, and the 172 lines between and including the final "}" are returned. 173 """ 174 found = False 175 stack = 0 176 lines = [] 177 178 for line in f.readlines(): 179 if not found: 180 # Skip current line if we're not in a body and the current line 181 # doesn't contain the given symbol. 182 # 183 # The symbol must be surrounded by non-word characters like spaces 184 # or parentheses. For example, a line "// DocObjs {" or "func 185 # DocLoader() {" should match. 186 if re.search(rf"\W{sym}\W", line) is None: 187 continue 188 189 found = True 190 191 # Count the amount of start delimiters. 192 stack += line.count(start) 193 194 if stack == 0: 195 # No opening delimiter found, ignore the line. 196 found = False 197 continue 198 199 lines.append(line) 200 201 # Count the amount of end delimiters and stop if we've escaped the 202 # current scope. 203 stack -= line.count(end) 204 if stack <= 0: 205 break 206 207 # Rewind the file for reuse. 208 f.seek(0) 209 210 if stack > 0: 211 raise LookupError(f"No end delimiter for {sym}") 212 213 if len(lines) == 0: 214 raise LookupError(f"Symbol {sym} not found") 215 216 return lines 217 218 219 def _search_body(path: str, sym: str) -> List[str]: 220 """Find the body of the given symbol in a path glob.""" 221 files = glob.glob(path, recursive=True) 222 if len(files) == 0: 223 raise LookupError(f"Path {path} did not match any files") 224 225 for file in files: 226 with open(file, mode="r") as f: 227 try: 228 return _get_body(f, sym) 229 except LookupError: 230 continue 231 232 raise LookupError(f"Symbol {sym} not found in any of {files}")