File: badges_buttons.py

package info (click to toggle)
sphinx-design 0.6.1-2
  • links: PTS
  • area: main
  • in suites: sid
  • size: 10,680 kB
  • sloc: python: 2,086; xml: 900; javascript: 56; sh: 8; makefile: 3
file content (235 lines) | stat: -rw-r--r-- 8,212 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
from typing import Optional

from docutils import nodes
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.util.docutils import ReferenceRole, SphinxRole

from sphinx_design.shared import SEMANTIC_COLORS, SdDirective, make_choice, text_align

ROLE_NAME_BADGE_PREFIX = "bdg"
ROLE_NAME_LINK_PREFIX = "bdg-link"
ROLE_NAME_REF_PREFIX = "bdg-ref"
DIRECTIVE_NAME_BUTTON_LINK = "button-link"
DIRECTIVE_NAME_BUTTON_REF = "button-ref"

# TODO defining arbitrary classes for badges
# (maybe split text right of last `;`, then split that by comma)
# in particular for rounded-pill class etc


def setup_badges_and_buttons(app: Sphinx) -> None:
    """Setup the badge components."""
    app.add_role(ROLE_NAME_BADGE_PREFIX, BadgeRole())
    app.add_role(ROLE_NAME_LINK_PREFIX, LinkBadgeRole())
    app.add_role(ROLE_NAME_REF_PREFIX, XRefBadgeRole())
    for color in SEMANTIC_COLORS:
        app.add_role("-".join((ROLE_NAME_BADGE_PREFIX, color)), BadgeRole(color))
        app.add_role(
            "-".join((ROLE_NAME_BADGE_PREFIX, color, "line")),
            BadgeRole(color, outline=True),
        )
        app.add_role("-".join((ROLE_NAME_LINK_PREFIX, color)), LinkBadgeRole(color))
        app.add_role(
            "-".join((ROLE_NAME_LINK_PREFIX, color, "line")),
            LinkBadgeRole(color, outline=True),
        )
        app.add_role("-".join((ROLE_NAME_REF_PREFIX, color)), XRefBadgeRole(color))
        app.add_role(
            "-".join((ROLE_NAME_REF_PREFIX, color, "line")),
            XRefBadgeRole(color, outline=True),
        )

    app.add_directive(DIRECTIVE_NAME_BUTTON_LINK, ButtonLinkDirective)
    app.add_directive(DIRECTIVE_NAME_BUTTON_REF, ButtonRefDirective)


def create_bdg_classes(color: Optional[str], outline: bool) -> list[str]:
    """Create the badge classes."""
    classes = [
        "sd-sphinx-override",
        "sd-badge",
    ]
    if color is None:
        return classes
    if outline:
        classes.extend([f"sd-outline-{color}", f"sd-text-{color}"])
    else:
        classes.extend([f"sd-bg-{color}", f"sd-bg-text-{color}"])
    return classes


class BadgeRole(SphinxRole):
    """Role to display a badge."""

    def __init__(self, color: Optional[str] = None, *, outline: bool = False) -> None:
        super().__init__()
        self.color = color
        self.outline = outline

    def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
        """Run the role."""
        node = nodes.inline(
            self.rawtext,
            self.text,
            classes=create_bdg_classes(self.color, self.outline),
        )
        self.set_source_info(node)
        return [node], []


class LinkBadgeRole(ReferenceRole):
    """Role to display a badge with an external link."""

    def __init__(self, color: Optional[str] = None, *, outline: bool = False) -> None:
        super().__init__()
        self.color = color
        self.outline = outline

    def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
        """Run the role."""
        node = nodes.reference(
            self.rawtext,
            refuri=self.target,
            classes=create_bdg_classes(self.color, self.outline),
        )
        # TODO open in new tab
        self.set_source_info(node)
        # if self.target != self.title:
        #     node["reftitle"] = self.target
        node += nodes.inline(self.title, self.title)
        return [node], []


class XRefBadgeRole(ReferenceRole):
    """Role to display a badge with an internal link."""

    def __init__(self, color: Optional[str] = None, *, outline: bool = False) -> None:
        super().__init__()
        self.color = color
        self.outline = outline

    def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
        """Run the role."""
        options = {
            "classes": create_bdg_classes(self.color, self.outline),
            "reftarget": self.target,
            "refdoc": self.env.docname,
            "refdomain": "",
            "reftype": "any",
            "refexplicit": self.has_explicit_title,
            "refwarn": True,
        }
        node = addnodes.pending_xref(self.rawtext, **options)
        self.set_source_info(node)
        node += nodes.inline(self.title, self.title, classes=["xref", "any"])
        return [node], []


class _ButtonDirective(SdDirective):
    """A base button directive."""

    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = True
    has_content = True
    option_spec = {
        "color": make_choice(SEMANTIC_COLORS),
        "outline": directives.flag,
        "align": text_align,
        # expand to fit parent width
        "expand": directives.flag,
        # make parent also clickable
        "click-parent": directives.flag,
        "tooltip": directives.unchanged_required,
        "shadow": directives.flag,
        # ref button only
        "ref-type": make_choice(["any", "ref", "doc", "myst"]),
        "class": directives.class_option,
    }

    def create_ref_node(
        self, rawtext: str, target: str, explicit_title: bool, classes: list[str]
    ) -> nodes.Node:
        """Create the reference node."""
        raise NotImplementedError

    def run_with_defaults(self) -> list[nodes.Node]:
        rawtext = self.arguments[0]
        target = directives.uri(rawtext)
        classes = ["sd-sphinx-override", "sd-btn", "sd-text-wrap"]
        if "color" in self.options:
            if "outline" in self.options:
                classes.append(f"sd-btn-outline-{self.options['color']}")
            else:
                classes.append(f"sd-btn-{self.options['color']}")
        if "click-parent" in self.options:
            classes.append("sd-stretched-link")
        if "shadow" in self.options:
            classes.append("sd-shadow-sm")
        if "class" in self.options:
            classes.extend(self.options["class"])
        node = self.create_ref_node(rawtext, target, bool(self.content), classes)
        # TODO open in new tab
        self.set_source_info(node)
        if "tooltip" in self.options:
            node["reftitle"] = self.options["tooltip"]  # TODO escape HTML

        if self.content:
            textnodes, _ = self.state.inline_text(
                "\n".join(self.content), self.lineno + self.content_offset
            )
            content = nodes.inline("", "")
            content.extend(textnodes)
        else:
            content = nodes.inline(target, target)
        node.append(content)

        if "expand" in self.options:
            grid_container = nodes.inline(classes=["sd-d-grid"])
            self.set_source_info(grid_container)
            grid_container += node
            node = grid_container

        # `visit_reference` requires that a reference be inside a `TextElement` parent
        container = nodes.paragraph(classes=self.options.get("align", []))
        self.set_source_info(container)
        container += node

        return [container]


class ButtonLinkDirective(_ButtonDirective):
    """A button directive with an external link."""

    def create_ref_node(
        self, rawtext: str, target: str, explicit_title: bool, classes: list[str]
    ) -> nodes.Node:
        """Create the reference node."""
        return nodes.reference(
            rawtext,
            refuri=target,
            classes=classes,
        )


class ButtonRefDirective(_ButtonDirective):
    """A button directive with an internal link."""

    def create_ref_node(
        self, rawtext: str, target: str, explicit_title: bool, classes: list[str]
    ) -> nodes.Node:
        """Create the reference node."""
        ref_type = self.options.get("ref-type", "any")
        options = {
            # TODO the presence of classes raises an error if the link cannot be found
            "classes": classes,
            "reftarget": target,
            "refdoc": self.env.docname,
            "refdomain": "std" if ref_type in {"ref", "doc"} else "",
            "reftype": ref_type,
            "refexplicit": explicit_title,
            "refwarn": True,
        }
        return addnodes.pending_xref(rawtext, **options)