diff --git a/src/django_nh3/templatetags/__init__.py b/src/django_nh3/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/django_nh3/templatetags/nh3_tags.py b/src/django_nh3/templatetags/nh3_tags.py
new file mode 100644
index 0000000..71c3d85
--- /dev/null
+++ b/src/django_nh3/templatetags/nh3_tags.py
@@ -0,0 +1,32 @@
+import nh3
+from django import template
+from django.utils.safestring import SafeText, mark_safe
+
+from django_nh3.utils import get_nh3_default_options
+
+register = template.Library()
+
+
+@register.filter(name="nh3")
+def nh3_value(value: str | None, tags: str | None = None) -> SafeText:
+ """
+ Takes an input HTML value and sanitizes it utilizing nh3,
+ returning a SafeText object that can be rendered by Django.
+
+ Accepts an optional argument of allowed tags. Should be a comma delimited
+ string (ie. "img,span" or "img")
+ """
+ if value is None:
+ return None
+
+ args = {}
+
+ nh3_args = get_nh3_default_options()
+ if tags is not None:
+ args = nh3_args.copy()
+ args["tags"] = set(tags.split(","))
+ else:
+ args = nh3_args
+
+ nh3_value = nh3.clean(value, **args)
+ return mark_safe(nh3_value)
diff --git a/src/django_nh3/utils.py b/src/django_nh3/utils.py
new file mode 100644
index 0000000..9d8ffbd
--- /dev/null
+++ b/src/django_nh3/utils.py
@@ -0,0 +1,50 @@
+import logging
+from typing import Any
+
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+
+def get_nh3_default_options() -> dict[str, Any]:
+ """
+ Pull the django-nh3 settings similarly to how django-bleach handled them.
+
+ Some django-bleach settings can be mapped to django-nh3 settings without
+ any changes:
+
+ BLEACH_ALLOWED_TAGS -> NH3_ALLOWED_TAGS
+ BLEACH_ALLOWED_ATTRIBUTES -> NH3_ALLOWED_ATTRIBUTES
+ BLEACH_STRIP_COMMENTS -> NH3_STRIP_COMMENTS
+
+ While other settings are have no current support in nh3:
+
+ BLEACH_ALLOWED_STYLES -> There is no support for styling
+ BLEACH_ALLOWED_PROTOCOLS -> There is no suport for protocols
+ BLEACH_STRIP_TAGS -> This is the default behavior of nh3
+
+ """
+ nh3_args: dict[str, Any] = {}
+
+ nh3_settings = {
+ "NH3_ALLOWED_TAGS": "tags",
+ "NH3_ALLOWED_ATTRIBUTES": "attributes",
+ "NH3_STRIP_COMMENTS": "strip_comments",
+ }
+
+ for setting, kwarg in nh3_settings.items():
+ if hasattr(settings, setting):
+ attr = getattr(settings, setting)
+
+ # Convert from general iterables to sets
+ if setting == "NH3_ALLOWED_TAGS":
+ attr = set(attr)
+ elif setting == "NH3_ALLOWED_ATTRIBUTES":
+ copy_dict = attr.copy()
+ for tag, attributes in attr.items():
+ copy_dict[tag] = set(attributes)
+ attr = copy_dict
+
+ nh3_args[kwarg] = attr
+
+ return nh3_args
diff --git a/tests/constants.py b/tests/constants.py
new file mode 100644
index 0000000..d21e56e
--- /dev/null
+++ b/tests/constants.py
@@ -0,0 +1,5 @@
+ALLOWED_ATTRIBUTES = {"*": {"class", "style"}, "a": {"href", "title"}}
+
+ALLOWED_TAGS = {"a", "li", "ul"}
+
+STRIP_COMMENTS = True
diff --git a/tests/test_settings.py b/tests/test_settings.py
new file mode 100644
index 0000000..ab0581b
--- /dev/null
+++ b/tests/test_settings.py
@@ -0,0 +1,32 @@
+from unittest.mock import patch
+
+from django.test import TestCase
+from django_nh3.utils import get_nh3_default_options
+
+from .constants import ALLOWED_ATTRIBUTES, ALLOWED_TAGS, STRIP_COMMENTS
+
+
+class TestBleachOptions(TestCase):
+ @patch(
+ "django_nh3.utils.settings",
+ NH3_ALLOWED_ATTRIBUTES=ALLOWED_ATTRIBUTES,
+ )
+ def test_custom_attrs(self, settings):
+ nh3_args = get_nh3_default_options()
+ self.assertEqual(nh3_args["attributes"], ALLOWED_ATTRIBUTES)
+
+ @patch(
+ "django_nh3.utils.settings",
+ NH3_ALLOWED_TAGS=ALLOWED_TAGS,
+ )
+ def test_custom_tags(self, settings):
+ nh3_args = get_nh3_default_options()
+ self.assertEqual(nh3_args["tags"], ALLOWED_TAGS)
+
+ @patch(
+ "django_nh3.utils.settings",
+ NH3_STRIP_COMMENTS=STRIP_COMMENTS,
+ )
+ def test_strip_comments(self, settings):
+ nh3_args = get_nh3_default_options()
+ self.assertEqual(nh3_args["strip_comments"], STRIP_COMMENTS)
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
new file mode 100644
index 0000000..9d86d2d
--- /dev/null
+++ b/tests/test_templatetags.py
@@ -0,0 +1,45 @@
+from django.template import Context, Template
+from django.test import TestCase
+
+
+class TestBleachTemplates(TestCase):
+ """Test template tags"""
+
+ def test_bleaching(self):
+ """Test that unsafe tags are sanitised"""
+ context = Context(
+ {"some_unsafe_content": ''},
+ )
+ template_to_render = Template(
+ "{% load nh3_tags %}" "{{ some_unsafe_content|nh3 }}"
+ )
+ rendered_template = template_to_render.render(context)
+ self.assertEqual("", rendered_template)
+
+ def test_bleaching_none(self):
+ """Test that None is handled properly as an input"""
+ context = Context({"none_value": None})
+ template_to_render = Template(
+ "{% load nh3_tags %}" "{{ none_value|nh3 }}"
+ )
+ rendered_template = template_to_render.render(context)
+ self.assertEqual("None", rendered_template)
+
+ def test_bleaching_tags(self):
+ """Test provided tags are kept"""
+ context = Context(
+ {
+ "some_unsafe_content": (
+ ""
+ "I'm not trying to XSS you"
+ )
+ }
+ )
+ template_to_render = Template(
+ "{% load nh3_tags %}" '{{ some_unsafe_content|nh3:"img" }}'
+ )
+ rendered_template = template_to_render.render(context)
+ self.assertInHTML(
+ 'I\'m not trying to XSS you', rendered_template
+ )