Skip to content

Commit 8302ce5

Browse files
committed
Initial commit
0 parents  commit 8302ce5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1793
-0
lines changed

Diff for: .gitignore

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.idea/
2+
__pycache__/
3+
*.py[cod]
4+
dist/
5+
*.egg-info/
6+
build/
7+
htmlcov/
8+
.coverage*
9+
pip-wheel-metadata/
10+
.pytest_cache/
11+
.mypy_cache/
12+
site/
13+
pdm.lock
14+
.pdm.toml
15+
__pypackages__/
16+
.venv/
17+
.eggs/
18+
src/version.py

Diff for: README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# mkdocstrings-vba
2+
3+
A VBA handler for [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings).

Diff for: setup.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import setuptools
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setuptools.setup(
7+
name="mkdocstrings-vba",
8+
author="Rudolf Byker",
9+
author_email="[email protected]",
10+
description="MkDocstrings VBA handler",
11+
long_description=long_description,
12+
long_description_content_type="text/markdown",
13+
url="https://github.com/AutoActuary/mkdocstrings-vba",
14+
packages=setuptools.find_packages(),
15+
classifiers=[
16+
"Programming Language :: Python :: 3",
17+
"License :: Other/Proprietary License",
18+
"Operating System :: OS Independent",
19+
],
20+
python_requires=">=3.7",
21+
use_scm_version={
22+
"write_to": "src/version.py",
23+
},
24+
setup_requires=[
25+
"setuptools_scm",
26+
],
27+
install_requires=[
28+
"mkdocstrings[python]>=0.18",
29+
"mkdocs-material",
30+
],
31+
)

Diff for: src/mkdocstrings_handlers/vba/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""This package implements a handler for the VBA language."""
2+
3+
from mkdocstrings_handlers.vba.handler import get_handler
4+
5+
__all__ = ["get_handler"]

Diff for: src/mkdocstrings_handlers/vba/collector.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
This module implements a collector for the VBA language.
3+
"""
4+
from pathlib import Path
5+
6+
from mkdocstrings.handlers.base import BaseCollector
7+
8+
from mkdocstrings_handlers.vba.types import VbaModuleInfo
9+
from mkdocstrings_handlers.vba.util import (
10+
collapse_long_lines,
11+
find_file_docstring,
12+
find_procedures,
13+
)
14+
15+
16+
class VbaCollector(BaseCollector):
17+
"""
18+
Collect data from a VBA file.
19+
"""
20+
21+
def collect(
22+
self,
23+
identifier: str,
24+
config: dict,
25+
) -> VbaModuleInfo:
26+
"""Collect the documentation tree given an identifier and selection options.
27+
28+
Arguments:
29+
identifier: Which VBA file (.bas or .cls) to collect from.
30+
config: Selection options, used to alter the data collection.
31+
32+
Raises:
33+
CollectionError: When there was a problem collecting the documentation.
34+
35+
Returns:
36+
The collected object tree.
37+
"""
38+
p = Path(identifier)
39+
with p.open("r") as f:
40+
code = f.read()
41+
42+
code = collapse_long_lines(code)
43+
44+
return VbaModuleInfo(
45+
docstring=find_file_docstring(code),
46+
source=code.splitlines(),
47+
path=p,
48+
procedures=list(find_procedures(code)),
49+
)

Diff for: src/mkdocstrings_handlers/vba/handler.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""This module implements a handler for the VBA language."""
2+
3+
import posixpath
4+
from typing import Any, BinaryIO, Iterator, Optional, Tuple
5+
6+
from griffe.logger import patch_loggers
7+
from mkdocstrings.handlers.base import BaseHandler
8+
from mkdocstrings.inventory import Inventory
9+
from mkdocstrings.loggers import get_logger
10+
11+
from mkdocstrings_handlers.vba.collector import VbaCollector
12+
from mkdocstrings_handlers.vba.renderer import VbaRenderer
13+
14+
patch_loggers(get_logger)
15+
16+
17+
class VbaHandler(BaseHandler):
18+
"""The Vba handler class."""
19+
20+
domain: str = "vba"
21+
"""The cross-documentation domain/language for this handler."""
22+
23+
enable_inventory: bool = True
24+
"""Whether this handler is interested in enabling the creation of the `objects.inv` Sphinx inventory file."""
25+
26+
@classmethod
27+
def load_inventory(
28+
cls,
29+
in_file: BinaryIO,
30+
url: str,
31+
base_url: Optional[str] = None,
32+
**kwargs: Any,
33+
) -> Iterator[Tuple[str, str]]:
34+
"""Yield items and their URLs from an inventory file streamed from `in_file`.
35+
36+
This implements mkdocstrings' `load_inventory` "protocol" (see plugin.py).
37+
38+
Arguments:
39+
in_file: The binary file-like object to read the inventory from.
40+
url: The URL that this file is being streamed from (used to guess `base_url`).
41+
base_url: The URL that this inventory's sub-paths are relative to.
42+
**kwargs: Ignore additional arguments passed from the config.
43+
44+
Yields:
45+
Tuples of (item identifier, item URL).
46+
"""
47+
if base_url is None:
48+
base_url = posixpath.dirname(url)
49+
50+
for item in Inventory.parse_sphinx(in_file, domain_filter=("py",)).values():
51+
yield item.name, posixpath.join(base_url, item.uri)
52+
53+
54+
def get_handler(
55+
theme: str,
56+
custom_templates: Optional[str] = None,
57+
**config: Any,
58+
) -> VbaHandler:
59+
"""Simply return an instance of `VbaHandler`.
60+
61+
Arguments:
62+
theme: The theme to use when rendering contents.
63+
custom_templates: Directory containing custom templates.
64+
**config: Configuration passed to the handler.
65+
66+
Returns:
67+
An instance of `VbaHandler`.
68+
"""
69+
return VbaHandler(
70+
collector=VbaCollector(),
71+
renderer=VbaRenderer("vba", theme, custom_templates),
72+
)

Diff for: src/mkdocstrings_handlers/vba/renderer.py

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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

Comments
 (0)