diff --git a/.eslintrc b/.eslintrc index f7cde9d16..34a3463e0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,26 +1,12 @@ { "extends": "airbnb", "parser": "babel-eslint", - "rules": { - "no-unused-expressions": 0, - "no-unused-vars": [2, { "varsIgnorePattern": "^debug$" }], - "strict": 0, - "new-cap": 0, - "camelcase": 0, - "react/jsx-uses-react": 1, - "semi": 0, - - "react/sort-comp": [0, { - "order": [ - "lifecycle", - "everything-else", - "render" - ] - }], - "array-bracket-spacing": 0, "arrow-body-style": 0, + "arrow-parens": 0, + "camelcase": 0, + "class-methods-use-this": 0, "comma-dangle": [2, "never"], "consistent-return": 0, "default-case": 0, @@ -32,16 +18,21 @@ "indent": [2, 2, {"SwitchCase": 1}], "key-spacing": 0, "max-len": 0, + "new-cap": 0, "no-case-declarations": 0, "no-confusing-arrow": 0, "no-console": 2, + "no-mixed-operators": 0, "no-multi-spaces": 0, "no-nested-ternary": 0, "no-param-reassign": 0, + "no-plusplus": 0, "no-return-assign": 0, "no-shadow": 0, "no-throw-literal": 0, "no-underscore-dangle": 0, + "no-unused-expressions": 0, + "no-unused-vars": [2, { "varsIgnorePattern": "^debug$" }], "no-use-before-define": 0, "object-curly-spacing": 0, "object-shorthand": 0, @@ -50,30 +41,41 @@ "quote-props": 0, "quotes": 0, "radix": 0, + "semi": 0, "space-before-function-paren": [2, "always"], "space-in-parens": 0, + "strict": 0, "vars-on-top": 0, - "no-mixed-operators": 0, - "import/no-unresolved": 0, + "import/extensions": 0, "import/imports-first": 0, + "import/no-dynamic-require": 0, "import/no-extraneous-dependencies": 0, + "import/no-unresolved": 0, "import/prefer-default-export": 0, "react/jsx-boolean-value": 0, "react/jsx-closing-bracket-location": 0, "react/jsx-curly-spacing": 0, + "react/jsx-filename-extension": 0, "react/jsx-indent-props": 0, "react/jsx-no-bind": 0, + "react/jsx-no-target-blank": 0, + "react/jsx-uses-react": 1, + "react/no-danger": 0, "react/no-did-mount-set-state": 0, "react/no-did-update-set-state": 0, "react/no-multi-comp": 0, "react/prefer-stateless-function": 0, "react/prop-types": 0, - "react/jsx-filename-extension": 0, - "react/jsx-no-target-blank": 0 + "react/sort-comp": [0, { + "order": [ + "lifecycle", + "everything-else", + "render" + ] + }] }, - "env": { "browser": true, "node": true, diff --git a/.travis.yml b/.travis.yml index 28f570829..3e25e06d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,29 @@ sudo: false +env: + - CXX=g++-4.8 +language: node_js +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.8 + - g++-4.8 language: node_js node_js: - 4 - 5 - 6 script: + - npm install + - gulp build - npm run lint - - cd packages/mjml-core + - cd packages/mjml-validator + - npm link mjml-core + - npm link + - npm install + - cd ../mjml-core + - npm link mjml-validator - npm install + - npm link - npm test diff --git a/doc/components.md b/doc/components.md index 79d616e7e..50136528b 100644 --- a/doc/components.md +++ b/doc/components.md @@ -1,8 +1,9 @@ # Components - 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. +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. + For instance, the `mj-button` components is, on the inside, a complex HTML layout: ``` html @@ -25,8 +26,3 @@ For instance, the `mj-button` components is, on the inside, a complex HTML layou ``` - -# Standard components - -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. - diff --git a/doc/config.json b/doc/config.json index 8a7567121..baedfeac9 100644 --- a/doc/config.json +++ b/doc/config.json @@ -28,6 +28,7 @@ "mjml/packages/mjml-spacer/README.md", "mjml/packages/mjml-table/README.md", "mjml/packages/mjml-text/README.md", + "mjml/packages/mjml-validator/README.md", "mjml/doc/create.md", "mjml/doc/tooling.md" ] diff --git a/doc/create.md b/doc/create.md index 922c4b50a..8d7640918 100644 --- a/doc/create.md +++ b/doc/create.md @@ -1,197 +1,5 @@ # Create a Component -Creating a component is easy! With custom components, you can abstract complex patterns and reuse them easily whenever you need them in your emails! +One of the great advantages of MJML is that it's component based. Components abstract complex patterns and can easily be reused. Added to the standard library of components, it is also possible to create your own components! -Let's create a simple `Title` component. - -### Generate the template file - -``` - -$> mjml --init-component title - -``` -run the following in your terminal. It will create a `Title.js` file in the current working directory. - -### Imports - -``` javascript - -import React, { Component } from 'react' -import { - MJMLElement, - elements, - registerElement, -} from 'mjml' - -``` -These are the required modules to build your component: - -[React](https://facebook.github.io/react/) is used by the engine to abstract higher level components and render them into HTML. - -[MJMLElement](https://github.com/mjmlio/mjml/blob/master/src/components/MJMLElement.js) - -[elements](https://github.com/mjmlio/mjml/blob/master/src/MJMLElementsCollection.js) contains all the standard MJML components. - -[registerElement](https://github.com/mjmlio/mjml/blob/master/src/MJMLElementsCollection.js#L17) is a helper function that allows you to register the component within the MJML engine. - -### Declare your dependencies - -``` javascript -/* - * Wrap your dependencies here. - */ -const { - text: MjText, - // ... -} = elements; - -``` - -The first thing to do is to declare your dependencies at the top of your file. -The key is the component name, and its value is the name you're going to use in the file. -By convention it should be capitalized. - -## Class definition - -All MJML component have some special static that can be use to change the behaviour of your componenet - -``` javascript -Title.tagName = 'title' -Title.defaultMJMLDefinition = { - attributes: { - 'color': '#424242', - 'font-family': 'Helvetica', - 'margin-top': '10px' - } -} -Title.endingTag = true -Title.baseStyles = { - div: { - color: "blue" - } -} -Title.postRender = ($) => { - $('.title').removeAttr('data-title-color'); - return $ -} -``` - -- tagName: modify the tag name of your component, here it will be `` -- endingTag: set to false if your component can include some other MJML component (example: mj-body/mj-section/mj-column are not ending tags, and mj-text/mj-image are both ending tags)` - -## Default and readonly attributes - -``` javascript -const defaultMJMLDefinition = { - attributes: { - 'color': '#424242', - 'font-family': 'Helvetica', - 'margin-top': '10px' - } -} -``` - -Here you can modify and change your element's default and/or readonly attributes. -The attributes are stored within the defaultMJMLDefinition variable at the top. -It can contain any CSS property or component property, but please make sure it will be compatible with most email clients to keep MJML responsive and compatible. - -## Post render -In some case, you'll need to modify the rendered html, like replace some placeholder for outlook by conditional tag then you can define a postRender static function that take jQuery/[Cheerio](https://github.com/cheeriojs/cheerio) with the rendered document. - -``` javascript -Title.postRender = $ => { - $('.title').prepend(`<!--[if mso | IE]> - <table border="0" cellpadding="0" cellspacing="0" width="600" align="center" style="width:600}px;"><tr><td> - <![endif]-->`) - $('.title').append(`<!--[if mso | IE]> - </td></tr></table> - <![endif]-->`) - - return $ -} -``` - -Please note that postRender should return a valid jQuery/Cheerio object - -## Define your public attributes - -``` javascript - /* - * Build your styling here - */ - getStyles() { - const { mjAttribute, color } = this.props - - return _.merge({}, baseStyles, { - text: { - /* - * Get the color attribute - * Example: <mj-title color="blue">content</mj-title> - */ - color: mjAttribute('color') - } - }) - } -``` - -The getStyles method allows you to expose public attributes to the end user with `mjAttribute`. If the user does not provide any value, it will keep the default one. - -## Render your component - -``` javascript - - render() { - - const css = this.getStyles(), - content = 'Hello World!' - - return ( - <MjText style={ css }> - { content } - </MjText> - ) - } -} - -``` - -To render your component, you need to load your style. - -Finally, use the JSX syntax to define your component. Find out more about JSX [here](https://facebook.github.io/react/docs/jsx-in-depth.html). - -# Import your component - -## .mjmlconfig inside the folder - -You can add a simple .mjmlconfig file with path to your class file simple as : - -``` javascript -{ - "packages": [ - "./Title.js", - "mjml-github-component" - ] -} -``` -Note that if you install a MJML componenet from npm, you can declare them in the .mjmlconfig file - -The file should be at the root of where you launch the command in order to be use - -## Manually with a Javascript file - -``` javascript -import { mjml2html, registerMJElement } from 'mjml' -import Title from './Title' - -registerMJElement(Title) - -console.log(mjml2html(` - <mjml> - <mj-body> - <mj-title>Hello world!</mj-title> - </mj-body> - </mjml> -``` - -Then launch it with node script.js and the result will be shown in the console +To learn how to create your own component, follow this [step-by-step guide](https://medium.com/mjml-making-responsive-email-easy/tutorial-creating-your-own-mjml-component-d3a236ab7093#.pz0ebb537) which also includes a ready-to-use boilerplate. diff --git a/install.sh b/install.sh index 1314e28c9..bb9e5f103 100755 --- a/install.sh +++ b/install.sh @@ -23,23 +23,30 @@ BPurple='\033[1;35m' # Purple BCyan='\033[1;36m' # Cyan BWhite='\033[1;37m' # White -printf "${Yellow}Installing npm depencies for mono repo ${Color_Off} \n" +printf "${BYellow}Installing npm depencies for mono repo ${Color_Off} \n" npm install printf "${BGreen}Done.${Color_Off} \n" cd packages -printf "${Yellow}Linking dependencies for every mjml packages.${Color_Off} \n" +printf "${BYellow}Linking dependencies for every mjml packages.${Color_Off} \n" +# Core dependencies +printf "${Yellow}Linking core dependencies${Color_Off} \n" +cd mjml-validator && npm link && npm link mjml-core && cd .. +# Core +printf "${Yellow}Linking core${Color_Off} \n" +cd mjml-core && npm link && npm link mjml-validator && cd .. +# Mj elements +printf "${Yellow}Linking MJML standard elements${Color_Off} \n" cd mjml-button && npm link && npm link mjml-core && cd .. -cd mjml-cli && npm link && npm link mjml-core && cd .. cd mjml-column && npm link && npm link mjml-core && cd .. cd mjml-container && npm link && npm link mjml-core && cd .. -cd mjml-core && npm link && npm link mjml-core && cd .. cd mjml-divider && npm link && npm link mjml-core && cd .. cd mjml-group && npm link && npm link mjml-core && cd .. cd mjml-head-attributes && npm link && npm link mjml-core && cd .. cd mjml-head-font && npm link && npm link mjml-core && cd .. +cd mjml-head-style && npm link && cd .. cd mjml-head-title && npm link && npm link mjml-core && cd .. cd mjml-hero && npm link && npm link mjml-core && cd .. cd mjml-html && npm link && npm link mjml-core && cd .. @@ -54,10 +61,13 @@ cd mjml-social && npm link && npm link mjml-core && cd .. cd mjml-spacer && npm link && npm link mjml-core && cd .. cd mjml-table && npm link && npm link mjml-core && cd .. cd mjml-text && npm link && npm link mjml-core && cd .. +# Cli +printf "${Yellow}Linking core${Color_Off} \n" +cd mjml-cli && npm link && npm link mjml-core && cd .. printf "${BGreen}Done.${Color_Off} \n" -printf "${Yellow}Linking dependencies for MJML package.${Color_Off} \n" +printf "${BYellow}Linking dependencies for MJML package.${Color_Off} \n" cd mjml npm link mjml-button @@ -69,6 +79,7 @@ npm link mjml-divider npm link mjml-group npm link mjml-head-attributes npm link mjml-head-font +npm link mjml-head-style npm link mjml-head-title npm link mjml-hero npm link mjml-html @@ -83,11 +94,14 @@ npm link mjml-social npm link mjml-spacer npm link mjml-table npm link mjml-text +npm link mjml-validator printf "${BGreen}Done.${Color_Off} \n" -printf "${Yellow}Installing npm depencies for each MJML packages ${Color_Off} \n" +printf "${BYellow}Installing npm depencies for each MJML packages ${Color_Off} \n" gulp install cd ../.. -printf "${BGreen}Done.${Color_Off} Happy coding ! 🍺 \n" +printf "${BGreen}Done.${Color_Off} ${Green}Building the project ${Color_Off} \n" +gulp build +printf "${BGreen}Done.🍺 ${Color_Off} \n ${Green}Use ${Color_Off}${BGreen}gulp build${Color_Off}${Green} to build the whole monorepo ! \n And run ${Color_Off}${BGreen}node test.js${Color_Off}${Green} inside ${Color_Off}${BGreen}packages/mjml${Color_Off}${Green} to test your installation ${Color_Off}\n" diff --git a/package.json b/package.json index 8f230f884..9c20d4742 100644 --- a/package.json +++ b/package.json @@ -15,24 +15,24 @@ }, "devDependencies": { "babel": "^6.5.2", - "babel-core": "^6.13.2", - "babel-eslint": "^6.1.2", + "babel-core": "^6.14.0", + "babel-eslint": "^7.0.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", - "babel-preset-es2015": "^6.13.2", + "babel-preset-es2015": "^6.14.0", "babel-preset-react": "^6.11.1", "babel-preset-stage-0": "^6.5.0", - "babel-register": "^6.11.6", - "eslint": "^3.2.2", - "eslint-config-airbnb": "^10.0.0", - "eslint-plugin-import": "^1.13.0", - "eslint-plugin-jsx-a11y": "^2.1.0", - "eslint-plugin-react": "^6.0.0", + "babel-register": "^6.14.0", + "eslint": "^3.6.1", + "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-import": "^1.16.0", + "eslint-plugin-jsx-a11y": "^2.2.2", + "eslint-plugin-react": "^6.3.0", "gulp": "^3.9.1", "gulp-babel": "^6.1.2", "gulp-newer": "^1.2.0", "pre-commit": "^1.1.3", - "shelljs": "^0.7.3", + "shelljs": "^0.7.4", "through2": "^2.0.1", - "yargs": "^4.8.1" + "yargs": "^5.0.0" } } diff --git a/packages/mjml-button/README.md b/packages/mjml-button/README.md index 24c2cb4a8..7af0a9ad2 100644 --- a/packages/mjml-button/README.md +++ b/packages/mjml-button/README.md @@ -32,13 +32,17 @@ attribute | unit | description ----------------------------|-------------|--------------------------------------------------|--------------------- background-color | color | button background-color | #414141 container-background-color | color | button container background color | n/a +border | string | css border format | none +border-bottom | string | css border format | n/a +border-left | string | css border format | n/a +border-right | string | css border format | n/a +border-top | string | css border format | n/a border-radius | px | border radius | 3px font-style | string | normal/italic/oblique | n/a font-size | px | text size | 13px font-weight | number | text thickness | bold font-family | string | font name | Ubuntu, Helvetica, Arial, sans-serif color | color | text color | #ffffff -border | string | css border format | none text-decoration | string | underline/overline/none | none text-transform | string | capitalize/uppercase/lowercase | none align | string | horizontal alignment | center @@ -50,3 +54,5 @@ padding-top | px | top offset padding-bottom | px | bottom offset | n/a padding-left | px | left offset | n/a padding-right | px | right offset | n/a +width | px | button width | n/a +height | px | button height | n/a diff --git a/packages/mjml-button/package.json b/packages/mjml-button/package.json index 05b827725..253eb77d1 100644 --- a/packages/mjml-button/package.json +++ b/packages/mjml-button/package.json @@ -1,7 +1,7 @@ { "name": "mjml-button", "description": "mjml-button", - "version": "2.0.10", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-button/src/index.js b/packages/mjml-button/src/index.js index 43a2e9c4e..fc64481d4 100644 --- a/packages/mjml-button/src/index.js +++ b/packages/mjml-button/src/index.js @@ -3,24 +3,39 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-button' +const parentTag = ['mj-column', 'mj-hero-content'] +const endingTag = true const defaultMJMLDefinition = { content: '', attributes: { - 'align': 'center', - 'background-color': '#414141', - 'border-radius': '3px', - 'color': '#ffffff', - 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', - 'font-size': '13px', - 'font-weight': 'normal', - 'href': '', + "background-color": "#414141", + "border": "none", + "border-bottom": null, + "border-left": null, + "border-radius": "3px", + "border-right": null, + "border-top": null, + "container-background-color": null, + "font-style": null, + "font-size": "13px", + "font-weight": "normal", + "font-family": "Ubuntu, Helvetica, Arial, sans-serif", + "color": "#ffffff", + "text-decoration": "none", + "text-transform": "none", + "align": "center", + "vertical-align": "middle", + "href": null, + "inner-padding": "10px 25px", 'padding': '10px 25px', - 'inner-padding': '10px 25px', - 'text-decoration': 'none', - 'vertical-align': 'middle' + "padding-top": null, + "padding-bottom": null, + "padding-left": null, + "padding-right": null, + "width": null, + "height": null } } -const endingTag = true const baseStyles = { table: { borderCollapse: 'separate' @@ -40,22 +55,29 @@ class Button extends Component { const { mjAttribute, defaultUnit } = this.props return merge({}, baseStyles, { + table: { + width: mjAttribute('width') + }, td: { border: mjAttribute('border'), + borderBottom: mjAttribute('border-bottom'), + borderLeft: mjAttribute('border-left'), borderRadius: defaultUnit(mjAttribute('border-radius'), "px"), + borderRight: mjAttribute('border-right'), + borderTop: mjAttribute('border-top'), color: mjAttribute('color'), cursor: 'auto', fontStyle: mjAttribute('font-style'), + height: mjAttribute('height'), padding: defaultUnit(mjAttribute('inner-padding'), "px") }, a: { background: mjAttribute('background-color'), color: mjAttribute('color'), fontFamily: mjAttribute('font-family'), - fontSize: defaultUnit(mjAttribute('font-size'), "px"), + fontSize: defaultUnit(mjAttribute('font-size')), fontStyle: mjAttribute('font-style'), fontWeight: mjAttribute('font-weight'), - lineHeight: mjAttribute('height'), textDecoration: mjAttribute('text-decoration'), textTransform: mjAttribute('text-transform'), margin: "0px" @@ -112,8 +134,9 @@ class Button extends Component { } Button.tagName = tagName -Button.defaultMJMLDefinition = defaultMJMLDefinition +Button.parentTag = parentTag Button.endingTag = endingTag +Button.defaultMJMLDefinition = defaultMJMLDefinition Button.baseStyles = baseStyles export default Button diff --git a/packages/mjml-cli/bin/mjml b/packages/mjml-cli/bin/mjml index 01d0c8d5a..74966645a 100755 --- a/packages/mjml-cli/bin/mjml +++ b/packages/mjml-cli/bin/mjml @@ -15,21 +15,25 @@ binary stdout: process.argv.indexOf('-s') !== -1 || process.argv.indexOf('--stdout') !== -1, output: binary.output } - + cli.renderFile(files, options) }) + .option('-l, --level [level]', 'Specifies the level of validation of MJML parser (skip/soft/strict)', /^(skip|soft|strict)$/i, 'soft') .option('-r, --render <file>', 'Compiles an MJML file') .option('-i, --stdin', 'Compiles an MJML file from input stream') .option('-w, --watch <file>', 'Watch and render an MJML file') .option('-o, --output <file>', 'Redirect the HTML to a file') .option('-s, --stdout', 'Redirect the HTML to stdout') .option('-m, --min', 'Minify the final output file', 'false') - .option('-e, --ending', 'Specifies that the newly created component is an ending tag') - .option('-c, --column', 'Specifies that the newly created component is an column element') - .option('--init-component <name>', 'Initialize an MJML component') + .option('-f, --format <format>', 'Output format for MJML validation', /^(json|text)$/i, 'text') + .option('--validate <file>', 'Validate a MJML Document') .parse(process.argv) switch (true) { + case (!!binary.validate): + cli.validate(binary.validate, binary) + break + case (!!binary.watch): cli.watch(binary.watch, binary) break @@ -41,8 +45,4 @@ switch (true) { case (!!binary.stdin): cli.renderStream(binary) break - - case (!!binary.initComponent): - cli.initComponent(binary.initComponent, binary.ending, binary.column) - break } diff --git a/packages/mjml-cli/package.json b/packages/mjml-cli/package.json index d47663c83..5526c39e2 100644 --- a/packages/mjml-cli/package.json +++ b/packages/mjml-cli/package.json @@ -1,7 +1,7 @@ { "name": "mjml-cli", "description": "MJML: the only framework that makes responsive-email easy", - "version": "2.3.2", + "version": "3.0.0-beta.3", "main": "bin/mjml", "bin": { "mjml-cli": "bin/mjml" @@ -18,12 +18,12 @@ "dependencies": { "commander": "^2.9.0", "fs-promise": "^0.5.0", - "glob": "^7.0.3", - "lodash": "^4.14.2", - "mjml-core": "^2.3.2" + "glob": "^7.1.0", + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3" }, "devDependencies": { "chai": "^3.5.0", - "mocha": "^2.4.5" + "mocha": "^3.1.0" } } diff --git a/packages/mjml-cli/src/client.js b/packages/mjml-cli/src/client.js index ad7048534..60a281074 100644 --- a/packages/mjml-cli/src/client.js +++ b/packages/mjml-cli/src/client.js @@ -1,10 +1,7 @@ -import { MJMLRenderer, version } from 'mjml-core' +import { MJMLRenderer, version, documentParser, MJMLValidator } from 'mjml-core' import fs from 'fs' import glob from 'glob' import path from 'path' -import camelCase from 'lodash/camelCase' -import upperFirst from 'lodash/upperFirst' -import createComponent from './createComponent' /* * The version number is the NPM @@ -26,6 +23,15 @@ const promisify = fn => */ const error = e => console.log(e.stack || e) // eslint-disable-line no-console +const isDirectory = (file) => { + try { + const outputPath = path.resolve(process.cwd(), file) + return fs.statSync(outputPath).isDirectory() + } catch (e) { + return false + } +} + /* * Stdin to string buffer */ @@ -47,18 +53,31 @@ const stdinToBuffer = (stream, callback) => { * read: read a fileexists: ensure the file exists */ const write = promisify(fs.writeFile) -const mkdir = promisify(fs.mkdir) const read = promisify(fs.readFile) const readStdin = promisify(stdinToBuffer) /* * Render an input promise */ -const render = (bufferPromise, { min, output, stdout }) => { +const render = (bufferPromise, { min, output, stdout, fileName, level }) => { bufferPromise - .then(mjml => new MJMLRenderer(mjml.toString(), { minify: min }).render()) - .then(result => stdout ? process.stdout.write(result) : write(output, result)) - .catch(error) + .then(mjml => new MJMLRenderer(mjml.toString(), { minify: min, level }).render()) + .then(result => { + const { html: content, errors } = result + + if (errors) { + error(errors.map(err => err.formattedMessage).join('\n')) + } + + stdout ? process.stdout.write(content) : write(output, content) + }) + .catch(e => { + if (e.getMessages) { + return error(`${fileName ? `File: ${fileName} \n` : ``}${e.getMessages()}`) + } + + return error(e) + }) } /* @@ -66,28 +85,32 @@ const render = (bufferPromise, { min, output, stdout }) => { * min: boolean that specify the output format (pretty/minified) */ export const renderFile = (input, options) => { + const outputIsDirectory = !!options.output && isDirectory(options.output) + const renderFiles = files => { files.forEach((file, index) => { const inFile = path.basename(file, '.mjml') let output if (options.output) { - const extension = path.extname(options.output) || '.html' - const outFile = path.join(path.dirname(options.output), path.basename(options.output, extension)) - - if (files.length > 1) { - output = `${outFile}-${index + 1}${extension}` + const outputExtension = path.extname(options.output) || '.html' + const outFile = path.join(path.dirname(options.output), path.basename(options.output, outputExtension)) + const multipleFiles = files.length > 1 + + if (multipleFiles && outputIsDirectory) { + output = `${options.output}/${inFile}${outputExtension}` + } else if (multipleFiles) { + output = `${outFile}-${index + 1}${outputExtension}` } else { - output = `${outFile}${extension}` + output = `${outFile}${outputExtension}` } } else { - const extension = path.extname(inFile) || '.html' - output = `${inFile}${extension}` + output = `${inFile}${path.extname(inFile) || ".html"}` } const filePath = path.resolve(process.cwd(), file) - render(read(filePath), { min: options.min, stdout: options.stdout, output }) + render(read(filePath), { min: options.min, stdout: options.stdout, output, fileName: file, level: options.level }) }) } @@ -103,21 +126,37 @@ export const renderFile = (input, options) => { */ export const renderStream = options => render(readStdin(process.stdin), options) +const availableOutputFormat = { + json: JSON.stringify, + text: (errs) => errs.map(e => e.formattedMessage).join('\n') +} + +/** + * Validate an MJML document + */ +export const validate = (input, { format }) => { + read(input) + .then(content => { + const MJMLDocument = documentParser(content.toString()) + const report = MJMLValidator(MJMLDocument) + + const outputFormat = availableOutputFormat[format] || availableOutputFormat['text'] + + process.stdout.write(outputFormat(report)) + }) + .catch(e => { + return error(`Error: ${e}`) + }) +} + /* * Watch changes on a specific input file by calling render on each change */ export const watch = (input, options) => { renderFile(input, options) - const now = new Date(); - fs.watchFile(input, () => console.log(`[${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}] Reloading MJML`) || renderFile(input, options)) // eslint-disable-line no-console -} + fs.watchFile(input, () => { + const now = new Date() -/* - * Create a new component based on the default template - */ -export const initComponent = (name, ending) => { - mkdir(`./${name}`) - .then(() => mkdir(`./${name}/src`)) - .then(() => write(`./${name}/src/index.js`, createComponent(upperFirst(camelCase(name)), ending))) - .then(() => console.log(`Component created: ${name}`)) // eslint-disable-line no-console + console.log(`[${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}] Reloading MJML`) || renderFile(input, options) // eslint-disable-line no-console + }) } diff --git a/packages/mjml-cli/src/createComponent.js b/packages/mjml-cli/src/createComponent.js deleted file mode 100644 index d68c5e9e3..000000000 --- a/packages/mjml-cli/src/createComponent.js +++ /dev/null @@ -1,73 +0,0 @@ -export default (name, endingTag = 'false') => { - const lowerName = name.toLowerCase() - - return `import { MJMLElement } from 'mjml-core' -import merge from 'lodash/merge' -import MJMLText from 'mjml-text' -import React, { Component } from 'react' - -const tagName = '${lowerName}' -const endingTag = ${endingTag} - -/* - * Add your default mjml-attributes here - */ -const defaultMJMLDefinition = { - attributes: { - 'color': '#424242', - 'font-family': 'Helvetica', - 'margin-top': '10px' - } -} - -/* - * Add you default style here - */ -const baseStyles = { - div: { - cursor: 'auto' - } -} - -@MJMLElement -class ${name} extends Component { - - /* - * Build your styling here - */ - getStyles () { - const { mjAttribute, color } = this.props - - return merge({}, this.constructor.baseStyles, { - text: { - /* - * Get the color attribute - * Example: <mj-${lowerName} color="blue">content</mj-${lowerName}> - */ - color: mjAttribute('color') - } - }) - } - - render () { - const css = this.getStyles() - const content = 'Hello World!' - - return ( - <MJMLText style={css}> - {content} - </MJMLText> - ) - } - -} - -${name}.tagName = tagName -${name}.defaultMJMLDefinition = defaultMJMLDefinition -${name}.endingTag = endingTag -${name}.baseStyles = baseStyles - -export default ${name} - -` -} diff --git a/packages/mjml-column/README.md b/packages/mjml-column/README.md index ea3d44640..abdcc9cea 100644 --- a/packages/mjml-column/README.md +++ b/packages/mjml-column/README.md @@ -34,6 +34,12 @@ Every single column has to contain something because they are responsive contain attribute | unit | description | default attributes --------------------|-------------|--------------------------------|-------------------------------------- +background-color | string | background color for a column | n/a +border | string | css border format | none +border-bottom | string | css border format | n/a +border-left | string | css border format | n/a +border-right | string | css border format | n/a +border-top | string | css border format | n/a +border-radius | px | border radius | n/a width | percent/px | column width | (100 / number of columns in section)% vertical-align | string | middle/top/bottom | top -background-color | string | background color for a column | n/a diff --git a/packages/mjml-column/package.json b/packages/mjml-column/package.json index 37fcac323..cec45e669 100644 --- a/packages/mjml-column/package.json +++ b/packages/mjml-column/package.json @@ -1,7 +1,7 @@ { "name": "mjml-column", "description": "mjml-column", - "version": "2.0.11", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,9 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "classnames": "^2.2.5", + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-column/src/index.js b/packages/mjml-column/src/index.js index 011d32959..3884fdd79 100644 --- a/packages/mjml-column/src/index.js +++ b/packages/mjml-column/src/index.js @@ -1,10 +1,26 @@ import { MJMLElement, helpers } from 'mjml-core' +import cx from 'classnames' import each from 'lodash/each' import merge from 'lodash/merge' import React, { Component } from 'react' import uniq from 'lodash/uniq' const tagName = 'mj-column' +const parentTag = ['mj-section', 'mj-group', 'mj-navbar'] +const defaultMJMLDefinition = { + attributes: { + 'background': null, + 'background-color': null, + "border": null, + "border-bottom": null, + "border-left": null, + "border-radius": null, + "border-right": null, + "border-top": null, + 'vertical-align': null, + 'width': null + } +} const baseStyles = { div: { verticalAlign: 'top' @@ -48,19 +64,26 @@ class Column extends Component { styles = this.getStyles() getStyles () { - const { mjAttribute } = this.props + const { mjAttribute, defaultUnit } = this.props return merge({}, baseStyles, { div: { display: 'inline-block', - verticalAlign: mjAttribute('vertical-align'), + direction: 'ltr', fontSize: '13px', textAlign: 'left', + verticalAlign: mjAttribute('vertical-align'), width: this.getMobileWidth() }, table: { - verticalAlign: mjAttribute('vertical-align'), - background: mjAttribute('background-color') + background: mjAttribute('background-color'), + border: mjAttribute('border'), + borderBottom: mjAttribute('border-bottom'), + borderLeft: mjAttribute('border-left'), + borderRadius: defaultUnit(mjAttribute('border-radius'), "px"), + borderRight: mjAttribute('border-right'), + borderTop: mjAttribute('border-top'), + verticalAlign: mjAttribute('vertical-align') } }) } @@ -110,11 +133,12 @@ class Column extends Component { const { mjAttribute, children, sibling } = this.props const width = mjAttribute('width') || `${100 / sibling}%` const mjColumnClass = this.getColumnClass() + const divClasses = cx(mjColumnClass, 'outlook-group-fix') return ( <div aria-labelledby={mjColumnClass} - className={mjColumnClass} + className={divClasses} data-column-width={width} data-vertical-align={this.styles.div.verticalAlign} style={this.styles.div}> @@ -136,7 +160,9 @@ class Column extends Component { } +Column.defaultMJMLDefinition = defaultMJMLDefinition Column.tagName = tagName +Column.parentTag = parentTag Column.baseStyles = baseStyles Column.postRender = postRender diff --git a/packages/mjml-container/package.json b/packages/mjml-container/package.json index a36d4b00f..3368f1088 100644 --- a/packages/mjml-container/package.json +++ b/packages/mjml-container/package.json @@ -1,7 +1,7 @@ { "name": "mjml-container", "description": "mjml-container", - "version": "2.0.10", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,7 +13,7 @@ }, "homepage": "https://mjml.io", "dependencies": { - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-container/src/index.js b/packages/mjml-container/src/index.js index 89933858b..03a01d89d 100644 --- a/packages/mjml-container/src/index.js +++ b/packages/mjml-container/src/index.js @@ -1,10 +1,12 @@ -import { MJMLElement, helpers, elements } from 'mjml-core' +import { MJMLElement, helpers } from 'mjml-core' import React, { Component } from 'react' const tagName = 'mj-container' +const parentTag = ['mj-body'] const defaultMJMLDefinition = { attributes: { - 'width': '600' + 'width': '600px', + 'background-color': null }, inheritedAttributes: [ 'width' @@ -92,10 +94,8 @@ class Container extends Component { } Container.tagName = tagName +Container.parentTag = parentTag Container.defaultMJMLDefinition = defaultMJMLDefinition Container.postRender = postRender -// Support V1.X MJML mj-body -elements['mj-body'] = Container - export default Container diff --git a/packages/mjml-core/package.json b/packages/mjml-core/package.json index b51aa37c9..a0bd6f899 100644 --- a/packages/mjml-core/package.json +++ b/packages/mjml-core/package.json @@ -1,7 +1,7 @@ { "name": "mjml-core", "description": "mjml-core", - "version": "2.3.3", + "version": "3.0.0-beta.3", "main": "lib/index.js", "scripts": { "test": "mocha --compilers js:babel-register" @@ -21,21 +21,24 @@ "html-minifier": false }, "dependencies": { - "cheerio": "^0.20.0", + "cheerio": "^0.22.0", "classnames": "^2.2.5", "debug": "^2.2.0", "he": "^1.1.0", - "html-minifier": "^3.0.2", + "hoist-non-react-statics": "^1.2.0", + "html-minifier": "^3.1.0", "immutable": "^3.8.1", - "jquery": "^3.1.0", - "js-beautify": "^1.6.3", - "lodash": "^4.14.2", - "react": "^15.3.0", - "react-dom": "^15.3.0", + "jquery": "^3.1.1", + "js-beautify": "^1.6.4", + "juice": "^3.0.0", + "lodash": "^4.16.2", + "mjml-validator": "~3.0.0-beta.3", + "react-dom": "^15.3.2", + "react": "^15.3.2", "warning": "^3.0.0" }, "devDependencies": { "chai": "^3.5.0", - "mocha": "^3.0.2" + "mocha": "^3.1.0" } } diff --git a/packages/mjml-core/src/Error.js b/packages/mjml-core/src/Error.js index f9492e905..f889b9322 100644 --- a/packages/mjml-core/src/Error.js +++ b/packages/mjml-core/src/Error.js @@ -1,4 +1,3 @@ - /* * Create a custom Error class */ @@ -40,7 +39,18 @@ export const EmptyMJMLError = error('EmptyMJMLError', 2) */ export const NullElementError = error('EmptyMJMLError', 3) +class MJMLValidationError { + constructor (errors) { + this.errors = errors + } -/* - * TODO: Warnings - */ + getMessages () { + return this.errors.map(error => error.formattedMessage) + } + + getErrors () { + return this.errors + } +} + +export { MJMLValidationError } diff --git a/packages/mjml-core/src/MJMLElementsCollection.js b/packages/mjml-core/src/MJMLElementsCollection.js index 7181398a2..0ed2cebde 100644 --- a/packages/mjml-core/src/MJMLElementsCollection.js +++ b/packages/mjml-core/src/MJMLElementsCollection.js @@ -8,7 +8,7 @@ export const registerMJElement = Component => { const { endingTag, postRender, tagName } = Component if (!tagName) { - return warning(false, 'Component has no TagName') + return warning(false, 'Component has no tagName') } endingTag && !includes(endingTags, tagName) && endingTags.push(tagName) diff --git a/packages/mjml-core/src/MJMLRenderer.js b/packages/mjml-core/src/MJMLRenderer.js index e910c2857..7dbcd9eac 100644 --- a/packages/mjml-core/src/MJMLRenderer.js +++ b/packages/mjml-core/src/MJMLRenderer.js @@ -1,79 +1,81 @@ -import { EmptyMJMLError } from './Error' +import { EmptyMJMLError, MJMLValidationError } from './Error' import { fixLegacyAttrs, removeCDATA } from './helpers/postRender' import { parseInstance } from './helpers/mjml' import cloneDeep from 'lodash/cloneDeep' +import configParser from './parsers/config' +import curryRight from 'lodash/curryRight' +import documentParser from './parsers/document' +import defaults from 'lodash/defaults' import defaultContainer from './configs/defaultContainer' import defaultFonts from './configs/listFontsImports' +import dom from './helpers/dom' import he from 'he' import importFonts from './helpers/importFonts' import includeExternal from './includeExternal' -import isEmpty from 'lodash/isEmpty' -import MJMLElementsCollection, { postRenders, registerMJElement } from './MJMLElementsCollection' +import { html as beautify } from 'js-beautify' +import MJMLValidator from 'mjml-validator' +import MJMLElementsCollection, { postRenders } from './MJMLElementsCollection' +import isBrowser from './helpers/isBrowser' import React from 'react' import ReactDOMServer from 'react-dom/server' -import warning from 'warning' const debug = require('debug')('mjml-engine/mjml2html') +const minifyHTML = htmlDocument => { + const { minify } = require('html-minifier') + + return minify(htmlDocument, { collapseWhitespace: true, removeEmptyAttributes: true, minifyCSS: true }) +} +const beautifyHTML = htmlDocument => beautify(htmlDocument, { indent_size: 2, wrap_attributes_indent_size: 2 }) +const inlineExternal = (htmlDocument, css) => { + const juice = require('juice') + + return juice(htmlDocument, { extraCss: css, removeStyleTags: false, applyStyleTags: false, insertPreservedExtraCss: false }) +} + export default class MJMLRenderer { constructor (content, options = {}) { - this.registerDotfile() + if (!isBrowser) { + configParser() + } this.attributes = { container: defaultContainer(), defaultAttributes: {}, cssClasses: {}, + css: [], fonts: cloneDeep(defaultFonts) } this.content = content - this.options = options + this.options = defaults(options, { level: "soft", disableMjStyle: false, disableMjInclude: false, disableMinify: false }) if (typeof this.content === 'string') { - this.content = includeExternal(this.content) this.parseDocument() } } - registerDotfile () { - const fs = require('fs') - - const path = process.cwd() - - try { - fs.statSync(`${path}/.mjmlconfig`) - } catch (e) { - return warning(!isEmpty(MJMLElementsCollection), `No .mjmlconfig found in path ${path}, consider to add one`) + parseDocument () { + if (!this.options.disableMjInclude) { + this.content = includeExternal(this.content) } - try { - const mjmlConfig = JSON.parse(fs.readFileSync(`${path}/.mjmlconfig`).toString()) - const { packages } = mjmlConfig - - packages.forEach(file => { - if (!file) { - return - } - - try { - const Component = require.main.require(file) - registerMJElement(Component.default || Component) - } catch (e) { - warning(false, `.mjmlconfig file ${file} opened from ${path} has an error : ${e}`) - } - }) - } catch (e) { - warning(false, `.mjmlconfig has a ParseError: ${e}`) - } + debug('Start parsing document') + this.content = documentParser(this.content, this.attributes, this.options) + debug('Content parsed') } - parseDocument () { - const documentParser = require('./parsers/document').default + validate () { + if (this.options.level == "skip") { + return; + } - debug('Start parsing document') - this.content = documentParser(this.content, this.attributes) - debug('Content parsed') + this.errors = MJMLValidator(this.content) + + if (this.options.level == "strict" && this.errors.length > 0) { + throw new MJMLValidationError(this.errors) + } } render () { @@ -81,19 +83,27 @@ export default class MJMLRenderer { throw new EmptyMJMLError(`.render: No MJML to render in options ${this.options.toString()}`) } - const rootElemComponent = React.createElement(MJMLElementsCollection[this.content.tagName], { mjml: parseInstance(this.content, this.attributes ) }) + debug('Validating markup') + this.validate() + + const rootComponent = MJMLElementsCollection[this.content.tagName] + + if (!rootComponent) { + return { errors: this.errors } + } debug('Render to static markup') + const rootElemComponent = React.createElement(rootComponent, { mjml: parseInstance(this.content, this.attributes ) }) const renderedMJML = ReactDOMServer.renderToStaticMarkup(rootElemComponent) debug('React rendering done. Continue with special overrides.') const MJMLDocument = this.attributes.container.replace('__content__', renderedMJML) - return this.postRender(MJMLDocument) + return { errors: this.errors, html: this.postRender(MJMLDocument) } } postRender (MJMLDocument) { - const dom = require('./helpers/dom').default + const externalCSS = this.attributes.css.join('') let $ = dom.parseHTML(MJMLDocument) @@ -106,31 +116,12 @@ export default class MJMLRenderer { } }) - let finalMJMLDocument = dom.getHTML($) - finalMJMLDocument = removeCDATA(finalMJMLDocument) - - if (this.options.beautify) { - const beautify = require('js-beautify').html - - finalMJMLDocument = beautify(finalMJMLDocument, { - indent_size: 2, - wrap_attributes_indent_size: 2 - }) - } - - if (this.options.minify) { - const minify = require('html-minifier').minify - - finalMJMLDocument = minify(finalMJMLDocument, { - collapseWhitespace: true, - removeEmptyAttributes: true, - minifyCSS: true - }) - } - - finalMJMLDocument = he.decode(finalMJMLDocument) - - return finalMJMLDocument + return [ removeCDATA, + !this.options.disableMjStyle ? curryRight(inlineExternal)(externalCSS) : undefined, + this.options.beautify ? beautifyHTML : undefined, + !this.options.disableMinify && this.options.minify ? minifyHTML : undefined, + he.decode ].filter(element => typeof element == 'function') + .reduce((res, fun) => fun(res), dom.getHTML($)) } } diff --git a/packages/mjml-core/src/configs/defaultContainer.js b/packages/mjml-core/src/configs/defaultContainer.js index 5e3934337..f739b30e5 100644 --- a/packages/mjml-core/src/configs/defaultContainer.js +++ b/packages/mjml-core/src/configs/defaultContainer.js @@ -23,6 +23,13 @@ export default () => { </o:OfficeDocumentSettings> </xml> <![endif]--> +<!--[if lte mso 11]> +<style type="text/css"> + .outlook-group-fix { + width:100% !important; + } +</style> +<![endif]--> </head> <body> __content__ diff --git a/packages/mjml-core/src/decorators/MJMLElement.js b/packages/mjml-core/src/decorators/MJMLElement.js index e7e737fe8..0b99f32a4 100644 --- a/packages/mjml-core/src/decorators/MJMLElement.js +++ b/packages/mjml-core/src/decorators/MJMLElement.js @@ -1,11 +1,11 @@ import { widthParser, defaultUnit } from '../helpers/mjAttribute' import Immutable from 'immutable' -import merge from 'lodash/merge' import MJMLElementsCollection from '../MJMLElementsCollection' import React, { Component } from 'react' import ReactDOMServer from 'react-dom/server' import trim from 'lodash/trim' -import warning from 'warning' +import merge from 'lodash/merge' +import hoistNonReactStatic from 'hoist-non-react-statics'; const getElementWidth = ({ element, siblings, parentWidth }) => { const { mjml } = element.props @@ -134,7 +134,7 @@ function createComponent (ComposedComponent) { childProps.mjml = childProps.mjml.mergeIn(['attributes', this.inheritedAttributes()]) } } else { - Object.assign(childProps, {rawPxWidth: elementsWidth[i]}) + Object.assign(childProps, { rawPxWidth: elementsWidth[i] }) if (this.mjml.get('inheritedAttributes')) { Object.assign(childProps, this.inheritedAttributes()) @@ -144,7 +144,7 @@ function createComponent (ComposedComponent) { const childWithProps = React.cloneElement(child, childProps) wrappedElements.push(childWithProps) - if (childWithProps.type.tagName !== 'mj-raw' && i < realChildren.length - 1) { + if (!childWithProps.type.rawElement && i < realChildren.length - 1) { wrappedElements.push(<div key={`outlook-${i}`} className={`${prefix}-line`} data-width={elementsWidth[++i]} />) } }) @@ -200,8 +200,7 @@ function createComponent (ComposedComponent) { const Element = MJMLElementsCollection[tag] if (!Element) { - warning(false, `Could not find element for : ${tag}`) - return null + return null; } return ( @@ -213,8 +212,13 @@ function createComponent (ComposedComponent) { }) } + validChildren () { + const { children } = this.props + return ((children && React.Children.toArray(children)) || this.generateChildren()).filter(Boolean) + } + buildProps () { - const { parentMjml, children } = this.props + const { parentMjml } = this.props const childMethods = [ 'mjAttribute', @@ -236,7 +240,7 @@ function createComponent (ComposedComponent) { mjName: this.mjName(), // generate children - children: children || this.generateChildren(), + children: this.validChildren(), // siblings count, can change display sibling: siblingCount, @@ -276,8 +280,7 @@ function createComponent (ComposedComponent) { } } - return MJMLElement - + return hoistNonReactStatic(MJMLElement, ComposedComponent) } export default createComponent diff --git a/packages/mjml-core/src/helpers/dom.js b/packages/mjml-core/src/helpers/dom.js index c8cc7aa26..8984330fe 100644 --- a/packages/mjml-core/src/helpers/dom.js +++ b/packages/mjml-core/src/helpers/dom.js @@ -1,7 +1,8 @@ -const inBrowser = typeof window !== 'undefined' +import isBrowser from './isBrowser' + const dom = {} -if (inBrowser) { +if (isBrowser) { const jquery = require('jquery') const parseMarkup = str => { @@ -66,7 +67,7 @@ if (inBrowser) { dom.parseHTML = str => parseMarkup(str, { xmlMode: false, decodeEntities: false }) - dom.parseXML = str => parseMarkup(str, { xmlMode: true, decodeEntities: false }) + dom.parseXML = str => parseMarkup(str, { xmlMode: true, decodeEntities: false, withStartIndices: true }) dom.getAttributes = element => element.attribs || {} diff --git a/packages/mjml-core/src/helpers/isBrowser.js b/packages/mjml-core/src/helpers/isBrowser.js new file mode 100644 index 000000000..93c242550 --- /dev/null +++ b/packages/mjml-core/src/helpers/isBrowser.js @@ -0,0 +1 @@ +export default typeof window != 'undefined' && this === window diff --git a/packages/mjml-core/src/includeExternal.js b/packages/mjml-core/src/includeExternal.js index 3fde5e443..123cde590 100644 --- a/packages/mjml-core/src/includeExternal.js +++ b/packages/mjml-core/src/includeExternal.js @@ -1,13 +1,13 @@ import fs from 'fs' -const includes = /<mj-include\s+path=['"](.*\.mjml)['"]\s*(\/>|>\s*<\/mj-include>)/g +const includes = /<mj-include\s+path=['"](.*[\.mjml]?)['"]\s*(\/>|>\s*<\/mj-include>)/g const getContent = input => input .replace(/<mjml>[\n\s\t]+<mj-body>[\n\s\t]+<mj-container>/, '') .replace(/<\/mj-container>[\n\s\t]+<\/mj-body>[\n\s\t]+<\/mjml>/, '') -export default mjml => mjml.replace(includes, (_, path) => { +const replaceContent = (_, path) => { const mjmlExtension = file => file.trim().match(/.mjml$/) && file || `${file}.mjml` const template = fs.readFileSync(mjmlExtension(path), 'utf8') @@ -16,4 +16,14 @@ export default mjml => mjml.replace(includes, (_, path) => { if (!content) { throw new Error(`Error while parsing file: ${path}`) } return content -}) +} + +export default (baseMjml) => { + let mjml = baseMjml + + while (mjml.match(includes)) { + mjml = mjml.replace(includes, replaceContent) + } + + return mjml +} diff --git a/packages/mjml-core/src/index.js b/packages/mjml-core/src/index.js index 0cd4e89c0..ce16028da 100644 --- a/packages/mjml-core/src/index.js +++ b/packages/mjml-core/src/index.js @@ -1,6 +1,5 @@ -import warning from 'warning' - import MJMLRenderer from './MJMLRenderer' +import mjmlValidator from 'mjml-validator' import elements, { registerMJElement } from './MJMLElementsCollection' import MJMLHeadElements, { registerMJHeadElement } from './MJMLHead' import * as helpers from './helpers' @@ -13,9 +12,5 @@ export const documentParser = content => { return documentParser(content) } export const version = () => '__MJML_VERSION__' +export const MJMLValidator = mjmlValidator export const mjml2html = (mjml, options = {}) => new MJMLRenderer(mjml, options).render() -export const registerElement = Component => { - warning(false, 'Please now use registerMJElement, registerElement is deprecated will no longer be supported soon') - - return registerMJElement(Component) -} diff --git a/packages/mjml-core/src/parsers/config.js b/packages/mjml-core/src/parsers/config.js new file mode 100644 index 000000000..f6127f9a1 --- /dev/null +++ b/packages/mjml-core/src/parsers/config.js @@ -0,0 +1,66 @@ +import fs from 'fs' +import path from 'path' +import warning from 'warning' +import some from 'lodash/some' +import startsWith from 'lodash/startsWith' +import isEmpty from 'lodash/isEmpty' +import MJMLElementsCollection, { registerMJElement } from '../MJMLElementsCollection' + +const cwd = process.cwd() + +const isRelativePath = (name) => { + return some(['./', '.', '../'], (matcher) => startsWith(name, matcher)) +} + +const checkIfConfigFileExist = () => { + try { + fs.statSync(`${cwd}/.mjmlconfig`) + return true + } catch (e) { + warning(!isEmpty(MJMLElementsCollection), `No .mjmlconfig found in path ${cwd}, consider to add one`) + return false + } +} + +const parseConfigFile = () => { + if (!checkIfConfigFileExist()) { + return false + } + + try { + return JSON.parse(fs.readFileSync(`${cwd}/.mjmlconfig`).toString()) + } catch (e) { + warning(false, `.mjmlconfig has a ParseError: ${e}`) + } +} + +const parsePackages = (packages) => { + if (!packages) { + return; + } + + packages.forEach(file => { + if (!file) { + return + } + + try { + const filename = path.join(process.cwd(), file) + const Component = isRelativePath(file) ? require(filename) : require.main.require(file) + + registerMJElement(Component.default || Component) + } catch (e) { + warning(false, `.mjmlconfig file ${file} opened from ${cwd} has an error : ${e}`) + } + }) +} + +export default () => { + const config = parseConfigFile() + + if (!config) { + return; + } + + parsePackages(config.packages) +} diff --git a/packages/mjml-core/src/parsers/document.js b/packages/mjml-core/src/parsers/document.js index 6d30b7a09..32aebfaf5 100644 --- a/packages/mjml-core/src/parsers/document.js +++ b/packages/mjml-core/src/parsers/document.js @@ -3,7 +3,7 @@ import compact from 'lodash/compact' import dom from '../helpers/dom' import each from 'lodash/each' import filter from 'lodash/filter' -import MJMLElements, { endingTags } from '../MJMLElementsCollection' +import { endingTags } from '../MJMLElementsCollection' import MJMLHeadElements from '../MJMLHead' import warning from 'warning' @@ -32,27 +32,24 @@ const safeEndingTags = content => { /** * converts MJML body into a JSON representation */ -const mjmlElementParser = elem => { +const mjmlElementParser = (elem, content) => { if (!elem) { throw new NullElementError('Null element found in mjmlElementParser') } + const findLine = content.substr(0, elem.startIndex).match(/\n/g) + const lineNumber = findLine ? findLine.length + 1 : 1 const tagName = elem.tagName.toLowerCase() const attributes = dom.getAttributes(elem) - const element = { tagName, attributes } - - if (!MJMLElements[tagName]) { - warning(false, `Unregistered element: ${tagName}, skipping it`) - return - } + const element = { tagName, attributes, lineNumber } if (endingTags.indexOf(tagName) !== -1) { - const $ = dom.parseXML(elem) - element.content = $(tagName).html().trim() + const $local = dom.parseXML(elem) + element.content = $local(tagName).html().trim() } else { const children = dom.getChildren(elem) - element.children = children ? compact(filter(children, child => child.tagName).map(mjmlElementParser)) : [] + element.children = children ? compact(filter(children, child => child.tagName).map(child => mjmlElementParser(child, content))) : [] } return element @@ -81,25 +78,25 @@ const parseHead = (head, attributes) => { * - mjml: a json representation of the mjml */ const documentParser = (content, attributes) => { - let root + const safeContent = safeEndingTags(content) + + let body let head try { - const $ = dom.parseXML(safeEndingTags(content)) - root = $('mjml > mj-body') + const $ = dom.parseXML(safeContent) + + body = $('mjml > mj-body') head = $('mjml > mj-head') - if (root.length < 1) { - root = $('mj-body').get(0) - warning(false, 'Please upgrade your MJML markup to add a <mjml> root tag, <mj-body> as root will no longer be supported soon, see https://github.com/mjmlio/mjml/blob/master/UPGRADE.md') - } else { - root = root.children().get(0) + if (body.length > 0) { + body = body.children().get(0) } } catch (e) { throw new ParseError('Error while parsing the file') } - if (!root || root.length < 1) { + if (!body || body.length < 1) { throw new EmptyMJMLError('No root "<mjml>" or "<mj-body>" found in the file') } @@ -107,7 +104,7 @@ const documentParser = (content, attributes) => { parseHead(head.get(0), attributes) } - return mjmlElementParser(root) + return mjmlElementParser(body, safeContent) } export default documentParser diff --git a/packages/mjml-core/test/MockComponent.js b/packages/mjml-core/test/MockComponent.js index 90e48d574..94b10c0e1 100644 --- a/packages/mjml-core/test/MockComponent.js +++ b/packages/mjml-core/test/MockComponent.js @@ -3,7 +3,11 @@ import React, { Component } from 'react' const tagName = 'mj-mock' const endingTag = true +const parentTag = ['mj-mock-list'] const defaultMJMLDefinition = { + attributes: { + + } } @MJMLElement @@ -19,6 +23,7 @@ class MockComponent extends Component { MockComponent.tagName = tagName MockComponent.endingTag = endingTag +MockComponent.parentTag = parentTag MockComponent.defaultMJMLDefinition = defaultMJMLDefinition export default MockComponent diff --git a/packages/mjml-core/test/MockListComponent.js b/packages/mjml-core/test/MockListComponent.js index facb75fbf..526af8e74 100644 --- a/packages/mjml-core/test/MockListComponent.js +++ b/packages/mjml-core/test/MockListComponent.js @@ -2,7 +2,10 @@ import { MJMLElement } from '../src/index' import React, { Component } from 'react' const tagName = 'mj-mock-list' +const parentTag = ['mj-body'] const defaultMJMLDefinition = { + attributes: { + } } @MJMLElement @@ -17,6 +20,7 @@ class MockListComponent extends Component { } MockListComponent.tagName = tagName +MockListComponent.parentTag = parentTag MockListComponent.defaultMJMLDefinition = defaultMJMLDefinition export default MockListComponent diff --git a/packages/mjml-core/test/input-output.spec.js b/packages/mjml-core/test/input-output.spec.js index 25a997799..9205afada 100644 --- a/packages/mjml-core/test/input-output.spec.js +++ b/packages/mjml-core/test/input-output.spec.js @@ -29,11 +29,8 @@ describe('MJML Renderer', () => { expect(() => new MJMLRenderer(` <mjml> <mj-body> - <mj-container> - <mj-column /> - </mj-container> </mj-body> - </mjml>`).render() + </mjml>`, { level: "skip" }).render() ).to.throw(/EmptyMJMLError/) }) }) @@ -47,7 +44,7 @@ describe('MJML Renderer', () => { <mj-mock /> </mj-mock-list> </mj-body> - </mjml>`).render() + </mjml>`, { level: "skip" }).render().html ).to.not.contain('Mocked Component!') }) }) @@ -62,7 +59,7 @@ describe('MJML Renderer', () => { <mj-mock /> </mj-mock-list> </mj-body> - </mjml>`).render() + </mjml>`, { level: "skip" }).render().html ).to.contain('Mocked Component!') }) }) diff --git a/packages/mjml-divider/package.json b/packages/mjml-divider/package.json index 1b929b351..5c3d78f4b 100644 --- a/packages/mjml-divider/package.json +++ b/packages/mjml-divider/package.json @@ -1,7 +1,7 @@ { "name": "mjml-divider", "description": "mjml-divider", - "version": "2.0.11", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-divider/src/index.js b/packages/mjml-divider/src/index.js index 1a62788fd..782118710 100644 --- a/packages/mjml-divider/src/index.js +++ b/packages/mjml-divider/src/index.js @@ -3,19 +3,28 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-divider' +const parentTag = ['mj-column', 'mj-hero-content'] +const selfClosingTag = true const defaultMJMLDefinition = { attributes: { + 'align': null, 'border-color': '#000000', 'border-style': 'solid', 'border-width': '4px', + 'container-background-color': null, + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, 'padding': '10px 25px', + 'vertical-align': null, 'width': '100%' } } const baseStyles = { p: { fontSize: '1px', - margin: '0 auto' + margin: '0px auto' } } const postRender = $ => { @@ -37,12 +46,12 @@ class Divider extends Component { styles = this.getStyles() getStyles () { - const { mjAttribute } = this.props + const { mjAttribute, defaultUnit } = this.props return merge({}, baseStyles, { p: { - borderTop: `${mjAttribute('border-width')} ${mjAttribute('border-style')} ${mjAttribute('border-color')}`, - width: mjAttribute('width') + borderTop: `${defaultUnit(mjAttribute('border-width'))} ${mjAttribute('border-style')} ${mjAttribute('border-color')}`, + width: defaultUnit(mjAttribute('width')) } }) } @@ -53,12 +62,11 @@ class Divider extends Component { const { width, unit } = helpers.widthParser(mjAttribute('width')) switch (unit) { - case '%': { + case '%': return parentWidth * width / 100 - } - default: { + + default: return width - } } } @@ -74,6 +82,8 @@ class Divider extends Component { } Divider.tagName = tagName +Divider.parentTag = parentTag +Divider.selfClosingTag = selfClosingTag Divider.defaultMJMLDefinition = defaultMJMLDefinition Divider.baseStyles = baseStyles Divider.postRender = postRender diff --git a/packages/mjml-group/package.json b/packages/mjml-group/package.json index b11a33915..e7006d8c6 100644 --- a/packages/mjml-group/package.json +++ b/packages/mjml-group/package.json @@ -1,7 +1,7 @@ { "name": "mjml-group", "description": "mjml-group", - "version": "2.0.4", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://github.com/mjmlio/mjml", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-group/src/index.js b/packages/mjml-group/src/index.js index 8c2ac89ac..6237c3981 100644 --- a/packages/mjml-group/src/index.js +++ b/packages/mjml-group/src/index.js @@ -3,6 +3,14 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-group' +const parentTag = ['mj-section', 'mj-navbar'] +const defaultMJMLDefinition = { + attributes: { + 'width': null, + 'background-color': null, + 'vertical-align': null + } +} const baseStyles = { div: { verticalAlign: 'top' @@ -10,12 +18,16 @@ const baseStyles = { } const postRender = $ => { $('.mj-group-outlook-open').each(function () { + const $parent = $(this).parent() + const mjGroupBg = $parent.data('mj-group-background') const $columnDiv = $(this).next() + const bgColor = mjGroupBg ? `bgcolor="${mjGroupBg}"` : `` $(this).replaceWith(`${helpers.startConditionalTag} - <table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:${$columnDiv.data('vertical-align')};width:${parseInt($(this).data('width'))}px;"> + <table ${bgColor} role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:${$columnDiv.data('vertical-align')};width:${parseInt($(this).data('width'))}px;"> ${helpers.endConditionalTag}`) + $parent.removeAttr('data-mj-group-background') $columnDiv.removeAttr('data-vertical-align') }) @@ -48,16 +60,13 @@ class Group extends Component { return merge({}, baseStyles, { div: { + background: mjAttribute('background-color'), display: 'inline-block', verticalAlign: mjAttribute('vertical-align'), fontSize: '0px', lineHeight: '0px', textAlign: 'left', width: '100%' - }, - table: { - verticalAlign: mjAttribute('vertical-align'), - background: mjAttribute('background-color') } }) } @@ -100,6 +109,7 @@ class Group extends Component { className={mjGroupClass} data-column-width={parseInt(width)} data-vertical-align={this.styles.div.verticalAlign} + data-mj-group-background={mjAttribute('background-color')} style={this.styles.div}> {renderWrappedOutlookChildren(this.renderChildren())} </div> @@ -111,5 +121,7 @@ class Group extends Component { Group.tagName = tagName Group.baseStyles = baseStyles Group.postRender = postRender +Group.parentTag = parentTag +Group.defaultMJMLDefinition = defaultMJMLDefinition export default Group diff --git a/packages/mjml-head-attributes/package.json b/packages/mjml-head-attributes/package.json index 1fc26d138..a3f1d22db 100644 --- a/packages/mjml-head-attributes/package.json +++ b/packages/mjml-head-attributes/package.json @@ -1,7 +1,7 @@ { "name": "mjml-head-attributes", "description": "mjml-head-attributes", - "version": "2.0.7", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,6 +13,6 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2" + "lodash": "^4.16.2" } } diff --git a/packages/mjml-head-font/package.json b/packages/mjml-head-font/package.json index 9361f3b35..a126a6c99 100644 --- a/packages/mjml-head-font/package.json +++ b/packages/mjml-head-font/package.json @@ -1,7 +1,7 @@ { "name": "mjml-head-font", "description": "mjml-head-font", - "version": "2.0.1", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -11,5 +11,8 @@ "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, - "homepage": "https://mjml.io" + "homepage": "https://mjml.io", + "dependencies": { + "lodash": "^4.16.2" + } } diff --git a/packages/mjml-head-font/src/index.js b/packages/mjml-head-font/src/index.js index f34133caa..30832a6a1 100644 --- a/packages/mjml-head-font/src/index.js +++ b/packages/mjml-head-font/src/index.js @@ -1,6 +1,14 @@ +import _ from "lodash" + export default { name: "mj-font", handler: (el, { fonts }) => { - fonts.push({ name: el.attribs.name, url: el.attribs.href }) + const font = _.find(fonts, ['name', el.attribs.name]) + + if (font) { + font.url = el.attribs.href + } else { + fonts.push({ name: el.attribs.name, url: el.attribs.href }) + } } } diff --git a/packages/mjml-head-style/.npmignore b/packages/mjml-head-style/.npmignore new file mode 100644 index 000000000..246c4aa25 --- /dev/null +++ b/packages/mjml-head-style/.npmignore @@ -0,0 +1,3 @@ +node_modules +src +test diff --git a/packages/mjml-head-style/README.md b/packages/mjml-head-style/README.md new file mode 100644 index 000000000..22f346596 --- /dev/null +++ b/packages/mjml-head-style/README.md @@ -0,0 +1,32 @@ +## mjml-style + +This tag allows you to use CSS styles for the <b>HTML</b> in your MJML document. This CSS style will be inlined on the final HTML document. + + ```xml + <mjml> + <mj-head> + <mj-style> + .red-color { + color: red; + } + </mj-style> + </mj-head> + <mj-body> + <mj-container> + <mj-section> + <mj-column> + <mj-text> + <p class="red-color">Hello World!</p> + </mj-text> + </mj-column> + </mj-section> + </mj-container> + </mj-body> + </mjml> + ``` + +<p align="center"> + <a href="https://mjml.io/try-it-live/components/head-style"> + <img width="100px" src="http://imgh.us/TRYITLIVE.svg" alt="sexy" /> + </a> +</p> diff --git a/packages/mjml-head-style/package.json b/packages/mjml-head-style/package.json new file mode 100644 index 000000000..6ab6c0866 --- /dev/null +++ b/packages/mjml-head-style/package.json @@ -0,0 +1,15 @@ +{ + "name": "mjml-head-style", + "description": "mjml-head-style", + "version": "3.0.0-beta.3", + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/mjmlio/mjml.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/mjmlio/mjml/issues" + }, + "homepage": "https://mjml.io" +} diff --git a/packages/mjml-head-style/src/index.js b/packages/mjml-head-style/src/index.js new file mode 100644 index 000000000..6ac0fb6cb --- /dev/null +++ b/packages/mjml-head-style/src/index.js @@ -0,0 +1,8 @@ +export default { + name: "mj-style", + handler: (el, { css }) => { + const innerText = el.children.map(child => child.type === 'text' && child.data).join('') + + css.push(innerText) + } +} diff --git a/packages/mjml-head-title/package.json b/packages/mjml-head-title/package.json index 331e7e9c2..177dd6983 100644 --- a/packages/mjml-head-title/package.json +++ b/packages/mjml-head-title/package.json @@ -1,7 +1,7 @@ { "name": "mjml-head-title", "description": "mjml-head-title", - "version": "2.0.1", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", diff --git a/packages/mjml-hero/package.json b/packages/mjml-hero/package.json index dc61f31ce..ea1bebce0 100644 --- a/packages/mjml-hero/package.json +++ b/packages/mjml-hero/package.json @@ -1,7 +1,7 @@ { "name": "mjml-hero", "description": "mjml-hero", - "version": "2.0.10", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-hero/src/Hero.js b/packages/mjml-hero/src/Hero.js index a3e3b3266..7b5a85b3a 100644 --- a/packages/mjml-hero/src/Hero.js +++ b/packages/mjml-hero/src/Hero.js @@ -3,14 +3,20 @@ import React, { Component } from 'react' import merge from 'lodash/merge' const tagName = 'mj-hero' +const parentTag = ['mj-container'] const defaultMJMLDefinition = { attributes: { 'mode': 'fixed-height', 'height': '0px', + 'background-url': null, 'background-width': '0px', 'background-height': '0px', 'background-position': 'center center', 'padding': '0px', + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, 'background-color': '#ffffff' } } @@ -255,5 +261,6 @@ Hero.defaultMJMLDefinition = defaultMJMLDefinition Hero.endingTag = endingTag Hero.baseStyles = baseStyles Hero.postRender = postRender +Hero.parentTag = parentTag export default Hero diff --git a/packages/mjml-hero/src/HeroContent.js b/packages/mjml-hero/src/HeroContent.js index 271e25ff9..930d4357f 100644 --- a/packages/mjml-hero/src/HeroContent.js +++ b/packages/mjml-hero/src/HeroContent.js @@ -3,6 +3,7 @@ import React, { Component } from 'react' import merge from 'lodash/merge' const tagName = 'mj-hero-content' +const parentTag = ['mj-hero'] const defaultMJMLDefinition = { attributes: { 'width': '100%', @@ -115,5 +116,6 @@ HeroContent.defaultMJMLDefinition = defaultMJMLDefinition HeroContent.endingTag = endingTag HeroContent.baseStyles = baseStyles HeroContent.postRender = postRender +HeroContent.parentTag = parentTag export default HeroContent diff --git a/packages/mjml-html/package.json b/packages/mjml-html/package.json index 12a4fa99a..e0eadf12e 100644 --- a/packages/mjml-html/package.json +++ b/packages/mjml-html/package.json @@ -1,7 +1,7 @@ { "name": "mjml-html", "description": "mjml-html", - "version": "2.0.9", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-html/src/index.js b/packages/mjml-html/src/index.js index 0c9c79053..f5bc2c0dd 100644 --- a/packages/mjml-html/src/index.js +++ b/packages/mjml-html/src/index.js @@ -3,13 +3,21 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-html' +const parentTag = ['mj-column', 'mj-hero-content'] +const endingTag = true const defaultMJMLDefinition = { content: '', attributes: { - 'padding': '0px' + 'align': null, + 'container-background-color': null, + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, + 'padding': '0px', + 'vertical-align': null } } -const endingTag = true const baseStyles = { div: { fontSize: '13px' @@ -38,8 +46,9 @@ class Html extends Component { } Html.tagName = tagName -Html.defaultMJMLDefinition = defaultMJMLDefinition +Html.parentTag = parentTag Html.endingTag = endingTag +Html.defaultMJMLDefinition = defaultMJMLDefinition Html.baseStyles = baseStyles export default Html diff --git a/packages/mjml-image/package.json b/packages/mjml-image/package.json index 8409dac2f..01560a246 100644 --- a/packages/mjml-image/package.json +++ b/packages/mjml-image/package.json @@ -1,7 +1,7 @@ { "name": "mjml-image", "description": "mjml-image", - "version": "2.0.10", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-image/src/index.js b/packages/mjml-image/src/index.js index c02b6d342..d212e4a4a 100644 --- a/packages/mjml-image/src/index.js +++ b/packages/mjml-image/src/index.js @@ -4,21 +4,30 @@ import min from 'lodash/min' import React, { Component } from 'react' const tagName = 'mj-image' +const parentTag = ['mj-column', 'mj-hero-content'] +const endingTag = true +const selfClosingTag = true const defaultMJMLDefinition = { attributes: { - 'height': 'auto', - 'padding': '10px 25px', 'align': 'center', 'alt': '', 'border': 'none', 'border-radius': '', + 'container-background-color': null, + 'height': 'auto', 'href': '', + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, + 'padding': '10px 25px', 'src': '', 'target': '_blank', - 'title': '' + 'title': '', + 'vertical-align': null, + 'width': null } } -const endingTag = true const baseStyles = { table: { borderCollapse: 'collapse', @@ -43,7 +52,7 @@ class Image extends Component { const { mjAttribute, getPadding } = this.props const parentWidth = mjAttribute('parentWidth') - const width = min([parseInt(mjAttribute('width')), parseInt(parentWidth)]) + const width = mjAttribute('width') ? min([parseInt(mjAttribute('width')), parseInt(parentWidth)]) : parseInt(parentWidth) const paddingRight = getPadding('right') const paddingLeft = getPadding('left') @@ -57,7 +66,7 @@ class Image extends Component { return merge({}, baseStyles, { td: { - width: this.getContentWidth() + width: defaultUnit(this.getContentWidth()) }, img: { border: mjAttribute('border'), @@ -118,8 +127,10 @@ class Image extends Component { } Image.tagName = tagName -Image.defaultMJMLDefinition = defaultMJMLDefinition +Image.parentTag = parentTag Image.endingTag = endingTag +Image.selfClosingTag = selfClosingTag +Image.defaultMJMLDefinition = defaultMJMLDefinition Image.baseStyles = baseStyles export default Image diff --git a/packages/mjml-invoice/package.json b/packages/mjml-invoice/package.json index 4d000c8bf..1323f8af5 100644 --- a/packages/mjml-invoice/package.json +++ b/packages/mjml-invoice/package.json @@ -1,7 +1,7 @@ { "name": "mjml-invoice", "description": "mjml-invoice", - "version": "2.0.11", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,10 +13,10 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "mjml-table": "^2.0.9", + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "mjml-table": "~3.0.0-beta.3", "numeral": "^1.5.3", - "react": "^15.3.0" + "react": "^15.3.2" } } diff --git a/packages/mjml-invoice/src/Invoice.js b/packages/mjml-invoice/src/Invoice.js index 27676e807..b2653ffa2 100644 --- a/packages/mjml-invoice/src/Invoice.js +++ b/packages/mjml-invoice/src/Invoice.js @@ -6,12 +6,15 @@ import numeral from 'numeral' import React, { Component } from 'react' const tagName = 'mj-invoice' +const parentTag = ['mj-column'] const defaultMJMLDefinition = { attributes: { 'border': '1px solid #ecedee', 'color': '#b9b9b9', + 'container-background-color': null, 'font-family': 'Roboto, Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', + 'format': null, 'intl': 'name:Name;price:Price;quantity:Quantity', 'line-height': '22px' } @@ -133,7 +136,7 @@ class Invoice extends Component { </tr> </thead> <tbody> - {children} + {React.Children.map(children, child => React.cloneElement(child, { columnElement: true }))} </tbody> <tfoot> <tr style={this.styles.tfoot}> @@ -157,5 +160,6 @@ Invoice.tagName = tagName Invoice.defaultMJMLDefinition = defaultMJMLDefinition Invoice.baseStyles = baseStyles Invoice.intl = intl +Invoice.parentTag = parentTag export default Invoice diff --git a/packages/mjml-invoice/src/InvoiceItem.js b/packages/mjml-invoice/src/InvoiceItem.js index 70b4104eb..44905ef2e 100644 --- a/packages/mjml-invoice/src/InvoiceItem.js +++ b/packages/mjml-invoice/src/InvoiceItem.js @@ -3,6 +3,8 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-invoice-item' +const parentTag = ['mj-invoice'] +const endingTag = true const defaultMJMLDefinition = { attributes: { 'color': '#747474', @@ -15,7 +17,6 @@ const defaultMJMLDefinition = { 'text-align': 'left' } } -const endingTag = true const baseStyles = { td: { fontWeight: '500', @@ -41,8 +42,8 @@ class InvoiceItem extends Component { td: { color: mjAttribute('color'), fontFamily: mjAttribute('font-family'), - fontSize: defaultUnit(mjAttribute('font-size'), "px"), - padding: defaultUnit(mjAttribute('padding'), "px"), + fontSize: defaultUnit(mjAttribute('font-size')), + padding: defaultUnit(mjAttribute('padding')), textAlign: mjAttribute('text-align') } }) @@ -68,8 +69,9 @@ class InvoiceItem extends Component { } InvoiceItem.tagName = tagName -InvoiceItem.defaultMJMLDefinition = defaultMJMLDefinition +InvoiceItem.parentTag = parentTag InvoiceItem.endingTag = endingTag +InvoiceItem.defaultMJMLDefinition = defaultMJMLDefinition InvoiceItem.baseStyles = baseStyles export default InvoiceItem diff --git a/packages/mjml-list/package.json b/packages/mjml-list/package.json index 91cd63278..a7577c671 100644 --- a/packages/mjml-list/package.json +++ b/packages/mjml-list/package.json @@ -1,7 +1,7 @@ { "name": "mjml-list", "description": "mjml-list", - "version": "2.0.9", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-list/src/index.js b/packages/mjml-list/src/index.js index deb81d142..b04ed5ad4 100644 --- a/packages/mjml-list/src/index.js +++ b/packages/mjml-list/src/index.js @@ -3,18 +3,25 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-list' +const parentTag = ['mj-column', 'mj-hero-content'] +const endingTag = true const defaultMJMLDefinition = { content: '', attributes: { 'align': 'left', 'color': '#000000', + 'container-background-color': null, 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'line-height': '22px', - 'padding': '10px 25px' + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, + 'padding': '10px 25px', + 'vertical-align': null } } -const endingTag = true const baseStyles = { ul: { display: 'inline-block', @@ -35,8 +42,8 @@ class List extends Component { ul: { color: mjAttribute('color'), fontFamily: mjAttribute('font-family'), - fontSize: defaultUnit(mjAttribute('font-size'), "px"), - lineHeight: mjAttribute('line-height') + fontSize: defaultUnit(mjAttribute('font-size')), + lineHeight: defaultUnit(mjAttribute('line-height')) } }) } @@ -54,8 +61,9 @@ class List extends Component { } List.tagName = tagName -List.defaultMJMLDefinition = defaultMJMLDefinition +List.parentTag = parentTag List.endingTag = endingTag +List.defaultMJMLDefinition = defaultMJMLDefinition List.baseStyles = baseStyles export default List diff --git a/packages/mjml-location/package.json b/packages/mjml-location/package.json index 1d2d9c8ee..4cb1864fd 100644 --- a/packages/mjml-location/package.json +++ b/packages/mjml-location/package.json @@ -1,7 +1,7 @@ { "name": "mjml-location", "description": "mjml-location", - "version": "2.0.10", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,10 +13,10 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "mjml-image": "^2.0.9", - "mjml-text": "^2.0.9", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "mjml-image": "~3.0.0-beta.3", + "mjml-text": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-location/src/index.js b/packages/mjml-location/src/index.js index 1d6b5074d..a3e2445bc 100644 --- a/packages/mjml-location/src/index.js +++ b/packages/mjml-location/src/index.js @@ -4,17 +4,27 @@ import MJMLText from 'mjml-text' import React, { Component } from 'react' const tagName = 'mj-location' +const parentTag = ['mj-column', 'mj-hero-content'] +const endingTag = true +const selfClosingTag = true const defaultMJMLDefinition = { attributes: { + 'address': null, + 'align': null, 'color': '#3aa7ed', + 'container-background-color': null, 'font-family': 'Roboto, sans-serif', 'font-size': '18px', 'font-weight': '500', + 'img-src': 'http://i.imgur.com/DPCJHhy.png', + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, 'padding': '10px 25px', - 'img-src': 'http://i.imgur.com/DPCJHhy.png' + 'vertical-align': null } } -const endingTag = true @MJMLElement class Location extends Component { @@ -33,17 +43,19 @@ class Location extends Component { } getAttributes () { - const { mjAttribute } = this.props + const { mjAttribute, defaultUnit } = this.props return { text: { + 'columnElement': true, 'font-family': mjAttribute('font-family'), - 'font-size': mjAttribute('font-size'), + 'font-size': defaultUnit(mjAttribute('font-size')), 'font-weight': mjAttribute('font-weight'), 'padding': '0px', 'text-decoration': mjAttribute('text-decoration') }, img: { + 'columnElement': true, 'padding': '0px', 'src': mjAttribute('img-src') } @@ -85,7 +97,9 @@ class Location extends Component { } Location.tagName = tagName -Location.defaultMJMLDefinition = defaultMJMLDefinition +Location.parentTag = parentTag Location.endingTag = endingTag +Location.defaultMJMLDefinition = defaultMJMLDefinition +Location.selfClosingTag = selfClosingTag export default Location diff --git a/packages/mjml-navbar/README.md b/packages/mjml-navbar/README.md index 6ed776817..562415f41 100644 --- a/packages/mjml-navbar/README.md +++ b/packages/mjml-navbar/README.md @@ -37,13 +37,20 @@ Displays a full width section for navigation attribute | unit | description | default value --------------------|-------------|--------------------------------|--------------- +full-width | string | make the section full-width | n/a +border | string | css border format | none +border-bottom | string | css border format | n/a +border-left | string | css border format | n/a +border-right | string | css border format | n/a +border-top | string | css border format | n/a +border-radius | px | border radius | n/a background-color | color | section color | n/a background-url | url | background url | n/a background-repeat | string | css background repeat | repeat background-size | percent/px | css background size | auto vertical-align | string | css vertical-align | top text-align | string | css text-align | center -padding | px | supports up to 4 parameters | 20px 0 +padding | px | supports up to 4 parameters | 10px 25px padding-top | px | section top offset | n/a padding-bottom | px | section bottom offset | n/a padding-left | px | section left offset | n/a diff --git a/packages/mjml-navbar/package.json b/packages/mjml-navbar/package.json index 7713c21d2..5c10a11da 100644 --- a/packages/mjml-navbar/package.json +++ b/packages/mjml-navbar/package.json @@ -1,7 +1,7 @@ { "name": "mjml-navbar", "description": "mjml-navbar", - "version": "2.0.8", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,9 +13,9 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "mjml-section": "^2.0.10", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "mjml-section": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-navbar/src/InlineLinks.js b/packages/mjml-navbar/src/InlineLinks.js index e3237d79f..61ea906fd 100644 --- a/packages/mjml-navbar/src/InlineLinks.js +++ b/packages/mjml-navbar/src/InlineLinks.js @@ -5,9 +5,12 @@ import React, { Component } from 'react' import crypto from 'crypto' const tagName = 'mj-inline-links' +const parentTag = ['mj-column'] const defaultMJMLDefinition = { attributes: { 'align': 'center', + 'base-url': null, + 'hamburger': null, 'ico-align': 'center', 'ico-open': '9776', 'ico-close': '8855', @@ -52,9 +55,9 @@ const postRender = $ => { .each(function () { $(this) .prepend(`<!--[if gte mso 9]> - <table role="presentation" border="0" cellpadding="0" cellspacing="0" align="${$(this).data('align')}"> + <table role="presentation" border="0" cellpadding="0" cellspacing="0" align="${$(this).data('align')}"> <tr> - <![endif]-->`) + <![endif]-->`) .append(`<!--[if gte mso 9]> </tr> </table> @@ -182,5 +185,6 @@ InlineLinks.tagName = tagName InlineLinks.defaultMJMLDefinition = defaultMJMLDefinition InlineLinks.baseStyles = baseStyles InlineLinks.postRender = postRender +InlineLinks.parentTag = parentTag export default InlineLinks diff --git a/packages/mjml-navbar/src/Link.js b/packages/mjml-navbar/src/Link.js index c0f6be5b7..73acb0ebf 100644 --- a/packages/mjml-navbar/src/Link.js +++ b/packages/mjml-navbar/src/Link.js @@ -3,6 +3,7 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-link' +const parentTag = ['mj-inline-links'] const defaultMJMLDefinition = { attributes: { 'padding': '15px 10px', @@ -10,6 +11,7 @@ const defaultMJMLDefinition = { 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'font-weight': 'normal', + 'href': null, 'line-height': '22px' } } @@ -85,5 +87,6 @@ Link.defaultMJMLDefinition = defaultMJMLDefinition Link.baseStyles = baseStyles Link.endingTag = endingTag Link.postRender = postRender +Link.parentTag = parentTag export default Link diff --git a/packages/mjml-navbar/src/Navbar.js b/packages/mjml-navbar/src/Navbar.js index f9fe54a2c..d6f2a5b26 100644 --- a/packages/mjml-navbar/src/Navbar.js +++ b/packages/mjml-navbar/src/Navbar.js @@ -3,11 +3,27 @@ import MJMLSection from 'mjml-section' import React, { Component } from 'react' const tagName = 'mj-navbar' +const parentTag = ['mj-container'] const defaultMJMLDefinition = { attributes: { - 'navbar-hamburger': '', + 'background-color': null, + 'background-url': null, + 'background-repeat': 'repeat', + 'background-size': 'auto', + 'border': null, + 'border-bottom': null, + 'border-left': null, + 'border-radius': null, + 'border-right': null, + 'border-top': null, + 'full-width': null, 'padding': '10px 25px', - 'width': '100%' + 'padding-top': null, + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'text-align': 'center', + 'vertical-align': 'top' } } @@ -28,5 +44,6 @@ class Navbar extends Component { Navbar.tagName = tagName Navbar.defaultMJMLDefinition = defaultMJMLDefinition +Navbar.parentTag = parentTag export default Navbar diff --git a/packages/mjml-raw/package.json b/packages/mjml-raw/package.json index 64f5e3a91..e6e392825 100644 --- a/packages/mjml-raw/package.json +++ b/packages/mjml-raw/package.json @@ -1,7 +1,7 @@ { "name": "mjml-raw", "description": "mjml-raw", - "version": "2.0.9", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,7 +13,7 @@ }, "homepage": "https://mjml.io", "dependencies": { - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-raw/src/index.js b/packages/mjml-raw/src/index.js index 2f8f7b5b0..726c1814a 100644 --- a/packages/mjml-raw/src/index.js +++ b/packages/mjml-raw/src/index.js @@ -2,7 +2,13 @@ import { MJMLElement } from 'mjml-core' import React, { Component } from 'react' const tagName = 'mj-raw' +const parentTag = ['mj-body', 'mj-container', 'mj-section', 'mj-column'] const endingTag = true +const rawElement = true +const defaultMJMLDefinition = { + attributes: { + } +} const postRender = $ => { $('.mj-raw').each(function () { $(this).replaceWith($(this).html()) @@ -38,7 +44,10 @@ class Raw extends Component { } Raw.tagName = tagName +Raw.parentTag = parentTag Raw.endingTag = endingTag +Raw.rawElement = rawElement Raw.postRender = postRender +Raw.defaultMJMLDefinition = defaultMJMLDefinition export default Raw diff --git a/packages/mjml-section/README.md b/packages/mjml-section/README.md index 1e8377af1..df18adb0b 100644 --- a/packages/mjml-section/README.md +++ b/packages/mjml-section/README.md @@ -25,9 +25,19 @@ The `full-width` property will be used to manage the background width. By default, it will be 600px. With the `full-width` property on, it will be changed to 100%. +<aside class="notice"> + <b>Inverting the order in which columns display:</b> set the `direction` attribute to `rtl` to change the order in which columns display on desktop. Because MJML is mobile-first, structure the columns in the <b>order you want them to stack on mobile</b>, and use `direction` to change the order they display <b>on desktop</b>. +</aside> + attribute | unit | description | default value --------------------|-------------|--------------------------------|--------------- full-width | string | make the section full-width | n/a +border | string | css border format | none +border-bottom | string | css border format | n/a +border-left | string | css border format | n/a +border-right | string | css border format | n/a +border-top | string | css border format | n/a +border-radius | px | border radius | n/a background-color | color | section color | n/a background-url | url | background url | n/a background-repeat | string | css background repeat | repeat @@ -39,3 +49,4 @@ padding-top | px | section top offset | n/a padding-bottom | px | section bottom offset | n/a padding-left | px | section left offset | n/a padding-right | px | section right offset | n/a +direction | string | ltr / rtl | ltr diff --git a/packages/mjml-section/package.json b/packages/mjml-section/package.json index 34f9bc43f..ec9393a2f 100644 --- a/packages/mjml-section/package.json +++ b/packages/mjml-section/package.json @@ -1,7 +1,7 @@ { "name": "mjml-section", "description": "mjml-section", - "version": "2.0.10", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-section/src/index.js b/packages/mjml-section/src/index.js index 012f89f37..5d52c1be6 100644 --- a/packages/mjml-section/src/index.js +++ b/packages/mjml-section/src/index.js @@ -4,16 +4,33 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-section' +const parentTag = ['mj-container'] const defaultMJMLDefinition = { attributes: { + 'background-color': null, + 'background-url': null, 'background-repeat': 'repeat', + 'background-size': 'auto', + 'border': null, + 'border-bottom': null, + 'border-left': null, + 'border-radius': null, + 'border-right': null, + 'border-top': null, + 'direction': 'ltr', + 'full-width': null, 'padding': '20px 0', - 'background-size': 'auto' + 'padding-top': null, + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'text-align': 'center', + 'vertical-align': 'top' } } const baseStyles = { div: { - margin: '0 auto' + margin: '0px auto' }, table: { fontSize: '0px', @@ -101,6 +118,13 @@ class Section extends Component { return merge({}, baseStyles, { td: { + border: mjAttribute('border'), + borderBottom: mjAttribute('border-bottom'), + borderLeft: mjAttribute('border-left'), + borderRadius: defaultUnit(mjAttribute('border-radius'), "px"), + borderRight: mjAttribute('border-right'), + borderTop: mjAttribute('border-top'), + direction: mjAttribute('direction'), fontSize: '0px', padding: defaultUnit(mjAttribute('padding'), 'px'), paddingBottom: defaultUnit(mjAttribute('padding-bottom'), 'px'), @@ -178,6 +202,7 @@ class Section extends Component { } Section.tagName = tagName +Section.parentTag = parentTag Section.defaultMJMLDefinition = defaultMJMLDefinition Section.baseStyles = baseStyles Section.postRender = postRender diff --git a/packages/mjml-social/README.md b/packages/mjml-social/README.md index 0f743dc31..e6722db25 100644 --- a/packages/mjml-social/README.md +++ b/packages/mjml-social/README.md @@ -84,6 +84,7 @@ color | color | text color base-url | string | icon base url | https://www.mailjet.com/images/theme/v1/icons/ico-social/ display | string | List of social icons to display separated by a space, | facebook twitter google | | available values: `facebook google instagram pinterest linkedin twitter` | +inner-padding | px | social network surrounding padding | 4px padding | px | supports up to 4 parameters | 10px 25px padding-top | px | top offset | n/a padding-bottom | px | bottom offset | n/a diff --git a/packages/mjml-social/package.json b/packages/mjml-social/package.json index c1274f22b..a90122655 100644 --- a/packages/mjml-social/package.json +++ b/packages/mjml-social/package.json @@ -1,7 +1,7 @@ { "name": "mjml-social", "description": "mjml-social", - "version": "2.0.10", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-social/src/index.js b/packages/mjml-social/src/index.js index f416aa8b0..c02d93f8e 100644 --- a/packages/mjml-social/src/index.js +++ b/packages/mjml-social/src/index.js @@ -5,8 +5,15 @@ import clone from 'lodash/clone' import React, { Component } from 'react' const tagName = 'mj-social' +const parentTag = ['mj-column', 'mj-hero-content'] +const selfClosingTag = true const defaultMJMLDefinition = { attributes: { + 'align': 'center', + 'base-url': 'https://www.mailjet.com/images/theme/v1/icons/ico-social/', + 'color': '#333333', + 'container-background-color': null, + 'display': 'facebook:share twitter:share google:share', 'facebook-content': 'Share', 'facebook-href': '[[SHORT_PERMALINK]]', 'facebook-icon-color' : '#3b5998', @@ -16,6 +23,7 @@ const defaultMJMLDefinition = { 'google-href': '[[SHORT_PERMALINK]]', 'google-icon-color': '#dc4e41', 'icon-size': '20px', + 'inner-padding': null, 'instagram-content': 'Share', 'instagram-href': '[[SHORT_PERMALINK]]', 'instagram-icon-color': '#3f729b', @@ -23,8 +31,12 @@ const defaultMJMLDefinition = { 'linkedin-content': 'Share', 'linkedin-href': '[[SHORT_PERMALINK]]', 'linkedin-icon-color' : '#0077b5', - 'padding': '10px 25px', 'mode': 'horizontal', + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, + 'padding': '10px 25px', 'pinterest-content': 'Pin it', 'pinterest-href': '[[SHORT_PERMALINK]]', 'pinterest-icon-color': '#bd081c', @@ -33,10 +45,7 @@ const defaultMJMLDefinition = { 'twitter-content': 'Tweet', 'twitter-href': '[[SHORT_PERMALINK]]', 'twitter-icon-color': '#55acee', - 'align': 'center', - 'color': '#333333', - 'display': 'facebook:share twitter:share google:share', - 'base-url': 'https://www.mailjet.com/images/theme/v1/icons/ico-social/' + 'vertical-align': null } } const baseStyles = { @@ -135,6 +144,9 @@ class Social extends Component { lineHeight: defaultUnit(mjAttribute('line-height'), 'px'), textDecoration: mjAttribute('text-decoration') }, + td1: { + padding: defaultUnit(mjAttribute('inner-padding')) + }, td2: { width: defaultUnit(mjAttribute('icon-size'), 'px'), height: defaultUnit(mjAttribute('icon-size'), 'px') @@ -151,7 +163,7 @@ class Social extends Component { isInTextMode () { const { mjAttribute } = this.props - return mjAttribute('text-mode') == true || mjAttribute('text-mode') == 'true' + return mjAttribute('text-mode') === true || mjAttribute('text-mode') === 'true' } renderSocialButton (platform, share) { @@ -235,7 +247,7 @@ class Social extends Component { return null } - return this.renderSocialButton(platform, share != "url") + return this.renderSocialButton(platform, share !== 'url') }) } @@ -297,6 +309,8 @@ class Social extends Component { } Social.tagName = tagName +Social.parentTag = parentTag +Social.selfClosingTag = selfClosingTag Social.defaultMJMLDefinition = defaultMJMLDefinition Social.baseStyles = baseStyles Social.buttonDefinitions = buttonDefinitions diff --git a/packages/mjml-spacer/package.json b/packages/mjml-spacer/package.json index 45624a205..3fbd89895 100644 --- a/packages/mjml-spacer/package.json +++ b/packages/mjml-spacer/package.json @@ -1,7 +1,7 @@ { "name": "mjml-spacer", "description": "mjml-spacer", - "version": "2.0.8", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,7 +13,7 @@ }, "homepage": "https://mjml.io", "dependencies": { - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-spacer/src/index.js b/packages/mjml-spacer/src/index.js index c9f982e60..474019891 100644 --- a/packages/mjml-spacer/src/index.js +++ b/packages/mjml-spacer/src/index.js @@ -2,9 +2,18 @@ import { MJMLElement } from 'mjml-core' import React, { Component } from 'react' const tagName = 'mj-spacer' +const parentTag = ['mj-column', 'mj-hero-content'] +const selfClosingTag = true const defaultMJMLDefinition = { attributes: { - 'height': '20px' + 'align': null, + 'container-background-color': null, + 'height': '20px', + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, + 'vertical-align': null } } @@ -19,7 +28,7 @@ class Spacer extends Component { return { div: { fontSize: '1px', - lineHeight: defaultUnit(mjAttribute('height'), 'px') + lineHeight: defaultUnit(mjAttribute('height')) } } } @@ -35,6 +44,8 @@ class Spacer extends Component { } Spacer.tagName = tagName +Spacer.parentTag = parentTag +Spacer.selfClosingTag = selfClosingTag Spacer.defaultMJMLDefinition = defaultMJMLDefinition export default Spacer diff --git a/packages/mjml-table/package.json b/packages/mjml-table/package.json index 67908f488..5e49eb1a8 100644 --- a/packages/mjml-table/package.json +++ b/packages/mjml-table/package.json @@ -1,7 +1,7 @@ { "name": "mjml-table", "description": "mjml-table", - "version": "2.0.9", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,7 +13,7 @@ }, "homepage": "https://mjml.io", "dependencies": { - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-table/src/index.js b/packages/mjml-table/src/index.js index 64b9e7494..821eab34c 100644 --- a/packages/mjml-table/src/index.js +++ b/packages/mjml-table/src/index.js @@ -2,6 +2,8 @@ import { MJMLElement } from 'mjml-core' import React, { Component } from 'react' const tagName = 'mj-table' +const parentTag = ['mj-column', 'mj-hero-content'] +const endingTag = true const defaultMJMLDefinition = { content: '', attributes: { @@ -9,15 +11,20 @@ const defaultMJMLDefinition = { 'cellpadding': '0', 'cellspacing': '0', 'color': '#000', + 'container-background-color': null, 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'line-height': '22px', + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, 'padding': '10px 25px', 'table-layout': 'auto', + 'vertical-align': null, 'width': '100%' } } -const endingTag = true @MJMLElement class Table extends Component { @@ -29,10 +36,12 @@ class Table extends Component { return { table: { + cellpadding: mjAttribute('cellspadding'), + cellspacing: mjAttribute('cellspacing'), color: mjAttribute('color'), fontFamily: mjAttribute('font-family'), - fontSize: defaultUnit(mjAttribute('font-size'), "px"), - lineHeight: defaultUnit(mjAttribute('line-height'), "px"), + fontSize: defaultUnit(mjAttribute('font-size')), + lineHeight: defaultUnit(mjAttribute('line-height')), tableLayout: mjAttribute('table-layout') } } @@ -55,7 +64,8 @@ class Table extends Component { } Table.tagName = tagName -Table.defaultMJMLDefinition = defaultMJMLDefinition +Table.parentTag = parentTag Table.endingTag = endingTag +Table.defaultMJMLDefinition = defaultMJMLDefinition export default Table diff --git a/packages/mjml-text/package.json b/packages/mjml-text/package.json index be14bb5f0..702b89690 100644 --- a/packages/mjml-text/package.json +++ b/packages/mjml-text/package.json @@ -1,7 +1,7 @@ { "name": "mjml-text", "description": "mjml-text", - "version": "2.0.9", + "version": "3.0.0-beta.3", "main": "lib/index.js", "repository": { "type": "git", @@ -13,8 +13,8 @@ }, "homepage": "https://mjml.io", "dependencies": { - "lodash": "^4.14.2", - "mjml-core": "^2.3.2", - "react": "^15.3.0" + "lodash": "^4.16.2", + "mjml-core": "~3.0.0-beta.3", + "react": "^15.3.2" } } diff --git a/packages/mjml-text/src/index.js b/packages/mjml-text/src/index.js index 59e2cbce7..413c4776a 100644 --- a/packages/mjml-text/src/index.js +++ b/packages/mjml-text/src/index.js @@ -3,18 +3,29 @@ import merge from 'lodash/merge' import React, { Component } from 'react' const tagName = 'mj-text' +const parentTag = ['mj-column', 'mj-hero-content'] +const endingTag = true const defaultMJMLDefinition = { content: '', attributes: { 'align': 'left', 'color': '#000000', + 'container-background-color': null, 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', + 'font-style': null, + 'font-weight': null, 'line-height': '22px', - 'padding': '10px 25px' + 'padding-bottom': null, + 'padding-left': null, + 'padding-right': null, + 'padding-top': null, + 'padding': '10px 25px', + 'text-decoration': null, + 'text-transform': null, + 'vertical-align': null } } -const endingTag = true const baseStyles = { div: { cursor: 'auto' @@ -33,7 +44,7 @@ class Text extends Component { div: { color: mjAttribute('color'), fontFamily: mjAttribute('font-family'), - fontSize: defaultUnit(mjAttribute('font-size'), "px"), + fontSize: defaultUnit(mjAttribute('font-size')), fontStyle: mjAttribute('font-style'), fontWeight: mjAttribute('font-weight'), lineHeight: defaultUnit(mjAttribute('line-height'), "px"), @@ -56,8 +67,9 @@ class Text extends Component { } Text.tagName = tagName -Text.defaultMJMLDefinition = defaultMJMLDefinition +Text.parentTag = parentTag Text.endingTag = endingTag +Text.defaultMJMLDefinition = defaultMJMLDefinition Text.baseStyles = baseStyles export default Text diff --git a/packages/mjml-validator/.npmignore b/packages/mjml-validator/.npmignore new file mode 100644 index 000000000..246c4aa25 --- /dev/null +++ b/packages/mjml-validator/.npmignore @@ -0,0 +1,3 @@ +node_modules +src +test diff --git a/packages/mjml-validator/README.md b/packages/mjml-validator/README.md new file mode 100644 index 000000000..d90e22b53 --- /dev/null +++ b/packages/mjml-validator/README.md @@ -0,0 +1,20 @@ +# mjml-validator + +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: +- skip: your document is rendered without going through validation +- soft (default): your document is going through validation and is rendered, even if it has errors +- strict: your document is going through validation and is not rendered if it has any error + +## In CLI + +When using the `mjml` command line, you can add the option `-l` or `--level` with the validation level you want. Ex: `mjml -l strict -r my_template.mjml` + +## In Javascript + +In Javascript, you can provide the level through the `options` parameters on `MJMLRenderer`. Ex: `new MJMLRenderer(inputMJML, { level: strict })` + +`strict` will raise a `MJMLValidationError` exception. This object has 2 methods: +- `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. +- `getMessages` returns an array of `formattedMessage`. + +When using `soft`, no exception will be raised. You can get the errors using the object returned by the `render` method `errors`. It is the same object returned by `getErrors` on strict mode. diff --git a/packages/mjml-validator/package.json b/packages/mjml-validator/package.json new file mode 100644 index 000000000..fb0a846e9 --- /dev/null +++ b/packages/mjml-validator/package.json @@ -0,0 +1,18 @@ +{ + "name": "mjml-validator", + "description": "mjml-validator", + "version": "3.0.0-beta.3", + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/mjmlio/mjml.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/mjmlio/mjml/issues" + }, + "homepage": "https://mjml.io", + "dependencies": { + "lodash": "^4.16.1" + } +} diff --git a/packages/mjml-validator/src/index.js b/packages/mjml-validator/src/index.js new file mode 100644 index 000000000..7dae1427a --- /dev/null +++ b/packages/mjml-validator/src/index.js @@ -0,0 +1,17 @@ +import concat from 'lodash/concat' +import filter from 'lodash/filter' +import values from 'lodash/values' +import * as rules from './rules' + +const validateNode = element => { + const { children } = element + let errors = concat([], ...values(rules).map(rule => rule(element))) + + if (children && children.length > 0) { + errors = concat(errors, ...children.map(validateNode)) + } + + return filter(errors) +} + +export default validateNode diff --git a/packages/mjml-validator/src/rules/index.js b/packages/mjml-validator/src/rules/index.js new file mode 100644 index 000000000..73292abc1 --- /dev/null +++ b/packages/mjml-validator/src/rules/index.js @@ -0,0 +1,3 @@ +export * from './validAttributes' +export * from './validChildren' +export * from './validTag' diff --git a/packages/mjml-validator/src/rules/ruleError.js b/packages/mjml-validator/src/rules/ruleError.js new file mode 100644 index 000000000..f1e959c93 --- /dev/null +++ b/packages/mjml-validator/src/rules/ruleError.js @@ -0,0 +1,10 @@ +export default (message, element) => { + const { lineNumber: line, tagName } = element + + return { + line, + message, + tagName, + formattedMessage: `Line ${line} (${tagName}) — ${message}` + } +} diff --git a/packages/mjml-validator/src/rules/validAttributes.js b/packages/mjml-validator/src/rules/validAttributes.js new file mode 100644 index 000000000..ade0a9ff2 --- /dev/null +++ b/packages/mjml-validator/src/rules/validAttributes.js @@ -0,0 +1,26 @@ +import { elements } from 'mjml-core' +import concat from 'lodash/concat' +import keys from 'lodash/keys' +import includes from 'lodash/includes' +import filter from 'lodash/filter' +import ruleError from './ruleError' + +const WHITELIST = [ 'mj-class' ] + +export const validateAttribute = (element) => { + const { attributes, tagName } = element + const Component = elements[tagName] + + if (!Component) { + return; + } + + const avalaibleAttributes = concat(keys(Component.defaultMJMLDefinition.attributes), WHITELIST) + const unknownAttributes = filter(keys(attributes), attribute => !includes(avalaibleAttributes, attribute) ) + + if (unknownAttributes.length == 0) { + return; + } + + return ruleError(`${unknownAttributes.length > 1 ? "Attributes" : "Attribute"} ${unknownAttributes.join(', ')} ${unknownAttributes.length > 1 ? "are illegal" : "is illegal"}`, element) +} diff --git a/packages/mjml-validator/src/rules/validChildren.js b/packages/mjml-validator/src/rules/validChildren.js new file mode 100644 index 000000000..ef2014513 --- /dev/null +++ b/packages/mjml-validator/src/rules/validChildren.js @@ -0,0 +1,32 @@ +import { elements } from 'mjml-core' +import filter from 'lodash/filter' +import includes from 'lodash/includes' +import ruleError from './ruleError' + +export const validChildren = (element) => { + const { children, tagName } = element + const Component = elements[tagName] + + if (!Component) { + return; + } + + if (!children || children.length == 0) { + return; + } + + return filter(children.map((child) => { + const childTagName = child.tagName + const ChildComponent = elements[childTagName] + + if (!ChildComponent) { + return null; + } + + if (includes(ChildComponent.parentTag, tagName)) { + return null; + } + + return ruleError(`${ChildComponent.tagName} cannot be used inside ${tagName}, only inside: ${ChildComponent.parentTag.join(', ')}`, child) + })) +} diff --git a/packages/mjml-validator/src/rules/validTag.js b/packages/mjml-validator/src/rules/validTag.js new file mode 100644 index 000000000..d298d837b --- /dev/null +++ b/packages/mjml-validator/src/rules/validTag.js @@ -0,0 +1,11 @@ +import { elements } from 'mjml-core' +import ruleError from './ruleError' + +export const validateTag = (element) => { + const { tagName } = element + const Component = elements[tagName] + + if (!Component) { + return ruleError(`Element ${tagName} doesn't exist or is not registered`, element) + } +} diff --git a/packages/mjml/package.json b/packages/mjml/package.json index 79057d415..a6dc3d44a 100644 --- a/packages/mjml/package.json +++ b/packages/mjml/package.json @@ -1,7 +1,7 @@ { "name": "mjml", "description": "MJML: the only framework that makes responsive-email easy", - "version": "2.3.3", + "version": "3.0.0-beta.3", "main": "lib/index.js", "bin": { "mjml": "bin/mjml" @@ -16,28 +16,29 @@ }, "homepage": "https://mjml.io", "dependencies": { - "mjml-button": "^2.0.10", - "mjml-cli": "^2.3.2", - "mjml-column": "^2.0.10", - "mjml-container": "^2.0.10", - "mjml-core": "^2.3.2", - "mjml-divider": "^2.0.11", - "mjml-group": "^2.0.4", - "mjml-head-attributes": "^2.0.7", - "mjml-head-font": "^2.0.1", - "mjml-head-title": "^2.0.1", - "mjml-hero": "^2.0.10", - "mjml-html": "^2.0.9", - "mjml-image": "^2.0.10", - "mjml-invoice": "^2.0.11", - "mjml-list": "^2.0.9", - "mjml-location": "^2.0.10", - "mjml-navbar": "^2.0.8", - "mjml-raw": "^2.0.9", - "mjml-section": "^2.0.10", - "mjml-social": "^2.0.10", - "mjml-spacer": "^2.0.8", - "mjml-table": "^2.0.9", - "mjml-text": "^2.0.9" + "mjml-button": "~3.0.0-beta.3", + "mjml-cli": "~3.0.0-beta.3", + "mjml-column": "~3.0.0-beta.3", + "mjml-container": "~3.0.0-beta.3", + "mjml-core": "~3.0.0-beta.3", + "mjml-divider": "~3.0.0-beta.3", + "mjml-group": "~3.0.0-beta.3", + "mjml-head-attributes": "~3.0.0-beta.3", + "mjml-head-font": "~3.0.0-beta.3", + "mjml-head-title": "~3.0.0-beta.3", + "mjml-head-style": "~3.0.0-beta.3", + "mjml-hero": "~3.0.0-beta.3", + "mjml-html": "~3.0.0-beta.3", + "mjml-image": "~3.0.0-beta.3", + "mjml-invoice": "~3.0.0-beta.3", + "mjml-list": "~3.0.0-beta.3", + "mjml-location": "~3.0.0-beta.3", + "mjml-navbar": "~3.0.0-beta.3", + "mjml-raw": "~3.0.0-beta.3", + "mjml-section": "~3.0.0-beta.3", + "mjml-social": "~3.0.0-beta.3", + "mjml-spacer": "~3.0.0-beta.3", + "mjml-table": "~3.0.0-beta.3", + "mjml-text": "~3.0.0-beta.3" } } diff --git a/packages/mjml/src/index.js b/packages/mjml/src/index.js index eebc1bee8..1c0d31bfd 100644 --- a/packages/mjml/src/index.js +++ b/packages/mjml/src/index.js @@ -21,6 +21,7 @@ import Text from 'mjml-text' import MJHeadAttributes from 'mjml-head-attributes' import MJHeadFont from 'mjml-head-font' +import MJHeadStyle from 'mjml-head-style' import MJHeadTitle from 'mjml-head-title' const { Hero, HeroContent } = MJHero @@ -52,6 +53,7 @@ const { Navbar, InlineLinks, Link } = MJNavbar; [ MJHeadAttributes, MJHeadFont, + MJHeadStyle, MJHeadTitle ].map( headElement => registerMJHeadElement(headElement.name, headElement.handler)) export * from 'mjml-core' diff --git a/packages/mjml/test.html b/packages/mjml/test.html new file mode 100644 index 000000000..4c42f068d --- /dev/null +++ b/packages/mjml/test.html @@ -0,0 +1,20 @@ +<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title>
2 coloumnes $2
Check out promotions !
foo © bar ≠ baz 𝌆 qux
\ No newline at end of file diff --git a/packages/mjml/test.js b/packages/mjml/test.js index 8db441c60..e3d5a166e 100644 --- a/packages/mjml/test.js +++ b/packages/mjml/test.js @@ -1,31 +1,338 @@ var mjml = require('./lib/index') -console.log(mjml.mjml2html(` - - - Hello MJML - - +const inputMJML = ` - - - - - Check out promotions ! - - - foo © bar ≠ baz 𝌆 qux - - + + + + + [[HEADLINE]] + + + + + [[PERMALINK_LABEL]] + + + + + + + + + + + +

home               + blog            visit store  >

+
+
+
+ + + +

SPRING PROMO +

+

50% +

+

OFFER +

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit

+
+ + SHOP NOW + +
+ + + + + +
+ + + + + + + + +

FREE SHIPPING ON ORDER OVER 55€ + +

+
+
+
+ + + + + + +

CHESTERK TANK +

+

15€ + +

+
+ + BUY NOW + +
+ + + + + +

BEYOND BACKPACK +

+

20€ + +

+
+ + BUY NOW + +
+ + + + + +

JENSEN SHORTS +

+

28€ + +

+
+ + BUY NOW + +
+
+ + + + + + +

VERDANT CAP +

+

20€ + +

+
+ + BUY NOW + +
+ + + + + +

BLAKE POLO SHIRT +

+

25€ + +

+
+ + BUY NOW + +
+ + + + + +

SKETCH FLORAL +

+

23€ + +

+
+ + BUY NOW + +
+
+ + + + + + + + +

ANDERSON SWEATER +

+

75€ +

+

The Anderson Sweater features a floral all-over print with contrast colour.

+
+ + BUY NOW + +
+
+ + + +

ALDER TWO JONES JACKET +

+

100€ +

+

Colour-block design, zip entry, oxford hood lining, side pockets & TC lining.

+
+ + BUY NOW + +
+ + + + + +
+ + + +

DISCOVER OUR +

+

SUMMER COLLECTION +

+
+
+
+ + + + + + +

TOPAZ C3 SHOES +

+

70€ + +

+
+ + BUY NOW + +
+ + + + + +

CAMDEN BACKPACK +

+

50€ + +

+
+ + BUY NOW + +
+
+ + + + + + +

PAYMENT METHODS + +

+

+ We accept all majors payments options +

+
+
+ + + + + +

CURRENCIES CHOICE + +

+

+ You have the choice to pay with your own currencies +

+
+
+ + + + + +

EXPRESS SHIPPING + +

+

+ Delivered tomorrow before noon +

+
+
+
+ + + + + + + + +

Privacy policy

+
+
+ + + + + +
+ + + +

[[DELIVERY_INFO]]

+
+ +

[[POSTAL_ADDRESS]]

+
+
-
`, { beautify: true })) +
` + +try { + const { html, errors } = mjml.mjml2html(inputMJML, { beautify: true, level: "soft" }) + + if (errors) { + console.log(errors.map(e => e.formattedMessage).join('\n')) + } + + console.log(html) +} catch(e) { + if (e.getMessages) { + console.log(e.getMessages()) + } else { + throw e + } +} diff --git a/packages/mjml/test.mjml b/packages/mjml/test.mjml new file mode 100644 index 000000000..e9f9257b5 --- /dev/null +++ b/packages/mjml/test.mjml @@ -0,0 +1,29 @@ + + + + + + + 2 coloumnes $2 + + + + + Check out promotions ! + + + foo © bar ≠ baz 𝌆 qux + + + + + +