Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django integration #115

Open
Stranger6667 opened this issue Jun 23, 2021 · 4 comments
Open

Django integration #115

Stranger6667 opened this issue Jun 23, 2021 · 4 comments

Comments

@Stranger6667
Copy link
Owner

Stranger6667 commented Jun 23, 2021

Something like https://github.com/roverdotcom/django-inlinecss (template tag) and https://github.com/Dino16m/mailcomposer (CLI command) but simpler

@Mogost
Copy link
Contributor

Mogost commented Jul 29, 2021

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

@Stranger6667
Copy link
Owner Author

Thank you for 👍 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 :)

@Stranger6667
Copy link
Owner Author

Or, I'll be happy to review PRs if anyone is willing to contribute this feature :)

@larrybotha
Copy link

larrybotha commented Sep 29, 2022

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:

  1. finds all statically resolved stylesheets and replaces their paths with their absolute path equivalents
  2. inlines the modified HTML using css-inline
  3. 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants