Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

- ✨ NEW: FontAwesome v6 roles (`fa-solid`/`fa-brands`/`fa-regular`) and
declarative CSS loading via the new `sd_fontawesome_source` /
`sd_fontawesome_cdn_url` options (the supported path for FontAwesome Pro
kits) ({pr}`285`, {issue}`174`)
- 👌 IMPROVE: `sd_fontawesome_latex` now also accepts `"fontawesome5"` (adding
`fontawesome5.sty` and emitting `\faIcon{...}`), resolving package clashes
with themes that load `fontawesome5`; `True`/`False` keep working unchanged
({pr}`285`, {issue}`242`)
- 🐛 FIX: Synced tabs stay in sync when activated by click-and-drag or keyboard,
by syncing on the radio input's `change` event rather than a label click
({pr}`284`, {issue}`46`)
Expand Down
116 changes: 94 additions & 22 deletions docs/badges_buttons.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,48 +240,120 @@ Not all icons are available for each flavor, but most are. Instead of displaying

### FontAwesome Icons

FontAwesome icons are added via the Fontawesome CSS classes.
If the theme you are using does not already include the FontAwesome CSS, it should be loaded in your configuration from a [font-awesome CDN](https://cdnjs.com/libraries/font-awesome), with the [html_css_files](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files) option, e.g.:
FontAwesome icons are added via the FontAwesome CSS classes, with the
`fa-solid`, `fa-brands` and `fa-regular` roles (the FontAwesome v6 style names):

- A solid icon {fa-solid}`rocket;sd-text-primary`, some more text.
- A brand icon {fa-brands}`github`, some more text.
- A regular icon {fa-regular}`bell;sd-text-warning`, some more text.

Each role emits exactly the classes it is named after: the `fa-solid` role
applied to `rocket` produces `<span class="fa-solid fa-rocket">`.

#### Loading the FontAwesome CSS

sphinx-design does **not** bundle the FontAwesome CSS.
By default (`sd_fontawesome_source = "none"`) you, or your theme, are
responsible for making it available (many themes already include it).

To have sphinx-design load it for you from a
[FontAwesome CDN](https://cdnjs.com/libraries/font-awesome),
so you no longer have to hand-edit
[`html_css_files`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files),
set:

```python
html_css_files = [
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"
]
sd_fontawesome_source = "cdn"
```

Use either `fa` (deprecated in font-awesome v5), `fas`, `fab` or `far` for the role name.
Note that not all regular style icons are free, `far` role only works with free ones.
This adds `sd_fontawesome_cdn_url` (a FontAwesome v6 `all.min.css` by default)
to the HTML output. Override `sd_fontawesome_cdn_url` to pin a different
version or a self-hosted copy.

````{warning}
Since the FontAwesome icons are fetched directly from their distributed CSS, specifying a height/size to the `fa*` roles is not supported.
Since the FontAwesome icons are fetched directly from their distributed CSS, specifying a height/size to the icon roles is not supported.
However, you can always add your custom CSS class that controls the `font-size` property.

If a height/size is supplied to a `fa*` role, then it will be interpreted as a CSS class.
There can only be a maximum of 1 `;` in the `fa*` roles' arguments
If a height/size is supplied to an icon role, then it will be interpreted as a CSS class.
There can only be a maximum of 1 `;` in the roles' arguments
````

````{tab-set-code}
```markdown
- An icon {fas}`spinner;sd-text-primary`, some more text.
- An icon {fab}`github`, some more text.
- An icon {fab}`gitkraken;sd-text-success fa-xl`, some more text.
- An icon {fas}`skull;sd-text-danger`, some more text.
- An icon {fa-solid}`spinner;sd-text-primary`, some more text.
- An icon {fa-brands}`github`, some more text.
- An icon {fa-brands}`gitkraken;sd-text-success fa-xl`, some more text.
- An icon {fa-solid}`skull;sd-text-danger`, some more text.
```
```rst
- An icon :fas:`spinner;sd-text-primary`, some more text.
- An icon :fab:`github`, some more text.
- An icon :fab:`gitkraken;sd-text-success fa-xl`, some more text.
- An icon :fas:`skull;sd-text-danger`, some more text.
- An icon :fa-solid:`spinner;sd-text-primary`, some more text.
- An icon :fa-brands:`github`, some more text.
- An icon :fa-brands:`gitkraken;sd-text-success fa-xl`, some more text.
- An icon :fa-solid:`skull;sd-text-danger`, some more text.
```
````

- An icon {fa-solid}`spinner;sd-text-primary`, some more text.
- An icon {fa-brands}`github`, some more text.
- An icon {fa-brands}`gitkraken;sd-text-success fa-xl`, some more text.
- An icon {fa-solid}`skull;sd-text-danger`, some more text.

#### Using FontAwesome Pro kits

If you use a [FontAwesome Pro kit](https://fontawesome.com/kits), keep
`sd_fontawesome_source = "none"` (do **not** also load the free CDN, whose
own font-face would fight your kit), load the kit as usual, and use the v6
role names above (`fa-solid`/`fa-brands`/`fa-regular`), which emit exactly the
classes a Pro kit expects.

#### Concise role names

The `fas`, `fab` and `far` roles (and `fa`, which FontAwesome itself
deprecated in v5) are equally supported, with no plans to remove them. Each
role name is emitted verbatim as the leading CSS class, so these produce the
v4/v5 class scheme (`fas fa-...`):

- An icon {fas}`spinner;sd-text-primary`, some more text.
- An icon {fab}`github`, some more text.
- An icon {fab}`gitkraken;sd-text-success fa-xl`, some more text.
- An icon {fas}`skull;sd-text-danger`, some more text.
- An icon {far}`bell`, some more text.

Pick whichever spelling matches the FontAwesome CSS you load: the free CDN
builds define both class schemes, so the concise names work fine there. The
`fa-solid`/`fa-brands`/`fa-regular` names are only *required* for FontAwesome
v6+ setups that drop the compatibility aliases — most notably Pro kits — and
have the side benefit of matching what fontawesome.com shows for each icon.
Note that not all regular style icons are free; `far`/`fa-regular` only work
with the free ones.

By default, icons will only be output in HTML formats. But if you want FontAwesome icons to be output on LaTeX, using the [fontawesome package](https://ctan.org/pkg/fontawesome), you can add to your configuration:
#### FontAwesome in LaTeX output

By default, icons are only rendered for HTML builders.
To also render them in LaTeX output, set `sd_fontawesome_latex` to the LaTeX
package you want to use:

| `sd_fontawesome_latex` | LaTeX package | Rendering |
| ---------------------- | ------------- | --------- |
| `False` / `"none"` (default) | – | icons skipped (one warning per build) |
| `True` / `"fontawesome"` | [`fontawesome`](https://ctan.org/pkg/fontawesome) | `\faicon{name}` |
| `"fontawesome5"` | [`fontawesome5`](https://ctan.org/pkg/fontawesome5) | `\faIcon{name}` (see below) |

```python
sd_fontawesome_latex = True
sd_fontawesome_latex = "fontawesome5"
```

With `"fontawesome5"`, the icon style is mapped to that package's conventions:
brand icons resolve by name (`\faIcon{github}`), regular-style icons use the
optional style argument (`\faIcon[regular]{name}`), and solid icons use the
default (`\faIcon{name}`).

If your theme (or another extension) already loads the `fontawesome5` package,
set `sd_fontawesome_latex = "fontawesome5"` so both agree, avoiding the LaTeX
error that comes from mixing the `fontawesome` and `fontawesome5` packages.

```{note}
The LaTeX packages predate FontAwesome 6 and resolve icons by their **v5**
(or v4, for `fontawesome`) names. Icons that were renamed in v6 (for example
`arrow-up-right-from-square`, formerly `external-link-alt`) render in HTML
but will raise an "icon not found" error when building PDF output — use the
older name if you need LaTeX support for such an icon.
```
13 changes: 6 additions & 7 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
html_favicon = "_static/logo_square.svg"

if html_theme not in ("sphinx_book_theme", "pydata_sphinx_theme"):
html_css_files = [
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"
]
# let sphinx-design load FontAwesome from the CDN, rather than
# hand-editing html_css_files (the sphinx_book/pydata themes bundle it)
sd_fontawesome_source = "cdn"
if html_theme == "alabaster":
html_logo = ""
html_theme_options = {
Expand All @@ -67,10 +67,8 @@
"home_page_in_toc": False,
}
if html_theme == "furo":
html_css_files = [
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/fontawesome.min.css",
"furo.css",
]
# FontAwesome is loaded via sd_fontawesome_source="cdn" (set above)
html_css_files = ["furo.css"]
html_theme_options = {
"sidebar_hide_name": True,
}
Expand All @@ -81,6 +79,7 @@
if html_theme == "sphinx_immaterial":
extensions.append("sphinx_immaterial")
html_css_files = ["sphinx_immaterial.css"]
sd_fontawesome_source = "none" # immaterial provides its own icon fonts
html_theme_options = {
"icon": {
"repo": "fontawesome/brands/github",
Expand Down
102 changes: 98 additions & 4 deletions sphinx_design/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from sphinx.util.logging import getLogger

if TYPE_CHECKING:
from collections.abc import Sequence

from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
Expand Down Expand Up @@ -98,6 +100,58 @@ def _validator(
return _validator


def one_of(allowed: Sequence[Any]) -> ValidatorType:
"""Create a validator that raises a ``ValueError``
if the value is not one of the allowed values.

:param allowed: The allowed values.
"""
allowed = tuple(allowed)

def _validator(
inst: Any, field: dc.Field[Any], value: Any, suffix: str = ""
) -> None:
if value not in allowed:
raise ValueError(
f"'{field.name}{suffix}' must be one of {allowed!r} (got {value!r})."
)

return _validator


FONTAWESOME_LATEX_MODES = ("none", "fontawesome", "fontawesome5")
"""Allowed string values for the ``fontawesome_latex`` configuration."""


def validate_fontawesome_latex(
inst: Any, field: dc.Field[Any], value: Any, suffix: str = ""
) -> None:
"""Validate the ``fontawesome_latex`` value.

Accepts a ``bool`` (``True``/``False`` are kept for backwards
compatibility, and normalized by :attr:`SdConfig.fontawesome_latex_mode`),
or one of :data:`FONTAWESOME_LATEX_MODES`.

:param inst: The dataclass instance (or None if not yet created).
:param field: The dataclass field.
:param value: The value to validate.
:param suffix: Suffix to append to the field name in error messages.
:raises TypeError | ValueError: If the value is invalid.
"""
if isinstance(value, bool):
return
if not isinstance(value, str):
raise TypeError(
f"'{field.name}{suffix}' must be a bool or str "
f"(got {value!r} that is a {value.__class__!r})."
)
if value not in FONTAWESOME_LATEX_MODES:
raise ValueError(
f"'{field.name}{suffix}' must be a bool or one of "
f"{FONTAWESOME_LATEX_MODES!r} (got {value!r})."
)


def validate_custom_directive(field: dc.Field[Any], name: Any, data: Any) -> None:
"""Validate the shape of a single custom directive (name -> data) entry.

Expand Down Expand Up @@ -165,11 +219,29 @@ class SdConfig:
"doc_type": "dict[str, dict]",
},
)
fontawesome_latex: bool = dc.field(
fontawesome_source: str = dc.field(
default="none",
metadata={
"validator": one_of(("none", "cdn")),
"help": "How sphinx-design loads the FontAwesome CSS: "
'"none" (default, provided by you/your theme) or "cdn"',
},
)
fontawesome_cdn_url: str = dc.field(
default="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css",
metadata={
"validator": instance_of(str),
"help": 'FontAwesome CSS URL to add when sd_fontawesome_source="cdn"',
},
)
fontawesome_latex: bool | str = dc.field(
default=False,
metadata={
"validator": instance_of(bool),
"help": "Render fontawesome icons in LaTeX output",
"validator": validate_fontawesome_latex,
"types": (bool, str),
"help": "Render fontawesome icons in LaTeX output: "
'False/"none", True/"fontawesome", or "fontawesome5"',
"doc_type": "bool | str",
},
)
tabs_storage_prefix: str = dc.field(
Expand All @@ -186,6 +258,23 @@ class SdConfig:
def __post_init__(self) -> None:
validate_fields(self)

@property
def fontawesome_latex_mode(self) -> str:
"""The normalized LaTeX fontawesome mode.

Maps the backwards-compatible boolean values to their string
equivalents (``True`` -> ``"fontawesome"``, ``False`` -> ``"none"``),
so downstream code only has to handle :data:`FONTAWESOME_LATEX_MODES`.

:return: One of :data:`FONTAWESOME_LATEX_MODES`.
"""
value = self.fontawesome_latex
if value is True:
return "fontawesome"
if value is False:
return "none"
return value

@classmethod
def from_sphinx(cls, config: Config) -> SdConfig:
"""Create a validated instance from the flat ``sd_`` prefixed
Expand Down Expand Up @@ -217,7 +306,12 @@ def setup_sd_config(app: Sphinx) -> None:
:param app: The Sphinx application object.
"""
for field in dc.fields(SdConfig):
app.add_config_value(f"sd_{field.name}", _field_default(field), "env")
app.add_config_value(
f"sd_{field.name}",
_field_default(field),
"env",
types=field.metadata.get("types", ()),
)
# low priority, so that the values are validated
# before any other `config-inited` listener reads them
app.connect("config-inited", _validate_config_values, priority=400)
Expand Down
Loading
Loading