From 3374383d1b0731825b80218ee34472149f4ee80a Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 22 May 2024 17:00:53 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20NEW:=20Create=20custom=20directives?= =?UTF-8?q?=20(#194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You can use the `sd_custom_directives` configuration option in your `conf.py` to add custom directives, with default option values: ```python sd_custom_directives = { "dropdown-syntax": { "inherit": "dropdown", "argument": "Syntax", "options": { "color": "primary", "icon": "code", }, } } ``` The key is the new directive name to add, and the value is a dictionary with the following keys: - `inherit`: The directive to inherit from (e.g. `dropdown`) - `argument`: The default argument (optional, only for directives that take a single argument) - `options`: A dictionary of default options for the directive (optional) --- docs/additional.md | 4 +- docs/badges_buttons.md | 12 +-- docs/cards.md | 20 +--- docs/conf.py | 10 ++ docs/css_classes.md | 4 +- docs/dropdowns.md | 4 +- docs/get_started.md | 23 ++++ docs/grids.md | 20 +--- docs/tabs.md | 8 +- sphinx_design/article_info.py | 8 +- sphinx_design/badges_buttons.py | 9 +- sphinx_design/cards.py | 10 +- sphinx_design/dropdown.py | 9 +- sphinx_design/extension.py | 55 +++++++--- sphinx_design/grids.py | 17 ++- sphinx_design/icons.py | 9 +- sphinx_design/shared.py | 106 ++++++++++++++++++- sphinx_design/tabs.py | 18 ++-- tests/test_snippets.py | 32 ++++++ tests/test_snippets/sd_custom_directives.xml | 9 ++ 20 files changed, 267 insertions(+), 120 deletions(-) create mode 100644 tests/test_snippets/sd_custom_directives.xml diff --git a/docs/additional.md b/docs/additional.md index fd715df..adadad8 100644 --- a/docs/additional.md +++ b/docs/additional.md @@ -19,9 +19,7 @@ normally positioned just below the title of the article (shown below with non-st :class-container: sd-p-2 sd-outline-muted sd-rounded-1 ``` -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/article-info.txt diff --git a/docs/badges_buttons.md b/docs/badges_buttons.md index 0117e1c..ae1bd0b 100644 --- a/docs/badges_buttons.md +++ b/docs/badges_buttons.md @@ -17,9 +17,7 @@ Badges are available in each semantic color, with filled and outline variants: - {bdg-light}`light`, {bdg-light-line}`light-line` - {bdg-dark}`dark`, {bdg-dark-line}`dark-line` -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/badge-basic.txt @@ -40,9 +38,7 @@ The syntax is the same as for the `ref` role. {bdg-ref-primary}`badges` -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/badge-link.txt @@ -94,9 +90,7 @@ Button text Reference Button text ``` -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/button-link.txt diff --git a/docs/cards.md b/docs/cards.md index dd7947f..ac96154 100644 --- a/docs/cards.md +++ b/docs/cards.md @@ -13,9 +13,7 @@ Card content See the [Material Design](https://material.io/components/cards) and [Bootstrap card](https://getbootstrap.com/docs/5.0/layout/grid/) descriptions for further details. -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/card-basic.txt @@ -38,9 +36,7 @@ Card content Footer ::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/card-head-foot.txt @@ -115,9 +111,7 @@ Footer ::::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/card-images.txt @@ -151,9 +145,7 @@ The entire card can be clicked to navigate to . The entire card can be clicked to navigate to the `cards-clickable` reference target. ::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/card-link.txt @@ -223,9 +215,7 @@ content ::: :::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/card-carousel.txt diff --git a/docs/conf.py b/docs/conf.py index 8fde5f9..438742c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,6 +10,16 @@ suppress_warnings = ["design.fa-build"] sd_fontawesome_latex = True +sd_custom_directives = { + "dropdown-syntax": { + "inherit": "dropdown", + "argument": "Syntax", + "options": { + "color": "primary", + "icon": "code", + }, + } +} html_theme = os.environ.get("SPHINX_THEME", "alabaster") html_title = f"Sphinx Design ({html_theme.replace('_', '-')})" diff --git a/docs/css_classes.md b/docs/css_classes.md index 4e22424..b87e1d1 100644 --- a/docs/css_classes.md +++ b/docs/css_classes.md @@ -9,9 +9,7 @@ All CSS classes that are part of sphinx-design are prefixed with `sd-`. Some CSS styled text ::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/div-basic.txt diff --git a/docs/dropdowns.md b/docs/dropdowns.md index 8097c2f..c0372a0 100644 --- a/docs/dropdowns.md +++ b/docs/dropdowns.md @@ -20,9 +20,7 @@ Dropdown content Dropdown content ::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/dropdown-basic.txt diff --git a/docs/get_started.md b/docs/get_started.md index f1bab47..4394c5b 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -43,6 +43,29 @@ sd_hide_title: true ::: :::: +### Creating custom directives + +You can use the `sd_custom_directives` configuration option in your `conf.py` to add custom directives, with default option values: + +```python +sd_custom_directives = { + "dropdown-syntax": { + "inherit": "dropdown", + "argument": "Syntax", + "options": { + "color": "primary", + "icon": "code", + }, + } +} +``` + +The key is the new directive name to add, and the value is a dictionary with the following keys: + +- `inherit`: The directive to inherit from (e.g. `dropdown`) +- `argument`: The default argument (optional, only for directives that take a single argument) +- `options`: A dictionary of default options for the directive (optional) + ## Supported browsers - Chrome >= 60 diff --git a/docs/grids.md b/docs/grids.md index 5c37bd7..69b987d 100644 --- a/docs/grids.md +++ b/docs/grids.md @@ -29,9 +29,7 @@ D ::: :::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/grid-basic.txt @@ -80,9 +78,7 @@ B ::: :::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/grid-card.txt @@ -121,9 +117,7 @@ B ::: :::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/grid-gutter.txt @@ -160,9 +154,7 @@ C ::: :::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/grid-card-columns.txt @@ -254,9 +246,7 @@ Content :::::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/grid-nested.txt diff --git a/docs/tabs.md b/docs/tabs.md index 6664a6b..45aa2d7 100644 --- a/docs/tabs.md +++ b/docs/tabs.md @@ -17,9 +17,7 @@ Content 2 :::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/tab-basic.txt @@ -70,9 +68,7 @@ Content 2 :::: -`````{dropdown} Syntax -:icon: code -:color: primary +`````{dropdown-syntax} ````{tab-set-code} ```{literalinclude} ./snippets/myst/tab-sync.txt diff --git a/sphinx_design/article_info.py b/sphinx_design/article_info.py index 9201daf..7961d12 100644 --- a/sphinx_design/article_info.py +++ b/sphinx_design/article_info.py @@ -3,10 +3,9 @@ from docutils import nodes from docutils.parsers.rst import directives from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective from .icons import get_octicon -from .shared import SEMANTIC_COLORS, create_component, make_choice +from .shared import SEMANTIC_COLORS, SdDirective, create_component, make_choice def setup_article_info(app: Sphinx): @@ -14,7 +13,7 @@ def setup_article_info(app: Sphinx): app.add_directive("article-info", ArticleInfoDirective) -class ArticleInfoDirective(SphinxDirective): +class ArticleInfoDirective(SdDirective): """ """ has_content = False @@ -48,8 +47,7 @@ def _parse_text( output = [para] return output - def run(self) -> list[nodes.Node]: # noqa: PLR0915 - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: # noqa: PLR0915 parse_fields = True # parse field text top_grid = create_component( diff --git a/sphinx_design/badges_buttons.py b/sphinx_design/badges_buttons.py index 52a46a3..c610d12 100644 --- a/sphinx_design/badges_buttons.py +++ b/sphinx_design/badges_buttons.py @@ -4,9 +4,9 @@ from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.application import Sphinx -from sphinx.util.docutils import ReferenceRole, SphinxDirective, SphinxRole +from sphinx.util.docutils import ReferenceRole, SphinxRole -from sphinx_design.shared import SEMANTIC_COLORS, make_choice, text_align +from sphinx_design.shared import SEMANTIC_COLORS, SdDirective, make_choice, text_align ROLE_NAME_BADGE_PREFIX = "bdg" ROLE_NAME_LINK_PREFIX = "bdg-link" @@ -127,7 +127,7 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]: return [node], [] -class _ButtonDirective(SphinxDirective): +class _ButtonDirective(SdDirective): """A base button directive.""" required_arguments = 1 @@ -155,8 +155,7 @@ def create_ref_node( """Create the reference node.""" raise NotImplementedError - def run(self) -> list[nodes.Node]: - """Run the directive.""" + 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"] diff --git a/sphinx_design/cards.py b/sphinx_design/cards.py index a7d2619..c8225e9 100644 --- a/sphinx_design/cards.py +++ b/sphinx_design/cards.py @@ -13,6 +13,7 @@ from .shared import ( WARNING_TYPE, PassthroughTextElement, + SdDirective, create_component, is_component, make_choice, @@ -45,7 +46,7 @@ class CardContent(NamedTuple): footer: Optional[tuple[int, StringList]] = None -class CardDirective(SphinxDirective): +class CardDirective(SdDirective): """A card component.""" has_content = True @@ -73,7 +74,7 @@ class CardDirective(SphinxDirective): "class-img-bottom": directives.class_option, } - def run(self) -> list[nodes.Node]: + def run_with_defaults(self) -> list[nodes.Node]: return [self.create_card(self, self.arguments, self.options)] @classmethod @@ -256,7 +257,7 @@ def add_card_child_classes(node): # ] -class CardCarouselDirective(SphinxDirective): +class CardCarouselDirective(SdDirective): """A component, which is a container for cards in a single scrollable row.""" has_content = True @@ -266,8 +267,7 @@ class CardCarouselDirective(SphinxDirective): "class": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: self.assert_has_content() try: cols = make_choice([str(i) for i in range(1, 13)])( diff --git a/sphinx_design/dropdown.py b/sphinx_design/dropdown.py index 8732c86..17d4d04 100644 --- a/sphinx_design/dropdown.py +++ b/sphinx_design/dropdown.py @@ -4,10 +4,10 @@ from docutils.parsers.rst import directives from sphinx.application import Sphinx from sphinx.transforms.post_transforms import SphinxPostTransform -from sphinx.util.docutils import SphinxDirective from sphinx_design.shared import ( SEMANTIC_COLORS, + SdDirective, create_component, is_component, make_choice, @@ -52,7 +52,7 @@ def depart_dropdown_title(self, node): self.body.append("") -class DropdownDirective(SphinxDirective): +class DropdownDirective(SdDirective): """A directive to generate a collapsible container. Note: This directive generates a single container, @@ -87,8 +87,7 @@ class DropdownDirective(SphinxDirective): "class-body": directives.class_option, } - def run(self): - """Run the directive""" + def run_with_defaults(self) -> list[nodes.Node]: # default classes classes = { "container_classes": self.options.get("margin", ["sd-mb-3"]) @@ -149,7 +148,7 @@ class DropdownHtmlTransform(SphinxPostTransform): default_priority = 199 formats = ("html",) - def run(self): + def run(self) -> None: """Run the transform""" document: nodes.document = self.document for node in findall(document)(lambda node: is_component(node, "dropdown")): diff --git a/sphinx_design/extension.py b/sphinx_design/extension.py index f7d582c..9dd5f28 100644 --- a/sphinx_design/extension.py +++ b/sphinx_design/extension.py @@ -1,3 +1,5 @@ +from contextlib import contextmanager +from functools import partial import hashlib from pathlib import Path @@ -7,7 +9,6 @@ from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.transforms import SphinxTransform -from sphinx.util.docutils import SphinxDirective from . import compiled as static_module from ._compat import findall, read_text @@ -17,7 +18,12 @@ from .dropdown import setup_dropdown from .grids import setup_grids from .icons import setup_icons -from .shared import PassthroughTextElement, create_component +from .shared import ( + PassthroughTextElement, + SdDirective, + create_component, + setup_custom_directives, +) from .tabs import setup_tabs @@ -38,17 +44,36 @@ def setup_extension(app: Sphinx) -> None: man=(visit_depart_null, visit_depart_null), texinfo=(visit_depart_null, visit_depart_null), ) - app.add_directive( - "div", Div, override=True - ) # override sphinx-panels implementation - app.add_transform(AddFirstTitleCss) - setup_badges_and_buttons(app) - setup_cards(app) - setup_grids(app) - setup_dropdown(app) - setup_icons(app) - setup_tabs(app) - setup_article_info(app) + with capture_directives(app) as directive_map: + app.add_directive("div", Div, override=True) + app.add_transform(AddFirstTitleCss) + setup_badges_and_buttons(app) + setup_cards(app) + setup_grids(app) + setup_dropdown(app) + setup_icons(app) + setup_tabs(app) + setup_article_info(app) + + app.add_config_value("sd_custom_directives", {}, "env") + app.connect( + "config-inited", partial(setup_custom_directives, directive_map=directive_map) + ) + + +@contextmanager +def capture_directives(app: Sphinx): + """Capture the directives that are registered by the extension.""" + directive_map = {} + add_directive = app.add_directive + + def _add_directive(name, directive, **kwargs): + directive_map[name] = directive + add_directive(name, directive, **kwargs) + + app.add_directive = _add_directive + yield directive_map + app.add_directive = add_directive def update_css_js(app: Sphinx): @@ -111,7 +136,7 @@ def visit_depart_null(self, node: nodes.Element) -> None: """visit/depart passthrough""" -class Div(SphinxDirective): +class Div(SdDirective): """Same as the ``container`` directive, but does not add the ``container`` class in HTML outputs, which can interfere with Bootstrap CSS. @@ -122,7 +147,7 @@ class Div(SphinxDirective): option_spec = {"style": directives.unchanged, "name": directives.unchanged} has_content = True - def run(self): + def run_with_defaults(self) -> list[nodes.Node]: try: if self.arguments: classes = directives.class_option(self.arguments[0]) diff --git a/sphinx_design/grids.py b/sphinx_design/grids.py index a0bab89..3614822 100644 --- a/sphinx_design/grids.py +++ b/sphinx_design/grids.py @@ -3,12 +3,12 @@ from docutils import nodes from docutils.parsers.rst import directives from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger from .cards import CardDirective from .shared import ( WARNING_TYPE, + SdDirective, create_component, is_component, make_choice, @@ -94,7 +94,7 @@ def gutter_option(argument: Optional[str]) -> list[str]: return _media_option(argument, "sd-g-", min_num=0, max_num=5) -class GridDirective(SphinxDirective): +class GridDirective(SdDirective): """A grid component, which is a container for grid items (i.e. columns).""" has_content = True @@ -111,8 +111,7 @@ class GridDirective(SphinxDirective): "class-row": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: try: column_classes = ( row_columns_option(self.arguments[0]) if self.arguments else [] @@ -157,7 +156,7 @@ def run(self) -> list[nodes.Node]: return [container] -class GridItemDirective(SphinxDirective): +class GridItemDirective(SdDirective): """An item within a grid row. Can "occupy" 1 to 12 columns. @@ -174,8 +173,7 @@ class GridItemDirective(SphinxDirective): "class": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: if not is_component(self.state_machine.node, "grid-row"): LOGGER.warning( f"The parent of a 'grid-item' should be a 'grid-row' [{WARNING_TYPE}.grid]", @@ -205,7 +203,7 @@ def run(self) -> list[nodes.Node]: return [column] -class GridItemCardDirective(SphinxDirective): +class GridItemCardDirective(SdDirective): """An item within a grid row, with an internal card.""" has_content = True @@ -237,8 +235,7 @@ class GridItemCardDirective(SphinxDirective): "class-img-bottom": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: if not is_component(self.state_machine.node, "grid-row"): LOGGER.warning( f"The parent of a 'grid-item' should be a 'grid-row' [{WARNING_TYPE}.grid]", diff --git a/sphinx_design/icons.py b/sphinx_design/icons.py index a2555c1..9aceb12 100644 --- a/sphinx_design/icons.py +++ b/sphinx_design/icons.py @@ -8,11 +8,11 @@ from docutils.parsers.rst import directives from sphinx.application import Sphinx from sphinx.util import logging -from sphinx.util.docutils import SphinxDirective, SphinxRole +from sphinx.util.docutils import SphinxRole from . import compiled from ._compat import read_text -from .shared import WARNING_TYPE +from .shared import WARNING_TYPE, SdDirective logger = logging.getLogger(__name__) @@ -142,7 +142,7 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]: return [node], [] -class AllOcticons(SphinxDirective): +class AllOcticons(SdDirective): """Directive to generate all octicon icons. Primarily for self documentation. @@ -152,8 +152,7 @@ class AllOcticons(SphinxDirective): "class": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: classes = self.options.get("class", []) table = nodes.table() group = nodes.tgroup(cols=2) diff --git a/sphinx_design/shared.py b/sphinx_design/shared.py index 9b9bd92..a092048 100644 --- a/sphinx_design/shared.py +++ b/sphinx_design/shared.py @@ -1,10 +1,18 @@ """Shared constants and functions.""" +from __future__ import annotations + from collections.abc import Sequence -from typing import Optional +from typing import final from docutils import nodes from docutils.parsers.rst import directives +from sphinx.application import Sphinx +from sphinx.config import Config +from sphinx.util.docutils import SphinxDirective +from sphinx.util.logging import getLogger + +LOGGER = getLogger(__name__) WARNING_TYPE = "design" @@ -23,6 +31,94 @@ ) +def setup_custom_directives( + app: Sphinx, config: Config, directive_map: dict[str, SdDirective] +) -> None: + conf_value = config.sd_custom_directives + + def _warn(msg): + LOGGER.warning( + f"sd_custom_directives: {msg}", type=WARNING_TYPE, subtype="config" + ) + + if not isinstance(conf_value, dict): + _warn("must be a dictionary") + config.sd_custom_directives = {} + return + for name, data in conf_value.items(): + if not isinstance(name, str): + _warn(f"key must be a string: {name!r}") + continue + if not isinstance(data, dict): + _warn(f"{name!r} value must be a dictionary") + continue + if "inherit" not in data: + _warn(f"{name!r} value must have an 'inherit' key") + continue + if data["inherit"] not in directive_map: + _warn(f"'{name}.inherit' is an unknown directive key: {data['inherit']}") + continue + directive_cls = directive_map[data["inherit"]] + if "options" in data: + if not isinstance(data["options"], dict): + _warn(f"'{name}.options' value must be a dictionary") + continue + if "argument" in data and not isinstance(data["argument"], str): + _warn(f"'{name}.argument' value must be a string") + continue + for key, value in data["options"].items(): + if key not in directive_cls.option_spec: + _warn(f"'{name}.options' unknown key {key!r}") + continue + if not isinstance(value, str): + _warn(f"'{name}.options.{key}' value must be a string") + continue + app.add_directive(name, directive_cls, override=True) + + +class SdDirective(SphinxDirective): + """Base class for all sphinx-design directives. + + Having a base class allows for shared functionality to be implemented in one place. + Namely, we allow for default options to be configured, per directive name. + + This class should be sub-classed by all directives in the sphinx-design extension. + """ + + # TODO perhaps ideally there would be separate sphinx extension, + # that generalises the concept of default directive options (that does not require subclassing) + # but for now I couldn't think of a trivial way to achieve this. + + @final + def run(self) -> list[nodes.Node]: + """Run the directive. + + This method should not be overridden, instead override `run_with_defaults`. + """ + if data := self.config.sd_custom_directives.get(self.name): + if (not self.arguments) and (argument := data.get("argument")): # type: ignore[has-type] + self.arguments = [str(argument)] + for key, value in data.get("options", {}).items(): + if key not in self.options and key in self.option_spec: + try: + self.options[key] = self.option_spec[key](str(value)) + except Exception as exc: + LOGGER.warning( + f"Invalid default option {key!r} for {self.name!r}: {exc}", + type=WARNING_TYPE, + subtype="directive", + location=(self.env.docname, self.lineno), + ) + return self.run_with_defaults() + + def run_with_defaults(self) -> list[nodes.Node]: + """Run the directive, after default options have been set. + + This method should be overridden by subclasses. + """ + raise NotImplementedError + + def create_component( name: str, classes: Sequence[str] = (), @@ -53,7 +149,7 @@ def make_choice(choices: Sequence[str]): def _margin_or_padding_option( - argument: Optional[str], + argument: str | None, class_prefix: str, allowed: Sequence[str], ) -> list[str]: @@ -78,7 +174,7 @@ def _margin_or_padding_option( ) -def margin_option(argument: Optional[str]) -> list[str]: +def margin_option(argument: str | None) -> list[str]: """Validate the margin is one (all) or four (top bottom left right) integers, between 0 and 5 or 'auto'. """ @@ -87,14 +183,14 @@ def margin_option(argument: Optional[str]) -> list[str]: ) -def padding_option(argument: Optional[str]) -> list[str]: +def padding_option(argument: str | None) -> list[str]: """Validate the padding is one (all) or four (top bottom left right) integers, between 0 and 5. """ return _margin_or_padding_option(argument, "sd-p", ("0", "1", "2", "3", "4", "5")) -def text_align(argument: Optional[str]) -> list[str]: +def text_align(argument: str | None) -> list[str]: """Validate the text align is left, right, center or justify.""" value = directives.choice(argument, ["left", "right", "center", "justify"]) return [f"sd-text-{value}"] diff --git a/sphinx_design/tabs.py b/sphinx_design/tabs.py index 0bcca27..b9113f9 100644 --- a/sphinx_design/tabs.py +++ b/sphinx_design/tabs.py @@ -2,11 +2,10 @@ from docutils.parsers.rst import directives from sphinx.application import Sphinx from sphinx.transforms.post_transforms import SphinxPostTransform -from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger from ._compat import findall -from .shared import WARNING_TYPE, create_component, is_component +from .shared import WARNING_TYPE, SdDirective, create_component, is_component LOGGER = getLogger(__name__) @@ -20,7 +19,7 @@ def setup_tabs(app: Sphinx) -> None: app.add_node(sd_tab_label, html=(visit_tab_label, depart_tab_label)) -class TabSetDirective(SphinxDirective): +class TabSetDirective(SdDirective): """A container for a set of tab items.""" has_content = True @@ -28,8 +27,7 @@ class TabSetDirective(SphinxDirective): "class": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: self.assert_has_content() tab_set = create_component( "tab-set", classes=["sd-tab-set", *self.options.get("class", [])] @@ -49,7 +47,7 @@ def run(self) -> list[nodes.Node]: return [tab_set] -class TabItemDirective(SphinxDirective): +class TabItemDirective(SdDirective): """A single tab item in a tab set. Note: This directive generates a single container, @@ -79,8 +77,7 @@ class TabItemDirective(SphinxDirective): "class-content": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: self.assert_has_content() if not is_component(self.state_machine.node, "tab-set"): LOGGER.warning( @@ -119,7 +116,7 @@ def run(self) -> list[nodes.Node]: return [tab_item] -class TabSetCodeDirective(SphinxDirective): +class TabSetCodeDirective(SdDirective): """A container for a set of tab items, generated from code blocks.""" has_content = True @@ -129,8 +126,7 @@ class TabSetCodeDirective(SphinxDirective): "class-item": directives.class_option, } - def run(self) -> list[nodes.Node]: - """Run the directive.""" + def run_with_defaults(self) -> list[nodes.Node]: self.assert_has_content() tab_set = create_component( "tab-set", classes=["sd-tab-set", *self.options.get("class-set", [])] diff --git a/tests/test_snippets.py b/tests/test_snippets.py index 987b727..986d1a3 100644 --- a/tests/test_snippets.py +++ b/tests/test_snippets.py @@ -150,3 +150,35 @@ def test_sd_hide_title_myst( extension=".xml", encoding="utf8", ) + + +def test_sd_custom_directives( + sphinx_builder: Callable[..., SphinxBuilder], file_regression +): + """Test that the defaults are used.""" + builder = sphinx_builder( + conf_kwargs={ + "extensions": ["myst_parser", "sphinx_design"], + "sd_custom_directives": { + "dropdown-syntax": { + "inherit": "dropdown", + "argument": "Syntax", + "options": { + "color": "primary", + "icon": "code", + }, + } + }, + } + ) + content = "# Heading\n\n```{dropdown-syntax}\ncontent\n```" + builder.src_path.joinpath("index.md").write_text(content, encoding="utf8") + builder.build() + doctree = builder.get_doctree("index", post_transforms=False) + doctree.attributes.pop("translation_progress", None) # added in sphinx 7.1 + file_regression.check( + doctree.pformat(), + basename="sd_custom_directives", + extension=".xml", + encoding="utf8", + ) diff --git a/tests/test_snippets/sd_custom_directives.xml b/tests/test_snippets/sd_custom_directives.xml new file mode 100644 index 0000000..e5a6b63 --- /dev/null +++ b/tests/test_snippets/sd_custom_directives.xml @@ -0,0 +1,9 @@ + +
+ + Heading + <container body_classes="" chevron="True" container_classes="sd-mb-3" design_component="dropdown" has_title="True" icon="code" is_div="True" opened="False" title_classes="sd-bg-primary sd-bg-text-primary" type="dropdown"> + <rubric> + Syntax + <paragraph> + content