Repository: mjmlio/mjml Branch: master Commit: 4cb9615c5e48 Files: 230 Total size: 467.7 KB Directory structure: gitextract_vim4znp0/ ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── build-documentation.yml │ ├── mjml-workflow.yml │ └── npm-publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── babel.config.js ├── doc/ │ ├── basic.md │ ├── body_components.md │ ├── community-components.md │ ├── community-contributions.md │ ├── components_1.md │ ├── components_2.md │ ├── config.json │ ├── create.md │ ├── ending-tags.md │ ├── getting_started.md │ ├── guide.md │ ├── head_components.md │ ├── install.md │ ├── mjml-bar-chart.md │ ├── mjml-chart.md │ ├── mjml-chartjs.md │ ├── mjml-mso-button.md │ ├── mjml-qr-code.md │ ├── ports.md │ ├── tooling.md │ └── using_mjml_in_json.md ├── lerna.json ├── package.json ├── packages/ │ ├── mjml/ │ │ ├── README.md │ │ ├── bin/ │ │ │ └── mjml │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.js │ │ └── test/ │ │ ├── accordion-fontFamily.test.js │ │ ├── accordion-padding.test.js │ │ ├── accordionTitle-fontWeight.test.js │ │ ├── carousel-hoverSupported.test.js │ │ ├── column-border-radius.test.js │ │ ├── html-attributes.test.js │ │ ├── html-comments.test.js │ │ ├── lazy-head-style.test.js │ │ ├── navbar-ico-padding.test.js │ │ ├── social-align.test.js │ │ ├── social-icon-height.test.js │ │ ├── table-cellspacing.test.js │ │ ├── tableWidth.test.js │ │ ├── wrapper-border-radius.test.js │ │ └── wrapper-gap.test.js │ ├── mjml-accordion/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── Accordion.js │ │ ├── AccordionElement.js │ │ ├── AccordionText.js │ │ ├── AccordionTitle.js │ │ └── index.js │ ├── mjml-body/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-browser/ │ │ ├── README.md │ │ ├── browser-mocks/ │ │ │ ├── fs.js │ │ │ ├── path.js │ │ │ └── uglify-js.js │ │ ├── package.json │ │ └── webpack.config.js │ ├── mjml-button/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-carousel/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── Carousel.js │ │ ├── CarouselImage.js │ │ └── index.js │ ├── mjml-cli/ │ │ ├── README.md │ │ ├── bin/ │ │ │ └── mjml │ │ ├── package.json │ │ └── src/ │ │ ├── client.js │ │ ├── commands/ │ │ │ ├── outputToConsole.js │ │ │ ├── outputToFile.js │ │ │ ├── readFile.js │ │ │ ├── readStream.js │ │ │ └── watchFiles.js │ │ └── helpers/ │ │ ├── defaultOptions.js │ │ └── fileContext.js │ ├── mjml-column/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-core/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components.js │ │ │ ├── createComponent.js │ │ │ ├── helpers/ │ │ │ │ ├── conditionalTag.js │ │ │ │ ├── fonts.js │ │ │ │ ├── formatAttributes.js │ │ │ │ ├── genRandomHexString.js │ │ │ │ ├── jsonToXML.js │ │ │ │ ├── makeLowerBreakpoint.js │ │ │ │ ├── mediaQueries.js │ │ │ │ ├── mergeOutlookConditionnals.js │ │ │ │ ├── minifyOutlookConditionnals.js │ │ │ │ ├── mjmlconfig.js │ │ │ │ ├── preview.js │ │ │ │ ├── shorthandParser.js │ │ │ │ ├── skeleton.js │ │ │ │ ├── styles.js │ │ │ │ ├── suffixCssClasses.js │ │ │ │ └── widthParser.js │ │ │ ├── index.js │ │ │ └── types/ │ │ │ ├── boolean.js │ │ │ ├── color.js │ │ │ ├── enum.js │ │ │ ├── helpers/ │ │ │ │ └── colors.js │ │ │ ├── index.js │ │ │ ├── integer.js │ │ │ ├── string.js │ │ │ ├── type.js │ │ │ └── unit.js │ │ └── tests/ │ │ ├── .eslintrc │ │ ├── index.js │ │ ├── jsonToXml-test.js │ │ ├── mergeOutlookConditionnals-test.js │ │ ├── minifyOutlookConditionnals-test.js │ │ ├── shorthandParser-test.js │ │ ├── skeleton-test.js │ │ └── widthParser-test.js │ ├── mjml-divider/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-group/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head/ │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head-attributes/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head-breakpoint/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head-font/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head-html-attributes/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head-preview/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head-style/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-head-title/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-hero/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-image/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-migrate/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── cli.js │ │ ├── config.js │ │ └── migrate.js │ ├── mjml-navbar/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── Navbar.js │ │ ├── NavbarLink.js │ │ └── index.js │ ├── mjml-parser-xml/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── helpers/ │ │ │ │ ├── cleanNode.js │ │ │ │ ├── convertBooleansOnAttrs.js │ │ │ │ └── setEmptyAttributes.js │ │ │ └── index.js │ │ └── test/ │ │ ├── incl.mjml │ │ ├── test-preprocessors.js │ │ ├── test-utils.js │ │ ├── test-values.js │ │ └── test.js │ ├── mjml-preset-core/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── dependencies.js │ │ └── index.js │ ├── mjml-raw/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-section/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-social/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── Social.js │ │ ├── SocialElement.js │ │ └── index.js │ ├── mjml-spacer/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-table/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-text/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── mjml-validator/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── MJMLRulesCollection.js │ │ ├── dependencies.js │ │ ├── index.js │ │ └── rules/ │ │ ├── errorAttr.js │ │ ├── ruleError.js │ │ ├── validAttributes.js │ │ ├── validChildren.js │ │ ├── validTag.js │ │ └── validTypes.js │ └── mjml-wrapper/ │ ├── README.md │ ├── package.json │ └── src/ │ └── index.js ├── readme-ja.md ├── test.js └── type.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org # A special property that should be specified at the top of the file outside of # any sections. Set to true to stop .editor config file search on current file root = true # Indentation style # Possible values - tab, space indent_style = space # Indentation size in single-spaced characters # Possible values - an integer, tab indent_size = 2 # Line ending file format # Possible values - lf, crlf, cr end_of_line = lf # File character encoding # Possible values - latin1, utf-8, utf-16be, utf-16le charset = utf-8 # Denotes whether to trim whitespace at the end of lines # Possible values - true, false trim_trailing_whitespace = true # Denotes whether file should end with a newline # Possible values - true, false insert_final_newline = true ================================================ FILE: .eslintignore ================================================ node_modules lib packages/mjml-core/src/types* type.js test-html-attributes.js test.js babel.config.js ================================================ FILE: .eslintrc ================================================ { "extends": ["airbnb-base", "prettier"], "parser": "babel-eslint", "rules": { "comma-dangle": [2, "always-multiline"], "semi": [2, "never"], "no-mixed-operators": 0, "no-shadow": 0, "no-param-reassign": 0, "no-restricted-syntax": 0, }, "env": { "node": true, "mocha": true, }, "overrides": [ { "files": ["packages/mjml/test/*.test.js"], "rules": { "prefer-arrow-callback": "off", "func-names": "off", }, }, ], } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Create a file with this MJML code: `...` 2. Render it to HTML by doing '...' 3. Send the HTML to an email address with '...' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **MJML environment (please complete the following information):** - OS: [e.g. MacOS] - MJML Version [e.g. 4.2.0] - MJML tool used [e.g MJML App] **Email sending environment(for rendering issues)**: - Platform used to send the email [e.g [Putsmail](https://putsmail.com/)] **Affected email clients (for rendering issues):** - Email Client [e.g Gmail] - OS: [e.g. Windows] - Browser [e.g. Google Chrome] **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/build-documentation.yml ================================================ name: Build Documentation on: push: branches: - master jobs: buildDoc: runs-on: ubuntu-latest steps: - name: Trigger Documentation Build uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.documentation_token }} repository: mjmlio/slate event-type: build-doc ================================================ FILE: .github/workflows/mjml-workflow.yml ================================================ name: Mjml CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Run linting & tests run: | yarn install yarn build yarn lint yarn test ================================================ FILE: .github/workflows/npm-publish.yml ================================================ name: Publish Package to NPM on: workflow_dispatch: inputs: release_type: description: 'Version bump type' required: true default: 'patch' type: choice options: - patch - minor - major jobs: build: runs-on: ubuntu-latest permissions: contents: write id-token: write steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - run: yarn install - run: yarn build - run: yarn build-browser - name: Configure git user run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Bump version id: bump run: | yarn lerna version ${{ github.event.inputs.release_type }} --no-push --no-git-tag-version --yes --no-private --force-publish NEW_VERSION=$(jq -r '.version' lerna.json) echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT - name: Commit & tag release run: | git add lerna.json packages/*/package.json git commit -m "chore(release): v${{ steps.bump.outputs.version }}" git push origin HEAD - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.bump.outputs.version }} generate_release_notes: true - name: Publish packages to NPM run: yarn lerna publish from-package --yes ================================================ FILE: .gitignore ================================================ .DS_Store *.log .idea/ lib node_modules test.html /**/npmignore /testing-manual ================================================ FILE: .prettierignore ================================================ package.json ================================================ FILE: .prettierrc ================================================ { "printWidth": 80, "semi": false, "singleQuote": true, "trailingComma": "all" } ================================================ FILE: CONTRIBUTING.md ================================================ # What should I know before I get started? ## Code of Conduct This project adheres to the Contributor Covenant [code of conduct](https://contributor-covenant.org/version/1/4/). By participating, you are expected to uphold this code. Please report unacceptable behavior to [support@mjml.io](mailto:support@mjml.io). ## Packages MJML is made up of different [packages](https://github.com/mjmlio/mjml/tree/master/packages), which make it very modular but might also make it hard for you to know how it is organized. There are 3 types of packages: * `mjml-core`: the engine that renders mjml components * `mjml-cli`: the client, base on the mjml-core interface * `mjml`: a standalone client including the standard library of components * one standalone package for each component # How Can I Contribute? ## Reporting Bugs Here are the guidelines to help maintainers and the community better understand and solve your issue. ## Before Submitting a Bug Report or Enhancement * **Check the [FAQ](https://mjml.io/faq)** for a list of common questions and problems * **Check the [documentation](https://documentation.mjml.io/)** for more details on how to use MJML, MJML components, how to create a custom component and more * **Search [issues](https://github.com/mjmlio/mjml/issues?utf8=%E2%9C%93&q=is%3Aissue+)** and **[pull requests](https://github.com/mjmlio/mjml/pulls?utf8=%E2%9C%93&q=is%3Apr+)** to see if a similar one might have been already asked before ## How To Submit A Good Bug Report or Enhancement? Explain the problem you’re facing and include as many details as you can to help maintainers reproduce the problem: * **Use a clear and self-explanatory title** * **Provide all the specific information that might be needed to reproduce the problem, such as:** * **How you’re using MJML** (whether you’re using the [try it live](https://mjml.io/try-it-live), [running it locally](https://github.com/mjmlio/mjml/releases), [using the app](https://github.com/mjmlio/mjml-app), or any other way) * The **version of MJML** you’re using * The **MJML code** you used to encounter this bug, as copy/pasteable snippets, using [Markdown Code Blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/) * The **name and version of the email client(s)** on which a bug is encountered * **Screenshots** of the issue / behaviour before enhancement on the **given email clients** * Explain why **what you encountered is a bug** / how your enhancement would **improve MJML**: what did you expect to see and why? * If you want MJML to support a new styling attribute, **add screenshots **from Litmus or Email On Acid showing that this attribute is **supported for [email clients supported by MJML](https://mjml.io/faq#email-clients)** ## Template For Submitting Bug Reports [Short description of problem here] **Reproduction Steps:** 1. [First Step] 2. [Second Step] 3. [Other Steps...] **Expected behavior:** [Describe expected behavior here] **Observed behavior:** [Describe observed behavior here] **Screenshots and GIFs** ![Screenshots and GIFs which follow reproduction steps to demonstrate the problem](url) **MJML version:** [Enter MJML version here] **Email clients the bug is seen on:** [Enter email clients names and versions here] ## Your First Code Contribution If you’re not sure how you can contribute to MJML, start looking for the [beginner](https://github.com/mjmlio/mjml/labels/Beginner) and [help-wanted](https://github.com/mjmlio/mjml/labels/Community%20help%20wanted) labels. ## How to Submit A Good Pull Request * Document your code * Update the documentation (example: table of a component’s supported attributes if you add an attribute to this component) * Test your pull request locally * Include screenshots from [Litmus](https://litmus.com/) or [Email On Acid](https://www.emailonacid.com/) showing that your feature is supported for [email clients supported by MJML](https://mjml.io/faq#email-clients) * Provide the MJML code you used to test locally and on the screenshots * We suggest following the [React Styleguide](https://github.com/airbnb/javascript/tree/master/react) by Airbnb # Additional Notes ## Discussions vs Bugs & Enhancements ## Tags categories Type of issue and issue state #### Type of Issue and Issue State | Label name | Description | | --- | --- | | `Feature request` | Feature requests or improvements | | `Bug` | Confirmed bugs or reports likely to be bugs | | `Community-help-wanted` | The MJML team would appreciate help from the community in implementing these issues | | `Beginner` | Less complex issues that would be good first issues to work on for users who want to contribute to MJML | | `More information needed` | We need more information to solve this issue (see [How to submit a good bug report or enhancement]( https://github.com/mjmlio/mjml/blob/master/CONTRIBUTING.md#how-to-submit-a-good-bug-report-or-enhancement)) | | `Needs reproduction` | Likely bugs we couldn’t reproduce | | `Duplicate` | Issues that are duplicates of other issues | | `Invalid` | Issues which aren’t valid (e.g user errors) | | `Tooling idea` | Feature requests that might be good candidates for tools around MJML instead of extending MJML | #### Topic categories | Label name | Description | | --- | --- | | `Not rendering` | the engine won’t render a template without a valid reason | | `General rendering issue` | the HTML rendered is not responsive while respecting MJML’s best practices | | `Email client name` | The HTML rendered is not responsive for a specific email client | | `CLI`| issues related to the MJML Command Line Interface | | `Documentation` | issues related to the MJML documentation | #### Pull Requests labels | Label name | Description | | --- | --- | | `Work in progress` | PR which are still being worked on, more changes will follow | | `Needs review `| Pull requests which need code review and approval | | `Under review` | PR being reviewed | | `Requires changes` | PR which need to be updated based on review comments and then reviewed again | `Needs testing` | PRs which need testing on [Litmus](https://litmus.com/) or [Email On Acid](https://www.emailonacid.com/) | ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2016 Mailjet SAS, https://mjml.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # MJML 4 If you're looking for MJML 3.3.X check [this branch](https://github.com/mjmlio/mjml/tree/3.3.x)

github actions

| Translated documentation | Introduction | Installation | Usage |

--- # Translated documentation | Language | Link for documentation | | :-: | :-: | | 日本語 | [日本語ドキュメント](https://github.com/mjmlio/mjml/blob/master/readme-ja.md) | # Introduction `MJML` is a markup language created by [Mailjet](https://www.mailjet.com/) and designed to reduce the pain of coding a responsive email. Its semantic syntax makes the language easy and straightforward while its rich standard components library shortens your development time and lightens your email codebase. MJML’s open-source engine takes care of translating the `MJML` you wrote into responsive HTML.

# Installation You can install `MJML` with `NPM` to use it with NodeJS or the Command Line Interface. If you're not sure what those are, head over to Usage for other ways to use MJML. ```bash npm install mjml ``` # Development To work on MJML, make changes and create merge requests, download and install [yarn](https://yarnpkg.com/lang/en/docs/install/) for easy development. ```bash git clone https://github.com/mjmlio/mjml.git && cd mjml yarn yarn build ``` You can also run `yarn build:watch` to rebuild the package as you code. # Usage ## Online Don't want to install anything? Use the free online editor!

try it live


## Applications and plugins MJML comes with an ecosystem of tools and plugins, check out: - The [MJML App](https://mjmlio.github.io/mjml-app/) (MJML is included) - [Visual Studio Code plugin](https://github.com/mjmlio/vscode-mjml) (MJML is included) - [Sublime Text plugin](https://packagecontrol.io/packages/MJML-syntax) (MJML needs to be installed separately) For more tools, check the [Community](https://mjml.io/community) page. ## Command line interface > Compiles the file and outputs the HTML generated in `output.html` ```bash mjml input.mjml -o output.html ``` You can pass optional `arguments` to the CLI and combine them. argument | description | default value ---------|--------|-------------- `mjml -m [input]` | Migrates a v3 MJML file to the v4 syntax | NA `mjml [input] -o [output]` | Writes the output to [output] | NA `mjml [input] -s` | Writes the output to `stdout` | NA `mjml -w [input]` | Watches the changes made to `[input]` (file or folder) | NA `mjml [input] --config.beautify` | Beautifies the output (`true` or `false`) | true `mjml [input] --config.minify` | Minifies the output (`true` or `false`) | false See [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more information about config options. ## Inside Node.js ```javascript import mjml2html from 'mjml' /* Compile an mjml string */ const htmlOutput = mjml2html(` Hello World! `, options) /* Print the responsive HTML generated and MJML errors if any */ console.log(htmlOutput) ``` You can pass optional `options` as an object to the `mjml2html` function: option | unit | description | default value -------------|--------|--------------|--------------- fonts | object | Default fonts imported in the HTML rendered by MJML | See in [index.js](https://github.com/mjmlio/mjml/blob/master/packages/mjml-core/src/index.js#L100-L108) keepComments | boolean | Option to keep comments in the HTML output | true ignoreIncludes | boolean | Option to ignore mj-includes | false beautify | boolean | Option to beautify the HTML output | false minify | boolean | Option to minify the HTML output | false validationLevel | string | Available values for the [validator](https://github.com/mjmlio/mjml/tree/master/packages/mjml-validator#validating-mjml): 'strict', 'soft', 'skip' | 'soft' filePath | string | Path of file, used for relative paths in mj-includes | '.' preprocessors | array of functions | Preprocessors applied to the xml before parsing. Input must be xml, not json. Functions must be (xml: string) => string | [] juicePreserveTags | Preserve some tags when inlining css, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | NA minifyOptions | Options for html minifier, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | NA mjmlConfigPath | string | The path or directory of the `.mjmlconfig` file (for custom components use) | `process.cwd()` useMjmlConfigOptions | Allows to use the `options` attribute from `.mjmlconfig` file | false ## Client-side (in browser) ```javascript var mjml2html = require('mjml-browser') /* Compile a mjml string */ var htmlOutput = mjml2html(` Hello World! `, options) /* Print the responsive HTML generated and MJML errors if any */ console.log(htmlOutput) ``` ## API A free-to-use MJML API is available to make it easy to integrate MJML in your application. Head over [here](https://mjml.io/api) to learn more about the API. # MJML Slack MJML wouldn't be as cool without its amazing community. Head over the [Community Slack](https://join.slack.com/t/mjml/shared_invite/zt-gqmwfwmr-kPBnfuuB7wof5httaTcXxg) to meet fellow MJML'ers. # Contributors - [Maxime](https://github.com/iRyusa) - [Nicolas](https://github.com/ngarnier) - [Cedric](https://github.com/kmcb777) - [Loeck](https://github.com/lohek) - [Robin](https://github.com/robink) - [Guillaume](https://github.com/GuillaumeBadi) - [Meriadec](https://github.com/meriadec) - [Arnaud](https://github.com/arnaudbreton) - [HTeuMeuLeu](https://github.com/hteumeuleu) - [Emmanuel Payet](https://github.com/epayet) - [Matthieu](https://github.com/swibge) - [Rogier](https://github.com/rogierslag) ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [['@babel/env', { targets: { node: '10' }, include: ['transform-classes'], }]], plugins: [ '@babel/proposal-class-properties', [ '@babel/transform-runtime', { // by default the plugin assumes we have 7.0.0-beta.0 version of runtime // and inline all missing helpers instead of requiring them version: require('@babel/plugin-transform-runtime/package.json') .version, }, ], 'add-module-exports', 'lodash', ], } ================================================ FILE: doc/basic.md ================================================ ## Basic layout example In this section, you're going to learn how to code a basic email template using MJML. Here‘s what we’re building:
Basic email layout

Try it live

### Sections ```html ``` First, we’ll create the basic structure, dividing the email into six sections. #### Company Header ```html My Company ``` The first section of the email consists in a centered banner, containing only the company name. The following markup is the MJML representation of the layout we want to obtain. #### Image Header ```html Slogan here Promotion ``` Next comes a section with a background image and a block of text (representing the company slogan) and a button pointing to a page listing all the company promotions. To add the image header, you will have to replace the section's `background-color` with a `background-url`. Similarly to the first company header, you will have to center the text. The button `href` sets where the button links to. In order to have the background rendered full-width in the column, set the column width to 600px with `width="600px"`. #### Introduction Text ```html My Awesome Text Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus, sit amet suscipit nibh. Proin nec commodo purus. Sed eget nulla elit. Nulla aliquet mollis faucibus. Learn more ``` The introduction text will consist of a heading, the main text and a button. The title is a regular `mj-text` tag that can be styled as a heading. #### 2 Columns Section ```html Find amazing places Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus. ``` This section is made up of two columns. One containing an image, the other containing text. For the image, note that when a tag does not have any children, you can use the XML self-closing tag syntax: `` For the text, you are going to use two `mj-text` tags like, as previously; one with a heading style, and the other one styled as regular text. #### Icons ```html ``` This section uses a 3-column layout to display the 3 icons horizontally across the email. #### Social Icons ```html Share ``` MJML has an `mj-social` component as standard. Here, we're going to use `facebook` only, but there are several default social media sites to choose from, or you can add your own bespoke. ================================================ FILE: doc/body_components.md ================================================ ## Standard Body components Body components ease your development process by providing ready made responsive layouts that you can use to create your email template. ================================================ FILE: doc/community-components.md ================================================ ## Community components In addition to the standard components available in MJML, our awesome community is contributing by creating their own components. To use a community component, proceed as follows: - Install MJML locally with `npm install mjml` in a folder - Install the community component with `npm install {component-name}` in the same folder - Create a `.mjmlconfig` file in the same folder with this code: ```json { "packages": ["component-name/path-to-js-file"] } ``` You can now use the component in an MJML file, for example `index.mjml`, and run MJML locally in your terminal. Ensure that you’re in the folder where you installed MJML and the community component, e.g.: `./node_modules/.bin/mjml index.mjml`. ================================================ FILE: doc/community-contributions.md ================================================ ## Community Contributions The MJML ecosystem has a dedicated community that we count to help make it grow and provide it with even more awesome tools, always aiming to make development with MJML an efficient and fun process! Getting involved is really easy. If you want to contribute, feel free to [open an issue](https://github.com/mjmlio/mjml/issues) or [submit a pull-request](https://github.com/mjmlio/mjml/pulls)! Here are some tools that utilise MJML: ### Mailjet [Mailjet](https://www.mailjet.com/demo/) offers an integrated MJML workspace designed for creating, previewing, and managing email templates. Its MJML editor supports syntax highlighting, live preview, and validation, helping you move quickly while keeping your markup in good shape. The drag-and-drop editor in Mailjet is also built on MJML, so visually created templates share the same responsive structure as those coded by hand. When your template is ready, you can export it in MJML or HTML, or send emails directly through Mailjet. ### Parcel [Parcel](https://parcel.io) is the code editor built for email. This feature packed tool includes syntax highlighting, Emmet, inline documentation, autocomplete, live preview, screenshots, and full MJML, CSS, and HTML validation. Use Focus Mode to keep the preview aligned with the code you're working on, or Inspect Element to easily find the code that produces specific elements in the preview. Export MJML to HTML with a click. ### IntelliJ IDEA Plugin - MJML Support [IntelliJ IDEA](https://www.jetbrains.com/idea/) is an IDE developed by JetBrains. The plugin provides you with a (near) realtime preview, auto complete, inline documentation and code analysis. It is available on the [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/16418-mjml-support). ### Gradle Plugin - MJML Compilation [Gradle](https://gradle.org/) is a build tool for a various set of languages and environments. The plugin provides an easy way to embed your MJML templates to your Java/Kotlin application in its resources in precompiled form (HTML). It is available through the gradle plugin system [io.freefair.mjml.java](https://plugins.gradle.org/plugin/io.freefair.mjml.java) and documentation is available here [FreeFair User Guide](https://docs.freefair.io/gradle-plugins/current/reference/). ### Neos CMS [Neos CMS](https://www.neos.io/) is a content management system that combines structured content with application. This package adds the helper for compiling MJML markup as well as some prototypes which allow you to use TailwindCSS like classes in your MJML markup. It is available on [packagist](https://packagist.org/packages/garagist/mjml). ### Easy-email [Easy-email](https://github.com/zalify/easy-email) drag-and-drop email editor based on MJML. Transform structured JSON data into HTML that’s compatible with major email clients. ### Email Love The [Email Love Figma plugin](https://www.figma.com/community/plugin/1387891288648822744/email-love-html-email-builder) takes the headache out of the email development process by enabling you to export responsive, production-ready email HTML or MJML directly from Figma. ================================================ FILE: doc/components_1.md ================================================ ## Components Components are the core of MJML. A component is an abstraction of a more complex responsive HTML layout. It exposes attributes, enabling you to create bespoke styling. MJML comes out of the box with a set of standard components to help you easily build your first templates without having to reinvent the wheel. For instance, the `mj-button` component is, on the inside, a complex HTML layout: ```html Hello There!
Hello There!
``` ### Which email clients/versions are supported? For full details of component support, [please visit our support matrix](https://mjml.io/compatibility). ### mjml An MJML document starts with an `mjml` tag. It can contain only `mj-head` and `mj-body` tags. Both have the same purpose of `head` and `body` in a HTML document. #### Attributes | attribute | accepts | description | default value | | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | owa | string | if set to `desktop`, this will force the desktop version for older (self-hosted) versions of Outlook.com that don't support media queries (cf. [this issue](https://github.com/mjmlio/mjml/issues/2241)) | `none` | | lang | string | adds a `lang` attribute in the `html` and `body > div` tags | `und` | | dir | string | adds a `dir` attribute in the `html` and `body > div` tags | `auto` | ### mj-head Contains components related to the document head such as style and meta elements (see [head components](#standard-head-components)). ================================================ FILE: doc/components_2.md ================================================ ### mj-include The `mjml-core` package allows you to include external `.mjml` files to build your email template. ```xml This is a header ``` You can wrap your external `.mjml` files inside the default `mjml > mj-body` tags to make it easier to preview outside the main template. ```xml ``` The MJML engine will then replace your included files before starting the rendering process. #### Other file types You can include external `.css` files which will be inserted in the same way as using an `mj-style` tag. You need to specify that you're including a CSS file using the attribute `type="css"` attribute. If you want the CSS to be inlined, you can use the `css-inline="inline"` attribute. ```xml ``` You can also include external `html` files. They will be inserted the same way as when using an `mj-raw` tag. You need to specify that you're including an HTML file using the attribute `type="html"`. ```xml ``` ================================================ FILE: doc/config.json ================================================ [ "mjml/doc/guide.md", "mjml/doc/install.md", "mjml/doc/getting_started.md", "mjml/doc/basic.md", "mjml/doc/components_1.md", "mjml/packages/mjml-body/README.md", "mjml/doc/components_2.md", "mjml/doc/head_components.md", "mjml/packages/mjml-head-attributes/README.md", "mjml/packages/mjml-head-breakpoint/README.md", "mjml/packages/mjml-head-font/README.md", "mjml/packages/mjml-head-html-attributes/README.md", "mjml/packages/mjml-head-preview/README.md", "mjml/packages/mjml-head-style/README.md", "mjml/packages/mjml-head-title/README.md", "mjml/doc/body_components.md", "mjml/packages/mjml-accordion/README.md", "mjml/packages/mjml-button/README.md", "mjml/packages/mjml-carousel/README.md", "mjml/packages/mjml-column/README.md", "mjml/packages/mjml-divider/README.md", "mjml/packages/mjml-group/README.md", "mjml/packages/mjml-hero/README.md", "mjml/packages/mjml-image/README.md", "mjml/packages/mjml-navbar/README.md", "mjml/packages/mjml-raw/README.md", "mjml/packages/mjml-section/README.md", "mjml/packages/mjml-social/README.md", "mjml/packages/mjml-spacer/README.md", "mjml/packages/mjml-table/README.md", "mjml/packages/mjml-text/README.md", "mjml/packages/mjml-wrapper/README.md", "mjml/doc/ending-tags.md", "mjml/doc/community-components.md", "mjml/doc/ports.md", "mjml/doc/mjml-bar-chart.md", "mjml/doc/mjml-chart.md", "mjml/doc/mjml-chartjs.md", "mjml/doc/mjml-qr-code.md", "mjml/doc/mjml-mso-button.md", "mjml/packages/mjml-validator/README.md", "mjml/doc/create.md", "mjml/doc/using_mjml_in_json.md", "mjml/doc/tooling.md", "mjml/doc/community-contributions.md" ] ================================================ FILE: doc/create.md ================================================ ## Creating a Component One of the great advantages of MJML is that it's component-based. Components abstract complex patterns and can easily be reused. In addition to the standard library of components, it is also possible to create your own components! We have published [a step-by-step guide](https://medium.com/mjml-making-responsive-email-easy/tutorial-creating-your-own-component-with-mjml-4-1c0e84e97b36) that explains how to create a custom components with MJML. It will introduce you to the [boilerplate repo](https://github.com/mjmlio/mjml-component-boilerplate) hosted on Github, which provides a fast way of getting started with developing your own components. ================================================ FILE: doc/ending-tags.md ================================================ ### Ending tags Some MJML components are "ending tags". These are mostly the components that will contain text content, like `mj-text` or `mj-button`. These components can contain both text and HTML content, which will remain unprocessed by the MJML engine. You cannot use other MJML components. Since the content is not processed, this means that any text won't be escaped, so if you use characters that are used to define html tags in your text, like `<` or `>`, you should use the encoded characters `<` and `<`. There can also be issues if you use the `minify` option, `mj-html-attributes` or an inline `mj-style`, because these require the HTML to be re-parsed internally. If you're just using the `minify` option, and need to use the `< >` characters, e.g for a templating language, you can also avoid this problem by wrapping the troublesome content between two `` tags. Here is the list of all ending tags : - `mj-accordion-text` - `mj-accordion-title` - `mj-button` - `mj-navbar-link` - `mj-raw` - `mj-social-element` - `mj-text` - `mj-table` ================================================ FILE: doc/getting_started.md ================================================ ## Getting Started This is a responsive email:
layout
Like a regular HTML template, we can split this one into different parts to fit in a grid. The body of your email, represented by the `mj-body` tag contains the entire content of your document:
the body is overlaid with a semi opaque orange color
From here, you can first define your sections:
each section is overlaid with various semi opaque colors
Inside any section, there should be columns (even if you need only one column). Columns are what makes MJML responsive.
each column is overlaid with various semi opaque colors
Below, you'll find some basic rules of MJML to keep in mind for later. We'll remind them when useful but better start learning them early on. ### Column sizing #### Auto sizing The default behavior of the MJML translation engine is to divide the section space (600px by default, but it can be changed with the `width` attribute on `mj-body`) in as many columns as you declare. Let's take the following layout to illustrate this: ```html ``` Since the first section defines only 2 columns, the engine will translate that in a layout where each column takes 50% of the total space (300px each). If we add a third one, it goes down to 33%, and with a fourth one to 25%. #### Manual sizing You can also manually set the size of your columns, in pixels or percentage, by using the `width` attribute on `mj-column`. Let's take the following layout to illustrate this: ```html ``` ================================================ FILE: doc/guide.md ================================================ --- title: API Reference language_tabs: - html: MJML toc_footers: - Fork me on Github - Submit an Issue search: true --- # MJML Guide MJML (Mailjet Markup Language) is a markup language designed to reduce the pain of coding a responsive email. Its semantic syntax makes it easy and straightforward whilst its rich standard components library speeds up your development time and lightens your email codebase. MJML’s open-source engine generates high quality responsive HTML compliant with best practices. ## Overview MJML rolls up all of what Mailjet has learned about HTML email design and abstracts the whole layer of complexity related to responsive email design. Get your speed and productivity boosted with MJML’s semantic syntax. Say goodbye to endless HTML table nesting or email client specific CSS. Building a responsive email is super easy with tags such as `` and ``. MJML has been designed with responsiveness in mind. The abstraction it offers guarantee that you will always be up-to-date with the industry practices. Email clients update their specs and requirements regularly, but we geek about that stuff - we’ll stay on top of it so you can spend less time reading up on latest email client updates and more time designing beautiful emails. ```html Hello World ```

Try it live

================================================ FILE: doc/head_components.md ================================================ ## Standard Head components Head components ease your development process, for example, enabling you to import fonts, define default styles or create classes for MJML components. ================================================ FILE: doc/install.md ================================================ ## Installation You can [install MJML](https://www.npmjs.com/package/mjml) with NPM to use it with NodeJS or the Command Line Interface. If you're not sure what those are, [head over to Usage](#usage) for other ways to use MJML. ```bash npm install mjml ``` ## Development To work on MJML, make changes and create merge requests, [download and install yarn](https://yarnpkg.com/lang/en/docs/install/) for easy development. ```bash git clone https://github.com/mjmlio/mjml.git && cd mjml yarn yarn build ``` You can also run `yarn build:watch` to rebuild the package as you code. ## Usage ### Online Don't want to install anything? [Use the free online editor](https://mjml.io/try-it-live)!
try it live

Try it live

### Applications and plugins MJML comes with an ecosystem of tools and plugins, check out: - The [MJML App](https://mjmlio.github.io/mjml-app/) (MJML is included) - [Visual Studio Code plugin](https://github.com/mjmlio/vscode-mjml) (MJML is included) - [Sublime Text plugin](https://packagecontrol.io/packages/MJML-syntax) (MJML needs to be installed separately) For more information, [check the Tooling section](#tooling). For more tools, [check the Community page](https://mjml.io/community). ### Command line interface > Compiles the file and outputs the HTML generated in `output.html` ```bash mjml input.mjml -o output.html ``` You can pass optional `arguments` to the CLI and combine them. | argument | description | default value | | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `mjml -m [input]` | Migrates a v3 MJML file to the v4 syntax | | | `mjml [input] -o [output]` | Writes the output to [output] | | | `mjml [input] -s` | Writes the output to `stdout` | | | `mjml [input] -s --noStdoutFileComment` | Writes the output to `stdout` without an comment containing the source file in the first line | the outputs first line contains the file in the format `` | | `mjml -w [input]` | Watches the changes made to `[input]` (file or folder) | | | `mjml [input] --config.beautify` | Beautifies the output (`true` or `false`) | `true` | | `mjml [input] --config.minify` | Minifies the output (`true` or `false`) | `false` | | `mjml [input] --config.juicePreserveTags` | Preserve some tags when inlining css, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | | | `mjml [input] --config.minifyOptions` | Options for html minifier, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | | | `mjml [input] --config.mjmlConfigPath [mjmlconfigPath]` | Uses the `.mjmlconfig` file in the specified path or directory to include custom components | _The `.mjmlconfig` file in the current working directory, if any_ | | `mjml [input] --config.useMjmlConfigOptions` | Allows to use the `options` attribute from `.mjmlconfig` file | `false` | | `mjml [input] --config.validationLevel` | [Validation level](https://github.com/mjmlio/mjml/tree/master/packages/mjml-validator#validating-mjml): `strict`, `soft` or `skip` | `soft` | ### Inside Node.js ```javascript import mjml2html from 'mjml' /* Compile an mjml string */ const htmlOutput = mjml2html( ` Hello World! `, options, ) /* Print the responsive HTML generated and MJML errors if any */ console.log(htmlOutput) ``` You can pass optional `options` as an object to the `mjml2html` function: | option | accepts | description | default value | | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | fonts | object | Default fonts imported in the HTML rendered by MJML | See in [index.js](https://github.com/mjmlio/mjml/blob/master/packages/mjml-core/src/index.js#L100-L108) | | keepComments | boolean | Option to keep comments in the HTML output | `true` | | beautify | boolean | Option to beautify the HTML output | `false` | | minify | boolean | Option to minify the HTML output | `false` | | validationLevel | string | Available values for the [validator](https://github.com/mjmlio/mjml/tree/master/packages/mjml-validator#validating-mjml): `strict` `soft` `skip` | `soft` | | filePath | string | Full path of the specified file to use when resolving paths from [`mj-include` components](#mj-include) | `.` | | mjmlConfigPath | string | The path or directory of the [`.mjmlconfig` file](#community-components) | `process.cwd()` | | useMjmlConfigOptions | boolean | Allows to use the `options` attribute from `.mjmlconfig` file | `false` | | minifyOptions | object | Options for HTML minifier, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | `{"collapseWhitespace": true, "minifyCss": false, "removeEmptyAttributes": true}`

`minifyCss` can take a value of `false` or one of the two preset options `lite` or `default`. Within either preset, you can specify specific options from cssnano, for example `minifyCss: { options: { preset: [ 'default', { minifyFontValues: { removeQuotes: false }, }, ], }, },` | | juicePreserveTags | boolean | Optional setting when inlining CSS, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | | ### API A free-to-use MJML API is available to make it easy to integrate MJML in your application. Head over here to [learn more about the API](https://mjml.io/api). ================================================ FILE: doc/mjml-bar-chart.md ================================================ ### mj-bar-chart An open-source component that allows you to create fully embedded static bar charts in your MJML templates. These bar charts are easily customizable, 100% built with HTML tables, and do not require any external dependencies. The package is available on [GitHub](https://github.com/Freezystem/mjml-bar-chart) and [NPM](https://www.npmjs.com/package/@freezystem/mjml-bar-chart).
Basic mjml-bar-chart rendering
You can also render as stacked bars, add a link to your sources, vertically align labels, and much more:
Stacked rendering with vertically aligned legends
This component is lightweight, written with TypeScript, and is thoroughly tested. It is available as UMD (CJS + AMD), ESM and TS module format. ================================================ FILE: doc/mjml-chart.md ================================================ ### mj-chart Displays charts as images in your email. Thanks to [image-charts](https://image-charts.com/) for their contribution with this component. It's available on [Github](https://github.com/image-charts/mjml-charts) and [NPM](https://www.npmjs.com/package/mjml-chart).
mj-chart demo
================================================ FILE: doc/mjml-chartjs.md ================================================ ### mj-chartjs Displays [Chart.js](https://www.chartjs.org/) charts as images in your email. Chart.js is an open-source Javascript charting library. It’s available on [Github](https://github.com/typpo/mjml-chartjs) and [NPM](https://www.npmjs.com/package/mjml-chartjs). By default, it uses the open-source [QuickChart](https://quickchart.io/) API for chart rendering.
mj-chartjs demo
================================================ FILE: doc/mjml-mso-button.md ================================================ ### mjml-msobutton A button that uses the [VML](https://docs.microsoft.com/en-us/windows/win32/vml/shape-element--vml) solution for radius, which is supported in Outlook desktop It uses the same attributes as the standard `mj-button` but includes three additional ones: | attribute | accepts | description | default value | | ---------- | ------- | ------------------------------ | ------------- | | mso-proof | boolean | Active the bulletproof mode | `false` | | mso-width | `px` | The width of the VML solution | `200px` | | mso-height | `px` | The height of the VML solution | `40px` | These new attributes allow MJML to generate a “bulletproof button“ which incorporates radius, stroke and alignment, [using this method](https://buttons.cm/), It's available on [Github](https://github.com/adrien-zinger/mjml-mso-button) and [NPM](https://www.npmjs.com/package/mjml-msobutton). **Usage** Use it like a standard `mj-button`: ```html Click ! ``` **Problems that you should know** 1. This cannot be used with an image in background 2. It creates a duplication of code in the HTML 3. The width and the height cannot be used with the auto value. > Sample project on github [here](https://github.com/adrien-zinger/mjml-msobutton-sample) ================================================ FILE: doc/mjml-qr-code.md ================================================ ### mj-qr-code Displays QR codes in your email. It's available on [Github](https://github.com/typpo/mjml-qr-code) and [NPM](https://www.npmjs.com/package/mjml-qr-code). By default, it uses the open-source QuickChart [QR code API](https://quickchart.io/).
mj-qr-code demo
================================================ FILE: doc/ports.md ================================================ ## Ports and Language Bindings MJML is also available for other platforms to use. The community has created ports to these and wrappers for the official Node implementation. Note: These contributions are not directly supported by the MJML team. ### Rust: MRML This project is a reimplementation of the nice MJML markup language in Rust. [https://github.com/jdrouet/mrml](https://github.com/jdrouet/mrml) #### Missing implementations / components: - `mj-style set to inline`: not yet implemented. It requires parsing the generated HTML to apply the inline styles afterward (that's how it's done in MJML) which would kill the performance. Applying it at render time would improve the performance but it would still require it to parse the CSS. ### .NET: MJML.NET A blazingly-fast unofficial port of MJML 4 to .NET 6. [https://github.com/SebastianStehle/mjml-net](https://github.com/SebastianStehle/mjml-net) ### Elixir: MJML (Rust NIFs for Elixir) Native Implemented Function (NIF) bindings for the MJML Rust implementation (mrml). [https://github.com/adoptoposs/mjml_nif](https://github.com/adoptoposs/mjml_nif) ### Ruby: MRML Ruby Ruby wrapper for MRML, the MJML markup language implementation in Rust. [https://github.com/hardpixel/mrml-ruby](https://github.com/hardpixel/mrml-ruby) ### React: mjml-react React components for MJML components. [https://github.com/faire/mjml-react#readme](https://github.com/faire/mjml-react#readme) ### Python: mjml-python Python wrapper for MRML, the MJML markup language implementation in Rust. [https://github.com/mgd020/mjml-python](https://github.com/mgd020/mjml-python) ### Python: mjml-python Python implementation for MJML. [https://github.com/FelixSchwarz/mjml-python](https://github.com/FelixSchwarz/mjml-python) ### Python / Django: django-mjml The simplest way to use MJML in Django templates. [https://github.com/liminspace/django-mjml](https://github.com/liminspace/django-mjml) ### PHP / Laravel: Laravel MJML Build responsive e-mails easily using MJML and Laravel Mailables. - [https://github.com/EvanSchleret/lara-mjml](https://github.com/EvanSchleret/lara-mjml) - [https://github.com/asahasrabuddhe/laravel-mjml](https://github.com/asahasrabuddhe/laravel-mjml) (not maintained) ================================================ FILE: doc/tooling.md ================================================ ## Tooling In order to provide you with the best and most efficient experience using MJML, we've developed some tools to integrate it seamlessly into your development workflow: ### Visual Studio Code [Visual Studio Code](https://code.visualstudio.com/) is a free code editor made by [Microsoft](https://www.microsoft.com/). We recommend this package as it is among the most feature-rich MJML plugins for code editors; with live previews, syntax highlighting and linting, as well as export features including HTML and screenshots. It is available [on Github](https://github.com/mjmlio/vscode-mjml) and through the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=mjmlio.vscode-mjml). ### Sublime Text [Sublime Text](https://www.sublimetext.com/) is a powerful text editor. We’ve provided you with a package to color MJML tags. It is available [on Github](https://github.com/mjmlio/mjml-syntax) and through the [Sublime Package Control](https://packagecontrol.io/packages/MJML-syntax). ### Gulp [Gulp](https://gulpjs.com/) is a tool designed to help you automate and enhance your workflow. Our plugin enables you to plug the MJML translation engine into your workflow, helping you to streamline your development workflow. It is available here on [Github](https://github.com/mjmlio/gulp-mjml). ================================================ FILE: doc/using_mjml_in_json.md ================================================ ## Using MJML in JSON MJML can not only be used as a markup, but also as a JSON object, very useful for programmatic manipulation or with the MJML API. With the JSON format, a MJML component is defined as an `object` with the following properties: - a `tagName` as a `string` - a list of attributes as an `object` - either a `content` as a `string` or a list of `children` tags as an `array`. Exactly like using MJML as a markup, the JSON definition can be passed as an object to the `mjml2html` function. Here is working example: ```javascript var mjml2html = require('mjml') console.log( mjml2html({ tagName: 'mjml', attributes: {}, children: [ { tagName: 'mj-body', attributes: {}, children: [ { tagName: 'mj-section', attributes: {}, children: [ { tagName: 'mj-column', attributes: {}, children: [ { tagName: 'mj-image', attributes: { width: '100px', src: '/assets/img/logo-small.png', }, }, { tagName: 'mj-divider', attributes: { 'border-color': '#F46E43', }, }, { tagName: 'mj-text', attributes: { 'font-size': '20px', color: '#F45E43', 'font-family': 'Helvetica', }, content: 'Hello World', }, ], }, ], }, ], }, ], }), ) ``` ================================================ FILE: lerna.json ================================================ { "packages": [ "packages/*" ], "command": { "publish": { "exact": true } }, "npmClient": "yarn", "useWorkspaces": true, "version": "4.18.0" } ================================================ FILE: package.json ================================================ { "name": "mjml-master", "private": true, "scripts": { "build:watch": "lerna run build --parallel -- -- -w", "build": "lerna run build --parallel --ignore mjml-browser", "build-browser": "cd packages/mjml-browser && yarn build", "lint": "eslint .", "lint:fix": "eslint . --fix", "postinstall": "lerna bootstrap", "prettier": "prettier --write \"packages/**/{src,bin}/**/*.?(js|json)\"", "test": "lerna run test" }, "workspaces": [ "packages/*" ], "devDependencies": { "@babel/core": "^7.28.4", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-transform-runtime": "^7.28.3", "@babel/preset-env": "^7.28.3", "@babel/register": "^7.28.3", "babel-eslint": "^10.1.0", "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-lodash": "^3.3.4", "eslint": "^6.8.0", "eslint-config-airbnb-base": "^14.1.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.21.1", "lerna": "^3.22.1", "mocha": "10", "open": "^7.3.0", "prettier": "^3.2.4", "rimraf": "^3.0.2" }, "resolutions": { "@babel/runtime": "7.28.4" } } ================================================ FILE: packages/mjml/README.md ================================================ # MJML 4

github actions

| Introduction | Installation | Usage | Contribute |

--- # Introduction `MJML` is a markup language created by [Mailjet](https://www.mailjet.com/) and designed to reduce the pain of coding a responsive email. Its semantic syntax makes it easy and straightforward while its rich standard components library fastens your development time and lightens your email codebase. MJML’s open-source engine takes care of translating the `MJML` you wrote into responsive HTML.

# Installation You can install `MJML` with `NPM` to use it with NodeJS or the Command Line Interface. If you're not sure what those are, head over to Usage for other ways to use MJML. ```bash npm install mjml ``` # Usage ## Online Don't want to install anything? Use the free online editor!

try it live


## Applications and plugins MJML comes with an ecosystem of tools and plugins, check out: - The [MJML App](https://mjmlio.github.io/mjml-app/) (MJML is included) - [Visual Studio Code plugin](https://github.com/mjmlio/vscode-mjml) (MJML is included) - [Atom plugin](https://atom.io/users/mjmlio) (MJML needs to be installed separately) - [Sublime Text plugin](https://packagecontrol.io/packages/MJML-syntax) (MJML needs to be installed separately) For more tools, check the [Community](https://mjml.io/community) page. ## Command line interface > Compiles the file and outputs the HTML generated in `output.html` ```bash mjml input.mjml -o output.html ``` You can pass optional `arguments` to the CLI and combine them. argument | description | default value ---------|--------|-------------- `mjml -m [input]` | Migrates a v3 MJML file to the v4 syntax | NA `mjml [input] -o [output]` | Writes the output to [output] | NA `mjml [input] -s` | Writes the output to `stdout` | NA `mjml -w [input]` | Watches the changes made to `[input]` (file or folder) | NA `mjml [input] --config.beautify` | Beautifies the output (`true` or `false`) | true `mjml [input] --config.minify` | Minifies the output (`true` or `false`) | false See [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more information about config options. ## Inside Node.js ```javascript import mjml2html from 'mjml' /* Compile an mjml string */ const htmlOutput = mjml2html(` Hello World! `, options) /* Print the responsive HTML generated and MJML errors if any */ console.log(htmlOutput) ``` You can pass optional `options` as an object to the `mjml2html` function: option | unit | description | default value -------------|--------|--------------|--------------- fonts | object | Default fonts imported in the HTML rendered by MJML | See in [index.js](https://github.com/mjmlio/mjml/blob/master/packages/mjml-core/src/index.js#L100-L108) keepComments | boolean | Option to keep comments in the HTML output | true ignoreIncludes | boolean | Option to ignore mj-includes | false beautify | boolean | Option to beautify the HTML output | false minify | boolean | Option to minify the HTML output | false validationLevel | string | Available values for the [validator](https://github.com/mjmlio/mjml/tree/master/packages/mjml-validator#validating-mjml): 'strict', 'soft', 'skip' | 'soft' filePath | string | Path of file, used for relative paths in mj-includes | '.' preprocessors | array of functions | Preprocessors applied to the xml before parsing. Input must be xml, not json. Functions must be (xml: string) => string | [] juicePreserveTags | Preserve some tags when inlining css, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | NA minifyOptions | Options for html minifier, see [mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md) for more info | NA mjmlConfigPath | string | The path or directory of the `.mjmlconfig` file (for custom components use) | `process.cwd()` useMjmlConfigOptions | Allows to use the `options` attribute from `.mjmlconfig` file | false Note that it's also possible to define preprocessors in your mjmlconfig file. For this, you need to use a `.mjmlconfig.js` file. This js file needs to export an Object with the same structure as a standard JSON .mjmlconfig file. ## API A free-to-use MJML API is available to make it easy to integrate MJML in your application. Head over [here](https://mjml.io/api) to learn more about the API. # MJML Slack MJML wouldn't be as cool without its amazing community. Head over the [Community Slack](https://join.slack.com/t/mjml/shared_invite/zt-gqmwfwmr-kPBnfuuB7wof5httaTcXxg) to meet fellow MJML'ers. ================================================ FILE: packages/mjml/bin/mjml ================================================ #!/usr/bin/env node require('../lib/index') require('mjml-cli') ================================================ FILE: packages/mjml/package.json ================================================ { "name": "mjml", "description": "MJML: the only framework that makes responsive-email easy", "version": "4.18.0", "main": "lib/index.js", "bin": { "mjml": "bin/mjml" }, "files": [ "bin", "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward", "test": "mocha ./test/*.test.js" }, "dependencies": { "@babel/runtime": "^7.28.4", "mjml-cli": "4.18.0", "mjml-core": "4.18.0", "mjml-migrate": "4.18.0", "mjml-preset-core": "4.18.0", "mjml-validator": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "chai": "^4.1.1", "chai-spies": "^1.0.0", "cheerio": "1.0.0-rc.12", "lodash": "^4.17.21", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml/src/index.js ================================================ import mjml2html, { components, assignComponents } from 'mjml-core' import { dependencies, assignDependencies } from 'mjml-validator' import presetCore from 'mjml-preset-core' assignComponents(components, presetCore.components) assignDependencies(dependencies, presetCore.dependencies) export default mjml2html ================================================ FILE: packages/mjml/test/accordion-fontFamily.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-accordion font-family inheritance', function () { it('should render correct font-family in CSS style values on accordion-title and accordion-text', function () { const input = ` Why use an accordion? Because emails with a lot of content are most of the time a very bad experience on mobile, mj-accordion comes handy when you want to deliver a lot of information in a concise way. Why use an accordion? Because emails with a lot of content are most of the time a very bad experience on mobile, mj-accordion comes handy when you want to deliver a lot of information in a concise way. ` const { html } = mjml(input) const $ = load(html) // style values should be correct chai .expect( $( '.my-accordion-1 .mj-accordion-title td:first-child, .my-accordion-1 .mj-accordion-content td:first-child', '.my-accordion-2 .mj-accordion-title td:first-child, .my-accordion-2 .mj-accordion-content td:first-child, ', ) .map(function getAttr() { const start = $(this).attr('style').indexOf('font-family:') + 12 const end = $(this).attr('style').indexOf(';', start) const result = $(this).attr('style').substring(start, end) return result }) .get(), 'Font-family in CSS style values on accordion-title', ) .to.eql(['serif', 'serif', 'monospace', 'monospace']) }) }) ================================================ FILE: packages/mjml/test/accordion-padding.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-accordion padding-X', function () { it('should render correct padding in CSS style values on accordion-title and accordion-text', function () { const input = ` Why use an accordion? Because emails with a lot of content are most of the time a very bad experience on mobile, mj-accordion comes handy when you want to deliver a lot of information in a concise way. ` const { html } = mjml(input) const $ = load(html) function extractPadding(style, prop) { const start = style.indexOf(`${prop}:`) + prop.length + 1 const end = style.indexOf(';', start) return style.substring(start, end).trim() } const paddings = [ 'padding-left', 'padding-right', 'padding-top', 'padding-bottom', ] const results = paddings.map((padding) => $( '.mj-accordion-title td:first-child, .mj-accordion-content td:first-child', ) .map(function () { const style = $(this).attr('style') return extractPadding(style, padding) }) .get(), ) // Each padding should be ['40px', '40px'] paddings.forEach((padding, idx) => { chai .expect( results[idx], `${padding} in CSS style values on accordion-title and accordion-text`, ) .to.eql(['40px', '40px']) }) }) }) ================================================ FILE: packages/mjml/test/accordionTitle-fontWeight.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-accordion-title font-weight', function () { it('should render correct font-weight in CSS style values on accordion-title', function () { const input = ` Why use an accordion? Because emails with a lot of content are most of the time a very bad experience on mobile, mj-accordion comes handy when you want to deliver a lot of information in a concise way. How it works Content is stacked into tabs and users can expand them at will. If responsive styles are not supported (mostly on desktop clients), tabs are then expanded and your content is readable at once. ` const { html } = mjml(input) const $ = load(html) // style values should be correct chai .expect( $('.accordion-title') .map(function getAttr() { const start = $(this).attr('style').indexOf('font-weight:') + 12 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Font-weight in CSS style values on accordion-title', ) .to.eql(['bold', '700']) }) }) ================================================ FILE: packages/mjml/test/carousel-hoverSupported.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-carousel-thumbnail thumbnails supported', function () { it('should render correct display in CSS style values on mj-carousel-thumbnail', function () { const input = ` ` const { html } = mjml(input) const $ = load(html) // style values should be correct chai .expect( $('.mj-carousel-thumbnail') .map(function getAttr() { const start = $(this).attr('style').indexOf('display:') + 8 const end = $(this).attr('style').indexOf(';', start) const result = $(this).attr('style').substring(start, end) return result }) .get(), 'Display CSS style values on mj-carousel-thumbnail', ) .to.eql(['none', 'none', 'none']) }) }) ================================================ FILE: packages/mjml/test/column-border-radius.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-column border-radius', function () { it('should render correct border-radius / inner-border-radius (and border-collapse) in CSS style values on mj-column', function () { const input = ` Hello World ` const { html } = mjml(input) const $ = load(html) // border radius values should be correct chai .expect( $( '.mj-column-per-100 > table > tbody > tr > td, .mj-column-per-100 > table > tbody > tr > td > table', ) .map(function getAttr() { const start = $(this).attr('style').indexOf('border-radius:') + 14 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Border-radius / inner-border-radius in CSS style values on mj-column', ) .to.eql(['50px', '40px']) // border collapse values should be correct chai .expect( $( '.mj-column-per-100 > table > tbody > tr > td, .mj-column-per-100 > table > tbody > tr > td > table', ) .map(function getAttr() { const start = $(this).attr('style').indexOf('border-collapse:') + 16 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Border-collapse in CSS style values on mj-column', ) .to.eql(['separate', 'separate']) }) }) ================================================ FILE: packages/mjml/test/html-attributes.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const { sortBy } = require('lodash') const mjml = require('../lib') describe('html-attributes', function () { it('should put the attributes at the right place', function () { const input = ` 42 43 { if item < 5 } { if item > 10 } Hello World! { item } { end if } Hello World! { item + 1 } { end if } ` const { html } = mjml(input) const $ = load(html) // should put the attributes at the right place chai .expect( $('.text div') .map(function getAttr() { return $(this).attr('data-id') }) .get(), 'Custom attributes added on texts', ) .to.eql(['42', '42']) chai .expect( $('.image td') .map(function getAttr() { return $(this).attr('data-name') }) .get(), 'Custom attributes added on image', ) .to.eql(['43']) // should not alter templating syntax, or move the content that is outside any tag (mj-raws) const expected = [ '{ if item < 5 }', 'class="section"', '{ if item > 10 }', 'class="text"', '{ item }', '{ end if }', '{ item + 1 }', ] const indexes = expected.map((str) => html.indexOf(str)) chai.expect(indexes, 'Templating syntax unaltered').to.not.include(-1) chai .expect(sortBy(indexes), 'Mj-raws kept same positions') .to.deep.eql(indexes) }) }) ================================================ FILE: packages/mjml/test/html-comments.test.js ================================================ const chai = require('chai') const mjml = require('../lib') describe('HTML comments', function () { it('should not alter the whitespace between the opening/closing comment tags and the comment content', function () { const input = `

View source to see comments below



` const { html } = mjml(input) // should not alter templating syntax, or move the content that is outside any tag (mj-raws) const expected = [ '', '', '', ] const indexes = expected.map((str) => html.indexOf(str)) chai.expect(indexes, 'Cmment syntax unaltered').to.not.include(-1) }) }) ================================================ FILE: packages/mjml/test/lazy-head-style.test.js ================================================ const chai = require('chai') const spies = require('chai-spies') const mjml = require('../lib') const { HeadComponent, registerComponent, } = require('../../mjml-core/lib/index') chai.use(spies) describe('lazy-head-style', function () { it('should call style with correct breakpoint', function () { const addStyle = chai.spy( (breakpoint) => ` @media only screen and (max-width:${breakpoint}) { h1 { font-size: 20px; } } `, ) class HeadComponentWithFunctionStyle extends HeadComponent { handler() { const { add } = this.context add('style', addStyle) } } HeadComponentWithFunctionStyle.componentName = 'mj-head-component-with-function-style' HeadComponentWithFunctionStyle.endingTag = true HeadComponentWithFunctionStyle.allowedAttributes = {} registerComponent(HeadComponentWithFunctionStyle) mjml(` `) chai.expect(addStyle).to.have.been.called.with('300px') }) }) ================================================ FILE: packages/mjml/test/navbar-ico-padding.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-navbar ico-padding-X', function () { it('should render correct padding in CSS style values on navbar hamburger icon', function () { const input = ` Getting started Try it live ` const { html } = mjml(input) const $ = load(html) function extractPadding(style, prop) { const start = style.indexOf(`${prop}:`) + prop.length + 1 const end = style.indexOf(';', start) return style.substring(start, end).trim() } const paddings = [ 'padding-bottom', 'padding-left', 'padding-right', 'padding-top', ] const results = paddings.map((padding) => $('.mj-menu-label') .map(function () { const style = $(this).attr('style') return extractPadding(style, padding) }) .get(), ) // Padding should be ['20px', '30px', '40px', '50px'] const expected = { 'padding-bottom': ['20px'], 'padding-left': ['30px'], 'padding-right': ['40px'], 'padding-top': ['50px'], } paddings.forEach((padding, idx) => { chai .expect(results[idx], `${padding} in CSS style values on navbar icon`) .to.eql(expected[padding]) }) }) }) ================================================ FILE: packages/mjml/test/social-align.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-social-element align', function () { it('should render correct align in CSS style values on mj-social-element', function () { const input = ` Facebook ` const { html } = mjml(input) const $ = load(html) // align values should be correct chai .expect( $('.my-social-element > td:first-child') .map(function getAttr() { const start = $(this).attr('style').indexOf('text-align:') + 11 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'align values on social elements', ) .to.eql(['right']) }) }) ================================================ FILE: packages/mjml/test/social-icon-height.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-social icon-height', function () { it('should render correct icon-height align in CSS style values on mj-social', function () { const input = ` Facebook ` const { html } = mjml(input) const $ = load(html) // height values should be correct chai .expect( $('.my-social-element > td > table > tbody > tr > td') .map(function getAttr() { const start = $(this).attr('style').indexOf('height:') + 7 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'icon-height values on social elements', ) .to.eql(['40px']) chai .expect( $('.my-social-element > td > table > tbody > tr > td img') .map(function getAttr() { return $(this).attr('height') }) .get(), ) .to.satisfy((arr) => arr.every((val) => !val)) }) }) ================================================ FILE: packages/mjml/test/table-cellspacing.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-table cellspacing', function () { it('should render correct cellspacing (and border-collapse) in HTML tag / CSS style values on mj-table', function () { const input = ` Year Language Inspired from 1995 PHP C, Shell Unix ` const { html } = mjml(input) const $ = load(html) // border radius values should be correct chai .expect( $('.my-table > table') .map(function getAttr() { return $(this).attr('cellspacing') }) .get(), 'cellspacing values on table elements', ) .to.eql(['10']) // border collapse values should be correct chai .expect( $('.my-table > table') .map(function getAttr() { const start = $(this).attr('style').indexOf('border-collapse:') + 16 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Border-collapse in CSS style values on mj-table', ) .to.eql(['separate']) }) }) ================================================ FILE: packages/mjml/test/tableWidth.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-table width', function () { it('should render correct width in CSS style values on mj-table', function () { const input = ` Default Width 100% Pixel Width 500px Percentage Width 80% Auto Width Auto ` const { html } = mjml(input) const $ = load(html) // width values should be correct chai .expect( $('.table table') .map(function getAttr() { return $(this).attr('width') }) .get(), 'Width values on tables', ) .to.eql(['100%', '500', '80%', 'auto']) // style values should be correct chai .expect( $('.table table') .map(function getAttr() { const start = $(this).attr('style').indexOf('width:') + 6 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Width in CSS style values on tables', ) .to.eql(['100%', '500px', '80%', 'auto']) }) }) ================================================ FILE: packages/mjml/test/wrapper-border-radius.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-wrapper and mj-section border-radius', function () { it('should render correct border-radius (and border-collapse) in CSS style values on mj-wrapper and mj-section', function () { const input = ` Hello World ` const { html } = mjml(input) const $ = load(html) // border radius values should be correct chai .expect( $( 'body > div > div > table:first-child > tbody > tr > td, body > div > div', ) .map(function getAttr() { const start = $(this).attr('style').indexOf('border-radius:') + 14 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Border-radius in CSS style values on mj-wrapper', ) .to.eql(['10px', '10px']) // overflow value should be correct chai .expect( $('body > div > div') .map(function getAttr() { const start = $(this).attr('style').indexOf('overflow:') + 9 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Overflow in CSS style values on mj-wrapper', ) .to.eql(['hidden']) // border collapse values should be correct chai .expect( $('body > div > div > table:first-child') .map(function getAttr() { const start = $(this).attr('style').indexOf('border-collapse:') + 16 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) }) .get(), 'Border-collapse in CSS style values on mj-wrapper', ) .to.eql(['separate']) }) }) ================================================ FILE: packages/mjml/test/wrapper-gap.test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const mjml = require('../lib') describe('mj-wrapper gap', function () { it('should render correct gap values in CSS style values on children mj-section', function () { const input = ` Section 1 Section 2 Section 3 ` const { html } = mjml(input) const $ = load(html) // gap values should be correct chai .expect( $('.my-section') .map(function getAttr() { const str = $(this).attr('style') const substr = 'margin-top:' if (str.includes(substr)) { const start = $(this).attr('style').indexOf(substr) + 11 const end = $(this).attr('style').indexOf(';', start) return $(this).attr('style').substring(start, end) } return undefined }) .get(), 'Gap in CSS style values on mj-wrapper', ) .to.eql(['20px', '20px']) }) }) ================================================ FILE: packages/mjml-accordion/README.md ================================================ ### mj-accordion An interactive MJML component that stacks content in tabs, so the information is collapsed and only the titles are visible. Readers can interact by clicking on the tabs to reveal the content, providing a better experience for mobile users by reducing the amount of scrolling.
accordion
```xml Why use an accordion? Because emails with a lot of content are most of the time a very bad experience on mobile, mj-accordion comes handy when you want to deliver a lot of information in a concise way. How it works Content is stacked into tabs and users can expand them at will. If responsive styles are not supported (mostly on desktop clients), tabs are then expanded and your content is readable at once. ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ----------------------- | -------------------------------------------------- | -------------------------------------- | | border | string | CSS border format | `2px solid black` | | container-background-color | CSS color formats | background-color of the cell | | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font | `Ubuntu, Helvetica, Arial, sans-serif` | | icon-align | `top` `middle` `bottom` | icon alignment | `middle` | | icon-height | `px` `%` | icon height | `32px` | | icon-position | left,
right | display icon left or right | `right` | | icon-unwrapped-alt | string | alt text when accordion is unwrapped | `-` | | icon-unwrapped-url | string | icon when accordion is unwrapped | `https://i.imgur.com/w4uTygT.png` | | icon-width | `px` `%` | icon width | `32px` | | icon-wrapped-alt | string | alt text when accordion is wrapped | `+` | | icon-wrapped-url | string | icon when accordion is wrapped | `https://i.imgur.com/bIXv1bk.png` | | padding | `px` `%` | accordion padding, supports up to 4 parameters | `10px 25px` | | padding-bottom | `px` `%` | accordion bottom padding | | | padding-left | `px` `%` | accordion left padding | | | padding-right | `px` `%` | accordion right padding | | | padding-top | `px` `%` | accordion top padding | |

Try it live

#### mj-accordion-element Creates an accordion title/text pair. An accordion can have any number of these pairs. ##### Attributes | attribute | accepts | description | default value | | ------------------ | ----------------------- | ----------------------------------------------------------------------------------------- | ------------- | | background-color | CSS color formats | background color | | | border | string | CSS border format.
affects each horizontal border in the accordion except the top one | | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font | | | icon-align | `top` `middle` `bottom` | icon alignment | | | icon-height | `px` `%` | icon width | `32px` | | icon-position | `left` `right` | postion of icon | | | icon-unwrapped-alt | string | alt text when accordion is unwrapped | | | icon-unwrapped-url | string | icon when accordion is unwrapped | | | icon-width | `px` `%` | icon height | `32px` | | icon-wrapped-alt | string | alt text when accordion is wrapped | | | icon-wrapped-url | string | icon when accordion is wrapped | | #### mj-accordion-title Displays the title in a title/text pair. ##### Attributes | attribute | accepts | description | default value | | ---------------- | ----------------- | ---------------------------------------------------- | ------------- | | background-color | CSS color formats | background color | | | color | CSS color formats | text color | | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font family | | | font-size | `px` | font size | `13px` | | font-weight | string | text thickness | | | padding | `px` `%` | accordion title padding, supports up to 4 parameters | `16px` | | padding-bottom | `px` `%` | accordion title bottom padding | | | padding-left | `px` `%` | accordion title left padding | | | padding-right | `px` `%` | accordion title right padding | | | padding-top | `px` `%` | accordion title top padding | | #### mj-accordion-text Displays the text in a title/text pair. ##### Attributes | attribute | accepts | description | default value | | ---------------- | ----------------- | --------------------------------------------------- | ------------- | | background-color | CSS color formats | background color | | | color | CSS color formats | text color | | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font family | | | font-size | `px` | font size | `13px` | | font-weight | string | text thickness | | | letter-spacing | `px` `em` | letter spacing | | | line-height | `px` `%` | space between the lines | `1` | | padding | `px` `%` | accordion text padding, supports up to 4 parameters | `16px` | | padding-bottom | `px` `%` | accordion text bottom padding | | | padding-left | `px` `%` | accordion text left padding | | | padding-right | `px` `%` | accordion text right padding | | | padding-top | `px` `%` | accordion text top padding | | ================================================ FILE: packages/mjml-accordion/package.json ================================================ { "name": "mjml-accordion", "description": "mjml-accordion", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-accordion" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-accordion/src/Accordion.js ================================================ import { BodyComponent } from 'mjml-core' export default class MjAccordion extends BodyComponent { static componentName = 'mj-accordion' static allowedAttributes = { 'container-background-color': 'color', border: 'string', 'font-family': 'string', 'icon-align': 'enum(top,middle,bottom)', 'icon-width': 'unit(px,%)', 'icon-height': 'unit(px,%)', 'icon-wrapped-url': 'string', 'icon-wrapped-alt': 'string', 'icon-unwrapped-url': 'string', 'icon-unwrapped-alt': 'string', 'icon-position': 'enum(left,right)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', } static defaultAttributes = { border: '2px solid black', 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'icon-align': 'middle', 'icon-wrapped-url': 'https://i.imgur.com/bIXv1bk.png', 'icon-wrapped-alt': '+', 'icon-unwrapped-url': 'https://i.imgur.com/w4uTygT.png', 'icon-unwrapped-alt': '-', 'icon-position': 'right', 'icon-height': '32px', 'icon-width': '32px', padding: '10px 25px', } headStyle = () => ` noinput.mj-accordion-checkbox { display:block!important; } @media yahoo, only screen and (min-width:0) { .mj-accordion-element { display:block; } input.mj-accordion-checkbox, .mj-accordion-less { display:none!important; } input.mj-accordion-checkbox + * .mj-accordion-title { cursor:pointer; touch-action:manipulation; -webkit-user-select:none; -moz-user-select:none; user-select:none; } input.mj-accordion-checkbox + * .mj-accordion-content { overflow:hidden; display:none; } input.mj-accordion-checkbox + * .mj-accordion-more { display:block!important; } input.mj-accordion-checkbox:checked + * .mj-accordion-content { display:block; } input.mj-accordion-checkbox:checked + * .mj-accordion-more { display:none!important; } input.mj-accordion-checkbox:checked + * .mj-accordion-less { display:block!important; } } .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-title { cursor: auto; touch-action: auto; -webkit-user-select: auto; -moz-user-select: auto; user-select: auto; } .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-content { overflow: hidden; display: block; } .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-ico { display: none; } @goodbye { @gmail } ` getStyles() { return { table: { width: '100%', 'border-collapse': 'collapse', border: this.getAttribute('border'), 'border-bottom': 'none', 'font-family': this.getAttribute('font-family'), }, } } getChildContext() { return { ...this.context, accordionFontFamily: this.getAttribute('font-family'), } } render() { const childrenAttr = [ 'border', 'icon-align', 'icon-width', 'icon-height', 'icon-position', 'icon-wrapped-url', 'icon-wrapped-alt', 'icon-unwrapped-url', 'icon-unwrapped-alt', ].reduce( (res, val) => ({ ...res, [val]: this.getAttribute(val), }), {}, ) return ` ${this.renderChildren(this.props.children, { attributes: childrenAttr, })}
` } } ================================================ FILE: packages/mjml-accordion/src/AccordionElement.js ================================================ import { BodyComponent } from 'mjml-core' import { find } from 'lodash' import conditionalTag from 'mjml-core/lib/helpers/conditionalTag' import AccordionText from './AccordionText' import AccordionTitle from './AccordionTitle' export default class MjAccordionElement extends BodyComponent { static componentName = 'mj-accordion-element' static allowedAttributes = { 'background-color': 'color', border: 'string', 'font-family': 'string', 'icon-align': 'enum(top,middle,bottom)', 'icon-width': 'unit(px,%)', 'icon-height': 'unit(px,%)', 'icon-wrapped-url': 'string', 'icon-wrapped-alt': 'string', 'icon-unwrapped-url': 'string', 'icon-unwrapped-alt': 'string', 'icon-position': 'enum(left,right)', } static defaultAttributes = { title: { img: { width: '32px', height: '32px', }, }, } getStyles() { return { td: { padding: '0px', 'background-color': this.getAttribute('background-color'), }, label: { 'font-size': '13px', 'font-family': this.getAttribute('font-family'), }, input: { display: 'none', }, } } handleMissingChildren() { const { children } = this.props const childrenAttr = [ 'border', 'icon-align', 'icon-width', 'icon-height', 'icon-position', 'icon-wrapped-url', 'icon-wrapped-alt', 'icon-unwrapped-url', 'icon-unwrapped-alt', ].reduce( (res, val) => ({ ...res, [val]: this.getAttribute(val), }), {}, ) const result = [] if (!find(children, { tagName: 'mj-accordion-title' })) { result.push( new AccordionTitle({ attributes: childrenAttr, context: this.getChildContext(), }).render(), ) } result.push(this.renderChildren(children, { attributes: childrenAttr })) if (!find(children, { tagName: 'mj-accordion-text' })) { result.push( new AccordionText({ attributes: childrenAttr, context: this.getChildContext(), }).render(), ) } return result.join('\n') } getChildContext() { return { ...this.context, elementFontFamily: this.getAttribute('font-family'), } } render() { return ` ` } } ================================================ FILE: packages/mjml-accordion/src/AccordionText.js ================================================ import { BodyComponent } from 'mjml-core' export default class MjAccordionText extends BodyComponent { static componentName = 'mj-accordion-text' static endingTag = true static allowedAttributes = { 'background-color': 'color', 'font-size': 'unit(px)', 'font-family': 'string', 'font-weight': 'string', 'letter-spacing': 'unitWithNegative(px,em)', 'line-height': 'unit(px,%,)', color: 'color', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', } static defaultAttributes = { 'font-size': '13px', 'line-height': '1', padding: '16px', } getStyles() { return { td: { background: this.getAttribute('background-color'), 'font-size': this.getAttribute('font-size'), 'font-family': this.resolveFontFamily(), 'font-weight': this.getAttribute('font-weight'), 'letter-spacing': this.getAttribute('letter-spacing'), 'line-height': this.getAttribute('line-height'), color: this.getAttribute('color'), padding: this.getAttribute('padding'), 'padding-bottom': this.getAttribute('padding-bottom'), 'padding-left': this.getAttribute('padding-left'), 'padding-right': this.getAttribute('padding-right'), 'padding-top': this.getAttribute('padding-top'), }, table: { width: '100%', 'border-bottom': this.getAttribute('border'), }, } } renderContent() { return ` ${this.getContent()} ` } resolveFontFamily() { if ( this.props && this.props.rawAttrs && Object.prototype.hasOwnProperty.call(this.props.rawAttrs, 'font-family') ) { return this.getAttribute('font-family') } if (this.context && this.context.elementFontFamily) { return this.context.elementFontFamily } if (this.context && this.context.accordionFontFamily) { return this.context.accordionFontFamily } return MjAccordionText.defaultAttributes.fontFamily } render() { return `
${this.renderContent()}
` } } ================================================ FILE: packages/mjml-accordion/src/AccordionTitle.js ================================================ import { BodyComponent } from 'mjml-core' import conditionalTag from 'mjml-core/lib/helpers/conditionalTag' export default class MjAccordionTitle extends BodyComponent { static componentName = 'mj-accordion-title' static endingTag = true static allowedAttributes = { 'background-color': 'color', color: 'color', 'font-size': 'unit(px)', 'font-family': 'string', 'font-weight': 'string', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', } static defaultAttributes = { 'font-size': '13px', padding: '16px', } getStyles() { return { td: { width: '100%', 'background-color': this.getAttribute('background-color'), color: this.getAttribute('color'), 'font-size': this.getAttribute('font-size'), 'font-family': this.resolveFontFamily(), 'font-weight': this.getAttribute('font-weight'), padding: this.getAttribute('padding'), 'padding-bottom': this.getAttribute('padding-bottom'), 'padding-left': this.getAttribute('padding-left'), 'padding-right': this.getAttribute('padding-right'), 'padding-top': this.getAttribute('padding-top'), }, table: { width: '100%', 'border-bottom': this.getAttribute('border'), }, td2: { padding: '16px', background: this.getAttribute('background-color'), 'vertical-align': this.getAttribute('icon-align'), }, img: { display: 'none', width: this.getAttribute('icon-width'), height: this.getAttribute('icon-height'), }, } } resolveFontFamily() { if ( this.props && this.props.rawAttrs && Object.prototype.hasOwnProperty.call(this.props.rawAttrs, 'font-family') ) { return this.getAttribute('font-family') } if (this.context && this.context.elementFontFamily) { return this.context.elementFontFamily } if (this.context && this.context.accordionFontFamily) { return this.context.accordionFontFamily } return MjAccordionTitle.defaultAttributes.fontFamily } renderTitle() { return ` ${this.getContent()} ` } renderIcons() { return conditionalTag( ` `, true, ) } render() { const contentElements = [this.renderTitle(), this.renderIcons()] const content = ( this.getAttribute('icon-position') === 'right' ? contentElements : contentElements.reverse() ).join('\n') return `
${content}
` } } ================================================ FILE: packages/mjml-accordion/src/index.js ================================================ export { default as Accordion } from './Accordion' export { default as AccordionElement } from './AccordionElement' export { default as AccordionText } from './AccordionText' export { default as AccordionTitle } from './AccordionTitle' ================================================ FILE: packages/mjml-body/README.md ================================================ ### mj-body This is the starting point of your email. To aid accessibility, MJML automatically adds a `div` tag as the child of the body, with the following ARIA attributes `role="article"`, `aria-roledescription="email"` and `aria-label="EMAIL NAME"`, where 'EMAIL NAME' is taken from the content of the `mj-title` tag. The `lang` and `dir` attributes are also added here, with values taken from the `mjml` tag. ```xml ``` #### Attributes | attribute | accepts | description | default value | | ------------------- | ----------------- | -------------------------------------------------- | ------------- | | background-color | CSS color formats | the general background color | | | css-class           | string   | class name, added to the root HTML element created | | | width | `px` | email width | `600px` |

Try it live

================================================ FILE: packages/mjml-body/package.json ================================================ { "name": "mjml-body", "description": "mjml-body", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-body" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-body/src/index.js ================================================ import { BodyComponent } from 'mjml-core' export default class MjBody extends BodyComponent { static componentName = 'mj-body' static allowedAttributes = { width: 'unit(px)', 'background-color': 'color', } static defaultAttributes = { width: '600px', } getChildContext() { return { ...this.context, containerWidth: this.getAttribute('width'), } } getStyles() { return { div: { 'background-color': this.getAttribute('background-color'), }, } } render() { const { setBackgroundColor, globalData: { lang, dir, title }, } = this.context setBackgroundColor(this.getAttribute('background-color')) return `
${this.renderChildren()}
` } } ================================================ FILE: packages/mjml-browser/README.md ================================================ ## MJML Browser build This package allows MJML to be used client-side. ### Usage It can be used as the regular mjml package : ```javascript var mjml2html = require('mjml-browser') var result = mjml2html(mjml, options) ``` ### Unavailable features - `mj-include` tags are unavailable and will be ignored. - features involving the `.mjmlconfig` file are unavailable, which means no custom components. ================================================ FILE: packages/mjml-browser/browser-mocks/fs.js ================================================ module.exports = { readFileSync: () => { console.warn('fs should not be used in browser build') // eslint-disable-line no-console return null }, } ================================================ FILE: packages/mjml-browser/browser-mocks/path.js ================================================ const mockFn = () => { console.warn('fs should not be used in browser build') // eslint-disable-line no-console return null } module.exports = { parse: mockFn, resolve: mockFn, join: mockFn, dirname: mockFn, isAbsolute: mockFn, } ================================================ FILE: packages/mjml-browser/browser-mocks/uglify-js.js ================================================ module.exports = {} ================================================ FILE: packages/mjml-browser/package.json ================================================ { "name": "mjml-browser", "description": "MJML: the only framework that makes responsive-email easy", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-browser" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "webpack" }, "devDependencies": { "@babel/cli": "^7.8.4", "@babel/core": "^7.8.4", "@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-decorators": "^7.8.3", "@babel/plugin-proposal-export-default-from": "^7.8.3", "@babel/plugin-proposal-function-bind": "^7.8.3", "@babel/preset-env": "^7.8.4", "babel-loader": "^8.0.6", "rimraf": "^3.0.2", "uglifyjs-webpack-plugin": "^2.1.3", "webpack": "^4.36.1", "webpack-cli": "^3.3.6" } } ================================================ FILE: packages/mjml-browser/webpack.config.js ================================================ const path = require('path') const UglifyJsPlugin = require('uglifyjs-webpack-plugin') module.exports = { mode: 'production', entry: { "mjml": ['../mjml/lib/index'], }, optimization: { minimizer: [ new UglifyJsPlugin({ uglifyOptions: { ecma: 5, keep_classnames: true, keep_fnames: true, compress: { passes: 2, keep_fargs: false, }, output: { beautify: false, }, mangle: true, }, }), ], }, output: { library: 'mjml', filename: 'index.js', path: path.resolve(__dirname, './lib'), libraryTarget: 'umd', umdNamedDefine: true, }, resolve: { alias: { 'path': path.resolve(__dirname, 'browser-mocks/path'), 'fs': path.resolve(__dirname, 'browser-mocks/fs'), 'uglify-js': path.resolve(__dirname, 'browser-mocks/uglify-js'), }, }, module: { rules: [ { test: /\.js$/, exclude: path.join(__dirname, 'node_modules'), use: [ { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', ], plugins: [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose" : true }], "@babel/plugin-proposal-function-bind", "@babel/plugin-proposal-export-default-from", ], babelrc: false, }, }, ], }, ], }, } ================================================ FILE: packages/mjml-button/README.md ================================================ ### mj-button Displays a customizable button.
desktop
```xml Don't click me! ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ---------------------------------- | ----------------------------------------------------- | -------------------------------------- | | align | `left` `center` `right` | horizontal alignment | `center` | | background-color | CSS color formats | button background-color | `#414141` | | border | string | CSS border format | `none` | | border-bottom | string | CSS border format | | | border-left | string | CSS border format | | | border-radius | string | border radius | `3px` | | border-right | string | CSS border format | | | border-top | string | CSS border format | | | color | CSS color formats | text color | `#ffffff` | | container-background-color | CSS color formats | button container background color | | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font name | `Ubuntu, Helvetica, Arial, sans-serif` | | font-size | `px` | text size | `13px` | | font-style | string | CSS values, e.g. `normal` `italic` `oblique` | | | font-weight | string | text thickness | `normal` | | height | `px` `%` | button height | | | href | string | URL format | | | inner-padding | `px` `%` | inner button padding,
supports up to 4 parameters | `10px 25px` | | letter-spacing | `px` `em` | letter-spacing | | | line-height | `px` `%` | line-height on link | `120%` | | name | string | specify the name attribute for the button link | | padding | `px` `%` | button container padding, supports up to 4 parameters | `10px 25px` | | padding-bottom | `px` `%` | button container bottom padding | | | padding-left | `px` `%` | button container left padding | | | padding-right | `px` `%` | button container right padding | | | padding-top | `px` `%` | button container top padding | | | rel | string | specify the rel attribute for the button link | | | target | string | specify the target attribute for the button link | `_blank` | | text-align | `left` `center` `right` | text-align button content | | | text-decoration | string | underline/overline/none | `none` | | text-transform | string | capitalize/uppercase/lowercase | `none` | | title | string | tooltip & accessibility | | | vertical-align | `top` `bottom` `middle` `vertical` | vertical alignment | `middle` | | width | `px` `%` | button width | |

Try it live

================================================ FILE: packages/mjml-button/package.json ================================================ { "name": "mjml-button", "description": "mjml-button", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-button" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-button/src/index.js ================================================ import { BodyComponent } from 'mjml-core' import widthParser from 'mjml-core/lib/helpers/widthParser' export default class MjButton extends BodyComponent { static componentName = 'mj-button' static endingTag = true static allowedAttributes = { align: 'enum(left,center,right)', 'background-color': 'color', 'border-bottom': 'string', 'border-left': 'string', 'border-radius': 'string', 'border-right': 'string', 'border-top': 'string', border: 'string', color: 'color', 'container-background-color': 'color', 'font-family': 'string', 'font-size': 'unit(px)', 'font-style': 'string', 'font-weight': 'string', height: 'unit(px,%)', href: 'string', name: 'string', title: 'string', 'inner-padding': 'unit(px,%){1,4}', 'letter-spacing': 'unitWithNegative(px,em)', 'line-height': 'unit(px,%,)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', rel: 'string', target: 'string', 'text-decoration': 'string', 'text-transform': 'string', 'vertical-align': 'enum(top,bottom,middle)', 'text-align': 'enum(left,right,center)', width: 'unit(px,%)', } static defaultAttributes = { align: 'center', 'background-color': '#414141', border: 'none', 'border-radius': '3px', color: '#ffffff', 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'font-weight': 'normal', 'inner-padding': '10px 25px', 'line-height': '120%', padding: '10px 25px', target: '_blank', 'text-decoration': 'none', 'text-transform': 'none', 'vertical-align': 'middle', } getStyles() { return { table: { 'border-collapse': 'separate', width: this.getAttribute('width'), 'line-height': '100%', }, td: { border: this.getAttribute('border'), 'border-bottom': this.getAttribute('border-bottom'), 'border-left': this.getAttribute('border-left'), 'border-radius': this.getAttribute('border-radius'), 'border-right': this.getAttribute('border-right'), 'border-top': this.getAttribute('border-top'), cursor: 'auto', 'font-style': this.getAttribute('font-style'), height: this.getAttribute('height'), 'mso-padding-alt': this.getAttribute('inner-padding'), 'text-align': this.getAttribute('text-align'), background: this.getAttribute('background-color'), }, content: { display: 'inline-block', width: this.calculateAWidth(this.getAttribute('width')), background: this.getAttribute('background-color'), color: this.getAttribute('color'), 'font-family': this.getAttribute('font-family'), 'font-size': this.getAttribute('font-size'), 'font-style': this.getAttribute('font-style'), 'font-weight': this.getAttribute('font-weight'), 'line-height': this.getAttribute('line-height'), 'letter-spacing': this.getAttribute('letter-spacing'), margin: '0', 'text-decoration': this.getAttribute('text-decoration'), 'text-transform': this.getAttribute('text-transform'), padding: this.getAttribute('inner-padding'), 'mso-padding-alt': '0px', 'border-radius': this.getAttribute('border-radius'), }, } } calculateAWidth(width) { if (!width) return null const { parsedWidth, unit } = widthParser(width) // impossible to handle percents because it depends on padding and text width if (unit !== 'px') return null const { borders } = this.getBoxWidths() const innerPaddings = this.getShorthandAttrValue('inner-padding', 'left') + this.getShorthandAttrValue('inner-padding', 'right') return `${parsedWidth - innerPaddings - borders}px` } render() { const tag = this.getAttribute('href') ? 'a' : 'p' return `
<${tag} ${this.htmlAttributes({ href: this.getAttribute('href'), name: this.getAttribute('name'), rel: this.getAttribute('rel'), title: this.getAttribute('title'), style: 'content', target: tag === 'a' ? this.getAttribute('target') : undefined, })} > ${this.getContent()}
` } } ================================================ FILE: packages/mjml-carousel/README.md ================================================ ### mj-carousel Displays a gallery of images or "carousel". Readers can interact by hovering and clicking on thumbnails depending on the email client they use.
desktop
```xml ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ------------------------------ | ------------------------------------------------------ | --------------------------------- | | align | `left` `center` `right` | horizontal alignment | `center` | | border-radius | `px` `%` | border radius | `6px` | | container-background-color | CSS color formats | column background color | | | css-class | string | class name, added to the root HTML element created | | | icon-width | `px` `%` | width of the icons on left and right of the main image | `44px` | | left-icon | string | icon on the left of the main image | `https://i.imgur.com/xTh3hln.png` | | padding | `px` `%` | carousel padding, supports up to 4 parameters | | | padding-bottom | `px` `%` | carousel bottom padding | | | padding-left | `px` `%` | carousel left padding | | | padding-right | `px` `%` | carousel right padding | | | padding-top | `px` `%` | carousel top padding | | | right-icon | string | icon on the right of the main image | `https://i.imgur.com/os7o9kz.png` | | tb-border | string | border of the thumbnails in CSS border format | `2px solid transparent` | | tb-border-radius | `px` `%` | border-radius of the thumbnails | `6px` | | tb-hover-border-color | CSS color formats | border color of the hovered thumbnail | `#fead0d` | | tb-selected-border-color | CSS color formats | border color of the selected thumbnail | `#ccc` | | tb-width | `px` `%` | thumbnail width | | | thumbnails | `visible` `hidden` `supported` | display the thumbnails | `hidden` |

Try it live

#### mj-carousel-image Enables you to add and style the images in the carousel. #### Attributes | attribute | accepts | description | default value | | ---------------- | -------- | -------------------------------------------------- | ------------- | | alt | string | image description | `''` | | border-radius | `px` `%` | border radius of the main image | | | css-class | string | class name, added to the root HTML element created | | | href | string | link to redirect to on click,
URL format | | | rel | string | specify the rel attribute | | | src | string | URL format | | | target | string | link target on click | `_blank` | | tb-border | string | CSS border format | | | tb-border-radius | `px` `%` | border radius of the thumbnail | | | thumbnails-src | string | specify a different thumbnail image in URL format | | | title | string | tooltip & accessibility | | ================================================ FILE: packages/mjml-carousel/package.json ================================================ { "name": "mjml-carousel", "description": "mjml-carousel", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-carousel" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-carousel/src/Carousel.js ================================================ import { BodyComponent } from 'mjml-core' import { range, repeat, min, map } from 'lodash' import { msoConditionalTag } from 'mjml-core/lib/helpers/conditionalTag' import genRandomHexString from 'mjml-core/lib/helpers/genRandomHexString' export default class MjCarousel extends BodyComponent { static componentName = 'mj-carousel' static allowedAttributes = { align: 'enum(left,center,right)', 'border-radius': 'unit(px,%){1,4}', 'container-background-color': 'color', 'icon-width': 'unit(px,%)', 'left-icon': 'string', padding: 'unit(px,%){1,4}', 'padding-top': 'unit(px,%)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'right-icon': 'string', thumbnails: 'enum(visible,hidden,supported)', 'tb-border': 'string', 'tb-border-radius': 'unit(px,%)', 'tb-hover-border-color': 'color', 'tb-selected-border-color': 'color', 'tb-width': 'unit(px,%)', } static defaultAttributes = { align: 'center', 'border-radius': '6px', 'icon-width': '44px', 'left-icon': 'https://i.imgur.com/xTh3hln.png', 'right-icon': 'https://i.imgur.com/os7o9kz.png', thumbnails: 'visible', 'tb-border': '2px solid transparent', 'tb-border-radius': '6px', 'tb-hover-border-color': '#fead0d', 'tb-selected-border-color': '#ccc', } constructor(initialDatas = {}) { super(initialDatas) this.carouselId = genRandomHexString(16) } componentHeadStyle = () => { const { length } = this.props.children const { carouselId } = this if (!length) return '' const carouselCss = ` .mj-carousel { -webkit-user-select: none; -moz-user-select: none; user-select: none; } .mj-carousel-${carouselId}-icons-cell { display: table-cell !important; width: ${this.getAttribute('icon-width')} !important; } .mj-carousel-radio, .mj-carousel-next, .mj-carousel-previous { display: none !important; } .mj-carousel-thumbnail, .mj-carousel-next, .mj-carousel-previous { touch-action: manipulation; } ${range(0, length) .map( (i) => `.mj-carousel-${carouselId}-radio:checked ${repeat( '+ * ', i, )}+ .mj-carousel-content .mj-carousel-image`, ) .join(',')} { display: none !important; } ${range(0, length) .map( (i) => `.mj-carousel-${carouselId}-radio-${i + 1}:checked ${repeat( '+ * ', length - i - 1, )}+ .mj-carousel-content .mj-carousel-image-${i + 1}`, ) .join(',')} { display: block !important; } .mj-carousel-previous-icons, .mj-carousel-next-icons, ${range(0, length).map( (i) => `.mj-carousel-${carouselId}-radio-${i + 1}:checked ${repeat( '+ * ', length - i - 1, )}+ .mj-carousel-content .mj-carousel-next-${ ((i + (1 % length) + length) % length) + 1 }`, )}, ${range(0, length).map( (i) => `.mj-carousel-${carouselId}-radio-${i + 1}:checked ${repeat( '+ * ', length - i - 1, )}+ .mj-carousel-content .mj-carousel-previous-${ ((i - (1 % length) + length) % length) + 1 }`, )} { display: block !important; } ${range(0, length) .map( (i) => `.mj-carousel-${carouselId}-radio-${i + 1}:checked ${repeat( '+ * ', length - i - 1, )}+ .mj-carousel-content .mj-carousel-${carouselId}-thumbnail-${ i + 1 }`, ) .join(',')} { border-color: ${this.getAttribute('tb-selected-border-color')} !important; } ${range(0, length) .map( (i) => `.mj-carousel-${carouselId}-radio-${i + 1}:checked ${repeat( '+ * ', length - i - 1, )}+ .mj-carousel-content .mj-carousel-${carouselId}-thumbnail `, ) .join(',')} { display: inline-block !important; } .mj-carousel-image img + div, .mj-carousel-thumbnail img + div { display: none !important; } ${range(0, length) .map( (i) => `.mj-carousel-${carouselId}-thumbnail:hover ${repeat( '+ * ', length - i - 1, )}+ .mj-carousel-main .mj-carousel-image`, ) .join(',')} { display: none !important; } .mj-carousel-thumbnail:hover { border-color: ${this.getAttribute('tb-hover-border-color')} !important; } ${range(0, length) .map( (i) => `.mj-carousel-${carouselId}-thumbnail-${i + 1}:hover ${repeat( '+ * ', length - i - 1, )}+ .mj-carousel-main .mj-carousel-image-${i + 1}`, ) .join(',')} { display: block !important; } ` const fallback = ` .mj-carousel noinput { display:block !important; } .mj-carousel noinput .mj-carousel-image-1 { display: block !important; } .mj-carousel noinput .mj-carousel-arrows, .mj-carousel noinput .mj-carousel-thumbnails { display: none !important; } [owa] .mj-carousel-thumbnail { display: none !important; } @media screen yahoo { .mj-carousel-${this.carouselId}-icons-cell, .mj-carousel-previous-icons, .mj-carousel-next-icons { display: none !important; } .mj-carousel-${carouselId}-radio-1:checked ${repeat( '+ *', length - 1, )}+ .mj-carousel-content .mj-carousel-${carouselId}-thumbnail-1 { border-color: transparent; } } ` return `${carouselCss}\n${fallback}` } getStyles() { return { carousel: { div: { display: 'table', width: '100%', 'table-layout': 'fixed', 'text-align': 'center', 'font-size': '0px', }, table: { 'caption-side': 'top', display: 'table-caption', 'table-layout': 'fixed', width: '100%', }, }, images: { td: { padding: '0px', }, }, controls: { div: { display: 'none', 'mso-hide': 'all', }, img: { display: 'block', width: this.getAttribute('icon-width'), height: 'auto', }, td: { 'font-size': '0px', display: 'none', 'mso-hide': 'all', padding: '0px', }, }, } } thumbnailsWidth() { if (!this.props.children.length) return 0 return ( this.getAttribute('tb-width') || `${min([this.context.parentWidth / this.props.children.length, 110])}px` ) } imagesAttributes() { return map(this.children, 'attributes') } generateRadios() { return this.renderChildren(this.props.children, { renderer: (component) => component.renderRadio(), attributes: { carouselId: this.carouselId, }, }) } generateThumbnails() { if (!['visible', 'supported'].includes(this.getAttribute('thumbnails'))) return '' return this.renderChildren(this.props.children, { attributes: { 'tb-border': this.getAttribute('tb-border'), 'tb-border-radius': this.getAttribute('tb-border-radius'), 'tb-width': this.thumbnailsWidth(), carouselId: this.carouselId, }, renderer: (component) => component.renderThumbnail(), }) } generateControls(direction, icon) { const iconWidth = parseInt(this.getAttribute('icon-width'), 10) return `
${range(1, this.props.children.length + 1) .map( (i) => ` `, ) .join('')}
` } generateImages() { return `
${this.renderChildren(this.props.children, { attributes: { 'border-radius': this.getAttribute('border-radius'), }, })}
` } generateCarousel() { return ` ${this.generateControls('previous', this.getAttribute('left-icon'))} ${this.generateImages()} ${this.generateControls('next', this.getAttribute('right-icon'))}
` } renderFallback() { const { children } = this.props if (children.length === 0) return '' return msoConditionalTag( this.renderChildren([children[0]], { attributes: { 'border-radius': this.getAttribute('border-radius'), }, }), ) } getChildContext() { return { ...this.context, thumbnails: this.getAttribute('thumbnails'), } } render() { return ` ${msoConditionalTag( `
${this.generateRadios()}
${this.generateThumbnails()} ${this.generateCarousel()}
`, true, )} ${this.renderFallback()} ` } } ================================================ FILE: packages/mjml-carousel/src/CarouselImage.js ================================================ import { BodyComponent, suffixCssClasses } from 'mjml-core' export default class MjCarouselImage extends BodyComponent { static componentName = 'mj-carousel-image' static endingTag = true static allowedAttributes = { alt: 'string', href: 'string', rel: 'string', target: 'string', title: 'string', src: 'string', 'thumbnails-src': 'string', 'border-radius': 'unit(px,%){1,4}', 'tb-border': 'string', 'tb-border-radius': 'unit(px,%){1,4}', } static defaultAttributes = { alt: '', target: '_blank', } getStyles() { const hasThumbnailsSupported = this.hasThumbnailsSupported() return { images: { img: { 'border-radius': this.getAttribute('border-radius'), display: 'block', width: this.context.containerWidth, 'max-width': '100%', height: 'auto', }, firstImageDiv: {}, otherImageDiv: { display: 'none', 'mso-hide': 'all', }, }, radio: { input: { display: 'none', 'mso-hide': 'all', }, }, thumbnails: { a: { border: this.getAttribute('tb-border'), 'border-radius': this.getAttribute('tb-border-radius'), display: hasThumbnailsSupported ? 'none' : 'inline-block', overflow: 'hidden', width: this.getAttribute('tb-width'), }, img: { display: 'block', width: '100%', height: 'auto', }, }, } } hasThumbnailsSupported() { const thumbnails = this.getAttribute('thumbnails') || this.context.thumbnails return thumbnails === 'supported' } renderThumbnail() { const { carouselId, src, alt, 'tb-width': width, target } = this.attributes const imgIndex = this.props.index + 1 const cssClass = suffixCssClasses( this.getAttribute('css-class'), 'thumbnail', ) return ` ` } renderRadio() { const { index } = this.props const carouselId = this.getAttribute('carouselId') return ` ` } render() { const { src, alt, href, rel, title } = this.attributes const { index } = this.props const image = ` ` const cssClass = this.getAttribute('css-class') || '' return `
${ href ? `${image}` : image }
` } } ================================================ FILE: packages/mjml-carousel/src/index.js ================================================ export { default as Carousel } from './Carousel' export { default as CarouselImage } from './CarouselImage' ================================================ FILE: packages/mjml-cli/README.md ================================================ ## mjml-cli # Installation We recommend installing and using MJML locally, in a project folder where you'll use MJML: ```bash npm install mjml ``` In the folder where you installed MJML you can now run: ```bash ./node_modules/.bin/mjml input.mjml ``` To avoid typing `./node_modules/.bin/`, add it to your PATH: ```bash export PATH="$PATH:./node_modules/.bin" ``` You can now run MJML directly, in that folder: ```bash mjml input.mjml ``` MJML is written with [NodeJS](https://nodejs.org/en/) You can download and install the MJML engine from [NPM](https://www.npmjs.com). # Command Line Interface In addition to the translation engine, which converts MJML to email HTML, we've bundled a Command Line Interface (CLI) helping you to achieve the basic features it offers and integrate it seamlessly in your development flow. ### Render MJML to HTML ```bash mjml input.mjml ``` It will output a HTML file called `input.html`. Input can also be a directory. ### Migrate MJML3 to MJML4 ```bash $> mjml -m input.mjml -o result.mjml ``` It will output a MJML file called `result.mjml`. ### Validate MJML ```bash $> mjml -v input.mjml ``` It will log validation errors. If there are errors, exits with code 1. Otherwise, exits with code 0. ### Render and redirect the result to stdout ```bash mjml -s input.mjml # or mjml --stdout input.mjml ``` ### Render and redirect the result to a file ```bash mjml input.mjml -o my-email.html # or mjml input.mjml --output my-email.html ``` You can output the resulting email responsive HTML in a file. If the output file does not exist it will be created, but output directories must already exist. If output is a directory, output file(s) will be `output/input-file-name.html` ### Set the validation mode ```bash mjml -l skip -r input.mjml ``` Accepted values are - 'normal' : *(default)* will display validation messages but compile anyway - 'skip' : the file is rendered without being validated - 'strict' : will throw an error if validation fails ### Watch changes on a file ```bash mjml -w input.mjml # or mjml --watch input.mjml ``` If you like live-coding, you might want to use the `-w` option that enables you to re-render your file every time you save it. It can be time-saving when you can just split you screen and see the HTML output modified when you modify your MJML. Of course, the `-w` option can be used with an `--output` option too. ### Available options ```bash mjml input.mjml --config.optionName value # or mjml input.mjml -c.optionName value ``` All the options that can be passed to mjml2html (see general documentation) can be provided. The most common ones are detailed below. ### Minify and beautify the output HTML ```bash $> mjml input.mjml --config.beautify true --config.minify false ``` These are the default options. ### Change minify options ```bash $> mjml input.mjml --config.minifyOptions='{"minifyCSS": true, "removeEmptyAttributes": false}' ``` The defaults are "collapseWhitespace": true, "minifyCSS": false, "removeEmptyAttributes": true See html-minifier documentation for more available options ### Change juice options (library used for inlining mj-style css) ```bash $> mjml input.mjml --config.juiceOptions='{"preserveImportant": true}' ``` The defaults are "applyStyleTags": false, "insertPreservedExtraCss": false, "removeStyleTags": false See juice documentation for more available options ### Preserve specific tags when using inline mj-style ```bash $> mjml input.mjml --config.juicePreserveTags='{"myTag": { "start": "<#", "end": "` the css will be inlined using the juice library. As a side effect, juice will convert all tags' attributes into lower case. If you need to preserve some cases (i.e. for a templating lib) you can specify the tags to preserve. With the example above, all tags of the form `<# myVar="" >` or `` will be left untouched. By default juice already ignores `<% EJS %>` and `{{ HBS }}` tags. ### Override base path for mj-include relative paths ```bash $> mjml ./my-project/input.mjml --config.filePath ./my-partials/ ``` If you like to keep your partials together and you want to be able to mj-include them without having to change the relative path of the includes depending on the compiled file path, you can use this option. In this exemple, `` will include `./my-partials/header.mjml`, ignoring the actual path of `input.mjml`. ### Log error stack ```bash $> mjml input.mjml --config.stack true ``` ================================================ FILE: packages/mjml-cli/bin/mjml ================================================ #!/usr/bin/env node require('../lib/client.js')() ================================================ FILE: packages/mjml-cli/package.json ================================================ { "name": "mjml-cli", "description": "MJML: the only framework that makes responsive-email easy", "version": "4.18.0", "main": "bin/mjml", "bin": { "mjml-cli": "bin/mjml" }, "files": [ "bin", "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-cli" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "chokidar": "^3.0.0", "glob": "^10.3.10", "html-minifier": "^4.0.0", "js-beautify": "^1.6.14", "lodash": "^4.17.21", "minimatch": "^9.0.3", "mjml-core": "4.18.0", "mjml-migrate": "4.18.0", "mjml-parser-xml": "4.18.0", "mjml-validator": "4.18.0", "yargs": "^17.7.2" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-cli/src/client.js ================================================ import path from 'path' import yargs from 'yargs' import { flow, pick, isNil, negate, pickBy } from 'lodash/fp' import { isArray, isEmpty, map, get, omit } from 'lodash' import { html as htmlBeautify } from 'js-beautify' import { minify as htmlMinify } from 'html-minifier' import mjml2html, { components, initializeType } from 'mjml-core' import migrate from 'mjml-migrate' import validate, { dependencies } from 'mjml-validator' import MJMLParser from 'mjml-parser-xml' import { version as coreVersion } from 'mjml-core/package.json' import readFile, { flatMapPaths } from './commands/readFile' import watchFiles from './commands/watchFiles' import readStream from './commands/readStream' import outputToFile, { isDirectory } from './commands/outputToFile' import outputToConsole from './commands/outputToConsole' import { version as cliVersion } from '../package.json' import DEFAULT_OPTIONS from './helpers/defaultOptions' const beautifyConfig = { indent_size: 2, wrap_attributes_indent_size: 2, max_preserve_newline: 0, preserve_newlines: false, end_with_newline: true, } const minifyConfig = { collapseWhitespace: true, minifyCSS: false, caseSensitive: true, removeEmptyAttributes: true, } export default async () => { let EXIT_CODE = 0 let KEEP_OPEN = false const error = (msg) => { console.error('\nCommand line error:') // eslint-disable-line no-console console.error(msg) // eslint-disable-line no-console process.exit(1) } const pickArgs = (args) => flow( pick(args), pickBy((e) => negate(isNil)(e) && !(isArray(e) && isEmpty(e))), ) const { argv } = yargs .version(false) // cf. https://github.com/yargs/yargs/issues/961 .options({ r: { alias: 'read', describe: 'Compile MJML File(s)', type: 'array', }, m: { alias: 'migrate', describe: 'Migrate MJML3 File(s) (deprecated)', type: 'array', }, v: { alias: 'validate', describe: 'Run validator on File(s)', type: 'array', }, w: { alias: 'watch', type: 'array', describe: 'Watch and compile MJML File(s) when modified', }, i: { alias: 'stdin', describe: 'Compiles MJML from input stream', }, s: { alias: 'stdout', describe: 'Output HTML to stdout', }, o: { alias: 'output', type: 'string', describe: 'Filename/Directory to output compiled files', }, c: { alias: 'config', type: 'object', describe: 'Option to pass to mjml-core', }, version: { alias: 'V', }, noStdoutFileComment: { type: 'boolean', describe: 'Add no file comment to stdout', }, }) .help() .version(`mjml-core: ${coreVersion}\nmjml-cli: ${cliVersion}`) let juiceOptions let minifyOptions let juicePreserveTags let fonts try { juiceOptions = argv.c && argv.c.juiceOptions && JSON.parse(argv.c.juiceOptions) } catch (e) { error(`Failed to decode JSON for config.juiceOptions argument`) } try { minifyOptions = argv.c && argv.c.minifyOptions && JSON.parse(argv.c.minifyOptions) } catch (e) { error(`Failed to decode JSON for config.minifyOptions argument`) } try { juicePreserveTags = argv.c && argv.c.juicePreserveTags && JSON.parse(argv.c.juicePreserveTags) } catch (e) { error(`Failed to decode JSON for config.juicePreserveTags argument`) } try { fonts = argv.c && argv.c.fonts && JSON.parse(argv.c.fonts) } catch (e) { error(`Failed to decode JSON for config.fonts argument`) } const filePath = argv.c && argv.c.filePath const config = Object.assign( DEFAULT_OPTIONS, argv.c, fonts && { fonts }, minifyOptions && { minifyOptions }, juiceOptions && { juiceOptions }, juicePreserveTags && { juicePreserveTags }, argv.c && argv.c.keepComments === 'false' && { keepComments: false }, ) const inputArgs = pickArgs(['r', 'w', 'i', '_', 'm', 'v'])(argv) const outputArgs = pickArgs(['o', 's'])(argv) // implies (until yargs pr is accepted) ;[ [Object.keys(inputArgs).length === 0, 'No input argument received'], [Object.keys(inputArgs).length > 1, 'Too many input arguments received'], [Object.keys(outputArgs).length > 1, 'Too many output arguments received'], [ argv.w && argv.w.length > 1 && !argv.o, 'Need an output option when watching files', ], [ argv.w && argv.w.length > 1 && argv.o && !isDirectory(argv.o) && argv.o !== '', 'Need an output option when watching files', ], ].forEach((v) => (v[0] ? error(v[1]) : null)) const inputOpt = Object.keys(inputArgs)[0] const outputOpt = Object.keys(outputArgs)[0] || 's' const inputFiles = isArray(inputArgs[inputOpt]) ? inputArgs[inputOpt] : [inputArgs[inputOpt]] const inputs = [] switch (inputOpt) { case 'r': case 'v': case 'm': case '_': { flatMapPaths(inputFiles).forEach((file) => { inputs.push(readFile(file)) }) if (!inputs.length) { error('No input files found') return } break } case 'w': watchFiles(inputFiles, { ...argv, config, minifyConfig, beautifyConfig, }) KEEP_OPEN = true break case 'i': inputs.push(await readStream()) break default: error('Command line error: Incorrect input options') } const convertedStream = [] const failedStream = [] inputs.forEach((i) => { try { let compiled switch (inputOpt) { case 'm': compiled = { html: migrate(i.mjml, { beautify: true }) } break case 'v': // eslint-disable-next-line no-case-declarations const mjmlJson = MJMLParser(i.mjml, { components, filePath: filePath || i.file, actualPath: i.file, }) compiled = { errors: validate(mjmlJson, { dependencies, components, initializeType, }), } break default: { const beautify = config.beautify && config.beautify !== 'false' const minify = config.minify && config.minify !== 'false' compiled = mjml2html(i.mjml, { ...omit(config, ['minify', 'beautify']), filePath: filePath || i.file, actualPath: i.file, }) if (beautify) { compiled.html = htmlBeautify(compiled.html, beautifyConfig) } if (minify) { compiled.html = htmlMinify(compiled.html, { ...minifyConfig, ...config.minifyOptions, }) } } } convertedStream.push({ ...i, compiled }) } catch (e) { EXIT_CODE = 2 failedStream.push({ file: i.file, error: e }) } }) convertedStream.forEach((s) => { if (get(s, 'compiled.errors.length')) { console.error(map(s.compiled.errors, 'formattedMessage').join('\n')) // eslint-disable-line no-console } }) failedStream.forEach(({ error, file }) => { console.error(`${file ? `File: ${file}\n` : null}${error}`) // eslint-disable-line no-console if (config.stack) { console.error(error.stack) // eslint-disable-line no-console } }) if (inputOpt === 'v') { const isInvalid = failedStream.length || convertedStream.some((s) => !!get(s, 'compiled.errors.length')) if (isInvalid) { error('Validation failed') return } process.exitCode = 0 return } if (!KEEP_OPEN && convertedStream.length === 0) { error('Input file(s) failed to render') } switch (outputOpt) { case 'o': { if (inputs.length > 1 && !isDirectory(argv.o) && argv.o !== '') { error( `Multiple input files, but output option should be either an existing directory or an empty string: ${argv.o} given`, ) } const fullOutputPath = path.parse(path.resolve(process.cwd(), argv.o)) if (inputs.length === 1 && !isDirectory(fullOutputPath.dir)) { error(`Output directory doesn’t exist for path : ${argv.o}`) } Promise.all(convertedStream.map(outputToFile(argv.o))) .then(() => { if (!KEEP_OPEN) { process.exitCode = EXIT_CODE } }) .catch(({ outputName, err }) => { if (!KEEP_OPEN) { error(`Error writing file - ${outputName} : ${err}`) } }) break } case 's': { const addFileHeaderComment = !argv.noStdoutFileComment Promise.all( convertedStream.map((converted) => outputToConsole(converted, addFileHeaderComment), ), ) .then(() => (process.exitCode = EXIT_CODE)) // eslint-disable-line no-return-assign .catch(() => (process.exitCode = 1)) // eslint-disable-line no-return-assign break } default: error('Command line error: No output option available') } } ================================================ FILE: packages/mjml-cli/src/commands/outputToConsole.js ================================================ export default ({ compiled: { html }, file }, addFileHeaderComment) => new Promise((resolve) => { let output = '' if (addFileHeaderComment) { output = `\n` } output += `${html}\n` process.stdout.write(output, resolve) }) ================================================ FILE: packages/mjml-cli/src/commands/outputToFile.js ================================================ import fs from 'fs' import path from 'path' export const isDirectory = (file) => { try { const outputPath = path.resolve(process.cwd(), file) return fs.statSync(outputPath).isDirectory() } catch (e) { return false } } const replaceExtension = (input) => input.replace( '.mjml', input.replace('.mjml', '').match(/(.)*\.(.)+$/g) ? '' : '.html', ) const stripPath = (input) => input.match(/[^/\\]+$/g)[0] const makeGuessOutputName = (outputPath) => { if (isDirectory(outputPath)) { return (input) => path.join(outputPath, replaceExtension(stripPath(input))) } return (input) => { if (!outputPath) { return replaceExtension(stripPath(input)) } return outputPath } } export default (outputPath) => { const guessOutputName = makeGuessOutputName(outputPath) return ({ file, compiled: { html } }) => new Promise((resolve, reject) => { const outputName = guessOutputName(file) fs.writeFile(outputName, html, (err) => { if (err) { // eslint-disable-next-line prefer-promise-reject-errors return reject({ outputName, err }) } return resolve(outputName) }) }) } ================================================ FILE: packages/mjml-cli/src/commands/readFile.js ================================================ import fs from 'fs' import { sync } from 'glob' import { flatMap } from 'lodash' export const flatMapPaths = (paths) => flatMap(paths, (p) => sync(p, { nodir: true })) export default (path) => { try { return { file: path, mjml: fs.readFileSync(path).toString() } } catch (e) { // eslint-disable-next-line console.warn(`Cannot read file: ${path} doesn't exist or no access`, e) return {} } } ================================================ FILE: packages/mjml-cli/src/commands/readStream.js ================================================ const stdinSync = () => new Promise((res) => { let buffer = '' const stream = process.stdin stream.on('data', (chunck) => { buffer += chunck }) stream.on('end', () => res(buffer)) }) export default async () => { const mjml = await stdinSync() return { mjml } } ================================================ FILE: packages/mjml-cli/src/commands/watchFiles.js ================================================ /* eslint-disable no-console */ import chokidar from 'chokidar' import { sync } from 'glob' import { match } from 'minimatch' import path from 'path' import mjml2html from 'mjml-core' import { flow, pickBy, flatMap, uniq, difference, remove } from 'lodash/fp' import { omit } from 'lodash' import { html as htmlBeautify } from 'js-beautify' import { minify as htmlMinify } from 'html-minifier' import readFile from './readFile' import makeOutputToFile from './outputToFile' import fileContext from '../helpers/fileContext' let dirty = [] const _flatMap = flatMap.convert({ cap: false }) // eslint-disable-line no-underscore-dangle const flatMapAndJoin = _flatMap((v, k) => v.map((p) => path.join(k, p))) const flatMapKeyAndValues = flow( _flatMap((v, k) => [k, ...v]), uniq, ) export default (input, options) => { const dependencies = {} const outputToFile = makeOutputToFile(options.o) const getRelatedFiles = (file) => flow( pickBy((v, k) => k === file || v.indexOf(file) !== -1), Object.keys, )(dependencies) const synchronyzeWatcher = (filePath) => { getRelatedFiles(filePath).forEach((f) => { dependencies[f] = fileContext(f, options.config.filePath) if (dirty.indexOf(f) === -1) { dirty.push(f) } }) /* eslint-disable no-use-before-define */ const files = { toWatch: flatMapKeyAndValues(dependencies), watched: flatMapAndJoin(watcher.getWatched()), } watcher.add(difference(files.toWatch, files.watched)) watcher.unwatch(difference(files.watched, files.toWatch)) /* eslint-enable no-use-before-define */ } const readAndCompile = flow( (file) => ({ file, content: readFile(file).mjml }), (args) => { const { config, beautifyConfig, minifyConfig } = options const beautify = config.beautify && config.beautify !== 'false' const minify = config.minify && config.minify !== 'false' const compiled = mjml2html(args.content, { filePath: args.file, actualPath: args.file, ...omit(config, ['minify', 'beautify']), }) if (beautify) { compiled.html = htmlBeautify(compiled.html, beautifyConfig) } if (minify) { compiled.html = htmlMinify(compiled.html, { ...minifyConfig, ...config.minifyOptions, }) } return { ...args, compiled, } }, (args) => { const { compiled: { errors }, } = args errors.forEach((e) => console.warn(e.formattedMessage)) return args }, (args) => outputToFile(args) .then(() => console.log(`${args.file} - Successfully compiled`)) .catch(() => console.log(`${args.file} - Error while compiling file`)), ) const watcher = chokidar .watch(input.map((i) => i.replace(/\\/g, '/'))) .on('change', (file) => synchronyzeWatcher(path.resolve(file))) .on('add', (file) => { const filePath = path.resolve(file) console.log(`Now watching file: ${filePath}`) const matchInputOption = input.reduce( (found, file) => found || match(sync(path.resolve(file)), filePath)?.length > 0, false, ) if (matchInputOption) { dependencies[filePath] = getRelatedFiles(filePath) } synchronyzeWatcher(filePath) }) .on('unlink', (file) => { const filePath = path.resolve(file) delete dependencies[path.resolve(filePath)] remove(dirty, (f) => f === filePath) synchronyzeWatcher(filePath) }) setInterval(() => { dirty.forEach((f) => { console.log(`${f} - Change detected`) try { readAndCompile(f) } catch (e) { console.log(`${f} - Error while rendering the file : `, e) } }) dirty = [] }, 500) return [] } /* eslint-enable no-console */ ================================================ FILE: packages/mjml-cli/src/helpers/defaultOptions.js ================================================ export default { beautify: true, minify: false, } ================================================ FILE: packages/mjml-cli/src/helpers/fileContext.js ================================================ import fs from 'fs' import path from 'path' const includeRegexp = /]+path=['"](.*(?:\.mjml|\.css|\.html))['"]\s*[^<>]*(\/>|>\s*<\/mj-include>)/gi const ensureIncludeIsSupportedFile = (file) => path.extname(file).match(/\.mjml|\.css|\.html/) ? file : `${file}.mjml` const error = (e) => console.error(e.stack || e) // eslint-disable-line no-console export default (baseFile, filePath) => { const filesIncluded = [] let filePathDirectory = '' if (filePath) { try { const isFilePathDir = fs.lstatSync(filePath).isDirectory() filePathDirectory = isFilePathDir ? filePath : path.dirname(filePath) } catch (e) { if (e.code === 'ENOENT') { throw new Error('Specified filePath does not exist') } else { throw e } } } const readIncludes = (dir, file, base) => { const currentFile = path.resolve( dir ? path.join(dir, ensureIncludeIsSupportedFile(file)) : ensureIncludeIsSupportedFile(file), ) const currentDirectory = path.dirname(currentFile) const includes = new RegExp(includeRegexp) let content try { content = fs.readFileSync(currentFile, 'utf8') } catch (e) { error(`File not found ${currentFile} from ${base}`) return } let matchgroup = includes.exec(content) while (matchgroup != null) { const includedFile = ensureIncludeIsSupportedFile(matchgroup[1]) // when reading first level of includes we must join the path specified in filePath // when reading further nested includes, just take parent dir as base const targetDir = filePath && file === baseFile ? filePathDirectory : currentDirectory const includedFilePath = path.resolve(path.join(targetDir, includedFile)) filesIncluded.push(includedFilePath) readIncludes(targetDir, includedFile, currentFile) matchgroup = includes.exec(content) } } readIncludes(null, baseFile, baseFile) return filesIncluded } ================================================ FILE: packages/mjml-column/README.md ================================================ ### mj-column Columns enable you to organize the content of your sections into distinct columns which stack when viewed on a mobile device. They must be located within `mj-section` tags in order to be considered by the engine. Every single column has to contain something because they are responsive containers, and will be vertically stacked on a mobile view. Any standard component, or component that you have defined and registered, can be placed within a column – except `mj-column` or `mj-section` elements. ```xml ``` #### Attributes | attribute | accepts | description | default attributes | | ---------------------- | ----------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------- | | background-color | CSS color formats | background color for a column | | | border | string | CSS border format | | | border-bottom | string | CSS border format | | | border-left | string | CSS border format | | | border-radius | `px` `%` | border radius | | | border-right | string | CSS border format | | | border-top | string | CSS border format | | | css-class | string | class name, added to the root HTML element created | | | direction | `ltr` `rtl` | set the display order of direct children | `ltr` | | inner-background-color | CSS color formats | inner background color for column; requires a padding | | | inner-border | string | CSS border; requires a padding format | | | inner-border-bottom | string | CSS border format; requires a padding | | | inner-border-left | string | CSS border format; requires a padding | | | inner-border-radius | `px` `%` | border radius ; requires a padding | | | inner-border-right | string | CSS border format; requires a padding | | | inner-border-top | string | CSS border format; requires a padding | | | padding | `px` `%` | column padding, supports up to 4 parameters | | | padding-bottom | `px` `%` | column bottom padding | | | padding-left | `px` `%` | column left padding | | | padding-right | `px` `%` | column right padding | | | padding-top | `px` `%` | column top padding | | | width | `px` `%` | column width | (100 / number of non-raw elements in section)% | | vertical-align | `top` `middle` `bottom` | vertical alignment.
Note: `middle` only applies when all `mj-column` instances use it | `top` |

Try it live

================================================ FILE: packages/mjml-column/package.json ================================================ { "name": "mjml-column", "description": "mjml-column", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-column" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-column/src/index.js ================================================ import { BodyComponent } from 'mjml-core' import widthParser from 'mjml-core/lib/helpers/widthParser' export default class MjColumn extends BodyComponent { static componentName = 'mj-column' static allowedAttributes = { 'background-color': 'color', border: 'string', 'border-bottom': 'string', 'border-left': 'string', 'border-radius': 'unit(px,%){1,4}', 'border-right': 'string', 'border-top': 'string', direction: 'enum(ltr,rtl)', 'inner-background-color': 'color', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', 'inner-border': 'string', 'inner-border-bottom': 'string', 'inner-border-left': 'string', 'inner-border-radius': 'unit(px,%){1,4}', 'inner-border-right': 'string', 'inner-border-top': 'string', padding: 'unit(px,%){1,4}', 'vertical-align': 'enum(top,bottom,middle)', width: 'unit(px,%)', } static defaultAttributes = { direction: 'ltr', 'vertical-align': 'top', } getChildContext() { const { containerWidth: parentWidth } = this.context const { nonRawSiblings } = this.props const { borders, paddings } = this.getBoxWidths() const innerBorders = this.getShorthandBorderValue('left', 'inner-border') + this.getShorthandBorderValue('right', 'inner-border') const allPaddings = paddings + borders + innerBorders let containerWidth = this.getAttribute('width') || `${parseFloat(parentWidth) / nonRawSiblings}px` const { unit, parsedWidth } = widthParser(containerWidth, { parseFloatToInt: false, }) if (unit === '%') { containerWidth = `${ (parseFloat(parentWidth) * parsedWidth) / 100 - allPaddings }px` } else { containerWidth = `${parsedWidth - allPaddings}px` } return { ...this.context, containerWidth, } } getStyles() { const hasBorderRadius = this.hasBorderRadius() const hasInnerBorderRadius = this.hasInnerBorderRadius() const tableStyle = { 'background-color': this.getAttribute('background-color'), border: this.getAttribute('border'), 'border-bottom': this.getAttribute('border-bottom'), 'border-left': this.getAttribute('border-left'), 'border-radius': this.getAttribute('border-radius'), 'border-right': this.getAttribute('border-right'), 'border-top': this.getAttribute('border-top'), 'vertical-align': this.getAttribute('vertical-align'), ...(hasBorderRadius && { 'border-collapse': 'separate' }), } return { div: { 'font-size': '0px', 'text-align': 'left', direction: this.getAttribute('direction'), display: 'inline-block', 'vertical-align': this.getAttribute('vertical-align'), width: this.getMobileWidth(), }, table: { ...(this.hasGutter() ? { 'background-color': this.getAttribute('inner-background-color'), border: this.getAttribute('inner-border'), 'border-bottom': this.getAttribute('inner-border-bottom'), 'border-left': this.getAttribute('inner-border-left'), 'border-radius': this.getAttribute('inner-border-radius'), 'border-right': this.getAttribute('inner-border-right'), 'border-top': this.getAttribute('inner-border-top'), } : tableStyle), ...(hasInnerBorderRadius && { 'border-collapse': 'separate' }), }, tdOutlook: { 'vertical-align': this.getAttribute('vertical-align'), width: this.getWidthAsPixel(), }, gutter: { ...tableStyle, padding: this.getAttribute('padding'), 'padding-top': this.getAttribute('padding-top'), 'padding-right': this.getAttribute('padding-right'), 'padding-bottom': this.getAttribute('padding-bottom'), 'padding-left': this.getAttribute('padding-left'), }, } } getMobileWidth() { const { containerWidth } = this.context const { nonRawSiblings } = this.props const width = this.getAttribute('width') const mobileWidth = this.getAttribute('mobileWidth') if (mobileWidth !== 'mobileWidth') { return '100%' } if (width === undefined) { return `${parseInt(100 / nonRawSiblings, 10)}%` } const { unit, parsedWidth } = widthParser(width, { parseFloatToInt: false, }) switch (unit) { case '%': return width case 'px': default: return `${(parsedWidth / parseInt(containerWidth, 10)) * 100}%` } } getWidthAsPixel() { const { containerWidth } = this.context const { unit, parsedWidth } = widthParser(this.getParsedWidth(true), { parseFloatToInt: false, }) if (unit === '%') { return `${(parseFloat(containerWidth) * parsedWidth) / 100}px` } return `${parsedWidth}px` } getParsedWidth(toString) { const { nonRawSiblings } = this.props const width = this.getAttribute('width') || `${100 / nonRawSiblings}%` const { unit, parsedWidth } = widthParser(width, { parseFloatToInt: false, }) if (toString) { return `${parsedWidth}${unit}` } return { unit, parsedWidth, } } getColumnClass() { const { addMediaQuery } = this.context let className = '' const { parsedWidth, unit } = this.getParsedWidth() const formattedClassNb = parsedWidth.toString().replace('.', '-') switch (unit) { case '%': className = `mj-column-per-${formattedClassNb}` break case 'px': default: className = `mj-column-px-${formattedClassNb}` break } // Add className to media queries addMediaQuery(className, { parsedWidth, unit, }) return className } hasBorderRadius() { const borderRadius = this.getAttribute('border-radius') return borderRadius !== '' && typeof borderRadius !== 'undefined' } hasInnerBorderRadius() { const innerBorderRadius = this.getAttribute('inner-border-radius') return innerBorderRadius !== '' && typeof innerBorderRadius !== 'undefined' } hasGutter() { return [ 'padding', 'padding-bottom', 'padding-left', 'padding-right', 'padding-top', ].some((attr) => this.getAttribute(attr) != null) } renderGutter() { const hasBorderRadius = this.hasBorderRadius() return `
${this.renderColumn()}
` } renderColumn() { const { children } = this.props return ` ${this.renderChildren(children, { renderer: (component) => component.constructor.isRawElement() ? component.render() : ` `, })}
${component.render()}
` } render() { let classesName = `${this.getColumnClass()} mj-outlook-group-fix` if (this.getAttribute('css-class')) { classesName += ` ${this.getAttribute('css-class')}` } return `
${this.hasGutter() ? this.renderGutter() : this.renderColumn()}
` } } ================================================ FILE: packages/mjml-core/README.md ================================================ ## mjml-core ### Installation ```bash npm install --save mjml-core ``` This is the core mjml library, composed by a set of functions for both parsing, and rendering mjml ### Usage ```javascript import mjml2html from 'mjml' console.log(mjml2html(`code`)) ``` ================================================ FILE: packages/mjml-core/package.json ================================================ { "name": "mjml-core", "description": "mjml-core", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-core" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward", "test": "node ./tests/index.js" }, "dependencies": { "@babel/runtime": "^7.28.4", "cheerio": "1.0.0-rc.12", "detect-node": "^2.0.4", "html-minifier": "^4.0.0", "js-beautify": "^1.6.14", "juice": "^10.0.0", "lodash": "^4.17.21", "mjml-migrate": "4.18.0", "mjml-parser-xml": "4.18.0", "mjml-validator": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "chai": "^4.1.1", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-core/src/components.js ================================================ import { kebabCase } from 'lodash' import { registerDependencies } from 'mjml-validator' const components = {} export function assignComponents(target, source) { for (const component of source) { target[component.componentName || kebabCase(component.name)] = component } } export function registerComponent(Component, options = {}) { assignComponents(components, [Component]) if (Component.dependencies && options.registerDependencies) { registerDependencies(Component.dependencies) } } export default components ================================================ FILE: packages/mjml-core/src/createComponent.js ================================================ // eslint-disable-next-line max-classes-per-file import { get, forEach, identity, reduce, kebabCase, find, filter, isNil, omitBy, } from 'lodash' import MJMLParser from 'mjml-parser-xml' import shorthandParser, { borderParser } from './helpers/shorthandParser' import formatAttributes from './helpers/formatAttributes' import jsonToXML from './helpers/jsonToXML' export function initComponent({ initialDatas, name }) { const Component = initialDatas.context.components[name] if (Component) { const component = new Component(initialDatas) if (component.headStyle) { component.context.addHeadStyle(name, component.headStyle) } if (component.componentHeadStyle) { component.context.addComponentHeadSyle(component.componentHeadStyle) } return component } return null } class Component { static getTagName() { return this.componentName || kebabCase(this.name) } static isRawElement() { return !!this.rawElement } static defaultAttributes = {} constructor(initialDatas = {}) { const { attributes = {}, children = [], content = '', context = {}, props = {}, globalAttributes = {}, absoluteFilePath = null, rawAttrs = {}, } = initialDatas this.props = { absoluteFilePath, ...props, children, content, rawAttrs, } this.attributes = formatAttributes( { ...this.constructor.defaultAttributes, ...globalAttributes, ...attributes, }, this.constructor.allowedAttributes, ) this.context = context return this } getChildContext() { return this.context } getAttribute(name) { return this.attributes[name] } getContent() { return this.props.content.trim() } renderMJML(mjml, options = {}) { if (typeof mjml === 'string') { // supports returning siblings elements from a custom component const partialMjml = MJMLParser(`${mjml}`, { ...options, components: this.context.components, ignoreIncludes: true, }) return partialMjml.children .map((child) => this.context.processing(child, this.context)) .join('') } return this.context.processing(mjml, this.context) } } export class BodyComponent extends Component { // eslint-disable-next-line class-methods-use-this getStyles() { return {} } getShorthandAttrValue(attribute, direction) { const mjAttributeDirection = this.getAttribute(`${attribute}-${direction}`) const mjAttribute = this.getAttribute(attribute) if (mjAttributeDirection) { return parseInt(mjAttributeDirection, 10) } if (!mjAttribute) { return 0 } return shorthandParser(mjAttribute, direction) } getShorthandBorderValue(direction, attribute = 'border') { const borderDirection = direction && this.getAttribute(`${attribute}-${direction}`) const border = this.getAttribute(attribute) return borderParser(borderDirection || border || '0') } getBoxWidths() { const { containerWidth } = this.context const parsedWidth = parseInt(containerWidth, 10) const paddings = this.getShorthandAttrValue('padding', 'right') + this.getShorthandAttrValue('padding', 'left') const borders = this.getShorthandBorderValue('right') + this.getShorthandBorderValue('left') return { totalWidth: parsedWidth, borders, paddings, box: parsedWidth - paddings - borders, } } htmlAttributes(attributes) { const specialAttributes = { style: (v) => this.styles(v), default: identity, } return reduce( omitBy(attributes, isNil), (output, v, name) => { const value = (specialAttributes[name] || specialAttributes.default)(v) return `${output} ${name}="${value}"` }, '', ) } styles(styles) { let stylesObject if (styles) { if (typeof styles === 'string') { stylesObject = get(this.getStyles(), styles) } else { stylesObject = styles } } return reduce( stylesObject, (output, value, name) => { if (!isNil(value)) { return `${output}${name}:${value};` } return output }, '', ) } renderChildren(children, options = {}) { const { props = {}, renderer = (component) => component.render(), attributes = {}, rawXML = false, } = options children = children || this.props.children if (rawXML) { return children .map((child) => { child.attributes = { ...attributes, ...child.attributes } return jsonToXML(child) }) .join('\n') } const sibling = children.length const rawComponents = filter(this.context.components, (c) => c.isRawElement(), ) const nonRawSiblings = children.filter( (child) => !find(rawComponents, (c) => c.getTagName() === child.tagName), ).length let output = '' let index = 0 forEach(children, (children) => { const component = initComponent({ name: children.tagName, initialDatas: { ...children, attributes: { ...attributes, ...children.attributes, }, context: this.getChildContext(), props: { ...props, first: index === 0, index, last: index + 1 === sibling, sibling, nonRawSiblings, }, }, }) if (component !== null) { output += renderer(component) } index++ // eslint-disable-line no-plusplus }) return output } } export class HeadComponent extends Component { static getTagName() { return this.componentName || kebabCase(this.name) } handlerChildren() { const { children } = this.props return children.map((children) => { const component = initComponent({ name: children.tagName, initialDatas: { ...children, context: this.getChildContext(), }, }) if (!component) { // eslint-disable-next-line no-console console.error(`No matching component for tag : ${children.tagName}`) return null } if (component.handler) { component.handler() } if (component.render) { return component.render() } return null }) } } ================================================ FILE: packages/mjml-core/src/helpers/conditionalTag.js ================================================ export const startConditionalTag = '' export const startNegationConditionalTag = '' export const startMsoNegationConditionalTag = '' export const endNegationConditionalTag = '' export default function conditionalTag(content, negation = false) { return ` ${negation ? startNegationConditionalTag : startConditionalTag} ${content} ${negation ? endNegationConditionalTag : endConditionalTag} ` } export function msoConditionalTag(content, negation = false) { return ` ${negation ? startMsoNegationConditionalTag : startMsoConditionalTag} ${content} ${negation ? endNegationConditionalTag : endConditionalTag} ` } ================================================ FILE: packages/mjml-core/src/helpers/fonts.js ================================================ import { forEach, map } from 'lodash' // eslint-disable-next-line import/prefer-default-export export function buildFontsTags(content, inlineStyle, fonts = {}) { const toImport = [] forEach(fonts, (url, name) => { const regex = new RegExp(`"[^"]*font-family:[^"]*${name}[^"]*"`, 'gmi') const inlineRegex = new RegExp(`font-family:[^;}]*${name}`, 'gmi') if (content.match(regex) || inlineStyle.some((s) => s.match(inlineRegex))) { toImport.push(url) } }) if (toImport.length > 0) { return ` ${map( toImport, (url) => ``, ).join('\n')} \n ` } return '' } ================================================ FILE: packages/mjml-core/src/helpers/formatAttributes.js ================================================ import { reduce } from 'lodash' import { initializeType } from '../types/type' export default (attributes, allowedAttributes) => reduce( attributes, (acc, val, attrName) => { if (allowedAttributes && allowedAttributes[attrName]) { const TypeConstructor = initializeType(allowedAttributes[attrName]) if (TypeConstructor) { const type = new TypeConstructor(val) return { ...acc, [attrName]: type.getValue(), } } } return { ...acc, [attrName]: val, } }, {}, ) ================================================ FILE: packages/mjml-core/src/helpers/genRandomHexString.js ================================================ export default function genRandomHexString(length) { let str = '' for (let i = 0; i < length; i += 1) { str += Math.floor(Math.random() * 16).toString(16) } return str } ================================================ FILE: packages/mjml-core/src/helpers/jsonToXML.js ================================================ const jsonToXML = ({ tagName, attributes, children, content }) => { const subNode = children && children.length > 0 ? children.map(jsonToXML).join('\n') : content || '' const stringAttrs = Object.keys(attributes) .map((attr) => `${attr}="${attributes[attr]}"`) .join(' ') return `<${tagName}${ stringAttrs === '' ? '>' : ` ${stringAttrs}>` }${subNode}` } export default jsonToXML ================================================ FILE: packages/mjml-core/src/helpers/makeLowerBreakpoint.js ================================================ export default function makeLowerBreakpoint(breakpoint) { try { const pixels = Number.parseInt(breakpoint.match('[0-9]+')[0], 10) return `${pixels - 1}px` } catch (e) { return breakpoint } } ================================================ FILE: packages/mjml-core/src/helpers/mediaQueries.js ================================================ import { map, isEmpty } from 'lodash' // eslint-disable-next-line import/prefer-default-export export default function buildMediaQueriesTags( breakpoint, mediaQueries = {}, options = {}, ) { if (isEmpty(mediaQueries)) { return '' } const { forceOWADesktop = false, printerSupport = false } = options const baseMediaQueries = map( mediaQueries, (mediaQuery, className) => `.${className} ${mediaQuery}`, ) const thunderbirdMediaQueries = map( mediaQueries, (mediaQuery, className) => `.moz-text-html .${className} ${mediaQuery}`, ) const owaQueries = map(baseMediaQueries, (mq) => `[owa] ${mq}`) return ` ${ printerSupport ? `` : `` } ${ forceOWADesktop ? `` : `` } ` } ================================================ FILE: packages/mjml-core/src/helpers/mergeOutlookConditionnals.js ================================================ // # OPTIMIZE ME: — check if previous conditionnal is `\s*?)/gm, (match, prefix, content, suffix) => { // find spaces between tags const processedContent = content .replace( /(^|>)(\s+)(<|$)/gm, (match, prefix, content, suffix) => `${prefix}${suffix}`, ) .replace(/\s{2,}/gm, ' ') return `${prefix}${processedContent}${suffix}` }, ) ================================================ FILE: packages/mjml-core/src/helpers/mjmlconfig.js ================================================ import path from 'path' import fs from 'fs' import { registerDependencies } from 'mjml-validator' import { registerComponent } from '../components' export function readMjmlConfig(configPathOrDir = process.cwd()) { let componentRootPath = process.cwd() let mjmlConfigPath = configPathOrDir try { mjmlConfigPath = path .basename(configPathOrDir) .match(/^\.mjmlconfig(\.js)?$/) ? path.resolve(configPathOrDir) : path.resolve(configPathOrDir, '.mjmlconfig') componentRootPath = path.dirname(mjmlConfigPath) const fullPath = path.resolve(mjmlConfigPath) let mjmlConfig if (path.extname(mjmlConfigPath) === '.js') { delete require.cache[fullPath] mjmlConfig = require(fullPath) // eslint-disable-line global-require, import/no-dynamic-require } else { mjmlConfig = JSON.parse(fs.readFileSync(fullPath, 'utf8')) } return { mjmlConfig, componentRootPath } } catch (e) { if (e.code !== 'ENOENT') { console.error('Error reading mjmlconfig : ', e) // eslint-disable-line no-console } return { mjmlConfig: { packages: [], options: {} }, mjmlConfigPath, componentRootPath, error: e, } } } export function resolveComponentPath(compPath, componentRootPath) { if (!compPath) { return null } if (!compPath.startsWith('.') && !path.isAbsolute(compPath)) { try { return require.resolve(compPath) } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') { console.error('Error resolving custom component path : ', e) // eslint-disable-line no-console return null } // we got a 'MODULE_NOT_FOUND' error try { // try again as relative path to node_modules: (this may be necessary if mjml is installed globally or by npm link) return resolveComponentPath( `./node_modules/${compPath}`, componentRootPath, ) } catch (e) { // try again as a plain local path: return resolveComponentPath(`./${compPath}`, componentRootPath) } } } return require.resolve(path.resolve(componentRootPath, compPath)) } export function registerCustomComponent( comp, registerCompFn = registerComponent, ) { if (comp instanceof Function) { registerCompFn(comp) } else { const compNames = Object.keys(comp) // this approach handles both an array and an object (like the mjml-accordion default export) compNames.forEach((compName) => { registerCustomComponent(comp[compName], registerCompFn) }) } } export function handleMjmlConfigComponents( packages, componentRootPath, registerCompFn, ) { const result = { success: [], failures: [], } packages.forEach((compPath) => { let resolvedPath = compPath try { resolvedPath = resolveComponentPath(compPath, componentRootPath) if (resolvedPath) { const requiredComp = require(resolvedPath) // eslint-disable-line global-require, import/no-dynamic-require registerCustomComponent( requiredComp.default || requiredComp, registerCompFn, ) registerDependencies( (requiredComp.default || requiredComp).dependencies || {}, ) result.success.push(compPath) } } catch (e) { result.failures.push({ error: e, compPath }) if (e.code === 'ENOENT' || e.code === 'MODULE_NOT_FOUND') { console.error('Missing or unreadable custom component : ', resolvedPath) // eslint-disable-line no-console } else { // eslint-disable-next-line no-console console.error( 'Error when registering custom component : ', resolvedPath, e, ) } } }) return result } export default function handleMjmlConfig( configPathOrDir = process.cwd(), registerCompFn = registerComponent, ) { const { mjmlConfig: { packages }, componentRootPath, error, } = readMjmlConfig(configPathOrDir) if (error) return { error } return handleMjmlConfigComponents(packages, componentRootPath, registerCompFn) } ================================================ FILE: packages/mjml-core/src/helpers/preview.js ================================================ export default function (content) { if (content === '') { return '' } return `
${content}
` } ================================================ FILE: packages/mjml-core/src/helpers/shorthandParser.js ================================================ import { get } from 'lodash' export default function (cssValue, direction) { const splittedCssValue = cssValue.trim().replace(/\s+/g, ' ').split(' ', 4) let directions = {} switch (splittedCssValue.length) { case 2: directions = { top: 0, bottom: 0, left: 1, right: 1 } break case 3: directions = { top: 0, left: 1, right: 1, bottom: 2 } break case 4: directions = { top: 0, right: 1, bottom: 2, left: 3 } break case 1: default: return parseInt(cssValue, 10) } return parseInt(splittedCssValue[directions[direction]] || 0, 10) } export function borderParser(border) { return parseInt(get(border.match(/(?:(?:^| )(\d+))/), 1), 10) || 0 } ================================================ FILE: packages/mjml-core/src/helpers/skeleton.js ================================================ import { negate, isNil } from 'lodash' import buildPreview from './preview' import { buildFontsTags } from './fonts' import buildMediaQueriesTags from './mediaQueries' import { buildStyleFromComponents, buildStyleFromTags } from './styles' export default function skeleton(options) { const { backgroundColor = '', beforeDoctype = '', breakpoint = '480px', content = '', fonts = {}, mediaQueries = {}, headStyle = {}, componentsHeadStyle = [], headRaw = [], preview, title = '', style = [], forceOWADesktop, printerSupport, inlineStyle, lang, dir, } = options return `${beforeDoctype ? `${beforeDoctype}\n` : ''} ${title} ${buildFontsTags(content, inlineStyle, fonts)} ${buildMediaQueriesTags(breakpoint, mediaQueries, { forceOWADesktop, printerSupport, })} ${buildStyleFromComponents(breakpoint, componentsHeadStyle, headStyle)} ${buildStyleFromTags(breakpoint, style)} ${headRaw.filter(negate(isNil)).join('\n')} ${buildPreview(preview)} ${content} ` } ================================================ FILE: packages/mjml-core/src/helpers/styles.js ================================================ import { isFunction } from 'lodash' export function buildStyleFromComponents( breakpoint, componentsHeadStyles, headStylesObject, ) { const headStyles = Object.values(headStylesObject) if (componentsHeadStyles.length === 0 && headStyles.length === 0) { return '' } return ` ` } export function buildStyleFromTags(breakpoint, styles) { if (styles.length === 0) { return '' } return ` ` } ================================================ FILE: packages/mjml-core/src/helpers/suffixCssClasses.js ================================================ export default (classes, suffix) => classes ? classes .split(' ') .map((c) => `${c}-${suffix}`) .join(' ') : '' ================================================ FILE: packages/mjml-core/src/helpers/widthParser.js ================================================ const unitRegex = /[\d.,]*(\D*)$/ export default function widthParser(width, options = {}) { const { parseFloatToInt = true } = options const widthUnit = unitRegex.exec(width.toString())[1] const unitParsers = { default: parseInt, px: parseInt, '%': parseFloatToInt ? parseInt : parseFloat, } const parser = unitParsers[widthUnit] || unitParsers.default return { parsedWidth: parser(width), unit: widthUnit || 'px', } } ================================================ FILE: packages/mjml-core/src/index.js ================================================ import { find, filter, get, identity, map, omit, reduce, isObject, each, isEmpty, } from 'lodash' import path from 'path' import juice from 'juice' import { html as htmlBeautify } from 'js-beautify' import { minify as htmlMinify } from 'html-minifier' import { load } from 'cheerio' import MJMLParser from 'mjml-parser-xml' import MJMLValidator, { dependencies as globalDependencies, assignDependencies, } from 'mjml-validator' import { handleMjml3 } from 'mjml-migrate' import { initComponent } from './createComponent' import globalComponents, { registerComponent, assignComponents, } from './components' import makeLowerBreakpoint from './helpers/makeLowerBreakpoint' import suffixCssClasses from './helpers/suffixCssClasses' import mergeOutlookConditionnals from './helpers/mergeOutlookConditionnals' import minifyOutlookConditionnals from './helpers/minifyOutlookConditionnals' import defaultSkeleton from './helpers/skeleton' import { initializeType } from './types/type' import handleMjmlConfig, { readMjmlConfig, handleMjmlConfigComponents, } from './helpers/mjmlconfig' const isNode = require('detect-node') class ValidationError extends Error { constructor(message, errors) { super(message) this.errors = errors } } export default function mjml2html(mjml, options = {}) { let content = '' let errors = [] if (isNode && typeof options.skeleton === 'string') { /* eslint-disable global-require */ /* eslint-disable import/no-dynamic-require */ options.skeleton = require( options.skeleton.charAt(0) === '.' ? path.resolve(process.cwd(), options.skeleton) : options.skeleton, ) /* eslint-enable global-require */ /* eslint-enable import/no-dynamic-require */ } let packages = {} let confOptions = {} let mjmlConfigOptions = {} let confPreprocessors = [] let error = null let componentRootPath = null if ((isNode && options.useMjmlConfigOptions) || options.mjmlConfigPath) { const mjmlConfigContent = readMjmlConfig(options.mjmlConfigPath) ;({ mjmlConfig: { packages, options: confOptions, preprocessors: confPreprocessors, }, componentRootPath, error, } = mjmlConfigContent) if (options.useMjmlConfigOptions) { mjmlConfigOptions = confOptions } } // if mjmlConfigPath is specified then we need to register components it on each call if (isNode && !error && options.mjmlConfigPath) { handleMjmlConfigComponents(packages, componentRootPath, registerComponent) } const { beautify = false, fonts = { 'Open Sans': 'https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700', 'Droid Sans': 'https://fonts.googleapis.com/css?family=Droid+Sans:300,400,500,700', Lato: 'https://fonts.googleapis.com/css?family=Lato:300,400,500,700', Roboto: 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700', Ubuntu: 'https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700', }, keepComments, minify = false, minifyOptions = {}, ignoreIncludes = false, juiceOptions = {}, juicePreserveTags = null, skeleton = defaultSkeleton, validationLevel = 'soft', filePath = '.', actualPath = '.', noMigrateWarn = false, preprocessors, presets = [], printerSupport = false, } = { ...mjmlConfigOptions, ...options, preprocessors: options.preprocessors ? [...confPreprocessors, ...options.preprocessors] : confPreprocessors, } const components = { ...globalComponents } const dependencies = assignDependencies({}, globalDependencies) for (const preset of presets) { assignComponents(components, preset.components) assignDependencies(dependencies, preset.dependencies) } if (typeof mjml === 'string') { mjml = MJMLParser(mjml, { keepComments, components, filePath, actualPath, preprocessors, ignoreIncludes, }) } mjml = handleMjml3(mjml, { noMigrateWarn }) const globalData = { backgroundColor: '', beforeDoctype: '', breakpoint: '480px', classes: {}, classesDefault: {}, defaultAttributes: {}, htmlAttributes: {}, fonts, inlineStyle: [], headStyle: {}, componentsHeadStyle: [], headRaw: [], mediaQueries: {}, preview: '', style: [], title: '', forceOWADesktop: get(mjml, 'attributes.owa', 'mobile') === 'desktop', lang: get(mjml, 'attributes.lang') || 'und', dir: get(mjml, 'attributes.dir') || 'auto', } const validatorOptions = { components, dependencies, initializeType, } switch (validationLevel) { case 'skip': break case 'strict': errors = MJMLValidator(mjml, validatorOptions) if (errors.length > 0) { throw new ValidationError( `ValidationError: \n ${errors .map((e) => e.formattedMessage) .join('\n')}`, errors, ) } break case 'soft': default: errors = MJMLValidator(mjml, validatorOptions) break } const mjBody = find(mjml.children, { tagName: 'mj-body' }) const mjHead = find(mjml.children, { tagName: 'mj-head' }) const mjOutsideRaws = filter(mjml.children, { tagName: 'mj-raw' }) const processing = (node, context, parseMJML = identity) => { if (!node) { return } const component = initComponent({ name: node.tagName, initialDatas: { ...parseMJML(node), context, }, }) if (component !== null) { if ('handler' in component) { return component.handler() // eslint-disable-line consistent-return } if ('render' in component) { return component.render() // eslint-disable-line consistent-return } } } const applyAttributes = (mjml) => { const parse = (mjml, parentMjClass = '') => { const { attributes, tagName, children } = mjml const classes = get(mjml.attributes, 'mj-class', '').split(' ') const attributesClasses = reduce( classes, (acc, value) => { const mjClassValues = globalData.classes[value] let multipleClasses = {} if (acc['css-class'] && get(mjClassValues, 'css-class')) { multipleClasses = { 'css-class': `${acc['css-class']} ${mjClassValues['css-class']}`, } } return { ...acc, ...mjClassValues, ...multipleClasses, } }, {}, ) const defaultAttributesForClasses = reduce( parentMjClass.split(' '), (acc, value) => ({ ...acc, ...get(globalData.classesDefault, `${value}.${tagName}`), }), {}, ) const nextParentMjClass = get(attributes, 'mj-class', parentMjClass) return { ...mjml, attributes: { ...globalData.defaultAttributes[tagName], ...attributesClasses, ...defaultAttributesForClasses, ...omit(attributes, ['mj-class']), }, rawAttrs: { ...omit(attributes, ['mj-class']) }, globalAttributes: { ...globalData.defaultAttributes['mj-all'], }, children: map(children, (mjml) => parse(mjml, nextParentMjClass)), } } return parse(mjml) } const bodyHelpers = { components, globalData, addMediaQuery(className, { parsedWidth, unit }) { globalData.mediaQueries[className] = `{ width:${parsedWidth}${unit} !important; max-width: ${parsedWidth}${unit}; }` }, addHeadStyle(identifier, headStyle) { globalData.headStyle[identifier] = headStyle }, addComponentHeadSyle(headStyle) { globalData.componentsHeadStyle.push(headStyle) }, setBackgroundColor: (color) => { globalData.backgroundColor = color }, processing: (node, context) => processing(node, context, applyAttributes), } const headHelpers = { components, globalData, add(attr, ...params) { if (Array.isArray(globalData[attr])) { globalData[attr].push(...params) } else if (Object.prototype.hasOwnProperty.call(globalData, attr)) { if (params.length > 1) { if (isObject(globalData[attr][params[0]])) { globalData[attr][params[0]] = { ...globalData[attr][params[0]], ...params[1], } } else { // eslint-disable-next-line prefer-destructuring globalData[attr][params[0]] = params[1] } } else { // eslint-disable-next-line prefer-destructuring globalData[attr] = params[0] } } else { throw Error( `An mj-head element add an unkown head attribute : ${attr} with params ${ Array.isArray(params) ? params.join('') : params }`, ) } }, } globalData.headRaw = processing(mjHead, headHelpers) content = processing(mjBody, bodyHelpers, applyAttributes) if (!content) { throw new Error( 'Malformed MJML. Check that your structure is correct and enclosed in tags.', ) } content = minifyOutlookConditionnals(content) if (mjOutsideRaws.length) { const toAddBeforeDoctype = mjOutsideRaws.filter( (elt) => elt.attributes.position && elt.attributes.position === 'file-start', ) if (toAddBeforeDoctype.length) { globalData.beforeDoctype = toAddBeforeDoctype .map((elt) => elt.content) .join('\n') } } if (!isEmpty(globalData.htmlAttributes)) { const $ = load(content, { xmlMode: true, // otherwise it may move contents that aren't in any tag decodeEntities: false, // won't escape special characters }) each(globalData.htmlAttributes, (data, selector) => { each(data, (value, attrName) => { $(selector).each(function getAttr() { $(this).attr(attrName, value || '') }) }) }) content = $.root().html() } content = skeleton({ content, ...globalData, printerSupport, }) if (globalData.inlineStyle.length > 0) { if (juicePreserveTags) { each(juicePreserveTags, (val, key) => { juice.codeBlocks[key] = val }) } content = juice(content, { applyStyleTags: false, extraCss: globalData.inlineStyle.join(''), insertPreservedExtraCss: false, removeStyleTags: false, ...juiceOptions, }) } content = mergeOutlookConditionnals(content) if (beautify) { // eslint-disable-next-line no-console console.warn( '"beautify" option is deprecated in mjml-core and only available in mjml cli.', ) content = htmlBeautify(content, { indent_size: 2, wrap_attributes_indent_size: 2, max_preserve_newline: 0, preserve_newlines: false, }) } if (minify) { // eslint-disable-next-line no-console console.warn( '"minify" option is deprecated in mjml-core and only available in mjml cli.', ) content = htmlMinify(content, { collapseWhitespace: true, minifyCSS: false, caseSensitive: true, removeEmptyAttributes: true, ...minifyOptions, }) } return { html: content, json: mjml, errors, } } if (isNode) { handleMjmlConfig(process.cwd(), registerComponent) } export { globalComponents as components, initComponent, registerComponent, assignComponents, makeLowerBreakpoint, suffixCssClasses, handleMjmlConfig, initializeType, } export { BodyComponent, HeadComponent } from './createComponent' ================================================ FILE: packages/mjml-core/src/types/boolean.js ================================================ import Type from './type' export const matcher = /^boolean/gim export default () => class Boolean extends Type { constructor(boolean) { super(boolean) this.matchers = [/^true$/i, /^false$/i] } isValid() { return this.value === true || this.value === false } } ================================================ FILE: packages/mjml-core/src/types/color.js ================================================ import Type from './type' import colors from './helpers/colors' export const matcher = /^color/gim const shorthandRegex = /^#\w{3}$/ const replaceInputRegex = /^#(\w)(\w)(\w)$/ const replaceOutput = '#$1$1$2$2$3$3' export default () => class Color extends Type { constructor(color) { super(color) this.matchers = [ /rgba\(\d{1,3},\s?\d{1,3},\s?\d{1,3},\s?\d(\.\d{1,3})?\)/gi, /rgb\(\d{1,3},\s?\d{1,3},\s?\d{1,3}\)/gi, /^#([0-9a-f]{3}){1,2}$/gi, new RegExp(`^(${colors.join('|')})$`), ] } getValue() { if (typeof this.value === 'string' && this.value.match(shorthandRegex)) { return this.value.replace(replaceInputRegex, replaceOutput) } return this.value } } ================================================ FILE: packages/mjml-core/src/types/enum.js ================================================ import { escapeRegExp } from 'lodash' import Type from './type' export const matcher = /^enum/gim export default (params) => { const matchers = params.match(/\(([^)]+)\)/)[1].split(',') return class Enum extends Type { static errorMessage = `has invalid value: $value for type Enum, only accepts ${matchers.join( ', ', )}` constructor(value) { super(value) this.matchers = matchers.map((m) => new RegExp(`^${escapeRegExp(m)}$`)) } } } ================================================ FILE: packages/mjml-core/src/types/helpers/colors.js ================================================ export default [ 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'inherit', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'transparent', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen', ] ================================================ FILE: packages/mjml-core/src/types/index.js ================================================ import NBoolean, { matcher as booleanMatcher } from './boolean' import Color, { matcher as colorMatcher } from './color' import Enum, { matcher as enumMatcher } from './enum' import Unit, { matcher as unitMatcher } from './unit' import NString, { matcher as stringMatcher } from './string' import NInteger, { matcher as intMatcher } from './integer' export default { boolean: { matcher: booleanMatcher, typeConstructor: NBoolean }, enum: { matcher: enumMatcher, typeConstructor: Enum }, color: { matcher: colorMatcher, typeConstructor: Color }, unit: { matcher: unitMatcher, typeConstructor: Unit }, string: { matcher: stringMatcher, typeConstructor: NString }, integer: { matcher: intMatcher, typeConstructor: NInteger }, } ================================================ FILE: packages/mjml-core/src/types/integer.js ================================================ import Type from './type' export const matcher = /^integer/gim export default () => class NInteger extends Type { constructor(value) { super(value) this.matchers = [/\d+/] } } ================================================ FILE: packages/mjml-core/src/types/string.js ================================================ import Type from './type' export const matcher = /^string/gim export default () => class NString extends Type { constructor(value) { super(value) this.matchers = [/.*/] } } ================================================ FILE: packages/mjml-core/src/types/type.js ================================================ import { some, find } from 'lodash' import typesConstructors from './index' // Avoid recreate existing types export const types = {} export const initializeType = (typeConfig) => { if (types[typeConfig]) { return types[typeConfig] } const { typeConstructor } = find(typesConstructors, (type) => !!typeConfig.match(type.matcher)) || {} if (!typeConstructor) { throw new Error(`No type found for ${typeConfig}`) } types[typeConfig] = typeConstructor(typeConfig) return types[typeConfig] } export default class Type { constructor(value) { this.value = value } isValid() { return some(this.matchers, (matcher) => `${this.value}`.match(matcher)) } getErrorMessage() { if (this.isValid()) { return } const errorMessage = this.constructor.errorMessage || `has invalid value: ${this.value} for type ${this.constructor.name} ` return errorMessage.replace(/\$value/g, this.value) } static check(type) { return !!type.match(this.constructor.typeChecker) } getValue() { return this.value } } ================================================ FILE: packages/mjml-core/src/types/unit.js ================================================ import { escapeRegExp } from 'lodash' import Type from './type' export const matcher = /^(unit|unitWithNegative)\(.*\)/gim export default (params) => { const allowNeg = params.match(/^unitWithNegative/) ? '-|' : '' const units = params.match(/\(([^)]+)\)/)[1].split(',') const argsMatch = params.match(/\{([^}]+)\}/) const args = (argsMatch && argsMatch[1] && argsMatch[1].split(',')) || ['1'] // defaults to 1 const allowAuto = units.includes('auto') ? '|auto' : '' const filteredUnits = units.filter((u) => u !== 'auto') return class Unit extends Type { static errorMessage = `has invalid value: $value for type Unit, only accepts (${units.join( ', ', )}) units and ${args.join(' to ')} value(s)` constructor(value) { super(value) this.matchers = [ new RegExp( `^(((${allowNeg}\\d|,|\\.){1,}(${filteredUnits .map(escapeRegExp) .join('|')})|0${allowAuto})( )?){${args.join(',')}}$`, ), ] } } } ================================================ FILE: packages/mjml-core/tests/.eslintrc ================================================ { "extends": "../../../.eslintrc", "rules": { "import/no-extraneous-dependencies": ["error", {"devDependencies": true}] } } ================================================ FILE: packages/mjml-core/tests/index.js ================================================ require('./jsonToXml-test') require('./mergeOutlookConditionnals-test') require('./minifyOutlookConditionnals-test') require('./shorthandParser-test') require('./skeleton-test') require('./widthParser-test') ================================================ FILE: packages/mjml-core/tests/jsonToXml-test.js ================================================ const chai = require('chai') const jsonToXml = require('../lib/helpers/jsonToXML') const json = { line: 1, includedIn: [], tagName: 'mjml', children: [ { line: 2, includedIn: [], tagName: 'mj-body', children: [ { line: 3, includedIn: [], tagName: 'mj-section', children: [ { line: 4, includedIn: [], tagName: 'mj-column', children: [ { line: 5, includedIn: [], tagName: 'mj-text', attributes: { 'font-size': '20px', color: '#F45E43', 'font-family': 'helvetica' }, content: 'Hello World' } ], attributes: {} } ], attributes: {} } ], attributes: {} } ], attributes: {} } const xml = jsonToXml(json) const validXml = 'Hello World' chai.expect(xml, 'jsonToXML test failed') .to.equal(validXml) ================================================ FILE: packages/mjml-core/tests/mergeOutlookConditionnals-test.js ================================================ const chai = require('chai') const mergeOutlookConditionnals = require('../lib/helpers/mergeOutlookConditionnals') const testValues = [ { input: ' \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Hello World\n
\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n`, output: `\n \n \n \n \n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Hello World\n
\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n`, }, ] testValues.forEach(testUnit => { const { input, output } = testUnit chai.expect(minifyOutlookConditionnals(input), `minifyOutlookConditionnals test failed`) .to.deep.equal(output) }) ================================================ FILE: packages/mjml-core/tests/shorthandParser-test.js ================================================ const chai = require('chai') const helper = require('../lib/helpers/shorthandParser') const shorthandParser = helper && helper.default const testValues = [ { input: '1px', output: { top: 1, right: 1, bottom: 1, left: 1 }, }, { input: '1px 0', output: { top: 1, right: 0, bottom: 1, left: 0 }, }, { input: '1px 2px 3px', output: { top: 1, right: 2, bottom: 3, left: 2 }, }, { input: '1px 2px 3px 4px', output: { top: 1, right: 2, bottom: 3, left: 4 }, }, { input: ' 1px 2px 3px 4px ', output: { top: 1, right: 2, bottom: 3, left: 4 }, }, ] testValues.forEach(testUnit => { const { input, output } = testUnit const directions = ['top', 'right', 'bottom', 'left'] directions.forEach(dir => { chai.expect(shorthandParser(input, dir), `shorthandParser test failed`) .to.deep.equal(output[dir]) }) }) ================================================ FILE: packages/mjml-core/tests/skeleton-test.js ================================================ const chai = require('chai') const { load } = require('cheerio') const skeleton = require('../lib/helpers/skeleton') // The conditional style tag for Outlook does not get parsed by cheerio, // so each outputStyleCount excludes it const testValues = [ { options: {}, outputStyleCount: 1, }, { options: { componentsHeadStyle: [ () => '.custom-component-1 .custom-child { background: red; }', ], }, outputStyleCount: 2, }, { options: { headStyle: { 'custom-component': () => '.custom-component .custom-child { background: orange; }', }, }, outputStyleCount: 2, }, { options: { componentsHeadStyle: [ () => '.custom-component-1 .custom-child { background: yellow; }', ], headStyle: { 'custom-component': () => '.custom-component .custom-child { background: green; }', }, }, outputStyleCount: 2, }, { options: { style: ['#title { background: blue; }'], }, outputStyleCount: 2, }, { options: { componentsHeadStyle: [ () => '.custom-component-1 .custom-child { background: purple; }', ], headStyle: { 'custom-component': () => '.custom-component .custom-child { background: black; }', }, style: [() => '#title { background: white; }'], }, outputStyleCount: 3, }, ] testValues.forEach((testUnit) => { const { options, outputStyleCount } = testUnit const $ = load(skeleton(options)) chai .expect($('head style').get().length, 'Unexpected number of style tags') .to.equal(outputStyleCount) }) ================================================ FILE: packages/mjml-core/tests/widthParser-test.js ================================================ const chai = require('chai') const widthParser = require('../lib/helpers/widthParser') const testValues = [ { input: '1px', options: {}, output: { parsedWidth: 1, unit: 'px'}, }, { input: '33.3px', options: {}, output: { parsedWidth: 33, unit: 'px'}, }, { input: '33.3%', options: {}, output: { parsedWidth: 33, unit: '%'}, }, { input: '33.3%', options: { parseFloatToInt: false }, output: { parsedWidth: 33.3, unit: '%'}, }, ] testValues.forEach(testUnit => { const { input, options, output } = testUnit chai.expect(widthParser(input, options), `widthParser test failed`) .to.deep.equal(output) }) ================================================ FILE: packages/mjml-divider/README.md ================================================ ### mj-divider Displays a horizontal divider that can be customized like a HTML border. ```xml ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ----------------------- | -------------------------------------------------- | ------------- | | align | `left` `center` `right` | horizontal alignment | `center` | | border-color | CSS color formats | divider color | `#000000` | | border-style | string | CSS values, e.g. `dashed` `dotted` `solid` | `solid` | | border-width | `px` | divider's border width | `4px` | | container-background-color | CSS color formats | inner element background color | | | css-class | string | class name, added to the root HTML element created | | | padding | `px` `%` | divider padding, supports up to 4 parameters | `10px 25px` | | padding-bottom | `px` `%` | divider bottom padding | | | padding-left | `px` `%` | divider left padding | | | padding-right | `px` `%` | divider right padding | | | padding-top | `px` `%` | divider top padding | | | width | `px` `%` | divider width | `100%` |

Try it live

================================================ FILE: packages/mjml-divider/package.json ================================================ { "name": "mjml-divider", "description": "mjml-divider", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-divider" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-divider/src/index.js ================================================ import { BodyComponent } from 'mjml-core' import widthParser from 'mjml-core/lib/helpers/widthParser' export default class MjDivider extends BodyComponent { static componentName = 'mj-divider' static allowedAttributes = { 'border-color': 'color', 'border-style': 'string', 'border-width': 'unit(px)', 'container-background-color': 'color', padding: 'unit(px,%){1,4}', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', width: 'unit(px,%)', align: 'enum(left,center,right)', } static defaultAttributes = { 'border-color': '#000000', 'border-style': 'solid', 'border-width': '4px', padding: '10px 25px', width: '100%', align: 'center', } getStyles() { let computeAlign = '0px auto' if (this.getAttribute('align') === 'left') { computeAlign = '0px' } else if (this.getAttribute('align') === 'right') { computeAlign = '0px 0px 0px auto' } const p = { 'border-top': ['style', 'width', 'color'] .map((attr) => this.getAttribute(`border-${attr}`)) .join(' '), 'font-size': '1px', margin: computeAlign, width: this.getAttribute('width'), } return { p, outlook: { ...p, width: this.getOutlookWidth(), }, } } getOutlookWidth() { const { containerWidth } = this.context const paddingSize = this.getShorthandAttrValue('padding', 'left') + this.getShorthandAttrValue('padding', 'right') const width = this.getAttribute('width') const { parsedWidth, unit } = widthParser(width) switch (unit) { case '%': { const effectiveWidth = parseInt(containerWidth, 10) - paddingSize const percentMultiplier = parseInt(parsedWidth, 10) / 100 return `${effectiveWidth * percentMultiplier}px` } case 'px': return width default: return `${parseInt(containerWidth, 10) - paddingSize}px` } } renderAfter() { return ` ` } render() { return `

${this.renderAfter()} ` } } ================================================ FILE: packages/mjml-group/README.md ================================================ ### mj-group Prevent adjacent `mj-column` instances from stacking on mobile by wrapping them inside an `mj-group` tag, keeping them side by side on mobile.
Desktop
easy and quick; responsive
Mobile
easy and quick; responsive
```xml

Easy and quick

Write less code, save time and code more efficiently with MJML’s semantic syntax.

Responsive

MJML is responsive by design on most-popular email clients, even Outlook.

``` #### Attributes | attribute | accepts | description | default attributes | | ---------------- | ----------------- | -------------------------------------------------- | ---------------------------------------------- | | background-color | CSS color formats | background color for a group | | | css-class | string | class name, added to the root HTML element created | | | direction | `ltr` `rtl` | set the display order of direct children | `ltr` | | vertical-align | string | CSS values, e.g. `middle` `top` `bottom` | | | width | `px` `%` | group width | (100 / number of non-raw elements in section)% |

Try it live

================================================ FILE: packages/mjml-group/package.json ================================================ { "name": "mjml-group", "description": "mjml-group", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-group" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-group/src/index.js ================================================ import { BodyComponent } from 'mjml-core' import widthParser from 'mjml-core/lib/helpers/widthParser' export default class MjGroup extends BodyComponent { static componentName = 'mj-group' static allowedAttributes = { 'background-color': 'color', direction: 'enum(ltr,rtl)', 'vertical-align': 'enum(top,bottom,middle)', width: 'unit(px,%)', } static defaultAttributes = { direction: 'ltr', } getChildContext() { const { containerWidth: parentWidth } = this.context const { nonRawSiblings, children } = this.props const paddingSize = this.getShorthandAttrValue('padding', 'left') + this.getShorthandAttrValue('padding', 'right') let containerWidth = this.getAttribute('width') || `${parseFloat(parentWidth) / nonRawSiblings}px` const { unit, parsedWidth } = widthParser(containerWidth, { parseFloatToInt: false, }) if (unit === '%') { containerWidth = `${ (parseFloat(parentWidth) * parsedWidth) / 100 - paddingSize }px` } else { containerWidth = `${parsedWidth - paddingSize}px` } return { ...this.context, containerWidth, nonRawSiblings: children.length, } } getStyles() { return { div: { 'font-size': '0', 'line-height': '0', 'text-align': 'left', display: 'inline-block', width: '100%', direction: this.getAttribute('direction'), 'vertical-align': this.getAttribute('vertical-align'), 'background-color': this.getAttribute('background-color'), }, tdOutlook: { 'vertical-align': this.getAttribute('vertical-align'), width: this.getWidthAsPixel(), }, } } getParsedWidth(toString) { const { nonRawSiblings } = this.props const width = this.getAttribute('width') || `${100 / nonRawSiblings}%` const { unit, parsedWidth } = widthParser(width, { parseFloatToInt: false, }) if (toString) { return `${parsedWidth}${unit}` } return { unit, parsedWidth, } } getWidthAsPixel() { const { containerWidth } = this.context const { unit, parsedWidth } = widthParser(this.getParsedWidth(true), { parseFloatToInt: false, }) if (unit === '%') { return `${(parseFloat(containerWidth) * parsedWidth) / 100}px` } return `${parsedWidth}px` } getColumnClass() { const { addMediaQuery } = this.context let className = '' const { parsedWidth, unit } = this.getParsedWidth() switch (unit) { case '%': className = `mj-column-per-${parseInt(parsedWidth, 10)}` break case 'px': default: className = `mj-column-px-${parseInt(parsedWidth, 10)}` break } // Add className to media queries addMediaQuery(className, { parsedWidth, unit, }) return className } render() { const { children, nonRawSiblings } = this.props const { containerWidth: groupWidth } = this.getChildContext() const { containerWidth } = this.context const getElementWidth = (width) => { if (!width) { return `${ parseInt(containerWidth, 10) / parseInt(nonRawSiblings, 10) }px` } const { unit, parsedWidth } = widthParser(width, { parseFloatToInt: false, }) if (unit === '%') { return `${(100 * parsedWidth) / groupWidth}px` } return `${parsedWidth}${unit}` } let classesName = `${this.getColumnClass()} mj-outlook-group-fix` if (this.getAttribute('css-class')) { classesName += ` ${this.getAttribute('css-class')}` } return `
${this.renderChildren(children, { attributes: { mobileWidth: 'mobileWidth' }, renderer: (component) => component.constructor.isRawElement() ? component.render() : ` ${component.render()} `, })}
` } } ================================================ FILE: packages/mjml-head/package.json ================================================ { "name": "mjml-head", "description": "mjml-head", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head/src/index.js ================================================ import { HeadComponent } from 'mjml-core' export default class MjHead extends HeadComponent { static componentName = 'mj-head' handler() { return this.handlerChildren() } } ================================================ FILE: packages/mjml-head-attributes/README.md ================================================ ### mj-attributes Inside the `mj-attributes` tag, you can cite other MJML components, like `mj-text` for example, to override the default settings for that component. An `mj-all` tag is like the above, but affects all MJML components. An `mj-class` tag creates a named group of MJML attributes you can apply to MJML components using `mj-class=""`. ```xml Hello World! ```

Try it live

================================================ FILE: packages/mjml-head-attributes/package.json ================================================ { "name": "mjml-head-attributes", "description": "mjml-head-attributes", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head-attributes" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head-attributes/src/index.js ================================================ import { forEach, omit, reduce } from 'lodash' import { HeadComponent } from 'mjml-core' export default class MjAttributes extends HeadComponent { static componentName = 'mj-attributes' handler() { const { add } = this.context const { children } = this.props forEach(children, (child) => { const { tagName, attributes, children } = child if (tagName === 'mj-class') { add('classes', attributes.name, omit(attributes, ['name'])) add( 'classesDefault', attributes.name, reduce( children, (acc, { tagName, attributes }) => ({ ...acc, [tagName]: attributes, }), {}, ), ) } else { add('defaultAttributes', tagName, attributes) } }) } } ================================================ FILE: packages/mjml-head-breakpoint/README.md ================================================ ### mj-breakpoint Allows you to control at what width the layout should change from the desktop/mobile view to the desktop one. ```xml Hello World! ``` #### Attributes | attribute | accepts | description | default value | | --------- | ------- | ------------------ | ------------- | | width | `px` | breakpoint's value | |

Try it live

================================================ FILE: packages/mjml-head-breakpoint/package.json ================================================ { "name": "mjml-head-breakpoint", "description": "mjml-head-breakpoint", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head-breakpoint" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head-breakpoint/src/index.js ================================================ import { HeadComponent } from 'mjml-core' export default class MjBreakpoint extends HeadComponent { static componentName = 'mj-breakpoint' static endingTag = true static allowedAttributes = { width: 'unit(px)', } handler() { const { add } = this.context add('breakpoint', this.getAttribute('width')) } } ================================================ FILE: packages/mjml-head-font/README.md ================================================ ### mj-font Imports external fonts and is only applied if the template uses the font. The `href` attribute should point to a hosted CSS file that contains a `@font-face` declaration, for example: [https://fonts .googleapis.com/css?family=Raleway](https://fonts.googleapis.com/css?family=Raleway) ```xml Hello World! ``` #### Attributes | attribute | accepts | description | default value | | --------- | ------- | ------------------------ | ------------- | | href | string | URL of a hosted CSS file | | | name | string | name of the font | |

Try it live

================================================ FILE: packages/mjml-head-font/package.json ================================================ { "name": "mjml-head-font", "description": "mjml-head-font", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head-font" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head-font/src/index.js ================================================ import { HeadComponent } from 'mjml-core' export default class MjFont extends HeadComponent { static componentName = 'mj-font' static allowedAttributes = { name: 'string', href: 'string', } handler() { const { add } = this.context add('fonts', this.getAttribute('name'), this.getAttribute('href')) } } ================================================ FILE: packages/mjml-head-html-attributes/README.md ================================================ ### mj-html-attributes Allows you to add custom attributes on any HTML tag within the generated HTML, using CSS selectors. It's not needed for most email creations, but can be useful in some cases, e.g. editable templates. ```xml 42 Hello World! ``` In the generated HTML, an `mj-text` tag becomes a `td` tag with a child `div` tag. In this example, the `td` tag will have the `class="custom"` attribute. Using the css selector `path=".custom div"`, the `div` inside the `td` will get the attribute `data-id="42"`. To use this component, you will likely have to look at the generated HTML to see exactly where the `css-class` is applied, to know which CSS selector you need to add your custom attribute to. You can use multiple `mj-selector` tags inside a `mj-html-attributes` tag, and multiple `mj-html-attribute` tags inside a `mj-selector` tag.

Try it live

================================================ FILE: packages/mjml-head-html-attributes/package.json ================================================ { "name": "mjml-head-html-attributes", "description": "mjml-head-html-attributes", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head-html-attributes" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head-html-attributes/src/index.js ================================================ import { get } from 'lodash' import { HeadComponent } from 'mjml-core' export default class MjHtmlAttributes extends HeadComponent { static componentName = 'mj-html-attributes' handler() { const { add } = this.context const { children } = this.props children .filter((c) => c.tagName === 'mj-selector') .forEach((selector) => { const { attributes, children } = selector const { path } = attributes const custom = children .filter( (c) => c.tagName === 'mj-html-attribute' && !!get(c, 'attributes.name'), ) .reduce( (acc, c) => ({ ...acc, [c.attributes.name]: c.content, }), {}, ) add('htmlAttributes', path, custom) }) } } ================================================ FILE: packages/mjml-head-preview/README.md ================================================ ### mj-preview This tag allows you to set the preview text that will be displayed in the inbox of the recipient. ```xml Hello MJML Hello World! ``` `mj-preview` doesn't support any attributes.

Try it live

================================================ FILE: packages/mjml-head-preview/package.json ================================================ { "name": "mjml-head-preview", "description": "mjml-head-preview", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head-preview" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head-preview/src/index.js ================================================ import { HeadComponent } from 'mjml-core' export default class MjPreview extends HeadComponent { static componentName = 'mj-preview' static endingTag = true handler() { const { add } = this.context add('preview', this.getContent()) } } ================================================ FILE: packages/mjml-head-style/README.md ================================================ ### mj-style Allows you to set CSS styles that will be applied to your MJML document as well as the outputted HTML. The CSS styles will be added to the `head` tag of the rendered HTML by default, but can also be inlined by using the `inline="inline"` attribute. Here is an example showing its use in combination with the `css-class` attribute, which is supported by all body components. ```xml .blue-text div { color: blue !important; } .red-text div { color: red !important; text-decoration: underline !important; } I'm red and underlined I'm blue because of inline I'm green ``` #### Attributes | attribute | accepts | description | default value | | --------- | ------- | -------------------------------- | ------------- | | inline | string | set to `inline` to inline styles | |

Try it live

================================================ FILE: packages/mjml-head-style/package.json ================================================ { "name": "mjml-head-style", "description": "mjml-head-style", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head-style" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head-style/src/index.js ================================================ import { HeadComponent } from 'mjml-core' export default class MjStyle extends HeadComponent { static componentName = 'mj-style' static endingTag = true static allowedAttributes = { inline: 'string', } handler() { const { add } = this.context add( this.getAttribute('inline') === 'inline' ? 'inlineStyle' : 'style', this.getContent(), ) } } ================================================ FILE: packages/mjml-head-title/README.md ================================================ ### mj-title Defines the document's title by populating the title tag. This can be shown in the browsers title bar in some cases. Its content is also used to populate the value of the `aria-label` attribute located in the immediate child `div` of the `body` tag, which is used to aid accessibility. ```xml Hello MJML Hello World! ```

Try it live

================================================ FILE: packages/mjml-head-title/package.json ================================================ { "name": "mjml-head-title", "description": "mjml-head-title", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-head-title" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-head-title/src/index.js ================================================ import { HeadComponent } from 'mjml-core' export default class MjTitle extends HeadComponent { static componentName = 'mj-title' static endingTag = true handler() { const { add } = this.context add('title', this.getContent()) } } ================================================ FILE: packages/mjml-hero/README.md ================================================ ### mj-hero Displays a hero image and behaves like an `mj-section` tag with a single `mj-column` tag. The `background-height` and `background-width` attributes are mandatory and it's best to use an image with width the same as the `mj-body` (`width="600px"` by default) and height the same or larger than the `height` of `mj-hero`. Use `background-color` to provide a fallback color in case an email client doesn't support `background-url`.
Fixed height
demo background picture with fixed height
```xml GO TO SPACE ORDER YOUR TICKET NOW ```
Fluid height
demo background picture with fixed height
```xml GO TO SPACE ORDER YOUR TICKET NOW ``` #### Attributes | attribute | accepts | description | default value | | ---------------------- | ----------------------- | ------------------------------------------------------------------ | ----------------------------- | | background-color | CSS color formats | hero background color | #ffffff | | background-height | `px` `%` | height of the image used, mandatory | | | background-position | string | CSS values, i.e. `left` `center` `right` + `top` `center` `bottom` | `center center` | | background-url | string | absolute background in URL format | `null` | | background-width | `px` `%` | width of the image used, mandatory | inherits parent element width | | border-radius | string | border radius | | | css-class | string | class name, added to the root HTML element created | | | height | `px` `%` | hero section height, (required for `fixed-height` mode) | `0px` | | inner-background-color | CSS color formats | content background color | | | mode | string | `fluid-height` or `fixed-height` | `fluid-height` | | padding | `px` `%` | hero padding, supports up to 4 parameters | `0px` | | padding-bottom | `px` `%` | hero bottom padding | `null` | | padding-left | `px` `%` | hero left padding | `null` | | padding-right | `px` `%` | hero right padding | `null` | | padding-top | `px` `%` | hero top padding | `null` | | vertical-align | `top` `middle` `bottom` | content vertical alignment | `top` | ================================================ FILE: packages/mjml-hero/package.json ================================================ { "name": "mjml-hero", "description": "mjml-hero", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-hero" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-hero/src/index.js ================================================ import { BodyComponent } from 'mjml-core' import { flow, identity, join, filter } from 'lodash/fp' import widthParser from 'mjml-core/lib/helpers/widthParser' const makeBackgroundString = flow(filter(identity), join(' ')) export default class MjHero extends BodyComponent { static componentName = 'mj-hero' static allowedAttributes = { mode: 'string', height: 'unit(px,%)', 'background-url': 'string', 'background-width': 'unit(px,%)', 'background-height': 'unit(px,%)', 'background-position': 'string', 'border-radius': 'string', 'container-background-color': 'color', 'inner-background-color': 'color', 'inner-padding': 'unit(px,%){1,4}', 'inner-padding-top': 'unit(px,%)', 'inner-padding-left': 'unit(px,%)', 'inner-padding-right': 'unit(px,%)', 'inner-padding-bottom': 'unit(px,%)', padding: 'unit(px,%){1,4}', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', 'background-color': 'color', 'vertical-align': 'enum(top,bottom,middle)', } static defaultAttributes = { mode: 'fixed-height', height: '0px', 'background-url': null, 'background-position': 'center center', padding: '0px', 'padding-bottom': null, 'padding-left': null, 'padding-right': null, 'padding-top': null, 'background-color': '#ffffff', 'vertical-align': 'top', } getChildContext() { // Refactor -- removePaddingFor(width, ['padding', 'inner-padding']) const { containerWidth } = this.context const paddingSize = this.getShorthandAttrValue('padding', 'left') + this.getShorthandAttrValue('padding', 'right') let currentContainerWidth = `${parseFloat(containerWidth)}px` const { unit, parsedWidth } = widthParser(currentContainerWidth, { parseFloatToInt: false, }) if (unit === '%') { currentContainerWidth = `${ (parseFloat(containerWidth) * parsedWidth) / 100 - paddingSize }px` } else { currentContainerWidth = `${parsedWidth - paddingSize}px` } return { ...this.context, containerWidth: currentContainerWidth, } } getStyles() { const { containerWidth } = this.context const backgroundRatio = Math.round( (parseInt(this.getAttribute('background-height'), 10) / parseInt(this.getAttribute('background-width'), 10)) * 100, ) const width = this.getAttribute('background-width') || containerWidth return { div: { margin: '0 auto', 'max-width': containerWidth, }, table: { width: '100%', }, tr: { 'vertical-align': 'top', }, 'td-fluid': { width: `0.01%`, 'padding-bottom': `${backgroundRatio}%`, 'mso-padding-bottom-alt': '0', }, 'outlook-table': { width: containerWidth, }, 'outlook-td': { 'line-height': 0, 'font-size': 0, 'mso-line-height-rule': 'exactly', }, 'outlook-inner-table': { width: containerWidth, }, 'outlook-image': { border: '0', height: this.getAttribute('background-height'), 'mso-position-horizontal': 'center', position: 'absolute', top: 0, width, 'z-index': '-3', }, 'outlook-inner-td': { 'background-color': this.getAttribute('inner-background-color'), padding: this.getAttribute('inner-padding'), 'padding-top': this.getAttribute('inner-padding-top'), 'padding-left': this.getAttribute('inner-padding-left'), 'padding-right': this.getAttribute('inner-padding-right'), 'padding-bottom': this.getAttribute('inner-padding-bottom'), }, 'inner-table': { width: '100%', margin: '0px', }, 'inner-div': { 'background-color': this.getAttribute('inner-background-color'), float: this.getAttribute('align'), margin: '0px auto', width: this.getAttribute('width'), }, } } getBackground = () => makeBackgroundString([ this.getAttribute('background-color'), ...(this.getAttribute('background-url') ? [ `url('${this.getAttribute('background-url')}')`, 'no-repeat', `${this.getAttribute('background-position')} / cover`, ] : []), ]) renderContent() { const { containerWidth } = this.context const { children } = this.props return `
${this.renderChildren(children, { renderer: (component) => component.constructor.isRawElement() ? component.render() : ` `, })}
${component.render()}
` } renderMode() { const commonAttributes = { background: this.getAttribute('background-url'), style: { background: this.getBackground(), 'background-position': this.getAttribute('background-position'), 'background-repeat': 'no-repeat', 'border-radius': this.getAttribute('border-radius'), padding: this.getAttribute('padding'), 'padding-top': this.getAttribute('padding-top'), 'padding-left': this.getAttribute('padding-left'), 'padding-right': this.getAttribute('padding-right'), 'padding-bottom': this.getAttribute('padding-bottom'), 'vertical-align': this.getAttribute('vertical-align'), }, } /* eslint-disable no-alert, no-case-declarations */ switch (this.getAttribute('mode')) { case 'fluid-height': const magicTd = this.htmlAttributes({ style: `td-fluid` }) return ` ${this.renderContent()} ` case 'fixed-height': default: const height = parseInt(this.getAttribute('height'), 10) - this.getShorthandAttrValue('padding', 'top') - this.getShorthandAttrValue('padding', 'bottom') return ` ${this.renderContent()} ` } /* eslint-enable no-alert, no-case-declarations */ } render() { const { containerWidth } = this.context return `
${this.renderMode()}
` } } ================================================ FILE: packages/mjml-image/README.md ================================================ ### mj-image Displays a responsive image in your email. It is similar to the HTML `` tag. Note that if no width is provided, the image will use the parent column width. ```xml ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ----------------------- | ------------------------------------------------------------------------------- | --------------------- | | align | `left` `center` `right` | image alignment | `center` | | alt | string | image description | `''` | | border | string | CSS border format | `0` | | border-bottom | string | CSS border format | | | border-left | string | CSS border format | | | border-radius | `px` `%` | border radius | | | border-right | string | CSS border format | | | border-top | string | CSS border format | | | container-background-color | CSS color formats | inner element background color | | | css-class | string | class name, added to the root HTML element created | | | fluid-on-mobile | boolean | if `true`, will be full width on mobile even if `width` is set | | | font-size | `px` | size of the alt text when image is not rendered | `13px` | | height | `px` | image height | `auto` | | href | string | link to redirect to on click, in URL format | | | max-height | `px` `%` | specify the maximum height of an image | | | name | string | specify the link name attribute | | | padding | `px` `%` | hero padding, supports up to 4 parameters | `10px 25px` | | padding-bottom | `px` `%` | hero bottom padding | | | padding-left | `px` `%` | hero left padding | | | padding-right | `px` `%` | hero right padding | | | padding-top | `px` `%` | hero top padding | | | rel | string | specify the rel attribute | | | sizes | string | set width based on query | | | src | string | image source in URL format | | | srcset | string | enables to set a different image source based on the viewport, using CSS syntax | | | target | string | link target on click | `_blank` | | title | string | tooltip & accessibility | | | usemap | string | reference to image map, be careful, it isn't supported everywhere | | | width | `px` | image width | inherits parent width |

Try it live

================================================ FILE: packages/mjml-image/package.json ================================================ { "name": "mjml-image", "description": "mjml-image", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-image" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-image/src/index.js ================================================ import { min } from 'lodash' import { BodyComponent, makeLowerBreakpoint } from 'mjml-core' import widthParser from 'mjml-core/lib/helpers/widthParser' export default class MjImage extends BodyComponent { static componentName = 'mj-image' static allowedAttributes = { alt: 'string', href: 'string', name: 'string', src: 'string', srcset: 'string', sizes: 'string', title: 'string', rel: 'string', align: 'enum(left,center,right)', border: 'string', 'border-bottom': 'string', 'border-left': 'string', 'border-right': 'string', 'border-top': 'string', 'border-radius': 'unit(px,%){1,4}', 'container-background-color': 'color', 'fluid-on-mobile': 'boolean', padding: 'unit(px,%){1,4}', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', target: 'string', width: 'unit(px)', height: 'unit(px,auto)', 'max-height': 'unit(px,%)', 'font-size': 'unit(px)', usemap: 'string', } static defaultAttributes = { alt: '', align: 'center', border: '0', height: 'auto', padding: '10px 25px', target: '_blank', 'font-size': '13px', } getStyles() { const width = this.getContentWidth() const fullWidth = this.getAttribute('full-width') === 'full-width' const { parsedWidth, unit } = widthParser(width) return { img: { border: this.getAttribute('border'), 'border-left': this.getAttribute('border-left'), 'border-right': this.getAttribute('border-right'), 'border-top': this.getAttribute('border-top'), 'border-bottom': this.getAttribute('border-bottom'), 'border-radius': this.getAttribute('border-radius'), display: 'block', outline: 'none', 'text-decoration': 'none', height: this.getAttribute('height'), 'max-height': this.getAttribute('max-height'), 'min-width': fullWidth ? '100%' : null, width: '100%', 'max-width': fullWidth ? '100%' : null, 'font-size': this.getAttribute('font-size'), }, td: { width: fullWidth ? null : `${parsedWidth}${unit}`, }, table: { 'min-width': fullWidth ? '100%' : null, 'max-width': fullWidth ? '100%' : null, width: fullWidth ? `${parsedWidth}${unit}` : null, 'border-collapse': 'collapse', 'border-spacing': '0px', }, } } getContentWidth() { const width = this.getAttribute('width') ? parseInt(this.getAttribute('width'), 10) : Infinity const { box } = this.getBoxWidths() return min([box, width]) } renderImage() { const height = this.getAttribute('height') const img = ` ` if (this.getAttribute('href')) { return ` ${img} ` } return img } headStyle = (breakpoint) => ` @media only screen and (max-width:${makeLowerBreakpoint(breakpoint)}) { table.mj-full-width-mobile { width: 100% !important; } td.mj-full-width-mobile { width: auto !important; } } ` render() { return `
${this.renderImage()}
` } } ================================================ FILE: packages/mjml-migrate/LICENSE ================================================ MIT License Copyright (c) 2017 Nicolas Garnier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/mjml-migrate/README.md ================================================ # mjml-migrate ## Purpose Makes a template following the MJML 3 syntax compatible with MJML 4. ## Installation Clone the repo & `npm install` or install via NPM: `npm install mjml-migrate` ## Usage `migrate ` ## What happens * `mj-container` is removed and its attributes are passed to `mj-body` * Unitless values are converted to `px` * `mj-social`'s syntax is replaced with the v4 syntax * Unsupported tags (defined in `unavailableTags` in `config.js`) are removed ================================================ FILE: packages/mjml-migrate/package.json ================================================ { "name": "mjml-migrate", "version": "4.18.0", "description": "A tool to migrate a template from MJML 3 to MJML 4", "main": "lib/migrate.js", "bin": { "migrate": "lib/cli.js" }, "files": [ "lib" ], "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-migrate" }, "author": "Nicolas Garnier", "license": "MIT", "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" }, "dependencies": { "@babel/runtime": "^7.28.4", "js-beautify": "^1.6.14", "lodash": "^4.17.21", "mjml-core": "4.18.0", "mjml-parser-xml": "4.18.0", "yargs": "^17.7.2" } } ================================================ FILE: packages/mjml-migrate/src/cli.js ================================================ #!/usr/bin/env node import fs from 'fs' import yargs from 'yargs' import migrate from './migrate' import { version } from '../package.json' const program = yargs .usage('$0 [options] ') .version(version) .help() if (program.argv._.length !== 2) { program.showHelp() process.exit(1) } const [inputFilename, outputFilename] = program.argv._ const input = fs.readFileSync(inputFilename, 'utf8') const output = migrate(input) fs.writeFileSync(outputFilename, output) // eslint-disable-next-line no-console console.log( `${inputFilename} was converted to the MJML 4 syntax in ${outputFilename}`, ) ================================================ FILE: packages/mjml-migrate/src/config.js ================================================ const unavailableTags = ['mj-html', 'mj-invoice', 'mj-list', 'mj-location'] const attributesWithUnit = [ 'background-size', 'border-radius', 'border-width', 'cellpadding', 'cellspacing', 'font-size', 'height', 'icon-height', 'ico-padding', 'ico-padding-bottom', 'ico-font-size', 'ico-line-height', 'ico-padding-left', 'ico-padding-right', 'ico-padding-top', 'icon-size', 'icon-width', 'inner-padding', 'letter-spacing', 'padding', 'padding-bottom', 'padding-left', 'padding-right', 'padding-left', 'tb-border-radius', 'tb-width', 'width', ] module.exports = { unavailableTags, attributesWithUnit, } ================================================ FILE: packages/mjml-migrate/src/migrate.js ================================================ /* eslint-disable no-console */ import { keys, find, isNil } from 'lodash' import MJMLParser from 'mjml-parser-xml' import { components } from 'mjml-core' import { html as htmlBeautify } from 'js-beautify' import { unavailableTags, attributesWithUnit } from './config' const beautifyOptions = { indent_size: 2, wrap_attributes_indent_size: 2, max_preserve_newline: 0, preserve_newlines: false, } function removeContainerTag(bodyTag) { if (bodyTag.children[0].tagName === 'mj-container') { bodyTag.attributes = bodyTag.children[0].attributes bodyTag.children = bodyTag.children[0].children } return bodyTag } const listAttributes = (tag) => tag.attributes function addPx(value) { // eslint-disable-next-line no-restricted-globals if (!isNaN(value) && !isNil(value)) { return `${value}px` } return value } function fixUnits(attribute, value) { const { length } = attributesWithUnit for (let i = 0; i < length; i += 1) { if (attributesWithUnit[i] === attribute) { return addPx(value) } } return value } function cleanAttributes(attributes) { keys(attributes).forEach((key) => { attributes[key] = fixUnits(key, attributes[key]) }) return attributes } const DEFAULT_SOCIAL_DISPLAY = 'facebook twitter google' function migrateSocialSyntax(socialTag) { const listAllNetworks = (tag) => { const attributes = (tag.attributes.display || DEFAULT_SOCIAL_DISPLAY).split( ' ', ) delete tag.attributes.display return attributes } const attributes = listAttributes(socialTag) const networks = listAllNetworks(socialTag) socialTag.children = [] // migrate all attributes to their child attributes keys(networks).forEach((network) => { const nameMigrated = networks[network] .replace(':url', '-noshare') .replace(':share', '') const nameWithoutOpts = nameMigrated.replace('-noshare', '') socialTag.children.push({ tagName: `mj-social-element`, attributes: { name: nameMigrated }, content: attributes[`${nameWithoutOpts}-content`] || '', }) keys(attributes).forEach((attribute) => { if (attribute.match(nameWithoutOpts) && !attribute.match('content')) { socialTag.children[network].attributes[ attribute.replace(`${nameWithoutOpts}-`, '') ] = socialTag.attributes[attribute] delete socialTag.attributes[attribute] } }) }) // delete all content attributes from the root tag after they've been migrated keys(attributes).forEach((attribute) => { if (attribute.match('content')) { delete attributes[attribute] } }) return socialTag } function migrateNavbarSyntax(navbarTag) { navbarTag.tagName = 'mj-section' navbarTag.attributes['full-width'] = 'full-width' return navbarTag } function migrateHeroSyntax(heroTag) { const child = find(heroTag.children, { tagName: 'mj-hero-content' }) return { ...heroTag, children: child.children, attributes: { ...heroTag.attributes, ...child.attributes, }, } } function isSupportedTag(tag) { return unavailableTags.indexOf(tag) === -1 } function loopThrough(tree) { keys(tree).forEach((key) => { if (key === 'children') { for (let i = 0; i < tree.children.length; i += 1) { if (isSupportedTag(tree.children[i].tagName)) { switch (tree.children[i].tagName) { case 'mj-body': tree.children[i] = removeContainerTag(tree.children[i]) break case 'mj-social': tree.children[i] = migrateSocialSyntax(tree.children[i]) break case 'mj-navbar': tree.children[i] = migrateNavbarSyntax(tree.children[i]) break case 'mj-inline-links': tree.children[i].tagName = 'mj-navbar' break case 'mj-link': tree.children[i].tagName = 'mj-navbar-link' break case 'mj-hero': tree.children[i] = migrateHeroSyntax(tree.children[i]) break // no default } tree.children[i].attributes = cleanAttributes( tree.children[i].attributes, ) loopThrough(tree.children[i]) } else { console.error( `Ignoring unsupported tag : ${tree.children[i].tagName} on line ${tree.children[i].line}`, ) delete tree.children[i] } } } }) return tree } function checkV3Through(node) { if (node.tagName === 'mj-container') return true if (!node.children || !node.children.length) return false return node.children.some(checkV3Through) } const jsonToXML = ({ tagName, attributes, children, content }) => { const subNode = children && children.length > 0 ? children.map(jsonToXML).join('\n') : content || '' const stringAttrs = Object.keys(attributes) .map((attr) => `${attr}="${attributes[attr]}"`) .join(' ') return `<${tagName}${ stringAttrs === '' ? '>' : ` ${stringAttrs}>` }${subNode}` } export default function migrate(input, options = {}) { console.warn('mjml-migrate is deprecated and will be removed in mjml 5') const { beautify } = options if (typeof input === 'object') return loopThrough(input) const mjmlJson = MJMLParser(input, { components, ignoreIncludes: true }) loopThrough(mjmlJson) return beautify ? htmlBeautify(jsonToXML(mjmlJson), beautifyOptions) : jsonToXML(mjmlJson) } export function handleMjml3(mjml, options = {}) { const isV3Synthax = checkV3Through(mjml) if (!isV3Synthax) return mjml if (!options.noMigrateWarn) console.log( 'MJML v3 syntax detected, migrating to MJML v4 syntax. Use mjml -m to get the migrated MJML.', ) return migrate(mjml) } /* eslint-enable no-console */ ================================================ FILE: packages/mjml-navbar/README.md ================================================ ### mj-navbar Displays a navigation menu with an optional `hamburger` mode for mobile devices. ```xml Getting started Try it live Templates Components ```
Standard Desktop
example desktop width navbar
Standard Mobile
example mobile width navbar
Mode hamburger enabled:
hamburger mode animation shows menu expansion after clicking hamburger icon
#### Attributes | attribute | accepts | description | default value | | ------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | align | `left`
`center`
`right` | align content | `center` | | base-url | string | base URL for child components | `null` | | css-class | string | class name, added to the root HTML element created | | | hamburger | string | activate the hamburger navigation on mobile if the value is hamburger | `null` | | ico-align | `left`
`center`
`right` | hamburger icon alignment
(`hamburger="hamburger"` required) | `center` | | ico-close | string | char code for a custom close icon, e.g. ASCII code decimal
(`hamburger="hamburger"` required) | `⊗` | | ico-color | CSS color formats | hamburger icon color
(`hamburger="hamburger"` required) | `#000000` | | ico-font-family | string | hamburger icon font
(`hamburger="hamburger"` required) | `Ubuntu, Helvetica, Arial, sans-serif` | | ico-font-size | `px` `%` | hamburger icon size
(`hamburger="hamburger"` required) | `30px` | | ico-line-height | `px` `%` | hamburger icon line height
(`hamburger="hamburger"` required) | `30px` | | ico-open | string | char code for a custom open icon, e.g. ASCII code decimal
(`hamburger="hamburger"` required) | `☰` | | ico-padding | `px` `%` | hamburger icon padding, supports up to 4 parameters
(`hamburger="hamburger"` required) | `10px` | | ico-padding-bottom | `px` `%` | hamburger icon bottom padding
(`hamburger="hamburger"` required) | | | ico-padding-left | `px` `%` | hamburger icon left padding
(`hamburger="hamburger"` required) | | | ico-padding-right | `px` `%` | hamburger icon right padding
(`hamburger="hamburger"` required) | | | ico-padding-top | `px` `%` | hamburger icon top padding
(`hamburger="hamburger"` required) | | | ico-text-decoration | string | hamburger icon text decoration e.g. `none` `underline` `overline` `line-through`
(`hamburger="hamburger"` required) | `none` | | ico-text-transform | string | hamburger icon text transformation `none` `capitalize` `uppercase` `lowercase`
(`hamburger="hamburger"` required) | `uppercase` | | padding | `px` `%` | navbar padding, supports up to 4 parameters | | | padding-bottom | `px` `%` | navbar bottom padding | | | padding-left | `px` `%` | navbar left padding | | | padding-right | `px` `%` | navbar right padding | | | padding-top | `px` `%` | navbar top padding | |

Try it live

#### mj-navbar-link Used to display an individual link in the navbar. Individual links of the menu should be wrapped inside `mj-navbar`. #### Attributes | attribute | accepts | description | default value | | --------------- | ----------------- | ------------------------------------------------------------ | -------------------------------------- | | color | CSS color formats | text color | `#000000` | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font | `Ubuntu, Helvetica, Arial, sans-serif` | | font-size | `px` | text size | `13px` | | font-style | string | CSS values, i.e. `normal` `italic` `oblique` | | | font-weight | string | text thickness | | | href | string | link to redirect to on click, in URL format | | | letter-spacing | `px` `em` | letter-spacing | | | line-height | `px` `%` | space between the lines | `22px` | | name | string | specify the link name attribute | | | padding | `px` `%` | navbar link padding, supports up to 4 parameters | `15px 10px` | | padding-bottom | `px` `%` | bottom padding | | | padding-left | `px` `%` | left padding | | | padding-right | `px` `%` | right padding | | | padding-top | `px` `%` | top padding | | | rel | string | specify the rel attribute | | | target | string | link target on click | | | text-decoration | string | CSS values, i.e. `underline` `overline` `none` | `none` | | text-transform | string | CSS values, i.e. `capitalize` `uppercase` `lowercase` `none` | `uppercase` | ================================================ FILE: packages/mjml-navbar/package.json ================================================ { "name": "mjml-navbar", "description": "mjml-navbar", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-navbar" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-navbar/src/Navbar.js ================================================ import { BodyComponent, makeLowerBreakpoint } from 'mjml-core' import conditionalTag, { msoConditionalTag, } from 'mjml-core/lib/helpers/conditionalTag' import genRandomHexString from 'mjml-core/lib/helpers/genRandomHexString' export default class MjNavbar extends BodyComponent { static componentName = 'mj-navbar' static allowedAttributes = { align: 'enum(left,center,right)', 'base-url': 'string', hamburger: 'string', 'ico-align': 'enum(left,center,right)', 'ico-open': 'string', 'ico-close': 'string', 'ico-color': 'color', 'ico-font-size': 'unit(px,%)', 'ico-font-family': 'string', 'ico-text-transform': 'string', 'ico-padding': 'unit(px,%){1,4}', 'ico-padding-left': 'unit(px,%)', 'ico-padding-top': 'unit(px,%)', 'ico-padding-right': 'unit(px,%)', 'ico-padding-bottom': 'unit(px,%)', padding: 'unit(px,%){1,4}', 'padding-left': 'unit(px,%)', 'padding-top': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-bottom': 'unit(px,%)', 'ico-text-decoration': 'string', 'ico-line-height': 'unit(px,%,)', } static defaultAttributes = { align: 'center', 'base-url': null, hamburger: null, 'ico-align': 'center', 'ico-open': '☰', 'ico-close': '⊗', 'ico-color': '#000000', 'ico-font-size': '30px', 'ico-font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'ico-text-transform': 'uppercase', 'ico-padding': '10px', 'ico-text-decoration': 'none', 'ico-line-height': '30px', } headStyle = (breakpoint) => ` noinput.mj-menu-checkbox { display:block!important; max-height:none!important; visibility:visible!important; } @media only screen and (max-width:${makeLowerBreakpoint(breakpoint)}) { .mj-menu-checkbox[type="checkbox"] ~ .mj-inline-links { display:none!important; } .mj-menu-checkbox[type="checkbox"]:checked ~ .mj-inline-links, .mj-menu-checkbox[type="checkbox"] ~ .mj-menu-trigger { display:block!important; max-width:none!important; max-height:none!important; font-size:inherit!important; } .mj-menu-checkbox[type="checkbox"] ~ .mj-inline-links > a { display:block!important; } .mj-menu-checkbox[type="checkbox"]:checked ~ .mj-menu-trigger .mj-menu-icon-close { display:block!important; } .mj-menu-checkbox[type="checkbox"]:checked ~ .mj-menu-trigger .mj-menu-icon-open { display:none!important; } } ` getStyles() { return { div: { align: this.getAttribute('align'), width: '100%', }, label: { display: 'block', cursor: 'pointer', 'mso-hide': 'all', '-moz-user-select': 'none', 'user-select': 'none', color: this.getAttribute('ico-color'), 'font-size': this.getAttribute('ico-font-size'), 'font-family': this.getAttribute('ico-font-family'), 'text-transform': this.getAttribute('ico-text-transform'), 'text-decoration': this.getAttribute('ico-text-decoration'), 'line-height': this.getAttribute('ico-line-height'), padding: this.getAttribute('ico-padding'), 'padding-top': this.getAttribute('ico-padding-top'), 'padding-right': this.getAttribute('ico-padding-right'), 'padding-bottom': this.getAttribute('ico-padding-bottom'), 'padding-left': this.getAttribute('ico-padding-left'), }, trigger: { display: 'none', 'max-height': '0px', 'max-width': '0px', 'font-size': '0px', overflow: 'hidden', }, icoOpen: { 'mso-hide': 'all', }, icoClose: { display: 'none', 'mso-hide': 'all', }, } } renderHamburger() { const labelKey = genRandomHexString(16) return ` ${msoConditionalTag( ` `, true, )}
` } render() { return ` ${ this.getAttribute('hamburger') === 'hamburger' ? this.renderHamburger() : '' }
${conditionalTag(` `)} ${this.renderChildren(this.props.children, { attributes: { navbarBaseUrl: this.getAttribute('base-url'), }, })} ${conditionalTag(`
`)}
` } } ================================================ FILE: packages/mjml-navbar/src/NavbarLink.js ================================================ import { BodyComponent, suffixCssClasses } from 'mjml-core' import conditionalTag from 'mjml-core/lib/helpers/conditionalTag' export default class MjNavbarLink extends BodyComponent { static componentName = 'mj-navbar-link' static endingTag = true static allowedAttributes = { color: 'color', 'font-family': 'string', 'font-size': 'unit(px)', 'font-style': 'string', 'font-weight': 'string', href: 'string', name: 'string', target: 'string', rel: 'string', 'letter-spacing': 'unitWithNegative(px,em)', 'line-height': 'unit(px,%,)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', 'text-decoration': 'string', 'text-transform': 'string', } static defaultAttributes = { color: '#000000', 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'font-weight': 'normal', 'line-height': '22px', padding: '15px 10px', target: '_blank', 'text-decoration': 'none', 'text-transform': 'uppercase', } getStyles() { return { a: { display: 'inline-block', color: this.getAttribute('color'), 'font-family': this.getAttribute('font-family'), 'font-size': this.getAttribute('font-size'), 'font-style': this.getAttribute('font-style'), 'font-weight': this.getAttribute('font-weight'), 'letter-spacing': this.getAttribute('letter-spacing'), 'line-height': this.getAttribute('line-height'), 'text-decoration': this.getAttribute('text-decoration'), 'text-transform': this.getAttribute('text-transform'), padding: this.getAttribute('padding'), 'padding-top': this.getAttribute('padding-top'), 'padding-left': this.getAttribute('padding-left'), 'padding-right': this.getAttribute('padding-right'), 'padding-bottom': this.getAttribute('padding-bottom'), }, td: { padding: this.getAttribute('padding'), 'padding-top': this.getAttribute('padding-top'), 'padding-left': this.getAttribute('padding-left'), 'padding-right': this.getAttribute('padding-right'), 'padding-bottom': this.getAttribute('padding-bottom'), }, } } renderContent() { const href = this.getAttribute('href') const navbarBaseUrl = this.getAttribute('navbarBaseUrl') const link = navbarBaseUrl ? `${navbarBaseUrl}${href}` : href const cssClass = this.getAttribute('css-class') ? ` ${this.getAttribute('css-class')}` : '' return ` ${this.getContent()} ` } render() { return ` ${conditionalTag(` `)} ${this.renderContent()} ${conditionalTag(` `)} ` } } ================================================ FILE: packages/mjml-navbar/src/index.js ================================================ export { default as Navbar } from './Navbar' export { default as NavbarLink } from './NavbarLink' ================================================ FILE: packages/mjml-parser-xml/package.json ================================================ { "name": "mjml-parser-xml", "description": "mjml-parser-xml", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-parser-xml" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward", "test": "node ./test/test.js" }, "dependencies": { "@babel/runtime": "^7.28.4", "detect-node": "2.1.0", "htmlparser2": "^9.1.0", "lodash": "^4.17.21" }, "devDependencies": { "@babel/cli": "^7.8.4", "chai": "^4.1.1", "mjml": "4.18.0", "mjml-core": "4.18.0", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-parser-xml/src/helpers/cleanNode.js ================================================ import _ from 'lodash' export default function cleanNode(node) { delete node.parent // Delete children if needed if (node.children && node.children.length) { _.forEach(node.children, cleanNode) } else { delete node.children } // Delete attributes if needed if (node.attributes && Object.keys(node.attributes).length === 0) { delete node.attributes } } ================================================ FILE: packages/mjml-parser-xml/src/helpers/convertBooleansOnAttrs.js ================================================ import { mapValues } from 'lodash' /** * Convert "true" and "false" string attributes values * to corresponding Booleans */ export default function convertBooleansOnAttrs(attrs) { return mapValues(attrs, (val) => { if (val === 'true') { return true } if (val === 'false') { return false } return val }) } ================================================ FILE: packages/mjml-parser-xml/src/helpers/setEmptyAttributes.js ================================================ import { forEach } from 'lodash' export default function setEmptyAttributes(node) { if (!node.attributes) { node.attributes = {} } if (node.children) { forEach(node.children, setEmptyAttributes) } } ================================================ FILE: packages/mjml-parser-xml/src/index.js ================================================ import { Parser } from 'htmlparser2' import { isObject, findLastIndex, find } from 'lodash' import { filter, map, flow } from 'lodash/fp' import path from 'path' import fs from 'fs' import cleanNode from './helpers/cleanNode' import convertBooleansOnAttrs from './helpers/convertBooleansOnAttrs' import setEmptyAttributes from './helpers/setEmptyAttributes' const isNode = require('detect-node') const indexesForNewLine = (xml) => { const regex = /\n/gi const indexes = [0] while (regex.exec(xml)) { indexes.push(regex.lastIndex) } return indexes } const isSelfClosing = (indexes, parser) => indexes.startIndex === parser.startIndex && indexes.endIndex === parser.endIndex export default function MJMLParser(xml, options = {}, includedIn = []) { const { addEmptyAttributes = true, components = {}, convertBooleans = true, keepComments = true, filePath = '.', actualPath = '.', ignoreIncludes = false, preprocessors = [], } = options const endingTags = flow( filter((component) => component.endingTag), map((component) => component.getTagName()), )({ ...components }) let cwd = process.cwd() if (isNode && filePath) { try { const isDir = fs.lstatSync(filePath).isDirectory() cwd = isDir ? filePath : path.dirname(filePath) } catch (e) { throw new Error('Specified filePath does not exist') } } let mjml = null let cur = null let inInclude = !!includedIn.length let inEndingTag = 0 const cssIncludes = [] const currentEndingTagIndexes = { startIndex: 0, endIndex: 0 } const findTag = (tagName, tree) => find(tree.children, { tagName }) const lineIndexes = indexesForNewLine(xml) const handleCssHtmlInclude = (file, attrs, line) => { const partialPath = path.resolve(cwd, file) let content try { content = fs.readFileSync(partialPath, 'utf8') } catch (e) { const newNode = { line, file, absoluteFilePath: path.resolve(cwd, actualPath), parent: cur, tagName: 'mj-raw', content: ``, children: [], errors: [ { type: 'include', params: { file, partialPath }, }, ], } cur.children.push(newNode) return } if (attrs.type === 'html') { const newNode = { line, file, absoluteFilePath: path.resolve(cwd, actualPath), parent: cur, tagName: 'mj-raw', content, } cur.children.push(newNode) return } const attributes = attrs['css-inline'] === 'inline' ? { inline: 'inline' } : {} const newNode = { line, file, absoluteFilePath: path.resolve(cwd, actualPath), tagName: 'mj-style', content, children: [], attributes, } cssIncludes.push(newNode) } const handleInclude = (file, line) => { const partialPath = path.resolve(cwd, file) const curBeforeInclude = cur if (find(cur.includedIn, { file: partialPath })) throw new Error(`Circular inclusion detected on file : ${partialPath}`) let content try { content = fs.readFileSync(partialPath, 'utf8') } catch (e) { const newNode = { line, file, absoluteFilePath: path.resolve(cwd, actualPath), parent: cur, tagName: 'mj-raw', content: ``, children: [], errors: [ { type: 'include', params: { file, partialPath }, }, ], } cur.children.push(newNode) return } content = content.indexOf('') === -1 ? `${content}` : content const partialMjml = MJMLParser( content, { ...options, filePath: partialPath, actualPath: partialPath, }, [ ...cur.includedIn, { file: cur.absoluteFilePath, line, }, ], ) const bindToTree = (children, tree = cur) => children.map((c) => ({ ...c, parent: tree })) if (partialMjml.tagName !== 'mjml') { return } const body = findTag('mj-body', partialMjml) const head = findTag('mj-head', partialMjml) if (body) { const boundChildren = bindToTree(body.children) cur.children = [...cur.children, ...boundChildren] } if (head) { let curHead = findTag('mj-head', mjml) if (!curHead) { mjml.children.push({ file: actualPath, absoluteFilePath: path.resolve(cwd, actualPath), parent: mjml, tagName: 'mj-head', children: [], includedIn: [], }) curHead = findTag('mj-head', mjml) } const boundChildren = bindToTree(head.children, curHead) curHead.children = [...curHead.children, ...boundChildren] } // must restore cur to the cur before include started cur = curBeforeInclude } const parser = new Parser( { onopentag: (name, attrs) => { const isAnEndingTag = endingTags.indexOf(name) !== -1 if (inEndingTag > 0) { if (isAnEndingTag) inEndingTag += 1 return } if (isAnEndingTag) { inEndingTag += 1 if (inEndingTag === 1) { // we're entering endingTag currentEndingTagIndexes.startIndex = parser.startIndex currentEndingTagIndexes.endIndex = parser.endIndex } } const line = findLastIndex(lineIndexes, (i) => i <= parser.startIndex) + 1 if (name === 'mj-include') { if (ignoreIncludes || !isNode) return if (attrs.type === 'css' || attrs.type === 'html') { handleCssHtmlInclude(decodeURIComponent(attrs.path), attrs, line) return } inInclude = true handleInclude(decodeURIComponent(attrs.path), line) return } if (convertBooleans) { // "true" and "false" will be converted to bools attrs = convertBooleansOnAttrs(attrs) } const newNode = { file: actualPath, absoluteFilePath: isNode ? path.resolve(cwd, actualPath) : actualPath, line, includedIn, parent: cur, tagName: name, attributes: attrs, children: [], } if (cur) { cur.children.push(newNode) } else { mjml = newNode } cur = newNode }, onclosetag: (name) => { if (endingTags.indexOf(name) !== -1) { inEndingTag -= 1 if (!inEndingTag) { // we're getting out of endingTag // if self-closing tag we don't get the content if (!isSelfClosing(currentEndingTagIndexes, parser)) { const partialVal = xml .substring( currentEndingTagIndexes.endIndex + 1, parser.endIndex, ) .trim() const val = partialVal.substring( 0, partialVal.lastIndexOf(` 0) return if (inInclude) { inInclude = false } // for includes, setting cur is handled in handleInclude because when there is // only mj-head in include it doesn't create any elements, so setting back to parent is wrong if (name !== 'mj-include') cur = (cur && cur.parent) || null }, ontext: (text) => { if (inEndingTag > 0) return if (text && text.trim() && cur) { cur.content = `${(cur && cur.content) || ''}${text.trim()}`.trim() } }, oncomment: (data) => { if (inEndingTag > 0) return if (cur && keepComments) { cur.children.push({ line: findLastIndex(lineIndexes, (i) => i <= parser.startIndex) + 1, tagName: 'mj-raw', content: ``, includedIn, }) } }, }, { recognizeCDATA: true, decodeEntities: false, recognizeSelfClosing: true, lowerCaseAttributeNames: false, }, ) // Apply preprocessors to raw xml xml = flow(preprocessors)(xml) parser.write(xml) parser.end() if (!isObject(mjml)) { throw new Error('Parsing failed. Check your mjml.') } cleanNode(mjml) // Assign "attributes" property if not set if (addEmptyAttributes) { setEmptyAttributes(mjml) } if (cssIncludes.length) { const head = find(mjml.children, { tagName: 'mj-head' }) if (head) { if (head.children) { head.children = [...head.children, ...cssIncludes] } else { head.children = cssIncludes } } else { mjml.children.push({ file: filePath, line: 0, tagName: 'mj-head', children: cssIncludes, }) } } return mjml } ================================================ FILE: packages/mjml-parser-xml/test/incl.mjml ================================================ COIN aze COIN2 aze2 ================================================ FILE: packages/mjml-parser-xml/test/test-preprocessors.js ================================================ const { template } = require('lodash') const MJMLParser = require('../lib') const mjml2html = require('../../mjml/lib') const { components } = require('../../mjml-core/lib') const parse = mjml => MJMLParser(mjml, { keepComments: true, components, preprocessors: [ data => template(data, { evaluate: /{{([\s\S]+?)}}/g, interpolate: /{{=([\s\S]+?)}}/g, escape: /{{-([\s\S]+?)}}/g, })({ buttons: [{ title: 'Title' }, { title: 'Title2' }], }), ], }) const xml = ` {{ buttons.forEach(function(button) { }} {{=button.title}} {{ }); }} ` const json = parse(xml) const { html } = mjml2html(json) console.log(html) // eslint-disable-line no-console ================================================ FILE: packages/mjml-parser-xml/test/test-utils.js ================================================ const _ = require('lodash') function omitDeepLodash(input, props) { function omitDeepOnOwnProps(obj) { if (!_.isArray(obj) && !_.isObject(obj)) { return obj } if (_.isArray(obj)) { return omitDeepLodash(obj, props) } const o = {} _.forOwn(obj, (value, key) => { o[key] = omitDeepLodash(value, props) }) return _.omit(o, props) } if (typeof input === "undefined") { return undefined } if (_.isArray(input)) { return input.map(omitDeepOnOwnProps) } return omitDeepOnOwnProps(input) } function deepDiff(object, base) { function changes(object, base) { return _.transform(object, (result, value, key) => { if (!_.isEqual(value, base[key])) { result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value } }) } return changes(object, base) } function displayDiff(obj1, obj2) { const diffs = deepDiff(obj1, obj2) if (_.isEqual(diffs, {})) { console.log('\x1b[32m', 'Parsing test successful') // eslint-disable-line no-console console.log('\x1b[0m', '') // eslint-disable-line no-console } else { console.log('\x1b[31m', 'Parsing test failed. Differences found :') // eslint-disable-line no-console console.log('\x1b[0m', JSON.stringify(diffs, null, 2)) // eslint-disable-line no-console } } module.exports = { omitDeepLodash, displayDiff, } ================================================ FILE: packages/mjml-parser-xml/test/test-values.js ================================================ /* eslint-disable comma-dangle */ module.exports = [ { test: 'Special characters', mjml: ` &end
Blu & end $1 & lorem
Blu https%3A%2F%2Fmjml.io bla
`, validJson: { "file": ".", "line": 2, "includedIn": [], "tagName": "mjml", "children": [ { "file": ".", "line": 3, "includedIn": [], "tagName": "mj-body", "children": [ { "file": ".", "line": 4, "includedIn": [], "tagName": "mj-section", "attributes": { "background-color": "#CCCCCC", "full-width": "full-width" }, "children": [ { "file": ".", "line": 5, "includedIn": [], "tagName": "mj-button", "attributes": { "href": "<% dynamic %>", "pouf": "$2" }, "content": "&end
\n Blu & end $1\n &\n lorem" }, { "file": ".", "line": 11, "includedIn": [], "tagName": "mj-button", "attributes": { "href": "https://mjml.io?encodedUrl=https%3A%2F%2Fmjml.io&coin=coi" }, "content": "Blu" }, { "file": ".", "line": 14, "includedIn": [], "tagName": "mj-button", "attributes": { "href": "&é(§&è!çà)" }, "content": "https%3A%2F%2Fmjml.io" }, { "file": ".", "line": 15, "includedIn": [], "tagName": "mj-raw", "content": "bla", "attributes": {} } ] } ], "attributes": {} } ], "attributes": {} } }, { test: 'Similar tags', mjml: ` MJML FTW FTW MJML `, validJson: { "file": ".", "line": 2, "includedIn": [], "tagName": "mjml", "children": [ { "file": ".", "line": 3, "includedIn": [], "tagName": "mj-body", "children": [ { "file": ".", "line": 4, "includedIn": [], "tagName": "mj-text-test-wrapper", "children": [ { "file": ".", "line": 5, "includedIn": [], "tagName": "mj-text", "content": "MJML", "attributes": {} }, { "file": ".", "line": 6, "includedIn": [], "tagName": "mj-text", "attributes": { "attr": "val" }, "content": "FTW" } ], "attributes": {} }, { "file": ".", "line": 8, "includedIn": [], "tagName": "mj-text-test-wrapper", "children": [ { "file": ".", "line": 9, "includedIn": [], "tagName": "mj-text", "attributes": { "attr": "val" }, "content": "FTW" }, { "file": ".", "line": 10, "includedIn": [], "tagName": "mj-text", "content": "MJML", "attributes": {} } ], "attributes": {} } ], "attributes": {} } ], "attributes": {} } }, { test: 'Self closing tags', mjml: ` Hello ! `, validJson: { "file": ".", "line": 2, "includedIn": [], "tagName": "mjml", "children": [ { "file": ".", "line": 3, "includedIn": [], "tagName": "mj-head", "children": [ { "file": ".", "line": 4, "includedIn": [], "tagName": "mj-attributes", "children": [ { "file": ".", "line": 5, "includedIn": [], "tagName": "mj-text", "attributes": { "color": "blue" } }, { "file": ".", "line": 6, "includedIn": [], "tagName": "mj-text", "attributes": { "font-size": "40px" } } ], "attributes": {} } ], "attributes": {} }, { "file": ".", "line": 9, "includedIn": [], "tagName": "mj-body", "children": [ { "file": ".", "line": 10, "includedIn": [], "tagName": "mj-section", "children": [ { "file": ".", "line": 11, "includedIn": [], "tagName": "mj-column", "children": [ { "file": ".", "line": 12, "includedIn": [], "tagName": "mj-text", "content": "Hello !", "attributes": {} } ], "attributes": {} } ], "attributes": {} } ], "attributes": {} } ], "attributes": {} } }, // Input that matches most of the CDATAs regex but not all, potentially resulting in regex timeout { test: 'Regex timeout', mjml: ` `, validJson: { "file": ".", "line": 2, "includedIn": [], "tagName": "mj-section", "children": [ { "file": ".", "line": 3, "includedIn": [], "tagName": "mj-text", "attributes": { "font-family": "Arial" } }, { "file": ".", "line": 4, "includedIn": [], "tagName": "mj-column", "attributes": { "background-color": "#ffffff", "css-class": "column1" } } ], "attributes": {} } }, { test: 'Multiline attributes', mjml: ` View blog ]]post `, validJson: { "file": ".", "line": 2, "includedIn": [], "tagName": "mj-text", "attributes": { "padding-left": "16px", "padding-right": "16px" }, "content": "View blog ]]post" } }, { test: 'Self closing Ending Tags', mjml: ` coin Majors and Minors bla Majors and Minors coin `, validJson: { file: '.', line: 2, includedIn: [], tagName: 'mjml', children: [ { file: '.', line: 3, includedIn: [], tagName: 'mj-head', children: [ { file: '.', line: 4, includedIn: [], tagName: 'mj-title', attributes: {} }, { file: '.', line: 5, includedIn: [], tagName: 'mj-attributes', children: [ { file: '.', line: 6, includedIn: [], tagName: 'mj-text', attributes: { 'font-size': '27px' } } ], attributes: {} } ], attributes: {} }, { file: '.', line: 9, includedIn: [], tagName: 'mj-body', children: [ { file: '.', line: 10, includedIn: [], tagName: 'mj-section', children: [ { file: '.', line: 11, includedIn: [], tagName: 'mj-column', attributes: { width: '65%' }, children: [ { file: '.', line: 12, includedIn: [], tagName: 'mj-text', attributes: { 'mj-class': 'small', align: 'left', 'font-family': 'Helvetica', color: '#000000', 'padding-top': '20px' }, content: 'coin\n Majors and Minors\n bla\n Majors and Minors\n \n coin\n ' } ] } ], attributes: {} } ], attributes: {} } ], attributes: {} } }, { test: 'Include', mjml: ` `, validJson: { file: '.', line: 2, includedIn: [], tagName: 'mjml', children: [ { file: '.', line: 3, includedIn: [], tagName: 'mj-body', children: [ { file: '.', line: 4, includedIn: [], tagName: 'mj-section', children: [ { file: '.', line: 1, includedIn: [ { file: '.', line: 5 } ], tagName: 'mj-column', children: [ { file: '.', line: 2, includedIn: [ { file: '.', line: 5 } ], tagName: 'mj-text', attributes: { 'font-size': '22px' }, content: 'COIN\n aze' }, { file: '.', line: 6, includedIn: [ { file: '.', line: 5 } ], tagName: 'mj-text', attributes: { 'font-size': '22px' }, content: 'COIN2\n aze2' } ], attributes: {} } ], attributes: {} } ], attributes: {} } ], attributes: {} } }, { test: 'Single opening tag in endingTag, single and multi-line', mjml: ` `, validJson: { line: 2, includedIn: [], tagName: 'mjml', children: [ { line: 3, includedIn: [], tagName: 'mj-body', children: [ { line: 4, includedIn: [], tagName: 'mj-section', children: [ { line: 5, includedIn: [], tagName: 'mj-column', children: [ { line: 6, includedIn: [], tagName: 'mj-raw', attributes: { test: 'test' }, content: '' }, { line: 7, includedIn: [], tagName: 'mj-raw', content: '', attributes: {} } ], attributes: {} } ], attributes: {} } ], attributes: {} } ], attributes: {} } } ] /* eslint-enable comma-dangle */ ================================================ FILE: packages/mjml-parser-xml/test/test.js ================================================ const MJMLParser = require('../lib/index.js') require('mjml') const components = require('mjml-core').components const chai = require('chai') const displayDiff = require('./test-utils').displayDiff const omitDeepLodash = require('./test-utils').omitDeepLodash const testValues = require('./test-values') /* If test fails, run it with --debug to log the details of the diff */ const parse = mjml => MJMLParser(mjml, { keepComments: true, components, filePath: '.' }) testValues.forEach(testUnit => { const { test, mjml, validJson } = testUnit if (process.argv.indexOf('--debug') !== -1) { displayDiff(omitDeepLodash(validJson, 'file'), omitDeepLodash(parse(mjml), ['absoluteFilePath', 'file'])) } chai.expect(omitDeepLodash(validJson, 'file'), `${test} test failed`) .to.deep.equal(omitDeepLodash(parse(mjml), ['absoluteFilePath', 'file'])) }) ================================================ FILE: packages/mjml-preset-core/README.md ================================================ ## mjml-preset-core ### Installation ```bash npm install --save mjml-preset-core ``` This is the set of mjml components bundled together for simple setup. ### Usage ```javascript import mjml2html from 'mjml-core' import presetCore from 'mjml-preset-core' console.log(mjml2html(`code`, { presets: [presetCore] })) ``` ================================================ FILE: packages/mjml-preset-core/package.json ================================================ { "name": "mjml-preset-core", "description": "mjml-preset-core", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-preset-core" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "mjml-accordion": "4.18.0", "mjml-body": "4.18.0", "mjml-button": "4.18.0", "mjml-carousel": "4.18.0", "mjml-column": "4.18.0", "mjml-divider": "4.18.0", "mjml-group": "4.18.0", "mjml-head": "4.18.0", "mjml-head-attributes": "4.18.0", "mjml-head-breakpoint": "4.18.0", "mjml-head-font": "4.18.0", "mjml-head-html-attributes": "4.18.0", "mjml-head-preview": "4.18.0", "mjml-head-style": "4.18.0", "mjml-head-title": "4.18.0", "mjml-hero": "4.18.0", "mjml-image": "4.18.0", "mjml-navbar": "4.18.0", "mjml-raw": "4.18.0", "mjml-section": "4.18.0", "mjml-social": "4.18.0", "mjml-spacer": "4.18.0", "mjml-table": "4.18.0", "mjml-text": "4.18.0", "mjml-wrapper": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-preset-core/src/dependencies.js ================================================ export default { mjml: ['mj-body', 'mj-head', 'mj-raw'], 'mj-accordion': ['mj-accordion-element', 'mj-raw'], 'mj-accordion-element': ['mj-accordion-title', 'mj-accordion-text', 'mj-raw'], 'mj-accordion-title': [], 'mj-accordion-text': [], 'mj-attributes': [/^.*^/], 'mj-body': ['mj-raw', 'mj-section', 'mj-wrapper', 'mj-hero'], 'mj-button': [], 'mj-carousel': ['mj-carousel-image'], 'mj-carousel-image': [], 'mj-column': [ 'mj-accordion', 'mj-button', 'mj-carousel', 'mj-divider', 'mj-image', 'mj-raw', 'mj-social', 'mj-spacer', 'mj-table', 'mj-text', 'mj-navbar', ], 'mj-html-attribute': [], 'mj-html-attributes': ['mj-selector'], 'mj-divider': [], 'mj-group': ['mj-column', 'mj-raw'], 'mj-head': [ 'mj-attributes', 'mj-breakpoint', 'mj-html-attributes', 'mj-font', 'mj-preview', 'mj-style', 'mj-title', 'mj-raw', ], 'mj-hero': [ 'mj-accordion', 'mj-button', 'mj-carousel', 'mj-divider', 'mj-image', 'mj-social', 'mj-spacer', 'mj-table', 'mj-text', 'mj-navbar', 'mj-raw', ], 'mj-image': [], 'mj-navbar': ['mj-navbar-link', 'mj-raw'], 'mj-raw': [], 'mj-section': ['mj-column', 'mj-group', 'mj-raw'], 'mj-selector': ['mj-html-attribute'], 'mj-social': ['mj-social-element', 'mj-raw'], 'mj-social-element': [], 'mj-spacer': [], 'mj-table': [], 'mj-text': [], 'mj-wrapper': ['mj-hero', 'mj-raw', 'mj-section'], } ================================================ FILE: packages/mjml-preset-core/src/index.js ================================================ import { Social, SocialElement } from 'mjml-social' import { Navbar, NavbarLink } from 'mjml-navbar' import { Carousel, CarouselImage } from 'mjml-carousel' import { Accordion, AccordionElement, AccordionText, AccordionTitle, } from 'mjml-accordion' import Body from 'mjml-body' import Head from 'mjml-head' import HeadAttributes from 'mjml-head-attributes' import HeadBreakpoint from 'mjml-head-breakpoint' import HeadHtmlAttributes from 'mjml-head-html-attributes' import HeadFont from 'mjml-head-font' import HeadPreview from 'mjml-head-preview' import HeadStyle from 'mjml-head-style' import HeadTitle from 'mjml-head-title' import Hero from 'mjml-hero' import Button from 'mjml-button' import Column from 'mjml-column' import Divider from 'mjml-divider' import Group from 'mjml-group' import Image from 'mjml-image' import Raw from 'mjml-raw' import Section from 'mjml-section' import Spacer from 'mjml-spacer' import Text from 'mjml-text' import Table from 'mjml-table' import Wrapper from 'mjml-wrapper' import dependencies from './dependencies' const components = [ Body, Head, HeadAttributes, HeadBreakpoint, HeadHtmlAttributes, HeadFont, HeadPreview, HeadStyle, HeadTitle, Hero, Button, Column, Divider, Group, Image, Raw, Section, Spacer, Text, Table, Wrapper, Social, SocialElement, Navbar, NavbarLink, Accordion, AccordionElement, AccordionText, AccordionTitle, Carousel, CarouselImage, ] const presetCore = { components, dependencies, } export default presetCore ================================================ FILE: packages/mjml-raw/README.md ================================================ ### mj-raw Displays raw HTML that is not parsed by the MJML engine. Anything left inside this tag should be raw, responsive HTML. If placed inside the `mj-head` tag, its content will be added at the end of the HTML `` tag. ```xml ``` You can tell the minifier to ignore some content by wrapping it between two `` tags. You can use `mj-raw` to add a templating language. Note that if you and use the `minify` option, you might get a `Parsing error`, especially when using the `<` character. These can be ignored by using the `` tags.mlmin:ignore -->` tags. ```xml {% if foo < 5 %} {% endif %} ``` You can also use `mj-raw` to add text at the beginning of the generated html, before the `` line. For this you need to: - add the `mj-raw` tag inside the `mjml` tag, outside of the `mj-head` and `mj-body` tags. - add the `position="file-start"` attribute to the `mj-raw` tag: Note that if you put multiple lines in this `mj-raw` and use the minify option, these lines will be joined into a single line by the minifier. These can be ignored by using the `` tags. ```xml This will be added at the beginning of the file ```

Try it live

================================================ FILE: packages/mjml-raw/package.json ================================================ { "name": "mjml-raw", "description": "mjml-raw", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-raw" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-raw/src/index.js ================================================ import { BodyComponent } from 'mjml-core' export default class MjRaw extends BodyComponent { static componentName = 'mj-raw' static endingTag = true static rawElement = true static allowedAttributes = { position: 'enum(file-start)', } render() { return this.getContent() } } ================================================ FILE: packages/mjml-section/README.md ================================================ ### mj-section Sections are rows within your email. They will be used to structure the layout. ```xml ``` The `full-width` attribute will be used to manage the background width. Setting it will change the width of the section from the default 600px to 100%. #### Attributes | attribute | accepts | description | default value | | --------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------ | ------------- | | background-color | CSS color formats | section color | | | background-position | string | CSS values, i.e. `left` `center` `right` + `top` `center` `bottom`
(see outlook limitations below) | `top center` | | background-position-x | string | CSS values, i.e. `left` `center` `right`
(see outlook limitations below) | | | background-position-y | string | CSS values, i.e. `top` `center` `bottom`
(see outlook limitations below) | | | background-repeat | `repeat` `no-repeat` | set the background image to repeat | | background-size | string | CSS values e.g. `auto` `cover` `contain` `px` `%` size | `auto` | | background-url | string | background image, in URL format | | | border | string | CSS border format | | | border-bottom | string | CSS border format | | | border-left | string | CSS border format | | | border-radius | string | border radius | | | border-right | string | CSS border format | | | border-top | string | CSS border format | | | css-class | string | class name, added to the root HTML element created | | | direction | `ltr` `rtl` | set the display order of direct children | `ltr` | | full-width | `full-width` `false` | make the section full-width | | | padding | `px` `%` | section padding, supports up to 4 parameters | `20px 0` | | padding-bottom | `px` `%` | section bottom padding | | | padding-left | `px` `%` | section left padding | | | padding-right | `px` `%` | section right padding | | | padding-top | `px` `%` | section top padding | | | text-align | `left` `center` `right` | CSS text-align | `center` |

Try it live

================================================ FILE: packages/mjml-section/package.json ================================================ { "name": "mjml-section", "description": "mjml-section", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-section" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-section/src/index.js ================================================ import { BodyComponent, suffixCssClasses } from 'mjml-core' import { flow, identity, join, filter } from 'lodash/fp' const makeBackgroundString = flow(filter(identity), join(' ')) export default class MjSection extends BodyComponent { static componentName = 'mj-section' static allowedAttributes = { 'background-color': 'color', 'background-url': 'string', 'background-repeat': 'enum(repeat,no-repeat)', 'background-size': 'string', 'background-position': 'string', 'background-position-x': 'string', 'background-position-y': 'string', border: 'string', 'border-bottom': 'string', 'border-left': 'string', 'border-radius': 'string', 'border-right': 'string', 'border-top': 'string', direction: 'enum(ltr,rtl)', 'full-width': 'enum(full-width,false,)', padding: 'unit(px,%){1,4}', 'padding-top': 'unit(px,%)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'text-align': 'enum(left,center,right)', 'text-padding': 'unit(px,%){1,4}', } static defaultAttributes = { 'background-repeat': 'repeat', 'background-size': 'auto', 'background-position': 'top center', direction: 'ltr', padding: '20px 0', 'text-align': 'center', 'text-padding': '4px 4px 4px 0', } getChildContext() { const { box } = this.getBoxWidths() return { ...this.context, containerWidth: `${box}px`, gap: this.getAttribute('gap'), } } getStyles() { const { containerWidth } = this.context const fullWidth = this.isFullWidth() const hasBorderRadius = this.hasBorderRadius() const isFirstSection = this.props.index === 0 const background = this.getAttribute('background-url') ? { background: this.getBackground(), // background size, repeat and position has to be seperate since yahoo does not support shorthand background css property 'background-position': this.getBackgroundString(), 'background-repeat': this.getAttribute('background-repeat'), 'background-size': this.getAttribute('background-size'), } : { background: this.getAttribute('background-color'), 'background-color': this.getAttribute('background-color'), } return { tableFullwidth: { ...(fullWidth ? background : {}), width: '100%', }, table: { ...(fullWidth ? {} : background), width: '100%', ...(hasBorderRadius && { 'border-collapse': 'separate' }), }, td: { border: this.getAttribute('border'), 'border-bottom': this.getAttribute('border-bottom'), 'border-left': this.getAttribute('border-left'), 'border-right': this.getAttribute('border-right'), 'border-top': this.getAttribute('border-top'), 'border-radius': this.getAttribute('border-radius'), direction: this.getAttribute('direction'), 'font-size': '0px', padding: this.getAttribute('padding'), 'padding-bottom': this.getAttribute('padding-bottom'), 'padding-left': this.getAttribute('padding-left'), 'padding-right': this.getAttribute('padding-right'), 'padding-top': this.getAttribute('padding-top'), 'text-align': this.getAttribute('text-align'), }, div: { ...(fullWidth ? {} : background), margin: '0px auto', 'max-width': containerWidth, 'border-radius': this.getAttribute('border-radius'), ...(hasBorderRadius && { overflow: 'hidden' }), 'margin-top': !isFirstSection ? this.context.gap : undefined, }, innerDiv: { 'line-height': '0', 'font-size': '0', }, } } getBackground() { return makeBackgroundString([ this.getAttribute('background-color'), ...(this.hasBackground() ? [ `url('${this.getAttribute('background-url')}')`, this.getBackgroundString(), `/ ${this.getAttribute('background-size')}`, this.getAttribute('background-repeat'), ] : []), ]) } getBackgroundString() { const { posX, posY } = this.getBackgroundPosition() return `${posX} ${posY}` } getBackgroundPosition() { const { x, y } = this.parseBackgroundPosition() return { posX: this.getAttribute('background-position-x') || x, posY: this.getAttribute('background-position-y') || y, } } parseBackgroundPosition() { const posSplit = this.getAttribute('background-position').split(' ') if (posSplit.length === 1) { const val = posSplit[0] // here we must determine if x or y was provided ; other will be center if (['top', 'bottom'].includes(val)) { return { x: 'center', y: val, } } return { x: val, y: 'center', } } if (posSplit.length === 2) { // x and y can be put in any order in background-position so we need to determine that based on values const val1 = posSplit[0] const val2 = posSplit[1] if ( ['top', 'bottom'].includes(val1) || (val1 === 'center' && ['left', 'right'].includes(val2)) ) { return { x: val2, y: val1, } } return { x: val1, y: val2, } } // more than 2 values is not supported, let's treat as default value return { x: 'center', y: 'top' } } hasBackground() { return this.getAttribute('background-url') != null } isFullWidth() { return this.getAttribute('full-width') === 'full-width' } hasBorderRadius() { const borderRadius = this.getAttribute('border-radius') return borderRadius !== '' && typeof borderRadius !== 'undefined' } hasGap() { const { gap } = this.context return gap != null && gap !== '' } renderBefore() { const { containerWidth } = this.context const bgcolorAttr = this.getAttribute('background-color') ? { bgcolor: this.getAttribute('background-color') } : {} const isFirstSection = this.props.index === 0 const hasGap = this.hasGap() return ` ` } // eslint-disable-next-line class-methods-use-this renderAfter() { return ` ` } renderWrappedChildren() { const { children } = this.props return ` ${this.renderChildren(children, { renderer: (component) => component.constructor.isRawElement() ? component.render() : ` ${component.render()} `, })} ` } renderWithBackground(content) { const fullWidth = this.isFullWidth() const { containerWidth } = this.context const isPercentage = (str) => /^\d+(\.\d+)?%$/.test(str) let vSizeAttributes = {} let { posX: bgPosX, posY: bgPosY } = this.getBackgroundPosition() switch (bgPosX) { case 'left': bgPosX = '0%' break case 'center': bgPosX = '50%' break case 'right': bgPosX = '100%' break default: if (!isPercentage(bgPosX)) { bgPosX = '50%' } break } switch (bgPosY) { case 'top': bgPosY = '0%' break case 'center': bgPosY = '50%' break case 'bottom': bgPosY = '100%' break default: if (!isPercentage(bgPosY)) { bgPosY = '0%' } break } // this logic is different when using repeat or no-repeat let [[vOriginX, vPosX], [vOriginY, vPosY]] = ['x', 'y'].map( (coordinate) => { const isX = coordinate === 'x' const bgRepeat = this.getAttribute('background-repeat') === 'repeat' let pos = isX ? bgPosX : bgPosY let origin = isX ? bgPosX : bgPosY if (isPercentage(pos)) { // Should be percentage at this point const percentageValue = pos.match(/^(\d+(\.\d+)?)%$/)[1] const decimal = parseInt(percentageValue, 10) / 100 if (bgRepeat) { pos = decimal origin = decimal } else { pos = (-50 + decimal * 100) / 100 origin = (-50 + decimal * 100) / 100 } } else if (bgRepeat) { // top (y) or center (x) origin = isX ? '0.5' : '0' pos = isX ? '0.5' : '0' } else { origin = isX ? '0' : '-0.5' pos = isX ? '0' : '-0.5' } return [origin, pos] }, this, ) // If background size is either cover or contain, we tell VML to keep the aspect // and fill the entire element. if ( this.getAttribute('background-size') === 'cover' || this.getAttribute('background-size') === 'contain' ) { vSizeAttributes = { size: '1,1', aspect: this.getAttribute('background-size') === 'cover' ? 'atleast' : 'atmost', } } else if (this.getAttribute('background-size') !== 'auto') { const bgSplit = this.getAttribute('background-size').split(' ') if (bgSplit.length === 1) { vSizeAttributes = { size: this.getAttribute('background-size'), aspect: 'atmost', // reproduces height auto } } else { vSizeAttributes = { size: bgSplit.join(','), } } } let vmlType = this.getAttribute('background-repeat') === 'no-repeat' ? 'frame' : 'tile' if (this.getAttribute('background-size') === 'auto') { vmlType = 'tile' // if no size provided, keep old behavior because outlook can't use original image size with "frame" ;[[vOriginX, vPosX], [vOriginY, vPosY]] = [ [0.5, 0.5], [0, 0], ] // also ensure that images are still cropped the same way } return ` ${content} ` } renderSection() { const hasBackground = this.hasBackground() return `
${ hasBackground ? `
` : '' }
${this.renderWrappedChildren()}
${hasBackground ? '
' : ''}
` } renderFullWidth() { const content = this.hasBackground() ? this.renderWithBackground(` ${this.renderBefore()} ${this.renderSection()} ${this.renderAfter()} `) : ` ${this.renderBefore()} ${this.renderSection()} ${this.renderAfter()} ` return `
${content}
` } renderSimple() { const section = this.renderSection() return ` ${this.renderBefore()} ${this.hasBackground() ? this.renderWithBackground(section) : section} ${this.renderAfter()} ` } render() { return this.isFullWidth() ? this.renderFullWidth() : this.renderSimple() } } ================================================ FILE: packages/mjml-social/README.md ================================================ ### mj-social Displays calls-to-action for various social networks with their associated logo. You can add multiple social networks using `mj-social-element` tags.
desktop
```xml Facebook Google Twitter X ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ----------------------- | -------------------------------------------------- | -------------------------------------- | | align | `left` `right` `center` | align content | `center` | | border-radius | `px` `%` | border radius | `3px` | | color | CSS color formats | text color | `#333333` | | css-class | string | class name, added to the root HTML element created | | | container-background-color | CSS color formats | inner element background color | | | font-family | string | font name | `Ubuntu, Helvetica, Arial, sans-serif` | | font-size | `px` | font size | `13px` | | font-style | string | font style | normal | | font-weight | string | font weight | normal | | icon-height | `px` `%` | icon height, overrides `icon-size` | icon-size | | icon-padding | `px` `%` | padding around the icons | | | icon-size | `px` `%` | icon size (width and height) | `20px` | | inner-padding | `px` `%` | social network surrounding padding | `null` | | line-height | `px` `%` | space between lines | `22px` | | mode | `horizontal` `vertical` | direction of social elements | `horizontal` | | padding | `px` `%` | social padding, supports up to 4 parameters | `10px 25px` | | padding-bottom | `px` `%` | bottom padding | | | padding-left | `px` `%` | left padding | | | padding-right | `px` `%` | right padding | | | padding-top | `px` `%` | top padding | | | text-padding | `px` `%` | padding around the text | | | text-decoration | string | CSS values, e.g. `underline` `overline` `none` | `none` |

Try it live

#### mj-social-element This component enables you to display a given social network inside `mj-social`. Note that default icons are transparent, which allows `background-color` to actually be the icon color. #### Attributes | attribute | accepts | description | default value | | ---------------- | ----------------------- | ------------------------------------------------------------------------------- | -------------------------------------- | | align | `left` `center` `right` | align content | `center` | | alt | string | image alt attribute | `''` | | background-color | CSS color formats | icon color | Each social `name` has its own default | | border-radius | `px` | border radius | `3px` | | color | CSS color formats | text color | `#000` | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font name | `Ubuntu, Helvetica, Arial, sans-serif` | | font-size | `px` | font size | `13px` | | font-style | string | font style | | | font-weight | string | font weight | | | href | string | button redirection, in URL format | | | icon-height | percent/px | icon height, overrides icon-size | `icon-size` | | icon-padding | `px` `%` | padding around the icon | | | icon-position | `left` `right` | sets the side of the icon | | | icon-size | `px` `%` | icon size (width and height) | | | line-height | `px` `%` | space between lines | `1` | | name | string | social network name, see supported list below | | | padding | `px` `%` | social element padding, supports up to 4 parameters | `4px` | | padding-bottom | `px` `%` | bottom padding | | | padding-left | `px` `%` | left padding | | | padding-right | `px` `%` | right padding | | | padding-top | `px` `%` | top padding | | | rel | string | specify the rel attribute for the link | | | sizes | string | set icon width based on query | | | src | string | image source, in URL format | Each social `name` has its own default | | srcset | string | enables to set a different image source based on the viewport, using CSS syntax | | | target | string | link target | `_blank` | | text-decoration | string | CSS values, e.g. `underline` `overline` `none` | `none` | | text-padding | `px` `%` | padding around the text | `4px 4px 4px 0` | | title | string | image title attribute | | | vertical-align | `top` `middle` `bottom` | vertically align elements | `middle` | Supported networks with a share url: - `facebook` - `twitter` - `x` - `google` - `pinterest` - `linkedin` - `tumblr` - `xing` Without a share url: - `github` - `instagram` - `web` - `snapchat` - `youtube` - `vimeo` - `medium` - `soundcloud` - `dribbble` When using a network with share url, the `href` attribute will be inserted in the share url (i.e. `https://www.facebook.com/sharer/sharer.php?u=[[URL]]`). To keep your `href` unchanged, add `-noshare` to the network name. Example : `Twitter` #### Custom Social Element You can add any unsupported network like this: ```xml Optional label ``` You can also use mj-social this way with no `href` attribute to make a simple list of inlined images-texts. ================================================ FILE: packages/mjml-social/package.json ================================================ { "name": "mjml-social", "description": "mjml-social", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-social" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-social/src/Social.js ================================================ import { BodyComponent } from 'mjml-core' import { isNil } from 'lodash' export default class MjSocial extends BodyComponent { static componentName = 'mj-social' static allowedAttributes = { align: 'enum(left,right,center)', 'border-radius': 'unit(px,%)', 'container-background-color': 'color', color: 'color', 'font-family': 'string', 'font-size': 'unit(px)', 'font-style': 'string', 'font-weight': 'string', 'icon-size': 'unit(px,%)', 'icon-height': 'unit(px,%)', 'icon-padding': 'unit(px,%){1,4}', 'inner-padding': 'unit(px,%){1,4}', 'line-height': 'unit(px,%,)', mode: 'enum(horizontal,vertical)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', 'table-layout': 'enum(auto,fixed)', 'text-padding': 'unit(px,%){1,4}', 'text-decoration': 'string', 'vertical-align': 'enum(top,bottom,middle)', } static defaultAttributes = { align: 'center', 'border-radius': '3px', color: '#333333', 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'icon-size': '20px', 'inner-padding': null, 'line-height': '22px', mode: 'horizontal', padding: '10px 25px', 'text-decoration': 'none', } // eslint-disable-next-line class-methods-use-this getStyles() { return { tableVertical: { margin: '0px', }, } } getSocialElementAttributes() { const base = {} if (this.getAttribute('inner-padding')) { base.padding = this.getAttribute('inner-padding') } return [ 'border-radius', 'color', 'font-family', 'font-size', 'font-weight', 'font-style', 'icon-size', 'icon-height', 'icon-padding', 'text-padding', 'line-height', 'text-decoration', ] .filter((e) => !isNil(this.getAttribute(e))) .reduce((res, attr) => { res[attr] = this.getAttribute(attr) return res }, base) } renderHorizontal() { const { children } = this.props return ` ${this.renderChildren(children, { attributes: this.getSocialElementAttributes(), renderer: (component) => component.constructor.isRawElement() ? component.render() : ` ${component.render()}
`, })} ` } renderVertical() { const { children } = this.props return ` ${this.renderChildren(children, { attributes: this.getSocialElementAttributes(), })}
` } render() { return ` ${ this.getAttribute('mode') === 'horizontal' ? this.renderHorizontal() : this.renderVertical() } ` } } ================================================ FILE: packages/mjml-social/src/SocialElement.js ================================================ import { BodyComponent } from 'mjml-core' import { get, each } from 'lodash' const IMG_BASE_URL = 'https://www.mailjet.com/images/theme/v1/icons/ico-social/' const defaultSocialNetworks = { facebook: { 'share-url': 'https://www.facebook.com/sharer/sharer.php?u=[[URL]]', 'background-color': '#3b5998', src: `${IMG_BASE_URL}facebook.png`, }, twitter: { 'share-url': 'https://twitter.com/intent/tweet?url=[[URL]]', 'background-color': '#55acee', src: `${IMG_BASE_URL}twitter.png`, }, x: { 'share-url': 'https://twitter.com/intent/tweet?url=[[URL]]', 'background-color': '#000000', src: `${IMG_BASE_URL}twitter-x.png`, }, google: { 'share-url': 'https://plus.google.com/share?url=[[URL]]', 'background-color': '#dc4e41', src: `${IMG_BASE_URL}google-plus.png`, }, pinterest: { 'share-url': 'https://pinterest.com/pin/create/button/?url=[[URL]]&media=&description=', 'background-color': '#bd081c', src: `${IMG_BASE_URL}pinterest.png`, }, linkedin: { 'share-url': 'https://www.linkedin.com/shareArticle?mini=true&url=[[URL]]&title=&summary=&source=', 'background-color': '#0077b5', src: `${IMG_BASE_URL}linkedin.png`, }, instagram: { 'background-color': '#3f729b', src: `${IMG_BASE_URL}instagram.png`, }, web: { src: `${IMG_BASE_URL}web.png`, 'background-color': '#4BADE9', }, snapchat: { src: `${IMG_BASE_URL}snapchat.png`, 'background-color': '#FFFA54', }, youtube: { src: `${IMG_BASE_URL}youtube.png`, 'background-color': '#EB3323', }, tumblr: { src: `${IMG_BASE_URL}tumblr.png`, 'share-url': 'https://www.tumblr.com/widgets/share/tool?canonicalUrl=[[URL]]', 'background-color': '#344356', }, github: { src: `${IMG_BASE_URL}github.png`, 'background-color': '#000000', }, xing: { src: `${IMG_BASE_URL}xing.png`, 'share-url': 'https://www.xing.com/app/user?op=share&url=[[URL]]', 'background-color': '#296366', }, vimeo: { src: `${IMG_BASE_URL}vimeo.png`, 'background-color': '#53B4E7', }, medium: { src: `${IMG_BASE_URL}medium.png`, 'background-color': '#000000', }, soundcloud: { src: `${IMG_BASE_URL}soundcloud.png`, 'background-color': '#EF7F31', }, dribbble: { src: `${IMG_BASE_URL}dribbble.png`, 'background-color': '#D95988', }, } each(defaultSocialNetworks, (val, key) => { defaultSocialNetworks[`${key}-noshare`] = { ...val, 'share-url': '[[URL]]', } }) export default class MjSocialElement extends BodyComponent { static componentName = 'mj-social-element' static endingTag = true static allowedAttributes = { align: 'enum(left,center,right)', 'icon-position': 'enum(left,right)', 'background-color': 'color', color: 'color', 'border-radius': 'unit(px)', 'font-family': 'string', 'font-size': 'unit(px)', 'font-style': 'string', 'font-weight': 'string', href: 'string', 'icon-size': 'unit(px,%)', 'icon-height': 'unit(px,%)', 'icon-padding': 'unit(px,%){1,4}', 'line-height': 'unit(px,%,)', name: 'string', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', 'text-padding': 'unit(px,%){1,4}', rel: 'string', src: 'string', srcset: 'string', sizes: 'string', alt: 'string', title: 'string', target: 'string', 'text-decoration': 'string', 'vertical-align': 'enum(top,middle,bottom)', } static defaultAttributes = { alt: '', align: 'left', 'icon-position': 'left', color: '#000', 'border-radius': '3px', 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'line-height': '1', padding: '4px', 'text-padding': '4px 4px 4px 0', target: '_blank', 'text-decoration': 'none', 'vertical-align': 'middle', } getStyles() { const { 'icon-size': iconSize, 'icon-height': iconHeight, 'background-color': backgroundColor, } = this.getSocialAttributes() return { td: { padding: this.getAttribute('padding'), 'padding-top': this.getAttribute('padding-top'), 'padding-right': this.getAttribute('padding-right'), 'padding-bottom': this.getAttribute('padding-bottom'), 'padding-left': this.getAttribute('padding-left'), 'vertical-align': this.getAttribute('vertical-align'), }, table: { background: backgroundColor, 'border-radius': this.getAttribute('border-radius'), width: iconSize, }, icon: { padding: this.getAttribute('icon-padding'), 'font-size': '0', height: iconHeight || iconSize, 'vertical-align': 'middle', width: iconSize, }, img: { 'border-radius': this.getAttribute('border-radius'), display: 'block', }, tdText: { 'vertical-align': 'middle', padding: this.getAttribute('text-padding'), 'text-align': this.getAttribute('align'), }, text: { color: this.getAttribute('color'), 'font-size': this.getAttribute('font-size'), 'font-weight': this.getAttribute('font-weight'), 'font-style': this.getAttribute('font-style'), 'font-family': this.getAttribute('font-family'), 'line-height': this.getAttribute('line-height'), 'text-decoration': this.getAttribute('text-decoration'), }, } } getSocialAttributes() { const socialNetwork = defaultSocialNetworks[this.getAttribute('name')] || {} let href = this.getAttribute('href') if (href && get(socialNetwork, 'share-url')) { href = socialNetwork['share-url'].replace('[[URL]]', href) } const attrs = [ 'icon-size', 'icon-height', 'srcset', 'sizes', 'src', 'background-color', ].reduce( (r, attr) => ({ ...r, [attr]: this.getAttribute(attr) || socialNetwork[attr], }), {}, ) return { href, ...attrs, } } render() { const { src, srcset, sizes, href, 'icon-size': iconSize, } = this.getSocialAttributes() const hasLink = !!this.getAttribute('href') const iconPosition = this.getAttribute('icon-position') const makeIcon = () => `
${ hasLink ? `` : '' } ${hasLink ? `` : ''}
` const makeContent = () => ` ${ this.getContent() ? ` ${ hasLink ? `` : `` } ${this.getContent()} ${hasLink ? `` : ''} ` : '' } ` const renderLeft = () => `${makeIcon()} ${makeContent()}` const renderRight = () => `${makeContent()} ${makeIcon()}` return ` ${iconPosition === 'left' ? renderLeft() : renderRight()} ` } } ================================================ FILE: packages/mjml-social/src/index.js ================================================ export { default as Social } from './Social' export { default as SocialElement } from './SocialElement' ================================================ FILE: packages/mjml-spacer/README.md ================================================ ### mj-spacer Displays a blank space, that can be used to separate content. ```xml A first line of text A second line of text ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ----------------- | -------------------------------------------------- | ------------- | | container-background-color | CSS color formats | inner element background color | | | css-class | string | class name, added to the root HTML element created | | | height | `px` `%` | spacer height | `0px` | | padding | `px` `%` | spacer padding, supports up to 4 parameters | | | padding-bottom | `px` `%` | bottom padding | | | padding-left | `px` `%` | left padding | | | padding-right | `px` `%` | right padding | | | padding-top | `px` `%` | top padding | |

Try it live

================================================ FILE: packages/mjml-spacer/package.json ================================================ { "name": "mjml-spacer", "description": "mjml-spacer", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-spacer" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-spacer/src/index.js ================================================ import { BodyComponent } from 'mjml-core' export default class MjSpacer extends BodyComponent { static componentName = 'mj-spacer' static allowedAttributes = { border: 'string', 'border-bottom': 'string', 'border-left': 'string', 'border-right': 'string', 'border-top': 'string', 'container-background-color': 'color', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', height: 'unit(px,%)', } static defaultAttributes = { height: '20px', } getStyles() { return { div: { height: this.getAttribute('height'), 'line-height': this.getAttribute('height'), }, } } render() { return `
` } } ================================================ FILE: packages/mjml-table/README.md ================================================ ### mj-table Display a data table. It only accepts plain HTML. ```xml Year Language Inspired from 1995 PHP C, Shell Unix 1995 JavaScript Scheme, Self ``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | ---------------------------------- | -------------------------------------------------- | -------------------------------------- | | align | `left` `right` `center` | table horizontal alignment | `left` | | border | string | CSS border format | `none` | | cellpadding | integer | space between cells | `0` | | cellspacing | integer | space between cell and border | `0` | | color | CSS color formats | text header & footer color | `#000000` | | container-background-color | CSS color formats | inner element background color | | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font name | `Ubuntu, Helvetica, Arial, sans-serif` | | font-size | `px` | font size | `13px` | | line-height | `px` `%` | space between lines | `22px` | | padding | `px` `%` | outer table padding, supports up to 4 parameters | `10px 25px` | | padding-bottom | `px` `%` | bottom padding | | | padding-left | `px` `%` | left padding | | | padding-right | `px` `%` | right padding | | | padding-top | `px` `%` | top padding | | | role | `none` `presentation` | specify the role attribute | | | table-layout | `auto` `fixed` `initial` `inherit` | sets the table layout | `auto` | | width | `px` `%` `auto` | table width | `100%` |

Try it live

================================================ FILE: packages/mjml-table/package.json ================================================ { "name": "mjml-table", "description": "mjml-atable", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-table" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-table/src/index.js ================================================ import widthParser from 'mjml-core/lib/helpers/widthParser' import { BodyComponent } from 'mjml-core' import { reduce } from 'lodash' export default class MjTable extends BodyComponent { static componentName = 'mj-table' static endingTag = true static allowedAttributes = { align: 'enum(left,right,center)', border: 'string', cellpadding: 'integer', cellspacing: 'integer', 'container-background-color': 'color', color: 'color', 'font-family': 'string', 'font-size': 'unit(px)', 'font-weight': 'string', 'line-height': 'unit(px,%,)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', role: 'enum(none,presentation)', 'table-layout': 'enum(auto,fixed,initial,inherit)', 'vertical-align': 'enum(top,bottom,middle)', width: 'unit(px,%,auto)', } static defaultAttributes = { align: 'left', border: 'none', cellpadding: '0', cellspacing: '0', color: '#000000', 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'line-height': '22px', padding: '10px 25px', 'table-layout': 'auto', width: '100%', } getStyles() { const hasCellspacing = this.hasCellspacing() return { table: { color: this.getAttribute('color'), 'font-family': this.getAttribute('font-family'), 'font-size': this.getAttribute('font-size'), 'line-height': this.getAttribute('line-height'), 'table-layout': this.getAttribute('table-layout'), width: this.getAttribute('width'), border: this.getAttribute('border'), ...(hasCellspacing && { 'border-collapse': 'separate' }), }, } } getWidth() { const width = this.getAttribute('width') if (width === 'auto') { return width } const { parsedWidth, unit } = widthParser(width) return unit === '%' ? width : parsedWidth } hasCellspacing() { const cellspacing = this.getAttribute('cellspacing') const numericValue = parseFloat(String(cellspacing).replace(/[^\d.]/g, '')) return !Number.isNaN(numericValue) && numericValue > 0 } render() { const tableAttributes = reduce( ['cellpadding', 'cellspacing', 'role'], (acc, v) => ({ ...acc, [v]: this.getAttribute(v), }), {}, ) return ` ${this.getContent()}
` } } ================================================ FILE: packages/mjml-text/README.md ================================================ ### mj-text Displays text which can be styled. ```xml

Title

Paragraph

Another paragraph

``` #### Attributes | attribute | accepts | description | default value | | -------------------------- | --------------------------------- | ------------------------------------------------------------ | -------------------------------------- | | align | `left` `right` `center` `justify` | text-alignment | `left` | | color | CSS color formats | text color | `#000000` | | container-background-color | CSS color formats | inner element background color | | | css-class | string | class name, added to the root HTML element created | | | font-family | string | font | `Ubuntu, Helvetica, Arial, sans-serif` | | font-size | `px` | text size | `13px` | | font-style | string | CSS values, e.g. `normal` `italic` `oblique` | | | font-weight | string | text thickness | | | height | `px` | height of the element | | | letter-spacing | `px` `em` | letter spacing | | | line-height | `px` `%` | space between the lines | `1` | | padding | `px` `%` | text padding, supports up to 4 parameters | `10px 25px` | | padding-bottom | `px` `%` | bottom offset | | | padding-left | `px` `%` | left offset | | | padding-right | `px` `%` | right offset | | | padding-top | `px` `%` | top offset | | | text-decoration | string | CSS values, e.g. `underline` `overline` `none` | | text-transform | string | CSS values, i.e. `capitalize` `uppercase` `lowercase` `none` | |

Try it live

================================================ FILE: packages/mjml-text/package.json ================================================ { "name": "mjml-text", "description": "mjml-text", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-text" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-text/src/index.js ================================================ import { BodyComponent } from 'mjml-core' import conditionalTag from 'mjml-core/lib/helpers/conditionalTag' export default class MjText extends BodyComponent { static componentName = 'mj-text' static endingTag = true static allowedAttributes = { align: 'enum(left,right,center,justify)', 'background-color': 'color', color: 'color', 'container-background-color': 'color', 'font-family': 'string', 'font-size': 'unit(px)', 'font-style': 'string', 'font-weight': 'string', height: 'unit(px,%)', 'letter-spacing': 'unitWithNegative(px,em)', 'line-height': 'unit(px,%,)', 'padding-bottom': 'unit(px,%)', 'padding-left': 'unit(px,%)', 'padding-right': 'unit(px,%)', 'padding-top': 'unit(px,%)', padding: 'unit(px,%){1,4}', 'text-decoration': 'string', 'text-transform': 'string', 'vertical-align': 'enum(top,bottom,middle)', } static defaultAttributes = { align: 'left', color: '#000000', 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', 'font-size': '13px', 'line-height': '1', padding: '10px 25px', } getStyles() { return { text: { 'font-family': this.getAttribute('font-family'), 'font-size': this.getAttribute('font-size'), 'font-style': this.getAttribute('font-style'), 'font-weight': this.getAttribute('font-weight'), 'letter-spacing': this.getAttribute('letter-spacing'), 'line-height': this.getAttribute('line-height'), 'text-align': this.getAttribute('align'), 'text-decoration': this.getAttribute('text-decoration'), 'text-transform': this.getAttribute('text-transform'), color: this.getAttribute('color'), height: this.getAttribute('height'), }, } } renderContent() { return `
${this.getContent()}
` } render() { const height = this.getAttribute('height') return height ? ` ${conditionalTag(`
`)} ${this.renderContent()} ${conditionalTag(`
`)} ` : this.renderContent() } } ================================================ FILE: packages/mjml-validator/README.md ================================================ ## Validating MJML 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`: your document will go 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 By default, the level is set to `soft`. ### In CLI When using the `mjml` command line, you can add the option `-c.validationLevel` or `--config.validationLevel` with the validation level you want. > Set the validation level to `skip` (so that the file is not validated) and render the file ```bash mjml --config.validationLevel=skip template.mjml ``` Alternatively, you can validate the file without rendering by adding the `--validate` option ```bash mjml --validate template.mjml ``` ### In Javascript In Javascript, you can provide the level through the `options` parameters on `mjml2html`. E.g. `mjml2html(inputMJML, { validationLevel: 'strict' })` Setting to `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 setting to `soft`, no exception will be raised. You can get the errors in the object returned by `mjml2html`. It is the same object returned by `getErrors` on strict mode. ================================================ FILE: packages/mjml-validator/package.json ================================================ { "name": "mjml-validator", "description": "mjml-validator", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-validator" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-validator/src/MJMLRulesCollection.js ================================================ import validAttributes from './rules/validAttributes' import validChildren from './rules/validChildren' import validTag from './rules/validTag' import validTypes from './rules/validTypes' import errorAttr from './rules/errorAttr' const MJMLRulesCollection = { validAttributes, validChildren, validTag, validTypes, errorAttr, } export function registerRule(rule, name) { if (typeof rule !== 'function') { return console.error('Your rule must be a function') } if (name) { MJMLRulesCollection[name] = rule } else { MJMLRulesCollection[rule.name] = rule } return true } export default MJMLRulesCollection ================================================ FILE: packages/mjml-validator/src/dependencies.js ================================================ export const assignDependencies = (target, ...sources) => { if (sources.length === 0) { return target } for (const source of sources) { if (typeof source === 'object' && source !== null) { for (const tag of Object.keys(source)) { if (typeof tag === 'string') { const list = [] if (target[tag]) { list.push(...target[tag]) } if (source[tag]) { list.push(...source[tag]) } target[tag] = Array.from(new Set(list)) } else { console.warn('dependency "tag" must be of type string') } } } else { console.warn('"dependencies" must be an object.') } } return target } const dependencies = {} export const registerDependencies = (dep) => { assignDependencies(dependencies, dep) } export default dependencies ================================================ FILE: packages/mjml-validator/src/index.js ================================================ import ruleError from './rules/ruleError' import rulesCollection, { registerRule } from './MJMLRulesCollection' import dependencies, { registerDependencies, assignDependencies, } from './dependencies' const SKIP_ELEMENTS = ['mjml'] export const formatValidationError = ruleError export { rulesCollection, registerRule } export { dependencies, registerDependencies, assignDependencies } export default function MJMLValidator(element, options = {}) { const { children, tagName } = element const errors = [] const skipElements = options.skipElements || SKIP_ELEMENTS if (options.dependencies == null) { console.warn('"dependencies" option should be provided to mjml validator') } if (!skipElements.includes(tagName)) { for (const rule of Object.values(rulesCollection)) { const ruleError = rule(element, { dependencies, skipElements, ...options, }) if (Array.isArray(ruleError)) { errors.push(...ruleError) } else if (ruleError) { errors.push(ruleError) } } } if (children && children.length > 0) { for (const child of children) { errors.push(...MJMLValidator(child, options)) } } return errors } ================================================ FILE: packages/mjml-validator/src/rules/errorAttr.js ================================================ import ruleError from './ruleError' export default function errorAttr(element) { const { errors } = element if (!errors) return null return errors.map((error) => { switch (error.type) { case 'include': { const { file, partialPath } = error.params return ruleError( `mj-include fails to read file : ${file} at ${partialPath}`, element, ) } default: return null } }) } ================================================ FILE: packages/mjml-validator/src/rules/ruleError.js ================================================ function formatInclude(element) { const { includedIn } = element if (!(includedIn && includedIn.length)) return '' const formattedIncluded = includedIn .slice() .reverse() .map(({ line, file }) => `line ${line} of file ${file}`) .join(', itself included at ') return `, included at ${formattedIncluded}` } export default function ruleError(message, element) { const { line, tagName, absoluteFilePath } = element return { line, message, tagName, formattedMessage: `Line ${line} of ${absoluteFilePath}${formatInclude( element, )} (${tagName}) — ${message}`, } } ================================================ FILE: packages/mjml-validator/src/rules/validAttributes.js ================================================ import ruleError from './ruleError' const WHITELIST = ['mj-class', 'css-class'] export default function validateAttribute(element, { components }) { const { attributes, tagName } = element const Component = components[tagName] if (!Component) { return null } const availableAttributes = [ ...Object.keys(Component.allowedAttributes || {}), ...WHITELIST, ] const unknownAttributes = Object.keys(attributes || {}).filter( (attribute) => !availableAttributes.includes(attribute), ) if (unknownAttributes.length === 0) { return null } const { attribute, illegal } = { attribute: unknownAttributes.length > 1 ? 'Attributes' : 'Attribute', illegal: unknownAttributes.length > 1 ? 'are illegal' : 'is illegal', } return ruleError( `${attribute} ${unknownAttributes.join(', ')} ${illegal}`, element, ) } ================================================ FILE: packages/mjml-validator/src/rules/validChildren.js ================================================ import ruleError from './ruleError' export default function validChildren( element, { components, dependencies, skipElements }, ) { const { children, tagName } = element const Component = components[tagName] if (!Component || !children || !children.length) { return null } const errors = [] for (const child of children) { const childTagName = child.tagName const ChildComponent = components[childTagName] const parentDependencies = dependencies[tagName] || [] const childIsValid = !ChildComponent || skipElements.includes(childTagName) || parentDependencies.includes(childTagName) || parentDependencies.some( (dep) => dep instanceof RegExp && dep.test(childTagName), ) if (childIsValid === false) { const allowedDependencies = Object.keys(dependencies).filter( (key) => dependencies[key].includes(childTagName) || dependencies[key].some( (dep) => dep instanceof RegExp && dep.test(childTagName), ), ) errors.push( ruleError( `${childTagName} cannot be used inside ${tagName}, only inside: ${allowedDependencies.join( ', ', )}`, child, ), ) } } return errors } ================================================ FILE: packages/mjml-validator/src/rules/validTag.js ================================================ import ruleError from './ruleError' // Tags that have no associated components but are allowed even so const componentLessTags = [ 'mj-all', 'mj-class', 'mj-selector', 'mj-html-attribute', ] export default function validateTag(element, { components }) { const { tagName } = element if (componentLessTags.includes(tagName)) return null const Component = components[tagName] if (!Component) { return ruleError( `Element ${tagName} doesn't exist or is not registered`, element, ) } return null } ================================================ FILE: packages/mjml-validator/src/rules/validTypes.js ================================================ import ruleError from './ruleError' export default function validateType(element, { components, initializeType }) { const { attributes, tagName } = element const Component = components[tagName] if (!Component) { return null } const errors = [] for (const [attr, value] of Object.entries(attributes || {})) { const attrType = Component.allowedAttributes && Component.allowedAttributes[attr] if (attrType) { const TypeChecker = initializeType(attrType) const result = new TypeChecker(value) if (result.isValid() === false) { errors.push( ruleError(`Attribute ${attr} ${result.getErrorMessage()}`, element), ) } } } return errors } ================================================ FILE: packages/mjml-wrapper/README.md ================================================ ### mj-wrapper Enables you to wrap multiple `mj-section` tags together. It's especially useful to achieve nested layouts with shared border or background images across sections.
wrapper
```xml First line of text Second line of text ``` The `full-width` attribute will be used to manage the background width. Setting it will change the width of the section from the default 600px to 100%. #### Attributes | attribute | accepts | description | default value | | --------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------ | ------------- | | background-color | CSS color formats | section color | | | background-position | string | CSS values, i.e. `left` `center` `right` + `top` `center` `bottom`
(see outlook limitations below) | `top center` | | background-position-x | string | CSS values, i.e. `left` `center` `right`
(see outlook limitations below) | | | background-position-y | string | CSS values, i.e. `top` `center` `bottom`
(see outlook limitations below) | | | background-repeat | `repeat` `no-repeat` | set the background image to repeat | | | background-size | string | CSS values e.g. `auto` `cover` `contain` `px` `%` size | `auto` | | background-url | string | background image, in URL format | | | border | string | CSS border format | | | border-bottom | string | CSS border format | | | border-left | string | CSS border format | | | border-radius | string | border radius | | | border-right | string | CSS border format | | | border-top | string | CSS border format | | | css-class | string | class name, added to the root HTML element created | | | full-width | `full-width` `false` | make the section full-width | | | gap | `px` | applies a vertical gap between child `mj-section` instances | | | padding | `px` `%` | section padding, supports up to 4 parameters | `20px 0` | | padding-bottom | `px` `%` | section bottom padding | | | padding-left | `px` `%` | section left padding | | | padding-right | `px` `%` | section right padding | | | padding-top | `px` `%` | section top padding | | | text-align | `left` `center` `right` | CSS text-align | `center` |

Try it live

================================================ FILE: packages/mjml-wrapper/package.json ================================================ { "name": "mjml-wrapper", "description": "mjml-wrapper", "version": "4.18.0", "main": "lib/index.js", "files": [ "lib" ], "repository": { "type": "git", "url": "git+https://github.com/mjmlio/mjml.git", "directory": "packages/mjml-wrapper" }, "license": "MIT", "bugs": { "url": "https://github.com/mjmlio/mjml/issues" }, "homepage": "https://mjml.io", "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib --root-mode upward" }, "dependencies": { "@babel/runtime": "^7.28.4", "lodash": "^4.17.21", "mjml-core": "4.18.0", "mjml-section": "4.18.0" }, "devDependencies": { "@babel/cli": "^7.8.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/mjml-wrapper/src/index.js ================================================ import MjSection from 'mjml-section' import { suffixCssClasses } from 'mjml-core' export default class MjWrapper extends MjSection { static componentName = 'mj-wrapper' static allowedAttributes = { ...MjSection.allowedAttributes, gap: 'unit(px)', } renderWrappedChildren() { const { children } = this.props const { containerWidth } = this.context return ` ${this.renderChildren(children, { renderer: (component) => component.constructor.isRawElement() ? component.render() : ` ${component.render()} `, })} ` } } ================================================ FILE: readme-ja.md ================================================ # MJML 4 もしも、MJML 3.3.Xについて探しているのであれば、[このブランチ](https://github.com/mjmlio/mjml/tree/3.3.x)をご確認ください。

github actions

| 翻訳されたドキュメント | 紹介 | インストール | 使い方 | 貢献 |

--- # 翻訳されたドキュメント | 言語 | ドキュメントのリンク | | :-: | :-: | | 日本語 | [日本語ドキュメント](https://github.com/mjmlio/mjml/blob/master/readme-ja.md) | # 紹介 `MJML`は[Mailjet](https://www.mailjet.com/)によって作成されたマークアップ言語で、レスポンシブemailをコーディングする際に生じる負担を軽減する設計がされています。そのセマンティックな構文は言語を簡単完結にし、その豊富な標準コンポーネントライブラリはあなたの開発時間とコードベースを短縮するでしょう。MJMLのオープンソースエンジンは、あなたの書いたMJMLをレスポンシブHTMLに変換します。

# インストール `MJML`は`NPM`と一緒にインストールすることで、NodeJSやCommand Line Interfaceから使用できます。これらについてわからないの場合は、使い方から他の方法をご確認ください。 ```bash npm install mjml ``` # 開発 MJMLに変更を加えたり、マージリクエストを提出するといった作業をする場合は、[yarn](https://yarnpkg.com/lang/en/docs/install/)をダウンロードして、インストールすることで簡単に開発できるようにしましょう。 ```bash git clone https://github.com/mjmlio/mjml.git && cd mjml yarn yarn build ``` `yarn build:watch`を実行することで、コードを書きながらパッケージを再構築することもできます。 # 使い方 ## オンライン 何もインストールしたくないですか?それならば、無料のオンラインエディターを使いましょう!

try it live


## アプリケーションとプラグイン MJMLにはツールやプラグインといったエコシステムが備わっています。以下をご確認ください: - The [MJML App](https://mjmlio.github.io/mjml-app/) (MJMLが含まれています) - [Visual Studio Code plugin](https://github.com/mjmlio/vscode-mjml) (MJMLが含まれています) - [Atom plugin](https://atom.io/users/mjmlio) (MJMLを別途インストールする必要があります) - [Sublime Text plugin](https://packagecontrol.io/packages/MJML-syntax) (MJMLを別途インストールする必要があります) その他のツールについては[コミュニティ](https://mjml.io/community)ページをご覧ください。 ## Command line interface > ファイルをコンパイルし、HTMLを`output.html`として出力します。 ```bash mjml input.mjml -o output.html ``` 任意の`引数`をCLIに渡すことができます。これらは複数合わせて渡すこともできます。 引数 | 説明 | 初期値 ---------|--------|-------------- `mjml -m [input]` | v3のMJMLファイルをv4の構文にマイグレートする | NA `mjml [input] -o [output]` | 出力を[output]に書き込みます | NA `mjml [input] -s` | 出力を`stdout`に書き込みます | NA `mjml -w [input]` | `[input]`(ファイルまたはフォルダー)の変更を監視します | NA `mjml [input] --config.beautify` | 出力を整えます(`true`または`false`) | true `mjml [input] --config.minify` | 出力をminify化します(`true`または`false`) | false 設定オプションの詳細については[mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md)をご覧ください。 ## Node.js ```javascript import mjml2html from 'mjml' /* mjml文字列をコンパイルする */ const htmlOutput = mjml2html(` Hello World! `, options) /* 生成されたレスポンシブHTMLとMJMLのエラーがあれば表示します */ console.log(htmlOutput) ``` 任意でオブジェクト形式の`オプション`を`mjml2html`関数に渡すことができます: オプション | 型 | 説明 | 初期値 -------------|--------|--------------|--------------- fonts | object | 初期フォントをインポートしたHTMLを描画する | 初期フォントについては[index.js](https://github.com/mjmlio/mjml/blob/master/packages/mjml-core/src/index.js#L100-L108)をご覧ください。 keepComments | boolean | 出力されるHTMLにコメントを残すオプション | true ignoreIncludes | boolean | mj-includesを無視するオプション | false beautify | boolean | 出力されるHTMLを整えるオプション | false minify | boolean | 出力されるHTMLをminify化するオプション | false validationLevel | string | [validator](https://github.com/mjmlio/mjml/tree/master/packages/mjml-validator#validating-mjml)で利用する値: 'strict', 'soft', 'skip' | 'soft' filePath | string | mj-includesの相対パスに使われるファイルパス | '.' preprocessors | array of functions | xmlのパース前に適用するプリプロセッサー。入力はjsonではなく、必ずxmlでなければなりません。関数の場合は必ず (xml: string) => string としなければなりません。 | [] juicePreserveTags | cssをインライン化する際にタグを保持する。詳しくは[mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md)をご覧ください。 | NA minifyOptions | htmlのminify化に関するオプション。詳しくは[mjml-cli documentation](https://github.com/mjmlio/mjml/blob/master/packages/mjml-cli/README.md)をご覧ください。 | NA mjmlConfigPath | string | `.mjmlconfig`ファイルのパスもしくはディレクトリー(カスタムコンポーネントの場合) | `process.cwd()` useMjmlConfigOptions | `.mjmlconfig`ファイルで`options`属性の使用を許可する | false ## クライアントサイド (ブラウザー) ```javascript var mjml2html = require('mjml-browser') /* mjml文字列をコンパイルする */ var htmlOutput = mjml2html(` Hello World! `, options) /* 生成されたレスポンシブHTMLとMJMLのエラーがあれば表示します */ console.log(htmlOutput) ``` ## API 無料のMJML APIを利用することで、あなたのアプリケーションにMJMLを簡単に統合できます。 APIの詳細については[ここ](https://mjml.io/api)をご覧ください。 # MJML Slack MJMLはその素晴らしいコミュニティなくしてはここまで良いものにならなかったでしょう。[コミュニティ Slack](https://join.slack.com/t/mjml/shared_invite/zt-gqmwfwmr-kPBnfuuB7wof5httaTcXxg)から、MJML'er達に会いにいきましょう。 # 貢献者 - [Maxime](https://github.com/iRyusa) - [Nicolas](https://github.com/ngarnier) - [Cedric](https://github.com/kmcb777) - [Loeck](https://github.com/lohek) - [Robin](https://github.com/robink) - [Guillaume](https://github.com/GuillaumeBadi) - [Meriadec](https://github.com/meriadec) - [Arnaud](https://github.com/arnaudbreton) - [HTeuMeuLeu](https://github.com/hteumeuleu) - [Emmanuel Payet](https://github.com/epayet) - [Matthieu](https://github.com/swibge) - [Rogier](https://github.com/rogierslag) ================================================ FILE: test.js ================================================ require('@babel/register') const mjml2html = require('./packages/mjml/src/index') const xml = ` lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem lorem ` console.time('mjml2html') const { html } = mjml2html(xml, { beautify: true, }) console.timeEnd('mjml2html') if (process.argv.includes('--output')) { console.log(html) } if (process.argv.includes('--open')) { const open = require('open') const path = require('path') const fs = require('fs') const testFile = path.resolve(__dirname, './test.html') fs.writeFileSync(testFile, html) open(testFile) } ================================================ FILE: type.js ================================================ const types = require('./packages/mjml-core/lib/types/type.js') const enumtype = types.initializeType('enum(top,left,center)') const colortype = types.initializeType('color') const booleantype = types.initializeType('boolean') const unittype = types.initializeType('unit(px,%){1,3}') const stringtype = types.initializeType('string') console.log(stringtype) const output = (t) => { console.log(`Type: ${t.constructor.name} — Value: ${t.value} — isValid: ${t.isValid()} ${t.getErrorMessage()}`) } [new colortype('grey'), new colortype('rgba(0,255,3,0.3)'), new colortype('#DDF'), new colortype('#DF'), new booleantype('true'), new booleantype('false'), new booleantype('banana'), new unittype('10 20px 20'), new unittype('10px 20px 20px'), new unittype('10px'), new unittype('10%'), new unittype('10px 10px'), new unittype('0'), new stringtype('hello world'), ].map(output)