GithubHelp home page GithubHelp logo

felixschwarz / mjml-python Goto Github PK

View Code? Open in Web Editor NEW
70.0 4.0 14.0 245 KB

Python implementation for MJML - a framework that makes responsive-email easy

License: MIT License

Python 100.00%
mjml python email responsive-email responsive

mjml-python's Introduction

mjml-python

This is an unofficial Python port of mjml v4. It is implemented in pure Python and does not require JavaScript/NodeJS. mjml is a markup language created by Mailjet and designed to reduce the pain of coding a responsive email.

Installation

pip install mjml

Usage

from mjml import mjml_to_html
with open('foo.mjml', 'rb') as mjml_fp:
    result = mjml_to_html(mjml_fp)
assert not result.errors
html: str = result.html

Alternatively you can run the code from the CLI:

$ mjml foo.mjml

Limitations

This library only implements a subset of the original MJML project. It lacks several features found in the JavaScript mjml implementation (e.g. minification, beautification and validation). Also the code likely contains many additional bugs.

The upside is that there are lot of possibilities for you to make a real difference when you improve the code :-)

Goals / Motivation

This library should track the JS version of mjml so ideally you should get the same HTML. However even under the best circumstances this library will always lag a bit behind as each change must be translated to Python manually (a mostly mechanical process).

While I like the idea behind mjml and all the knowledge about the quirks to get acceptable HTML rendering by various email clients we did not want to deploy a Node.js-based stack on our production servers. We did not feel comfortable auditing all 220 JS packages which are installed by npm install mjml (and re-doing this whenever new versions are available). Also due to data-privacy concerns we were unable to use any third-party products (i.e. MJML's API offering).

After a short spike to check the viability of a Python implementation I went ahead and wrote enough code to ensure some existing messages could be converted to mjml. Currently the library is deployed in some light production scenarios.

Another benefit of using Python is that we can integrate that in our web apps more closely. As the startup overhead of CPython is much lower than Node.js we can also generate a few mails via CLI applications without massive performance problems. CPython uses ~70ms to translate a trivial mjml template to HTML while Node.JS needs ~650ms.

Documentation

The idea is to implement the mjml XML dialect exactly like the JS implementation so eventually you should be able to use the official docs and other online resources found on mjml.io. However we are nowhere near that right now! The current code can render the "Hello World" example as well as images, tables and groups but many components remain to be reimplemented. I'd love to see your pull requests to improve the current state though.

Alternatives / Additional Resources

  • django-mjml: If deploying NodeJS is not an issue and you are using Django you could use the well established django-mjml library. That library integrates the mjml JavaScript implementation with Django templates so you can access all mjml features.
  • MJML.NET: This is an unofficial port of mjml to C# (github repo) which supports more components than this Python implementation.
  • mrml: rust implementation of mjml (github repo)
  • email-bugs is a github project which contains a lot of knowledge about rendering quirks in various email clients.
  • htmlemailcheck is a commercial offering to help you checking email rendering in various environments. I don't have any experience with their services but they provide a free knowledgebase.
  • #emailgeeks - Slack community for email marketers, designers, and developers

mjml-python's People

Contributors

barsch avatar benjipott avatar buttle avatar caseyjhol avatar felixschwarz avatar pcorpet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

mjml-python's Issues

TypeError in mj_section.py

Hi,

i got the following error:

File "/Users/handorf/.pyenv/versions/3.10.2/envs/ga3-exporter/lib/python3.10/site-packages/mjml/elements/mj_section.py", line 342, in renderWithBackground
vY = self._calc_origin_pos_value(is_y=False, bg_pos=bgPosY)
TypeError: MjSection._calc_origin_pos_value() got an unexpected keyword argument 'is_y'

seems to be a simple typo.

Unable to render template

When running mjml-python I get this error that I do not get compared to running : ./node_modules/.bin/mjml index.mjml input.mjml -o output.html

Value Example:

<mjml>
    <mj-head>
        <mj-title>Your order has been cancelled!</mj-title>
        <mj-attributes>
            <mj-all padding="0px"></mj-all>
            <mj-text font-family="Roboto, Helvetica, sans-serif" font-size="14px"></mj-text>
            <mj-table font-family="Roboto, Helvetica, sans-serif" font-size="14px"></mj-table>
            <mj-section background-color="#ffffff"></mj-section>
            <mj-wrapper background-color="#ffffff"></mj-wrapper>
        </mj-attributes>
        
<mj-style inline="inline">
    .summary-header {
        color: #85807f;
        font-size: 16px;
    }

    .delimited {
        border-bottom: 1px solid #b8b8b8;
    }

    .delimited > td {
        padding-bottom: 16px;
    }

    .extra-info {
        margin-top: 8px;
    }

    .cost {
        font-weight: bold;
        padding-left: 1em;
        text-align: right;
        vertical-align: top;
        white-space: nowrap;
    }

    .total {
        text-align: right;
        font-size: 24px;
        font-weight: bold;
    }

    table {
        margin: 0 auto;
        border-collapse: collapse;
        font-family: Roboto, Helvetica, sans-serif;
    }

    td {
        padding-top: 16px;
    }

    .bottom-spacer {
        height: 3rem;
    }
</mj-style>

        <mj-style inline="inline">
            .footer-text div {
                font-size: 11px !important;
                color: #000000 !important;
                text-align: center !important;
            }
        </mj-style>
    </mj-head>
    <mj-body background-color="#dddddd">
        <!-- Email Header -->
        <mj-section>
            <mj-column width="100%">
                <mj-image src="https://www.foodandwine.com/thmb/DI29Houjc_ccAtFKly0BbVsusHc=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/crispy-comte-cheesburgers-FT-RECIPE0921-6166c6552b7148e8a8561f7765ddf20b.jpg" alt="" padding="0px"></mj-image>
            </mj-column>
        </mj-section>
        <!-- Email Content -->
        <mj-wrapper padding="20px">
            
    <mj-section>
        
            <mj-raw>{% for item in items %} </mj-raw>
            <mj-column width="80%">
                <mj-text>{{ item.title }}</mj-text>
                <mj-image src="{{ item.image }}" alt="" padding="0px"></mj-image>
            </mj-column>
            <mj-raw>{% endfor %}</mj-raw>
        
    </mj-section>

        </mj-wrapper>
        <!-- Footer -->
        <mj-section padding-bottom="20px">
            <mj-column>
                <mj-text css-class="footer-text" padding-top="40px">
                    Copyright  &copy;  All rights reserved.
                </mj-text>
                <mj-text css-class="footer-text" padding-top="10px">
                    <a href="/terms">Terms and Conditions</a>
                </mj-text>
            </mj-column>
        </mj-section>
    </mj-body>
</mjml>

If I pass the string directly I get:

Traceback (most recent call last):
  File "lib/python3.9/site-packages/mjml/mjml2html.py", line 79, in mjml_to_html
    mjBody = mjml_root('mj-body')[0]
TypeError: 'NoneType' object is not callable

If I save the value to a file then I get:

    results = mjml_to_html(rendered_html)
  File "lib/python3.9/site-packages/mjml/mjml2html.py", line 230, in mjml_to_html
    raise ImportError('CSS inlining is an optional feature. Run `pip install -e ".[css_inlining]"` to install the required dependencies.') # noqa: E501
ImportError: CSS inlining is an optional feature. Run `pip install -e ".[css_inlining]"` to install the required dependencies.

When I run pip install, I get:

ERROR: Could not find a version that satisfies the requirement css_inlining (from versions: none)
ERROR: No matching distribution found for css_inlining

Again, I don't get this error when using node.

Add missing MJML components

The following MJML components are currently missing:

  • mjml-accordion
  • mjml-carousel
  • mjml-head-breakpoint
  • mjml-head-html-attributes
  • mjml-hero
  • mjml-navbar
    • mjml-navbar-link
  • mjml-social
  • mjml-spacer
  • mjml-wrapper

I'll begin work on a PR and check off components as I work my way through the above list.

Whitespace gets stripped between text & link inside <mj-text />

This appears to be a bug inside mj-text rendering. If I run this code:

from mjml import mjml_to_html
print(mjml_to_html('<mjml><mj-body><mj-text>Hello <a href="#">world</a></mj-text></mj-body></mjml>')['html'].split('\n')[-3])

I get this output:

    <div style=""><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000">Hello<a href="#">world</a></div></div>

The space between "Hello" and "<a" gets stripped during the parsing.

I haven’t been able to dive through the code enough, yet, but seems like the first children element inside this mj-text should have elem["tail"] == " " https://github.com/FelixSchwarz/mjml-stub/blob/main/mjml/elements/mj_text.py#L104

mj-attributes with mj-body leads to empty body

With mjml==0.10.0 I get the following unexpected results when passing in an mj-body tag inside mj-attributes within mj-head. Also, if I try this on the MJML test page the body properly gets a background color.

Call with mj-body

mjml_to_html('''<mjml>
  <mj-head>
    <mj-attributes>
      <mj-body background-color="#F6F6F6" />
    </mj-attributes>
  </mj-head>
  <mj-body>
    <mj-text>
      TESTTESTTESTTESTTESTTESTTEST
    </mj-text>
  </mj-body>
</mjml>''')

Result has no content inside body:

{"html": "<!doctype html>\n<html xmlns="http://www.w3.org/1999/xhtml\" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">\n \n <title>\n \n </title>\n \n <meta http-equiv="X-UA-Compatible" content="IE=edge">\n \n <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <style type="text/css">\n #outlook a { padding:0; }\n body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n p { display:block;margin:13px 0; }\n </style>\n \n \n \n \n <style type="text/css">\n\n \n \n </style>\n <style type="text/css"></style>\n \n \n <body style="word-spacing:normal;">\n \n <div style="">\n \n", "errors": []}

Call with mj-html instead

I’m not sure mj-html is even supported in mjml, but you can also just remove it entirely and get the same result below:

mjml_to_html('''<mjml>
  <mj-head>
    <mj-attributes>
      <mj-html background-color="#F6F6F6" />
    </mj-attributes>
  </mj-head>
  <mj-body>
    <mj-text>
      TESTTESTTESTTESTTESTTESTTEST
    </mj-text>
  </mj-body>
</mjml>''')

Result has expected TESTTEST… content inside body:

{"html": "<!doctype html>\n<html xmlns="http://www.w3.org/1999/xhtml\" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">\n \n <title>\n \n </title>\n \n <meta http-equiv="X-UA-Compatible" content="IE=edge">\n \n <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <style type="text/css">\n #outlook a { padding:0; }\n body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n p { display:block;margin:13px 0; }\n </style>\n \n \n \n \n <link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700\" rel="stylesheet" type="text/css">\n <style type="text/css">\n @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);\n </style>\n \n\n \n \n <style type="text/css">\n\n \n \n </style>\n <style type="text/css"></style>\n \n \n <body style="word-spacing:normal;">\n \n <div style=""><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000">TESTTESTTESTTESTTESTTESTTEST\n \n", "errors": []}

Orphans opening tags from HTML `<br>`

From Passport (mailjet's editor) I receive some <br> HTML tags inside mjml-text blobs. In XHTML, those should actually be <br/> and it currently make the mjml-stub library choke.

  1. Should I clean them up before using mjml-stub?
  2. Can I try to fix the lib to accept them?

Note that mjml.io is able to work with those tags
https://mjml.io/try-it-live/gb2kuVgCdRn
The HTML is generated properly with br tags, there are no errors mentioned. Only the syntax highlighter gets lost.

I'd be in favor of doing 2, but don't want to start before your approval.

css-class does not work with mj-section

The logic here is backwards from what it is in upstream:

    wrapper_class = self.get_attr('css-class') if self.isFullWidth() else None

https://github.com/FelixSchwarz/mjml-stub/blob/main/mjml/elements/mj_section.py#L202

   <div ${this.htmlAttributes({
    class: this.isFullWidth() ? null : this.getAttribute('css-class'),
    style: 'div',
  })}>

https://github.com/mjmlio/mjml/blob/988819de3375867c09585d28f555166b97415200/packages/mjml-section/src/index.js#L414

I'll work on a PR and regression test.

release planning for 0.9

@caseyjhol: Is there anything else you would like to add to this repo soon-ish or should we rename the repo and release 0.9?
I remember you were working on a different css inlining mechanism.

No hurry from my side but it might be nice to release all the work you did so far.

Use mjml-stub inside a script

Hi,

I would like to be able to do something like this

from mjml import parser

with open(file.mjml, 'r') as mjml_file:
    html = parser(mjml_file)

Is that possible?

Cheers.

extend functionality to handle "welcome-email" from email-templates repo

The upstream project also hosts a couple of example templates in the email-templates repo. One of these examples is welcome-email.mjml but that template can not processed currently due to shortcomings (even after PR #2) in this Python port:

  File "…/mjml/mjml2html.py", line 168, in mjml_to_html
    content = processing(mjBody, bodyHelpers, applyAttributes)
  File "…/mjml/mjml2html.py", line 74, in processing
    return component.render()
  File "…/mjml/elements/mj_body.py", line 37, in render
    children_str = self.renderChildren()
  File "…/mjml/elements/_base.py", line 144, in renderChildren
    output += renderer(component)
  File "…/mjml/elements/_base.py", line 86, in <lambda>
    renderer = lambda component: component.render()
  File "…/mjml/elements/mj_section.py", line 92, in render
    return self.renderSimple()
  File "…/mjml/elements/mj_section.py", line 98, in renderSimple
    section = self.renderSection()
  File "…/mjml/elements/mj_section.py", line 145, in renderSection
    {self.renderWrappedChildren()}
  File "…/mjml/elements/mj_section.py", line 188, in renderWrappedChildren
    {self.renderChildren(children, renderer=render_child)}
  File "…/mjml/elements/_base.py", line 144, in renderChildren
    output += renderer(component)
  File "…/mjml/elements/mj_section.py", line 178, in render_child
    {component.render()}
  File "…/mjml/elements/mj_column.py", line 122, in render
    column_str = self.renderColumn() if (not self.hasGutter()) else self.renderGutter()
  File "…/mjml/elements/mj_column.py", line 233, in renderColumn
    {self.renderChildren(children, renderer=render_child)}
  File "…/mjml/elements/_base.py", line 144, in renderChildren
    output += renderer(component)
  File "…/mjml/elements/mj_column.py", line 220, in render_child
    {component.render()}
  File "…/mjml/elements/mj_text.py", line 54, in render
    return self._render_content()
  File "…/mjml/elements/mj_text.py", line 69, in _render_content
    content_html = self.getContent() + children_html
  File "…/mjml/core/api.py", line 62, in getContent
    return self.content.strip()
AttributeError: 'NoneType' object has no attribute 'strip'

I noticed the upstream JS code complains about some illegal attributes and the mjml template in general looks pretty convoluted:

Line 2 of welcome-email.mjml (mj-body) — Attribute font-size is illegal
Line 8 of welcome-email.mjml (mj-section) — Attribute vertical-align is illegal
Line 10 of welcome-email.mjml (mj-text) — Attribute padding-top has invalid value: 50 for type Unit, only accepts (px, %) units and 1 value(s)
Line 13 of welcome-email.mjml (mj-section) — Attribute padding-top has invalid value: 20 for type Unit, only accepts (px, %) units and 1 value(s)

Nevertheless I think the Python code should be able to handle that (unless the Python code becomes too messy).

Also there is an upstream pull request to clean up that template by @willhertz but even after applying that change the Python code fails.

  File "…/mjml/mjml2html.py", line 167, in mjml_to_html
    globalDatas.headRaw = processing(mjHead, headHelpers)
  File "…/mjml/mjml2html.py", line 72, in processing
    return component.handler()
  File "…/mjml/elements/head/mj_head.py", line 9, in handler
    return self.handlerChildren()
  File "…/mjml/elements/head/_head_base.py", line 28, in handlerChildren
    return tuple(map(handle_children, childrens))
  File "…/mjml/elements/head/_head_base.py", line 22, in handle_children
    component.handler()
  File "…/mjml/elements/head/mj_attributes.py", line 14, in handler
    tagName = child['tagName']
TypeError: 'NoneType' object is not subscriptable

The Python code must be able to handle the updated template as the upstream validator does not complain about any invalid attributes there.

mj-include not supported

Until now we just relied on our templating engine to achieve a similar effect like <mj-include> (reference documentation) but feedback from web designers is that they'd like <mj-include> because it allows them to still use Atom's mjml preview feature.

Type hints for public interface?

Thanks for the really useful library!

It would be even better if it came with type annotations for its public interface (i.e. just mjml_to_html, as far as I can tell) and a py.typed marker to allow dependent projects to type check their usage of the library.

Escaped HTML tags are "un-escaped" when rendering HTML

Consider:

from mjml import mjml_to_html

result = mjml_to_html("""
<mjml>
  <mj-body>
    <mj-text>
      Pretty unsafe: &lt;script&gt;
    </mj-text>
  </mj-body>
</mjml>""")

print(result["html"])

In the resulting HTML output, the formerly HTML-escaped < (&lt;) and > (&gt) are "un-escaped", so the rendered HTML actually contains Pretty unsafe: <script>.

Why does this happen? This reverses the user's safety measures and can be dangerous.

The MJML reference implementation doesn't do this and correctly keeps such escape sequences untouched: https://mjml.io/try-it-live/fvvhZhdu9V

support CSS inlining

It would be nice to support CSS inlining (similar to mjml upstream). There are some Python libraries which might be helpful:

Based on the number of github stars, forks I guess premailer seems to be the most popular library.

Child selector styles not being applied

.parent {
  overflow: hidden;
  box-shadow: 0 4px 10px 0px rgba(0, 0, 0, 0.1);
}
.parent > table > tbody > tr > td,
.parent > table > tbody > tr > td > div {
  border-radius: 3px;
}
<div class="parent">
    <table>
        <tbody>
            <tr>
                <td>
                    <div>
                        Test
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
</div>

Actual:

<div class="parent" style="overflow: hidden; box-shadow: 0 4px 10px 0px rgba(0, 0, 0, 0.1);">
    <table>
        <tbody>
            <tr>
                <td>
                    <div>
                        Test
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
</div>

Expected:

<div class="parent" style="overflow: hidden; box-shadow: 0 4px 10px 0px rgba(0, 0, 0, 0.1);">
    <table>
        <tbody>
            <tr>
                <td style="border-radius: 3px;">
                    <div style="border-radius: 3px;">
                        Test
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
</div>

Originally I thought this was an issue with css-inline, but it's actually an issue on our end. BeautifulSoup is escaping the carets (> becomes &gt;), preventing child selectors from getting applied. We can either use html.unescape in mjStyle, or perhaps we can decode from BeautifulSoup using formatter=None. It might be better to limit this to mjStyle only and use html.unescape so as not to affect the rest of the HTML. I'm still investigating the best approach.

add changelog

We should have a changelog file to mention changes for non-git users.

Add json format support

The javascript version of mjml handles json MJML both as input (if you give a dict to the mjml2html function) and as an output (in a json field).

Before sending you a PR:

  • is it OK to add this feature?
  • for input, the API is not exactly the same as in js (python accepts a file descriptor, whereas js gets a string), is it OK to accept a dict for json-like format?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.