braindecode/model-explorer
0
1"""Render braindecode rST docstrings to HTML for the model-explorer Space.2 3Braindecode docstrings use NumpyDoc + Sphinx extensions:4 .. figure:: (architecture image)5 :bdg-danger: (sphinx-design colored badges)6 .. versionadded:: (Sphinx admonition)7 .. important:: (admonition)8 .. code-block:: (highlighted code)9 [ref]_ (NumpyDoc citation reference)10 11Pure docutils does not know about the Sphinx directives. Rather than12spinning up a full Sphinx build inside the Space, we:13 141. Pre-process the docstring with regex substitutions that map15 Sphinx-only directives to plain rST equivalents docutils can parse.162. Hand the result to docutils.publish_parts for rST -> HTML.173. Wrap with a small CSS style block that matches braindecode.org colors.18 19This keeps the Space dependency-light (no sphinx at runtime) while still20rendering figures, headings, parameter tables, and references.21"""22 23from __future__ import annotations24 25import inspect26import re27from textwrap import dedent28 29from docutils.core import publish_parts30from docutils.utils import SystemMessage31 32_BADGE_COLORS = {33 "bdg-danger": "#d9534f",34 "bdg-success": "#5cb85c",35 "bdg-primary": "#0072B2",36 "bdg-info": "#56B4E9",37 "bdg-warning": "#E69F00",38 "bdg-secondary": "#6c757d",39 "bdg-light": "#f0f0f0",40 "bdg-dark": "#343a40",41}42 43_BADGE_RE = re.compile(r":(bdg-[a-z]+):`([^`]+)`")44_VERSIONADDED_RE = re.compile(r"^\.\. versionadded::\s*(.+)$", re.MULTILINE)45_VERSIONCHANGED_RE = re.compile(r"^\.\. versionchanged::\s*(.+)$", re.MULTILINE)46_CODE_BLOCK_RE = re.compile(r"^(\s*)\.\. code-block::\s*(\w+)?\s*$", re.MULTILINE)47 48 49def _replace_badges(text: str) -> str:50 """Convert :bdg-danger:`Foundation Model` to inline raw HTML."""51 52 def repl(match: re.Match) -> str:53 cls, label = match.group(1), match.group(2)54 color = _BADGE_COLORS.get(cls, "#888")55 # Use rST raw HTML inline pass-through.56 return (57 f"\n\n.. raw:: html\n\n"58 f" <span class=\"bd-badge\" style=\"background:{color};\">"59 f"{label}</span>\n\n"60 )61 62 return _BADGE_RE.sub(repl, text)63 64 65def _replace_versionadded(text: str) -> str:66 """Convert Sphinx versionadded/versionchanged to plain admonitions."""67 text = _VERSIONADDED_RE.sub(68 r".. note::\n\n *New in version \1.*", text69 )70 text = _VERSIONCHANGED_RE.sub(71 r".. note::\n\n *Changed in version \1.*", text72 )73 return text74 75 76def _normalize_code_block(text: str) -> str:77 """Convert `.. code-block:: python` to vanilla `.. code::` (docutils-OK)."""78 79 def repl(match: re.Match) -> str:80 indent, lang = match.group(1), match.group(2) or ""81 return f"{indent}.. code:: {lang}".rstrip()82 83 return _CODE_BLOCK_RE.sub(repl, text)84 85 86def _strip_unsupported_directives(text: str) -> str:87 """Drop directives that docutils cannot parse and we do not want rendered.88 89 Currently: rubric (treated as a small heading) and bibliography-only items.90 """91 text = re.sub(r"^\.\. rubric::\s*(.+)$", r"\n**\1**\n", text, flags=re.MULTILINE)92 return text93 94 95def preprocess_docstring(doc: str) -> str:96 """Apply all rST → docutils-friendly transformations."""97 if not doc:98 return ""99 doc = dedent(doc)100 doc = _replace_badges(doc)101 doc = _replace_versionadded(doc)102 doc = _normalize_code_block(doc)103 doc = _strip_unsupported_directives(doc)104 return doc105 106 107# All visual styling now lives in app.py's GLOBAL_CSS so it's injected108# once via Blocks(css=...) instead of being re-emitted on every model109# switch. The renderer just returns the structural HTML wrapped in110# .bd-doc.111 112 113def render_docstring_html(doc: str | None) -> str:114 """Render an rST docstring to an HTML fragment wrapped in ``.bd-doc``.115 116 Styling is supplied by app.py's GLOBAL_CSS — this function emits117 structural HTML only. Failures fall back to a ``<pre>`` dump so the118 Space never blanks out.119 """120 if not doc:121 return "<div class='bd-doc'><em>No docstring available.</em></div>"122 123 processed = preprocess_docstring(doc)124 try:125 parts = publish_parts(126 source=processed,127 writer_name="html5",128 settings_overrides={129 "report_level": 5, # suppress all docutils warnings130 "halt_level": 5,131 "embed_stylesheet": False,132 "input_encoding": "unicode",133 "output_encoding": "unicode",134 "doctitle_xform": False,135 "initial_header_level": 2,136 },137 )138 body = parts["html_body"]139 except SystemMessage as exc: # pragma: no cover — defensive140 body = f"<pre>{processed}</pre><p><em>(rST parse error: {exc})</em></p>"141 142 return f"<div class='bd-doc'>{body}</div>"143 144 145def get_signature_str(cls: type) -> str:146 """Return the formatted __init__ signature for display."""147 try:148 sig = inspect.signature(cls.__init__)149 return f"{cls.__name__}{sig}"150 except (ValueError, TypeError):151 return f"{cls.__name__}(...)"152 153 154def get_source_link(cls: type, branch: str = "master") -> str | None:155 """Return a github link to the class definition."""156 try:157 module = inspect.getmodule(cls)158 if module is None or not hasattr(module, "__file__"):159 return None160 rel_path = module.__file__.split("braindecode/", 1)[-1]161 rel_path = "braindecode/" + rel_path162 try:163 _, lineno = inspect.getsourcelines(cls)164 except OSError:165 lineno = 1166 return (167 f"https://github.com/braindecode/braindecode/blob/{branch}/"168 f"{rel_path}#L{lineno}"169 )170 except Exception:171 return None172 