Skip to content

Support <mj-raw position="file-start"> #74

Description

@FelixSchwarz

Upstream MJML supports placing raw content before the generated <!doctype html> declaration:

<mjml>
  <mj-raw position="file-start">
    <!-- generated by our mail template system -->
  </mj-raw>

  <mj-body>
    <mj-section>
      <mj-column>
        <mj-text>Hello!</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

Expected output:

<!-- generated by our mail template system -->
<!doctype html>
<html ...>

mjml-python currently drops the root-level mj-raw element and starts the output directly with the doctype.

Example use case

Some template and email systems store metadata at the start of the template. For example, a Twig/Craft CMS template may contain routing information consumed before the HTML is sent:

<mjml>
  <mj-raw position="file-start">
    {# subject: Password reset for {{ user.email }} #}
    {# fromEmail: {{ systemEmail }} #}
    {# fromName: Support #}
    {# includeAttachments: false #}
  </mj-raw>

  <mj-body>
    ...
  </mj-body>
</mjml>

This metadata must remain outside the HTML document. Putting it inside <head> or <body> could expose it as document content or interfere with later template processing.

Upstream itself uses Twig-style metadata as a regression-test example for this feature.

Upstream behavior

Upstream collects direct mj-raw children of <mjml> whose position attribute is file-start. Their contents are joined with newlines and stored as globalData.beforeDoctype.

The skeleton then emits:

`${beforeDoctype ? `${beforeDoctype}\n` : ''}<!doctype html>`

Relevant upstream code:

  • packages/mjml-core/src/index.js, handling of mjOutsideRaws
  • packages/mjml-core/src/helpers/skeleton.js, beforeDoctype
  • packages/mjml-raw/src/index.js, declaration of position
  • packages/mjml-raw/README.md

Proposed implementation

  • Add position: enum(file-start) to MjRaw.allowed_attrs().
  • Collect direct mj-raw children of the root <mjml> element.
  • Select elements whose position is file-start.
  • Join their contents with \n, preserving document order.
  • Pass the result to the skeleton as beforeDoctype.
  • Emit one newline between the prefix and the doctype.
  • Continue ignoring root-level mj-raw elements without position="file-start", matching upstream.
  • Do not change the behavior of mj-raw inside mj-head or mj-body.

Acceptance criteria

Given:

<mjml>
  <mj-raw position="file-start">first</mj-raw>
  <mj-raw position="file-start">second</mj-raw>
  <mj-body>
    <mj-section>
      <mj-column><mj-text>Hello</mj-text></mj-column>
    </mj-section>
  </mj-body>
</mjml>

the result must start with:

first
second
<!doctype html>

The remainder of the generated document must continue to match upstream.

Add regression coverage for:

  • one file-start element;
  • multiple elements and their ordering;
  • template metadata containing Twig/Jinja-style syntax;
  • an ordinary root-level mj-raw without position;
  • ordinary mj-raw inside the body.

Testing caveat

The existing assert_same_html() comparison cannot detect a missing HTML comment before the doctype. HTMLCompare parses only the resulting HTML document tree; pre-doctype comments are currently discarded. This remains true even with ignore_comments=False.

The mjml-python regression test must therefore contain a raw-string assertion, for example:

assert actual_html.startswith(
    "<!-- generated by our mail template system -->\n<!doctype html>"
)

The remainder of the document can still be checked with assert_same_html().

A useful separate HTMLCompare enhancement would be an opt-in option such as compare_document_prefix=True. It would need to capture the source before the doctype before passing the document to html5lib, then report something like DOCUMENT_PREFIX_MISMATCH. Making it opt-in would preserve HTMLCompare’s current semantic behavior and avoid unexpectedly treating ordinary pre-doctype comments as significant.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions