├── .gitignore
├── CHANGELOG.md
├── .eslintrc.json
├── menus
└── mjml-preview.cson
├── keymaps
└── mjml-preview.cson
├── README.md
├── package.json
├── .editorconfig
├── LICENSE.md
└── lib
├── mjml-preview.js
└── mjml-view.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.0 - First Release
2 | * Every feature added
3 | * Every bug fixed
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "globals": {
5 | "atom": true
6 | },
7 | "rules": {
8 | "padded-blocks": 0,
9 | "space-before-function-paren": 0,
10 | "semi": 0,
11 | "react/require-extension": "off",
12 | "react/jsx-filename-extension": 0,
13 | "react/prop-types": 0
14 | },
15 | "settings": {
16 | "import/core-modules": [
17 | "atom"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/menus/mjml-preview.cson:
--------------------------------------------------------------------------------
1 | 'context-menu':
2 | 'atom-text-editor': [
3 | {
4 | 'label': 'Preview MJML'
5 | 'command': 'mjml-preview:preview'
6 | }
7 | ]
8 | 'menu': [
9 | {
10 | 'label': 'Packages'
11 | 'submenu': [
12 | 'label': 'mjml-preview'
13 | 'submenu': [
14 | {
15 | 'label': 'Preview MJML'
16 | 'command': 'mjml-preview:preview'
17 | }
18 | ]
19 | ]
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/keymaps/mjml-preview.cson:
--------------------------------------------------------------------------------
1 | # Keybindings require three things to be fully defined: A selector that is
2 | # matched against the focused element, the keystroke and the command to
3 | # execute.
4 | #
5 | # Below is a basic keybinding which registers on all platforms by applying to
6 | # the root workspace element.
7 |
8 | # For more detailed documentation see
9 | # https://atom.io/docs/latest/behind-atom-keymaps-in-depth
10 | 'atom-workspace':
11 | 'ctrl-alt-p': 'mjml-preview:preview'
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MJML Preview Package
2 |
3 | Show the rendered HTML from MJML on the right pane of the editor. It is enabled for `.mjml` files.
4 |
5 | 
6 |
7 | To toggle the preview, go to "Packages" > "mjml-preview" > "Preview MJML".
8 |
9 | ## Trigger on Save
10 |
11 | To refresh the preview pane on each save, tick the box "Trigger on Save" in the settings of the package: ⌘+, or ctrl+, to open package settings > look for "mjml-preview" in "installed packages" > "Settings" > "Trigger on Save".
12 |
13 | 
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mjml-preview",
3 | "main": "./lib/mjml-preview.js",
4 | "version": "2.0.3",
5 | "description": "A package to preview how your email renders right from within Atom",
6 | "keywords": [],
7 | "repository": "https://github.com/mjmlio/atom-mjml-preview",
8 | "license": "MIT",
9 | "engines": {
10 | "atom": ">=1.0.0 <2.0.0"
11 | },
12 | "dependencies": {
13 | "mjml": "^4.8.1",
14 | "atom-space-pen-views": "^2.0.5"
15 | },
16 | "devDependencies": {
17 | "eslint": "^3.7.1",
18 | "eslint-config-airbnb-base": "^8.0.0",
19 | "eslint-plugin-import": "^2.0.0",
20 | "babel-eslint": "^6.1.2"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | # A special property that should be specified at the top of the file outside of
4 | # any sections. Set to true to stop .editor config file search on current file
5 | root = true
6 |
7 | # Indentation style
8 | # Possible values - tab, space
9 | indent_style = space
10 |
11 | # Indentation size in single-spaced characters
12 | # Possible values - an integer, tab
13 | indent_size = 2
14 |
15 | # Line ending file format
16 | # Possible values - lf, crlf, cr
17 | end_of_line = lf
18 |
19 | # File character encoding
20 | # Possible values - latin1, utf-8, utf-16be, utf-16le
21 | charset = utf-8
22 |
23 | # Denotes whether to trim whitespace at the end of lines
24 | # Possible values - true, false
25 | trim_trailing_whitespace = true
26 |
27 | # Denotes whether file should end with a newline
28 | # Possible values - true, false
29 | insert_final_newline = true
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/lib/mjml-preview.js:
--------------------------------------------------------------------------------
1 | 'use babel';
2 |
3 | import url from 'url'
4 | import { CompositeDisposable } from 'atom'
5 | import MJMLView from './mjml-view'
6 |
7 | let MJMLPaneView = null
8 |
9 | export default {
10 | config: {
11 | triggerOnSave: {
12 | type: 'boolean',
13 | description: 'Update the preview on each save.',
14 | default: true,
15 | },
16 | },
17 |
18 | desactivate() {
19 | this.subscriptions.dispose()
20 | },
21 |
22 | activate() {
23 | this.subscriptions = new CompositeDisposable()
24 | this.subscriptions.add(atom.workspace.observeTextEditors((editor) => {
25 | this.subscriptions.add(editor.getBuffer().onDidSave(() => {
26 | if (atom.config.get('mjml-preview.triggerOnSave')) {
27 | this.openPane(editor)
28 | }
29 | }))
30 | }))
31 |
32 | atom.workspace.addOpener(this.mjmlPreviewOpener)
33 |
34 | this.keybindings()
35 | },
36 |
37 | keybindings() {
38 | this.subscriptions.add(atom.commands.add('atom-workspace', { 'mjml-preview:preview': () => this.openPane() }))
39 | },
40 |
41 | openPane(editor) {
42 | const activeEditor = atom.workspace.getActiveTextEditor()
43 | const currentEditor = editor || activeEditor
44 |
45 | if (!currentEditor) {
46 | // probably trying to render an mjml-preview pane
47 | return;
48 | }
49 |
50 | if (currentEditor.id === atom.workspace.getActiveTextEditor().id) {
51 | const uri = `mjml-preview://editor/${currentEditor.id}`
52 | const fileGrammar = currentEditor.getGrammar()
53 | if (fileGrammar.scopeName !== 'text.mjml.basic') {
54 | return;
55 | }
56 |
57 | const previousActivePane = atom.workspace.getActivePane()
58 |
59 | atom.workspace.open(uri, { split: 'right', searchAllPanes: true })
60 | .then((view) => {
61 | if (view instanceof MJMLView) {
62 | return view.render(currentEditor)
63 | }
64 | })
65 | .then(() => previousActivePane.activate())
66 | }
67 | },
68 |
69 | mjmlPreviewOpener(uri) {
70 | try {
71 | const { protocol, pathname } = url.parse(uri)
72 | if (protocol !== 'mjml-preview:') {
73 | return;
74 | }
75 |
76 | const filePath = decodeURI(pathname)
77 |
78 | MJMLPaneView = new MJMLView({editorId: filePath.substring(1), filePath})
79 |
80 | return MJMLPaneView
81 | } catch (e) {
82 | return;
83 | }
84 | },
85 | }
86 |
--------------------------------------------------------------------------------
/lib/mjml-view.js:
--------------------------------------------------------------------------------
1 | 'use babel';
2 |
3 | import path from 'path'
4 | import fs from 'fs'
5 | import os from 'os'
6 | import { $, ScrollView } from 'atom-space-pen-views'
7 | import mjml2html from 'mjml'
8 |
9 | class MJMLView extends ScrollView {
10 | static serialize() {
11 | return {
12 | deserializer: 'MJMLView',
13 | filePath: this.filePath,
14 | editorId: this.editorId
15 | }
16 | }
17 |
18 | static deserialize (state) {
19 | return new MJMLView(state)
20 | }
21 |
22 | static content () {
23 | return MJMLView.div({ 'class': 'atom-html-preview native-key-bindings', 'tabindex': -1 })
24 | }
25 |
26 | constructor({editorId, filePath}) {
27 | super()
28 | this.webViewLoaded = false
29 | this.reloadLater = false
30 | this.filePath = filePath
31 | this.editorId = editorId
32 | atom.deserializers.add(this)
33 |
34 | if (this.editorId) {
35 | this.editor = this.editorForId(this.editorId)
36 | }
37 |
38 | this.createWebView()
39 | this.addReadyListener()
40 | }
41 |
42 | addReadyListener() {
43 | this.webview.addEventListener('dom-ready', () => {
44 | this.webViewLoaded = true
45 | if (this.reloadLater) {
46 | this.reloadLater = false
47 | this.webview.reload()
48 | }
49 | })
50 | }
51 |
52 | createWebView() {
53 | this.webview = document.createElement('webview')
54 | this.webview.setAttribute('sandbox', 'allow-scripts allow-same-origin')
55 | this.webview.setAttribute('style', 'height: 100%')
56 | this.append($(this.webview))
57 | }
58 |
59 | editorForId(editorId) {
60 | const editors = atom.workspace.getTextEditors()
61 | for (let i = 0 ; i < editors.length ; i++) {
62 | const editor = editors[i]
63 | if (editor && editor.id === parseInt(editorId)) return editor
64 | }
65 | }
66 |
67 | renderMJML(TextEditor, done) {
68 | const mjmlTempPath = path.resolve(path.join(os.tmpdir(), `${TextEditor.getTitle()}.html`))
69 | const outputHTML = mjml2html(TextEditor.getText(), { level: 'skip', disableMinify: true, filePath: TextEditor.getPath() }).html
70 |
71 | fs.writeFile(mjmlTempPath, outputHTML, (err) => {
72 | if (err) {
73 | throw err
74 | }
75 |
76 | done(mjmlTempPath)
77 | })
78 | }
79 |
80 | render(TextEditor) {
81 | this.renderMJML(TextEditor, (file) => {
82 | this.webview.src = file
83 |
84 | if (this.webViewLoaded) {
85 | try {
86 | this.webview.reload()
87 | } catch (error) {
88 | return null
89 | }
90 | } else {
91 | this.reloadLater = true
92 | }
93 | })
94 | }
95 |
96 | getTitle() {
97 | return `PREVIEW - ${this.editor.getTitle()}`
98 | }
99 |
100 | getURI() {
101 | return `mjml-preview://editor/${this.editorId}`
102 | }
103 | }
104 |
105 | export default MJMLView
106 |
--------------------------------------------------------------------------------