├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build-documentation.yml
│ └── mjml-workflow.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── babel.config.js
├── doc
├── basic.md
├── body_components.md
├── community-components.md
├── components_1.md
├── components_2.md
├── config.json
├── create.md
├── ending-tags.md
├── getting_started.md
├── guide.md
├── head_components.md
├── install.md
├── mjml-chart.md
├── mjml-chartjs.md
├── mjml-mso-button.md
├── mjml-qr-code.md
├── passport.md
├── ports.md
├── tooling.md
└── using_mjml_in_json.md
├── lerna.json
├── package.json
├── packages
├── mjml-accordion
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── Accordion.js
│ │ ├── AccordionElement.js
│ │ ├── AccordionText.js
│ │ ├── AccordionTitle.js
│ │ └── index.js
├── mjml-body
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-browser
│ ├── README.md
│ ├── browser-mocks
│ │ ├── fs.js
│ │ ├── path.js
│ │ └── uglify-js.js
│ ├── package.json
│ └── webpack.config.js
├── mjml-button
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-carousel
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── Carousel.js
│ │ ├── CarouselImage.js
│ │ └── index.js
├── mjml-cli
│ ├── README.md
│ ├── bin
│ │ └── mjml
│ ├── package.json
│ └── src
│ │ ├── client.js
│ │ ├── commands
│ │ ├── outputToConsole.js
│ │ ├── outputToFile.js
│ │ ├── readFile.js
│ │ ├── readStream.js
│ │ └── watchFiles.js
│ │ └── helpers
│ │ ├── defaultOptions.js
│ │ └── fileContext.js
├── mjml-column
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-core
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── components.js
│ │ ├── createComponent.js
│ │ ├── helpers
│ │ │ ├── conditionalTag.js
│ │ │ ├── fonts.js
│ │ │ ├── formatAttributes.js
│ │ │ ├── genRandomHexString.js
│ │ │ ├── jsonToXML.js
│ │ │ ├── makeLowerBreakpoint.js
│ │ │ ├── mediaQueries.js
│ │ │ ├── mergeOutlookConditionnals.js
│ │ │ ├── minifyOutlookConditionnals.js
│ │ │ ├── mjmlconfig.js
│ │ │ ├── preview.js
│ │ │ ├── shorthandParser.js
│ │ │ ├── skeleton.js
│ │ │ ├── styles.js
│ │ │ ├── suffixCssClasses.js
│ │ │ └── widthParser.js
│ │ ├── index.js
│ │ └── types
│ │ │ ├── boolean.js
│ │ │ ├── color.js
│ │ │ ├── enum.js
│ │ │ ├── helpers
│ │ │ └── colors.js
│ │ │ ├── index.js
│ │ │ ├── integer.js
│ │ │ ├── string.js
│ │ │ ├── type.js
│ │ │ └── unit.js
│ └── tests
│ │ ├── .eslintrc
│ │ ├── index.js
│ │ ├── jsonToXml-test.js
│ │ ├── mergeOutlookConditionnals-test.js
│ │ ├── minifyOutlookConditionnals-test.js
│ │ ├── shorthandParser-test.js
│ │ ├── skeleton-test.js
│ │ └── widthParser-test.js
├── mjml-divider
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-group
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head-attributes
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head-breakpoint
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head-font
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head-html-attributes
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head-preview
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head-style
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head-title
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-head
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-hero
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-image
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-migrate
│ ├── LICENSE
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── cli.js
│ │ ├── config.js
│ │ └── migrate.js
├── mjml-navbar
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── Navbar.js
│ │ ├── NavbarLink.js
│ │ └── index.js
├── mjml-parser-xml
│ ├── package.json
│ ├── src
│ │ ├── helpers
│ │ │ ├── cleanNode.js
│ │ │ ├── convertBooleansOnAttrs.js
│ │ │ └── setEmptyAttributes.js
│ │ └── index.js
│ └── test
│ │ ├── incl.mjml
│ │ ├── test-preprocessors.js
│ │ ├── test-utils.js
│ │ ├── test-values.js
│ │ └── test.js
├── mjml-preset-core
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── dependencies.js
│ │ └── index.js
├── mjml-raw
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-section
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-social
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── Social.js
│ │ ├── SocialElement.js
│ │ └── index.js
├── mjml-spacer
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-table
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-text
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
├── mjml-validator
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── MJMLRulesCollection.js
│ │ ├── dependencies.js
│ │ ├── index.js
│ │ └── rules
│ │ ├── errorAttr.js
│ │ ├── ruleError.js
│ │ ├── validAttributes.js
│ │ ├── validChildren.js
│ │ ├── validTag.js
│ │ └── validTypes.js
├── mjml-wrapper
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.js
└── mjml
│ ├── README.md
│ ├── bin
│ └── mjml
│ ├── package.json
│ ├── src
│ └── index.js
│ └── test
│ ├── html-attributes.test.js
│ ├── index.js
│ └── lazy-head-style.test.js
├── readme-ja.md
├── test.js
├── type.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 |
3 | # A special property that should be specified at the top of the file outside of
4 | # any sections. Set to true to stop .editor config file search on current file
5 | root = true
6 |
7 | # Indentation style
8 | # Possible values - tab, space
9 | indent_style = space
10 |
11 | # Indentation size in single-spaced characters
12 | # Possible values - an integer, tab
13 | indent_size = 2
14 |
15 | # Line ending file format
16 | # Possible values - lf, crlf, cr
17 | end_of_line = lf
18 |
19 | # File character encoding
20 | # Possible values - latin1, utf-8, utf-16be, utf-16le
21 | charset = utf-8
22 |
23 | # Denotes whether to trim whitespace at the end of lines
24 | # Possible values - true, false
25 | trim_trailing_whitespace = true
26 |
27 | # Denotes whether file should end with a newline
28 | # Possible values - true, false
29 | insert_final_newline = true
30 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | packages/mjml-core/src/types*
4 | type.js
5 | test-html-attributes.js
6 | test.js
7 | babel.config.js
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb-base",
4 | "prettier"
5 | ],
6 | "parser": "babel-eslint",
7 | "rules": {
8 | "comma-dangle": [ 2, "always-multiline" ],
9 | "semi": [ 2, "never" ],
10 | "no-mixed-operators": 0,
11 | "no-shadow": 0,
12 | "no-param-reassign": 0,
13 | "no-restricted-syntax": 0
14 | },
15 | "env": {
16 | "node": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Create a file with this MJML code: `... `
13 | 2. Render it to HTML by doing '...'
14 | 3. Send the HTML to an email address with '...'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **MJML environment (please complete the following information):**
21 | - OS: [e.g. MacOS]
22 | - MJML Version [e.g. 4.2.0]
23 | - MJML tool used [e.g MJML App]
24 |
25 | **Email sending environment(for rendering issues)**:
26 | - Platform used to send the email [e.g [Putsmail](https://putsmail.com/)]
27 |
28 | **Affected email clients (for rendering issues):**
29 | - Email Client [e.g Gmail]
30 | - OS: [e.g. Windows]
31 | - Browser [e.g. Google Chrome]
32 |
33 | **Screenshots**
34 | If applicable, add screenshots to help explain your problem.
35 |
36 | **Additional context**
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/workflows/build-documentation.yml:
--------------------------------------------------------------------------------
1 | name: Build Documentation
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | buildDoc:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Trigger Documentation Build
13 | uses: peter-evans/repository-dispatch@v1
14 | with:
15 | token: ${{ secrets.documentation_token }}
16 | repository: mjmlio/slate
17 | event-type: build-doc
18 |
--------------------------------------------------------------------------------
/.github/workflows/mjml-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Mjml CI
2 | on: [push, pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | matrix:
8 | node-version: [ 16.x, 18.x, 20.x]
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: Use Node.js ${{ matrix.node-version }}
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: ${{ matrix.node-version }}
15 | - name: Run linting & tests
16 | run: |
17 | yarn install
18 | yarn build
19 | yarn lint
20 | cd packages/mjml-parser-xml
21 | yarn install
22 | yarn test --debug
23 | cd ../../packages/mjml-core
24 | yarn test --debug
25 | cd ../../packages/mjml
26 | yarn test
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | .idea/
4 | lib
5 | node_modules
6 | test.html
7 | /**/npmignore
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "semi": false,
4 | "singleQuote": true,
5 | "trailingComma": "all"
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Mailjet SAS, https://mjml.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [['@babel/env', {
3 | targets: { node: '10' },
4 | include: ['transform-classes'],
5 | }]],
6 | plugins: [
7 | '@babel/proposal-class-properties',
8 | [
9 | '@babel/transform-runtime',
10 | {
11 | // by default the plugin assumes we have 7.0.0-beta.0 version of runtime
12 | // and inline all missing helpers instead of requiring them
13 | version: require('@babel/plugin-transform-runtime/package.json')
14 | .version,
15 | },
16 | ],
17 | 'add-module-exports',
18 | 'lodash',
19 | ],
20 | }
21 |
--------------------------------------------------------------------------------
/doc/body_components.md:
--------------------------------------------------------------------------------
1 | # Standard Body components
2 |
3 | MJML comes out of the box with a set of standard components to help you build easily your first templates without having to reinvent the wheel.
4 |
--------------------------------------------------------------------------------
/doc/community-components.md:
--------------------------------------------------------------------------------
1 | # Community components
2 |
3 | In addition to the standard components available in MJML, our awesome community is contributing by creating their own components.
4 |
5 | To use a community component, proceed as follows:
6 | - Install MJML locally with `npm install mjml` in a folder
7 | - Install the community component with `npm install {component-name}` in the same folder
8 | - Create a `.mjmlconfig` file in the same folder with:
9 |
10 | ```json
11 | {
12 | "packages": [
13 | "component-name/path-to-js-file"
14 | ]
15 | }
16 | ```
17 |
18 | Finally, you can now use the component in a MJML file, for example `index.mjml`, and run MJML locally in your terminal (make sure to be in the folder where you installed MJML and the community component): `./node_modules/.bin/mjml index.mjml`.
19 |
--------------------------------------------------------------------------------
/doc/components_1.md:
--------------------------------------------------------------------------------
1 | # Components
2 |
3 | Components are the core of MJML. A component is an abstraction of a more complex email-responsive HTML layout. It exposes attributes, enabling you to interact with the final component visual aspect.
4 |
5 | MJML comes out of the box with a set of standard components to help you build easily your first templates without having to reinvent the wheel.
6 |
7 | For instance, the `mj-button` component is, on the inside, a complex HTML layout:
8 |
9 | ``` html
10 |
11 |
12 | Hello There!
13 |
14 |
15 |
16 |
27 | ```
28 |
29 | ## mjml
30 |
31 | A MJML document starts with a `` tag, it can contain only `mj-head` and `mj-body` tags. Both have the same purpose of `head` and `body` in a HTML document.
32 |
33 | attribute | unit | description | default value
34 | ----------|------|-------------|---------------
35 | owa | string | if set to "desktop", switch force desktop version for older (self-hosted) version of Outlook.com that doesn't support media queries (cf. [this issue](https://github.com/mjmlio/mjml/issues/2241)) | none
36 | lang | string | used as `` attribute | und
37 | dir | string | used as `` attribute | auto
38 |
39 |
40 | ## mj-head
41 |
42 | mj-head contains head components, related to the document such as style and meta elements (see [head components](#standard-head-components)).
43 |
--------------------------------------------------------------------------------
/doc/components_2.md:
--------------------------------------------------------------------------------
1 | ## mj-include
2 |
3 | The `mjml-core` package allows you to include external `mjml` files
4 | to build your email template.
5 |
6 | ```xml
7 |
8 |
9 |
10 | This is a header
11 |
12 |
13 | ```
14 |
15 | You can wrap your external mjml files inside the default `mjml > mj-body`
16 | tags to make it easier to preview outside the main template.
17 |
18 |
19 | ```xml
20 |
21 |
22 |
23 |
24 |
25 |
26 | ```
27 |
28 |
29 | The `MJML` engine will then replace your included files before starting the rendering process.
30 |
31 | ### Other file types
32 |
33 | You can include external `css` files. They will be inserted the same way as when using a `mj-style`.
34 | You need to specify that you're including a css file using the `type="css"` attribute.
35 | If you want the css to be inlined, you can use the `css-inline="inline"` attribute.
36 |
37 | ```xml
38 |
39 |
40 |
41 | ```
42 |
43 | You can also include external `html` files. They will be inserted the same way as when using a `mj-raw`.
44 | You need to specify that you're including a html file using the `type="html"` attribute.
45 |
46 | ```xml
47 |
48 |
49 | ```
50 |
--------------------------------------------------------------------------------
/doc/config.json:
--------------------------------------------------------------------------------
1 | [
2 | "mjml/doc/guide.md",
3 | "mjml/doc/install.md",
4 | "mjml/doc/getting_started.md",
5 | "mjml/doc/basic.md",
6 | "mjml/doc/components_1.md",
7 | "mjml/packages/mjml-body/README.md",
8 | "mjml/doc/components_2.md",
9 | "mjml/doc/head_components.md",
10 | "mjml/packages/mjml-head-attributes/README.md",
11 | "mjml/packages/mjml-head-breakpoint/README.md",
12 | "mjml/packages/mjml-head-font/README.md",
13 | "mjml/packages/mjml-head-html-attributes/README.md",
14 | "mjml/packages/mjml-head-preview/README.md",
15 | "mjml/packages/mjml-head-style/README.md",
16 | "mjml/packages/mjml-head-title/README.md",
17 | "mjml/doc/body_components.md",
18 | "mjml/packages/mjml-accordion/README.md",
19 | "mjml/packages/mjml-button/README.md",
20 | "mjml/packages/mjml-carousel/README.md",
21 | "mjml/packages/mjml-column/README.md",
22 | "mjml/packages/mjml-divider/README.md",
23 | "mjml/packages/mjml-group/README.md",
24 | "mjml/packages/mjml-hero/README.md",
25 | "mjml/packages/mjml-image/README.md",
26 | "mjml/packages/mjml-navbar/README.md",
27 | "mjml/packages/mjml-raw/README.md",
28 | "mjml/packages/mjml-section/README.md",
29 | "mjml/packages/mjml-social/README.md",
30 | "mjml/packages/mjml-spacer/README.md",
31 | "mjml/packages/mjml-table/README.md",
32 | "mjml/packages/mjml-text/README.md",
33 | "mjml/packages/mjml-wrapper/README.md",
34 | "mjml/doc/ending-tags.md",
35 | "mjml/doc/community-components.md",
36 | "mjml/doc/ports.md",
37 | "mjml/doc/mjml-chart.md",
38 | "mjml/doc/mjml-chartjs.md",
39 | "mjml/doc/mjml-qr-code.md",
40 | "mjml/doc/mjml-mso-button.md",
41 | "mjml/packages/mjml-validator/README.md",
42 | "mjml/doc/create.md",
43 | "mjml/doc/using_mjml_in_json.md",
44 | "mjml/doc/tooling.md",
45 | "mjml/doc/passport.md"
46 | ]
47 |
--------------------------------------------------------------------------------
/doc/create.md:
--------------------------------------------------------------------------------
1 | # Creating a Component
2 |
3 | One of the great advantages of `MJML` is that it's component-based. Components abstract complex patterns and can easily be reused. In addition to the standard library of components, it is also possible to create your own components!
4 |
5 | We have published a step-by-step guide [here](https://medium.com/mjml-making-responsive-email-easy/tutorial-creating-your-own-component-with-mjml-4-1c0e84e97b36) that explains how to create a custom components with `MJML 4`. It will introduce to you the [boilerplate repo](https://github.com/mjmlio/mjml-component-boilerplate) hosted on Github, which provides a fast way of getting started developing your own components.
6 |
--------------------------------------------------------------------------------
/doc/ending-tags.md:
--------------------------------------------------------------------------------
1 | ## Ending tags
2 |
3 | Some of the mjml components are "ending tags". These are mostly the components that will contain text contents, like `mj-text` or `mj-buttons`.
4 | These components can contain not only text, but also any HTML content, which will be completely unprocessed and left as it is. This means you cannot use other MJML components inside them, but you can use any HTML tag, like ` ` or ``.
5 |
6 | This has a little downside : The content is not modified at all, this means that the text won't be escaped, so if you use characters that are used to define html tags in your text, like `<` or `>`, you should use the encoded characters `<` and `<`. If you don't, sometimes the browser can be clever enough to understand that you're not really trying to open/close an html tag, and display the unescaped character as normal text, but this may cause problems in some cases.
7 | For instance, this will likely cause problems if you use the `minify` option, `mj-html-attributes` or an inline `mj-style`, because these require the html to be re-parsed internally. If you're just using the `minify` option, and really need to use the `< >` characters, i.e. for templating language, you can also avoid this problem by wrapping the troublesome content between two `` tags.
8 |
9 | Here is the list of all ending tags :
10 | - mj-accordion-text
11 | - mj-accordion-title
12 | - mj-button
13 | - mj-navbar-link
14 | - mj-raw
15 | - mj-social-element
16 | - mj-text
17 | - mj-table
18 |
--------------------------------------------------------------------------------
/doc/getting_started.md:
--------------------------------------------------------------------------------
1 |
2 | # Getting Started
3 | This is a responsive email
4 |
5 |
6 |
7 |
8 |
9 | Like a regular HTML template, we can split this one into different parts to fit in a grid.
10 |
11 | The body of your email, represented by the `mj-body` tag contains the entire content of your document:
12 |
13 |
14 |
15 |
16 |
17 | From here, you can first define your sections:
18 |
19 |
20 |
21 |
22 |
23 | Inside any section, there should be columns (even if you need only one column). Columns are what makes MJML responsive.
24 |
25 |
26 |
27 |
28 |
29 | Below, you'll find some basic rules of MJML to keep in mind for later. We'll remind them when useful but better start learning them early on.
30 |
31 | ## Column sizing
32 |
33 | ### Auto sizing
34 |
35 | The default behavior of the MJML translation engine is to divide the section space (600px by default, but it can be changed with the `width` attribute on `mj-body`) in as many columns as you declare.
36 |
37 |
38 | Any mj-element included in a column will have a width equivalent to 100% of this column's width.
39 |
40 |
41 | Let's take the following layout to illustrate this:
42 |
43 | ```html
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | ```
57 |
58 | Since the first section defines only 2 columns, the engine will translate that in a layout where each column takes 50% of the total space (300px each). If we add a third one, it goes down to 33%, and with a fourth one to 25%.
59 |
60 |
61 |
62 |
63 |
64 | ### Manual sizing
65 | You can also manually set the size of your columns, in pixels or percentage, by using the `width` attribute on `mj-column`.
66 |
67 | Let's take the following layout to illustrate this:
68 |
69 | ```html
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | ```
83 |
--------------------------------------------------------------------------------
/doc/guide.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API Reference
3 |
4 | language_tabs:
5 | - html : MJML
6 |
7 | toc_footers:
8 | - Fork me on Github
9 | - Submit an Issue
10 |
11 | search: true
12 | ---
13 |
14 |
15 | # MJML Guides
16 |
17 | MJML is a markup language designed to reduce the pain of coding a responsive email. Its semantic syntax makes it easy and straightforward and its rich standard components library speeds up your development time and lightens your email codebase. MJML’s open-source engine generates high quality responsive HTML compliant with best practices.
18 |
19 | ## Overview
20 |
21 | MJML rolls up all of what Mailjet has learned about HTML email design over the past few years and abstracts the whole layer of complexity related to responsive email design.
22 |
23 | Get your speed and productivity boosted with MJML’s semantic syntax. Say goodbye to endless HTML table nesting or email client specific CSS. Building a responsive email is super easy with tags such as `` and ``.
24 |
25 | MJML has been designed with responsiveness in mind. The abstraction it offers guarantee you to always be up-to-date with the industry practices and responsive. Email clients update their specs and requirements regularly, but we geek about that stuff - we’ll stay on top of it so you can spend less time reading up on latest email client updates and more time designing beautiful email.
26 |
27 | ``` html
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Hello World
36 |
37 |
38 |
39 |
40 |
41 | ```
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/doc/head_components.md:
--------------------------------------------------------------------------------
1 | # Standard Head components
2 |
3 | Head components ease your development process, enabling you to import fonts, define default styles or create classes for MJML components among others.
4 |
--------------------------------------------------------------------------------
/doc/mjml-chart.md:
--------------------------------------------------------------------------------
1 | ## mj-chart
2 |
3 | Thanks to [image-charts](https://image-charts.com/) for their contribution with this component. It's available on [Github](https://github.com/image-charts/mjml-charts) and [NPM](https://www.npmjs.com/package/mjml-chart).
4 |
5 |
6 |
7 |
8 |
9 | Displays charts as images in your email.
10 |
--------------------------------------------------------------------------------
/doc/mjml-chartjs.md:
--------------------------------------------------------------------------------
1 | ## mj-chartjs
2 |
3 | This component displays [Chart.js](https://www.chartjs.org/) charts as images in your email. Chart.js is an open-source Javascript charting library.
4 |
5 | mj-chartjs is available on [Github](https://github.com/typpo/mjml-chartjs) and [NPM](https://www.npmjs.com/package/mjml-chartjs). By default, it uses the open-source [QuickChart](https://quickchart.io/) API for chart rendering.
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/doc/mjml-mso-button.md:
--------------------------------------------------------------------------------
1 | ## mjml-msobutton
2 |
3 | Button that also use the [VML](https://docs.microsoft.com/en-us/windows/win32/vml/shape-element--vml) solution for radius.
4 |
5 | This component can be usefull if you want to try an outlook proof adventure.
6 | The msobutton has exactly the same behaviours of the classic button but add three more attributes.
7 |
8 | attribute | unit | description | default value
9 | ----------------------------|-------------|--------------------------------------------------|---------------------
10 | mso-proof | boolean | Active the bulletproof mode | false
11 | mso-width | px | The width of the VML solution | 200px
12 | mso-height | px | The height of the VML solution | 40px
13 |
14 | More important, these 3 new attributes allow mjml to generate a bulletproof button with radius and stroke with the same method that you can see [here](https://buttons.cm/), including the alignment.
15 |
16 | It's available on [Github](https://github.com/adrien-zinger/mjml-mso-button) and [NPM](https://www.npmjs.com/package/mjml-msobutton).
17 |
18 | **Usage**
19 |
20 | Use it like an mj-button:
21 | ```html
22 | Click !
23 | ```
24 |
25 | **Problems that you should know**
26 |
27 | This outlook solution isn't really bulletproof.
28 | 1. This cannot be used with an image in background
29 | 2. It creates a duplication of code in the HTML
30 | 3. The width and the height cannot be used with the *auto* value
31 |
32 | > Sample project on github [here](https://github.com/adrien-zinger/mjml-msobutton-sample)
33 |
--------------------------------------------------------------------------------
/doc/mjml-qr-code.md:
--------------------------------------------------------------------------------
1 | ## mj-qr-code
2 |
3 | This component displays QR codes in your email. It's available on [Github](https://github.com/typpo/mjml-qr-code) and [NPM](https://www.npmjs.com/package/mjml-qr-code).
4 |
5 | By default, mj-qr-code uses the open-source QuickChart [QR code API](https://quickchart.io/).
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/doc/passport.md:
--------------------------------------------------------------------------------
1 | # Drag-and-drop interface
2 |
3 | Would you rather use a friendly drag-and-drop interface rather than coding? [Try Passport](https://www.mailjet.com/demo/), the email builder based on MJML!
4 |
--------------------------------------------------------------------------------
/doc/ports.md:
--------------------------------------------------------------------------------
1 | # Ports and Language Bindings
2 |
3 | MJML is available for other platforms. The community has created ports to other platforms and wrappers for the official Node implementation. These contributions are not officially supported by the MJML teams.
4 |
5 | ## Rust: MRML
6 |
7 | This project is a reimplementation of the nice MJML markup language in Rust.
8 |
9 | https://github.com/jdrouet/mrml
10 |
11 | ### Missing implementations / components:
12 |
13 | - `mj-style[inline]`: not yet implemented. It requires parsing the generated html to apply the inline styles afterward (that's how it's done in mjml) which would kill the performances. Applying it at render time would improve the performance but it would still require to parse the CSS.
14 |
15 | ## .NET: MJML.NET
16 |
17 | A blazingly-fast unofficial port of MJML 4 to .NET 6.
18 |
19 | https://github.com/SebastianStehle/mjml-net
20 |
21 | ## Elixir: MJML (Rust NIFs for Elixir)
22 |
23 | Native Implemented Function (NIF) bindings for the MJML Rust implementation (mrml).
24 |
25 | https://github.com/adoptoposs/mjml_nif
26 |
27 | ## Ruby: MRML Ruby
28 |
29 | Ruby wrapper for MRML, the MJML markup language implementation in Rust.
30 |
31 | https://github.com/hardpixel/mrml-ruby
32 |
33 | ## React: mjml-react
34 |
35 | React components for MJML components.
36 |
37 | https://github.com/faire/mjml-react#readme
38 |
39 | ## Python: mjml-python
40 |
41 | Python wrapper for MRML, the MJML markup language implementation in Rust.
42 |
43 | https://github.com/mgd020/mjml-python
44 |
45 | ## Python: mjml-python
46 |
47 | Python implementation for MJML.
48 |
49 | https://github.com/FelixSchwarz/mjml-python
50 |
51 | ## Python / Django: django-mjml
52 |
53 | The simplest way to use MJML in Django templates.
54 |
55 | https://github.com/liminspace/django-mjml
56 |
57 | ## PHP / Laravel: Laravel MJML
58 |
59 | Build responsive e-mails easily using MJML and Laravel Mailables.
60 |
61 | - https://github.com/EvanSchleret/lara-mjml
62 | - https://github.com/asahasrabuddhe/laravel-mjml (not maintained)
63 |
--------------------------------------------------------------------------------
/doc/tooling.md:
--------------------------------------------------------------------------------
1 | # Tooling
2 |
3 | In order to provide you with the best experience with MJML and help you use it more efficiently, we've developed some tools to integrate it seamlessly in your development workflow:
4 |
5 | ## Visual Studio Code
6 |
7 | [Visual Studio Code](https://code.visualstudio.com/) is a free code editor made by [Microsoft](https://www.microsoft.com/). We recommend this package as it is among the most feature-rich MJML plugins for code editors with live previews, syntax highlighting and linting as well as export features including HTML and screenshots. It is available [on Github](https://github.com/mjmlio/vscode-mjml) and through the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=mjmlio.vscode-mjml).
8 |
9 | ## Parcel
10 |
11 | [Parcel](https://parcel.io) is the code editor built for email. This feature packed tool includes syntax highlighting, Emmet, inline documentation, autocomplete, live preview, screenshots, and full MJML, CSS, and HTML validation. Use Focus Mode to keep the preview aligned with the code you're working on, or Inspect Element to easily find the code that produces specific elements in the preview. Export MJML to HTML with a click.
12 |
13 | ## Atom language plugin
14 |
15 | [Atom](https://atom.io) is a powerful text editor originally released by [Github](https://github.com). This package provides autocompletion and syntax highlighting for MJML. It is available [on Github](https://github.com/mjmlio/language-mjml) and through the [Atom Package Manager (APM)](https://atom.io/packages/language-mjml).
16 |
17 | ## Atom linter
18 |
19 | In addition to the language plugin, a linter is available to highlight errors in MJML. The linter is available [on Github](https://github.com/mjmlio/atom-linter-mjml) and through the [Atom Package Manager (APM)](https://atom.io/packages/linter-mjml).
20 |
21 | ## Sublime Text
22 |
23 | [Sublime Text](https://www.sublimetext.com/) is a powerful text editor. We're providing you with a package to color MJML tags. It is available [on Github](https://github.com/mjmlio/mjml-syntax) and through the [Sublime Package Control](https://packagecontrol.io/packages/MJML-syntax).
24 |
25 | ## IntelliJ IDEA Plugin - MJML Support
26 |
27 | [IntelliJ IDEA](https://www.jetbrains.com/idea/) is an IDE developed by JetBrains. The plugin provides you with a (near) realtime preview, auto complete, inline documentation and code analysis. Its available on the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/16418-mjml-support).
28 |
29 | ## Gradle Plugin - MJML Compilation
30 | [Gradle](https://gradle.org/) is a build tool for a various set of languages and environments, mainly used for java/kotlin. The plugin provides an easy way to embed your mjml templates to your java/kotlin application in its resources in precompiled form (html).
31 | It's available through the gradle plugin system [io.freefair.mjml.java](https://plugins.gradle.org/plugin/io.freefair.mjml.java) and documentation is available here [FreeFair User Guide](https://docs.freefair.io/gradle-plugins/current/reference/)
32 |
33 | ## Gulp
34 |
35 | Gulp is a tool designed to help you automate and enhance your workflow. Our plugin enables you to plug the MJML translation engine into your workflow, helping you to streamline your development workflow. It is available here on [Github](https://github.com/mjmlio/gulp-mjml)
36 |
37 | ## Neos CMS
38 |
39 | [Neos CMS](https://www.neos.io/) is a content management system that combines structured content with application. This package adds the helper for compiling `MJML` markup as well as some prototypes which allow to use TailwindCSS like classes in your `MJML` markup. It is available on [packagist](https://packagist.org/packages/garagist/mjml)
40 |
41 | ## Easy-email
42 |
43 | [Easy-email](https://github.com/zalify/easy-email) is a Drag-and-Drop Email Editor based on MJML. Transform structured JSON data into major email clients compatible HTML. Written in Typescript and supported both in browser and Node.js.
44 |
45 | ## Contribute to the MJML ecosystem
46 |
47 | The MJML ecosystem is still young and we're also counting on your help to help us make it grow and provide its community with even more awesome tools, always aiming to making development with MJML an efficient and fun process!
48 |
49 | Getting involved is really easy. If you want to contribute, feel free to [open an issue](https://github.com/mjmlio/mjml/issues) or [submit a pull-request](https://github.com/mjmlio/mjml/pulls)!
50 |
--------------------------------------------------------------------------------
/doc/using_mjml_in_json.md:
--------------------------------------------------------------------------------
1 | # Using MJML in JSON
2 |
3 | MJML can not only be used as a markup, but also as a JSON object, which can be very useful for
4 | programmatic manipulation or with the MJML API.
5 |
6 | With the JSON format, a MJML component is defined as an `object` with the following properties:
7 |
8 | * a `tagName` as a `string`
9 | * a list of attributes as an `object`
10 | * either a `content` as a `string` or a list of `children` tags as an `array`.
11 |
12 | Exactly like using MJML as a markup, the JSON definition can be passed as an object to the `mjml2html` function.
13 | Here is working example:
14 |
15 | ```javascript
16 | var mjml2html = require('mjml')
17 |
18 | console.log(mjml2html({
19 | tagName: 'mjml',
20 | attributes: {},
21 | children: [{
22 | tagName: 'mj-body',
23 | attributes: {},
24 | children: [{
25 | tagName: 'mj-section',
26 | attributes: {},
27 | children: [{
28 | tagName: 'mj-column',
29 | attributes: {},
30 | children: [{
31 | tagName: 'mj-image',
32 | attributes: {
33 | 'width': '100px',
34 | 'src': '/assets/img/logo-small.png'
35 | }
36 | },
37 | {
38 | tagName: 'mj-divider',
39 | attributes: {
40 | 'border-color' : '#F46E43'
41 | }
42 | },
43 | {
44 | tagName: 'mj-text',
45 | attributes: {
46 | 'font-size': '20px',
47 | 'color': '#F45E43',
48 | 'font-family': 'Helvetica'
49 | },
50 | content: 'Hello World'
51 | }]
52 | }]
53 | }]
54 | }]
55 | }))
56 | ```
57 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | "packages/*"
4 | ],
5 | "command": {
6 | "publish": {
7 | "exact": true
8 | }
9 | },
10 | "npmClient": "yarn",
11 | "useWorkspaces": true,
12 | "version": "4.15.3"
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-master",
3 | "private": true,
4 | "scripts": {
5 | "build:watch": "lerna run build --parallel -- -- -w",
6 | "build": "lerna run build --parallel --ignore mjml-browser",
7 | "build-browser": "cd packages/mjml-browser && yarn build",
8 | "lint": "eslint .",
9 | "lint:fix": "eslint . --fix",
10 | "postinstall": "lerna bootstrap",
11 | "prettier": "prettier --write \"packages/**/{src,bin}/**/*.?(js|json)\""
12 | },
13 | "workspaces": [
14 | "packages/*"
15 | ],
16 | "devDependencies": {
17 | "@babel/core": "^7.8.7",
18 | "@babel/plugin-proposal-class-properties": "^7.8.3",
19 | "@babel/plugin-transform-runtime": "^7.8.3",
20 | "@babel/preset-env": "^7.8.7",
21 | "@babel/register": "^7.8.6",
22 | "babel-eslint": "^10.1.0",
23 | "babel-plugin-add-module-exports": "^1.0.2",
24 | "babel-plugin-lodash": "^3.3.4",
25 | "eslint": "^6.8.0",
26 | "eslint-config-airbnb-base": "^14.1.0",
27 | "eslint-config-prettier": "^9.1.0",
28 | "eslint-plugin-import": "^2.21.1",
29 | "lerna": "^3.22.1",
30 | "open": "^7.3.0",
31 | "prettier": "^3.2.4",
32 | "rimraf": "^3.0.2"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/mjml-accordion/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-accordion",
3 | "description": "mjml-accordion",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-accordion"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-accordion/src/Accordion.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 |
3 | export default class MjAccordion extends BodyComponent {
4 | static componentName = 'mj-accordion'
5 |
6 | static allowedAttributes = {
7 | 'container-background-color': 'color',
8 | border: 'string',
9 | 'font-family': 'string',
10 | 'icon-align': 'enum(top,middle,bottom)',
11 | 'icon-width': 'unit(px,%)',
12 | 'icon-height': 'unit(px,%)',
13 | 'icon-wrapped-url': 'string',
14 | 'icon-wrapped-alt': 'string',
15 | 'icon-unwrapped-url': 'string',
16 | 'icon-unwrapped-alt': 'string',
17 | 'icon-position': 'enum(left,right)',
18 | 'padding-bottom': 'unit(px,%)',
19 | 'padding-left': 'unit(px,%)',
20 | 'padding-right': 'unit(px,%)',
21 | 'padding-top': 'unit(px,%)',
22 | padding: 'unit(px,%){1,4}',
23 | }
24 |
25 | static defaultAttributes = {
26 | border: '2px solid black',
27 | 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif',
28 | 'icon-align': 'middle',
29 | 'icon-wrapped-url': 'https://i.imgur.com/bIXv1bk.png',
30 | 'icon-wrapped-alt': '+',
31 | 'icon-unwrapped-url': 'https://i.imgur.com/w4uTygT.png',
32 | 'icon-unwrapped-alt': '-',
33 | 'icon-position': 'right',
34 | 'icon-height': '32px',
35 | 'icon-width': '32px',
36 | padding: '10px 25px',
37 | }
38 |
39 | headStyle = () =>
40 | `
41 | noinput.mj-accordion-checkbox { display:block!important; }
42 |
43 | @media yahoo, only screen and (min-width:0) {
44 | .mj-accordion-element { display:block; }
45 | input.mj-accordion-checkbox, .mj-accordion-less { display:none!important; }
46 | input.mj-accordion-checkbox + * .mj-accordion-title { cursor:pointer; touch-action:manipulation; -webkit-user-select:none; -moz-user-select:none; user-select:none; }
47 | input.mj-accordion-checkbox + * .mj-accordion-content { overflow:hidden; display:none; }
48 | input.mj-accordion-checkbox + * .mj-accordion-more { display:block!important; }
49 | input.mj-accordion-checkbox:checked + * .mj-accordion-content { display:block; }
50 | input.mj-accordion-checkbox:checked + * .mj-accordion-more { display:none!important; }
51 | input.mj-accordion-checkbox:checked + * .mj-accordion-less { display:block!important; }
52 | }
53 |
54 | .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-title { cursor: auto; touch-action: auto; -webkit-user-select: auto; -moz-user-select: auto; user-select: auto; }
55 | .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-content { overflow: hidden; display: block; }
56 | .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-ico { display: none; }
57 |
58 | @goodbye { @gmail }
59 | `
60 |
61 | getStyles() {
62 | return {
63 | table: {
64 | width: '100%',
65 | 'border-collapse': 'collapse',
66 | border: this.getAttribute('border'),
67 | 'border-bottom': 'none',
68 | 'font-family': this.getAttribute('font-family'),
69 | },
70 | }
71 | }
72 |
73 | render() {
74 | const childrenAttr = [
75 | 'border',
76 | 'icon-align',
77 | 'icon-width',
78 | 'icon-height',
79 | 'icon-position',
80 | 'icon-wrapped-url',
81 | 'icon-wrapped-alt',
82 | 'icon-unwrapped-url',
83 | 'icon-unwrapped-alt',
84 | ].reduce(
85 | (res, val) => ({
86 | ...res,
87 | [val]: this.getAttribute(val),
88 | }),
89 | {},
90 | )
91 |
92 | return `
93 |
101 |
102 | ${this.renderChildren(this.props.children, {
103 | attributes: childrenAttr,
104 | })}
105 |
106 |
107 | `
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/packages/mjml-accordion/src/AccordionElement.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 | import { find } from 'lodash'
3 | import conditionalTag from 'mjml-core/lib/helpers/conditionalTag'
4 | import AccordionText from './AccordionText'
5 | import AccordionTitle from './AccordionTitle'
6 |
7 | export default class MjAccordionElement extends BodyComponent {
8 | static componentName = 'mj-accordion-element'
9 |
10 | static allowedAttributes = {
11 | 'background-color': 'color',
12 | border: 'string',
13 | 'font-family': 'string',
14 | 'icon-align': 'enum(top,middle,bottom)',
15 | 'icon-width': 'unit(px,%)',
16 | 'icon-height': 'unit(px,%)',
17 | 'icon-wrapped-url': 'string',
18 | 'icon-wrapped-alt': 'string',
19 | 'icon-unwrapped-url': 'string',
20 | 'icon-unwrapped-alt': 'string',
21 | 'icon-position': 'enum(left,right)',
22 | }
23 |
24 | static defaultAttributes = {
25 | title: {
26 | img: {
27 | width: '32px',
28 | height: '32px',
29 | },
30 | },
31 | }
32 |
33 | getStyles() {
34 | return {
35 | td: {
36 | padding: '0px',
37 | 'background-color': this.getAttribute('background-color'),
38 | },
39 | label: {
40 | 'font-size': '13px',
41 | 'font-family': this.getAttribute('font-family'),
42 | },
43 | input: {
44 | display: 'none',
45 | },
46 | }
47 | }
48 |
49 | handleMissingChildren() {
50 | const { children } = this.props
51 | const childrenAttr = [
52 | 'border',
53 | 'icon-align',
54 | 'icon-width',
55 | 'icon-height',
56 | 'icon-position',
57 | 'icon-wrapped-url',
58 | 'icon-wrapped-alt',
59 | 'icon-unwrapped-url',
60 | 'icon-unwrapped-alt',
61 | ].reduce(
62 | (res, val) => ({
63 | ...res,
64 | [val]: this.getAttribute(val),
65 | }),
66 | {},
67 | )
68 |
69 | const result = []
70 |
71 | if (!find(children, { tagName: 'mj-accordion-title' })) {
72 | result.push(
73 | new AccordionTitle({
74 | attributes: childrenAttr,
75 | context: this.getChildContext(),
76 | }).render(),
77 | )
78 | }
79 |
80 | result.push(this.renderChildren(children, { attributes: childrenAttr }))
81 |
82 | if (!find(children, { tagName: 'mj-accordion-text' })) {
83 | result.push(
84 | new AccordionText({
85 | attributes: childrenAttr,
86 | context: this.getChildContext(),
87 | }).render(),
88 | )
89 | }
90 |
91 | return result.join('\n')
92 | }
93 |
94 | render() {
95 | return `
96 |
101 |
102 |
108 | ${conditionalTag(
109 | `
110 |
117 | `,
118 | true,
119 | )}
120 |
121 | ${this.handleMissingChildren()}
122 |
123 |
124 |
125 |
126 | `
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/packages/mjml-accordion/src/AccordionText.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 |
3 | export default class MjAccordionText extends BodyComponent {
4 | static componentName = 'mj-accordion-text'
5 |
6 | static endingTag = true
7 |
8 | static allowedAttributes = {
9 | 'background-color': 'color',
10 | 'font-size': 'unit(px)',
11 | 'font-family': 'string',
12 | 'font-weight': 'string',
13 | 'letter-spacing': 'unitWithNegative(px,em)',
14 | 'line-height': 'unit(px,%,)',
15 | color: 'color',
16 | 'padding-bottom': 'unit(px,%)',
17 | 'padding-left': 'unit(px,%)',
18 | 'padding-right': 'unit(px,%)',
19 | 'padding-top': 'unit(px,%)',
20 | padding: 'unit(px,%){1,4}',
21 | }
22 |
23 | static defaultAttributes = {
24 | 'font-size': '13px',
25 | 'line-height': '1',
26 | padding: '16px',
27 | }
28 |
29 | getStyles() {
30 | return {
31 | td: {
32 | background: this.getAttribute('background-color'),
33 | 'font-size': this.getAttribute('font-size'),
34 | 'font-family': this.getAttribute('font-family'),
35 | 'font-weight': this.getAttribute('font-weight'),
36 | 'letter-spacing': this.getAttribute('letter-spacing'),
37 | 'line-height': this.getAttribute('line-height'),
38 | color: this.getAttribute('color'),
39 | 'padding-bottom': this.getAttribute('padding-bottom'),
40 | 'padding-left': this.getAttribute('padding-left'),
41 | 'padding-right': this.getAttribute('padding-right'),
42 | 'padding-top': this.getAttribute('padding-top'),
43 | padding: this.getAttribute('padding'),
44 | },
45 | table: {
46 | width: '100%',
47 | 'border-bottom': this.getAttribute('border'),
48 | },
49 | }
50 | }
51 |
52 | renderContent() {
53 | return `
54 |
60 | ${this.getContent()}
61 |
62 | `
63 | }
64 |
65 | render() {
66 | return `
67 |
72 |
79 |
80 |
81 | ${this.renderContent()}
82 |
83 |
84 |
85 |
86 | `
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/packages/mjml-accordion/src/AccordionTitle.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 | import conditionalTag from 'mjml-core/lib/helpers/conditionalTag'
3 |
4 | export default class MjAccordionTitle extends BodyComponent {
5 | static componentName = 'mj-accordion-title'
6 |
7 | static endingTag = true
8 |
9 | static allowedAttributes = {
10 | 'background-color': 'color',
11 | color: 'color',
12 | 'font-size': 'unit(px)',
13 | 'font-family': 'string',
14 | 'padding-bottom': 'unit(px,%)',
15 | 'padding-left': 'unit(px,%)',
16 | 'padding-right': 'unit(px,%)',
17 | 'padding-top': 'unit(px,%)',
18 | padding: 'unit(px,%){1,4}',
19 | }
20 |
21 | static defaultAttributes = {
22 | 'font-size': '13px',
23 | padding: '16px',
24 | }
25 |
26 | getStyles() {
27 | return {
28 | td: {
29 | width: '100%',
30 | 'background-color': this.getAttribute('background-color'),
31 | color: this.getAttribute('color'),
32 | 'font-size': this.getAttribute('font-size'),
33 | 'font-family': this.getAttribute('font-family'),
34 | 'padding-bottom': this.getAttribute('padding-bottom'),
35 | 'padding-left': this.getAttribute('padding-left'),
36 | 'padding-right': this.getAttribute('padding-right'),
37 | 'padding-top': this.getAttribute('padding-top'),
38 | padding: this.getAttribute('padding'),
39 | },
40 | table: {
41 | width: '100%',
42 | 'border-bottom': this.getAttribute('border'),
43 | },
44 | td2: {
45 | padding: '16px',
46 | background: this.getAttribute('background-color'),
47 | 'vertical-align': this.getAttribute('icon-align'),
48 | },
49 | img: {
50 | display: 'none',
51 | width: this.getAttribute('icon-width'),
52 | height: this.getAttribute('icon-height'),
53 | },
54 | }
55 | }
56 |
57 | renderTitle() {
58 | return `
59 |
65 | ${this.getContent()}
66 |
67 | `
68 | }
69 |
70 | renderIcons() {
71 | return conditionalTag(
72 | `
73 |
79 |
87 |
95 |
96 | `,
97 | true,
98 | )
99 | }
100 |
101 | render() {
102 | const contentElements = [this.renderTitle(), this.renderIcons()]
103 | const content = (
104 | this.getAttribute('icon-position') === 'right'
105 | ? contentElements
106 | : contentElements.reverse()
107 | ).join('\n')
108 |
109 | return `
110 |
111 |
118 |
119 |
120 | ${content}
121 |
122 |
123 |
124 |
125 | `
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/packages/mjml-accordion/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Accordion } from './Accordion'
2 | export { default as AccordionElement } from './AccordionElement'
3 | export { default as AccordionText } from './AccordionText'
4 | export { default as AccordionTitle } from './AccordionTitle'
5 |
--------------------------------------------------------------------------------
/packages/mjml-body/README.md:
--------------------------------------------------------------------------------
1 | ## mj-body
2 |
3 | This is the starting point of your email.
4 |
5 | ```xml
6 |
7 |
8 |
9 |
10 |
11 | ```
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | mj-body replaces the couple mj-body and mj-container of MJML v3.
21 |
22 |
23 | attribute | unit | description | default value
24 | ---------------------|---------------|--------------------------------|---------------
25 | background-color | color formats | the general background color | n/a
26 | css-class | string | class name, added to the root HTML element created | n/a
27 | width | px | email's width | 600px
28 |
29 |
--------------------------------------------------------------------------------
/packages/mjml-body/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-body",
3 | "description": "mjml-body",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-body"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-body/src/index.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 |
3 | export default class MjBody extends BodyComponent {
4 | static componentName = 'mj-body'
5 |
6 | static allowedAttributes = {
7 | width: 'unit(px)',
8 | 'background-color': 'color',
9 | }
10 |
11 | static defaultAttributes = {
12 | width: '600px',
13 | }
14 |
15 | getChildContext() {
16 | return {
17 | ...this.context,
18 | containerWidth: this.getAttribute('width'),
19 | }
20 | }
21 |
22 | getStyles() {
23 | return {
24 | div: {
25 | 'background-color': this.getAttribute('background-color'),
26 | },
27 | }
28 | }
29 |
30 | render() {
31 | const {
32 | setBackgroundColor,
33 | globalData: { lang, dir },
34 | } = this.context
35 | setBackgroundColor(this.getAttribute('background-color'))
36 |
37 | return `
38 |
46 | ${this.renderChildren()}
47 |
48 | `
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/mjml-browser/README.md:
--------------------------------------------------------------------------------
1 | ## MJML Browser build
2 |
3 | This package allows MJML to be used client-side.
4 |
5 | ### Usage
6 |
7 | It can be used as the regular mjml package :
8 |
9 | ```javascript
10 | var mjml2html = require('mjml-browser')
11 |
12 | var result = mjml2html(mjml, options)
13 | ```
14 |
15 | ### Unavailable features
16 |
17 | - `mj-include` tags are unavailable and will be ignored.
18 | - features involving the `.mjmlconfig` file are unavailable, which means no custom components.
19 |
--------------------------------------------------------------------------------
/packages/mjml-browser/browser-mocks/fs.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | readFileSync: () => {
3 | console.warn('fs should not be used in browser build') // eslint-disable-line no-console
4 | return null
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/packages/mjml-browser/browser-mocks/path.js:
--------------------------------------------------------------------------------
1 | const mockFn = () => {
2 | console.warn('fs should not be used in browser build') // eslint-disable-line no-console
3 | return null
4 | }
5 |
6 | module.exports = {
7 | parse: mockFn,
8 | resolve: mockFn,
9 | join: mockFn,
10 | dirname: mockFn,
11 | isAbsolute: mockFn,
12 | }
13 |
--------------------------------------------------------------------------------
/packages/mjml-browser/browser-mocks/uglify-js.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
2 |
--------------------------------------------------------------------------------
/packages/mjml-browser/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-browser",
3 | "description": "MJML: the only framework that makes responsive-email easy",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-browser"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "webpack"
22 | },
23 | "devDependencies": {
24 | "@babel/cli": "^7.8.4",
25 | "@babel/core": "^7.8.4",
26 | "@babel/plugin-proposal-class-properties": "^7.8.3",
27 | "@babel/plugin-proposal-decorators": "^7.8.3",
28 | "@babel/plugin-proposal-export-default-from": "^7.8.3",
29 | "@babel/plugin-proposal-function-bind": "^7.8.3",
30 | "@babel/preset-env": "^7.8.4",
31 | "babel-loader": "^8.0.6",
32 | "rimraf": "^3.0.2",
33 | "uglifyjs-webpack-plugin": "^2.1.3",
34 | "webpack": "^4.36.1",
35 | "webpack-cli": "^3.3.6"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/mjml-browser/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
3 |
4 | module.exports = {
5 | mode: 'production',
6 | entry: {
7 | "mjml": ['../mjml/lib/index'],
8 | },
9 | optimization: {
10 | minimizer: [
11 | new UglifyJsPlugin({
12 | uglifyOptions: {
13 | ecma: 5,
14 | keep_classnames: true,
15 | keep_fnames: true,
16 | compress: {
17 | passes: 2,
18 | keep_fargs: false,
19 | },
20 | output: {
21 | beautify: false,
22 | },
23 | mangle: true,
24 | },
25 | }),
26 | ],
27 | },
28 | output: {
29 | library: 'mjml',
30 | filename: 'index.js',
31 | path: path.resolve(__dirname, './lib'),
32 | libraryTarget: 'umd',
33 | umdNamedDefine: true,
34 | },
35 | resolve: {
36 | alias: {
37 | 'path': path.resolve(__dirname, 'browser-mocks/path'),
38 | 'fs': path.resolve(__dirname, 'browser-mocks/fs'),
39 | 'uglify-js': path.resolve(__dirname, 'browser-mocks/uglify-js'),
40 | },
41 | },
42 | module: {
43 | rules: [
44 | {
45 | test: /\.js$/,
46 | exclude: path.join(__dirname, 'node_modules'),
47 | use: [
48 | {
49 | loader: 'babel-loader',
50 | options: {
51 | presets: [
52 | '@babel/preset-env',
53 | ],
54 | plugins: [
55 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
56 | ["@babel/plugin-proposal-class-properties", { "loose" : true }],
57 | "@babel/plugin-proposal-function-bind",
58 | "@babel/plugin-proposal-export-default-from",
59 | ],
60 | babelrc: false,
61 | },
62 | },
63 | ],
64 | },
65 | ],
66 | },
67 | }
68 |
--------------------------------------------------------------------------------
/packages/mjml-button/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-button",
3 | "description": "mjml-button",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-button"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-carousel/README.md:
--------------------------------------------------------------------------------
1 | ## mj-carousel
2 |
3 |
4 |
5 |
6 |
7 | `mj-carousel` displays a gallery of images or "carousel". Readers can interact by hovering and clicking on thumbnails depending on the email client they use.
8 |
9 | This component enables you to set the styles of the carousel elements.
10 |
11 | ```xml
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | `mj-carousel-image` is an "ending tag", which means it can contain HTML code which will be left as it is, so it can contain HTML tags with attributes, but it cannot contain other MJML components. More information about ending tags in this section .
35 |
36 |
37 |
38 | attribute | unit | description | default value
39 | ----------|------|-------------|---------------
40 | align | string | horizontal alignment | center
41 | container-background-color | string | column background color | none
42 | border-radius | px | border radius | n/a
43 | css-class | string | class name, added to the root HTML element created | n/a
44 | icon-width | px | width of the icons on left and right of the main image | 44px
45 | left-icon | url | icon on the left of the main image | https://i.imgur.com/xTh3hln.png
46 | right-icon | url | icon on the right of the main image | https://i.imgur.com/os7o9kz.png
47 | tb-border | css border format | border of the thumbnails | none
48 | tb-border-radius | px | border-radius of the thumbnails | none
49 | tb-hover-border-color | string | css border color of the hovered thumbnail | none
50 | tb-selected-border-color | string | css border color of the selected thumbnail | none
51 | tb-width | px | thumbnail width | null
52 | thumbnails | String | display or not the thumbnails (visible | hidden)
53 |
54 | ### mj-carousel-image
55 |
56 | This component enables you to add and style the images in the carousel.
57 |
58 | attribute | unit | description | default value
59 | ----------|------|-------------|---------------
60 | alt | string | image description | ''
61 | css-class | string | class name, added to the root HTML element created | n/a
62 | href | url | link to redirect to on click | n/a
63 | rel | string | specify the rel attribute | n/a
64 | src | url | image source | n/a
65 | target | string | link target on click | \_blank
66 | thumbnails-src | url | image source to have a thumbnail different than the image it's linked to | null
67 | title | string | tooltip & accessibility | n/a
68 |
--------------------------------------------------------------------------------
/packages/mjml-carousel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-carousel",
3 | "description": "mjml-carousel",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-carousel"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-carousel/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Carousel } from './Carousel'
2 | export { default as CarouselImage } from './CarouselImage'
3 |
--------------------------------------------------------------------------------
/packages/mjml-cli/bin/mjml:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('../lib/client.js')()
4 |
--------------------------------------------------------------------------------
/packages/mjml-cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-cli",
3 | "description": "MJML: the only framework that makes responsive-email easy",
4 | "version": "4.15.3",
5 | "main": "bin/mjml",
6 | "bin": {
7 | "mjml-cli": "bin/mjml"
8 | },
9 | "files": [
10 | "bin",
11 | "lib"
12 | ],
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/mjmlio/mjml.git",
16 | "directory": "packages/mjml-cli"
17 | },
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/mjmlio/mjml/issues"
21 | },
22 | "homepage": "https://mjml.io",
23 | "scripts": {
24 | "clean": "rimraf lib",
25 | "build": "babel src --out-dir lib --root-mode upward"
26 | },
27 | "dependencies": {
28 | "@babel/runtime": "^7.23.9",
29 | "chokidar": "^3.0.0",
30 | "glob": "^10.3.10",
31 | "html-minifier": "^4.0.0",
32 | "js-beautify": "^1.6.14",
33 | "lodash": "^4.17.21",
34 | "minimatch": "^9.0.3",
35 | "mjml-core": "4.15.3",
36 | "mjml-migrate": "4.15.3",
37 | "mjml-parser-xml": "4.15.3",
38 | "mjml-validator": "4.15.3",
39 | "yargs": "^17.7.2"
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.8.4",
43 | "rimraf": "^3.0.2"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/mjml-cli/src/commands/outputToConsole.js:
--------------------------------------------------------------------------------
1 | export default ({ compiled: { html }, file }, addFileHeaderComment) =>
2 | new Promise((resolve) => {
3 | let output = ''
4 | if (addFileHeaderComment) {
5 | output = `\n`
6 | }
7 | output += `${html}\n`
8 |
9 | process.stdout.write(output, resolve)
10 | })
11 |
--------------------------------------------------------------------------------
/packages/mjml-cli/src/commands/outputToFile.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 |
4 | export const isDirectory = (file) => {
5 | try {
6 | const outputPath = path.resolve(process.cwd(), file)
7 |
8 | return fs.statSync(outputPath).isDirectory()
9 | } catch (e) {
10 | return false
11 | }
12 | }
13 |
14 | const replaceExtension = (input) =>
15 | input.replace(
16 | '.mjml',
17 | input.replace('.mjml', '').match(/(.)*\.(.)+$/g) ? '' : '.html',
18 | )
19 |
20 | const stripPath = (input) => input.match(/[^/\\]+$/g)[0]
21 |
22 | const makeGuessOutputName = (outputPath) => {
23 | if (isDirectory(outputPath)) {
24 | return (input) => path.join(outputPath, replaceExtension(stripPath(input)))
25 | }
26 |
27 | return (input) => {
28 | if (!outputPath) {
29 | return replaceExtension(stripPath(input))
30 | }
31 |
32 | return outputPath
33 | }
34 | }
35 |
36 | export default (outputPath) => {
37 | const guessOutputName = makeGuessOutputName(outputPath)
38 |
39 | return ({ file, compiled: { html } }) =>
40 | new Promise((resolve, reject) => {
41 | const outputName = guessOutputName(file)
42 |
43 | fs.writeFile(outputName, html, (err) => {
44 | if (err) {
45 | // eslint-disable-next-line prefer-promise-reject-errors
46 | return reject({ outputName, err })
47 | }
48 |
49 | return resolve(outputName)
50 | })
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/packages/mjml-cli/src/commands/readFile.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import { sync } from 'glob'
3 | import { flatMap } from 'lodash'
4 |
5 | export const flatMapPaths = (paths) =>
6 | flatMap(paths, (p) => sync(p, { nodir: true }))
7 |
8 | export default (path) => {
9 | try {
10 | return { file: path, mjml: fs.readFileSync(path).toString() }
11 | } catch (e) {
12 | // eslint-disable-next-line
13 | console.warn(`Cannot read file: ${path} doesn't exist or no access`, e)
14 | return {}
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/mjml-cli/src/commands/readStream.js:
--------------------------------------------------------------------------------
1 | const stdinSync = () =>
2 | new Promise((res) => {
3 | let buffer = ''
4 |
5 | const stream = process.stdin
6 |
7 | stream.on('data', (chunck) => {
8 | buffer += chunck
9 | })
10 |
11 | stream.on('end', () => res(buffer))
12 | })
13 |
14 | export default async () => {
15 | const mjml = await stdinSync()
16 | return { mjml }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/mjml-cli/src/helpers/defaultOptions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beautify: true,
3 | minify: false,
4 | }
5 |
--------------------------------------------------------------------------------
/packages/mjml-cli/src/helpers/fileContext.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 |
4 | const includeRegexp =
5 | /]+path=['"](.*(?:\.mjml|\.css|\.html))['"]\s*[^<>]*(\/>|>\s*<\/mj-include>)/gi
6 |
7 | const ensureIncludeIsSupportedFile = (file) =>
8 | path.extname(file).match(/\.mjml|\.css|\.html/) ? file : `${file}.mjml`
9 |
10 | const error = (e) => console.error(e.stack || e) // eslint-disable-line no-console
11 |
12 | export default (baseFile, filePath) => {
13 | const filesIncluded = []
14 |
15 | let filePathDirectory = ''
16 | if (filePath) {
17 | try {
18 | const isFilePathDir = fs.lstatSync(filePath).isDirectory()
19 |
20 | filePathDirectory = isFilePathDir ? filePath : path.dirname(filePath)
21 | } catch (e) {
22 | if (e.code === 'ENOENT') {
23 | throw new Error('Specified filePath does not exist')
24 | } else {
25 | throw e
26 | }
27 | }
28 | }
29 |
30 | const readIncludes = (dir, file, base) => {
31 | const currentFile = path.resolve(
32 | dir
33 | ? path.join(dir, ensureIncludeIsSupportedFile(file))
34 | : ensureIncludeIsSupportedFile(file),
35 | )
36 |
37 | const currentDirectory = path.dirname(currentFile)
38 |
39 | const includes = new RegExp(includeRegexp)
40 |
41 | let content
42 | try {
43 | content = fs.readFileSync(currentFile, 'utf8')
44 | } catch (e) {
45 | error(`File not found ${currentFile} from ${base}`)
46 | return
47 | }
48 |
49 | let matchgroup = includes.exec(content)
50 | while (matchgroup != null) {
51 | const includedFile = ensureIncludeIsSupportedFile(matchgroup[1])
52 |
53 | // when reading first level of includes we must join the path specified in filePath
54 | // when reading further nested includes, just take parent dir as base
55 | const targetDir =
56 | filePath && file === baseFile ? filePathDirectory : currentDirectory
57 |
58 | const includedFilePath = path.resolve(path.join(targetDir, includedFile))
59 |
60 | filesIncluded.push(includedFilePath)
61 |
62 | readIncludes(targetDir, includedFile, currentFile)
63 | matchgroup = includes.exec(content)
64 | }
65 | }
66 |
67 | readIncludes(null, baseFile, baseFile)
68 |
69 | return filesIncluded
70 | }
71 |
--------------------------------------------------------------------------------
/packages/mjml-column/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-column",
3 | "description": "mjml-column",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-column"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-core/README.md:
--------------------------------------------------------------------------------
1 | ## mjml-core
2 |
3 | ### Installation
4 |
5 | ```bash
6 | npm install --save mjml-core
7 | ```
8 |
9 | This is the core mjml library, composed by a set of functions for both parsing, and rendering mjml
10 |
11 | ### Usage
12 |
13 | ```javascript
14 | import mjml2html from 'mjml'
15 |
16 | console.log(mjml2html(`code`))
17 | ```
18 |
--------------------------------------------------------------------------------
/packages/mjml-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-core",
3 | "description": "mjml-core",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-core"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward",
22 | "test": "node ./tests/index.js"
23 | },
24 | "dependencies": {
25 | "@babel/runtime": "^7.23.9",
26 | "cheerio": "1.0.0-rc.12",
27 | "detect-node": "^2.0.4",
28 | "html-minifier": "^4.0.0",
29 | "js-beautify": "^1.6.14",
30 | "juice": "^10.0.0",
31 | "lodash": "^4.17.21",
32 | "mjml-migrate": "4.15.3",
33 | "mjml-parser-xml": "4.15.3",
34 | "mjml-validator": "4.15.3"
35 | },
36 | "devDependencies": {
37 | "@babel/cli": "^7.8.4",
38 | "chai": "^4.1.1",
39 | "rimraf": "^3.0.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/components.js:
--------------------------------------------------------------------------------
1 | import { kebabCase } from 'lodash'
2 | import { registerDependencies } from 'mjml-validator'
3 |
4 | const components = {}
5 |
6 | export function assignComponents(target, source) {
7 | for (const component of source) {
8 | target[component.componentName || kebabCase(component.name)] = component
9 | }
10 | }
11 |
12 | export function registerComponent(Component, options = {}) {
13 | assignComponents(components, [Component])
14 |
15 | if (Component.dependencies && options.registerDependencies) {
16 | registerDependencies(Component.dependencies)
17 | }
18 | }
19 |
20 | export default components
21 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/conditionalTag.js:
--------------------------------------------------------------------------------
1 | export const startConditionalTag = ''
4 | export const startNegationConditionalTag = ''
5 | export const startMsoNegationConditionalTag = ''
6 | export const endNegationConditionalTag = ''
7 |
8 | export default function conditionalTag(content, negation = false) {
9 | return `
10 | ${negation ? startNegationConditionalTag : startConditionalTag}
11 | ${content}
12 | ${negation ? endNegationConditionalTag : endConditionalTag}
13 | `
14 | }
15 |
16 | export function msoConditionalTag(content, negation = false) {
17 | return `
18 | ${negation ? startMsoNegationConditionalTag : startMsoConditionalTag}
19 | ${content}
20 | ${negation ? endNegationConditionalTag : endConditionalTag}
21 | `
22 | }
23 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/fonts.js:
--------------------------------------------------------------------------------
1 | import { forEach, map } from 'lodash'
2 |
3 | // eslint-disable-next-line import/prefer-default-export
4 | export function buildFontsTags(content, inlineStyle, fonts = {}) {
5 | const toImport = []
6 |
7 | forEach(fonts, (url, name) => {
8 | const regex = new RegExp(`"[^"]*font-family:[^"]*${name}[^"]*"`, 'gmi')
9 | const inlineRegex = new RegExp(`font-family:[^;}]*${name}`, 'gmi')
10 |
11 | if (content.match(regex) || inlineStyle.some((s) => s.match(inlineRegex))) {
12 | toImport.push(url)
13 | }
14 | })
15 |
16 | if (toImport.length > 0) {
17 | return `
18 |
19 | ${map(
20 | toImport,
21 | (url) => ` `,
22 | ).join('\n')}
23 |
26 | \n
27 | `
28 | }
29 |
30 | return ''
31 | }
32 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/formatAttributes.js:
--------------------------------------------------------------------------------
1 | import { reduce } from 'lodash'
2 | import { initializeType } from '../types/type'
3 |
4 | export default (attributes, allowedAttributes) =>
5 | reduce(
6 | attributes,
7 | (acc, val, attrName) => {
8 | if (allowedAttributes && allowedAttributes[attrName]) {
9 | const TypeConstructor = initializeType(allowedAttributes[attrName])
10 |
11 | if (TypeConstructor) {
12 | const type = new TypeConstructor(val)
13 |
14 | return {
15 | ...acc,
16 | [attrName]: type.getValue(),
17 | }
18 | }
19 | }
20 |
21 | return {
22 | ...acc,
23 | [attrName]: val,
24 | }
25 | },
26 | {},
27 | )
28 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/genRandomHexString.js:
--------------------------------------------------------------------------------
1 | export default function genRandomHexString(length) {
2 | let str = ''
3 | for (let i = 0; i < length; i += 1) {
4 | str += Math.floor(Math.random() * 16).toString(16)
5 | }
6 | return str
7 | }
8 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/jsonToXML.js:
--------------------------------------------------------------------------------
1 | const jsonToXML = ({ tagName, attributes, children, content }) => {
2 | const subNode =
3 | children && children.length > 0
4 | ? children.map(jsonToXML).join('\n')
5 | : content || ''
6 |
7 | const stringAttrs = Object.keys(attributes)
8 | .map((attr) => `${attr}="${attributes[attr]}"`)
9 | .join(' ')
10 |
11 | return `<${tagName}${
12 | stringAttrs === '' ? '>' : ` ${stringAttrs}>`
13 | }${subNode}${tagName}>`
14 | }
15 |
16 | export default jsonToXML
17 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/makeLowerBreakpoint.js:
--------------------------------------------------------------------------------
1 | export default function makeLowerBreakpoint(breakpoint) {
2 | try {
3 | const pixels = Number.parseInt(breakpoint.match('[0-9]+')[0], 10)
4 | return `${pixels - 1}px`
5 | } catch (e) {
6 | return breakpoint
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/mediaQueries.js:
--------------------------------------------------------------------------------
1 | import { map, isEmpty } from 'lodash'
2 |
3 | // eslint-disable-next-line import/prefer-default-export
4 | export default function buildMediaQueriesTags(
5 | breakpoint,
6 | mediaQueries = {},
7 | options = {},
8 | ) {
9 | if (isEmpty(mediaQueries)) {
10 | return ''
11 | }
12 |
13 | const { forceOWADesktop = false, printerSupport = false } = options
14 |
15 | const baseMediaQueries = map(
16 | mediaQueries,
17 | (mediaQuery, className) => `.${className} ${mediaQuery}`,
18 | )
19 | const thunderbirdMediaQueries = map(
20 | mediaQueries,
21 | (mediaQuery, className) => `.moz-text-html .${className} ${mediaQuery}`,
22 | )
23 | const owaQueries = map(baseMediaQueries, (mq) => `[owa] ${mq}`)
24 |
25 | return `
26 |
31 |
34 | ${
35 | printerSupport
36 | ? ``
41 | : ``
42 | }
43 | ${
44 | forceOWADesktop
45 | ? ``
46 | : ``
47 | }
48 | `
49 | }
50 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/mergeOutlookConditionnals.js:
--------------------------------------------------------------------------------
1 | // # OPTIMIZE ME: — check if previous conditionnal is `\s*?)/gm,
5 | (match, prefix, content, suffix) => {
6 | // find spaces between tags
7 | const processedContent = content
8 | .replace(
9 | /(^|>)(\s+)(<|$)/gm,
10 | (match, prefix, content, suffix) => `${prefix}${suffix}`,
11 | )
12 | .replace(/\s{2,}/gm, ' ')
13 | return `${prefix}${processedContent}${suffix}`
14 | },
15 | )
16 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/preview.js:
--------------------------------------------------------------------------------
1 | export default function (content) {
2 | if (content === '') {
3 | return ''
4 | }
5 |
6 | return `
7 | ${content}
8 | `
9 | }
10 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/shorthandParser.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 |
3 | export default function (cssValue, direction) {
4 | const splittedCssValue = cssValue.trim().replace(/\s+/g, ' ').split(' ', 4)
5 | let directions = {}
6 |
7 | switch (splittedCssValue.length) {
8 | case 2:
9 | directions = { top: 0, bottom: 0, left: 1, right: 1 }
10 | break
11 |
12 | case 3:
13 | directions = { top: 0, left: 1, right: 1, bottom: 2 }
14 | break
15 |
16 | case 4:
17 | directions = { top: 0, right: 1, bottom: 2, left: 3 }
18 | break
19 | case 1:
20 | default:
21 | return parseInt(cssValue, 10)
22 | }
23 |
24 | return parseInt(splittedCssValue[directions[direction]] || 0, 10)
25 | }
26 |
27 | export function borderParser(border) {
28 | return parseInt(get(border.match(/(?:(?:^| )(\d+))/), 1), 10) || 0
29 | }
30 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/skeleton.js:
--------------------------------------------------------------------------------
1 | import { negate, isNil } from 'lodash'
2 | import buildPreview from './preview'
3 | import { buildFontsTags } from './fonts'
4 | import buildMediaQueriesTags from './mediaQueries'
5 | import { buildStyleFromComponents, buildStyleFromTags } from './styles'
6 |
7 | export default function skeleton(options) {
8 | const {
9 | backgroundColor = '',
10 | beforeDoctype = '',
11 | breakpoint = '480px',
12 | content = '',
13 | fonts = {},
14 | mediaQueries = {},
15 | headStyle = {},
16 | componentsHeadStyle = [],
17 | headRaw = [],
18 | preview,
19 | title = '',
20 | style = [],
21 | forceOWADesktop,
22 | printerSupport,
23 | inlineStyle,
24 | lang,
25 | dir,
26 | } = options
27 |
28 | return `${beforeDoctype ? `${beforeDoctype}\n` : ''}
29 |
30 |
31 | ${title}
32 |
33 |
34 |
35 |
36 |
37 |
44 |
54 |
59 | ${buildFontsTags(content, inlineStyle, fonts)}
60 | ${buildMediaQueriesTags(breakpoint, mediaQueries, {
61 | forceOWADesktop,
62 | printerSupport,
63 | })}
64 | ${buildStyleFromComponents(breakpoint, componentsHeadStyle, headStyle)}
65 | ${buildStyleFromTags(breakpoint, style)}
66 | ${headRaw.filter(negate(isNil)).join('\n')}
67 |
68 |
71 | ${buildPreview(preview)}
72 | ${content}
73 |
74 |
75 | `
76 | }
77 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/styles.js:
--------------------------------------------------------------------------------
1 | import { isFunction } from 'lodash'
2 |
3 | export function buildStyleFromComponents(
4 | breakpoint,
5 | componentsHeadStyles,
6 | headStylesObject,
7 | ) {
8 | const headStyles = Object.values(headStylesObject)
9 |
10 | if (componentsHeadStyles.length === 0 && headStyles.length === 0) {
11 | return ''
12 | }
13 |
14 | return `
15 | `
20 | }
21 |
22 | export function buildStyleFromTags(breakpoint, styles) {
23 | if (styles.length === 0) {
24 | return ''
25 | }
26 |
27 | return `
28 | `
34 | }
35 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/suffixCssClasses.js:
--------------------------------------------------------------------------------
1 | export default (classes, suffix) =>
2 | classes
3 | ? classes
4 | .split(' ')
5 | .map((c) => `${c}-${suffix}`)
6 | .join(' ')
7 | : ''
8 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/helpers/widthParser.js:
--------------------------------------------------------------------------------
1 | const unitRegex = /[\d.,]*(\D*)$/
2 |
3 | export default function widthParser(width, options = {}) {
4 | const { parseFloatToInt = true } = options
5 |
6 | const widthUnit = unitRegex.exec(width.toString())[1]
7 | const unitParsers = {
8 | default: parseInt,
9 | px: parseInt,
10 | '%': parseFloatToInt ? parseInt : parseFloat,
11 | }
12 | const parser = unitParsers[widthUnit] || unitParsers.default
13 |
14 | return {
15 | parsedWidth: parser(width),
16 | unit: widthUnit || 'px',
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/boolean.js:
--------------------------------------------------------------------------------
1 | import Type from './type'
2 |
3 | export const matcher = /^boolean/gim
4 |
5 | export default () =>
6 | class Boolean extends Type {
7 | constructor(boolean) {
8 | super(boolean)
9 |
10 | this.matchers = [/^true$/i, /^false$/i]
11 | }
12 |
13 | isValid() {
14 | return this.value === true || this.value === false
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/color.js:
--------------------------------------------------------------------------------
1 | import Type from './type'
2 | import colors from './helpers/colors'
3 |
4 | export const matcher = /^color/gim
5 |
6 | const shorthandRegex = /^#\w{3}$/
7 | const replaceInputRegex = /^#(\w)(\w)(\w)$/
8 | const replaceOutput = '#$1$1$2$2$3$3'
9 |
10 | export default () =>
11 | class Color extends Type {
12 | constructor(color) {
13 | super(color)
14 |
15 | this.matchers = [
16 | /rgba\(\d{1,3},\s?\d{1,3},\s?\d{1,3},\s?\d(\.\d{1,3})?\)/gi,
17 | /rgb\(\d{1,3},\s?\d{1,3},\s?\d{1,3}\)/gi,
18 | /^#([0-9a-f]{3}){1,2}$/gi,
19 | new RegExp(`^(${colors.join('|')})$`),
20 | ]
21 | }
22 |
23 | getValue() {
24 | if (typeof this.value === 'string' && this.value.match(shorthandRegex)) {
25 | return this.value.replace(replaceInputRegex, replaceOutput)
26 | }
27 |
28 | return this.value
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/enum.js:
--------------------------------------------------------------------------------
1 | import { escapeRegExp } from 'lodash'
2 | import Type from './type'
3 |
4 | export const matcher = /^enum/gim
5 |
6 | export default (params) => {
7 | const matchers = params.match(/\(([^)]+)\)/)[1].split(',')
8 |
9 | return class Enum extends Type {
10 | static errorMessage = `has invalid value: $value for type Enum, only accepts ${matchers.join(
11 | ', ',
12 | )}`
13 |
14 | constructor(value) {
15 | super(value)
16 |
17 | this.matchers = matchers.map((m) => new RegExp(`^${escapeRegExp(m)}$`))
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/helpers/colors.js:
--------------------------------------------------------------------------------
1 | export default [
2 | 'aliceblue',
3 | 'antiquewhite',
4 | 'aqua',
5 | 'aquamarine',
6 | 'azure',
7 | 'beige',
8 | 'bisque',
9 | 'black',
10 | 'blanchedalmond',
11 | 'blue',
12 | 'blueviolet',
13 | 'brown',
14 | 'burlywood',
15 | 'cadetblue',
16 | 'chartreuse',
17 | 'chocolate',
18 | 'coral',
19 | 'cornflowerblue',
20 | 'cornsilk',
21 | 'crimson',
22 | 'cyan',
23 | 'darkblue',
24 | 'darkcyan',
25 | 'darkgoldenrod',
26 | 'darkgray',
27 | 'darkgreen',
28 | 'darkgrey',
29 | 'darkkhaki',
30 | 'darkmagenta',
31 | 'darkolivegreen',
32 | 'darkorange',
33 | 'darkorchid',
34 | 'darkred',
35 | 'darksalmon',
36 | 'darkseagreen',
37 | 'darkslateblue',
38 | 'darkslategray',
39 | 'darkslategrey',
40 | 'darkturquoise',
41 | 'darkviolet',
42 | 'deeppink',
43 | 'deepskyblue',
44 | 'dimgray',
45 | 'dimgrey',
46 | 'dodgerblue',
47 | 'firebrick',
48 | 'floralwhite',
49 | 'forestgreen',
50 | 'fuchsia',
51 | 'gainsboro',
52 | 'ghostwhite',
53 | 'gold',
54 | 'goldenrod',
55 | 'gray',
56 | 'green',
57 | 'greenyellow',
58 | 'grey',
59 | 'honeydew',
60 | 'hotpink',
61 | 'indianred',
62 | 'indigo',
63 | 'inherit',
64 | 'ivory',
65 | 'khaki',
66 | 'lavender',
67 | 'lavenderblush',
68 | 'lawngreen',
69 | 'lemonchiffon',
70 | 'lightblue',
71 | 'lightcoral',
72 | 'lightcyan',
73 | 'lightgoldenrodyellow',
74 | 'lightgray',
75 | 'lightgreen',
76 | 'lightgrey',
77 | 'lightpink',
78 | 'lightsalmon',
79 | 'lightseagreen',
80 | 'lightskyblue',
81 | 'lightslategray',
82 | 'lightslategrey',
83 | 'lightsteelblue',
84 | 'lightyellow',
85 | 'lime',
86 | 'limegreen',
87 | 'linen',
88 | 'magenta',
89 | 'maroon',
90 | 'mediumaquamarine',
91 | 'mediumblue',
92 | 'mediumorchid',
93 | 'mediumpurple',
94 | 'mediumseagreen',
95 | 'mediumslateblue',
96 | 'mediumspringgreen',
97 | 'mediumturquoise',
98 | 'mediumvioletred',
99 | 'midnightblue',
100 | 'mintcream',
101 | 'mistyrose',
102 | 'moccasin',
103 | 'navajowhite',
104 | 'navy',
105 | 'oldlace',
106 | 'olive',
107 | 'olivedrab',
108 | 'orange',
109 | 'orangered',
110 | 'orchid',
111 | 'palegoldenrod',
112 | 'palegreen',
113 | 'paleturquoise',
114 | 'palevioletred',
115 | 'papayawhip',
116 | 'peachpuff',
117 | 'peru',
118 | 'pink',
119 | 'plum',
120 | 'powderblue',
121 | 'purple',
122 | 'rebeccapurple',
123 | 'red',
124 | 'rosybrown',
125 | 'royalblue',
126 | 'saddlebrown',
127 | 'salmon',
128 | 'sandybrown',
129 | 'seagreen',
130 | 'seashell',
131 | 'sienna',
132 | 'silver',
133 | 'skyblue',
134 | 'slateblue',
135 | 'slategray',
136 | 'slategrey',
137 | 'snow',
138 | 'springgreen',
139 | 'steelblue',
140 | 'tan',
141 | 'teal',
142 | 'thistle',
143 | 'tomato',
144 | 'transparent',
145 | 'turquoise',
146 | 'violet',
147 | 'wheat',
148 | 'white',
149 | 'whitesmoke',
150 | 'yellow',
151 | 'yellowgreen',
152 | ]
153 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/index.js:
--------------------------------------------------------------------------------
1 | import NBoolean, { matcher as booleanMatcher } from './boolean'
2 | import Color, { matcher as colorMatcher } from './color'
3 | import Enum, { matcher as enumMatcher } from './enum'
4 | import Unit, { matcher as unitMatcher } from './unit'
5 | import NString, { matcher as stringMatcher } from './string'
6 | import NInteger, { matcher as intMatcher } from './integer'
7 |
8 | export default {
9 | boolean: { matcher: booleanMatcher, typeConstructor: NBoolean },
10 | enum: { matcher: enumMatcher, typeConstructor: Enum },
11 | color: { matcher: colorMatcher, typeConstructor: Color },
12 | unit: { matcher: unitMatcher, typeConstructor: Unit },
13 | string: { matcher: stringMatcher, typeConstructor: NString },
14 | integer: { matcher: intMatcher, typeConstructor: NInteger },
15 | }
16 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/integer.js:
--------------------------------------------------------------------------------
1 | import Type from './type'
2 |
3 | export const matcher = /^integer/gim
4 |
5 | export default () =>
6 | class NInteger extends Type {
7 | constructor(value) {
8 | super(value)
9 |
10 | this.matchers = [/\d+/]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/string.js:
--------------------------------------------------------------------------------
1 | import Type from './type'
2 |
3 | export const matcher = /^string/gim
4 |
5 | export default () =>
6 | class NString extends Type {
7 | constructor(value) {
8 | super(value)
9 |
10 | this.matchers = [/.*/]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/type.js:
--------------------------------------------------------------------------------
1 | import { some, find } from 'lodash'
2 | import typesConstructors from './index'
3 |
4 | // Avoid recreate existing types
5 | export const types = {}
6 |
7 | export const initializeType = (typeConfig) => {
8 | if (types[typeConfig]) {
9 | return types[typeConfig]
10 | }
11 |
12 | const { typeConstructor } =
13 | find(typesConstructors, (type) => !!typeConfig.match(type.matcher)) || {}
14 |
15 | if (!typeConstructor) {
16 | throw new Error(`No type found for ${typeConfig}`)
17 | }
18 |
19 | types[typeConfig] = typeConstructor(typeConfig)
20 |
21 | return types[typeConfig]
22 | }
23 |
24 | export default class Type {
25 | constructor(value) {
26 | this.value = value
27 | }
28 |
29 | isValid() {
30 | return some(this.matchers, (matcher) => `${this.value}`.match(matcher))
31 | }
32 |
33 | getErrorMessage() {
34 | if (this.isValid()) {
35 | return
36 | }
37 |
38 | const errorMessage =
39 | this.constructor.errorMessage ||
40 | `has invalid value: ${this.value} for type ${this.constructor.name} `
41 |
42 | return errorMessage.replace(/\$value/g, this.value)
43 | }
44 |
45 | static check(type) {
46 | return !!type.match(this.constructor.typeChecker)
47 | }
48 |
49 | getValue() {
50 | return this.value
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/mjml-core/src/types/unit.js:
--------------------------------------------------------------------------------
1 | import { escapeRegExp } from 'lodash'
2 | import Type from './type'
3 |
4 | export const matcher = /^(unit|unitWithNegative)\(.*\)/gim
5 |
6 | export default (params) => {
7 | const allowNeg = params.match(/^unitWithNegative/) ? '-|' : ''
8 |
9 | const units = params.match(/\(([^)]+)\)/)[1].split(',')
10 | const argsMatch = params.match(/\{([^}]+)\}/)
11 | const args = (argsMatch && argsMatch[1] && argsMatch[1].split(',')) || ['1'] // defaults to 1
12 |
13 | const allowAuto = units.includes('auto') ? '|auto' : ''
14 | const filteredUnits = units.filter((u) => u !== 'auto')
15 |
16 | return class Unit extends Type {
17 | static errorMessage = `has invalid value: $value for type Unit, only accepts (${units.join(
18 | ', ',
19 | )}) units and ${args.join(' to ')} value(s)`
20 |
21 | constructor(value) {
22 | super(value)
23 |
24 | this.matchers = [
25 | new RegExp(
26 | `^(((${allowNeg}\\d|,|\\.){1,}(${filteredUnits
27 | .map(escapeRegExp)
28 | .join('|')})|0${allowAuto})( )?){${args.join(',')}}$`,
29 | ),
30 | ]
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/mjml-core/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../.eslintrc",
3 | "rules": {
4 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/mjml-core/tests/index.js:
--------------------------------------------------------------------------------
1 | require('./jsonToXml-test')
2 | require('./mergeOutlookConditionnals-test')
3 | require('./minifyOutlookConditionnals-test')
4 | require('./shorthandParser-test')
5 | require('./skeleton-test')
6 | require('./widthParser-test')
7 |
--------------------------------------------------------------------------------
/packages/mjml-core/tests/jsonToXml-test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai')
2 | const jsonToXml = require('../lib/helpers/jsonToXML')
3 |
4 | const json = {
5 | line: 1,
6 | includedIn: [],
7 | tagName: 'mjml',
8 | children:
9 | [ {
10 | line: 2,
11 | includedIn: [],
12 | tagName: 'mj-body',
13 | children:
14 | [ {
15 | line: 3,
16 | includedIn: [],
17 | tagName: 'mj-section',
18 | children:
19 | [ {
20 | line: 4,
21 | includedIn: [],
22 | tagName: 'mj-column',
23 | children:
24 | [ {
25 | line: 5,
26 | includedIn: [],
27 | tagName: 'mj-text',
28 | attributes:
29 | { 'font-size': '20px',
30 | color: '#F45E43',
31 | 'font-family': 'helvetica' },
32 | content: 'Hello World' } ],
33 | attributes: {} } ],
34 | attributes: {} } ],
35 | attributes: {} } ],
36 | attributes: {} }
37 |
38 | const xml = jsonToXml(json)
39 |
40 | const validXml = 'Hello World '
41 |
42 | chai.expect(xml, 'jsonToXML test failed')
43 | .to.equal(validXml)
44 |
--------------------------------------------------------------------------------
/packages/mjml-core/tests/mergeOutlookConditionnals-test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai')
2 | const mergeOutlookConditionnals = require('../lib/helpers/mergeOutlookConditionnals')
3 |
4 | const testValues = [
5 | {
6 | input: '
13 | \n \n \n \n \n \n \n \n \n \n \n \n \n \n
100 | `
101 | }
102 |
103 | render() {
104 | return `
105 |
110 |
111 | ${this.renderAfter()}
112 | `
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/packages/mjml-group/README.md:
--------------------------------------------------------------------------------
1 | ## mj-group
2 |
3 |
4 |
5 | Desktop
6 |
8 |
9 |
10 |
11 | Mobile
12 |
14 |
15 |
16 | mj-group allows you to prevent columns from stacking on mobile. To do so, wrap the columns inside a `mj-group` tag, so they'll stay side by side on mobile.
17 |
18 | ```xml
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Easy and quick
27 | Write less code, save time and code more efficiently with MJML’s semantic syntax.
28 |
29 |
30 |
31 |
32 |
33 | Responsive
34 | MJML is responsive by design on most-popular email clients, even Outlook.
35 |
36 |
37 |
38 |
39 |
40 |
41 | ```
42 |
43 |
44 |
45 |
46 |
47 |
48 | Column inside a group must have a width in percentage, not in pixel
49 |
50 |
51 |
52 |
53 | You can have both column and group inside a Section
54 |
55 |
56 |
57 | iOS 9 Issue: If you use a HTML beautifier for MJML output, iOS9 will render your columns inside a mj-group as stacked. On the output HTML, remove the blank space between the two columns inside a mj-group.
58 |
59 |
60 |
61 | attribute | unit | description | default attributes
62 | --------------------|-------------|--------------------------------|--------------------------------------
63 | width | percent/px | group width | (100 / number of non-raw elements in section)%
64 | vertical-align | string | middle/top/bottom | top
65 | background-color | string | background color for a group | n/a
66 | direction | ltr / rtl | set the display order of direct children | ltr
67 | css-class | string | class name, added to the root HTML element created | n/a
68 |
--------------------------------------------------------------------------------
/packages/mjml-group/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-group",
3 | "description": "mjml-group",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-group"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-attributes/README.md:
--------------------------------------------------------------------------------
1 | ## mj-attributes
2 |
3 | Inside `mj-attributes`, a tag citing one MJML component (like `mj-text`;
4 | see example) overrides default settings for listed MJML attributes
5 | on the one component.
6 |
7 | An `mj-all` is like the above, but affects all MJML components via the one tag.
8 |
9 | `mj-class` tags create a named group of MJML attributes you can apply to MJML
10 | components. To apply them, use `mj-class=""`.
11 |
12 | ```xml
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Hello World!
27 |
28 |
29 |
30 |
31 |
32 | ```
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | In the following list, MJML applies only the first MJML attributes found:
42 |
43 | inline MJML attributes,
44 | the entry for the same MJML component (like, "mj-text") in "mj-attributes",
45 | "mj-all" in "mj-attributes", and
46 | default MJML values.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/packages/mjml-head-attributes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head-attributes",
3 | "description": "mjml-head-attributes",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head-attributes"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-attributes/src/index.js:
--------------------------------------------------------------------------------
1 | import { forEach, omit, reduce } from 'lodash'
2 |
3 | import { HeadComponent } from 'mjml-core'
4 |
5 | export default class MjAttributes extends HeadComponent {
6 | static componentName = 'mj-attributes'
7 |
8 | handler() {
9 | const { add } = this.context
10 |
11 | const { children } = this.props
12 |
13 | forEach(children, (child) => {
14 | const { tagName, attributes, children } = child
15 |
16 | if (tagName === 'mj-class') {
17 | add('classes', attributes.name, omit(attributes, ['name']))
18 |
19 | add(
20 | 'classesDefault',
21 | attributes.name,
22 | reduce(
23 | children,
24 | (acc, { tagName, attributes }) => ({
25 | ...acc,
26 | [tagName]: attributes,
27 | }),
28 | {},
29 | ),
30 | )
31 | } else {
32 | add('defaultAttributes', tagName, attributes)
33 | }
34 | })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/mjml-head-breakpoint/README.md:
--------------------------------------------------------------------------------
1 | ## mj-breakpoint
2 | This tag allows you to control on which breakpoint the layout should go desktop/mobile.
3 |
4 | ```xml
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Hello World!
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | attribute | unit | description | default value
29 | ---------------------|---------------|--------------------------------|---------------
30 | width | px | breakpoint's value | n/a
31 |
--------------------------------------------------------------------------------
/packages/mjml-head-breakpoint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head-breakpoint",
3 | "description": "mjml-head-breakpoint",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head-breakpoint"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-breakpoint/src/index.js:
--------------------------------------------------------------------------------
1 | import { HeadComponent } from 'mjml-core'
2 |
3 | export default class MjBreakpoint extends HeadComponent {
4 | static componentName = 'mj-breakpoint'
5 |
6 | static endingTag = true
7 |
8 | static allowedAttributes = {
9 | width: 'unit(px)',
10 | }
11 |
12 | handler() {
13 | const { add } = this.context
14 |
15 | add('breakpoint', this.getAttribute('width'))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/mjml-head-font/README.md:
--------------------------------------------------------------------------------
1 | ## mj-font
2 |
3 | This tag imports fonts.
4 | The tag has effect only if the template uses the font, too.
5 | The `href` attribute points to a hosted css file; that file contains a `@font-face` declaration.
6 | Example: [https://fonts
7 | .googleapis.com/css?family=Raleway](https://fonts.googleapis.com/css?family=Raleway)
8 |
9 | ```xml
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 | Hello World!
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | attribute | unit | description | default value
34 | ------------|----------|----------------------------|---------------
35 | href | string | URL of a hosted CSS file | n/a
36 | name | string | name of the font | n/a
37 |
--------------------------------------------------------------------------------
/packages/mjml-head-font/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head-font",
3 | "description": "mjml-head-font",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head-font"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-font/src/index.js:
--------------------------------------------------------------------------------
1 | import { HeadComponent } from 'mjml-core'
2 |
3 | export default class MjFont extends HeadComponent {
4 | static componentName = 'mj-font'
5 |
6 | static allowedAttributes = {
7 | name: 'string',
8 | href: 'string',
9 | }
10 |
11 | handler() {
12 | const { add } = this.context
13 |
14 | add('fonts', this.getAttribute('name'), this.getAttribute('href'))
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/mjml-head-html-attributes/README.md:
--------------------------------------------------------------------------------
1 | ## mj-html-attributes
2 |
3 | This tag allows you to add custom attributes on any html tag of the generated html, using css selectors.
4 | It's not needed for most email creations, but can be useful in some cases, i.e. editable templates.
5 |
6 | ```xml
7 |
8 |
9 |
10 |
11 | 42
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Hello World!
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 | In the generated html, a mj-text becomes a `td`, and a `div` inside this `td`. In this example, the `td` will have the `class="custom"`. Using the css selector `path=".custom div"`, the `div` inside the `td` will get the attribute `data-id="42"`.
28 |
29 | To use this component, you will likely have to look at the generated html to see where exactly are the `css-class` applied, to know which css selector you need to use to add your custom attribute on the right html tag.
30 |
31 | You can use multiple `mj-selector` inside a `mj-html-attributes`, and multiple `mj-html-attribute` inside a `mj-selector`.
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/packages/mjml-head-html-attributes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head-html-attributes",
3 | "description": "mjml-head-html-attributes",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head-html-attributes"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-html-attributes/src/index.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 | import { HeadComponent } from 'mjml-core'
3 |
4 | export default class MjHtmlAttributes extends HeadComponent {
5 | static componentName = 'mj-html-attributes'
6 |
7 | handler() {
8 | const { add } = this.context
9 | const { children } = this.props
10 |
11 | children
12 | .filter((c) => c.tagName === 'mj-selector')
13 | .forEach((selector) => {
14 | const { attributes, children } = selector
15 | const { path } = attributes
16 |
17 | const custom = children
18 | .filter(
19 | (c) =>
20 | c.tagName === 'mj-html-attribute' && !!get(c, 'attributes.name'),
21 | )
22 | .reduce(
23 | (acc, c) => ({
24 | ...acc,
25 | [c.attributes.name]: c.content,
26 | }),
27 | {},
28 | )
29 |
30 | add('htmlAttributes', path, custom)
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/mjml-head-preview/README.md:
--------------------------------------------------------------------------------
1 | ## mj-preview
2 |
3 | This tag allows you to set the preview that will be displayed in the inbox of the recipient.
4 |
5 | ```xml
6 |
7 |
8 | Hello MJML
9 |
10 |
11 |
12 |
13 |
14 | Hello World!
15 |
16 |
17 |
18 |
19 |
20 | ```
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | `mj-preview` doesn't support any attribute.
29 |
30 |
--------------------------------------------------------------------------------
/packages/mjml-head-preview/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head-preview",
3 | "description": "mjml-head-preview",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head-preview"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-preview/src/index.js:
--------------------------------------------------------------------------------
1 | import { HeadComponent } from 'mjml-core'
2 |
3 | export default class MjPreview extends HeadComponent {
4 | static componentName = 'mj-preview'
5 |
6 | static endingTag = true
7 |
8 | handler() {
9 | const { add } = this.context
10 |
11 | add('preview', this.getContent())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/mjml-head-style/README.md:
--------------------------------------------------------------------------------
1 | ## mj-style
2 |
3 | This tag allows you to set CSS styles that will be applied to the HTML in your MJML document as well as the HTML outputted. The CSS styles will be added to the head of the rendered HTML by default, but can also be inlined by using the `inline="inline"` attribute.
4 |
5 | Here is an example showing the use in combination with the `css-class` attribute, which is supported by all body components.
6 |
7 | ```xml
8 |
9 |
10 |
11 |
12 |
13 |
14 | .blue-text div {
15 | color: blue !important;
16 | }
17 |
18 |
19 | .red-text div {
20 | color: red !important;
21 | text-decoration: underline !important;
22 | }
23 |
24 |
25 |
26 |
27 |
28 | I'm red and underlined
29 | I'm blue because of inline
30 | I'm green
31 |
32 |
33 |
34 |
35 | ```
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | attribute | unit | description | default value
44 | ---------------------|---------------|-------------------------------------|---------------
45 | inline | string | set to "inline" to inline styles | n/a
46 |
47 |
48 | Mjml generates multiple html elements from a single mjml element. For optimal flexibility, the `css-class` will be applied to the most outer html element, so if you want to target a specific sub-element with a css selector, you may need to look at the generated html to see which exact selector you need.
49 |
50 |
--------------------------------------------------------------------------------
/packages/mjml-head-style/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head-style",
3 | "description": "mjml-head-style",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head-style"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-style/src/index.js:
--------------------------------------------------------------------------------
1 | import { HeadComponent } from 'mjml-core'
2 |
3 | export default class MjStyle extends HeadComponent {
4 | static componentName = 'mj-style'
5 |
6 | static endingTag = true
7 |
8 | static allowedAttributes = {
9 | inline: 'string',
10 | }
11 |
12 | handler() {
13 | const { add } = this.context
14 |
15 | add(
16 | this.getAttribute('inline') === 'inline' ? 'inlineStyle' : 'style',
17 | this.getContent(),
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/mjml-head-title/README.md:
--------------------------------------------------------------------------------
1 | ## mj-title
2 |
3 | Defines the document's title that browsers show in the title bar or a page's tab.
4 |
5 | ```xml
6 |
7 |
8 | Hello MJML
9 |
10 |
11 |
12 |
13 |
14 | Hello World!
15 |
16 |
17 |
18 |
19 |
20 | ```
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/mjml-head-title/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head-title",
3 | "description": "mjml-head-title",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head-title"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head-title/src/index.js:
--------------------------------------------------------------------------------
1 | import { HeadComponent } from 'mjml-core'
2 |
3 | export default class MjTitle extends HeadComponent {
4 | static componentName = 'mj-title'
5 |
6 | static endingTag = true
7 |
8 | handler() {
9 | const { add } = this.context
10 |
11 | add('title', this.getContent())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/mjml-head/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-head",
3 | "description": "mjml-head",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-head"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-head/src/index.js:
--------------------------------------------------------------------------------
1 | import { HeadComponent } from 'mjml-core'
2 |
3 | export default class MjHead extends HeadComponent {
4 | static componentName = 'mj-head'
5 |
6 | handler() {
7 | return this.handlerChildren()
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/mjml-hero/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-hero",
3 | "description": "mjml-hero",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-hero"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-image/README.md:
--------------------------------------------------------------------------------
1 | ## mj-image
2 |
3 | Displays a responsive image in your email. It is similar to the HTML ` ` tag.
4 | Note that if no width is provided, the image will use the parent column width.
5 |
6 | ```xml
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ```
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | attribute | unit | description | default value
26 | ------------------------------|---------------|--------------------------------|-----------------------------
27 | align | position | image alignment | center
28 | alt | string | image description | ''
29 | border | string | css border definition | none
30 | border-top | string | css border definition | none
31 | border-bottom | string | css border definition | none
32 | border-left | string | css border definition | none
33 | border-right | string | css border definition | none
34 | border-radius | px | border radius | n/a
35 | container-background-color | color | inner element background color | n/a
36 | css-class | string | class name, added to the root HTML element created | n/a
37 | fluid-on-mobile | string | if "true", will be full width on mobile even if width is set | n/a
38 | height | px | image height | auto
39 | href | url | link to redirect to on click | n/a
40 | name | string | specify the link name attribute | n/a
41 | padding | px | supports up to 4 parameters | 10px 25px
42 | padding-bottom | px | bottom offset | n/a
43 | padding-left | px | left offset | n/a
44 | padding-right | px | right offset | n/a
45 | padding-top | px | top offset | n/a
46 | rel | string | specify the rel attribute | n/a
47 | sizes | media query & width | set width based on query | n/a
48 | src | url | image source | n/a
49 | srcset | url & width | enables to set a different image source based on the viewport | n/a
50 | target | string | link target on click | \_blank
51 | title | string | tooltip & accessibility | n/a
52 | usemap | string | reference to image map, be careful, it isn't supported everywhere | n/a
53 | width | px | image width | parent width
54 |
--------------------------------------------------------------------------------
/packages/mjml-image/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-image",
3 | "description": "mjml-image",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-image"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-migrate/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Nicolas Garnier
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/mjml-migrate/README.md:
--------------------------------------------------------------------------------
1 | # mjml-migrate
2 |
3 | ## Purpose
4 |
5 | Makes a template following the MJML 3 syntax compatible with MJML 4.
6 |
7 | ## Installation
8 |
9 | Clone the repo & `npm install` or install via NPM: `npm install mjml-migrate`
10 |
11 | ## Usage
12 |
13 | `migrate `
14 |
15 | ## What happens
16 |
17 | * `mj-container` is removed and its attributes are passed to `mj-body`
18 | * Unitless values are converted to `px`
19 | * `mj-social`'s syntax is replaced with the v4 syntax
20 | * Unsupported tags (defined in `unavailableTags` in `config.js`) are removed
21 |
22 |
--------------------------------------------------------------------------------
/packages/mjml-migrate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-migrate",
3 | "version": "4.15.3",
4 | "description": "A tool to migrate a template from MJML 3 to MJML 4",
5 | "main": "lib/migrate.js",
6 | "bin": {
7 | "migrate": "lib/cli.js"
8 | },
9 | "files": [
10 | "lib"
11 | ],
12 | "scripts": {
13 | "clean": "rimraf lib",
14 | "build": "babel src --out-dir lib --root-mode upward"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/mjmlio/mjml.git",
19 | "directory": "packages/mjml-migrate"
20 | },
21 | "author": "Nicolas Garnier",
22 | "license": "MIT",
23 | "devDependencies": {
24 | "@babel/cli": "^7.8.4",
25 | "rimraf": "^3.0.2"
26 | },
27 | "dependencies": {
28 | "@babel/runtime": "^7.23.9",
29 | "js-beautify": "^1.6.14",
30 | "lodash": "^4.17.21",
31 | "mjml-core": "4.15.3",
32 | "mjml-parser-xml": "4.15.3",
33 | "yargs": "^17.7.2"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/mjml-migrate/src/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import fs from 'fs'
4 | import yargs from 'yargs'
5 | import migrate from './migrate'
6 | import { version } from '../package.json'
7 |
8 | const program = yargs
9 | .usage('$0 [options] ')
10 | .version(version)
11 | .help()
12 |
13 | if (program.argv._.length !== 2) {
14 | program.showHelp()
15 | process.exit(1)
16 | }
17 |
18 | const [inputFilename, outputFilename] = program.argv._
19 |
20 | const input = fs.readFileSync(inputFilename, 'utf8')
21 | const output = migrate(input)
22 |
23 | fs.writeFileSync(outputFilename, output)
24 |
25 | // eslint-disable-next-line no-console
26 | console.log(
27 | `${inputFilename} was converted to the MJML 4 syntax in ${outputFilename}`,
28 | )
29 |
--------------------------------------------------------------------------------
/packages/mjml-migrate/src/config.js:
--------------------------------------------------------------------------------
1 | const unavailableTags = ['mj-html', 'mj-invoice', 'mj-list', 'mj-location']
2 |
3 | const attributesWithUnit = [
4 | 'background-size',
5 | 'border-radius',
6 | 'border-width',
7 | 'cellpadding',
8 | 'cellspacing',
9 | 'font-size',
10 | 'height',
11 | 'icon-height',
12 | 'ico-padding',
13 | 'ico-padding-bottom',
14 | 'ico-font-size',
15 | 'ico-line-height',
16 | 'ico-padding-left',
17 | 'ico-padding-right',
18 | 'ico-padding-top',
19 | 'icon-size',
20 | 'icon-width',
21 | 'inner-padding',
22 | 'letter-spacing',
23 | 'padding',
24 | 'padding-bottom',
25 | 'padding-left',
26 | 'padding-right',
27 | 'padding-left',
28 | 'tb-border-radius',
29 | 'tb-width',
30 | 'width',
31 | ]
32 |
33 | module.exports = {
34 | unavailableTags,
35 | attributesWithUnit,
36 | }
37 |
--------------------------------------------------------------------------------
/packages/mjml-navbar/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-navbar",
3 | "description": "mjml-navbar",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-navbar"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-navbar/src/NavbarLink.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent, suffixCssClasses } from 'mjml-core'
2 |
3 | import conditionalTag from 'mjml-core/lib/helpers/conditionalTag'
4 |
5 | export default class MjNavbarLink extends BodyComponent {
6 | static componentName = 'mj-navbar-link'
7 |
8 | static endingTag = true
9 |
10 | static allowedAttributes = {
11 | color: 'color',
12 | 'font-family': 'string',
13 | 'font-size': 'unit(px)',
14 | 'font-style': 'string',
15 | 'font-weight': 'string',
16 | href: 'string',
17 | name: 'string',
18 | target: 'string',
19 | rel: 'string',
20 | 'letter-spacing': 'unitWithNegative(px,em)',
21 | 'line-height': 'unit(px,%,)',
22 | 'padding-bottom': 'unit(px,%)',
23 | 'padding-left': 'unit(px,%)',
24 | 'padding-right': 'unit(px,%)',
25 | 'padding-top': 'unit(px,%)',
26 | padding: 'unit(px,%){1,4}',
27 | 'text-decoration': 'string',
28 | 'text-transform': 'string',
29 | }
30 |
31 | static defaultAttributes = {
32 | color: '#000000',
33 | 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif',
34 | 'font-size': '13px',
35 | 'font-weight': 'normal',
36 | 'line-height': '22px',
37 | padding: '15px 10px',
38 | target: '_blank',
39 | 'text-decoration': 'none',
40 | 'text-transform': 'uppercase',
41 | }
42 |
43 | getStyles() {
44 | return {
45 | a: {
46 | display: 'inline-block',
47 | color: this.getAttribute('color'),
48 | 'font-family': this.getAttribute('font-family'),
49 | 'font-size': this.getAttribute('font-size'),
50 | 'font-style': this.getAttribute('font-style'),
51 | 'font-weight': this.getAttribute('font-weight'),
52 | 'letter-spacing': this.getAttribute('letter-spacing'),
53 | 'line-height': this.getAttribute('line-height'),
54 | 'text-decoration': this.getAttribute('text-decoration'),
55 | 'text-transform': this.getAttribute('text-transform'),
56 | padding: this.getAttribute('padding'),
57 | 'padding-top': this.getAttribute('padding-top'),
58 | 'padding-left': this.getAttribute('padding-left'),
59 | 'padding-right': this.getAttribute('padding-right'),
60 | 'padding-bottom': this.getAttribute('padding-bottom'),
61 | },
62 | td: {
63 | padding: this.getAttribute('padding'),
64 | 'padding-top': this.getAttribute('padding-top'),
65 | 'padding-left': this.getAttribute('padding-left'),
66 | 'padding-right': this.getAttribute('padding-right'),
67 | 'padding-bottom': this.getAttribute('padding-bottom'),
68 | },
69 | }
70 | }
71 |
72 | renderContent() {
73 | const href = this.getAttribute('href')
74 | const navbarBaseUrl = this.getAttribute('navbarBaseUrl')
75 | const link = navbarBaseUrl ? `${navbarBaseUrl}${href}` : href
76 |
77 | const cssClass = this.getAttribute('css-class')
78 | ? ` ${this.getAttribute('css-class')}`
79 | : ''
80 |
81 | return `
82 |
92 | ${this.getContent()}
93 |
94 | `
95 | }
96 |
97 | render() {
98 | return `
99 | ${conditionalTag(`
100 |
109 | `)}
110 | ${this.renderContent()}
111 | ${conditionalTag(`
112 |
113 | `)}
114 | `
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/packages/mjml-navbar/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar'
2 | export { default as NavbarLink } from './NavbarLink'
3 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-parser-xml",
3 | "description": "mjml-parser-xml",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-parser-xml"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward",
22 | "test": "node ./test/test.js"
23 | },
24 | "dependencies": {
25 | "@babel/runtime": "^7.23.9",
26 | "detect-node": "2.1.0",
27 | "htmlparser2": "^9.1.0",
28 | "lodash": "^4.17.21"
29 | },
30 | "devDependencies": {
31 | "@babel/cli": "^7.8.4",
32 | "chai": "^4.1.1",
33 | "mjml": "4.15.3",
34 | "mjml-core": "4.15.3",
35 | "rimraf": "^3.0.2"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/src/helpers/cleanNode.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 |
3 | export default function cleanNode(node) {
4 | delete node.parent
5 |
6 | // Delete children if needed
7 | if (node.children && node.children.length) {
8 | _.forEach(node.children, cleanNode)
9 | } else {
10 | delete node.children
11 | }
12 |
13 | // Delete attributes if needed
14 | if (node.attributes && Object.keys(node.attributes).length === 0) {
15 | delete node.attributes
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/src/helpers/convertBooleansOnAttrs.js:
--------------------------------------------------------------------------------
1 | import { mapValues } from 'lodash'
2 |
3 | /**
4 | * Convert "true" and "false" string attributes values
5 | * to corresponding Booleans
6 | */
7 |
8 | export default function convertBooleansOnAttrs(attrs) {
9 | return mapValues(attrs, (val) => {
10 | if (val === 'true') {
11 | return true
12 | }
13 | if (val === 'false') {
14 | return false
15 | }
16 |
17 | return val
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/src/helpers/setEmptyAttributes.js:
--------------------------------------------------------------------------------
1 | import { forEach } from 'lodash'
2 |
3 | export default function setEmptyAttributes(node) {
4 | if (!node.attributes) {
5 | node.attributes = {}
6 | }
7 | if (node.children) {
8 | forEach(node.children, setEmptyAttributes)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/test/incl.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 | COIN
4 | aze
5 |
6 |
7 | COIN2
8 | aze2
9 |
10 |
11 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/test/test-preprocessors.js:
--------------------------------------------------------------------------------
1 | const { template } = require('lodash')
2 | const MJMLParser = require('../lib')
3 | const mjml2html = require('../../mjml/lib')
4 | const { components } = require('../../mjml-core/lib')
5 |
6 | const parse = mjml =>
7 | MJMLParser(mjml, {
8 | keepComments: true,
9 | components,
10 | preprocessors: [
11 | data =>
12 | template(data, {
13 | evaluate: /{{([\s\S]+?)}}/g,
14 | interpolate: /{{=([\s\S]+?)}}/g,
15 | escape: /{{-([\s\S]+?)}}/g,
16 | })({
17 | buttons: [{ title: 'Title' }, { title: 'Title2' }],
18 | }),
19 | ],
20 | })
21 |
22 | const xml = `
23 |
24 |
25 | {{ buttons.forEach(function(button) { }}
26 | {{=button.title}}
27 | {{ }); }}
28 |
29 |
30 |
31 | `
32 |
33 | const json = parse(xml)
34 | const { html } = mjml2html(json)
35 |
36 | console.log(html) // eslint-disable-line no-console
37 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/test/test-utils.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 |
3 | function omitDeepLodash(input, props) {
4 | function omitDeepOnOwnProps(obj) {
5 | if (!_.isArray(obj) && !_.isObject(obj)) {
6 | return obj
7 | }
8 |
9 | if (_.isArray(obj)) {
10 | return omitDeepLodash(obj, props)
11 | }
12 |
13 | const o = {}
14 | _.forOwn(obj, (value, key) => {
15 | o[key] = omitDeepLodash(value, props)
16 | })
17 |
18 | return _.omit(o, props)
19 | }
20 |
21 | if (typeof input === "undefined") {
22 | return undefined
23 | }
24 |
25 | if (_.isArray(input)) {
26 | return input.map(omitDeepOnOwnProps)
27 | }
28 |
29 | return omitDeepOnOwnProps(input)
30 | }
31 |
32 | function deepDiff(object, base) {
33 | function changes(object, base) {
34 | return _.transform(object, (result, value, key) => {
35 | if (!_.isEqual(value, base[key])) {
36 | result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value
37 | }
38 | })
39 | }
40 | return changes(object, base)
41 | }
42 |
43 | function displayDiff(obj1, obj2) {
44 | const diffs = deepDiff(obj1, obj2)
45 | if (_.isEqual(diffs, {})) {
46 | console.log('\x1b[32m', 'Parsing test successful') // eslint-disable-line no-console
47 | console.log('\x1b[0m', '') // eslint-disable-line no-console
48 | } else {
49 | console.log('\x1b[31m', 'Parsing test failed. Differences found :') // eslint-disable-line no-console
50 | console.log('\x1b[0m', JSON.stringify(diffs, null, 2)) // eslint-disable-line no-console
51 | }
52 | }
53 |
54 | module.exports = {
55 | omitDeepLodash,
56 | displayDiff,
57 | }
58 |
--------------------------------------------------------------------------------
/packages/mjml-parser-xml/test/test.js:
--------------------------------------------------------------------------------
1 | const MJMLParser = require('../lib/index.js')
2 | require('mjml')
3 | const components = require('mjml-core').components
4 | const chai = require('chai')
5 | const displayDiff = require('./test-utils').displayDiff
6 | const omitDeepLodash = require('./test-utils').omitDeepLodash
7 | const testValues = require('./test-values')
8 |
9 | /*
10 | If test fails, run it with --debug to log the details of the diff
11 | */
12 |
13 | const parse = mjml => MJMLParser(mjml, {
14 | keepComments: true,
15 | components,
16 | filePath: '.'
17 | })
18 |
19 | testValues.forEach(testUnit => {
20 | const { test, mjml, validJson } = testUnit
21 |
22 | if (process.argv.indexOf('--debug') !== -1) {
23 | displayDiff(omitDeepLodash(validJson, 'file'), omitDeepLodash(parse(mjml), ['absoluteFilePath', 'file']))
24 | }
25 |
26 | chai.expect(omitDeepLodash(validJson, 'file'), `${test} test failed`)
27 | .to.deep.equal(omitDeepLodash(parse(mjml), ['absoluteFilePath', 'file']))
28 | })
29 |
--------------------------------------------------------------------------------
/packages/mjml-preset-core/README.md:
--------------------------------------------------------------------------------
1 | ## mjml-preset-core
2 |
3 | ### Installation
4 |
5 | ```bash
6 | npm install --save mjml-preset-core
7 | ```
8 |
9 | This is the set of mjml components bundled together for simple setup.
10 |
11 | ### Usage
12 |
13 | ```javascript
14 | import mjml2html from 'mjml-core'
15 | import presetCore from 'mjml-preset-core'
16 |
17 | console.log(mjml2html(`code`, { presets: [presetCore] }))
18 | ```
19 |
--------------------------------------------------------------------------------
/packages/mjml-preset-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-preset-core",
3 | "description": "mjml-preset-core",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-preset-core"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "mjml-accordion": "4.15.3",
26 | "mjml-body": "4.15.3",
27 | "mjml-button": "4.15.3",
28 | "mjml-carousel": "4.15.3",
29 | "mjml-column": "4.15.3",
30 | "mjml-divider": "4.15.3",
31 | "mjml-group": "4.15.3",
32 | "mjml-head": "4.15.3",
33 | "mjml-head-attributes": "4.15.3",
34 | "mjml-head-breakpoint": "4.15.3",
35 | "mjml-head-font": "4.15.3",
36 | "mjml-head-html-attributes": "4.15.3",
37 | "mjml-head-preview": "4.15.3",
38 | "mjml-head-style": "4.15.3",
39 | "mjml-head-title": "4.15.3",
40 | "mjml-hero": "4.15.3",
41 | "mjml-image": "4.15.3",
42 | "mjml-navbar": "4.15.3",
43 | "mjml-raw": "4.15.3",
44 | "mjml-section": "4.15.3",
45 | "mjml-social": "4.15.3",
46 | "mjml-spacer": "4.15.3",
47 | "mjml-table": "4.15.3",
48 | "mjml-text": "4.15.3",
49 | "mjml-wrapper": "4.15.3"
50 | },
51 | "devDependencies": {
52 | "@babel/cli": "^7.8.4",
53 | "rimraf": "^3.0.2"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/mjml-preset-core/src/dependencies.js:
--------------------------------------------------------------------------------
1 | export default {
2 | mjml: ['mj-body', 'mj-head', 'mj-raw'],
3 | 'mj-accordion': ['mj-accordion-element', 'mj-raw'],
4 | 'mj-accordion-element': ['mj-accordion-title', 'mj-accordion-text', 'mj-raw'],
5 | 'mj-accordion-title': [],
6 | 'mj-accordion-text': [],
7 | 'mj-attributes': [/^.*^/],
8 | 'mj-body': ['mj-raw', 'mj-section', 'mj-wrapper', 'mj-hero'],
9 | 'mj-button': [],
10 | 'mj-carousel': ['mj-carousel-image'],
11 | 'mj-carousel-image': [],
12 | 'mj-column': [
13 | 'mj-accordion',
14 | 'mj-button',
15 | 'mj-carousel',
16 | 'mj-divider',
17 | 'mj-image',
18 | 'mj-raw',
19 | 'mj-social',
20 | 'mj-spacer',
21 | 'mj-table',
22 | 'mj-text',
23 | 'mj-navbar',
24 | ],
25 | 'mj-html-attribute': [],
26 | 'mj-html-attributes': ['mj-selector'],
27 | 'mj-divider': [],
28 | 'mj-group': ['mj-column', 'mj-raw'],
29 | 'mj-head': [
30 | 'mj-attributes',
31 | 'mj-breakpoint',
32 | 'mj-html-attributes',
33 | 'mj-font',
34 | 'mj-preview',
35 | 'mj-style',
36 | 'mj-title',
37 | 'mj-raw',
38 | ],
39 | 'mj-hero': [
40 | 'mj-accordion',
41 | 'mj-button',
42 | 'mj-carousel',
43 | 'mj-divider',
44 | 'mj-image',
45 | 'mj-social',
46 | 'mj-spacer',
47 | 'mj-table',
48 | 'mj-text',
49 | 'mj-navbar',
50 | 'mj-raw',
51 | ],
52 | 'mj-image': [],
53 | 'mj-navbar': ['mj-navbar-link', 'mj-raw'],
54 | 'mj-raw': [],
55 | 'mj-section': ['mj-column', 'mj-group', 'mj-raw'],
56 | 'mj-selector': ['mj-html-attribute'],
57 | 'mj-social': ['mj-social-element', 'mj-raw'],
58 | 'mj-social-element': [],
59 | 'mj-spacer': [],
60 | 'mj-table': [],
61 | 'mj-text': [],
62 | 'mj-wrapper': ['mj-hero', 'mj-raw', 'mj-section'],
63 | }
64 |
--------------------------------------------------------------------------------
/packages/mjml-preset-core/src/index.js:
--------------------------------------------------------------------------------
1 | import { Social, SocialElement } from 'mjml-social'
2 | import { Navbar, NavbarLink } from 'mjml-navbar'
3 | import { Carousel, CarouselImage } from 'mjml-carousel'
4 | import {
5 | Accordion,
6 | AccordionElement,
7 | AccordionText,
8 | AccordionTitle,
9 | } from 'mjml-accordion'
10 | import Body from 'mjml-body'
11 | import Head from 'mjml-head'
12 | import HeadAttributes from 'mjml-head-attributes'
13 | import HeadBreakpoint from 'mjml-head-breakpoint'
14 | import HeadHtmlAttributes from 'mjml-head-html-attributes'
15 | import HeadFont from 'mjml-head-font'
16 | import HeadPreview from 'mjml-head-preview'
17 | import HeadStyle from 'mjml-head-style'
18 | import HeadTitle from 'mjml-head-title'
19 | import Hero from 'mjml-hero'
20 | import Button from 'mjml-button'
21 | import Column from 'mjml-column'
22 | import Divider from 'mjml-divider'
23 | import Group from 'mjml-group'
24 | import Image from 'mjml-image'
25 | import Raw from 'mjml-raw'
26 | import Section from 'mjml-section'
27 | import Spacer from 'mjml-spacer'
28 | import Text from 'mjml-text'
29 | import Table from 'mjml-table'
30 | import Wrapper from 'mjml-wrapper'
31 | import dependencies from './dependencies'
32 |
33 | const components = [
34 | Body,
35 | Head,
36 | HeadAttributes,
37 | HeadBreakpoint,
38 | HeadHtmlAttributes,
39 | HeadFont,
40 | HeadPreview,
41 | HeadStyle,
42 | HeadTitle,
43 | Hero,
44 | Button,
45 | Column,
46 | Divider,
47 | Group,
48 | Image,
49 |
50 | Raw,
51 | Section,
52 | Spacer,
53 | Text,
54 | Table,
55 | Wrapper,
56 |
57 | Social,
58 | SocialElement,
59 | Navbar,
60 | NavbarLink,
61 | Accordion,
62 | AccordionElement,
63 | AccordionText,
64 | AccordionTitle,
65 | Carousel,
66 | CarouselImage,
67 | ]
68 |
69 | const presetCore = {
70 | components,
71 | dependencies,
72 | }
73 |
74 | export default presetCore
75 |
--------------------------------------------------------------------------------
/packages/mjml-raw/README.md:
--------------------------------------------------------------------------------
1 | ## mj-raw
2 |
3 | Displays raw HTML that is not going to be parsed by the MJML engine. Anything left inside this tag should be raw, responsive HTML.
4 | If placed inside ``, its content will be added at the end of the ``.
5 |
6 | ```xml
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ```
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | If you use mj-raw to add templating language, and use the `minify` option, you might get a `Parsing error`, especially when using the `<` character. You can tell the minifier to ignore some content by wrapping it between two `` tags.
24 |
25 |
26 |
27 | `mj-raw` is an "ending tag", which means it can contain HTML code which will be left as it is, so it can contain HTML tags with attributes, but it cannot contain other MJML components. More information about ending tags in this section .
28 |
29 |
30 | ```xml
31 |
32 |
33 |
34 | {% if foo < 5 %}
35 |
36 |
37 |
38 | {% endif %}
39 |
40 |
41 |
42 | ```
43 |
44 | One more possible use of mj-raw is to add text at the beginning of the generated html, before the `` line. For this you need to :
45 | - put the mj-raw inside the `` tag, outside of `mj-head` and `mj-body`
46 | - add this attribute on this mj-raw : `position="file-start"`
47 |
48 | Note that if you put multiple lines in this mj-raw and use the minify option, these lines will be joined into a single line by the minifier. To prevent this you can wrap the content in `` tags as explained above.
49 |
50 | ```xml
51 |
52 | This will be added at the beginning of the file
53 |
54 |
55 |
56 |
57 | ```
58 |
--------------------------------------------------------------------------------
/packages/mjml-raw/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-raw",
3 | "description": "mjml-raw",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-raw"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-raw/src/index.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 |
3 | export default class MjRaw extends BodyComponent {
4 | static componentName = 'mj-raw'
5 |
6 | static endingTag = true
7 |
8 | static rawElement = true
9 |
10 | static allowedAttributes = {
11 | position: 'enum(file-start)',
12 | }
13 |
14 | render() {
15 | return this.getContent()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/mjml-section/README.md:
--------------------------------------------------------------------------------
1 | ## mj-section
2 |
3 | Sections are intended to be used as rows within your email.
4 | They will be used to structure the layout.
5 |
6 | ```xml
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ```
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | The `full-width` property will be used to manage the background width.
23 | By default, it will be 600px. With the `full-width` property on, it will be
24 | changed to 100%.
25 |
26 |
27 | Inverting the order in which columns display: set the `direction` attribute to `rtl` to change the order in which columns display on desktop. Because MJML is mobile-first, structure the columns in the order you want them to stack on mobile , and use `direction` to change the order they display on desktop .
28 |
29 |
30 |
31 | Sections cannot nest in sections. Columns can nest in sections; all content must be in a column.
32 |
33 |
34 | attribute | unit | description | default value
35 | ----------------------|-------------|--------------------------------|---------------
36 | background-color | color | section color | n/a
37 | background-position | percent / 'left','top',... (2 values max) | css background position (see outlook limitations below) | top center
38 | background-position-x | percent / keyword | css background position x | none
39 | background-position-y | percent / keyword | css background position y | none
40 | background-repeat | string | css background repeat | repeat
41 | background-size | px/percent/'cover'/'contain' | css background size | auto
42 | background-url | url | background url | n/a
43 | border | string | css border format | none
44 | border-bottom | string | css border format | n/a
45 | border-left | string | css border format | n/a
46 | border-radius | px | border radius | n/a
47 | border-right | string | css border format | n/a
48 | border-top | string | css border format | n/a
49 | css-class | string | class name, added to the root HTML element created | n/a
50 | direction | ltr / rtl | set the display order of direct children | ltr
51 | full-width | string | make the section full-width | n/a
52 | padding | px | supports up to 4 parameters | 20px 0
53 | padding-bottom | px | section bottom offset | n/a
54 | padding-left | px | section left offset | n/a
55 | padding-right | px | section right offset | n/a
56 | padding-top | px | section top offset | n/a
57 | text-align | string | css text-align | center
58 |
59 |
60 |
61 | Limitations of background-images size and position on Outlook desktop :
62 | - If background-size is not specified, no-repeat will be ignored on Outlook.
63 | - If the specified size is a single attribute in percent, the height will be auto as in standard css. On outlook, the image will never overflow the element, and it will be shrinked instead of being cropped like on other clients.
64 |
65 |
--------------------------------------------------------------------------------
/packages/mjml-section/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-section",
3 | "description": "mjml-section",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-section"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-social/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-social",
3 | "description": "mjml-social",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-social"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-social/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Social } from './Social'
2 | export { default as SocialElement } from './SocialElement'
3 |
--------------------------------------------------------------------------------
/packages/mjml-spacer/README.md:
--------------------------------------------------------------------------------
1 | ## mj-spacer
2 |
3 | Displays a blank space.
4 |
5 | ```xml
6 |
7 |
8 |
9 |
10 | A first line of text
11 |
12 | A second line of text
13 |
14 |
15 |
16 |
17 | ```
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | attribute | unit | description | default value
26 | ----------------------------|-------------|--------------------------------|------------------------------
27 | container-background-color | color | inner element background color | n/a
28 | css-class | string | class name, added to the root HTML element created | n/a
29 | height | px | spacer height | 20px
30 | padding | px | supports up to 4 parameters | none
31 | padding-bottom | px | bottom offset | n/a
32 | padding-left | px | left offset | n/a
33 | padding-right | px | right offset | n/a
34 | padding-top | px | top offset | n/a
35 |
--------------------------------------------------------------------------------
/packages/mjml-spacer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-spacer",
3 | "description": "mjml-spacer",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-spacer"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-spacer/src/index.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 |
3 | export default class MjSpacer extends BodyComponent {
4 | static componentName = 'mj-spacer'
5 |
6 | static allowedAttributes = {
7 | border: 'string',
8 | 'border-bottom': 'string',
9 | 'border-left': 'string',
10 | 'border-right': 'string',
11 | 'border-top': 'string',
12 | 'container-background-color': 'color',
13 | 'padding-bottom': 'unit(px,%)',
14 | 'padding-left': 'unit(px,%)',
15 | 'padding-right': 'unit(px,%)',
16 | 'padding-top': 'unit(px,%)',
17 | padding: 'unit(px,%){1,4}',
18 | height: 'unit(px,%)',
19 | }
20 |
21 | static defaultAttributes = {
22 | height: '20px',
23 | }
24 |
25 | getStyles() {
26 | return {
27 | div: {
28 | height: this.getAttribute('height'),
29 | 'line-height': this.getAttribute('height'),
30 | },
31 | }
32 | }
33 |
34 | render() {
35 | return `
36 |
41 | `
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/mjml-table/README.md:
--------------------------------------------------------------------------------
1 | ## mj-table
2 |
3 | This tag allows you to display table and filled it with data. It only accepts plain HTML.
4 |
5 | ```xml
6 |
7 |
8 |
9 |
10 |
11 |
12 | Year
13 | Language
14 | Inspired from
15 |
16 |
17 | 1995
18 | PHP
19 | C, Shell Unix
20 |
21 |
22 | 1995
23 | JavaScript
24 | Scheme, Self
25 |
26 |
27 |
28 |
29 |
30 |
31 | ```
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | `mj-table` is an "ending tag", which means it can contain HTML code which will be left as it is, so it can contain HTML tags with attributes, but it cannot contain other MJML components. In `mj-table` specifically, you can put anything you would put in a `<table>` element. More information about ending tags in this section .
41 |
42 |
43 |
44 | attribute | unit | description | default value
45 | ----------------------------|-----------------------------|------------------------------- |--------------
46 | align | left/right/center | self horizontal alignment | left
47 | border | border | table external border | none
48 | cellpadding | pixels | space between cells | n/a
49 | cellspacing | pixels | space between cell and border | n/a
50 | color | color | text header & footer color | #000000
51 | container-background-color | color | inner element background color | n/a
52 | css-class | string | class name, added to the root HTML element created | n/a
53 | font-family | string | font name | Ubuntu, Helvetica, Arial, sans-serif
54 | font-size | px | font size | 13px
55 | line-height | percent/px | space between lines | 22px
56 | padding | percent/px | supports up to 4 parameters | 10px 25px
57 | padding-bottom | percent/px | bottom offset | n/a
58 | padding-left | percent/px | left offset | n/a
59 | padding-right | percent/px | right offset | n/a
60 | padding-top | percent/px | top offset | n/a
61 | role | none/presentation | specify the role attribute | n/a
62 | table-layout | auto/fixed/initial/inherit | sets the table layout. | auto
63 | width | percent/px | table width | 100%
64 |
--------------------------------------------------------------------------------
/packages/mjml-table/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-table",
3 | "description": "mjml-atable",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-table"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-table/src/index.js:
--------------------------------------------------------------------------------
1 | import widthParser from 'mjml-core/lib/helpers/widthParser'
2 |
3 | import { BodyComponent } from 'mjml-core'
4 | import { reduce } from 'lodash'
5 |
6 | export default class MjTable extends BodyComponent {
7 | static componentName = 'mj-table'
8 |
9 | static endingTag = true
10 |
11 | static allowedAttributes = {
12 | align: 'enum(left,right,center)',
13 | border: 'string',
14 | cellpadding: 'integer',
15 | cellspacing: 'integer',
16 | 'container-background-color': 'color',
17 | color: 'color',
18 | 'font-family': 'string',
19 | 'font-size': 'unit(px)',
20 | 'font-weight': 'string',
21 | 'line-height': 'unit(px,%,)',
22 | 'padding-bottom': 'unit(px,%)',
23 | 'padding-left': 'unit(px,%)',
24 | 'padding-right': 'unit(px,%)',
25 | 'padding-top': 'unit(px,%)',
26 | padding: 'unit(px,%){1,4}',
27 | role: 'enum(none,presentation)',
28 | 'table-layout': 'enum(auto,fixed,initial,inherit)',
29 | 'vertical-align': 'enum(top,bottom,middle)',
30 | width: 'unit(px,%)',
31 | }
32 |
33 | static defaultAttributes = {
34 | align: 'left',
35 | border: 'none',
36 | cellpadding: '0',
37 | cellspacing: '0',
38 | color: '#000000',
39 | 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif',
40 | 'font-size': '13px',
41 | 'line-height': '22px',
42 | padding: '10px 25px',
43 | 'table-layout': 'auto',
44 | width: '100%',
45 | }
46 |
47 | getStyles() {
48 | return {
49 | table: {
50 | color: this.getAttribute('color'),
51 | 'font-family': this.getAttribute('font-family'),
52 | 'font-size': this.getAttribute('font-size'),
53 | 'line-height': this.getAttribute('line-height'),
54 | 'table-layout': this.getAttribute('table-layout'),
55 | width: this.getAttribute('width'),
56 | border: this.getAttribute('border'),
57 | },
58 | }
59 | }
60 |
61 | getWidth() {
62 | const width = this.getAttribute('width')
63 | const { parsedWidth, unit } = widthParser(width)
64 |
65 | return unit === '%' ? width : parsedWidth
66 | }
67 |
68 | render() {
69 | const tableAttributes = reduce(
70 | ['cellpadding', 'cellspacing', 'role'],
71 | (acc, v) => ({
72 | ...acc,
73 | [v]: this.getAttribute(v),
74 | }),
75 | {},
76 | )
77 |
78 | return `
79 |
87 | ${this.getContent()}
88 |
89 | `
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/packages/mjml-text/README.md:
--------------------------------------------------------------------------------
1 | ## mj-text
2 |
3 | This tag allows you to display text and HTML in your email.
4 |
5 | ```xml
6 |
7 |
8 |
9 |
10 |
11 | Title
12 |
13 | Paragraph
14 | Another paragraph
15 |
16 |
17 |
18 |
19 |
20 | ```
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | `mj-text` is an "ending tag", which means it can contain HTML code which will be left as it is, so it can contain HTML tags with attributes, but it cannot contain other MJML components. More information about ending tags in this section .
30 |
31 |
32 |
33 | attribute | unit | description | default value
34 | ------------------------------|---------------|---------------------------------------------|-------------------------------------
35 | color | color | text color | #000000
36 | font-family | string | font | Ubuntu, Helvetica, Arial, sans-serif
37 | font-size | px | text size | 13px
38 | font-style | string | normal/italic/oblique | n/a
39 | font-weight | number | text thickness | n/a
40 | line-height | px | space between the lines | 1
41 | letter-spacing | px,em | letter spacing | none
42 | height | px | The height of the element | n/a
43 | text-decoration | string | underline/overline/line-through/none | n/a
44 | text-transform | string | uppercase/lowercase/capitalize | n/a
45 | align | string | left/right/center/justify | left
46 | container-background-color | color | inner element background color | n/a
47 | padding | px | supports up to 4 parameters | 10px 25px
48 | padding-top | px | top offset | n/a
49 | padding-bottom | px | bottom offset | n/a
50 | padding-left | px | left offset | n/a
51 | padding-right | px | right offset | n/a
52 | css-class | string | class name, added to the root HTML element created | n/a
53 |
--------------------------------------------------------------------------------
/packages/mjml-text/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-text",
3 | "description": "mjml-text",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-text"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3"
27 | },
28 | "devDependencies": {
29 | "@babel/cli": "^7.8.4",
30 | "rimraf": "^3.0.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/mjml-text/src/index.js:
--------------------------------------------------------------------------------
1 | import { BodyComponent } from 'mjml-core'
2 |
3 | import conditionalTag from 'mjml-core/lib/helpers/conditionalTag'
4 |
5 | export default class MjText extends BodyComponent {
6 | static componentName = 'mj-text'
7 |
8 | static endingTag = true
9 |
10 | static allowedAttributes = {
11 | align: 'enum(left,right,center,justify)',
12 | 'background-color': 'color',
13 | color: 'color',
14 | 'container-background-color': 'color',
15 | 'font-family': 'string',
16 | 'font-size': 'unit(px)',
17 | 'font-style': 'string',
18 | 'font-weight': 'string',
19 | height: 'unit(px,%)',
20 | 'letter-spacing': 'unitWithNegative(px,em)',
21 | 'line-height': 'unit(px,%,)',
22 | 'padding-bottom': 'unit(px,%)',
23 | 'padding-left': 'unit(px,%)',
24 | 'padding-right': 'unit(px,%)',
25 | 'padding-top': 'unit(px,%)',
26 | padding: 'unit(px,%){1,4}',
27 | 'text-decoration': 'string',
28 | 'text-transform': 'string',
29 | 'vertical-align': 'enum(top,bottom,middle)',
30 | }
31 |
32 | static defaultAttributes = {
33 | align: 'left',
34 | color: '#000000',
35 | 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif',
36 | 'font-size': '13px',
37 | 'line-height': '1',
38 | padding: '10px 25px',
39 | }
40 |
41 | getStyles() {
42 | return {
43 | text: {
44 | 'font-family': this.getAttribute('font-family'),
45 | 'font-size': this.getAttribute('font-size'),
46 | 'font-style': this.getAttribute('font-style'),
47 | 'font-weight': this.getAttribute('font-weight'),
48 | 'letter-spacing': this.getAttribute('letter-spacing'),
49 | 'line-height': this.getAttribute('line-height'),
50 | 'text-align': this.getAttribute('align'),
51 | 'text-decoration': this.getAttribute('text-decoration'),
52 | 'text-transform': this.getAttribute('text-transform'),
53 | color: this.getAttribute('color'),
54 | height: this.getAttribute('height'),
55 | },
56 | }
57 | }
58 |
59 | renderContent() {
60 | return `
61 | ${this.getContent()}
66 | `
67 | }
68 |
69 | render() {
70 | const height = this.getAttribute('height')
71 |
72 | return height
73 | ? `
74 | ${conditionalTag(`
75 |
76 | `)}
77 | ${this.renderContent()}
78 | ${conditionalTag(`
79 |
80 | `)}
81 | `
82 | : this.renderContent()
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/mjml-validator/README.md:
--------------------------------------------------------------------------------
1 | # Validating MJML
2 |
3 | MJML provides a validation layer that helps you building your email. It can detect if you misplaced or mispelled a MJML component, or if you used any unauthorised attribute on a specific component. It supports 3 levels of validation:
4 |
5 | * `skip`: your document is rendered without going through validation
6 | * `soft`: your document is going through validation and is rendered, even if it has errors
7 | * `strict`: your document is going through validation and is not rendered if it has any error
8 |
9 | By default, the level is set to `soft`.
10 |
11 | ## In CLI
12 |
13 | When using the `mjml` command line, you can add the option `-c.validationLevel` or `--config.validationLevel` with the validation level you want.
14 |
15 | > Set the validation level to `skip` (so that the file is not validated) and render the file
16 |
17 | ```bash
18 | mjml --config.validationLevel=skip template.mjml
19 | ```
20 |
21 | Alternatively, you can just validate file without rendering it by add ing the `--validate` option
22 |
23 | ```bash
24 | mjml --validate template.mjml
25 | ```
26 |
27 | ## In Javascript
28 |
29 | In Javascript, you can provide the level through the `options` parameters on `mjml2html`. Ex: `mjml2html(inputMJML, { validationLevel: 'strict' })`
30 |
31 | `strict` will raise a `MJMLValidationError` exception. This object has 2 methods:
32 | * `getErrors` returns an array of objects with `line`, `message`, `tagName` as well as a `formattedMessage` which contains the `line`, `message` and `tagName` concatenated in a sentence.
33 | * `getMessages` returns an array of `formattedMessage`.
34 |
35 | When using `soft`, no exception will be raised. You can get the errors in the object returned by `mjml2html`. It is the same object returned by `getErrors` on strict mode.
36 |
37 |
--------------------------------------------------------------------------------
/packages/mjml-validator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-validator",
3 | "description": "mjml-validator",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-validator"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9"
25 | },
26 | "devDependencies": {
27 | "@babel/cli": "^7.8.4",
28 | "rimraf": "^3.0.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/MJMLRulesCollection.js:
--------------------------------------------------------------------------------
1 | import validAttributes from './rules/validAttributes'
2 | import validChildren from './rules/validChildren'
3 | import validTag from './rules/validTag'
4 | import validTypes from './rules/validTypes'
5 | import errorAttr from './rules/errorAttr'
6 |
7 | const MJMLRulesCollection = {
8 | validAttributes,
9 | validChildren,
10 | validTag,
11 | validTypes,
12 | errorAttr,
13 | }
14 |
15 | export function registerRule(rule, name) {
16 | if (typeof rule !== 'function') {
17 | return console.error('Your rule must be a function')
18 | }
19 |
20 | if (name) {
21 | MJMLRulesCollection[name] = rule
22 | } else {
23 | MJMLRulesCollection[rule.name] = rule
24 | }
25 |
26 | return true
27 | }
28 |
29 | export default MJMLRulesCollection
30 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/dependencies.js:
--------------------------------------------------------------------------------
1 | export const assignDependencies = (target, ...sources) => {
2 | if (sources.length === 0) {
3 | return target
4 | }
5 |
6 | for (const source of sources) {
7 | if (typeof source === 'object' && source !== null) {
8 | for (const tag of Object.keys(source)) {
9 | if (typeof tag === 'string') {
10 | const list = []
11 | if (target[tag]) {
12 | list.push(...target[tag])
13 | }
14 | if (source[tag]) {
15 | list.push(...source[tag])
16 | }
17 | target[tag] = Array.from(new Set(list))
18 | } else {
19 | console.warn('dependency "tag" must be of type string')
20 | }
21 | }
22 | } else {
23 | console.warn('"dependencies" must be an object.')
24 | }
25 | }
26 | return target
27 | }
28 |
29 | const dependencies = {}
30 |
31 | export const registerDependencies = (dep) => {
32 | assignDependencies(dependencies, dep)
33 | }
34 |
35 | export default dependencies
36 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/index.js:
--------------------------------------------------------------------------------
1 | import ruleError from './rules/ruleError'
2 | import rulesCollection, { registerRule } from './MJMLRulesCollection'
3 | import dependencies, {
4 | registerDependencies,
5 | assignDependencies,
6 | } from './dependencies'
7 |
8 | const SKIP_ELEMENTS = ['mjml']
9 |
10 | export const formatValidationError = ruleError
11 |
12 | export { rulesCollection, registerRule }
13 |
14 | export { dependencies, registerDependencies, assignDependencies }
15 |
16 | export default function MJMLValidator(element, options = {}) {
17 | const { children, tagName } = element
18 | const errors = []
19 |
20 | const skipElements = options.skipElements || SKIP_ELEMENTS
21 |
22 | if (options.dependencies == null) {
23 | console.warn('"dependencies" option should be provided to mjml validator')
24 | }
25 |
26 | if (!skipElements.includes(tagName)) {
27 | for (const rule of Object.values(rulesCollection)) {
28 | const ruleError = rule(element, {
29 | dependencies,
30 | skipElements,
31 | ...options,
32 | })
33 | if (Array.isArray(ruleError)) {
34 | errors.push(...ruleError)
35 | } else if (ruleError) {
36 | errors.push(ruleError)
37 | }
38 | }
39 | }
40 |
41 | if (children && children.length > 0) {
42 | for (const child of children) {
43 | errors.push(...MJMLValidator(child, options))
44 | }
45 | }
46 |
47 | return errors
48 | }
49 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/rules/errorAttr.js:
--------------------------------------------------------------------------------
1 | import ruleError from './ruleError'
2 |
3 | export default function errorAttr(element) {
4 | const { errors } = element
5 |
6 | if (!errors) return null
7 |
8 | return errors.map((error) => {
9 | switch (error.type) {
10 | case 'include': {
11 | const { file, partialPath } = error.params
12 |
13 | return ruleError(
14 | `mj-include fails to read file : ${file} at ${partialPath}`,
15 | element,
16 | )
17 | }
18 | default:
19 | return null
20 | }
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/rules/ruleError.js:
--------------------------------------------------------------------------------
1 | function formatInclude(element) {
2 | const { includedIn } = element
3 | if (!(includedIn && includedIn.length)) return ''
4 |
5 | const formattedIncluded = includedIn
6 | .slice()
7 | .reverse()
8 | .map(({ line, file }) => `line ${line} of file ${file}`)
9 | .join(', itself included at ')
10 |
11 | return `, included at ${formattedIncluded}`
12 | }
13 |
14 | export default function ruleError(message, element) {
15 | const { line, tagName, absoluteFilePath } = element
16 |
17 | return {
18 | line,
19 | message,
20 | tagName,
21 | formattedMessage: `Line ${line} of ${absoluteFilePath}${formatInclude(
22 | element,
23 | )} (${tagName}) — ${message}`,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/rules/validAttributes.js:
--------------------------------------------------------------------------------
1 | import ruleError from './ruleError'
2 |
3 | const WHITELIST = ['mj-class', 'css-class']
4 |
5 | export default function validateAttribute(element, { components }) {
6 | const { attributes, tagName } = element
7 |
8 | const Component = components[tagName]
9 |
10 | if (!Component) {
11 | return null
12 | }
13 |
14 | const availableAttributes = [
15 | ...Object.keys(Component.allowedAttributes || {}),
16 | ...WHITELIST,
17 | ]
18 | const unknownAttributes = Object.keys(attributes || {}).filter(
19 | (attribute) => !availableAttributes.includes(attribute),
20 | )
21 |
22 | if (unknownAttributes.length === 0) {
23 | return null
24 | }
25 |
26 | const { attribute, illegal } = {
27 | attribute: unknownAttributes.length > 1 ? 'Attributes' : 'Attribute',
28 | illegal: unknownAttributes.length > 1 ? 'are illegal' : 'is illegal',
29 | }
30 |
31 | return ruleError(
32 | `${attribute} ${unknownAttributes.join(', ')} ${illegal}`,
33 | element,
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/rules/validChildren.js:
--------------------------------------------------------------------------------
1 | import ruleError from './ruleError'
2 |
3 | export default function validChildren(
4 | element,
5 | { components, dependencies, skipElements },
6 | ) {
7 | const { children, tagName } = element
8 |
9 | const Component = components[tagName]
10 |
11 | if (!Component || !children || !children.length) {
12 | return null
13 | }
14 |
15 | const errors = []
16 |
17 | for (const child of children) {
18 | const childTagName = child.tagName
19 | const ChildComponent = components[childTagName]
20 | const parentDependencies = dependencies[tagName] || []
21 |
22 | const childIsValid =
23 | !ChildComponent ||
24 | skipElements.includes(childTagName) ||
25 | parentDependencies.includes(childTagName) ||
26 | parentDependencies.some(
27 | (dep) => dep instanceof RegExp && dep.test(childTagName),
28 | )
29 |
30 | if (childIsValid === false) {
31 | const allowedDependencies = Object.keys(dependencies).filter(
32 | (key) =>
33 | dependencies[key].includes(childTagName) ||
34 | dependencies[key].some(
35 | (dep) => dep instanceof RegExp && dep.test(childTagName),
36 | ),
37 | )
38 |
39 | errors.push(
40 | ruleError(
41 | `${childTagName} cannot be used inside ${tagName}, only inside: ${allowedDependencies.join(
42 | ', ',
43 | )}`,
44 | child,
45 | ),
46 | )
47 | }
48 | }
49 |
50 | return errors
51 | }
52 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/rules/validTag.js:
--------------------------------------------------------------------------------
1 | import ruleError from './ruleError'
2 |
3 | // Tags that have no associated components but are allowed even so
4 | const componentLessTags = [
5 | 'mj-all',
6 | 'mj-class',
7 | 'mj-selector',
8 | 'mj-html-attribute',
9 | ]
10 |
11 | export default function validateTag(element, { components }) {
12 | const { tagName } = element
13 |
14 | if (componentLessTags.includes(tagName)) return null
15 |
16 | const Component = components[tagName]
17 |
18 | if (!Component) {
19 | return ruleError(
20 | `Element ${tagName} doesn't exist or is not registered`,
21 | element,
22 | )
23 | }
24 |
25 | return null
26 | }
27 |
--------------------------------------------------------------------------------
/packages/mjml-validator/src/rules/validTypes.js:
--------------------------------------------------------------------------------
1 | import ruleError from './ruleError'
2 |
3 | export default function validateType(element, { components, initializeType }) {
4 | const { attributes, tagName } = element
5 |
6 | const Component = components[tagName]
7 |
8 | if (!Component) {
9 | return null
10 | }
11 |
12 | const errors = []
13 |
14 | for (const [attr, value] of Object.entries(attributes || {})) {
15 | const attrType =
16 | Component.allowedAttributes && Component.allowedAttributes[attr]
17 | if (attrType) {
18 | const TypeChecker = initializeType(attrType)
19 | const result = new TypeChecker(value)
20 | if (result.isValid() === false) {
21 | errors.push(
22 | ruleError(`Attribute ${attr} ${result.getErrorMessage()}`, element),
23 | )
24 | }
25 | }
26 | }
27 |
28 | return errors
29 | }
30 |
--------------------------------------------------------------------------------
/packages/mjml-wrapper/README.md:
--------------------------------------------------------------------------------
1 | ## mj-wrapper
2 |
3 |
4 |
5 |
6 |
7 | Wrapper enables to wrap multiple sections together. It's especially useful to achieve nested layouts with shared border or background images across sections.
8 |
9 | ```xml
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | First line of text
21 |
22 | Second line of text
23 |
24 |
25 |
26 |
27 |
28 | ```
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | The `full-width` property will be used to manage the background width.
37 | By default, it will be 600px. With the `full-width` property on, it will be
38 | changed to 100%.
39 |
40 |
41 | You can't nest a full-width section inside a full-width wrapper, section will act as a non-full-width section.
42 |
43 |
44 |
45 | If you're using a background-url on a `mj-wrapper` then do not add one into a section within the mj-wrapper. Outlook Desktop doesn't support nested VML, so it will most likely break your email.
46 | Also, if you use a background-color on mj-wrapper and a background-url on its section/hero child, the background-color will be over the background-image on Outlook desktop. There is no way to keep the vml image under the content and over the wrapper's background-color due to z-index being ignored on most tags.
47 |
48 |
49 |
50 | attribute | unit | description | default value
51 | --------------------|-------------|--------------------------------|---------------
52 | background-color | color | section color | n/a
53 | background-position | percent / 'left','top',... (2 values max) | css background position (see outlook limitations in mj-section doc) | top center
54 | background-position-x | percent / keyword | css background position x | none
55 | background-position-y | percent / keyword | css background position y | none
56 | background-repeat | string | css background repeat | repeat
57 | background-size | px/percent/'cover'/'contain' | css background size | auto
58 | background-url | url | background url | n/a
59 | border | string | css border format | none
60 | border-bottom | string | css border format | n/a
61 | border-left | string | css border format | n/a
62 | border-radius | px | border radius | n/a
63 | border-right | string | css border format | n/a
64 | border-top | string | css border format | n/a
65 | css-class | string | class name, added to the root HTML element created | n/a
66 | full-width | string | make the wrapper full-width | n/a
67 | padding | px | supports up to 4 parameters | 20px 0
68 | padding-bottom | px | section bottom offset | n/a
69 | padding-left | px | section left offset | n/a
70 | padding-right | px | section right offset | n/a
71 | padding-top | px | section top offset | n/a
72 | text-align | string | css text-align | center
73 |
--------------------------------------------------------------------------------
/packages/mjml-wrapper/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-wrapper",
3 | "description": "mjml-wrapper",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/mjmlio/mjml.git",
12 | "directory": "packages/mjml-wrapper"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/mjmlio/mjml/issues"
17 | },
18 | "homepage": "https://mjml.io",
19 | "scripts": {
20 | "clean": "rimraf lib",
21 | "build": "babel src --out-dir lib --root-mode upward"
22 | },
23 | "dependencies": {
24 | "@babel/runtime": "^7.23.9",
25 | "lodash": "^4.17.21",
26 | "mjml-core": "4.15.3",
27 | "mjml-section": "4.15.3"
28 | },
29 | "devDependencies": {
30 | "@babel/cli": "^7.8.4",
31 | "rimraf": "^3.0.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/mjml-wrapper/src/index.js:
--------------------------------------------------------------------------------
1 | import MjSection from 'mjml-section'
2 | import { suffixCssClasses } from 'mjml-core'
3 |
4 | export default class MjWrapper extends MjSection {
5 | static componentName = 'mj-wrapper'
6 |
7 | renderWrappedChildren() {
8 | const { children } = this.props
9 | const { containerWidth } = this.context
10 |
11 | return `
12 | ${this.renderChildren(children, {
13 | renderer: (component) =>
14 | component.constructor.isRawElement()
15 | ? component.render()
16 | : `
17 |
30 | ${component.render()}
31 |
35 | `,
36 | })}
37 | `
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/mjml/bin/mjml:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('../lib/index')
4 | require('mjml-cli')
5 |
--------------------------------------------------------------------------------
/packages/mjml/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml",
3 | "description": "MJML: the only framework that makes responsive-email easy",
4 | "version": "4.15.3",
5 | "main": "lib/index.js",
6 | "bin": {
7 | "mjml": "bin/mjml"
8 | },
9 | "files": [
10 | "bin",
11 | "lib"
12 | ],
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/mjmlio/mjml.git",
16 | "directory": "packages/mjml"
17 | },
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/mjmlio/mjml/issues"
21 | },
22 | "homepage": "https://mjml.io",
23 | "scripts": {
24 | "clean": "rimraf lib",
25 | "build": "babel src --out-dir lib --root-mode upward",
26 | "test": "node ./test/index.js"
27 | },
28 | "dependencies": {
29 | "@babel/runtime": "^7.23.9",
30 | "mjml-cli": "4.15.3",
31 | "mjml-core": "4.15.3",
32 | "mjml-migrate": "4.15.3",
33 | "mjml-preset-core": "4.15.3",
34 | "mjml-validator": "4.15.3"
35 | },
36 | "devDependencies": {
37 | "@babel/cli": "^7.8.4",
38 | "chai": "^4.1.1",
39 | "chai-spies": "^1.0.0",
40 | "cheerio": "1.0.0-rc.12",
41 | "lodash": "^4.17.21",
42 | "rimraf": "^3.0.2"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/mjml/src/index.js:
--------------------------------------------------------------------------------
1 | import mjml2html, { components, assignComponents } from 'mjml-core'
2 | import { dependencies, assignDependencies } from 'mjml-validator'
3 | import presetCore from 'mjml-preset-core'
4 |
5 | assignComponents(components, presetCore.components)
6 | assignDependencies(dependencies, presetCore.dependencies)
7 |
8 | export default mjml2html
9 |
--------------------------------------------------------------------------------
/packages/mjml/test/html-attributes.test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai')
2 | const { load } = require('cheerio')
3 | const { sortBy } = require('lodash')
4 | const mjml = require('../lib')
5 |
6 | const input = `
7 |
8 |
9 |
10 |
11 | 42
12 |
13 |
14 | 43
15 |
16 |
17 |
18 |
19 | { if item < 5 }
20 |
21 |
22 | { if item > 10 }
23 |
24 | Hello World! { item }
25 |
26 | { end if }
27 |
28 | Hello World! { item + 1 }
29 |
30 |
31 |
32 |
33 | { end if }
34 |
35 |
36 | `
37 |
38 | const { html } = mjml(input)
39 | const $ = load(html)
40 |
41 | // should put the attributes at the right place
42 | chai
43 | .expect(
44 | $('.text div')
45 | .map(function getAttr() {
46 | return $(this).attr('data-id')
47 | })
48 | .get(),
49 | 'Custom attributes added on texts',
50 | )
51 | .to.eql(['42', '42'])
52 |
53 | chai
54 | .expect(
55 | $('.image td')
56 | .map(function getAttr() {
57 | return $(this).attr('data-name')
58 | })
59 | .get(),
60 | 'Custom attributes added on image',
61 | )
62 | .to.eql(['43'])
63 |
64 | // should not alter templating syntax, or move the content that is outside any tag (mj-raws)
65 | const expected = [
66 | '{ if item < 5 }',
67 | 'class="section"',
68 | '{ if item > 10 }',
69 | 'class="text"',
70 | '{ item }',
71 | '{ end if }',
72 | '{ item + 1 }',
73 | ]
74 | const indexes = expected.map((str) => html.indexOf(str))
75 |
76 | chai.expect(indexes, 'Templating syntax unaltered').to.not.include(-1)
77 |
78 | chai.expect(sortBy(indexes), 'Mj-raws kept same positions').to.deep.eql(indexes)
79 |
--------------------------------------------------------------------------------
/packages/mjml/test/index.js:
--------------------------------------------------------------------------------
1 | require('./html-attributes.test')
2 | require('./lazy-head-style.test')
3 |
--------------------------------------------------------------------------------
/packages/mjml/test/lazy-head-style.test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai')
2 | const spies = require('chai-spies')
3 | const mjml = require('../lib')
4 |
5 | chai.use(spies)
6 |
7 | const {
8 | HeadComponent,
9 | registerComponent,
10 | } = require('../../mjml-core/lib/index')
11 |
12 | const addStyle = chai.spy(
13 | (breakpoint) => `
14 | @media only screen and (max-width:${breakpoint}) {
15 | h1 {
16 | font-size: 20px;
17 | }
18 | }
19 | `,
20 | )
21 |
22 | class HeadComponentWithFunctionStyle extends HeadComponent {
23 | handler() {
24 | const { add } = this.context
25 | add('style', addStyle)
26 | }
27 | }
28 | HeadComponentWithFunctionStyle.componentName = 'mj-head-component-with-function-style'
29 | HeadComponentWithFunctionStyle.endingTag = true
30 | HeadComponentWithFunctionStyle.allowedAttributes = {}
31 |
32 |
33 | registerComponent(HeadComponentWithFunctionStyle)
34 |
35 | mjml(`
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | `)
45 |
46 | chai.expect(addStyle).to.have.been.called.with('300px')
47 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | require('@babel/register')
2 |
3 | const mjml2html = require('./packages/mjml/src/index')
4 |
5 | const xml = `
6 |
7 |
8 |
9 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem
24 |
25 |
26 |
27 |
28 |
29 |
30 | `
31 |
32 | console.time('mjml2html')
33 |
34 | const { html } = mjml2html(xml, {
35 | beautify: true,
36 | })
37 |
38 | console.timeEnd('mjml2html')
39 |
40 | if (process.argv.includes('--output')) {
41 | console.log(html)
42 | }
43 |
44 | if (process.argv.includes('--open')) {
45 | const open = require('open')
46 | const path = require('path')
47 | const fs = require('fs')
48 |
49 | const testFile = path.resolve(__dirname, './test.html')
50 |
51 | fs.writeFileSync(testFile, html)
52 |
53 | open(testFile)
54 | }
55 |
--------------------------------------------------------------------------------
/type.js:
--------------------------------------------------------------------------------
1 | const types = require('./packages/mjml-core/lib/types/type.js')
2 |
3 | const enumtype = types.initializeType('enum(top,left,center)')
4 | const colortype = types.initializeType('color')
5 | const booleantype = types.initializeType('boolean')
6 | const unittype = types.initializeType('unit(px,%){1,3}')
7 | const stringtype = types.initializeType('string')
8 |
9 | console.log(stringtype)
10 |
11 | const output = (t) => { console.log(`Type: ${t.constructor.name} — Value: ${t.value} — isValid: ${t.isValid()} ${t.getErrorMessage()}`) }
12 |
13 | [new colortype('grey'),
14 | new colortype('rgba(0,255,3,0.3)'),
15 | new colortype('#DDF'),
16 | new colortype('#DF'),
17 | new booleantype('true'),
18 | new booleantype('false'),
19 | new booleantype('banana'),
20 | new unittype('10 20px 20'),
21 | new unittype('10px 20px 20px'),
22 | new unittype('10px'),
23 | new unittype('10%'),
24 | new unittype('10px 10px'),
25 | new unittype('0'),
26 | new stringtype('hello world'),
27 | ].map(output)
28 |
29 |
--------------------------------------------------------------------------------