|
| 1 | +""" |
| 2 | +This module implements a renderer for the VBA language. |
| 3 | +
|
| 4 | +Most of this is just copied / hacked together from the Python renderer. |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import enum |
| 10 | +import re |
| 11 | +import sys |
| 12 | +from collections import ChainMap |
| 13 | +from typing import Any, Sequence |
| 14 | + |
| 15 | +from griffe.dataclasses import Alias, Object |
| 16 | +from markdown import Markdown |
| 17 | +from markupsafe import Markup |
| 18 | +from mkdocstrings.extension import PluginError |
| 19 | +from mkdocstrings.handlers.base import BaseRenderer, CollectorItem |
| 20 | +from mkdocstrings.loggers import get_logger |
| 21 | + |
| 22 | +from mkdocstrings_handlers.vba.types import VbaModuleInfo |
| 23 | + |
| 24 | +logger = get_logger(__name__) |
| 25 | + |
| 26 | + |
| 27 | +class Order(enum.Enum): |
| 28 | + """Enumeration for the possible members ordering.""" |
| 29 | + |
| 30 | + alphabetical = "alphabetical" |
| 31 | + source = "source" |
| 32 | + |
| 33 | + |
| 34 | +def _sort_key_alphabetical(item: CollectorItem) -> Any: |
| 35 | + # chr(sys.maxunicode) is a string that contains the final unicode |
| 36 | + # character, so if 'name' isn't found on the object, the item will go to |
| 37 | + # the end of the list. |
| 38 | + return item.name or chr(sys.maxunicode) |
| 39 | + |
| 40 | + |
| 41 | +def _sort_key_source(item: CollectorItem) -> Any: |
| 42 | + # if 'lineno' is none, the item will go to the start of the list. |
| 43 | + return item.lineno if item.lineno is not None else -1 |
| 44 | + |
| 45 | + |
| 46 | +order_map = { |
| 47 | + Order.alphabetical: _sort_key_alphabetical, |
| 48 | + Order.source: _sort_key_source, |
| 49 | +} |
| 50 | + |
| 51 | + |
| 52 | +class VbaRenderer(BaseRenderer): |
| 53 | + """The class responsible for loading Jinja templates and rendering them. |
| 54 | +
|
| 55 | + It defines some configuration options, implements the `render` method, |
| 56 | + and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer]. |
| 57 | + """ |
| 58 | + |
| 59 | + fallback_theme = "material" |
| 60 | + """ |
| 61 | + The theme to fall back to. |
| 62 | + """ |
| 63 | + |
| 64 | + default_config: dict = { |
| 65 | + "show_root_heading": False, |
| 66 | + "show_root_toc_entry": True, |
| 67 | + "show_root_full_path": True, |
| 68 | + "show_root_members_full_path": False, |
| 69 | + "show_object_full_path": False, |
| 70 | + "show_category_heading": False, |
| 71 | + "show_if_no_docstring": False, |
| 72 | + "show_signature": True, |
| 73 | + "separate_signature": False, |
| 74 | + "line_length": 60, |
| 75 | + "show_source": True, |
| 76 | + "show_bases": True, |
| 77 | + "show_submodules": True, |
| 78 | + "heading_level": 2, |
| 79 | + "members_order": Order.alphabetical.value, |
| 80 | + "docstring_section_style": "table", |
| 81 | + } |
| 82 | + """The default rendering options. |
| 83 | + |
| 84 | + See [`default_config`][mkdocstrings_handlers.vba.renderer.VbaRenderer.default_config]. |
| 85 | +
|
| 86 | + Option | Type | Description | Default |
| 87 | + ------ | ---- | ----------- | ------- |
| 88 | + **`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False` |
| 89 | + **`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True` |
| 90 | + **`show_root_full_path`** | `bool` | Show the full VBA path for the root object heading. | `True` |
| 91 | + **`show_object_full_path`** | `bool` | Show the full VBA path of every object. | `False` |
| 92 | + **`show_root_members_full_path`** | `bool` | Show the full VBA path of objects that are children of the root object (for example, classes in a module). When False, `show_object_full_path` overrides. | `False` |
| 93 | + **`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False` |
| 94 | + **`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False` |
| 95 | + **`show_signature`** | `bool` | Show method and function signatures. | `True` |
| 96 | + **`separate_signature`** | `bool` | Whether to put the whole signature in a code block below the heading. | `False` |
| 97 | + **`line_length`** | `int` | Maximum line length when formatting code. | `60` |
| 98 | + **`show_source`** | `bool` | Show the source code of this object. | `True` |
| 99 | + **`show_bases`** | `bool` | Show the base classes of a class. | `True` |
| 100 | + **`show_submodules`** | `bool` | When rendering a module, show its submodules recursively. | `True` |
| 101 | + **`heading_level`** | `int` | The initial heading level to use. | `2` |
| 102 | + **`members_order`** | `str` | The members ordering to use. Options: `alphabetical` - order by the members names, `source` - order members as they appear in the source file. | `alphabetical` |
| 103 | + **`docstring_section_style`** | `str` | The style used to render docstring sections. Options: `table`, `list`, `spacy`. | `table` |
| 104 | + """ # noqa: E501 |
| 105 | + |
| 106 | + def render( |
| 107 | + self, |
| 108 | + data: VbaModuleInfo, |
| 109 | + config: dict, |
| 110 | + ) -> str: |
| 111 | + final_config = ChainMap(config, self.default_config) |
| 112 | + render_type = "module" |
| 113 | + |
| 114 | + template = self.env.get_template(f"{render_type}.html") |
| 115 | + |
| 116 | + # Heading level is a "state" variable, that will change at each step |
| 117 | + # of the rendering recursion. Therefore, it's easier to use it as a plain value |
| 118 | + # than as an item in a dictionary. |
| 119 | + heading_level = final_config["heading_level"] |
| 120 | + try: |
| 121 | + final_config["members_order"] = Order(final_config["members_order"]) |
| 122 | + except ValueError: |
| 123 | + choices = "', '".join(item.value for item in Order) |
| 124 | + raise PluginError( |
| 125 | + f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'." |
| 126 | + ) |
| 127 | + |
| 128 | + return template.render( |
| 129 | + **{ |
| 130 | + "config": final_config, |
| 131 | + render_type: data, |
| 132 | + "heading_level": heading_level, |
| 133 | + "root": True, |
| 134 | + }, |
| 135 | + ) |
| 136 | + |
| 137 | + def get_anchors(self, data: VbaModuleInfo) -> list[str]: |
| 138 | + return list( |
| 139 | + {data.path.as_posix(), *(p.signature.name for p in data.procedures)} |
| 140 | + ) |
| 141 | + |
| 142 | + def update_env(self, md: Markdown, config: dict) -> None: |
| 143 | + super().update_env(md, config) |
| 144 | + self.env.trim_blocks = True |
| 145 | + self.env.lstrip_blocks = True |
| 146 | + self.env.keep_trailing_newline = False |
| 147 | + self.env.filters["crossref"] = self.do_crossref |
| 148 | + self.env.filters["multi_crossref"] = self.do_multi_crossref |
| 149 | + self.env.filters["order_members"] = self.do_order_members |
| 150 | + |
| 151 | + @staticmethod |
| 152 | + def do_order_members( |
| 153 | + members: Sequence[Object | Alias], order: Order |
| 154 | + ) -> Sequence[Object | Alias]: |
| 155 | + """Order members given an ordering method. |
| 156 | +
|
| 157 | + Parameters: |
| 158 | + members: The members to order. |
| 159 | + order: The ordering method. |
| 160 | +
|
| 161 | + Returns: |
| 162 | + The same members, ordered. |
| 163 | + """ |
| 164 | + return sorted(members, key=order_map[order]) |
| 165 | + |
| 166 | + @staticmethod |
| 167 | + def do_crossref(path: str, brief: bool = True) -> Markup: |
| 168 | + """Filter to create cross-references. |
| 169 | +
|
| 170 | + Parameters: |
| 171 | + path: The path to link to. |
| 172 | + brief: Show only the last part of the path, add full path as hover. |
| 173 | +
|
| 174 | + Returns: |
| 175 | + Markup text. |
| 176 | + """ |
| 177 | + full_path = path |
| 178 | + if brief: |
| 179 | + path = full_path.split(".")[-1] |
| 180 | + return Markup( |
| 181 | + "<span data-autorefs-optional-hover={full_path}>{path}</span>" |
| 182 | + ).format(full_path=full_path, path=path) |
| 183 | + |
| 184 | + @staticmethod |
| 185 | + def do_multi_crossref(text: str, code: bool = True) -> Markup: |
| 186 | + """Filter to create cross-references. |
| 187 | +
|
| 188 | + Parameters: |
| 189 | + text: The text to scan. |
| 190 | + code: Whether to wrap the result in a code tag. |
| 191 | +
|
| 192 | + Returns: |
| 193 | + Markup text. |
| 194 | + """ |
| 195 | + group_number = 0 |
| 196 | + variables = {} |
| 197 | + |
| 198 | + def repl(match): # noqa: WPS430 |
| 199 | + nonlocal group_number # noqa: WPS420 |
| 200 | + group_number += 1 |
| 201 | + path = match.group() |
| 202 | + path_var = f"path{group_number}" |
| 203 | + variables[path_var] = path |
| 204 | + return f"<span data-autorefs-optional-hover={{{path_var}}}>{{{path_var}}}</span>" |
| 205 | + |
| 206 | + text = re.sub(r"([\w.]+)", repl, text) |
| 207 | + if code: |
| 208 | + text = f"<code>{text}</code>" |
| 209 | + return Markup(text).format(**variables) |
0 commit comments