├── .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 | 17 | 18 | 19 | 24 | 25 | 26 |
20 | 21 | Hello There! 22 | 23 |
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 | layout 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 | body 15 |

16 | 17 | From here, you can first define your sections: 18 | 19 |

20 | sections 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 | columns 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 | 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 | try it live 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 | mj-chart demo 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 | mj-chartjs demo 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 | mj-qr-code demo 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 | 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 | try it live 16 | 17 |

18 | 19 | 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 | desktop 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 | sexy 30 | 31 |

32 | 33 | 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}` 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 | easy and quick; responsive 8 |

9 | 10 |

11 | Mobile
12 | easy and quick; responsive 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 | try it live 45 |

46 | 47 | 50 | 51 | 52 | 55 | 56 | 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 | try it live 37 | 38 |

39 | 40 | 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 | try it live 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 | try it live 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 | try it live 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 | try it live 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 | try it live 40 | 41 |

42 | 43 | attribute | unit | description | default value 44 | ---------------------|---------------|-------------------------------------|--------------- 45 | inline | string | set to "inline" to inline styles | n/a 46 | 47 | 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 | try it live 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 | try it live 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 | try it live 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 | 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 | try it live 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 | 29 | 30 | 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 | 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 | try it live 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 | try it live 36 | 37 |

38 | 39 | 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 | try it live 25 | 26 |

27 | 28 | 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 | wrapper 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 | try it live 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 | 43 | 44 | 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 | --------------------------------------------------------------------------------