├── .gitignore ├── .npmrc ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── extensions.js ├── package.json ├── pom.xml ├── src ├── extensions │ ├── index.js │ ├── mjml-comment.js │ ├── mjml-conditional-comment.js │ ├── mjml-html.js │ ├── mjml-tracking-pixel.js │ └── mjml-yahoo-style.js ├── index.js ├── mjml-accordion-element.js ├── mjml-accordion-text.js ├── mjml-accordion-title.js ├── mjml-accordion.js ├── mjml-all.js ├── mjml-attributes.js ├── mjml-body.js ├── mjml-breakpoint.js ├── mjml-button.js ├── mjml-carousel-image.js ├── mjml-carousel.js ├── mjml-class.js ├── mjml-column.js ├── mjml-divider.js ├── mjml-font.js ├── mjml-group.js ├── mjml-head.js ├── mjml-hero.js ├── mjml-html-attribute.js ├── mjml-html-attributes.js ├── mjml-image.js ├── mjml-navbar-link.js ├── mjml-navbar.js ├── mjml-preview.js ├── mjml-raw.js ├── mjml-section.js ├── mjml-selector.js ├── mjml-social-element.js ├── mjml-social.js ├── mjml-spacer.js ├── mjml-style.js ├── mjml-table.js ├── mjml-text.js ├── mjml-title.js ├── mjml-wrapper.js ├── mjml.js ├── utils.js └── utils │ ├── add-query-params.js │ ├── fix-conditional-comment.js │ ├── get-text-align.js │ ├── html-entities.js │ ├── html-entities.json │ ├── index.js │ ├── render-to-json.js │ ├── render-to-json2.js │ ├── render-utils.js │ ├── to-mobile-font-size.js │ └── use-https.js ├── test ├── extensions.spec.js ├── mjml-tags.spec.js ├── render-to-json.spec.js ├── render.spec.js └── utils.spec.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | *npm-debug.log 4 | target 5 | maven 6 | package-lock.json 7 | coverage 8 | typings 9 | .history 10 | .idea 11 | *.iml 12 | test/e2e/screenshots 13 | .yo-rc.json 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mantasm@wix.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wix 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mjml-react · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/wix-incubator/mjml-react/blob/master/LICENSE) npm version [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/wix-incubator/mjml-react/pulls) 2 | 3 | --- 4 | 5 | ## NOTICE: This project is no longer maintained. :warning: 6 | 7 | This project is no longer maintained. There will be no more new features, fixes and releases. Feel free to fork this repository, use different build system and release this project under different name. 8 | 9 | Known forks: 10 | 11 | * [Faire mjml-react fork](https://github.com/Faire/mjml-react) 12 | 13 | --- 14 | 15 | · 16 | 17 | There is an awesome library [mjml](https://mjml.io/) with github repo here [https://github.com/mjmlio/mjml](https://github.com/mjmlio/mjml). 18 | 19 | `MJML` is a markup language created by [Mailjet](https://www.mailjet.com/). 20 | So in order to create emails on the fly we created a library with `React` components. 21 | 22 | ## How it works 23 | 24 | Install the required dependencies first: 25 | 26 | ```bash 27 | npm install react react-dom mjml mjml-react 28 | ``` 29 | 30 | And afterwards write a code like a pro: 31 | 32 | ```js 33 | import { 34 | render, 35 | Mjml, 36 | MjmlHead, 37 | MjmlTitle, 38 | MjmlPreview, 39 | MjmlBody, 40 | MjmlSection, 41 | MjmlColumn, 42 | MjmlButton, 43 | MjmlImage, 44 | } from "mjml-react"; 45 | 46 | const { html, errors } = render( 47 | 48 | 49 | Last Minute Offer 50 | Last Minute Offer... 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | I like it! 66 | 67 | 68 | 69 | 70 | , 71 | { validationLevel: "soft" } 72 | ); 73 | ``` 74 | 75 | And as the result you will get a nice looking email HTML (works in mobile too!) 76 | 77 | ![preview](https://user-images.githubusercontent.com/10008149/41058394-59b8ce9e-69d2-11e8-9eb9-c294f35bae9f.png) 78 | 79 | ## Options 80 | 81 | mjml-react sets the following MJML options when rendering to HTML: 82 | 83 | ```js 84 | { 85 | keepComments: false, 86 | beautify: false, 87 | minify: true, 88 | validationLevel: 'strict' 89 | } 90 | ``` 91 | 92 | If you want to override these, you can pass an object to `render` as a second argument. See the [MJML docs](https://documentation.mjml.io/#inside-node-js) for the full list of options you can set. 93 | 94 | ## Extensions 95 | 96 | ```js 97 | import { 98 | MjmlHtml, 99 | MjmlComment, 100 | MjmlConditionalComment 101 | } from 'mjml-react/extensions'; 102 | 103 | Built with ... at ... 104 | // 105 | 106 | MSO conditionals 107 | // 108 | 109 | MSO conditionals 110 | // 111 | 112 | 113 | //
Hello World!
114 | ``` 115 | 116 | ## Utils 117 | 118 | We do have also some utils for post processing the output HTML. 119 | Because not all mail clients do support named HTML entities, like `'`. 120 | So we need to replace them to hex. 121 | 122 | ```js 123 | import { namedEntityToHexCode, fixConditionalComment } from "mjml-react/utils"; 124 | 125 | const html = "
'
"; 126 | namedEntityToHexCode(html); 127 | //
'
128 | 129 | fixConditionalComment( 130 | "", 131 | "Hello", 132 | "if IE" 133 | ); 134 | // 135 | ``` 136 | 137 | ## Limitations 138 | 139 | Currently `mjml` and `mjml-react` libraries are meant to be run inside a node. 140 | 141 | ## Example project 142 | 143 | You can find an example project here 144 | [https://github.com/wix-incubator/mjml-react-example](https://github.com/wix-incubator/mjml-react-example) 145 | -------------------------------------------------------------------------------- /extensions.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/src/extensions'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mjml-react", 3 | "version": "2.0.2", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Mantas Miliukas", 7 | "email": "mantasm@wix.com" 8 | }, 9 | "homepage": "https://github.com/wix-incubator/mjml-react", 10 | "bugs": "https://github.com/wix-incubator/mjml-react/issues", 11 | "main": "dist/src/index.js", 12 | "files": [ 13 | "extensions.js", 14 | "utils.js", 15 | "dist" 16 | ], 17 | "publishConfig": { 18 | "registry": "https://registry.npmjs.org/", 19 | "versionBumpStrategy": "patch" 20 | }, 21 | "scripts": { 22 | "start": "yoshi start", 23 | "pretest": "yoshi build", 24 | "test": "yoshi test --mocha", 25 | "posttest": "yoshi lint" 26 | }, 27 | "devDependencies": { 28 | "babel-plugin-transform-runtime": "~6.23.0", 29 | "chai": "~4.1.0", 30 | "mjml": "^4.7.0", 31 | "mjml2json": "^1.0.1", 32 | "puppeteer": "^1.4.0", 33 | "react": "^17.0.0", 34 | "react-dom": "^17.0.0", 35 | "@wix/yoshi": "^4.186.0" 36 | }, 37 | "peerDependencies": { 38 | "mjml": "^4.7.0", 39 | "react": "^16.0.0 || ^17.0.0 || ^18.0.0", 40 | "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" 41 | }, 42 | "yoshi": { 43 | "hmr": "auto" 44 | }, 45 | "module": "dist/es/src/index.js", 46 | "dependencies": { 47 | "@babel/runtime": "^7.14.6", 48 | "babel-runtime": "~6.25.0", 49 | "color": "^3.1.3", 50 | "react-reconciler": "^0.26.1" 51 | }, 52 | "babel": { 53 | "plugins": [ 54 | "transform-runtime" 55 | ], 56 | "presets": [ 57 | "wix" 58 | ] 59 | }, 60 | "eslintConfig": { 61 | "extends": "yoshi", 62 | "rules": { 63 | "quote-props": 0, 64 | "indent": [ 65 | "error", 66 | 2, 67 | { 68 | "SwitchCase": 1, 69 | "ignoredNodes": [ 70 | "JSXElement", 71 | "JSXElement *" 72 | ] 73 | } 74 | ] 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.wixpress 5 | mjml-react 6 | pom 7 | mjml-react 8 | mjml-react 9 | 1.0.0-SNAPSHOT 10 | 11 | 12 | Promote 13 | 14 | 15 | 16 | com.wixpress.common 17 | wix-master-parent 18 | 100.0.0-SNAPSHOT 19 | 20 | 21 | 22 | 23 | Mantas Miliukas 24 | mantasm@wix.com 25 | 26 | owner 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/extensions/index.js: -------------------------------------------------------------------------------- 1 | export { MjmlComment } from './mjml-comment'; 2 | export { MjmlConditionalComment } from './mjml-conditional-comment'; 3 | export { MjmlYahooStyle } from './mjml-yahoo-style'; 4 | export { MjmlTrackingPixel } from './mjml-tracking-pixel'; 5 | export { MjmlHtml } from './mjml-html'; 6 | -------------------------------------------------------------------------------- /src/extensions/mjml-comment.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { string } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from '../utils'; 5 | 6 | export class MjmlComment extends Component { 7 | static propTypes = { 8 | children: string.isRequired, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | if (children && children.trim().length) { 14 | return React.createElement('mj-raw', { 15 | ...handleMjmlProps(rest), 16 | dangerouslySetInnerHTML: { 17 | __html: ``, 18 | }, 19 | }); 20 | } 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/extensions/mjml-conditional-comment.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { string } from 'prop-types'; 3 | 4 | import { MjmlComment } from './mjml-comment'; 5 | 6 | export class MjmlConditionalComment extends Component { 7 | static propTypes = { 8 | children: string.isRequired, 9 | condition: string.isRequired, 10 | }; 11 | 12 | static defaultProps = { 13 | condition: 'if gte mso 9', 14 | }; 15 | 16 | render() { 17 | const { children, condition, ...rest } = this.props; 18 | if (children && children.trim().length) { 19 | return ( 20 | 21 | {`[${condition}]>${children} 23 | ); 24 | } 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/extensions/mjml-html.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { string } from 'prop-types'; 3 | 4 | export class MjmlHtml extends Component { 5 | static propTypes = { 6 | tag: string, 7 | html: string.isRequired, 8 | }; 9 | 10 | render() { 11 | const { tag, html } = this.props; 12 | return React.createElement(tag || 'mj-raw', { 13 | dangerouslySetInnerHTML: { 14 | __html: html, 15 | }, 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/extensions/mjml-tracking-pixel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { string } from 'prop-types'; 3 | 4 | import { MjmlRaw } from '../mjml-raw'; 5 | 6 | export class MjmlTrackingPixel extends Component { 7 | static propTypes = { 8 | src: string.isRequired, 9 | }; 10 | 11 | render() { 12 | const { src } = this.props; 13 | const trackingPixelStyle = { 14 | display: 'table', 15 | height: '1px!important', 16 | width: '1px!important', 17 | border: '0!important', 18 | margin: '0!important', 19 | padding: '0!important', 20 | }; 21 | return ( 22 | 23 | {/* eslint-disable-next-line jsx-a11y/alt-text */} 24 | 31 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/extensions/mjml-yahoo-style.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { string } from 'prop-types'; 3 | 4 | import { MjmlRaw } from '../mjml-raw'; 5 | 6 | export class MjmlYahooStyle extends Component { 7 | static propTypes = { 8 | children: string.isRequired, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | if (children && children.trim().length) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOMServer from 'react-dom/server'; 2 | import mjml2html from 'mjml'; 3 | import { minify as htmlMinify } from 'html-minifier'; 4 | 5 | import { renderToJSON } from './utils/render-to-json'; 6 | import { renderToJSON2 } from './utils/render-to-json2'; 7 | 8 | export { render, renderToMjml, renderToJSON, renderToJSON2 }; 9 | 10 | export { Mjml } from './mjml'; 11 | export { MjmlAccordion } from './mjml-accordion'; 12 | export { MjmlAccordionElement } from './mjml-accordion-element'; 13 | export { MjmlAccordionText } from './mjml-accordion-text'; 14 | export { MjmlAccordionTitle } from './mjml-accordion-title'; 15 | export { MjmlAll } from './mjml-all'; 16 | export { MjmlAttributes } from './mjml-attributes'; 17 | export { MjmlBody } from './mjml-body'; 18 | export { MjmlBreakpoint } from './mjml-breakpoint'; 19 | export { MjmlButton } from './mjml-button'; 20 | export { MjmlCarousel } from './mjml-carousel'; 21 | export { MjmlCarouselImage } from './mjml-carousel-image'; 22 | export { MjmlClass } from './mjml-class'; 23 | export { MjmlColumn } from './mjml-column'; 24 | export { MjmlDivider } from './mjml-divider'; 25 | export { MjmlFont } from './mjml-font'; 26 | export { MjmlGroup } from './mjml-group'; 27 | export { MjmlHead } from './mjml-head'; 28 | export { MjmlHero } from './mjml-hero'; 29 | export { MjmlHtmlAttributes } from './mjml-html-attributes'; 30 | export { MjmlHtmlAttribute } from './mjml-html-attribute'; 31 | export { MjmlImage } from './mjml-image'; 32 | export { MjmlNavbar } from './mjml-navbar'; 33 | export { MjmlNavbarLink } from './mjml-navbar-link'; 34 | export { MjmlPreview } from './mjml-preview'; 35 | export { MjmlRaw } from './mjml-raw'; 36 | export { MjmlSection } from './mjml-section'; 37 | export { MjmlSelector } from './mjml-selector'; 38 | export { MjmlSocial } from './mjml-social'; 39 | export { MjmlSocialElement } from './mjml-social-element'; 40 | export { MjmlSpacer } from './mjml-spacer'; 41 | export { MjmlStyle } from './mjml-style'; 42 | export { MjmlTable } from './mjml-table'; 43 | export { MjmlText } from './mjml-text'; 44 | export { MjmlTitle } from './mjml-title'; 45 | export { MjmlWrapper } from './mjml-wrapper'; 46 | 47 | function render(email, options = {}) { 48 | const defaults = { 49 | keepComments: false, 50 | beautify: false, 51 | validationLevel: 'strict', 52 | }; 53 | 54 | const html = mjml2html(renderToMjml(email), { 55 | ...defaults, 56 | ...options, 57 | minify: undefined, 58 | }); 59 | 60 | if (options.minify) { 61 | return { 62 | html: htmlMinify(html.html, { 63 | caseSensitive: true, 64 | collapseWhitespace: true, 65 | minifyCSS: true, 66 | removeComments: true, 67 | removeEmptyAttributes: true, 68 | }), 69 | }; 70 | } 71 | 72 | return html; 73 | } 74 | 75 | function renderToMjml(email) { 76 | return ReactDOMServer.renderToStaticMarkup(email); 77 | } 78 | -------------------------------------------------------------------------------- /src/mjml-accordion-element.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlAccordionElement extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-accordion-element', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-accordion-text.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlAccordionText extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-accordion-text', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-accordion-title.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlAccordionTitle extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-accordion-title', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-accordion.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlAccordion extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-accordion', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-all.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlAll extends Component { 6 | render() { 7 | return React.createElement('mj-all', handleMjmlProps(this.props), null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mjml-attributes.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlAttributes extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-attributes', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-body.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlBody extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-body', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-breakpoint.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlBreakpoint extends Component { 6 | render() { 7 | return React.createElement( 8 | 'mj-breakpoint', 9 | handleMjmlProps(this.props), 10 | null, 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/mjml-button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlButton extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-button', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-carousel-image.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlCarouselImage extends Component { 6 | render() { 7 | return React.createElement( 8 | 'mj-carousel-image', 9 | handleMjmlProps(this.props), 10 | null, 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/mjml-carousel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlCarousel extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-carousel', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-class.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlClass extends Component { 6 | render() { 7 | return React.createElement('mj-class', handleMjmlProps(this.props), null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mjml-column.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlColumn extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-column', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-divider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlDivider extends Component { 6 | render() { 7 | return React.createElement('mj-divider', handleMjmlProps(this.props), null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mjml-font.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlFont extends Component { 6 | render() { 7 | return React.createElement('mj-font', handleMjmlProps(this.props), null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mjml-group.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlGroup extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-group', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-head.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlHead extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-head', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-hero.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlHero extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-hero', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-html-attribute.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlHtmlAttribute extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-html-attribute', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-html-attributes.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlHtmlAttributes extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-html-attributes', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-image.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlImage extends Component { 6 | render() { 7 | return React.createElement('mj-image', handleMjmlProps(this.props), null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mjml-navbar-link.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlNavbarLink extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-navbar-link', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlNavbar extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-navbar', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-preview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { string } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlPreview extends Component { 7 | static propTypes = { 8 | children: string, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-preview', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-raw.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlRaw extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-raw', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-section.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlSection extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-section', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-selector.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlSelector extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-selector', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-social-element.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlSocialElement extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement( 14 | 'mj-social-element', 15 | handleMjmlProps(rest), 16 | children, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mjml-social.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlSocial extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-social', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-spacer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { handleMjmlProps } from './utils'; 4 | 5 | export class MjmlSpacer extends Component { 6 | render() { 7 | return React.createElement('mj-spacer', handleMjmlProps(this.props), null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mjml-style.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { string } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlStyle extends Component { 7 | static propTypes = { 8 | children: string, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-style', { 14 | ...handleMjmlProps(rest), 15 | dangerouslySetInnerHTML: { __html: children }, 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/mjml-table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlTable extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-table', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-text.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlText extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-text', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-title.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlTitle extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-title', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml-wrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class MjmlWrapper extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mj-wrapper', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mjml.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node } from 'prop-types'; 3 | 4 | import { handleMjmlProps } from './utils'; 5 | 6 | export class Mjml extends Component { 7 | static propTypes = { 8 | children: node, 9 | }; 10 | 11 | render() { 12 | const { children, ...rest } = this.props; 13 | return React.createElement('mjml', handleMjmlProps(rest), children); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const Color = require('color'); 2 | 3 | const handlers = { 4 | inline: boolToString, 5 | 'full-width': boolToString, 6 | width: numberToPx, 7 | height: numberToPx, 8 | 'border-radius': numberToPx, 9 | 'border-width': numberToPx, 10 | 'background-size': numberToPx, 11 | padding: numberToPx, 12 | 'padding-top': numberToPx, 13 | 'padding-right': numberToPx, 14 | 'padding-bottom': numberToPx, 15 | 'padding-left': numberToPx, 16 | 'font-size': numberToPx, 17 | 'letter-spacing': numberToPx, 18 | 'line-height': numberToPx, 19 | 'icon-padding': numberToPx, 20 | 'text-padding': numberToPx, 21 | color: handleColor, 22 | 'border-color': handleColor, 23 | 'background-color': handleColor, 24 | 'container-background-color': handleColor, 25 | 'inner-background-color': handleColor, 26 | }; 27 | 28 | export function handleMjmlProps(props) { 29 | return Object.keys(props).reduce((acc, curr) => { 30 | const mjmlProp = kebabCase(curr); 31 | return { 32 | ...acc, 33 | [mjmlProp]: handleMjmlProp(mjmlProp, props[curr]), 34 | }; 35 | }, {}); 36 | } 37 | 38 | function handleMjmlProp(name, value) { 39 | if (typeof value === 'undefined' || value === null) { 40 | return undefined; 41 | } 42 | const handler = handlers[name] || ((_name, value_) => value_); 43 | return handler(name, value); 44 | } 45 | 46 | function kebabCase(string) { 47 | return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 48 | } 49 | 50 | function boolToString(name, value) { 51 | return value ? name : undefined; 52 | } 53 | 54 | function numberToPx(name, value) { 55 | if (typeof value === 'number') { 56 | return `${value}px`; 57 | } 58 | return value; 59 | } 60 | 61 | function handleColor(name, value) { 62 | const color = parseColor(value); 63 | if (color) { 64 | if (value[0] === '#' && value.length === 9) { 65 | const alpha = color.alpha().toFixed(2); 66 | return color.rgb().alpha(alpha).toString(); 67 | } 68 | return value; 69 | } 70 | return ''; 71 | } 72 | 73 | function parseColor(value) { 74 | try { 75 | return new Color(value); 76 | } catch (e) {} 77 | return null; 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/add-query-params.js: -------------------------------------------------------------------------------- 1 | export function addQueryParams(url, params) { 2 | const query = Object.keys(params) 3 | .reduce((acc, curr) => { 4 | return acc.concat(`${curr}=${encodeURIComponent(params[curr])}`); 5 | }, []) 6 | .join('&'); 7 | if (url.indexOf('?') !== -1) { 8 | return `${url}&${query}`; 9 | } 10 | return `${url}?${query}`; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/fix-conditional-comment.js: -------------------------------------------------------------------------------- 1 | export function fixConditionalComment(html, havingContent, newCondition) { 2 | const comments = //g; 3 | const conditionals = /', 25 | ); 26 | }); 27 | it('should not render if comment is empty', () => { 28 | expect(renderToMjml()).to.equal(''); 29 | expect(renderToMjml({''})).to.equal(''); 30 | expect(renderToMjml( )).to.equal(''); 31 | }); 32 | }); 33 | 34 | describe('conditional comment', () => { 35 | it('should render', () => { 36 | const comment = ( 37 | 38 | First, solve the problem. Then, write the code. 39 | 40 | ); 41 | const markup = renderToMjml(comment); 42 | expect(markup).to.equal( 43 | '', 44 | ); 45 | }); 46 | it('should allow changing condition', () => { 47 | const comment = ( 48 | 49 | First, solve the problem. Then, write the code. 50 | 51 | ); 52 | const markup = renderToMjml(comment); 53 | expect(markup).to.equal( 54 | '', 55 | ); 56 | }); 57 | it('should not render if comment is empty', () => { 58 | expect(renderToMjml()).to.equal(''); 59 | expect( 60 | renderToMjml({''}), 61 | ).to.equal(''); 62 | expect( 63 | renderToMjml( ), 64 | ).to.equal(''); 65 | }); 66 | }); 67 | 68 | describe('yahoo style', () => { 69 | it('should render', () => { 70 | const markup = renderToMjml( 71 | {`a { color: blue; }`}, 72 | ); 73 | expect(markup).to.equal( 74 | '', 75 | ); 76 | }); 77 | }); 78 | 79 | describe('tracking pixel', () => { 80 | it('should render 1x1 raw image with provided src', () => { 81 | const markup = renderToMjml(); 82 | expect(markup).to.equal( 83 | '', 84 | ); 85 | }); 86 | }); 87 | 88 | describe('html', () => { 89 | it('should allow rendering given HTML using mj-raw tag by default', () => { 90 | const markup = renderToMjml(); 91 | expect(markup).to.equal('
hello World
'); 92 | }); 93 | it('should allow rendering given HTML using specified tag', () => { 94 | const markup = renderToMjml( 95 | , 96 | ); 97 | expect(markup).to.equal('
hello World
'); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/mjml-tags.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | 4 | import * as tags from '../src'; 5 | import { MjmlComment } from '../src/extensions'; 6 | 7 | const renderToMjml = tags.renderToMjml; 8 | 9 | describe('mjml tags', () => { 10 | it('should render with content', () => { 11 | expect(renderToMjml(Content)).to.equal( 12 | 'Content', 13 | ); 14 | }); 15 | 16 | describe('', () => { 17 | it('should render string', () => { 18 | expect(renderToMjml(Content)).to.equal( 19 | 'Content', 20 | ); 21 | }); 22 | 23 | it('should render an expression', () => { 24 | const variable = 'Nice'; 25 | expect( 26 | renderToMjml({variable} Content), 27 | ).to.equal('Nice Content'); 28 | }); 29 | 30 | it('should render functional component', () => { 31 | const HelloWorld = () => 'Hello World!'; 32 | expect( 33 | renderToMjml( 34 | 35 | 36 | , 37 | ), 38 | ).to.equal('Hello World!'); 39 | }); 40 | 41 | it('should render component', () => { 42 | class HelloWorld extends React.Component { 43 | render() { 44 | return 'Hello World!'; 45 | } 46 | } 47 | expect( 48 | renderToMjml( 49 | 50 | 51 | , 52 | ), 53 | ).to.equal('Hello World!'); 54 | }); 55 | }); 56 | 57 | it('should render with content', () => { 58 | expect( 59 | renderToMjml({'.button{}'}), 60 | ).to.equal('.button{}'); 61 | expect( 62 | renderToMjml({'.button{}'}), 63 | ).to.equal('.button{}'); 64 | expect( 65 | renderToMjml({'body > div {}'}), 66 | ).to.equal('body > div {}'); 67 | }); 68 | 69 | it('should render with content', () => { 70 | expect( 71 | renderToMjml( 72 | 73 |

Hello World!

74 |
, 75 | ), 76 | ).to.equal('

Hello World!

'); 77 | }); 78 | 79 | it('should render with content', () => { 80 | expect( 81 | renderToMjml(Hello World!), 82 | ).to.equal('Hello World!'); 83 | }); 84 | 85 | it('should render with content', () => { 86 | expect(renderToMjml(I am the head)).to.equal( 87 | 'I am the head', 88 | ); 89 | }); 90 | 91 | it('should render ', () => { 92 | expect( 93 | renderToMjml( 94 | , 98 | ), 99 | ).to.equal( 100 | '', 101 | ); 102 | }); 103 | 104 | it('should render ', () => { 105 | expect(renderToMjml()).to.equal( 106 | '', 107 | ); 108 | }); 109 | 110 | it('should render with content', () => { 111 | expect( 112 | renderToMjml( 113 | 114 | Hello World! 115 | , 116 | ), 117 | ).to.equal( 118 | 'Hello World!', 119 | ); 120 | }); 121 | 122 | it('should render with content', () => { 123 | expect( 124 | renderToMjml( 125 | 126 | Content 127 | , 128 | ), 129 | ).to.equal( 130 | 'Content', 131 | ); 132 | }); 133 | 134 | it('should render with content', () => { 135 | expect( 136 | renderToMjml( 137 | 138 | Content 139 | , 140 | ), 141 | ).to.equal( 142 | 'Content', 143 | ); 144 | }); 145 | 146 | it('should render with content', () => { 147 | expect( 148 | renderToMjml( 149 | 150 | Click Me 151 | , 152 | ), 153 | ).to.equal( 154 | 'Click Me', 155 | ); 156 | }); 157 | 158 | it('should allow passing array as content of ', () => { 159 | expect( 160 | renderToMjml( 161 | 162 | {'First Line'} 163 | {'Second Line'} 164 | , 165 | ), 166 | ).to.equal('First LineSecond Line'); 167 | }); 168 | 169 | it('should render ', () => { 170 | expect( 171 | renderToMjml(), 172 | ).to.equal( 173 | '', 174 | ); 175 | }); 176 | 177 | it('should render ', () => { 178 | expect( 179 | renderToMjml(), 180 | ).to.equal(''); 181 | }); 182 | 183 | it('should render ', () => { 184 | expect(renderToMjml()).to.equal( 185 | '', 186 | ); 187 | }); 188 | 189 | it('should render ', () => { 190 | expect( 191 | renderToMjml( 192 | 193 | Hello World! 194 | , 195 | ), 196 | ).to.equal( 197 | 'Hello World!', 198 | ); 199 | }); 200 | 201 | it('should render ', () => { 202 | expect( 203 | renderToMjml( 204 | 205 | Hello World! 206 | , 207 | ), 208 | ).to.equal( 209 | 'Hello World!', 210 | ); 211 | }); 212 | 213 | it('should render ', () => { 214 | expect( 215 | renderToMjml( 216 | 217 | Line Of Text 218 | 219 | 220 | , 221 | ), 222 | ).to.equal( 223 | 'Line Of Text', 224 | ); 225 | }); 226 | 227 | it('should render ', () => { 228 | expect( 229 | renderToMjml( 230 | 231 | 232 | , 233 | ), 234 | ).to.equal( 235 | '', 236 | ); 237 | }); 238 | 239 | it('should render ', () => { 240 | expect(renderToMjml(Hello World!)).to.equal( 241 | '', 242 | ); 243 | }); 244 | 245 | describe('validate color value', () => { 246 | it('should render with passed values', () => { 247 | expect( 248 | renderToMjml( 249 | 253 | 257 | Content 258 | , 259 | ), 260 | ).to.equal( 261 | 'Content', 262 | ); 263 | }); 264 | 265 | it('should render empty color attribute with passed values', () => { 266 | expect( 267 | renderToMjml( 268 | 272 | 276 | Content 277 | , 278 | ), 279 | ).to.equal( 280 | 'Content', 281 | ); 282 | }); 283 | }); 284 | }); 285 | -------------------------------------------------------------------------------- /test/render-to-json.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import mjml2json from 'mjml2json'; 4 | 5 | import { 6 | renderToJSON, 7 | renderToJSON2, 8 | Mjml, 9 | MjmlStyle, 10 | MjmlBody, 11 | MjmlSection, 12 | MjmlColumn, 13 | MjmlImage, 14 | MjmlText, 15 | MjmlDivider, 16 | MjmlRaw, 17 | MjmlHead, 18 | MjmlButton, 19 | renderToMjml, 20 | } from '../src'; 21 | 22 | import { 23 | MjmlComment, 24 | MjmlConditionalComment, 25 | MjmlTrackingPixel, 26 | MjmlYahooStyle, 27 | } from '../extensions'; 28 | 29 | const useCases = [ 30 | , 31 | 32 | content, 33 | 34 | 35 | content 36 | , 37 | 38 | 39 | 40 |
content
41 |
42 |
, 43 | 44 | {''}, 45 | 46 | 47 | 48 | {'.class {color: red;}'} 49 | 50 | , 51 | 52 | 53 | 54 | 55 |
{''}
56 |

hello ' & world

57 |
58 |
59 |
, 60 | 61 | 62 |
Hello World!
' }}> 63 |
, 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Hello World 73 | 74 | 75 | 76 | 77 | , 78 | 79 | content, 80 | 81 | content, 82 | 83 | {'
'}, 84 | 85 | , 86 | 87 | content, 88 | 89 | , 90 | 91 | , 92 | 93 | , 94 | 95 | 96 |
Hello World!
' }}>
97 |
, 98 | 99 | {' & $ " '}, 100 | 101 | 10$ & Free Delivery, 102 | 103 | 109 | {' '}Hello World ! Hello World ! {' '} 110 | , 111 | 112 | 113 |
114 |
Nested Element
115 |
116 |
, 117 | ]; 118 | 119 | useCases.forEach((tree, i) => { 120 | it(`should render usecase ${i}`, () => { 121 | const mjml = renderToMjml(tree); 122 | const base = mjml2json(mjml); 123 | expect(base).to.eql(renderToJSON(tree)); 124 | expect(base).to.eql(renderToJSON2(tree)); 125 | }); 126 | }); 127 | 128 | // it('measure times', () => { 129 | // const n = 10000; 130 | // const durationMjml = time( 131 | // times(n, () => { 132 | // useCases.forEach((_) => { 133 | // renderToMjml(_); 134 | // }); 135 | // }), 136 | // ); 137 | 138 | // const durationJson = time( 139 | // times(n, () => { 140 | // useCases.forEach((_) => { 141 | // renderToJSON(_); 142 | // }); 143 | // }), 144 | // ); 145 | 146 | // console.log({ n, durationMjml, durationJson }); 147 | // }); 148 | 149 | // function time(fn) { 150 | // const start = Date.now(); 151 | // fn(); 152 | // return Date.now() - start; 153 | // } 154 | 155 | // function times(n, fn) { 156 | // return () => { 157 | // for (let i = 0; i < n; i++) { 158 | // fn(); 159 | // } 160 | // }; 161 | // } 162 | -------------------------------------------------------------------------------- /test/render.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | 4 | import { 5 | render, 6 | Mjml, 7 | MjmlHead, 8 | MjmlTitle, 9 | MjmlBody, 10 | MjmlRaw, 11 | MjmlSection, 12 | MjmlHtmlAttribute, 13 | MjmlHtmlAttributes, 14 | MjmlSelector, 15 | MjmlColumn, 16 | MjmlText, 17 | } from '../src'; 18 | 19 | describe('render()', () => { 20 | it('should render the HTML', () => { 21 | const email = ( 22 | 23 | 24 | Title 25 | 26 | 27 | 28 |

Paragraph

29 |
30 |
31 |
32 | ); 33 | const { html } = render(email, { minify: true }); 34 | expect(html).to.not.be.undefined; 35 | expect(html).to.contain(''); 36 | expect(html).to.contain('Title'); 37 | expect(html).to.contain('

Paragraph

'); 38 | }); 39 | 40 | it('should throw an error if invalid markup is given', () => { 41 | const email = ( 42 | 43 | 44 |
Ooops!
45 |
46 |
47 | ); 48 | expect(() => render(email)).to.throw( 49 | "Element div doesn't exist or is not registered", 50 | ); 51 | }); 52 | 53 | it('should not throw an error if soft validation level is passed', () => { 54 | const email = ( 55 | 56 | 57 |
Ooops!
58 |
59 |
60 | ); 61 | const { errors } = render(email, { 62 | validationLevel: 'soft', 63 | minify: false, 64 | }); 65 | expect(errors.length).to.equal(1); 66 | expect(errors[0].message).to.contain( 67 | "Element div doesn't exist or is not registered", 68 | ); 69 | }); 70 | 71 | it('should render html attributes with custom selector', () => { 72 | const email = ( 73 | 74 | 75 | 76 | 77 | 42 78 | 79 | 80 | 81 | 82 | 83 | 84 | Hello World! 85 | 86 | 87 | 88 | 89 | ); 90 | const { html } = render(email, { 91 | validationLevel: 'soft', 92 | minify: false, 93 | }); 94 | expect(html).contains( 95 | '
Hello World!
', 96 | ); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { 4 | namedEntityToHexCode, 5 | fixConditionalComment, 6 | useHttps, 7 | toMobileFontSize, 8 | getTextAlign, 9 | addQueryParams, 10 | } from '../src/utils/index'; 11 | 12 | describe('utils', () => { 13 | describe('namedEntityToHexCode', () => { 14 | it('should not replace incomplete entity', () => { 15 | expect(namedEntityToHexCode('&')).to.equal('&'); 16 | }); 17 | 18 | it('should not replace unknown entity', () => { 19 | expect(namedEntityToHexCode('&rambo;')).to.equal('&rambo;'); 20 | }); 21 | 22 | it('should not replace entity in hex code', () => { 23 | expect(namedEntityToHexCode('&')).to.equal('&'); 24 | }); 25 | 26 | it('should replace known entity to hex code', () => { 27 | expect(namedEntityToHexCode('&')).to.equal('&'); 28 | expect(namedEntityToHexCode(''')).to.equal('''); 29 | }); 30 | }); 31 | 32 | describe('fixConditionalComment', () => { 33 | it('should not replace if there is no MSO conditionals', () => { 34 | expect( 35 | fixConditionalComment('', 'what ever', 'if IE'), 36 | ).to.equal(''); 37 | }); 38 | it('should replace condition matching the content', () => { 39 | expect( 40 | fixConditionalComment('', '...', 'if IE'), 41 | ).to.equal(''); 42 | }); 43 | }); 44 | 45 | describe('useHttps', () => { 46 | it('should not alter falsy url', () => { 47 | expect(useHttps('')).to.equal(''); 48 | }); 49 | it('should not alter valid url', () => { 50 | expect(useHttps('https://www.wix.com')).to.equal('https://www.wix.com'); 51 | }); 52 | it('should replace http://', () => { 53 | expect(useHttps('http://www.wix.com')).to.equal('https://www.wix.com'); 54 | }); 55 | it('should replace //', () => { 56 | expect(useHttps('//www.wix.com')).to.equal('https://www.wix.com'); 57 | }); 58 | it('should not add missing schema', () => { 59 | expect(useHttps('www.wix.com')).to.equal('www.wix.com'); 60 | }); 61 | }); 62 | 63 | describe('toMobileFontSize', () => { 64 | it('minimum should be 12', () => { 65 | [...Array(12).keys()].forEach((value) => { 66 | expect(toMobileFontSize(value)).to.equal(12); 67 | expect(toMobileFontSize(`${value}px`)).to.equal(12); 68 | }); 69 | }); 70 | it('not modified from 12 to 26', () => { 71 | Array.from(Array(14).keys()).forEach((value) => { 72 | expect(toMobileFontSize(value + 12)).to.equal(value + 12); 73 | expect(toMobileFontSize(`${value + 12}px`)).to.equal(value + 12); 74 | }); 75 | }); 76 | it('max should be 50', () => { 77 | expect(toMobileFontSize(200)).to.equal(50); 78 | }); 79 | }); 80 | 81 | describe('getTextAlign', () => { 82 | it('should return default alignment', () => { 83 | expect(getTextAlign()).to.equal('center'); 84 | expect(getTextAlign(null, 'left')).to.equal('left'); 85 | }); 86 | it('should return default alignment if value is unrecognized', () => { 87 | expect(getTextAlign('blah')).to.equal('center'); 88 | }); 89 | it('should return a valid text align', () => { 90 | expect(getTextAlign('left')).to.equal('left'); 91 | expect(getTextAlign('right')).to.equal('right'); 92 | expect(getTextAlign('center')).to.equal('center'); 93 | expect(getTextAlign('inherit')).to.equal('inherit'); 94 | expect(getTextAlign('justify')).to.equal('justify'); 95 | }); 96 | }); 97 | 98 | describe('addQueryParams', () => { 99 | it('should add a single query param', () => { 100 | expect(addQueryParams('url', { one: 'two' })).to.equal('url?one=two'); 101 | }); 102 | it('should add multiple query params', () => { 103 | expect(addQueryParams('url', { one: 'two', three: 'four' })).to.equal( 104 | 'url?one=two&three=four', 105 | ); 106 | }); 107 | it('should escape param value', () => { 108 | expect(addQueryParams('url', { one: '?two' })).to.equal('url?one=%3Ftwo'); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/src/utils/index'); 2 | --------------------------------------------------------------------------------