import json import warnings from django import VERSION from django.forms.utils import flatatt from django.templatetags.static import static from django.utils.html import format_html, html_safe, mark_safe __all__ = ("JS", "static") _sentinel = object() class JS: """ Use this to insert a script tag via ``forms.Media`` containing additional attributes (such as ``id`` and ``data-*`` for CSP-compatible data injection.):: forms.Media(js=[ JS('asset.js', { 'id': 'asset-script', 'data-answer': '"42"', }), ]) The rendered media tag (via ``{{ media.js }}`` or ``{{ media }}`` will now contain a script tag as follows, without line breaks:: The attributes are automatically escaped. The data attributes may now be accessed inside ``asset.js``:: var answer = document.querySelector('#asset-script').dataset.answer; """ def __init__(self, js, attrs=None, static=_sentinel): self.js = js self.attrs = attrs or {} if static is not _sentinel: warnings.warn( "JS automatically determines whether it received an absolute" " path or not. Stop passing the 'static' argument please.", DeprecationWarning, stacklevel=2, ) def startswith(self, _): # Masquerade as absolute path so that we are returned as-is. return True def __repr__(self): return f"JS({self.js}, {json.dumps(self.attrs, sort_keys=True)})" if VERSION >= (4, 1): def __str__(self): return format_html( '', self.js if self.js.startswith(("http://", "https://", "/")) else static(self.js), mark_safe(flatatt(self.attrs)), ) else: def __html__(self): js = ( self.js if self.js.startswith(("http://", "https://", "/")) else static(self.js) ) return ( format_html('{}"{}', js, mark_safe(flatatt(self.attrs)))[:-1] if self.attrs else js ) def __eq__(self, other): if isinstance(other, JS): return self.js == other.js and self.attrs == other.attrs return self.js == other and not self.attrs def __hash__(self): return hash((self.js, json.dumps(self.attrs, sort_keys=True))) if VERSION >= (4, 1): JS = html_safe(JS)