├── .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 | ![mjml-preview](http://i.imgur.com/o6Y3q6f.png) 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 | ![settings](http://i.imgur.com/R17UQmQ.png) 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 | --------------------------------------------------------------------------------