from django.forms import (
    BaseForm,
    BaseFormSet,
    BoundField,
    CheckboxInput,
    CheckboxSelectMultiple,
    ClearableFileInput,
    MultiWidget,
    RadioSelect,
    Select,
)
from django.forms.widgets import Input, SelectMultiple, Textarea
from django.utils.html import conditional_escape, format_html, strip_tags
from django.utils.safestring import mark_safe

from .core import get_bootstrap_setting
from .css import merge_css_classes
from .forms import render_field, render_form, render_label
from .size import DEFAULT_SIZE, SIZE_MD, get_size_class, parse_size
from .text import text_value
from .utils import render_template_file
from .widgets import ReadOnlyPasswordHashWidget, is_widget_with_placeholder


class BaseRenderer(object):
    """A content renderer."""

    def __init__(self, *args, **kwargs):
        self.layout = kwargs.get("layout", "")
        self.wrapper_class = kwargs.get("wrapper_class", get_bootstrap_setting("wrapper_class"))
        self.inline_wrapper_class = kwargs.get("inline_wrapper_class", get_bootstrap_setting("inline_wrapper_class"))
        self.field_class = kwargs.get("field_class", "")
        self.label_class = kwargs.get("label_class", "")
        self.show_help = kwargs.get("show_help", True)
        self.show_label = kwargs.get("show_label", True)
        self.exclude = kwargs.get("exclude", "")
        self.set_placeholder = kwargs.get("set_placeholder", True)
        self.size = parse_size(kwargs.get("size", ""), default=SIZE_MD)
        self.horizontal_label_class = kwargs.get(
            "horizontal_label_class", get_bootstrap_setting("horizontal_label_class")
        )
        self.horizontal_field_class = kwargs.get(
            "horizontal_field_class", get_bootstrap_setting("horizontal_field_class")
        )
        self.checkbox_layout = kwargs.get("checkbox_layout", get_bootstrap_setting("checkbox_layout"))
        self.checkbox_style = kwargs.get("checkbox_style", get_bootstrap_setting("checkbox_style"))
        self.horizontal_field_offset_class = kwargs.get(
            "horizontal_field_offset_class", get_bootstrap_setting("horizontal_field_offset_class")
        )
        self.inline_field_class = kwargs.get("inline_field_class", get_bootstrap_setting("inline_field_class"))
        self.server_side_validation = kwargs.get(
            "server_side_validation", get_bootstrap_setting("server_side_validation")
        )
        self.error_css_class = kwargs.get("error_css_class", None)
        self.required_css_class = kwargs.get("required_css_class", None)
        self.success_css_class = kwargs.get("success_css_class", None)
        self.alert_error_type = kwargs.get("alert_error_type", "non_fields")

    @property
    def is_floating(self):
        """Return whether to render `form-control` widgets as floating."""
        return self.layout == "floating"

    @property
    def is_horizontal(self):
        """Return whether to render form horizontally."""
        return self.layout == "horizontal"

    @property
    def is_inline(self):
        """Return whether to render widgets with inline layout."""
        return self.layout == "inline"

    def get_size_class(self, prefix):
        """Return size class for given prefix."""
        return get_size_class(self.size, prefix=prefix) if self.size in ["sm", "lg"] else ""

    def get_kwargs(self):
        """Return kwargs to pass on to child renderers."""
        context = {
            "layout": self.layout,
            "wrapper_class": self.wrapper_class,
            "field_class": self.field_class,
            "label_class": self.label_class,
            "show_help": self.show_help,
            "show_label": self.show_label,
            "exclude": self.exclude,
            "set_placeholder": self.set_placeholder,
            "size": self.size,
            "horizontal_label_class": self.horizontal_label_class,
            "horizontal_field_class": self.horizontal_field_class,
            "checkbox_layout": self.checkbox_layout,
            "checkbox_style": self.checkbox_style,
            "inline_field_class": self.inline_field_class,
            "error_css_class": self.error_css_class,
            "success_css_class": self.success_css_class,
            "required_css_class": self.required_css_class,
            "alert_error_type": self.alert_error_type,
        }
        return context

    def render(self):
        """Render to string."""
        return ""


class FormsetRenderer(BaseRenderer):
    """Default formset renderer."""

    def __init__(self, formset, *args, **kwargs):
        if not isinstance(formset, BaseFormSet):
            raise TypeError('Parameter "formset" should contain a valid Django Formset.')
        self.formset = formset
        super().__init__(*args, **kwargs)

    def render_management_form(self):
        """Return HTML for management form."""
        return text_value(self.formset.management_form)

    def render_forms(self):
        rendered_forms = mark_safe("")
        kwargs = self.get_kwargs()
        for form in self.formset.forms:
            rendered_forms += render_form(form, **kwargs)
        return rendered_forms

    def get_formset_errors(self):
        return self.formset.non_form_errors()

    def render_errors(self):
        formset_errors = self.get_formset_errors()
        if formset_errors:
            return render_template_file(
                "django_bootstrap5/form_errors.html",
                context={
                    "errors": formset_errors,
                    "form": self.formset,
                    "layout": self.layout,
                },
            )
        return mark_safe("")

    def render(self):
        return format_html(self.render_management_form() + "{}{}", self.render_errors(), self.render_forms())


class FormRenderer(BaseRenderer):
    """Default form renderer."""

    def __init__(self, form, *args, **kwargs):
        if not isinstance(form, BaseForm):
            raise TypeError('Parameter "form" should contain a valid Django Form.')
        self.form = form
        super().__init__(*args, **kwargs)

    def render_fields(self):
        rendered_fields = mark_safe("")
        kwargs = self.get_kwargs()
        for field in self.form:
            rendered_fields += render_field(field, **kwargs)
        return rendered_fields

    def get_fields_errors(self):
        form_errors = []
        for field in self.form:
            if not field.is_hidden and field.errors:
                form_errors += field.errors
        return form_errors

    def render_errors(self, type="all"):
        form_errors = None
        if type == "all":
            form_errors = self.get_fields_errors() + self.form.non_field_errors()
        elif type == "fields":
            form_errors = self.get_fields_errors()
        elif type == "non_fields":
            form_errors = self.form.non_field_errors()

        if form_errors:
            return render_template_file(
                "django_bootstrap5/form_errors.html",
                context={"errors": form_errors, "form": self.form, "layout": self.layout, "type": type},
            )

        return mark_safe("")

    def render(self):
        errors = self.render_errors(self.alert_error_type)
        fields = self.render_fields()
        return errors + fields


class FieldRenderer(BaseRenderer):
    """Default field renderer."""

    def __init__(self, field, *args, **kwargs):
        if not isinstance(field, BoundField):
            raise TypeError('Parameter "field" should contain a valid Django BoundField.')
        self.field = field
        super().__init__(*args, **kwargs)

        self.widget = field.field.widget
        self.is_multi_widget = isinstance(field.field.widget, MultiWidget)
        self.initial_attrs = self.widget.attrs.copy()
        self.help_text = text_value(field.help_text) if self.show_help and field.help_text else ""
        self.field_errors = [conditional_escape(text_value(error)) for error in field.errors]

        self.placeholder = text_value(kwargs.get("placeholder", self.default_placeholder))

        self.addon_before = kwargs.get("addon_before", self.widget.attrs.pop("addon_before", ""))
        self.addon_after = kwargs.get("addon_after", self.widget.attrs.pop("addon_after", ""))
        self.addon_before_class = kwargs.get(
            "addon_before_class", self.widget.attrs.pop("addon_before_class", "input-group-text")
        )
        self.addon_after_class = kwargs.get(
            "addon_after_class", self.widget.attrs.pop("addon_after_class", "input-group-text")
        )

        # These are set in Django or in the global BOOTSTRAP5 settings, and can be overwritten in the template
        error_css_class = kwargs.get("error_css_class", None)
        self.error_css_class = (
            getattr(field.form, "error_css_class", get_bootstrap_setting("error_css_class"))
            if error_css_class is None
            else error_css_class
        )

        required_css_class = kwargs.get("required_css_class", None)
        self.required_css_class = (
            getattr(field.form, "required_css_class", get_bootstrap_setting("required_css_class"))
            if required_css_class is None
            else required_css_class
        )
        if self.field.form.empty_permitted:
            self.required_css_class = ""

        success_css_class = kwargs.get("success_css_class", None)
        self.success_css_class = (
            getattr(field.form, "success_css_class", get_bootstrap_setting("success_css_class"))
            if success_css_class is None
            else success_css_class
        )

    @property
    def is_floating(self):
        return (
            super().is_floating
            and self.can_widget_float(self.widget)
            and not self.addon_before
            and not self.addon_after
        )

    @property
    def default_placeholder(self):
        """Return default placeholder for field."""
        return self.field.label if get_bootstrap_setting("set_placeholder") else ""

    def restore_widget_attrs(self):
        self.widget.attrs = self.initial_attrs.copy()

    def get_widget_input_type(self, widget):
        """Return input type of widget, or None."""
        return widget.input_type if isinstance(widget, Input) else None

    def is_form_control_widget(self, widget=None):
        widget = widget or self.widget
        if isinstance(widget, Input):
            return self.get_widget_input_type(widget) in [
                "text",
                "number",
                "email",
                "url",
                "tel",
                "date",
                "time",
                "password",
            ]

        return isinstance(widget, Textarea)

    def can_widget_have_server_side_validation(self, widget):
        """Return whether given widget can be rendered with server-side validation classes."""
        return self.get_widget_input_type(widget) != "color"

    def can_widget_float(self, widget):
        """Return whether given widget can be set to `form-floating` behavior."""
        if self.is_form_control_widget(widget):
            return True
        if isinstance(widget, Select):
            return self.size == DEFAULT_SIZE and not isinstance(widget, (SelectMultiple, RadioSelect))
        return False

    def add_widget_class_attrs(self, widget=None):
        """Add class attribute to widget."""
        if widget is None:
            widget = self.widget
        size_prefix = None

        before = []
        classes = [widget.attrs.get("class", "")]

        if ReadOnlyPasswordHashWidget is not None and isinstance(widget, ReadOnlyPasswordHashWidget):
            before.append("form-control-static")
        elif isinstance(widget, Select):
            before.append("form-select")
            size_prefix = "form-select"
        elif isinstance(widget, CheckboxInput):
            before.append("form-check-input")
        elif isinstance(widget, (Input, Textarea)):
            input_type = self.get_widget_input_type(widget)
            if input_type == "range":
                before.append("form-range")
            else:
                before.append("form-control")
                if input_type == "color":
                    before.append("form-control-color")
                size_prefix = "form-control"

        if size_prefix:
            classes.append(get_size_class(self.size, prefix=size_prefix, skip=["xs", "md"]))

        if self.server_side_validation and self.can_widget_have_server_side_validation(widget):
            classes.append(self.get_server_side_validation_classes())

        classes = before + classes
        widget.attrs["class"] = merge_css_classes(*classes)

    def add_placeholder_attrs(self, widget=None):
        """Add placeholder attribute to widget."""
        if widget is None:
            widget = self.widget
        placeholder = widget.attrs.get("placeholder", self.placeholder)
        if placeholder and self.set_placeholder and is_widget_with_placeholder(widget):
            widget.attrs["placeholder"] = conditional_escape(strip_tags(placeholder))

    def add_widget_attrs(self):
        """Return HTML attributes for widget as dict."""
        if self.is_multi_widget:
            widgets = self.widget.widgets
        else:
            widgets = [self.widget]
        for widget in widgets:
            self.add_widget_class_attrs(widget)
            self.add_placeholder_attrs(widget)
            if isinstance(widget, (RadioSelect, CheckboxSelectMultiple)):
                widget.template_name = "django_bootstrap5/widgets/radio_select.html"
            elif isinstance(widget, ClearableFileInput):
                widget.template_name = "django_bootstrap5/widgets/clearable_file_input.html"

    def get_label_class(self, horizontal=False):
        """Return CSS class for label."""
        label_classes = [text_value(self.label_class)]
        if not self.show_label:
            label_classes.append("visually-hidden")
        else:
            if isinstance(self.widget, CheckboxInput):
                widget_label_class = "form-check-label"
            elif self.is_inline:
                widget_label_class = "visually-hidden"
            elif horizontal:
                widget_label_class = merge_css_classes(self.horizontal_label_class, "col-form-label")
            else:
                widget_label_class = "form-label"
            label_classes = [widget_label_class] + label_classes
        return merge_css_classes(*label_classes)

    def get_field_html(self):
        """Return HTML for field."""
        self.add_widget_attrs()
        field_html = self.field.as_widget(attrs=self.widget.attrs)
        self.restore_widget_attrs()
        return field_html

    def get_label_html(self, horizontal=False):
        """Return value for label."""
        label_html = "" if self.show_label == "skip" else self.field.label
        if label_html:
            label_html = render_label(
                label_html,
                label_for=self.field.id_for_label,
                label_class=self.get_label_class(horizontal=horizontal),
            )
        return label_html

    def get_help_html(self):
        """Return HTML for help text."""
        help_text = self.help_text or ""
        if help_text:
            return render_template_file(
                "django_bootstrap5/field_help_text.html",
                context={
                    "field": self.field,
                    "help_text": help_text,
                    "layout": self.layout,
                    "show_help": self.show_help,
                },
            )
        return ""

    def get_errors_html(self):
        """Return HTML for field errors."""
        field_errors = self.field_errors
        if field_errors:
            return render_template_file(
                "django_bootstrap5/field_errors.html",
                context={
                    "field": self.field,
                    "field_errors": field_errors,
                    "layout": self.layout,
                    "show_help": self.show_help,
                },
            )
        return ""

    def get_server_side_validation_classes(self):
        """Return CSS classes for server-side validation."""
        if self.field_errors:
            return "is-invalid"
        elif self.field.form.is_bound:
            return "is-valid"
        return ""

    def get_inline_field_class(self):
        """Return CSS class for inline field."""
        return self.inline_field_class or "col-12"

    def get_checkbox_classes(self):
        """Return CSS classes for checkbox."""
        classes = ["form-check"]
        if self.checkbox_style == "switch":
            classes.append("form-switch")
        if self.checkbox_layout == "inline":
            classes.append("form-check-inline")
        return merge_css_classes(*classes)

    def get_wrapper_classes(self):
        """Return classes for wrapper."""
        wrapper_classes = []

        if self.is_inline:
            wrapper_classes.append(self.get_inline_field_class())
            wrapper_classes.append(self.inline_wrapper_class)
        else:
            if self.is_horizontal:
                wrapper_classes.append("row")
            wrapper_classes.append(self.wrapper_class)

        if self.is_floating:
            wrapper_classes.append("form-floating")

        # The indicator classes are added to the wrapper class. Bootstrap 5 server-side validation classes
        # are added to the fields
        if self.field.errors:
            wrapper_classes.append(self.error_css_class)
        elif self.field.form.is_bound:
            wrapper_classes.append(self.success_css_class)
        if self.field.field.required:
            wrapper_classes.append(self.required_css_class)

        return merge_css_classes(*wrapper_classes)

    def field_before_label(self):
        """Return whether field should be placed before label."""
        return isinstance(self.widget, CheckboxInput) or self.is_floating

    def render(self):
        if self.field.name in self.exclude.replace(" ", "").split(","):
            return ""
        if self.field.is_hidden:
            return text_value(self.field)

        field = self.get_field_html()

        if self.field_before_label():
            label = self.get_label_html()
            field = field + label
            label = mark_safe("")
            horizontal_class = merge_css_classes(self.horizontal_field_class, self.horizontal_field_offset_class)
        else:
            label = self.get_label_html(horizontal=self.is_horizontal)
            horizontal_class = self.horizontal_field_class

        help = self.get_help_html()
        errors = self.get_errors_html()

        if self.is_form_control_widget():
            addon_before = (
                format_html('<span class="input-group-text">{}</span>', self.addon_before) if self.addon_before else ""
            )
            addon_after = (
                format_html('<span class="input-group-text">{}</span>', self.addon_after) if self.addon_after else ""
            )
            if addon_before or addon_after:
                classes = "input-group"
                if self.server_side_validation and self.get_server_side_validation_classes():
                    classes = merge_css_classes(classes, "has-validation")
                field = format_html('<div class="{}">{}{}{}{}</div>', classes, addon_before, field, addon_after, errors)
                errors = ""

        if isinstance(self.widget, CheckboxInput):
            field = format_html('<div class="{}">{}{}{}</div>', self.get_checkbox_classes(), field, errors, help)
            errors = ""
            help = ""

        field_with_errors_and_help = format_html("{}{}{}", field, errors, help)

        if self.is_horizontal:
            field_with_errors_and_help = format_html(
                '<div class="{}">{}</div>', horizontal_class, field_with_errors_and_help
            )

        return format_html(
            '<div class="{wrapper_classes}">{label}{field_with_errors_and_help}</div>',
            wrapper_classes=self.get_wrapper_classes(),
            label=label,
            field_with_errors_and_help=field_with_errors_and_help,
        )

src/django_bootstrap5/renderers.py
from django.forms import (
    BaseForm,
    BaseFormSet,
    BoundField,
    CheckboxInput,
    CheckboxSelectMultiple,
    ClearableFileInput,
    MultiWidget,
    RadioSelect,
    Select,
)
from django.forms.widgets import Input, SelectMultiple, Textarea
from django.utils.html import conditional_escape, format_html, strip_tags
from django.utils.safestring import mark_safe

from .core import get_bootstrap_setting
from .css import merge_css_classes
from .forms import render_field, render_form, render_label
from .size import DEFAULT_SIZE, SIZE_MD, get_size_class, parse_size
from .text import text_value
from .utils import render_template_file
from .widgets import ReadOnlyPasswordHashWidget, is_widget_with_placeholder


class BaseRenderer(object):
    """A content renderer."""

    def __init__(self, *args, **kwargs):
        self.layout = kwargs.get("layout", "")
        self.wrapper_class = kwargs.get("wrapper_class", get_bootstrap_setting("wrapper_class"))
        self.inline_wrapper_class = kwargs.get("inline_wrapper_class", get_bootstrap_setting("inline_wrapper_class"))
        self.field_class = kwargs.get("field_class", "")
        self.label_class = kwargs.get("label_class", "")
        self.show_help = kwargs.get("show_help", True)
        self.show_label = kwargs.get("show_label", True)
        self.exclude = kwargs.get("exclude", "")
        self.set_placeholder = kwargs.get("set_placeholder", True)
        self.size = parse_size(kwargs.get("size", ""), default=SIZE_MD)
        self.horizontal_label_class = kwargs.get(
            "horizontal_label_class", get_bootstrap_setting("horizontal_label_class")
        )
        self.horizontal_field_class = kwargs.get(
            "horizontal_field_class", get_bootstrap_setting("horizontal_field_class")
        )
        self.checkbox_layout = kwargs.get("checkbox_layout", get_bootstrap_setting("checkbox_layout"))
        self.checkbox_style = kwargs.get("checkbox_style", get_bootstrap_setting("checkbox_style"))
        self.horizontal_field_offset_class = kwargs.get(
            "horizontal_field_offset_class", get_bootstrap_setting("horizontal_field_offset_class")
        )
        self.inline_field_class = kwargs.get("inline_field_class", get_bootstrap_setting("inline_field_class"))
        self.server_side_validation = kwargs.get(
            "server_side_validation", get_bootstrap_setting("server_side_validation")
        )
        self.error_css_class = kwargs.get("error_css_class", None)
        self.required_css_class = kwargs.get("required_css_class", None)
        self.success_css_class = kwargs.get("success_css_class", None)
        self.alert_error_type = kwargs.get("alert_error_type", "non_fields")

    @property
    def is_floating(self):
        """Return whether to render `form-control` widgets as floating."""
        return self.layout == "floating"

    @property
    def is_horizontal(self):
        """Return whether to render form horizontally."""
        return self.layout == "horizontal"

    @property
    def is_inline(self):
        """Return whether to render widgets with inline layout."""
        return self.layout == "inline"

    def get_size_class(self, prefix):
        """Return size class for given prefix."""
        return get_size_class(self.size, prefix=prefix) if self.size in ["sm", "lg"] else ""

    def get_kwargs(self):
        """Return kwargs to pass on to child renderers."""
        context = {
            "layout": self.layout,
            "wrapper_class": self.wrapper_class,
            "field_class": self.field_class,
            "label_class": self.label_class,
            "show_help": self.show_help,
            "show_label": self.show_label,
            "exclude": self.exclude,
            "set_placeholder": self.set_placeholder,
            "size": self.size,
            "horizontal_label_class": self.horizontal_label_class,
            "horizontal_field_class": self.horizontal_field_class,
            "checkbox_layout": self.checkbox_layout,
            "checkbox_style": self.checkbox_style,
            "inline_field_class": self.inline_field_class,
            "error_css_class": self.error_css_class,
            "success_css_class": self.success_css_class,
            "required_css_class": self.required_css_class,
            "alert_error_type": self.alert_error_type,
        }
        return context

    def render(self):
        """Render to string."""
        return ""


class FormsetRenderer(BaseRenderer):
    """Default formset renderer."""

    def __init__(self, formset, *args, **kwargs):
        if not isinstance(formset, BaseFormSet):
            raise TypeError('Parameter "formset" should contain a valid Django Formset.')
        self.formset = formset
        super().__init__(*args, **kwargs)

    def render_management_form(self):
        """Return HTML for management form."""
        return text_value(self.formset.management_form)

    def render_forms(self):
        rendered_forms = mark_safe("")
        kwargs = self.get_kwargs()
        for form in self.formset.forms:
            rendered_forms += render_form(form, **kwargs)
        return rendered_forms

    def get_formset_errors(self):
        return self.formset.non_form_errors()

    def render_errors(self):
        formset_errors = self.get_formset_errors()
        if formset_errors:
            return render_template_file(
                "django_bootstrap5/form_errors.html",
                context={
                    "errors": formset_errors,
                    "form": self.formset,
                    "layout": self.layout,
                },
            )
        return mark_safe("")

    def render(self):
        return format_html(self.render_management_form() + "{}{}", self.render_errors(), self.render_forms())


class FormRenderer(BaseRenderer):
    """Default form renderer."""

    def __init__(self, form, *args, **kwargs):
        if not isinstance(form, BaseForm):
            raise TypeError('Parameter "form" should contain a valid Django Form.')
        self.form = form
        super().__init__(*args, **kwargs)

    def render_fields(self):
        rendered_fields = mark_safe("")
        kwargs = self.get_kwargs()
        for field in self.form:
            rendered_fields += render_field(field, **kwargs)
        return rendered_fields

    def get_fields_errors(self):
        form_errors = []
        for field in self.form:
            if not field.is_hidden and field.errors:
                form_errors += field.errors
        return form_errors

    def render_errors(self, type="all"):
        form_errors = None
        if type == "all":
            form_errors = self.get_fields_errors() + self.form.non_field_errors()
        elif type == "fields":
            form_errors = self.get_fields_errors()
        elif type == "non_fields":
            form_errors = self.form.non_field_errors()

        if form_errors:
            return render_template_file(
                "django_bootstrap5/form_errors.html",
                context={"errors": form_errors, "form": self.form, "layout": self.layout, "type": type},
            )

        return mark_safe("")

    def render(self):
        errors = self.render_errors(self.alert_error_type)
        fields = self.render_fields()
        return errors + fields


class FieldRenderer(BaseRenderer):
    """Default field renderer."""

    def __init__(self, field, *args, **kwargs):
        if not isinstance(field, BoundField):
            raise TypeError('Parameter "field" should contain a valid Django BoundField.')
        self.field = field
        super().__init__(*args, **kwargs)

        self.widget = field.field.widget
        self.is_multi_widget = isinstance(field.field.widget, MultiWidget)
        self.initial_attrs = self.widget.attrs.copy()
        self.help_text = text_value(field.help_text) if self.show_help and field.help_text else ""
        self.field_errors = [conditional_escape(text_value(error)) for error in field.errors]

        self.placeholder = text_value(kwargs.get("placeholder", self.default_placeholder))

        self.addon_before = kwargs.get("addon_before", self.widget.attrs.pop("addon_before", ""))
        self.addon_after = kwargs.get("addon_after", self.widget.attrs.pop("addon_after", ""))
        self.addon_before_class = kwargs.get(
            "addon_before_class", self.widget.attrs.pop("addon_before_class", "input-group-text")
        )
        self.addon_after_class = kwargs.get(
            "addon_after_class", self.widget.attrs.pop("addon_after_class", "input-group-text")
        )

        # These are set in Django or in the global BOOTSTRAP5 settings, and can be overwritten in the template
        error_css_class = kwargs.get("error_css_class", None)
        self.error_css_class = (
            getattr(field.form, "error_css_class", get_bootstrap_setting("error_css_class"))
            if error_css_class is None
            else error_css_class
        )

        required_css_class = kwargs.get("required_css_class", None)
        self.required_css_class = (
            getattr(field.form, "required_css_class", get_bootstrap_setting("required_css_class"))
            if required_css_class is None
            else required_css_class
        )
        if self.field.form.empty_permitted:
            self.required_css_class = ""

        success_css_class = kwargs.get("success_css_class", None)
        self.success_css_class = (
            getattr(field.form, "success_css_class", get_bootstrap_setting("success_css_class"))
            if success_css_class is None
            else success_css_class
        )

    @property
    def is_floating(self):
        return (
            super().is_floating
            and self.can_widget_float(self.widget)
            and not self.addon_before
            and not self.addon_after
        )

    @property
    def default_placeholder(self):
        """Return default placeholder for field."""
        return self.field.label if get_bootstrap_setting("set_placeholder") else ""

    def restore_widget_attrs(self):
        self.widget.attrs = self.initial_attrs.copy()

    def get_widget_input_type(self, widget):
        """Return input type of widget, or None."""
        return widget.input_type if isinstance(widget, Input) else None

    def is_form_control_widget(self, widget=None):
        widget = widget or self.widget
        if isinstance(widget, Input):
            return self.get_widget_input_type(widget) in [
                "text",
                "number",
                "email",
                "url",
                "tel",
                "date",
                "time",
                "password",
            ]

        return isinstance(widget, Textarea)

    def can_widget_have_server_side_validation(self, widget):
        """Return whether given widget can be rendered with server-side validation classes."""
        return self.get_widget_input_type(widget) != "color"

    def can_widget_float(self, widget):
        """Return whether given widget can be set to `form-floating` behavior."""
        if self.is_form_control_widget(widget):
            return True
        if isinstance(widget, Select):
            return self.size == DEFAULT_SIZE and not isinstance(widget, (SelectMultiple, RadioSelect))
        return False

    def add_widget_class_attrs(self, widget=None):
        """Add class attribute to widget."""
        if widget is None:
            widget = self.widget
        size_prefix = None

        before = []
        classes = [widget.attrs.get("class", "")]

        if ReadOnlyPasswordHashWidget is not None and isinstance(widget, ReadOnlyPasswordHashWidget):
            before.append("form-control-static")
        elif isinstance(widget, Select):
            before.append("form-select")
            size_prefix = "form-select"
        elif isinstance(widget, CheckboxInput):
            before.append("form-check-input")
        elif isinstance(widget, (Input, Textarea)):
            input_type = self.get_widget_input_type(widget)
            if input_type == "range":
                before.append("form-range")
            else:
                before.append("form-control")
                if input_type == "color":
                    before.append("form-control-color")
                size_prefix = "form-control"

        if size_prefix:
            classes.append(get_size_class(self.size, prefix=size_prefix, skip=["xs", "md"]))

        if self.server_side_validation and self.can_widget_have_server_side_validation(widget):
            classes.append(self.get_server_side_validation_classes())

        classes = before + classes
        widget.attrs["class"] = merge_css_classes(*classes)

    def add_placeholder_attrs(self, widget=None):
        """Add placeholder attribute to widget."""
        if widget is None:
            widget = self.widget
        placeholder = widget.attrs.get("placeholder", self.placeholder)
        if placeholder and self.set_placeholder and is_widget_with_placeholder(widget):
            widget.attrs["placeholder"] = conditional_escape(strip_tags(placeholder))

    def add_widget_attrs(self):
        """Return HTML attributes for widget as dict."""
        if self.is_multi_widget:
            widgets = self.widget.widgets
        else:
            widgets = [self.widget]
        for widget in widgets:
            self.add_widget_class_attrs(widget)
            self.add_placeholder_attrs(widget)
            if isinstance(widget, (RadioSelect, CheckboxSelectMultiple)):
                widget.template_name = "django_bootstrap5/widgets/radio_select.html"
            elif isinstance(widget, ClearableFileInput):
                widget.template_name = "django_bootstrap5/widgets/clearable_file_input.html"

    def get_label_class(self, horizontal=False):
        """Return CSS class for label."""
        label_classes = [text_value(self.label_class)]
        if not self.show_label:
            label_classes.append("visually-hidden")
        else:
            if isinstance(self.widget, CheckboxInput):
                widget_label_class = "form-check-label"
            elif self.is_inline:
                widget_label_class = "visually-hidden"
            elif horizontal:
                widget_label_class = merge_css_classes(self.horizontal_label_class, "col-form-label")
            else:
                widget_label_class = "form-label"
            label_classes = [widget_label_class] + label_classes
        return merge_css_classes(*label_classes)

    def get_field_html(self):
        """Return HTML for field."""
        self.add_widget_attrs()
        field_html = self.field.as_widget(attrs=self.widget.attrs)
        self.restore_widget_attrs()
        return field_html

    def get_label_html(self, horizontal=False):
        """Return value for label."""
        label_html = "" if self.show_label == "skip" else self.field.label
        if label_html:
            label_html = render_label(
                label_html,
                label_for=self.field.id_for_label,
                label_class=self.get_label_class(horizontal=horizontal),
            )
        return label_html

    def get_help_html(self):
        """Return HTML for help text."""
        help_text = self.help_text or ""
        if help_text:
            return render_template_file(
                "django_bootstrap5/field_help_text.html",
                context={
                    "field": self.field,
                    "help_text": help_text,
                    "layout": self.layout,
                    "show_help": self.show_help,
                },
            )
        return ""

    def get_errors_html(self):
        """Return HTML for field errors."""
        field_errors = self.field_errors
        if field_errors:
            return render_template_file(
                "django_bootstrap5/field_errors.html",
                context={
                    "field": self.field,
                    "field_errors": field_errors,
                    "layout": self.layout,
                    "show_help": self.show_help,
                },
            )
        return ""

    def get_server_side_validation_classes(self):
        """Return CSS classes for server-side validation."""
        if self.field_errors:
            return "is-invalid"
        elif self.field.form.is_bound:
            return "is-valid"
        return ""

    def get_inline_field_class(self):
        """Return CSS class for inline field."""
        return self.inline_field_class or "col-12"

    def get_checkbox_classes(self):
        """Return CSS classes for checkbox."""
        classes = ["form-check"]
        if self.checkbox_style == "switch":
            classes.append("form-switch")
        if self.checkbox_layout == "inline":
            classes.append("form-check-inline")
        return merge_css_classes(*classes)

    def get_wrapper_classes(self):
        """Return classes for wrapper."""
        wrapper_classes = []

        if self.is_inline:
            wrapper_classes.append(self.get_inline_field_class())
            wrapper_classes.append(self.inline_wrapper_class)
        else:
            if self.is_horizontal:
                wrapper_classes.append("row")
            wrapper_classes.append(self.wrapper_class)

        if self.is_floating:
            wrapper_classes.append("form-floating")

        # The indicator classes are added to the wrapper class. Bootstrap 5 server-side validation classes
        # are added to the fields
        if self.field.errors:
            wrapper_classes.append(self.error_css_class)
        elif self.field.form.is_bound:
            wrapper_classes.append(self.success_css_class)
        if self.field.field.required:
            wrapper_classes.append(self.required_css_class)

        return merge_css_classes(*wrapper_classes)

    def field_before_label(self):
        """Return whether field should be placed before label."""
        return isinstance(self.widget, CheckboxInput) or self.is_floating

    def render(self):
        if self.field.name in self.exclude.replace(" ", "").split(","):
            return mark_safe("")
        if self.field.is_hidden:
            return text_value(self.field)

        field = self.get_field_html()

        if self.field_before_label():
            label = self.get_label_html()
            field = field + label
            label = mark_safe("")
            horizontal_class = merge_css_classes(self.horizontal_field_class, self.horizontal_field_offset_class)
        else:
            label = self.get_label_html(horizontal=self.is_horizontal)
            horizontal_class = self.horizontal_field_class

        help = self.get_help_html()
        errors = self.get_errors_html()

        if self.is_form_control_widget():
            addon_before = (
                format_html('<span class="input-group-text">{}</span>', self.addon_before) if self.addon_before else ""
            )
            addon_after = (
                format_html('<span class="input-group-text">{}</span>', self.addon_after) if self.addon_after else ""
            )
            if addon_before or addon_after:
                classes = "input-group"
                if self.server_side_validation and self.get_server_side_validation_classes():
                    classes = merge_css_classes(classes, "has-validation")
                field = format_html('<div class="{}">{}{}{}{}</div>', classes, addon_before, field, addon_after, errors)
                errors = ""

        if isinstance(self.widget, CheckboxInput):
            field = format_html('<div class="{}">{}{}{}</div>', self.get_checkbox_classes(), field, errors, help)
            errors = ""
            help = ""

        field_with_errors_and_help = format_html("{}{}{}", field, errors, help)

        if self.is_horizontal:
            field_with_errors_and_help = format_html(
                '<div class="{}">{}</div>', horizontal_class, field_with_errors_and_help
            )

        return format_html(
            '<div class="{wrapper_classes}">{label}{field_with_errors_and_help}</div>',
            wrapper_classes=self.get_wrapper_classes(),
            label=label,
            field_with_errors_and_help=field_with_errors_and_help,
        )

src/django_bootstrap5/renderers.py
diff --git a/src/django_bootstrap5/renderers.py b/src/django_bootstrap5/renderers.py
index 41bb4aa4..2d54f2a1 100644
--- a/src/django_bootstrap5/renderers.py
+++ b/src/django_bootstrap5/renderers.py
@@ -464,7 +464,7 @@ def field_before_label(self):
 
     def render(self):
         if self.field.name in self.exclude.replace(" ", "").split(","):
-            return ""
+            return mark_safe("")
         if self.field.is_hidden:
             return text_value(self.field)
 

Rendering form with exclude is broken
Example:

```
{% bootstrap_form form exclude="ignore_field" %}
```
field renderer returns empty (unsafe) string when encountering the ignored field. When concatenated with safe text, the resulting string becomes unsafe and renders escaped html string into template instead rendering the form.
