Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
rudolfbyker committed Feb 10, 2022
0 parents commit 8302ce5
Show file tree
Hide file tree
Showing 53 changed files with 1,793 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.idea/
__pycache__/
*.py[cod]
dist/
*.egg-info/
build/
htmlcov/
.coverage*
pip-wheel-metadata/
.pytest_cache/
.mypy_cache/
site/
pdm.lock
.pdm.toml
__pypackages__/
.venv/
.eggs/
src/version.py
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# mkdocstrings-vba

A VBA handler for [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings).
31 changes: 31 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import setuptools

with open("README.md", "r") as fh:
long_description = fh.read()

setuptools.setup(
name="mkdocstrings-vba",
author="Rudolf Byker",
author_email="[email protected]",
description="MkDocstrings VBA handler",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/AutoActuary/mkdocstrings-vba",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: Other/Proprietary License",
"Operating System :: OS Independent",
],
python_requires=">=3.7",
use_scm_version={
"write_to": "src/version.py",
},
setup_requires=[
"setuptools_scm",
],
install_requires=[
"mkdocstrings[python]>=0.18",
"mkdocs-material",
],
)
5 changes: 5 additions & 0 deletions src/mkdocstrings_handlers/vba/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""This package implements a handler for the VBA language."""

from mkdocstrings_handlers.vba.handler import get_handler

__all__ = ["get_handler"]
49 changes: 49 additions & 0 deletions src/mkdocstrings_handlers/vba/collector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
This module implements a collector for the VBA language.
"""
from pathlib import Path

from mkdocstrings.handlers.base import BaseCollector

from mkdocstrings_handlers.vba.types import VbaModuleInfo
from mkdocstrings_handlers.vba.util import (
collapse_long_lines,
find_file_docstring,
find_procedures,
)


class VbaCollector(BaseCollector):
"""
Collect data from a VBA file.
"""

def collect(
self,
identifier: str,
config: dict,
) -> VbaModuleInfo:
"""Collect the documentation tree given an identifier and selection options.
Arguments:
identifier: Which VBA file (.bas or .cls) to collect from.
config: Selection options, used to alter the data collection.
Raises:
CollectionError: When there was a problem collecting the documentation.
Returns:
The collected object tree.
"""
p = Path(identifier)
with p.open("r") as f:
code = f.read()

code = collapse_long_lines(code)

return VbaModuleInfo(
docstring=find_file_docstring(code),
source=code.splitlines(),
path=p,
procedures=list(find_procedures(code)),
)
72 changes: 72 additions & 0 deletions src/mkdocstrings_handlers/vba/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""This module implements a handler for the VBA language."""

import posixpath
from typing import Any, BinaryIO, Iterator, Optional, Tuple

from griffe.logger import patch_loggers
from mkdocstrings.handlers.base import BaseHandler
from mkdocstrings.inventory import Inventory
from mkdocstrings.loggers import get_logger

from mkdocstrings_handlers.vba.collector import VbaCollector
from mkdocstrings_handlers.vba.renderer import VbaRenderer

patch_loggers(get_logger)


class VbaHandler(BaseHandler):
"""The Vba handler class."""

domain: str = "vba"
"""The cross-documentation domain/language for this handler."""

enable_inventory: bool = True
"""Whether this handler is interested in enabling the creation of the `objects.inv` Sphinx inventory file."""

@classmethod
def load_inventory(
cls,
in_file: BinaryIO,
url: str,
base_url: Optional[str] = None,
**kwargs: Any,
) -> Iterator[Tuple[str, str]]:
"""Yield items and their URLs from an inventory file streamed from `in_file`.
This implements mkdocstrings' `load_inventory` "protocol" (see plugin.py).
Arguments:
in_file: The binary file-like object to read the inventory from.
url: The URL that this file is being streamed from (used to guess `base_url`).
base_url: The URL that this inventory's sub-paths are relative to.
**kwargs: Ignore additional arguments passed from the config.
Yields:
Tuples of (item identifier, item URL).
"""
if base_url is None:
base_url = posixpath.dirname(url)

for item in Inventory.parse_sphinx(in_file, domain_filter=("py",)).values():
yield item.name, posixpath.join(base_url, item.uri)


def get_handler(
theme: str,
custom_templates: Optional[str] = None,
**config: Any,
) -> VbaHandler:
"""Simply return an instance of `VbaHandler`.
Arguments:
theme: The theme to use when rendering contents.
custom_templates: Directory containing custom templates.
**config: Configuration passed to the handler.
Returns:
An instance of `VbaHandler`.
"""
return VbaHandler(
collector=VbaCollector(),
renderer=VbaRenderer("vba", theme, custom_templates),
)
209 changes: 209 additions & 0 deletions src/mkdocstrings_handlers/vba/renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""
This module implements a renderer for the VBA language.
Most of this is just copied / hacked together from the Python renderer.
"""

from __future__ import annotations

import enum
import re
import sys
from collections import ChainMap
from typing import Any, Sequence

from griffe.dataclasses import Alias, Object
from markdown import Markdown
from markupsafe import Markup
from mkdocstrings.extension import PluginError
from mkdocstrings.handlers.base import BaseRenderer, CollectorItem
from mkdocstrings.loggers import get_logger

from mkdocstrings_handlers.vba.types import VbaModuleInfo

logger = get_logger(__name__)


class Order(enum.Enum):
"""Enumeration for the possible members ordering."""

alphabetical = "alphabetical"
source = "source"


def _sort_key_alphabetical(item: CollectorItem) -> Any:
# chr(sys.maxunicode) is a string that contains the final unicode
# character, so if 'name' isn't found on the object, the item will go to
# the end of the list.
return item.name or chr(sys.maxunicode)


def _sort_key_source(item: CollectorItem) -> Any:
# if 'lineno' is none, the item will go to the start of the list.
return item.lineno if item.lineno is not None else -1


order_map = {
Order.alphabetical: _sort_key_alphabetical,
Order.source: _sort_key_source,
}


class VbaRenderer(BaseRenderer):
"""The class responsible for loading Jinja templates and rendering them.
It defines some configuration options, implements the `render` method,
and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer].
"""

fallback_theme = "material"
"""
The theme to fall back to.
"""

default_config: dict = {
"show_root_heading": False,
"show_root_toc_entry": True,
"show_root_full_path": True,
"show_root_members_full_path": False,
"show_object_full_path": False,
"show_category_heading": False,
"show_if_no_docstring": False,
"show_signature": True,
"separate_signature": False,
"line_length": 60,
"show_source": True,
"show_bases": True,
"show_submodules": True,
"heading_level": 2,
"members_order": Order.alphabetical.value,
"docstring_section_style": "table",
}
"""The default rendering options.
See [`default_config`][mkdocstrings_handlers.vba.renderer.VbaRenderer.default_config].
Option | Type | Description | Default
------ | ---- | ----------- | -------
**`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False`
**`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True`
**`show_root_full_path`** | `bool` | Show the full VBA path for the root object heading. | `True`
**`show_object_full_path`** | `bool` | Show the full VBA path of every object. | `False`
**`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`
**`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False`
**`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False`
**`show_signature`** | `bool` | Show method and function signatures. | `True`
**`separate_signature`** | `bool` | Whether to put the whole signature in a code block below the heading. | `False`
**`line_length`** | `int` | Maximum line length when formatting code. | `60`
**`show_source`** | `bool` | Show the source code of this object. | `True`
**`show_bases`** | `bool` | Show the base classes of a class. | `True`
**`show_submodules`** | `bool` | When rendering a module, show its submodules recursively. | `True`
**`heading_level`** | `int` | The initial heading level to use. | `2`
**`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`
**`docstring_section_style`** | `str` | The style used to render docstring sections. Options: `table`, `list`, `spacy`. | `table`
""" # noqa: E501

def render(
self,
data: VbaModuleInfo,
config: dict,
) -> str:
final_config = ChainMap(config, self.default_config)
render_type = "module"

template = self.env.get_template(f"{render_type}.html")

# Heading level is a "state" variable, that will change at each step
# of the rendering recursion. Therefore, it's easier to use it as a plain value
# than as an item in a dictionary.
heading_level = final_config["heading_level"]
try:
final_config["members_order"] = Order(final_config["members_order"])
except ValueError:
choices = "', '".join(item.value for item in Order)
raise PluginError(
f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'."
)

return template.render(
**{
"config": final_config,
render_type: data,
"heading_level": heading_level,
"root": True,
},
)

def get_anchors(self, data: VbaModuleInfo) -> list[str]:
return list(
{data.path.as_posix(), *(p.signature.name for p in data.procedures)}
)

def update_env(self, md: Markdown, config: dict) -> None:
super().update_env(md, config)
self.env.trim_blocks = True
self.env.lstrip_blocks = True
self.env.keep_trailing_newline = False
self.env.filters["crossref"] = self.do_crossref
self.env.filters["multi_crossref"] = self.do_multi_crossref
self.env.filters["order_members"] = self.do_order_members

@staticmethod
def do_order_members(
members: Sequence[Object | Alias], order: Order
) -> Sequence[Object | Alias]:
"""Order members given an ordering method.
Parameters:
members: The members to order.
order: The ordering method.
Returns:
The same members, ordered.
"""
return sorted(members, key=order_map[order])

@staticmethod
def do_crossref(path: str, brief: bool = True) -> Markup:
"""Filter to create cross-references.
Parameters:
path: The path to link to.
brief: Show only the last part of the path, add full path as hover.
Returns:
Markup text.
"""
full_path = path
if brief:
path = full_path.split(".")[-1]
return Markup(
"<span data-autorefs-optional-hover={full_path}>{path}</span>"
).format(full_path=full_path, path=path)

@staticmethod
def do_multi_crossref(text: str, code: bool = True) -> Markup:
"""Filter to create cross-references.
Parameters:
text: The text to scan.
code: Whether to wrap the result in a code tag.
Returns:
Markup text.
"""
group_number = 0
variables = {}

def repl(match): # noqa: WPS430
nonlocal group_number # noqa: WPS420
group_number += 1
path = match.group()
path_var = f"path{group_number}"
variables[path_var] = path
return f"<span data-autorefs-optional-hover={{{path_var}}}>{{{path_var}}}</span>"

text = re.sub(r"([\w.]+)", repl, text)
if code:
text = f"<code>{text}</code>"
return Markup(text).format(**variables)
Loading

0 comments on commit 8302ce5

Please sign in to comment.