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}")