css-inline
css-inline copied to clipboard
Django integration
Something like https://github.com/roverdotcom/django-inlinecss (template tag) and https://github.com/Dino16m/mailcomposer (CLI command) but simpler
django-inlinecss is not maintained for some time. And the appearance of a template tag for django in css-inline looks promising. In fact django-inlinecss supports different engines and it takes 5 minutes to put css-inline in there.
https://github.com/roverdotcom/django-inlinecss/blob/aca7106337cdc85a77dfbffb333ae91aaae74e14/django_inlinecss/engines.py#L1-L29
Thank you for :+1: on this :)
It is unfortunate that django-inlinecss is not actively maintained; it would be nice to have a direct integration there indeed. However, it will also be nice to have everything integrated under the same roof. I.e., running tests against all current Django versions, etc., on each change in css-inline.
I was thinking about deriving a design similar to django-inlinecss in this repo and distributing something like css_inline.contrib.django. Maybe also some jinja tag as well.
I don't have any timeline for this, though, but I hope to get back to this somewhere in August / September.
Btw, feel free to open an issue if you miss anything in css-inline :)
Or, I'll be happy to review PRs if anyone is willing to contribute this feature :)
I've written my own component tag which may be a source of inspiration:
src/app/templatetags/css.py
# This is free and unencumbered software released into the public domain.
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
# For more information, please refer to <https://unlicense.org>
from typing import List
import css_inline
import lxml.html
import logging
from django.conf import settings
from django.template import Node
from django.template.base import Parser, Token
from django.template.library import Library
register = Library()
_INLINE_CSS_TAG_NAME = "inline_css"
@register.tag(name=_INLINE_CSS_TAG_NAME)
def inline_css(parser: Parser, token: Token):
"""inline_css
Inlines CSS in a given document, and then strips the link tags from the resulting HTML
Usage:
{% inline_css %}
<html>
<head>
<link rel="stylesheet" href="{% static 'style.css' %}">
<link rel="stylesheet" href="https://some.url/to/style.css">
</head>
// ...
</html>
{% end_inline_css %}
"""
nodelist = parser.parse(f"end_{_INLINE_CSS_TAG_NAME}")
parser.delete_first_token()
return _InlineCssNode(nodelist)
class _InlineCssNode(Node):
def __init__(self, nodelist):
self.nodelist = nodelist
def render(self, context):
html = self.nodelist.render(context)
# attempt to inline CSS. If inlining / DOM manipulation fails, return the original html
try:
html_with_abs_style_hrefs = self._rewrite_relative_style_hrefs(html)
html_inlined_styles = self._inline_styles(html_with_abs_style_hrefs)
html = self._strip_link_tags(html_inlined_styles)
except (OSError, css_inline.InlineError) as e:
logging.getLogger(__name__).exception(e)
return html
def _rewrite_relative_style_hrefs(self, html: str, /) -> str:
tree = lxml.html.fromstring(html)
xpath_pattern = f"//link[starts-with(@href, '{settings.STATIC_URL}')]"
xpath_result = tree.xpath(xpath_pattern)
tags = xpath_result if isinstance(xpath_result, list) else []
link_tags: List[lxml.html.HtmlElement] = [
x for x in tags if isinstance(x, lxml.html.HtmlElement)
]
# rewrite `/static/[asset]` paths to absolute URLs so that css_inline can resolve
# the stylesheets
for tag in link_tags:
original_href = tag.get("href")
absolute_href = original_href.replace(
settings.STATIC_URL, f"{settings.STATIC_ROOT}/"
)
tag.set("href", absolute_href)
return lxml.html.tostring(tree).decode("utf-8")
def _inline_styles(self, html: str, /) -> str:
html = css_inline.inline(html)
return html
def _strip_link_tags(self, html: str, /) -> str:
tree = lxml.html.fromstring(html)
xpath_result = tree.xpath("//link")
tags = xpath_result if isinstance(xpath_result, list) else []
link_tags: List[lxml.html.HtmlElement] = [
x for x in tags if isinstance(x, lxml.html.HtmlElement)
]
# strip all <link> tags - different OS's and OS-level deps require different configurations
# when reading from the OS, so we eliminate any discrepancies by removing the link
# tags entirely
for tag in link_tags:
tag.getparent().remove(tag)
return lxml.html.tostring(tree).decode("utf-8")
What it does:
- finds all statically resolved stylesheets and replaces their paths with their absolute path equivalents
- inlines the modified HTML using
css-inline - strips the link tags from the document
The stripping of link tags is quite likely the responsibility of another custom tag, but it suits my use-case when generating PDFs - wkhtmltopdf on MacOSX fails to resolve the absolute URLs with its default config, but succeeds in Docker - so I encapsulated the removal in the inline_css tag.
Loving css-inline - great work!