django-simple-icons 16.26.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 M4p4
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ include src/django_simple_icons/simple-icons.zip
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-simple-icons
3
+ Version: 16.26.0
4
+ Summary: Use Simple Icons brand icons in your Django and Jinja templates.
5
+ Author-email: M4p4 <homejr@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Changelog, https://github.com/M4p4/django-simple-icons/blob/main/CHANGELOG.md
8
+ Project-URL: Repository, https://github.com/M4p4/django-simple-icons
9
+ Keywords: django,icons,jinja,simple-icons,svg
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Framework :: Django :: 6.0
15
+ Classifier: Framework :: Django :: 6.1
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: django
27
+ Requires-Dist: django>=4.2; extra == "django"
28
+ Provides-Extra: jinja
29
+ Requires-Dist: jinja2>=2.8; extra == "jinja"
30
+ Dynamic: license-file
31
+
32
+ # django-simple-icons
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/django-simple-icons.svg)](https://pypi.org/project/django-simple-icons/)
35
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-simple-icons.svg)](https://pypi.org/project/django-simple-icons/)
36
+ [![CI](https://github.com/M4p4/django-simple-icons/actions/workflows/main.yml/badge.svg)](https://github.com/M4p4/django-simple-icons/actions/workflows/main.yml)
37
+
38
+ Use [Simple Icons](https://simpleicons.org/), 3,449 SVG brand icons, in your Django and
39
+ Jinja templates. The icons ship inside the package, so there is no CDN to call and no
40
+ static files to collect. Each icon renders as inline SVG that follows the surrounding
41
+ text color.
42
+
43
+ ## Installation
44
+
45
+ Install with the extra matching your template engine:
46
+
47
+ ```console
48
+ python -m pip install 'django-simple-icons[django]'
49
+ python -m pip install 'django-simple-icons[jinja]'
50
+ ```
51
+
52
+ The package itself has no runtime dependencies and needs Python 3.12 or later.
53
+
54
+ ## Django
55
+
56
+ Add the app to `INSTALLED_APPS` so the template tag library can be loaded:
57
+
58
+ ```python
59
+ INSTALLED_APPS = [
60
+ ...,
61
+ "django_simple_icons",
62
+ ]
63
+ ```
64
+
65
+ Then load the library and render an icon by its slug:
66
+
67
+ ```html
68
+ {% load simple_icons %}
69
+
70
+ {% simple_icon "django" %}
71
+ ```
72
+
73
+ That produces a 24x24 inline SVG which inherits the current text color and is hidden
74
+ from screen readers:
75
+
76
+ ```html
77
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M11.146 0h3.924…"/></svg>
78
+ ```
79
+
80
+ ### Sizing
81
+
82
+ `size` sets both `width` and `height` and defaults to 24:
83
+
84
+ ```html
85
+ {% simple_icon "django" size=48 %}
86
+ ```
87
+
88
+ Pass `size=None` to omit both attributes, which is what you want when CSS classes
89
+ control the size:
90
+
91
+ ```html
92
+ {% simple_icon "django" size=None class="h-6 w-6" %}
93
+ ```
94
+
95
+ ### Color
96
+
97
+ Icons use `fill="currentColor"` by default, so they follow the surrounding text. Pass
98
+ `color="brand"` to use the brand's official color, or any CSS color to set it directly:
99
+
100
+ ```html
101
+ {% simple_icon "django" color="brand" %}
102
+ {% simple_icon "django" color="#ff6600" %}
103
+ ```
104
+
105
+ ### Extra attributes
106
+
107
+ Any other keyword argument becomes an attribute on the `<svg>` element, with
108
+ underscores converted to dashes:
109
+
110
+ ```html
111
+ {% simple_icon "django" class="h-6 w-6 text-green-700" data_controller="icon" %}
112
+ ```
113
+
114
+ ```html
115
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true" class="h-6 w-6 text-green-700" data-controller="icon"><path d="M11.146 0h3.924…"/></svg>
116
+ ```
117
+
118
+ Values are escaped, so `data_test="a < 2"` renders as `data-test="a &lt; 2"`.
119
+
120
+ ### Accessibility
121
+
122
+ Icons are decorative by default, so the upstream `<title>` is removed and
123
+ `aria-hidden="true"` is set. An icon next to a text label needs nothing more.
124
+
125
+ For a standalone icon that carries meaning, pass `title=True` to keep the brand name as
126
+ the accessible name, or pass a string to replace it:
127
+
128
+ ```html
129
+ {% simple_icon "django" title=True %}
130
+ {% simple_icon "django" title="Django framework" %}
131
+ ```
132
+
133
+ Either form keeps `role="img"` and drops `aria-hidden`:
134
+
135
+ ```html
136
+ <svg role="img" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><title>Django framework</title><path d="M11.146 0h3.924…"/></svg>
137
+ ```
138
+
139
+ ## Jinja
140
+
141
+ Register the function as a global on your environment:
142
+
143
+ ```python
144
+ from django_simple_icons.jinja import simple_icon
145
+
146
+ environment.globals["simple_icon"] = simple_icon
147
+ ```
148
+
149
+ Call it with the same arguments as the Django tag:
150
+
151
+ ```jinja
152
+ {{ simple_icon("django") }}
153
+ {{ simple_icon("django", size=48, color="brand") }}
154
+ {{ simple_icon("django", title="Django framework") }}
155
+ ```
156
+
157
+ The result is a `markupsafe.Markup`, so it renders unescaped under autoescaping.
158
+
159
+ Because `class` is a reserved word in Python, spell it `class_` when calling the
160
+ function directly. The trailing underscore is stripped:
161
+
162
+ ```jinja
163
+ {{ simple_icon("django", class_="h-6 w-6") }}
164
+ ```
165
+
166
+ ## Arguments
167
+
168
+ | Argument | Default | Description |
169
+ | --------- | ---------------- | ---------------------------------------------------------- |
170
+ | `name` | required | The icon slug, such as `"django"` |
171
+ | `size` | `24` | Sets `width` and `height`; `None` omits both |
172
+ | `color` | `"currentColor"` | Any CSS color, or `"brand"` for the brand's official color |
173
+ | `title` | `False` | `True` keeps the brand name, a string replaces it |
174
+ | `**attrs` | none | Extra attributes; underscores become dashes |
175
+
176
+ ## Brand colors
177
+
178
+ `get_brand_color` returns a brand's official color, which is useful outside a template:
179
+
180
+ ```pycon
181
+ >>> from django_simple_icons import get_brand_color
182
+ >>> get_brand_color("django")
183
+ '#092E20'
184
+ ```
185
+
186
+ ## Finding icon slugs
187
+
188
+ Slugs match the names on [simpleicons.org](https://simpleicons.org/), where you can
189
+ search the full set. An unknown slug raises `IconDoesNotExist` with suggestions:
190
+
191
+ ```pycon
192
+ >>> from django_simple_icons import get_brand_color
193
+ >>> get_brand_color("djngo")
194
+ Traceback (most recent call last):
195
+ ...
196
+ django_simple_icons.IconDoesNotExist: Icon 'djngo' does not exist. Did you mean: django, deno, fandango? Browse the available icons at https://simpleicons.org/
197
+ ```
198
+
199
+ ## Supported versions
200
+
201
+ - Python 3.12, 3.13 and 3.14
202
+ - Django 5.2, 6.0 and 6.1 are tested. Django is declared as `>=4.2` because the code
203
+ uses nothing newer, but only the tested versions are supported.
204
+ - Jinja2 2.8 and later
205
+
206
+ ## Versioning
207
+
208
+ The package version mirrors the bundled Simple Icons release, so version 16.26.0
209
+ contains Simple Icons 16.26.0. A fix to this package that ships no new icons appends a
210
+ fourth segment, such as 16.26.0.1.
211
+
212
+ ## License
213
+
214
+ The code in this package is MIT licensed. The icons come from Simple Icons and are
215
+ [CC0-1.0](https://github.com/simple-icons/simple-icons/blob/master/LICENSE.md).
216
+
217
+ Brand icons and names are trademarks of their respective owners. Using an icon does not
218
+ imply endorsement, and some brands restrict how their marks may be used. Read the
219
+ [Simple Icons legal disclaimer](https://github.com/simple-icons/simple-icons/blob/master/DISCLAIMER.md)
220
+ before shipping an icon.
@@ -0,0 +1,189 @@
1
+ # django-simple-icons
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/django-simple-icons.svg)](https://pypi.org/project/django-simple-icons/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-simple-icons.svg)](https://pypi.org/project/django-simple-icons/)
5
+ [![CI](https://github.com/M4p4/django-simple-icons/actions/workflows/main.yml/badge.svg)](https://github.com/M4p4/django-simple-icons/actions/workflows/main.yml)
6
+
7
+ Use [Simple Icons](https://simpleicons.org/), 3,449 SVG brand icons, in your Django and
8
+ Jinja templates. The icons ship inside the package, so there is no CDN to call and no
9
+ static files to collect. Each icon renders as inline SVG that follows the surrounding
10
+ text color.
11
+
12
+ ## Installation
13
+
14
+ Install with the extra matching your template engine:
15
+
16
+ ```console
17
+ python -m pip install 'django-simple-icons[django]'
18
+ python -m pip install 'django-simple-icons[jinja]'
19
+ ```
20
+
21
+ The package itself has no runtime dependencies and needs Python 3.12 or later.
22
+
23
+ ## Django
24
+
25
+ Add the app to `INSTALLED_APPS` so the template tag library can be loaded:
26
+
27
+ ```python
28
+ INSTALLED_APPS = [
29
+ ...,
30
+ "django_simple_icons",
31
+ ]
32
+ ```
33
+
34
+ Then load the library and render an icon by its slug:
35
+
36
+ ```html
37
+ {% load simple_icons %}
38
+
39
+ {% simple_icon "django" %}
40
+ ```
41
+
42
+ That produces a 24x24 inline SVG which inherits the current text color and is hidden
43
+ from screen readers:
44
+
45
+ ```html
46
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M11.146 0h3.924…"/></svg>
47
+ ```
48
+
49
+ ### Sizing
50
+
51
+ `size` sets both `width` and `height` and defaults to 24:
52
+
53
+ ```html
54
+ {% simple_icon "django" size=48 %}
55
+ ```
56
+
57
+ Pass `size=None` to omit both attributes, which is what you want when CSS classes
58
+ control the size:
59
+
60
+ ```html
61
+ {% simple_icon "django" size=None class="h-6 w-6" %}
62
+ ```
63
+
64
+ ### Color
65
+
66
+ Icons use `fill="currentColor"` by default, so they follow the surrounding text. Pass
67
+ `color="brand"` to use the brand's official color, or any CSS color to set it directly:
68
+
69
+ ```html
70
+ {% simple_icon "django" color="brand" %}
71
+ {% simple_icon "django" color="#ff6600" %}
72
+ ```
73
+
74
+ ### Extra attributes
75
+
76
+ Any other keyword argument becomes an attribute on the `<svg>` element, with
77
+ underscores converted to dashes:
78
+
79
+ ```html
80
+ {% simple_icon "django" class="h-6 w-6 text-green-700" data_controller="icon" %}
81
+ ```
82
+
83
+ ```html
84
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true" class="h-6 w-6 text-green-700" data-controller="icon"><path d="M11.146 0h3.924…"/></svg>
85
+ ```
86
+
87
+ Values are escaped, so `data_test="a < 2"` renders as `data-test="a &lt; 2"`.
88
+
89
+ ### Accessibility
90
+
91
+ Icons are decorative by default, so the upstream `<title>` is removed and
92
+ `aria-hidden="true"` is set. An icon next to a text label needs nothing more.
93
+
94
+ For a standalone icon that carries meaning, pass `title=True` to keep the brand name as
95
+ the accessible name, or pass a string to replace it:
96
+
97
+ ```html
98
+ {% simple_icon "django" title=True %}
99
+ {% simple_icon "django" title="Django framework" %}
100
+ ```
101
+
102
+ Either form keeps `role="img"` and drops `aria-hidden`:
103
+
104
+ ```html
105
+ <svg role="img" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><title>Django framework</title><path d="M11.146 0h3.924…"/></svg>
106
+ ```
107
+
108
+ ## Jinja
109
+
110
+ Register the function as a global on your environment:
111
+
112
+ ```python
113
+ from django_simple_icons.jinja import simple_icon
114
+
115
+ environment.globals["simple_icon"] = simple_icon
116
+ ```
117
+
118
+ Call it with the same arguments as the Django tag:
119
+
120
+ ```jinja
121
+ {{ simple_icon("django") }}
122
+ {{ simple_icon("django", size=48, color="brand") }}
123
+ {{ simple_icon("django", title="Django framework") }}
124
+ ```
125
+
126
+ The result is a `markupsafe.Markup`, so it renders unescaped under autoescaping.
127
+
128
+ Because `class` is a reserved word in Python, spell it `class_` when calling the
129
+ function directly. The trailing underscore is stripped:
130
+
131
+ ```jinja
132
+ {{ simple_icon("django", class_="h-6 w-6") }}
133
+ ```
134
+
135
+ ## Arguments
136
+
137
+ | Argument | Default | Description |
138
+ | --------- | ---------------- | ---------------------------------------------------------- |
139
+ | `name` | required | The icon slug, such as `"django"` |
140
+ | `size` | `24` | Sets `width` and `height`; `None` omits both |
141
+ | `color` | `"currentColor"` | Any CSS color, or `"brand"` for the brand's official color |
142
+ | `title` | `False` | `True` keeps the brand name, a string replaces it |
143
+ | `**attrs` | none | Extra attributes; underscores become dashes |
144
+
145
+ ## Brand colors
146
+
147
+ `get_brand_color` returns a brand's official color, which is useful outside a template:
148
+
149
+ ```pycon
150
+ >>> from django_simple_icons import get_brand_color
151
+ >>> get_brand_color("django")
152
+ '#092E20'
153
+ ```
154
+
155
+ ## Finding icon slugs
156
+
157
+ Slugs match the names on [simpleicons.org](https://simpleicons.org/), where you can
158
+ search the full set. An unknown slug raises `IconDoesNotExist` with suggestions:
159
+
160
+ ```pycon
161
+ >>> from django_simple_icons import get_brand_color
162
+ >>> get_brand_color("djngo")
163
+ Traceback (most recent call last):
164
+ ...
165
+ django_simple_icons.IconDoesNotExist: Icon 'djngo' does not exist. Did you mean: django, deno, fandango? Browse the available icons at https://simpleicons.org/
166
+ ```
167
+
168
+ ## Supported versions
169
+
170
+ - Python 3.12, 3.13 and 3.14
171
+ - Django 5.2, 6.0 and 6.1 are tested. Django is declared as `>=4.2` because the code
172
+ uses nothing newer, but only the tested versions are supported.
173
+ - Jinja2 2.8 and later
174
+
175
+ ## Versioning
176
+
177
+ The package version mirrors the bundled Simple Icons release, so version 16.26.0
178
+ contains Simple Icons 16.26.0. A fix to this package that ships no new icons appends a
179
+ fourth segment, such as 16.26.0.1.
180
+
181
+ ## License
182
+
183
+ The code in this package is MIT licensed. The icons come from Simple Icons and are
184
+ [CC0-1.0](https://github.com/simple-icons/simple-icons/blob/master/LICENSE.md).
185
+
186
+ Brand icons and names are trademarks of their respective owners. Using an icon does not
187
+ imply endorsement, and some brands restrict how their marks may be used. Read the
188
+ [Simple Icons legal disclaimer](https://github.com/simple-icons/simple-icons/blob/master/DISCLAIMER.md)
189
+ before shipping an icon.
@@ -0,0 +1,136 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [ "setuptools>=77" ]
4
+
5
+ [project]
6
+ name = "django-simple-icons"
7
+ version = "16.26.0"
8
+ description = "Use Simple Icons brand icons in your Django and Jinja templates."
9
+ readme = "README.md"
10
+ keywords = [ "django", "icons", "jinja", "simple-icons", "svg" ]
11
+ license = "MIT"
12
+ license-files = [ "LICENSE" ]
13
+ authors = [
14
+ { name = "M4p4", email = "homejr@protonmail.com" },
15
+ ]
16
+ requires-python = ">=3.12"
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Framework :: Django",
20
+ "Framework :: Django :: 4.2",
21
+ "Framework :: Django :: 5.2",
22
+ "Framework :: Django :: 6.0",
23
+ "Framework :: Django :: 6.1",
24
+ "Intended Audience :: Developers",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Typing :: Typed",
31
+ ]
32
+ optional-dependencies.django = [ "django>=4.2" ]
33
+ optional-dependencies.jinja = [ "jinja2>=2.8" ]
34
+ urls.Changelog = "https://github.com/M4p4/django-simple-icons/blob/main/CHANGELOG.md"
35
+ urls.Repository = "https://github.com/M4p4/django-simple-icons"
36
+
37
+ [dependency-groups]
38
+ test = [
39
+ "coverage[toml]",
40
+ "jinja2",
41
+ "pytest",
42
+ "pytest-randomly",
43
+ ]
44
+ django52 = [ "django>=5.2,<5.3" ]
45
+ django60 = [ "django>=6,<6.1" ]
46
+ # Django 6.1 is still in beta; the specifier opts uv into prereleases and will
47
+ # resolve to the final release once it lands.
48
+ django61 = [ "django>=6.1b1,<6.2" ]
49
+
50
+ [tool.setuptools]
51
+ packages.find.where = [ "src" ]
52
+ package-data.django_simple_icons = [ "py.typed", "simple-icons.zip" ]
53
+
54
+ [tool.uv]
55
+ # uv only auto-installs the `dev` group, so name the groups a bare `uv run`
56
+ # needs. The full version matrix is exercised by tox, not by the default env.
57
+ default-groups = [ "test", "django60" ]
58
+ conflicts = [
59
+ [
60
+ { group = "django52" },
61
+ { group = "django60" },
62
+ { group = "django61" },
63
+ ],
64
+ ]
65
+
66
+ [tool.ruff]
67
+ lint.select = [
68
+ # flake8-bugbear
69
+ "B",
70
+ # flake8-comprehensions
71
+ "C4",
72
+ # pycodestyle
73
+ "E",
74
+ # Pyflakes errors
75
+ "F",
76
+ # isort
77
+ "I",
78
+ # flake8-simplify
79
+ "SIM",
80
+ # flake8-tidy-imports
81
+ "TID",
82
+ # pyupgrade
83
+ "UP",
84
+ # Pyflakes warnings
85
+ "W",
86
+ ]
87
+ lint.ignore = [
88
+ # flake8-bugbear opinionated rules
89
+ "B9",
90
+ # line-too-long
91
+ "E501",
92
+ # suppressible-exception
93
+ "SIM105",
94
+ # if-else-block-instead-of-if-exp
95
+ "SIM108",
96
+ ]
97
+ lint.extend-safe-fixes = [
98
+ # non-pep585-annotation
99
+ "UP006",
100
+ ]
101
+ lint.isort.required-imports = [ "from __future__ import annotations" ]
102
+
103
+ [tool.pyproject-fmt]
104
+ max_supported_python = "3.14"
105
+
106
+ [tool.mypy]
107
+ mypy_path = "src/"
108
+ namespace_packages = false
109
+ warn_unreachable = true
110
+ enable_error_code = [
111
+ "ignore-without-code",
112
+ "redundant-expr",
113
+ "truthy-bool",
114
+ ]
115
+ strict = true
116
+ overrides = [ { module = "tests.*", allow_untyped_defs = true, allow_untyped_calls = true } ]
117
+
118
+ [tool.pytest]
119
+ ini_options.testpaths = [ "tests" ]
120
+ ini_options.addopts = "-ra --strict-config --strict-markers"
121
+
122
+ [tool.coverage]
123
+ run.branch = true
124
+ run.data_file = ".coverage/cov"
125
+ run.parallel = true
126
+ run.source = [
127
+ "django_simple_icons",
128
+ "tests",
129
+ ]
130
+ paths.source = [
131
+ "src",
132
+ ".tox/**/site-packages",
133
+ ]
134
+ report.show_missing = true
135
+ report.skip_covered = true
136
+ report.skip_empty = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Iterator
5
+ from contextlib import contextmanager
6
+ from copy import deepcopy
7
+ from difflib import get_close_matches
8
+ from functools import lru_cache
9
+ from importlib.resources import files
10
+ from typing import Any
11
+ from xml.etree import ElementTree
12
+ from zipfile import ZipFile
13
+
14
+ __all__ = ["IconDoesNotExist", "get_brand_color"]
15
+
16
+ _ICONS_URL = "https://simpleicons.org/"
17
+
18
+
19
+ class IconDoesNotExist(Exception):
20
+ pass
21
+
22
+
23
+ @contextmanager
24
+ def _zip_file() -> Iterator[ZipFile]:
25
+ resource = files("django_simple_icons").joinpath("simple-icons.zip")
26
+ with resource.open("rb") as file, ZipFile(file) as archive:
27
+ yield archive
28
+
29
+
30
+ @lru_cache(maxsize=1)
31
+ def _load_metadata() -> dict[str, dict[str, str]]:
32
+ with _zip_file() as archive:
33
+ data: dict[str, dict[str, str]] = json.loads(archive.read("data.json"))
34
+ return data
35
+
36
+
37
+ def _unknown_icon(name: str) -> IconDoesNotExist:
38
+ matches = get_close_matches(name, _load_metadata(), n=3)
39
+ suggestion = f" Did you mean: {', '.join(matches)}?" if matches else ""
40
+ return IconDoesNotExist(
41
+ f"Icon {name!r} does not exist.{suggestion}"
42
+ f" Browse the available icons at {_ICONS_URL}"
43
+ )
44
+
45
+
46
+ @lru_cache(maxsize=128)
47
+ def _load_icon(name: str) -> ElementTree.Element:
48
+ with _zip_file() as archive:
49
+ try:
50
+ svg_bytes = archive.read(f"icons/{name}.svg")
51
+ except KeyError:
52
+ raise _unknown_icon(name) from None
53
+
54
+ svg = ElementTree.fromstring(svg_bytes.decode())
55
+ # Parsing resolves the xmlns declaration into '{ns}tag' names; undo that so
56
+ # the icon serializes as plain inline SVG.
57
+ for element in svg.iter():
58
+ _, _, element.tag = element.tag.rpartition("}")
59
+ return svg
60
+
61
+
62
+ def get_brand_color(name: str) -> str:
63
+ """Return the official brand color of ``name`` as ``"#RRGGBB"``."""
64
+ try:
65
+ hex_ = _load_metadata()[name]["hex"]
66
+ except KeyError:
67
+ raise _unknown_icon(name) from None
68
+ return f"#{hex_}"
69
+
70
+
71
+ def _plain_str(value: Any) -> Any:
72
+ """Demote a ``str`` subclass to ``str``, leaving anything else untouched.
73
+
74
+ ``markupsafe.Markup`` overrides ``replace()`` to escape its arguments, so
75
+ ElementTree's serializer would turn its own ``&lt;`` into ``&amp;lt;``.
76
+ Non-strings pass through so ElementTree still rejects what it cannot
77
+ serialize instead of silently emitting ``"None"``.
78
+ """
79
+ return str(value) if isinstance(value, str) else value
80
+
81
+
82
+ def _render_icon(
83
+ name: str,
84
+ *,
85
+ size: int | None,
86
+ color: str,
87
+ title: str | bool,
88
+ attrs: dict[str, Any],
89
+ ) -> str:
90
+ color = _plain_str(color)
91
+ title = _plain_str(title)
92
+
93
+ svg = deepcopy(_load_icon(name))
94
+
95
+ if size is not None:
96
+ svg.set("width", str(size))
97
+ svg.set("height", str(size))
98
+
99
+ if color == "brand":
100
+ svg.set("fill", get_brand_color(name))
101
+ else:
102
+ svg.set("fill", color)
103
+
104
+ title_element = svg.find("title")
105
+ assert title_element is not None # every Simple Icon ships a <title>
106
+ if title is False:
107
+ # Decorative by default: no accessible name, hidden from screen readers.
108
+ svg.remove(title_element)
109
+ svg.attrib.pop("role", None)
110
+ svg.set("aria-hidden", "true")
111
+ elif title is not True:
112
+ title_element.text = title
113
+
114
+ for key, value in attrs.items():
115
+ # removesuffix: `class` and `for` are reserved words, so the Python and
116
+ # Jinja surfaces have to spell them `class_` / `for_`.
117
+ svg.set(key.removesuffix("_").replace("_", "-"), str(_plain_str(value)))
118
+
119
+ # Tags carry no namespace after _load_icon(), so no xmlns is serialized —
120
+ # inline SVG inherits it from the surrounding HTML document anyway.
121
+ return ElementTree.tostring(svg, encoding="unicode")