├── .babelrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── assets │ ├── avocado-235px.jpg │ ├── avocado-logo-sml.png │ ├── facebook.png │ ├── footer-bg.jpg │ ├── github.png │ ├── gplus.png │ ├── guac-485px.jpg │ ├── instagram.png │ ├── tomatoes-235px.jpg │ ├── twitter.png │ └── white-avocado-logo-sml.png └── avocado-industries-with-images.heml ├── gulpfile.js ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── heml-elements │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── Base.js │ │ ├── Block.js │ │ ├── Body.js │ │ ├── Button.js │ │ ├── Column.js │ │ ├── Container.js │ │ ├── Font.js │ │ ├── Head.js │ │ ├── Heml.js │ │ ├── Hr.js │ │ ├── Img.js │ │ ├── Meta.js │ │ ├── Preview.js │ │ ├── Row.js │ │ ├── Style.js │ │ ├── Subject.js │ │ ├── Table.js │ │ ├── Typography.js │ │ └── index.js ├── heml-inline │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── fixWidthsFor.js │ │ ├── index.js │ │ ├── inlineMargins.js │ │ ├── preferMaxWidth.js │ │ ├── removeProcessingIds.js │ │ └── styleHelper.js ├── heml-parse │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.js ├── heml-render │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── createHtmlElement.js │ │ ├── index.js │ │ ├── renderElement.js │ │ └── stringifyAttributes.js ├── heml-styles │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── index.js │ │ └── plugins │ │ ├── postcss-element-expander │ │ ├── coerceElements.js │ │ ├── expanders.js │ │ ├── findDirectElementSelectors.js │ │ ├── index.js │ │ └── tagAliasSelectors.js │ │ ├── postcss-expand-shorthand │ │ └── index.js │ │ ├── postcss-merge-adjacent-media │ │ └── index.js │ │ └── postcss-zero-out-margin │ │ └── index.js ├── heml-utils │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── HEMLError.js │ │ ├── condition.js │ │ ├── createElement.js │ │ ├── index.js │ │ └── transforms │ │ ├── convertProp.js │ │ ├── fallbackFor.js │ │ ├── ieAlignFallback.js │ │ ├── index.js │ │ └── trueHide.js ├── heml-validate │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── index.js │ │ └── validators │ │ ├── attrs.js │ │ ├── children.js │ │ ├── index.js │ │ ├── parent.js │ │ └── unique.js └── heml │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ ├── bin │ ├── commands │ │ ├── build.js │ │ └── develop.js │ ├── heml.js │ └── utils │ │ ├── buildErrorPage.js │ │ ├── isHemlFile.js │ │ └── renderHemlFile.js │ └── index.js └── test ├── __fixtures__ ├── index.js └── simple.fixture.js ├── __snapshots__ └── snapshot.spec.js.snap └── snapshot.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": 4 6 | }, 7 | }] 8 | ], 9 | "plugins": [ 10 | "transform-runtime", 11 | "transform-object-rest-spread", 12 | ["transform-react-jsx", { 13 | "pragma": "HEML.renderElement" 14 | }] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = "utf-8" 8 | 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build/ 4 | .env 5 | *.log 6 | testing/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2017-present SparkPost 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

<heml>

2 | 3 | 4 |

5 | Guide • 6 | Documentation • 7 | Editor 8 |

9 | 10 | 11 | HEML is an open source markup language for building responsive email. 12 | 13 | - **Native Feel:** Do you know HTML and CSS? Check out our docs and you're off to the races! No special rules or styling paradigms to master. 14 | 15 | - **Forward Thinking:** HEML is designed to take advantage of all that email can do while still providing a solid experience for all clients. 16 | 17 | - **Extendable:** You can create your own powerful elements and style rules. Share them with the world, or keep them to yourself. Your choice. 18 | 19 | 20 | ## FAQ 21 | 22 | ### Why should I use HEML? 23 | 24 | It makes building emails easier. 25 | 26 | ### How do I use it? 27 | 28 | Check out our [usage guide](http://heml.io/docs/getting-started/usage). 29 | 30 | ### What do I do if I found a bug? 31 | 32 | Open up an [issue](https://github.com/SparkPost/heml/issues/new) on the repository. Thanks for catching it! 🙏 33 | 34 | ### Want to help? 35 | 36 | Awesome!! We welcome any and all help! Head over to the [issues](https://github.com/SparkPost/heml/issues) and see if anything catches your eye. 37 | 38 | ## License 39 | 40 | [MIT](https://github.com/SparkPost/heml/blob/master/LICENSE) 41 | -------------------------------------------------------------------------------- /examples/assets/avocado-235px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/avocado-235px.jpg -------------------------------------------------------------------------------- /examples/assets/avocado-logo-sml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/avocado-logo-sml.png -------------------------------------------------------------------------------- /examples/assets/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/facebook.png -------------------------------------------------------------------------------- /examples/assets/footer-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/footer-bg.jpg -------------------------------------------------------------------------------- /examples/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/github.png -------------------------------------------------------------------------------- /examples/assets/gplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/gplus.png -------------------------------------------------------------------------------- /examples/assets/guac-485px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/guac-485px.jpg -------------------------------------------------------------------------------- /examples/assets/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/instagram.png -------------------------------------------------------------------------------- /examples/assets/tomatoes-235px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/tomatoes-235px.jpg -------------------------------------------------------------------------------- /examples/assets/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/twitter.png -------------------------------------------------------------------------------- /examples/assets/white-avocado-logo-sml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SparkPost/heml/9c600cb97b4d5f46a97a0c8af75851c15d5c681c/examples/assets/white-avocado-logo-sml.png -------------------------------------------------------------------------------- /examples/avocado-industries-with-images.heml: -------------------------------------------------------------------------------- 1 | 2 | 3 | America's Finest Homemade Guacamole! 4 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 | 30 | 31 | 32 | 33 |

34 |
35 |
36 |

America's Finest Homemade Guacamole!

37 |

38 | 39 |
40 | Spicy heml tasty responsive jalapeno bacon ipsum dolor amet pariatur mollit fatback venison, 41 | cillum occaecat quis ut labore pork belly culpa ea bacon in spare ribs.

42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 |

Tasty Recipes!

52 |

Velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.

53 |
54 | 55 | 56 |

All Natural Ingredients

57 |

Velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.

58 |
59 |
60 |
61 | 62 | 63 | 64 | 68 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
-------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Inspired by https://github.com/babel/minify/blob/master/gulpfile.babel.js 3 | 4 | const del = require('del') 5 | const through = require('through2') 6 | const newer = require('gulp-newer') 7 | const babel = require('gulp-babel') 8 | const util = require('gulp-util') 9 | const plumber = require('gulp-plumber') 10 | const gulp = require('gulp') 11 | const path = require('path') 12 | const { cyan } = util.colors 13 | 14 | const scripts = './packages/*/src/**/*.js' 15 | const builds = './packages/*/build' 16 | const dest = 'packages' 17 | 18 | let srcEx, libFragment 19 | 20 | if (path.win32 === path) { 21 | srcEx = /(packages\\[^\\]+)\\src\\/ 22 | libFragment = '$1\\build\\' 23 | } else { 24 | srcEx = new RegExp('(packages/[^/]+)/src/') 25 | libFragment = '$1/build/' 26 | } 27 | 28 | function build () { 29 | return gulp 30 | .src(scripts) 31 | .pipe(plumber()) 32 | .pipe(through.obj((file, enc, callback) => { 33 | file._path = file.path 34 | file.path = file.path.replace(srcEx, libFragment) 35 | callback(null, file) 36 | })) 37 | .pipe(newer(dest)) 38 | .pipe(babel()) 39 | .pipe(gulp.dest(dest)) 40 | .on('end', () => { 41 | util.log(`Finished '${cyan('build')}'`) 42 | }) 43 | } 44 | 45 | gulp.task('build', build) 46 | 47 | gulp.task('watch', ['build'], function () { 48 | gulp.watch(scripts, { debounceDelay: 200 }, build) 49 | }) 50 | 51 | gulp.task('clean', () => { 52 | return del(builds) 53 | }) 54 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.4.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "1.1.3" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "lint": "standard 'packages/**/src/**/*.js' 'test/**/*.js' --fix", 5 | "test": "npm run bootstrap && npm run build && npm run lint && jest ./test/*.spec.js", 6 | "bootstrap": "npm install && lerna bootstrap", 7 | "clean": "gulp clean && lerna clean --yes", 8 | "build": "gulp build", 9 | "watch": "gulp watch", 10 | "publish": "npm run clean && npm test && lerna publish" 11 | }, 12 | "devDependencies": { 13 | "babel-cli": "^6.26.0", 14 | "babel-plugin-transform-async-to-generator": "^6.24.1", 15 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 16 | "babel-plugin-transform-react-jsx": "^6.24.1", 17 | "babel-plugin-transform-runtime": "^6.23.0", 18 | "babel-preset-env": "^1.6.0", 19 | "del": "^3.0.0", 20 | "gulp": "^3.9.1", 21 | "gulp-babel": "^7.0.0", 22 | "gulp-newer": "^1.3.0", 23 | "gulp-plumber": "^1.1.0", 24 | "gulp-util": "^3.0.8", 25 | "husky": "^0.14.3", 26 | "jest": "^21.2.1", 27 | "lerna": "^2.4.0", 28 | "require-all": "^2.2.0", 29 | "standard": "^10.0.3", 30 | "through2": "^2.0.3" 31 | }, 32 | "standard": { 33 | "globals": [ 34 | "afterAll", 35 | "afterEach", 36 | "beforeAll", 37 | "beforeEach", 38 | "describe", 39 | "expect", 40 | "test" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/heml-elements/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/elements", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.17.0", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.0.tgz", 10 | "integrity": "sha1-fadHkW24A/dhZR1gkdcIeJuVPGo=", 11 | "requires": { 12 | "follow-redirects": "1.2.5", 13 | "is-buffer": "1.1.5" 14 | } 15 | }, 16 | "debug": { 17 | "version": "2.6.9", 18 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 19 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 20 | "requires": { 21 | "ms": "2.0.0" 22 | } 23 | }, 24 | "follow-redirects": { 25 | "version": "1.2.5", 26 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.5.tgz", 27 | "integrity": "sha512-lMhwQTryFbG+wYsAIEKC1Kf5IGDlVNnONRogIBllh7LLoV7pNIxW0z9fhjRar9NBql+hd2Y49KboVVNxf6GEfg==", 28 | "requires": { 29 | "debug": "2.6.9" 30 | } 31 | }, 32 | "image-size": { 33 | "version": "0.6.1", 34 | "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.1.tgz", 35 | "integrity": "sha512-lHMlI2MykfeHAQdtydQh4fTcBQVf4zLTA91w1euBe9rbmAfJ/iyzMh8H3KD9u1RldlHaMS3tmMV5TEe9BkmW9g==" 36 | }, 37 | "is-absolute-url": { 38 | "version": "2.1.0", 39 | "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", 40 | "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=" 41 | }, 42 | "is-buffer": { 43 | "version": "1.1.5", 44 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", 45 | "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" 46 | }, 47 | "lodash": { 48 | "version": "4.17.4", 49 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 50 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 51 | }, 52 | "ms": { 53 | "version": "2.0.0", 54 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 55 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/heml-elements/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/elements", 3 | "version": "1.1.3", 4 | "description": "Core elements for HEML", 5 | "keywords": [ 6 | "heml" 7 | ], 8 | "homepage": "https://heml.io", 9 | "bugs": "https://github.com/SparkPost/heml/issues", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SparkPost/heml.git" 14 | }, 15 | "author": "SparkPost (https://sparkpost.com)", 16 | "files": [ 17 | "build/" 18 | ], 19 | "main": "build/index.js", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "@heml/styles": "^1.1.2", 25 | "@heml/utils": "^1.1.2", 26 | "axios": "^0.17.0", 27 | "image-size": "^0.6.1", 28 | "is-absolute-url": "^2.1.0", 29 | "lodash": "^4.17.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Base.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Meta from './Meta' 3 | import isAbsoluteUrl from 'is-absolute-url' 4 | import { resolve } from 'url' 5 | import { has, first } from 'lodash' 6 | 7 | export default createElement('base', { 8 | parent: [ 'head' ], 9 | children: false, 10 | unique: true, 11 | defaultAttrs: { href: '' }, 12 | 13 | render (attrs, contents) { 14 | Meta.set('base', attrs.href) 15 | 16 | return false 17 | }, 18 | 19 | preRender ({ $ }) { 20 | const base = first($.findNodes('base')) 21 | 22 | if (base) { 23 | const baseUrl = base.attr('href') 24 | 25 | $('[href], [src]') 26 | .each((i, node) => { 27 | const attr = has(node.attribs, 'href') ? 'href' : 'src' 28 | 29 | if (has(node.attribs, attr) && !isAbsoluteUrl(node.attribs[attr])) { 30 | node.attribs[attr] = resolve(baseUrl, node.attribs[attr]) 31 | } 32 | }) 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Block.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms, cssGroups, condition } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Style from './Style' 3 | 4 | const { 5 | trueHide, 6 | ieAlignFallback } = transforms 7 | 8 | const { 9 | background, 10 | margin, 11 | padding, 12 | border, 13 | borderRadius, 14 | width, 15 | height, 16 | table, 17 | box } = cssGroups 18 | 19 | export default createElement('block', { 20 | containsText: true, 21 | 22 | rules: { 23 | '.block': [ { '@pseudo': 'root' }, { display: trueHide('block') }, margin, width ], 24 | 25 | '.block__table__ie': [ 'width', 'max-width', { [margin]: ieAlignFallback } ], 26 | 27 | '.block__table': [ { '@pseudo': 'table' }, table ], 28 | 29 | '.block__row': [ { '@pseudo': 'row' } ], 30 | 31 | '.block__cell': [ { '@pseudo': 'cell' }, height, background, box, padding, border, borderRadius, 'vertical-align' ] 32 | }, 33 | 34 | render (attrs, contents) { 35 | attrs.class += ' block' 36 | return ( 37 |
38 | {condition('mso | IE', ``)} 45 | 50 |
51 | ) 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Body.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms, cssGroups } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Style from './Style' 3 | import Preview from './Preview' 4 | 5 | const { 6 | background, 7 | padding, 8 | font, 9 | text } = cssGroups 10 | 11 | export default createElement('body', { 12 | unique: true, 13 | parent: [ 'heml' ], 14 | containsText: true, 15 | 16 | rules: { 17 | '.body': [ { '@pseudo': 'root' }, background ], 18 | 19 | '.bodyTable': [ { '@pseudo': 'table' }, '@default', background ], 20 | 21 | '.body__content': [ { '@pseudo': 'content' }, padding, font, text ], 22 | 23 | '.preview': [ { 'background-color': transforms.convertProp('color') } ] 24 | }, 25 | 26 | async render (attrs, contents) { 27 | attrs.class += ' body' 28 | 29 | return ( 30 | 31 | {Preview.flush()} 32 | 33 | 34 | 35 | 36 | 37 |
{'  '.repeat(30)}
38 | 48 | ) 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Button.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms, cssGroups } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import { omit, pick } from 'lodash' 3 | import Style from './Style' 4 | 5 | const { 6 | background, 7 | margin, 8 | padding, 9 | border, 10 | borderRadius, 11 | width, 12 | height, 13 | table, 14 | text, 15 | font, 16 | box } = cssGroups 17 | 18 | export default createElement('button', { 19 | attrs: [ 'href', 'target' ], 20 | defaultAttrs: { 21 | href: '#' 22 | }, 23 | 24 | rules: { 25 | '.button': [ 26 | { '@pseudo': 'root' }, { display: transforms.trueHide('block') } ], 27 | 28 | '.button__table': [ 29 | { '@pseudo': 'table' }, margin, table ], 30 | 31 | '.button__cell': [ 32 | { '@pseudo': 'cell' }, background, padding, borderRadius, border, height, width, box ], 33 | 34 | '.button__link': [ 35 | { '@pseudo': 'link' }, background, text, font ], 36 | '.button__text': [ 37 | { '@pseudo': 'text' }, 'color', 'text-decoration' ] 38 | }, 39 | 40 | render (attrs, contents) { 41 | attrs.class += ' button' 42 | 43 | return ( 44 |
45 | 46 | 47 | 58 | 59 |
48 | 49 | 50 | 55 | 56 | 57 |
60 | 70 |
) 71 | } 72 | }) 73 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Column.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms, cssGroups } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Style from './Style' 3 | 4 | const { 5 | background, 6 | box, 7 | padding, 8 | border, 9 | borderRadius } = cssGroups 10 | 11 | const breakpoint = 600 12 | 13 | export default createElement('column', { 14 | attrs: [ 'small', 'large' ], 15 | parent: [ 'row' ], 16 | defaultAttrs: { small: 12, large: 12 }, 17 | containsText: true, 18 | 19 | rules: { 20 | '.column': [ { '@pseudo': 'root' }, { display: transforms.trueHide(undefined, true) }, background, box, padding, border, borderRadius, 'vertical-align' ] 21 | }, 22 | 23 | render (attrs, contents) { 24 | const small = parseInt(attrs.small, 10) 25 | const large = parseInt(attrs.large, 10) 26 | const largeWidth = `${Math.round((100 * large) / 12)}%` 27 | attrs.class += ` column col-sm-${small}` 28 | 29 | delete attrs.large 30 | delete attrs.small 31 | 32 | return ([ 33 | 34 | {contents.length === 0 ? ' ' : contents} 35 | , 36 | small === large ? '' : () 45 | ]) 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Container.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms, cssGroups, condition } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Style from './Style' 3 | 4 | const { 5 | trueHide, 6 | ieAlignFallback } = transforms 7 | 8 | const { 9 | background, 10 | margin, 11 | padding, 12 | border, 13 | borderRadius, 14 | width, 15 | height, 16 | table, 17 | box } = cssGroups 18 | 19 | export default createElement('container', { 20 | containsText: true, 21 | 22 | rules: { 23 | '.container': [ { '@pseudo': 'root' }, { display: trueHide('block') }, margin, width ], 24 | 25 | '.container__table__ie': [ 'width', 'max-width', { [margin]: ieAlignFallback } ], 26 | 27 | '.container__table': [ { '@pseudo': 'table' }, table ], 28 | 29 | '.container__row': [ { '@pseudo': 'row' } ], 30 | 31 | '.container__cell': [ { '@pseudo': 'cell' }, height, background, box, padding, border, borderRadius ] 32 | }, 33 | 34 | render (attrs, contents) { 35 | attrs.class += ' container' 36 | return ( 37 |
38 | {condition('mso | IE', ``)} 45 | 52 |
53 | ) 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Font.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | 3 | export default createElement('font', { 4 | parent: [ 'head' ], 5 | children: false, 6 | defaultAttrs: { href: '' }, 7 | 8 | render (attrs, contents) { 9 | return ([ 10 | ``, 11 | , 12 | , 15 | `` 16 | ]) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Head.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Subject from './Subject' 3 | import Style from './Style' 4 | 5 | export default createElement('head', { 6 | unique: true, 7 | parent: [ 'heml' ], 8 | attrs: [], 9 | 10 | async render (attrs, contents) { 11 | return ([ 12 | {/* Fake head for Yahoo */} , 13 | 14 | 15 | 16 | 17 | 18 | {/* */ 19 | ``} 20 | 21 | {``} 22 | 23 | {/* http://tabletrtd.com/opening-css-resets/ */} 24 | 34 | 44 | {``} 50 | {Subject.flush()} 51 | {await Style.flush()} 52 | {``} 53 | {/* drop in the contents */ 54 | contents} 55 | {/* https://litmus.com/community/discussions/151-mystery-solved-dpi-scaling-in-outlook-2007-2013 */ 56 | ``} 62 | ]) 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Heml.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | 3 | export default createElement('heml', { 4 | unique: true, 5 | parent: [], 6 | children: [ 'head', 'body' ], 7 | defaultAttrs: { 8 | 'lang': 'en', 9 | 'xmlns': 'http://www.w3.org/1999/xhtml', 10 | 'xmlns:v': 'urn:schemas-microsoft-com:vml', 11 | 'xmlns:o': 'urn:schemas-microsoft-com:office:office' 12 | }, 13 | 14 | render (attrs, contents) { 15 | return ([ 16 | ``, 17 | 18 | {contents} 19 | ]) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Hr.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms, cssGroups, condition } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Style from './Style' 3 | 4 | const { 5 | trueHide, 6 | ieAlignFallback } = transforms 7 | 8 | const { 9 | background, 10 | margin, 11 | padding, 12 | border, 13 | borderRadius, 14 | width, 15 | height, 16 | table, 17 | box } = cssGroups 18 | 19 | export default createElement('hr', { 20 | children: false, 21 | 22 | rules: { 23 | '.hr': [ { '@pseudo': 'root' }, { display: trueHide() }, margin, width ], 24 | 25 | '.hr__table__ie': [ 'width', 'max-width', { [margin]: ieAlignFallback } ], 26 | 27 | '.hr__table': [ { '@pseudo': 'table' }, table ], 28 | 29 | '.hr__row': [ { '@pseudo': 'row' } ], 30 | 31 | '.hr__cell': [ { '@pseudo': 'cell' }, height, background, box, padding, border, borderRadius, 'vertical-align' ] 32 | }, 33 | 34 | render (attrs, contents) { 35 | attrs.class += ' hr' 36 | return ( 37 |
38 | {condition('mso | IE', ``)} 45 | 52 |
53 | ) 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Img.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Style from './Style' 3 | import { omit, has } from 'lodash' 4 | import fs from 'fs-extra' 5 | import isAbsoluteUrl from 'is-absolute-url' 6 | import axios from 'axios' 7 | import sizeOf from 'image-size' 8 | 9 | export default createElement('img', { 10 | attrs: [ 'src', 'width', 'height', 'alt', 'infer', 'inline', 'style' ], 11 | children: false, 12 | defaultAttrs: { 13 | border: '0', 14 | alt: '' 15 | }, 16 | 17 | rules: { 18 | 'img': [ { '@pseudo': 'root' }, { display: transforms.trueHide() }, '@default' ] 19 | }, 20 | 21 | async render (attrs, contents) { 22 | const isBlock = !attrs.inline 23 | 24 | if (!!attrs.infer && has(attrs, 'src') && !attrs.width) { 25 | attrs.width = await getWidth(attrs.src, attrs.infer === 'retina') 26 | } 27 | 28 | attrs.class += ` ${isBlock ? 'img__block' : 'img__inline'}` 29 | attrs.style = isBlock ? '' : 'display: inline-block;' 30 | 31 | return ([ 32 | , 33 | ]) 39 | } 40 | }) 41 | 42 | async function getWidth (path, isRetina) { 43 | try { 44 | const image = await (isAbsoluteUrl(path) ? getRemoteBuffer(path) : fs.readFile(path)) 45 | 46 | const { width } = sizeOf(image) 47 | if (!width) { return 'auto' } 48 | 49 | return isRetina ? Math.round(width / 2) : width 50 | } catch (e) { 51 | return 'auto' // if we fail fall back to auto 52 | } 53 | } 54 | 55 | function getRemoteBuffer (path) { 56 | return axios({ 57 | method: 'get', 58 | url: path, 59 | responseType: 'arraybuffer' 60 | }) 61 | .then(({ data }) => { 62 | return Buffer.from(data, 'binary') 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Meta.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | 3 | let metaMap 4 | 5 | export default createElement('meta', { 6 | attrs: true, 7 | parent: [ 'head' ], 8 | 9 | preRender () { metaMap = new Map([ [ 'meta', [] ] ]) }, 10 | 11 | render (attrs, contents) { 12 | metaMap.get('meta').push(attrs) 13 | 14 | return true 15 | }, 16 | 17 | get (key) { 18 | return metaMap.get(key) 19 | }, 20 | 21 | set (key, value) { 22 | return metaMap.set(key, value) 23 | }, 24 | 25 | flush () { 26 | let metaObject = {} 27 | 28 | for (let [ key, value ] of metaMap) { 29 | metaObject[key] = value 30 | } 31 | 32 | metaMap = null 33 | 34 | return metaObject 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Preview.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Meta from './Meta' 3 | 4 | export default createElement('preview', { 5 | parent: [ 'head' ], 6 | unique: true, 7 | 8 | render (attrs, contents) { 9 | Meta.set('preview', contents) 10 | 11 | return false 12 | }, 13 | 14 | flush () { 15 | const preview = Meta.get('preview') 16 | 17 | return preview ? : '' 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Row.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import { sum, max, isUndefined } from 'lodash' 3 | 4 | export default createElement('row', { 5 | children: [ 'column' ], 6 | 7 | rules: { 8 | '.row': [ { '@pseudo': 'root' }, { display: transforms.trueHide('block') } ], 9 | 10 | '.row__table': [ { '@pseudo': 'table' } ], 11 | 12 | '.row__row': [ { '@pseudo': 'row' } ] 13 | }, 14 | 15 | render (attrs, contents) { 16 | attrs.class += ' row' 17 | return ( 18 |
19 | 20 | {contents} 21 | 22 |
) 23 | }, 24 | 25 | preRender ({ $ }) { 26 | $.findNodes('row').forEach(($row) => { 27 | const $columns = $row.children().toNodes() 28 | const columnSizes = $columns.map(($column) => parseInt($column.attr('large') || 0, 10)) 29 | const remainingSpace = 12 - sum(columnSizes) 30 | const remainingColumns = columnSizes.filter((size) => size === 0).length 31 | const spacePerColumn = max([Math.floor(remainingSpace / remainingColumns), 1]) 32 | const overageSpace = remainingSpace - spacePerColumn * remainingColumns 33 | 34 | let filledColumns = 0 35 | $columns.forEach(($column) => { 36 | if (isUndefined($column.attr('large'))) { 37 | filledColumns++ 38 | $column.attr('large', spacePerColumn + (filledColumns === remainingColumns ? overageSpace : 0)) 39 | } 40 | }) 41 | 42 | // if they don't add up to 12 43 | // and there are no specified columns 44 | if (remainingColumns === 0 && remainingSpace > 0) { 45 | $row.append('') 46 | } 47 | }) 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Style.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import hemlstyles from '@heml/styles' 3 | import { castArray, isEqual, uniqWith, sortBy } from 'lodash' 4 | 5 | const START_EMBED_CSS = `/*!***START:EMBED_CSS*****/` 6 | const START_INLINE_CSS = `/*!***START:INLINE_CSS*****/` 7 | 8 | let styleMap 9 | let options 10 | 11 | export default createElement('style', { 12 | parent: [ 'head' ], 13 | attrs: [ 'for', 'heml-embed' ], 14 | defaultAttrs: { 15 | 'heml-embed': false, 16 | 'for': 'global' 17 | }, 18 | 19 | preRender ({ $, elements }) { 20 | styleMap = new Map([ [ 'global', [] ] ]) 21 | options = { 22 | plugins: [], 23 | elements: {}, 24 | aliases: {} 25 | } 26 | 27 | for (let element of elements) { 28 | if (element.postcss) { 29 | options.plugins = options.plugins.concat(castArray(element.postcss)) 30 | } 31 | 32 | if (element.rules) { 33 | options.elements[element.tagName] = element.rules 34 | } 35 | 36 | options.aliases[element.tagName] = $.findNodes(element.tagName) 37 | } 38 | }, 39 | 40 | render (attrs, contents) { 41 | if (!styleMap.get(attrs.for)) { 42 | styleMap.set(attrs.for, []) 43 | } 44 | 45 | styleMap.get(attrs.for).push({ 46 | embed: !!attrs['heml-embed'], 47 | ignore: !!attrs['heml-ignore'], 48 | css: contents 49 | }) 50 | 51 | return false 52 | }, 53 | 54 | async flush () { 55 | /** 56 | * reverse the styles so they fall in an order that mirrors their position 57 | * - they get rendered bottom to top - should be styled top to bottom 58 | * 59 | * the global styles should always be rendered last 60 | */ 61 | const globalStyles = styleMap.get('global') 62 | styleMap.delete('global') 63 | styleMap = new Map([...styleMap].reverse()) 64 | styleMap.set('global', globalStyles) 65 | 66 | let ignoredCSS = [] 67 | let fullCSS = '' 68 | 69 | /** combine the non-ignored css to be combined */ 70 | for (let [ element, styles ] of styleMap) { 71 | styles = uniqWith(styles, isEqual) 72 | styles = element === 'global' ? styles : sortBy(styles, ['embed', 'css']) 73 | 74 | styles.forEach(({ ignore, embed, css }) => { 75 | /** replace the ignored css with placeholders that will be swapped later */ 76 | if (ignore) { 77 | ignoredCSS.push({ embed, css }) 78 | fullCSS += ignoreComment(ignoredCSS.length - 1) 79 | } else if (embed) { 80 | fullCSS += `${START_EMBED_CSS}${css}` 81 | } else { 82 | fullCSS += `${START_INLINE_CSS}${css}` 83 | } 84 | }) 85 | } 86 | 87 | let { css: processedCss } = await hemlstyles(fullCSS, options) 88 | 89 | /** put the ignored css back in */ 90 | ignoredCSS.forEach(({ embed, css }, index) => { 91 | processedCss = processedCss.replace(ignoreComment(index), embed ? `${START_EMBED_CSS}${css}` : `${START_INLINE_CSS}${css}`) 92 | }) 93 | 94 | /** split on the dividers and map it so each part starts with INLINE or EMBED */ 95 | let processedCssParts = processedCss.split(/\/\*!\*\*\*START:/g).splice(1).map((css) => css.replace(/_CSS\*\*\*\*\*\//, '')) 96 | 97 | /** build the html */ 98 | let html = '' 99 | let lastType = null 100 | 101 | for (let cssPart of processedCssParts) { 102 | const css = cssPart.replace(/^(EMBED|INLINE)/, '') 103 | const type = cssPart.startsWith('EMBED') ? 'EMBED' : 'INLINE' 104 | 105 | if (type === lastType) { 106 | html += css 107 | } else { 108 | lastType = type 109 | html += `${html === '' ? '' : ''}\n${css}\n` 110 | } 111 | } 112 | 113 | html += '' 114 | 115 | /** reset the styles and options */ 116 | styleMap = options = null 117 | 118 | return html 119 | } 120 | }) 121 | 122 | function ignoreComment (index) { 123 | return `/*!***IGNORE_${index}*****/` 124 | } 125 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Subject.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import Meta from './Meta' 3 | 4 | export default createElement('subject', { 5 | parent: [ 'head' ], 6 | unique: true, 7 | 8 | render (attrs, contents) { 9 | Meta.set('subject', contents) 10 | 11 | return false 12 | }, 13 | 14 | flush () { 15 | return Meta.get('subject') || '' 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Table.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | 3 | const Table = createElement('table', { 4 | attrs: true, 5 | containsText: true, 6 | rules: { 7 | '.table': [ { '@pseudo': 'root' }, '@default', { display: transforms.trueHide('table') } ] 8 | }, 9 | 10 | render (attrs, contents) { 11 | attrs.class += ' table' 12 | return {contents}
13 | } 14 | }) 15 | 16 | const Tr = createElement('tr', { 17 | attrs: true, 18 | containsText: true, 19 | rules: { 20 | '.tr': [ { '@pseudo': 'root' }, '@default' ] 21 | }, 22 | 23 | render (attrs, contents) { 24 | attrs.class += ' tr' 25 | return {contents} 26 | } 27 | }) 28 | 29 | const Td = createElement('td', { 30 | attrs: true, 31 | containsText: true, 32 | rules: { 33 | '.td': [ { '@pseudo': 'root' }, '@default' ] 34 | }, 35 | 36 | render (attrs, contents) { 37 | attrs.class += ' td' 38 | return {contents} 39 | } 40 | }) 41 | 42 | export { Table, Tr, Td } 43 | -------------------------------------------------------------------------------- /packages/heml-elements/src/Typography.js: -------------------------------------------------------------------------------- 1 | import HEML, { createElement, transforms, cssGroups } from '@heml/utils' // eslint-disable-line no-unused-vars 2 | import { merge } from 'lodash' 3 | 4 | const { 5 | margin, background, border, borderRadius, text, font 6 | } = cssGroups 7 | 8 | /** 9 | * create mergable text element 10 | * @param {String} name 11 | * @param {Object} element 12 | * @return {Object} 13 | */ 14 | function createTextElement (name, element = {}) { 15 | let classToAdd = '' 16 | const Tag = name 17 | 18 | if (/^h\d$/i.test(name)) { 19 | classToAdd = 'header' 20 | } else { 21 | classToAdd = 'text' 22 | } 23 | 24 | return createElement(name, merge({ 25 | attrs: true, 26 | rules: { 27 | [`.${name}.${classToAdd}`]: [ { '@pseudo': 'root' }, '@default', { display: transforms.trueHide() }, margin, background, border, borderRadius, text, font ] 28 | }, 29 | render (attrs, contents) { 30 | attrs.class += ` ${classToAdd} ${name}` 31 | 32 | return {contents} 33 | } 34 | }, element)) 35 | } 36 | 37 | const H1 = createTextElement('h1') 38 | const H2 = createTextElement('h2') 39 | const H3 = createTextElement('h3') 40 | const H4 = createTextElement('h4') 41 | const H5 = createTextElement('h5') 42 | const H6 = createTextElement('h6') 43 | const P = createTextElement('p') 44 | const Ol = createTextElement('ol') 45 | const Ul = createTextElement('ul') 46 | const Li = createTextElement('li') 47 | 48 | const A = createElement('a', { 49 | attrs: true, 50 | defaultAttrs: { href: '#' }, 51 | 52 | rules: { 53 | '.a': [ { '@pseudo': 'root' }, { '@default': true }, { display: transforms.trueHide('inline') }, 'color', 'text-decoration' ], 54 | '.a__text': [ { '@pseudo': 'text' }, 'color', 'text-decoration' ] 55 | }, 56 | 57 | render (attrs, contents) { 58 | attrs.class += ' a' 59 | return {contents} 60 | } 61 | }) 62 | 63 | export { H1, H2, H3, H4, H5, H6, P, Ol, Ul, Li, A } 64 | -------------------------------------------------------------------------------- /packages/heml-elements/src/index.js: -------------------------------------------------------------------------------- 1 | /* Document */ 2 | import Heml from './Heml' 3 | import Head from './Head' 4 | import Body from './Body' 5 | 6 | /* Meta */ 7 | import Meta from './Meta' 8 | import Subject from './Subject' 9 | import Preview from './Preview' 10 | import Base from './Base' 11 | import Font from './Font' 12 | import Style from './Style' 13 | 14 | /* Table */ 15 | import { Table, Tr, Td } from './Table' 16 | 17 | /* Grid */ 18 | import Block from './Block' 19 | import Container from './Container' 20 | import Row from './Row' 21 | import Column from './Column' 22 | 23 | /* Content */ 24 | import { H1, H2, H3, H4, H5, H6, P, Ol, Ul, Li, A } from './Typography' 25 | import Hr from './Hr' 26 | import Button from './Button' 27 | import Img from './Img' 28 | 29 | export { 30 | /* Document */ 31 | Heml, 32 | Head, 33 | Body, 34 | 35 | /* Meta */ 36 | Meta, 37 | Subject, 38 | Preview, 39 | Base, 40 | Font, 41 | Style, 42 | 43 | /* Table */ 44 | Table, 45 | Tr, 46 | Td, 47 | 48 | /* Grid */ 49 | Block, 50 | Container, 51 | Row, 52 | Column, 53 | 54 | /* Content */ 55 | H1, 56 | H2, 57 | H3, 58 | H4, 59 | H5, 60 | H6, 61 | P, 62 | Ol, 63 | Ul, 64 | Li, 65 | A, 66 | Hr, 67 | Button, 68 | Img 69 | } 70 | -------------------------------------------------------------------------------- /packages/heml-inline/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "ajv": { 6 | "version": "5.2.3", 7 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", 8 | "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", 9 | "requires": { 10 | "co": "4.6.0", 11 | "fast-deep-equal": "1.0.0", 12 | "json-schema-traverse": "0.3.1", 13 | "json-stable-stringify": "1.0.1" 14 | } 15 | }, 16 | "ansi-regex": { 17 | "version": "2.1.1", 18 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 19 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 20 | }, 21 | "ansi-styles": { 22 | "version": "2.2.1", 23 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 24 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" 25 | }, 26 | "asn1": { 27 | "version": "0.2.3", 28 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", 29 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" 30 | }, 31 | "assert-plus": { 32 | "version": "1.0.0", 33 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 34 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 35 | }, 36 | "async": { 37 | "version": "2.5.0", 38 | "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", 39 | "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", 40 | "requires": { 41 | "lodash": "4.17.4" 42 | }, 43 | "dependencies": { 44 | "lodash": { 45 | "version": "4.17.4", 46 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 47 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 48 | } 49 | } 50 | }, 51 | "asynckit": { 52 | "version": "0.4.0", 53 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 54 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 55 | }, 56 | "aws-sign2": { 57 | "version": "0.7.0", 58 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 59 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 60 | }, 61 | "aws4": { 62 | "version": "1.6.0", 63 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", 64 | "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" 65 | }, 66 | "bcrypt-pbkdf": { 67 | "version": "1.0.1", 68 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", 69 | "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", 70 | "optional": true, 71 | "requires": { 72 | "tweetnacl": "0.14.5" 73 | } 74 | }, 75 | "boolbase": { 76 | "version": "1.0.0", 77 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 78 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" 79 | }, 80 | "boom": { 81 | "version": "4.3.1", 82 | "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", 83 | "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", 84 | "requires": { 85 | "hoek": "4.2.0" 86 | } 87 | }, 88 | "caseless": { 89 | "version": "0.12.0", 90 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 91 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 92 | }, 93 | "chalk": { 94 | "version": "1.1.3", 95 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 96 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 97 | "requires": { 98 | "ansi-styles": "2.2.1", 99 | "escape-string-regexp": "1.0.5", 100 | "has-ansi": "2.0.0", 101 | "strip-ansi": "3.0.1", 102 | "supports-color": "2.0.0" 103 | } 104 | }, 105 | "cheerio": { 106 | "version": "0.22.0", 107 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", 108 | "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", 109 | "requires": { 110 | "css-select": "1.2.0", 111 | "dom-serializer": "0.1.0", 112 | "entities": "1.1.1", 113 | "htmlparser2": "3.9.2", 114 | "lodash.assignin": "4.2.0", 115 | "lodash.bind": "4.2.1", 116 | "lodash.defaults": "4.2.0", 117 | "lodash.filter": "4.6.0", 118 | "lodash.flatten": "4.4.0", 119 | "lodash.foreach": "4.5.0", 120 | "lodash.map": "4.6.0", 121 | "lodash.merge": "4.6.0", 122 | "lodash.pick": "4.4.0", 123 | "lodash.reduce": "4.6.0", 124 | "lodash.reject": "4.6.0", 125 | "lodash.some": "4.6.0" 126 | } 127 | }, 128 | "co": { 129 | "version": "4.6.0", 130 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 131 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 132 | }, 133 | "combined-stream": { 134 | "version": "1.0.5", 135 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", 136 | "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", 137 | "requires": { 138 | "delayed-stream": "1.0.0" 139 | } 140 | }, 141 | "commander": { 142 | "version": "2.9.0", 143 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", 144 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", 145 | "requires": { 146 | "graceful-readlink": "1.0.1" 147 | } 148 | }, 149 | "core-util-is": { 150 | "version": "1.0.2", 151 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 152 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 153 | }, 154 | "cross-spawn": { 155 | "version": "5.1.0", 156 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", 157 | "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", 158 | "requires": { 159 | "lru-cache": "4.1.1", 160 | "shebang-command": "1.2.0", 161 | "which": "1.3.0" 162 | } 163 | }, 164 | "cryptiles": { 165 | "version": "3.1.2", 166 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", 167 | "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", 168 | "requires": { 169 | "boom": "5.2.0" 170 | }, 171 | "dependencies": { 172 | "boom": { 173 | "version": "5.2.0", 174 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 175 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 176 | "requires": { 177 | "hoek": "4.2.0" 178 | } 179 | } 180 | } 181 | }, 182 | "css-select": { 183 | "version": "1.2.0", 184 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", 185 | "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", 186 | "requires": { 187 | "boolbase": "1.0.0", 188 | "css-what": "2.1.0", 189 | "domutils": "1.5.1", 190 | "nth-check": "1.0.1" 191 | } 192 | }, 193 | "css-what": { 194 | "version": "2.1.0", 195 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", 196 | "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" 197 | }, 198 | "dashdash": { 199 | "version": "1.14.1", 200 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 201 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 202 | "requires": { 203 | "assert-plus": "1.0.0" 204 | } 205 | }, 206 | "datauri": { 207 | "version": "1.0.5", 208 | "resolved": "https://registry.npmjs.org/datauri/-/datauri-1.0.5.tgz", 209 | "integrity": "sha1-0JddGrbI8uDOPKQ7qkU5vhLSiaA=", 210 | "requires": { 211 | "image-size": "0.3.5", 212 | "mimer": "0.2.1", 213 | "semver": "5.4.1" 214 | } 215 | }, 216 | "deep-extend": { 217 | "version": "0.5.0", 218 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.0.tgz", 219 | "integrity": "sha1-bvSgmwX5iw41jW2T1Mo8rsZnKAM=" 220 | }, 221 | "delayed-stream": { 222 | "version": "1.0.0", 223 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 224 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 225 | }, 226 | "dom-serializer": { 227 | "version": "0.1.0", 228 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", 229 | "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", 230 | "requires": { 231 | "domelementtype": "1.1.3", 232 | "entities": "1.1.1" 233 | }, 234 | "dependencies": { 235 | "domelementtype": { 236 | "version": "1.1.3", 237 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", 238 | "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" 239 | } 240 | } 241 | }, 242 | "domelementtype": { 243 | "version": "1.3.0", 244 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", 245 | "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" 246 | }, 247 | "domhandler": { 248 | "version": "2.4.1", 249 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", 250 | "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", 251 | "requires": { 252 | "domelementtype": "1.3.0" 253 | } 254 | }, 255 | "domutils": { 256 | "version": "1.5.1", 257 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", 258 | "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", 259 | "requires": { 260 | "dom-serializer": "0.1.0", 261 | "domelementtype": "1.3.0" 262 | } 263 | }, 264 | "ecc-jsbn": { 265 | "version": "0.1.1", 266 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", 267 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", 268 | "optional": true, 269 | "requires": { 270 | "jsbn": "0.1.1" 271 | } 272 | }, 273 | "entities": { 274 | "version": "1.1.1", 275 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", 276 | "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" 277 | }, 278 | "escape-string-regexp": { 279 | "version": "1.0.5", 280 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 281 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 282 | }, 283 | "extend": { 284 | "version": "3.0.1", 285 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 286 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 287 | }, 288 | "extsprintf": { 289 | "version": "1.3.0", 290 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 291 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 292 | }, 293 | "fast-deep-equal": { 294 | "version": "1.0.0", 295 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", 296 | "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" 297 | }, 298 | "forever-agent": { 299 | "version": "0.6.1", 300 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 301 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 302 | }, 303 | "form-data": { 304 | "version": "2.3.1", 305 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", 306 | "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", 307 | "requires": { 308 | "asynckit": "0.4.0", 309 | "combined-stream": "1.0.5", 310 | "mime-types": "2.1.17" 311 | } 312 | }, 313 | "getpass": { 314 | "version": "0.1.7", 315 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 316 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 317 | "requires": { 318 | "assert-plus": "1.0.0" 319 | } 320 | }, 321 | "graceful-readlink": { 322 | "version": "1.0.1", 323 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 324 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" 325 | }, 326 | "har-schema": { 327 | "version": "2.0.0", 328 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 329 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 330 | }, 331 | "har-validator": { 332 | "version": "5.0.3", 333 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", 334 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", 335 | "requires": { 336 | "ajv": "5.2.3", 337 | "har-schema": "2.0.0" 338 | } 339 | }, 340 | "has-ansi": { 341 | "version": "2.0.0", 342 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 343 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 344 | "requires": { 345 | "ansi-regex": "2.1.1" 346 | } 347 | }, 348 | "hawk": { 349 | "version": "6.0.2", 350 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", 351 | "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", 352 | "requires": { 353 | "boom": "4.3.1", 354 | "cryptiles": "3.1.2", 355 | "hoek": "4.2.0", 356 | "sntp": "2.0.2" 357 | } 358 | }, 359 | "hoek": { 360 | "version": "4.2.0", 361 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", 362 | "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" 363 | }, 364 | "htmlparser2": { 365 | "version": "3.9.2", 366 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", 367 | "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", 368 | "requires": { 369 | "domelementtype": "1.3.0", 370 | "domhandler": "2.4.1", 371 | "domutils": "1.5.1", 372 | "entities": "1.1.1", 373 | "inherits": "2.0.3", 374 | "readable-stream": "2.3.3" 375 | } 376 | }, 377 | "http-signature": { 378 | "version": "1.2.0", 379 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 380 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 381 | "requires": { 382 | "assert-plus": "1.0.0", 383 | "jsprim": "1.4.1", 384 | "sshpk": "1.13.1" 385 | } 386 | }, 387 | "image-size": { 388 | "version": "0.3.5", 389 | "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.3.5.tgz", 390 | "integrity": "sha1-gyQOqy+1sAsEqrjHSwRx6cunrYw=" 391 | }, 392 | "inherits": { 393 | "version": "2.0.3", 394 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 395 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 396 | }, 397 | "is-typedarray": { 398 | "version": "1.0.0", 399 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 400 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 401 | }, 402 | "isarray": { 403 | "version": "1.0.0", 404 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 405 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 406 | }, 407 | "isexe": { 408 | "version": "2.0.0", 409 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 410 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 411 | }, 412 | "isstream": { 413 | "version": "0.1.2", 414 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 415 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 416 | }, 417 | "jsbn": { 418 | "version": "0.1.1", 419 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 420 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 421 | "optional": true 422 | }, 423 | "json-schema": { 424 | "version": "0.2.3", 425 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 426 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 427 | }, 428 | "json-schema-traverse": { 429 | "version": "0.3.1", 430 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 431 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" 432 | }, 433 | "json-stable-stringify": { 434 | "version": "1.0.1", 435 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", 436 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", 437 | "requires": { 438 | "jsonify": "0.0.0" 439 | } 440 | }, 441 | "json-stringify-safe": { 442 | "version": "5.0.1", 443 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 444 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 445 | }, 446 | "jsonify": { 447 | "version": "0.0.0", 448 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", 449 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" 450 | }, 451 | "jsprim": { 452 | "version": "1.4.1", 453 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 454 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 455 | "requires": { 456 | "assert-plus": "1.0.0", 457 | "extsprintf": "1.3.0", 458 | "json-schema": "0.2.3", 459 | "verror": "1.10.0" 460 | } 461 | }, 462 | "juice": { 463 | "version": "4.2.0", 464 | "resolved": "https://registry.npmjs.org/juice/-/juice-4.2.0.tgz", 465 | "integrity": "sha512-JCOZtBxh/BnzYNeHYPs3EhFjD5uUKpmsaOXMO7aeZuDH4HrSTdIpBtoWE2mUCb+5F9KysImSUGmKHpIULSd5HQ==", 466 | "requires": { 467 | "cheerio": "0.22.0", 468 | "commander": "2.9.0", 469 | "cross-spawn": "5.1.0", 470 | "deep-extend": "0.5.0", 471 | "mensch": "0.3.3", 472 | "slick": "1.12.2", 473 | "web-resource-inliner": "4.2.0" 474 | } 475 | }, 476 | "lodash": { 477 | "version": "4.17.4", 478 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 479 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 480 | }, 481 | "lodash.assignin": { 482 | "version": "4.2.0", 483 | "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", 484 | "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" 485 | }, 486 | "lodash.bind": { 487 | "version": "4.2.1", 488 | "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", 489 | "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" 490 | }, 491 | "lodash.defaults": { 492 | "version": "4.2.0", 493 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 494 | "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" 495 | }, 496 | "lodash.filter": { 497 | "version": "4.6.0", 498 | "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", 499 | "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" 500 | }, 501 | "lodash.flatten": { 502 | "version": "4.4.0", 503 | "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", 504 | "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" 505 | }, 506 | "lodash.foreach": { 507 | "version": "4.5.0", 508 | "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", 509 | "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" 510 | }, 511 | "lodash.map": { 512 | "version": "4.6.0", 513 | "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", 514 | "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" 515 | }, 516 | "lodash.merge": { 517 | "version": "4.6.0", 518 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", 519 | "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=" 520 | }, 521 | "lodash.pick": { 522 | "version": "4.4.0", 523 | "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", 524 | "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" 525 | }, 526 | "lodash.reduce": { 527 | "version": "4.6.0", 528 | "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", 529 | "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" 530 | }, 531 | "lodash.reject": { 532 | "version": "4.6.0", 533 | "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", 534 | "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" 535 | }, 536 | "lodash.some": { 537 | "version": "4.6.0", 538 | "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", 539 | "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" 540 | }, 541 | "lodash.unescape": { 542 | "version": "4.0.1", 543 | "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", 544 | "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" 545 | }, 546 | "lru-cache": { 547 | "version": "4.1.1", 548 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", 549 | "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", 550 | "requires": { 551 | "pseudomap": "1.0.2", 552 | "yallist": "2.1.2" 553 | } 554 | }, 555 | "mensch": { 556 | "version": "0.3.3", 557 | "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.3.tgz", 558 | "integrity": "sha1-4gD/TdgjcX+OBWOzLj9UgfyiYrI=" 559 | }, 560 | "mime-db": { 561 | "version": "1.30.0", 562 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 563 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 564 | }, 565 | "mime-types": { 566 | "version": "2.1.17", 567 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 568 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 569 | "requires": { 570 | "mime-db": "1.30.0" 571 | } 572 | }, 573 | "mimer": { 574 | "version": "0.2.1", 575 | "resolved": "https://registry.npmjs.org/mimer/-/mimer-0.2.1.tgz", 576 | "integrity": "sha1-xjxaF/6GQj9RYahdVcPtUYm6r/w=" 577 | }, 578 | "nth-check": { 579 | "version": "1.0.1", 580 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", 581 | "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", 582 | "requires": { 583 | "boolbase": "1.0.0" 584 | } 585 | }, 586 | "oauth-sign": { 587 | "version": "0.8.2", 588 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", 589 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" 590 | }, 591 | "performance-now": { 592 | "version": "2.1.0", 593 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 594 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 595 | }, 596 | "process-nextick-args": { 597 | "version": "1.0.7", 598 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 599 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" 600 | }, 601 | "pseudomap": { 602 | "version": "1.0.2", 603 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 604 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 605 | }, 606 | "punycode": { 607 | "version": "1.4.1", 608 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 609 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 610 | }, 611 | "qs": { 612 | "version": "6.5.1", 613 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 614 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 615 | }, 616 | "readable-stream": { 617 | "version": "2.3.3", 618 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", 619 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", 620 | "requires": { 621 | "core-util-is": "1.0.2", 622 | "inherits": "2.0.3", 623 | "isarray": "1.0.0", 624 | "process-nextick-args": "1.0.7", 625 | "safe-buffer": "5.1.1", 626 | "string_decoder": "1.0.3", 627 | "util-deprecate": "1.0.2" 628 | } 629 | }, 630 | "request": { 631 | "version": "2.83.0", 632 | "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", 633 | "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", 634 | "requires": { 635 | "aws-sign2": "0.7.0", 636 | "aws4": "1.6.0", 637 | "caseless": "0.12.0", 638 | "combined-stream": "1.0.5", 639 | "extend": "3.0.1", 640 | "forever-agent": "0.6.1", 641 | "form-data": "2.3.1", 642 | "har-validator": "5.0.3", 643 | "hawk": "6.0.2", 644 | "http-signature": "1.2.0", 645 | "is-typedarray": "1.0.0", 646 | "isstream": "0.1.2", 647 | "json-stringify-safe": "5.0.1", 648 | "mime-types": "2.1.17", 649 | "oauth-sign": "0.8.2", 650 | "performance-now": "2.1.0", 651 | "qs": "6.5.1", 652 | "safe-buffer": "5.1.1", 653 | "stringstream": "0.0.5", 654 | "tough-cookie": "2.3.3", 655 | "tunnel-agent": "0.6.0", 656 | "uuid": "3.1.0" 657 | } 658 | }, 659 | "safe-buffer": { 660 | "version": "5.1.1", 661 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 662 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 663 | }, 664 | "semver": { 665 | "version": "5.4.1", 666 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 667 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" 668 | }, 669 | "shebang-command": { 670 | "version": "1.2.0", 671 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 672 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 673 | "requires": { 674 | "shebang-regex": "1.0.0" 675 | } 676 | }, 677 | "shebang-regex": { 678 | "version": "1.0.0", 679 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 680 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" 681 | }, 682 | "slick": { 683 | "version": "1.12.2", 684 | "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", 685 | "integrity": "sha1-vQSN23TefRymkV+qSldXCzVQwtc=" 686 | }, 687 | "sntp": { 688 | "version": "2.0.2", 689 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", 690 | "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=", 691 | "requires": { 692 | "hoek": "4.2.0" 693 | } 694 | }, 695 | "sshpk": { 696 | "version": "1.13.1", 697 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", 698 | "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", 699 | "requires": { 700 | "asn1": "0.2.3", 701 | "assert-plus": "1.0.0", 702 | "bcrypt-pbkdf": "1.0.1", 703 | "dashdash": "1.14.1", 704 | "ecc-jsbn": "0.1.1", 705 | "getpass": "0.1.7", 706 | "jsbn": "0.1.1", 707 | "tweetnacl": "0.14.5" 708 | } 709 | }, 710 | "string_decoder": { 711 | "version": "1.0.3", 712 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 713 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 714 | "requires": { 715 | "safe-buffer": "5.1.1" 716 | } 717 | }, 718 | "stringstream": { 719 | "version": "0.0.5", 720 | "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", 721 | "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" 722 | }, 723 | "strip-ansi": { 724 | "version": "3.0.1", 725 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 726 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 727 | "requires": { 728 | "ansi-regex": "2.1.1" 729 | } 730 | }, 731 | "supports-color": { 732 | "version": "2.0.0", 733 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 734 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" 735 | }, 736 | "tough-cookie": { 737 | "version": "2.3.3", 738 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", 739 | "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", 740 | "requires": { 741 | "punycode": "1.4.1" 742 | } 743 | }, 744 | "tunnel-agent": { 745 | "version": "0.6.0", 746 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 747 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 748 | "requires": { 749 | "safe-buffer": "5.1.1" 750 | } 751 | }, 752 | "tweetnacl": { 753 | "version": "0.14.5", 754 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 755 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 756 | "optional": true 757 | }, 758 | "util-deprecate": { 759 | "version": "1.0.2", 760 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 761 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 762 | }, 763 | "uuid": { 764 | "version": "3.1.0", 765 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 766 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 767 | }, 768 | "valid-data-url": { 769 | "version": "0.1.4", 770 | "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-0.1.4.tgz", 771 | "integrity": "sha512-p3bCVl3Vrz42TV37a1OjagyLLd6qQAXBDWarIazuo7NQzCt8Kw8ZZwSAbUVPGlz5ubgbgJmgT0KRjLeCFNrfoQ==" 772 | }, 773 | "verror": { 774 | "version": "1.10.0", 775 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 776 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 777 | "requires": { 778 | "assert-plus": "1.0.0", 779 | "core-util-is": "1.0.2", 780 | "extsprintf": "1.3.0" 781 | } 782 | }, 783 | "web-resource-inliner": { 784 | "version": "4.2.0", 785 | "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-4.2.0.tgz", 786 | "integrity": "sha512-NvLvZzKvnNAB3LXG5c12WwUx5ZA7ZfNMYq82GnbhFyBLuu3jtamW4tQ40M02XiQzkFsyDuWG6Y2TOq9yywaxlg==", 787 | "requires": { 788 | "async": "2.5.0", 789 | "chalk": "1.1.3", 790 | "datauri": "1.0.5", 791 | "htmlparser2": "3.9.2", 792 | "lodash.unescape": "4.0.1", 793 | "request": "2.83.0", 794 | "valid-data-url": "0.1.4", 795 | "xtend": "4.0.1" 796 | } 797 | }, 798 | "which": { 799 | "version": "1.3.0", 800 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", 801 | "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", 802 | "requires": { 803 | "isexe": "2.0.0" 804 | } 805 | }, 806 | "xtend": { 807 | "version": "4.0.1", 808 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 809 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 810 | }, 811 | "yallist": { 812 | "version": "2.1.2", 813 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 814 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 815 | } 816 | } 817 | } 818 | -------------------------------------------------------------------------------- /packages/heml-inline/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/inline", 3 | "version": "1.1.2", 4 | "description": "CSS inliner for HEML", 5 | "keywords": [ 6 | "heml" 7 | ], 8 | "homepage": "https://heml.io", 9 | "bugs": "https://github.com/SparkPost/heml/issues", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SparkPost/heml.git" 14 | }, 15 | "author": "SparkPost (https://sparkpost.com)", 16 | "files": [ 17 | "build/" 18 | ], 19 | "main": "build/index.js", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "juice": "^4.2.0", 25 | "lodash": "^4.17.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/heml-inline/src/fixWidthsFor.js: -------------------------------------------------------------------------------- 1 | import { setProp, getProp } from './styleHelper' 2 | 3 | /** 4 | * Converts all width properties on the given tag to be a fixed value 5 | * when any given image has an ancestor with a fixed width 6 | * (fix for outlook) 7 | * @param {Cheerio} $ 8 | * @param {String} selector 9 | */ 10 | export default function fixWidthsFor ($, selector) { 11 | // get all relative widths and set them to fixed values by default 12 | $(`${selector}`).filter(`[width*="%"]`).toNodes().forEach(($node) => { 13 | const nodeWidth = $node.attr('width') 14 | /** 15 | * Gather all the parent percents and multiply them against 16 | * the image and fixed parent width 17 | */ 18 | let parentPercent = 1 19 | 20 | for (let $el of $node.parents().toNodes()) { 21 | const parentWidth = $el.attr('width') || getProp($el.attr('style'), 'width') 22 | 23 | if (parentWidth && !parentWidth.endsWith('%')) { 24 | const currentStyles = $node.attr('style') 25 | 26 | $node.attr('style', setProp(currentStyles, 'width', nodeWidth)) 27 | $node.attr('width', parseFloat(parentWidth, 10) * parentPercent * parseFloat(nodeWidth, 10) / 100) 28 | 29 | break 30 | } else if (parentWidth && parentWidth.endsWith('%')) { 31 | parentPercent = parentPercent * parseFloat(parentWidth, 10) / 100 32 | } 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /packages/heml-inline/src/index.js: -------------------------------------------------------------------------------- 1 | import juice from 'juice' 2 | import inlineMargins from './inlineMargins' 3 | import fixWidthsFor from './fixWidthsFor' 4 | import removeProcessingIds from './removeProcessingIds' 5 | import preferMaxWidth from './preferMaxWidth' 6 | 7 | function inline ($, options = {}) { 8 | const { juice: juiceOptions = {} } = options 9 | 10 | juice.juiceDocument($, { 11 | ...juiceOptions 12 | }) 13 | 14 | inlineMargins($) 15 | preferMaxWidth($, '[class$="__ie"]') 16 | fixWidthsFor($, 'img, .block__table__ie, .column') 17 | removeProcessingIds($) 18 | 19 | return $ 20 | } 21 | 22 | export default inline 23 | -------------------------------------------------------------------------------- /packages/heml-inline/src/inlineMargins.js: -------------------------------------------------------------------------------- 1 | import { compact, first, last, nth } from 'lodash' 2 | 3 | /** 4 | * finds all the tables that are centered with margins 5 | * and centers them with the align attribute 6 | * @param {Cheerio} $ 7 | */ 8 | function inlineMargins ($) { 9 | $('table[style*=margin]').toNodes().forEach(($el) => { 10 | const { left, right } = getSideMargins($el.attr('style')) 11 | 12 | if (left === 'auto' && right === 'auto') { 13 | $el.attr('align', 'center') 14 | } else if (left === 'auto' && right !== 'auto') { 15 | $el.attr('align', 'right') 16 | } else if (left !== 'auto') { 17 | $el.attr('align', 'left') 18 | } 19 | }) 20 | } 21 | 22 | /** 23 | * pulls the left and right margins from the given inline styles 24 | * @param {String} style the inline styles 25 | * @return {Object} object with left and right margins 26 | */ 27 | function getSideMargins (style) { 28 | const margins = compact(style.split(';')).map((decl) => { 29 | const split = decl.split(':') 30 | 31 | return { 32 | prop: first(split).trim().toLowerCase(), 33 | value: last(split).trim().toLowerCase() 34 | } 35 | }).filter(({ prop, value }) => prop.indexOf('margin') === 0) 36 | 37 | let left = 0 38 | let right = 0 39 | margins.forEach(({ prop, value }) => { 40 | if (prop === 'margin-left') { 41 | left = value 42 | return 43 | } 44 | 45 | if (prop === 'margin-right') { 46 | right = value 47 | return 48 | } 49 | 50 | if (prop === 'margin') { 51 | const values = value.split(' ').map((i) => i.trim()) 52 | 53 | switch (values.length) { 54 | case 1: 55 | right = first(values) 56 | left = first(values) 57 | break 58 | 59 | case 2: 60 | right = last(values) 61 | left = last(values) 62 | break 63 | 64 | case 3: 65 | right = nth(values, 1) 66 | left = nth(values, 1) 67 | break 68 | 69 | default: 70 | right = nth(values, 1) 71 | left = nth(values, 3) 72 | break 73 | } 74 | } 75 | }) 76 | 77 | return { left, right } 78 | } 79 | 80 | export default inlineMargins 81 | -------------------------------------------------------------------------------- /packages/heml-inline/src/preferMaxWidth.js: -------------------------------------------------------------------------------- 1 | import { setProp, getProp, removeProp } from './styleHelper' 2 | 3 | export default function preferMaxWidth ($, selector) { 4 | $(selector).toNodes().forEach(($node) => { 5 | const maxWidth = getProp($node.attr('style'), 'max-width') 6 | const width = $node.attr('width') || '' 7 | 8 | if (!maxWidth) { return } 9 | 10 | const maxWidthIsPxValue = maxWidth && maxWidth.endsWith('px') 11 | 12 | const maxWidthIsSmallerThenWidth = maxWidth.endsWith('%') && width.endsWith('%') && (parseInt(maxWidth, 10) < parseInt(width, 10)) 13 | 14 | if (maxWidthIsPxValue || maxWidthIsSmallerThenWidth) { 15 | let styles = removeProp($node.attr('style'), 'max-width') 16 | styles = removeProp(styles, 'width') 17 | 18 | $node.attr('width', maxWidth.replace('px', '')) 19 | $node.attr('style', setProp(styles, 'width', maxWidth)) 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/heml-inline/src/removeProcessingIds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * remove all ids used for processing only 3 | * @param {Cheerio} $ 4 | */ 5 | function removeProcessingIds ($) { 6 | $('[id^="heml-"]').removeAttr('id') 7 | } 8 | 9 | export default removeProcessingIds 10 | -------------------------------------------------------------------------------- /packages/heml-inline/src/styleHelper.js: -------------------------------------------------------------------------------- 1 | import { compact } from 'lodash' 2 | 3 | /** 4 | * Gets the value of a prop in a given inline style string 5 | * @param {String} style inline styles 6 | * @param {String} prop prop to get 7 | * 8 | * @return {String} style 9 | */ 10 | function getProp (style = '', prop) { 11 | prop = prop.trim().toLowerCase() 12 | const decls = style.split(';') 13 | 14 | let value = false 15 | 16 | decls.forEach((decl) => { 17 | if (decl.trim().toLowerCase().startsWith(`${prop}:`)) { 18 | value = decl.split(':')[1].trim() 19 | } 20 | }) 21 | 22 | return value 23 | } 24 | 25 | /** 26 | * Sets the value of a prop in a given inline style string 27 | * @param {String} style inline styles 28 | * @param {String} prop prop to update/add 29 | * @param {String} value new value 30 | * 31 | * @return {String} style 32 | */ 33 | function setProp (style = '', prop, value) { 34 | prop = prop.trim().toLowerCase() 35 | const decls = style.split(';') 36 | 37 | let updated = false 38 | 39 | const updatedDecls = decls.map((decl) => { 40 | if (decl.trim().toLowerCase().startsWith(`${prop}:`)) { 41 | updated = true 42 | return `${prop}: ${value}` 43 | } 44 | 45 | return decl 46 | }) 47 | 48 | if (!updated) { updatedDecls.push(`${prop}: ${value}`) } 49 | 50 | return compact(updatedDecls).join(';') 51 | } 52 | 53 | /** 54 | * removes a prop in a given inline style string 55 | * @param {String} style inline styles 56 | * @param {String} prop prop to remove 57 | * 58 | * @return {String} style 59 | */ 60 | function removeProp (style = '', prop) { 61 | prop = prop.trim().toLowerCase() 62 | const decls = style.split(';') 63 | 64 | const updatedDecls = decls.map((decl) => { 65 | if (decl.trim().toLowerCase().startsWith(`${prop}:`)) { 66 | return false 67 | } 68 | 69 | return decl 70 | }) 71 | 72 | return compact(updatedDecls).join(';') 73 | } 74 | 75 | export { getProp, setProp, removeProp } 76 | -------------------------------------------------------------------------------- /packages/heml-parse/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/parse", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "6.0.89", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.89.tgz", 10 | "integrity": "sha512-Z/67L97+6H1qJiEEHSN1SQapkWjDss1D90rAnFcQ6UxKkah9juzotK5UNEP1bDv/0lJ3NAQTnVfc/JWdgCGruA==" 11 | }, 12 | "boolbase": { 13 | "version": "1.0.0", 14 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 15 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" 16 | }, 17 | "cheerio": { 18 | "version": "1.0.0-rc.2", 19 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", 20 | "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", 21 | "requires": { 22 | "css-select": "1.2.0", 23 | "dom-serializer": "0.1.0", 24 | "entities": "1.1.1", 25 | "htmlparser2": "3.9.2", 26 | "lodash": "4.17.4", 27 | "parse5": "3.0.2" 28 | } 29 | }, 30 | "core-util-is": { 31 | "version": "1.0.2", 32 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 33 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 34 | }, 35 | "crypto-random-string": { 36 | "version": "1.0.0", 37 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", 38 | "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" 39 | }, 40 | "css-select": { 41 | "version": "1.2.0", 42 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", 43 | "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", 44 | "requires": { 45 | "boolbase": "1.0.0", 46 | "css-what": "2.1.0", 47 | "domutils": "1.5.1", 48 | "nth-check": "1.0.1" 49 | } 50 | }, 51 | "css-what": { 52 | "version": "2.1.0", 53 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", 54 | "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" 55 | }, 56 | "dom-serializer": { 57 | "version": "0.1.0", 58 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", 59 | "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", 60 | "requires": { 61 | "domelementtype": "1.1.3", 62 | "entities": "1.1.1" 63 | }, 64 | "dependencies": { 65 | "domelementtype": { 66 | "version": "1.1.3", 67 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", 68 | "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" 69 | } 70 | } 71 | }, 72 | "domelementtype": { 73 | "version": "1.3.0", 74 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", 75 | "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" 76 | }, 77 | "domhandler": { 78 | "version": "2.4.1", 79 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", 80 | "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", 81 | "requires": { 82 | "domelementtype": "1.3.0" 83 | } 84 | }, 85 | "domutils": { 86 | "version": "1.5.1", 87 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", 88 | "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", 89 | "requires": { 90 | "dom-serializer": "0.1.0", 91 | "domelementtype": "1.3.0" 92 | } 93 | }, 94 | "entities": { 95 | "version": "1.1.1", 96 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", 97 | "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" 98 | }, 99 | "html-tags": { 100 | "version": "2.0.0", 101 | "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", 102 | "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=" 103 | }, 104 | "htmlparser2": { 105 | "version": "3.9.2", 106 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", 107 | "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", 108 | "requires": { 109 | "domelementtype": "1.3.0", 110 | "domhandler": "2.4.1", 111 | "domutils": "1.5.1", 112 | "entities": "1.1.1", 113 | "inherits": "2.0.3", 114 | "readable-stream": "2.3.3" 115 | } 116 | }, 117 | "inherits": { 118 | "version": "2.0.3", 119 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 120 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 121 | }, 122 | "isarray": { 123 | "version": "1.0.0", 124 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 125 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 126 | }, 127 | "lodash": { 128 | "version": "4.17.4", 129 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 130 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 131 | }, 132 | "nth-check": { 133 | "version": "1.0.1", 134 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", 135 | "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", 136 | "requires": { 137 | "boolbase": "1.0.0" 138 | } 139 | }, 140 | "parse5": { 141 | "version": "3.0.2", 142 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.2.tgz", 143 | "integrity": "sha1-Be/1fw70V3+xRKefi5qWemzERRA=", 144 | "requires": { 145 | "@types/node": "6.0.89" 146 | } 147 | }, 148 | "process-nextick-args": { 149 | "version": "1.0.7", 150 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 151 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" 152 | }, 153 | "readable-stream": { 154 | "version": "2.3.3", 155 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", 156 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", 157 | "requires": { 158 | "core-util-is": "1.0.2", 159 | "inherits": "2.0.3", 160 | "isarray": "1.0.0", 161 | "process-nextick-args": "1.0.7", 162 | "safe-buffer": "5.1.1", 163 | "string_decoder": "1.0.3", 164 | "util-deprecate": "1.0.2" 165 | } 166 | }, 167 | "safe-buffer": { 168 | "version": "5.1.1", 169 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 170 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 171 | }, 172 | "string_decoder": { 173 | "version": "1.0.3", 174 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 175 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 176 | "requires": { 177 | "safe-buffer": "5.1.1" 178 | } 179 | }, 180 | "util-deprecate": { 181 | "version": "1.0.2", 182 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 183 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /packages/heml-parse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/parse", 3 | "version": "1.1.2", 4 | "description": "Parser for HEML", 5 | "keywords": [ 6 | "heml" 7 | ], 8 | "homepage": "https://heml.io", 9 | "bugs": "https://github.com/SparkPost/heml/issues", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SparkPost/heml.git" 14 | }, 15 | "author": "SparkPost (https://sparkpost.com)", 16 | "files": [ 17 | "build/" 18 | ], 19 | "main": "build/index.js", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "cheerio": "^1.0.0-rc.2", 25 | "crypto-random-string": "^1.0.0", 26 | "html-tags": "^2.0.0", 27 | "lodash": "^4.17.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/heml-parse/src/index.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio' 2 | import { difference, compact, first } from 'lodash' 3 | import randomString from 'crypto-random-string' 4 | import htmlTags from 'html-tags' 5 | import selfClosingHtmlTags from 'html-tags/void' 6 | 7 | const wrappingHtmlTags = difference(htmlTags, selfClosingHtmlTags) 8 | 9 | function parse (contents, options = {}) { 10 | const { 11 | elements = [], 12 | cheerio: cheerioOptions = {} 13 | } = options 14 | 15 | const $ = load(contents, { 16 | xmlMode: true, 17 | lowerCaseTags: true, 18 | decodeEntities: false, 19 | ...cheerioOptions 20 | }) 21 | 22 | $.findNodes = function (q) { 23 | return $(Array.isArray(q) ? q.join(',') : q) 24 | .not('[heml-ignore]') 25 | .toNodes() 26 | } 27 | 28 | $.prototype.toNodes = function () { 29 | return this 30 | .toArray() 31 | .map((node) => $(node)) 32 | } 33 | 34 | const selfClosingTags = [ 35 | ...selfClosingHtmlTags, 36 | ...elements.filter((element) => element.children === false).map(({ tagName }) => tagName) ] 37 | const wrappingTags = [ 38 | ...wrappingHtmlTags, 39 | ...elements.filter((element) => element.children !== false).map(({ tagName }) => tagName) ] 40 | 41 | const $selfClosingNodes = $.findNodes(selfClosingTags).reverse() 42 | const $wrappingNodes = $.findNodes(wrappingTags).reverse() 43 | 44 | /** Move contents from self wrapping tags outside of itself */ 45 | $selfClosingNodes.forEach(($node) => { 46 | $node.after($node.html()) 47 | $node.html('') 48 | }) 49 | 50 | /** ensure that all wrapping tags have at least a zero-width, non-joining character */ 51 | $wrappingNodes.forEach(($node) => { 52 | if ($node.html().length === 0) { 53 | $node.html(' ') 54 | } 55 | }) 56 | 57 | /** try for head, fallback to body, then heml */ 58 | const $head = first(compact([...$('head').toNodes(), ...$('body').toNodes(), ...$('heml').toNodes()])) 59 | 60 | /** move inline styles to a style tag with unique ids so they can be hit by the css processor */ 61 | if ($head) { 62 | const $inlineStyleNodes = $.findNodes(elements.map(({ tagName }) => tagName)).filter($node => !!$node.attr('style')) 63 | 64 | const inlineCSS = $inlineStyleNodes.map(($node) => { 65 | let id = $node.attr('id') 66 | const css = $node.attr('style') 67 | $node.removeAttr('style') 68 | 69 | if (!id) { 70 | id = `heml-${randomString(5)}` 71 | $node.attr('id', id) 72 | } 73 | 74 | return `#${id} {${css}}` 75 | }).join('\n') 76 | 77 | $head.append(``) 78 | } 79 | 80 | return $ 81 | } 82 | 83 | export default parse 84 | -------------------------------------------------------------------------------- /packages/heml-render/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "escape-goat": { 6 | "version": "1.3.0", 7 | "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-1.3.0.tgz", 8 | "integrity": "sha512-E2nU1Y39N5UgfLU8qwMlK0vZrZprIwWLeVmDYN8wd/e37hMtGzu2w1DBiREts0XHfgyZEQlj/hYr0H0izF0HDQ==" 9 | }, 10 | "html-tags": { 11 | "version": "2.0.0", 12 | "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", 13 | "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=" 14 | }, 15 | "is-promise": { 16 | "version": "2.1.0", 17 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 18 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 19 | }, 20 | "lodash": { 21 | "version": "4.17.4", 22 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 23 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 24 | }, 25 | "stringify-attributes": { 26 | "version": "1.0.0", 27 | "resolved": "https://registry.npmjs.org/stringify-attributes/-/stringify-attributes-1.0.0.tgz", 28 | "integrity": "sha1-nosvmpRn57SAk8shJOvBwX5jgsU=", 29 | "requires": { 30 | "escape-goat": "1.3.0" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/heml-render/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/render", 3 | "version": "1.1.2", 4 | "description": "Renderer for HEML", 5 | "keywords": [ 6 | "heml" 7 | ], 8 | "homepage": "https://heml.io", 9 | "bugs": "https://github.com/SparkPost/heml/issues", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SparkPost/heml.git" 14 | }, 15 | "author": "SparkPost (https://sparkpost.com)", 16 | "files": [ 17 | "build/" 18 | ], 19 | "main": "build/index.js", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "html-tags": "^2.0.0", 25 | "is-promise": "^2.1.0", 26 | "lodash": "^4.17.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/heml-render/src/createHtmlElement.js: -------------------------------------------------------------------------------- 1 | import stringifyAttributes from './stringifyAttributes' 2 | import selfClosingHtmlTags from 'html-tags/void' 3 | 4 | export default function createHtmlElement ({ name, attrs, contents = ' ' }) { 5 | if (selfClosingHtmlTags.includes(name)) { 6 | return `<${name}${attrs ? stringifyAttributes(attrs) : ''} />` 7 | } 8 | 9 | return `<${name}${attrs ? stringifyAttributes(attrs) : ''}>${contents || ' '}` 10 | } 11 | -------------------------------------------------------------------------------- /packages/heml-render/src/index.js: -------------------------------------------------------------------------------- 1 | import { filter, difference, keyBy, first } from 'lodash' 2 | import renderElement from './renderElement' 3 | 4 | export { renderElement } 5 | 6 | /** 7 | * preRender, render, and postRender all elements 8 | * @param {Array} elements List of element definitons 9 | * @param {Object} globals 10 | * @return {Promise} Returns an object with the cheerio object and metadata 11 | */ 12 | export default async function render ($, options = {}) { 13 | const { 14 | elements = [] 15 | } = options 16 | 17 | const globals = { $, elements, options } 18 | const Meta = first(elements.filter(({ tagName }) => tagName === 'meta')) 19 | 20 | await preRenderElements(elements, globals) 21 | await renderElements(elements, globals) 22 | await postRenderElements(elements, globals) 23 | 24 | return { $, metadata: Meta ? Meta.flush() : {} } 25 | } 26 | 27 | /** 28 | * Run the async preRender functions for each element 29 | * @param {Array} elements List of element definitons 30 | * @param {Object} globals 31 | * @return {Promise} 32 | */ 33 | async function preRenderElements (elements, globals) { 34 | for (let element of elements) { 35 | await element.preRender(globals) 36 | } 37 | } 38 | 39 | /** 40 | * Run the async postRender functions for each element 41 | * @param {Array} elements List of element definitons 42 | * @param {Object} globals 43 | * @return {Promise} 44 | */ 45 | async function postRenderElements (elements, globals) { 46 | for (let element of elements) { 47 | await element.postRender(globals) 48 | } 49 | } 50 | 51 | /** 52 | * Renders all HEML elements 53 | * @param {Array} elements List of element definitons 54 | * @param {Object} globals 55 | * @return {Promise} 56 | */ 57 | async function renderElements (elements, globals) { 58 | const { $ } = globals 59 | const elementMap = keyBy(elements, 'tagName') 60 | const metaTagNames = filter(elements, { parent: [ 'head' ] }).map(({ tagName }) => tagName) 61 | const nonMetaTagNames = difference(elements.map(({ tagName }) => tagName), metaTagNames) 62 | 63 | const $nodes = [ 64 | ...$.findNodes(metaTagNames), /** Render the meta elements first to last */ 65 | ...$.findNodes(nonMetaTagNames).reverse() /** Render the elements last to first/outside to inside */ 66 | ] 67 | 68 | for (let $node of $nodes) { 69 | const element = elementMap[$node.prop('tagName').toLowerCase()] 70 | const contents = $node.html() 71 | const attrs = $node[0].attribs 72 | 73 | const renderedValue = await Promise.resolve(renderElement(element, attrs, contents)) 74 | 75 | $node.replaceWith(renderedValue.trim()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/heml-render/src/renderElement.js: -------------------------------------------------------------------------------- 1 | import isPromise from 'is-promise' 2 | import { isPlainObject, defaults, mapValues, castArray, compact, flattenDeep } from 'lodash' 3 | import createHtmlElement from './createHtmlElement' 4 | 5 | export default function (name, attrs, ...contents) { 6 | /** catch all promises in this content and wait for them to finish */ 7 | if (contents.filter(isPromise).length > 0) { return Promise.all(contents).then((contents) => render(name, attrs, contents.join(''))) } 8 | 9 | return render(name, attrs, contents.join('')) 10 | } 11 | 12 | function render (name, attrs, contents) { 13 | if (!name || (isPlainObject(name) && !name.render)) { 14 | throw new Error(`name must be a HEML element or HTML tag name (.e.g 'td'). Received: ${JSON.stringify(name)}`) 15 | } 16 | 17 | if (isPlainObject(name) && name.render) { 18 | /** set the defaults and massage attribute values */ 19 | attrs = defaults({}, attrs, name.defaultAttrs || {}) 20 | attrs = mapValues(attrs, (value, name) => { 21 | if ((value === '' && name !== 'class') || value === 'true' || value === 'on') { return true } 22 | 23 | if (value === 'false' || value === 'off') { return false } 24 | 25 | return value 26 | }) 27 | 28 | /** 29 | * custom elements can return promises, arrays, or strings 30 | * 31 | * we will: 32 | * 1. check for the shorthands and render on that 33 | * 2. return a string synchronously if we can 34 | * 3. return a string in a promise 35 | */ 36 | const renderResults = castArray(name.render(attrs, contents)) 37 | 38 | /** 1. catch shorthands for rerendering the element */ 39 | if (renderResults.length === 1 && renderResults[0] === true) { 40 | return render(name.tagName, attrs, contents) 41 | } 42 | 43 | /** 2. we want to return synchronously if we can */ 44 | if (renderResults.filter(isPromise).length === 0) { 45 | return compact(renderResults).join('') 46 | } 47 | 48 | /** otherwise, combine the array of promises/strings into a single string */ 49 | return Promise.all(renderResults).then((results) => { 50 | return compact(flattenDeep(results)).join('') 51 | }) 52 | } 53 | 54 | /** if we have a regular ol element go ahead and convert it to a string */ 55 | if (attrs && attrs.class === '') { delete attrs.class } 56 | if (attrs && attrs.class) { attrs.class = attrs.class.trim() } 57 | 58 | return createHtmlElement({ name, attrs, contents }) 59 | } 60 | -------------------------------------------------------------------------------- /packages/heml-render/src/stringifyAttributes.js: -------------------------------------------------------------------------------- 1 | /** escapeless version of npmjs.com/stringify-attributes */ 2 | export default function stringifyAttributes (attrsObj) { 3 | const attributes = [] 4 | 5 | for (let [ key, value ] of Object.entries(attrsObj)) { 6 | if (value === false) { continue } 7 | 8 | if (Array.isArray(value)) { value = value.join(' ') } 9 | 10 | value = value === true ? '' : `="${String(value)}"` 11 | 12 | attributes.push(`${key}${value}`) 13 | } 14 | 15 | return attributes.length > 0 ? ' ' + attributes.join(' ') : '' 16 | } 17 | -------------------------------------------------------------------------------- /packages/heml-styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/styles", 3 | "version": "1.1.2", 4 | "description": "CSS processor for HEML", 5 | "keywords": [ 6 | "heml" 7 | ], 8 | "homepage": "https://heml.io", 9 | "bugs": "https://github.com/SparkPost/heml/issues", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SparkPost/heml.git" 14 | }, 15 | "author": "SparkPost (https://sparkpost.com)", 16 | "files": [ 17 | "build/" 18 | ], 19 | "main": "build/index.js", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "css-declaration-sorter": "^2.1.0", 25 | "css-shorthand-expand": "^1.1.0", 26 | "lodash": "^4.17.4", 27 | "postcss": "^6.0.13", 28 | "postcss-calc": "^6.0.1", 29 | "postcss-color-rgba-fallback": "^3.0.0", 30 | "postcss-colornames-to-hex": "^1.0.1", 31 | "postcss-convert-values": "^2.6.1", 32 | "postcss-discard-comments": "^2.0.4", 33 | "postcss-discard-duplicates": "^2.1.0", 34 | "postcss-discard-empty": "^2.1.0", 35 | "postcss-discard-overridden": "^0.1.1", 36 | "postcss-email-important": "^1.0.0", 37 | "postcss-hex-format": "^1.0.0", 38 | "postcss-merge-longhand": "^3.0.0", 39 | "postcss-merge-rules": "^2.1.2", 40 | "postcss-minify-font-values": "^1.0.5", 41 | "postcss-minify-gradients": "^1.0.5", 42 | "postcss-minify-params": "^1.2.2", 43 | "postcss-minify-selectors": "^2.1.1", 44 | "postcss-normalize-display-values": "^4.0.0-rc.2", 45 | "postcss-normalize-positions": "^4.0.0-rc.2", 46 | "postcss-normalize-repeat-style": "^4.0.0-rc.2", 47 | "postcss-normalize-string": "^4.0.0-rc.2", 48 | "postcss-normalize-timing-functions": "^4.0.0-rc.2", 49 | "postcss-ordered-values": "^2.2.3", 50 | "postcss-rgba-hex": "^0.3.7", 51 | "postcss-safe-parser": "^3.0.1", 52 | "postcss-shorthand-expand": "^1.0.1", 53 | "postcss-unique-selectors": "^2.0.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/heml-styles/src/index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import safeParser from 'postcss-safe-parser' 3 | 4 | /** optimize css - credz to cssnano */ 5 | import discardComments from 'postcss-discard-comments' 6 | import minifyGradients from 'postcss-minify-gradients' 7 | import normalizeDisplayValues from 'postcss-normalize-display-values' 8 | import normalizeTimingFunctions from 'postcss-normalize-timing-functions' 9 | import convertValues from 'postcss-convert-values' 10 | import reduceCalc from 'postcss-calc' 11 | import orderedValues from 'postcss-ordered-values' 12 | import minifySelectors from 'postcss-minify-selectors' 13 | import minifyParams from 'postcss-minify-params' 14 | import discardOverridden from 'postcss-discard-overridden' 15 | import normalizeString from 'postcss-normalize-string' 16 | import minifyFontValues from 'postcss-minify-font-values' 17 | import normalizeRepeatStyle from 'postcss-normalize-repeat-style' 18 | import normalizePositions from 'postcss-normalize-positions' 19 | import discardEmpty from 'postcss-discard-empty' 20 | import uniqueSelectors from 'postcss-unique-selectors' 21 | import declarationSorter from 'css-declaration-sorter' 22 | import mergeAdjacentMedia from './plugins/postcss-merge-adjacent-media' 23 | import discardDuplicates from 'postcss-discard-duplicates' 24 | import mergeRules from 'postcss-merge-rules' 25 | 26 | /** format colors */ 27 | import rgbToHex from 'postcss-rgba-hex' 28 | import colorNamesToHex from 'postcss-colornames-to-hex' 29 | import rgbaFallback from 'postcss-color-rgba-fallback' 30 | import formatHexColors from 'postcss-hex-format' 31 | 32 | /** email fixes */ 33 | import shorthandExpand from './plugins/postcss-expand-shorthand' 34 | import emailImportant from 'postcss-email-important' 35 | import zeroOutMargin from './plugins/postcss-zero-out-margin' 36 | 37 | /** custom element expander */ 38 | import elementExpander from './plugins/postcss-element-expander' 39 | 40 | import mergeLonghand from 'postcss-merge-longhand' 41 | 42 | async function hemlstyles (contents, options = {}) { 43 | const { 44 | elements = {}, 45 | aliases = {}, 46 | plugins = [] 47 | } = options 48 | 49 | return postcss([ 50 | ...plugins, 51 | 52 | // /** optimize css */ 53 | discardComments({ removeAll: false }), 54 | minifyGradients(), 55 | normalizeDisplayValues(), 56 | normalizeTimingFunctions(), 57 | convertValues({ length: false }), 58 | reduceCalc(), 59 | orderedValues(), 60 | minifySelectors(), 61 | minifyParams(), 62 | discardOverridden(), 63 | normalizeString(), 64 | minifyFontValues({ removeQuotes: false }), 65 | normalizeRepeatStyle(), 66 | normalizePositions(), 67 | discardEmpty(), 68 | uniqueSelectors(), 69 | declarationSorter(), 70 | mergeAdjacentMedia(), 71 | discardDuplicates(), 72 | mergeRules(), 73 | 74 | /** color handling */ 75 | colorNamesToHex(), 76 | rgbToHex({ rgbOnly: true, silent: true }), 77 | rgbaFallback(), 78 | formatHexColors(), 79 | 80 | /** email fixes */ 81 | emailImportant(), 82 | shorthandExpand(), // so we can match for margin-top/margin-left etc. 83 | zeroOutMargin(), 84 | 85 | /** expanding to match heml elements */ 86 | elementExpander({ elements, aliases }), 87 | mergeLonghand(), 88 | discardEmpty() 89 | ]) 90 | .process(contents, { parser: safeParser }) 91 | } 92 | 93 | export default hemlstyles 94 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-element-expander/coerceElements.js: -------------------------------------------------------------------------------- 1 | import { isPlainObject, escapeRegExp, isString, compact } from 'lodash' 2 | 3 | /** 4 | * remap the elements var to looks like this 5 | * [ 6 | * { 7 | * tag: 'button', 8 | * pseudos: { root: '.button', text: '.text' }, 9 | * defaults: [ '.button' ], 10 | * rules: { 11 | * '.button': [ { prop: /^background-color$/, tranform: () => {} } ], 12 | * '.text': [ { prop: /^color$/, transform: function() { tranform here } } ], 13 | * } 14 | * } 15 | * ... 16 | * ] 17 | */ 18 | 19 | /** 20 | * coerce the elements for use in the plugin 21 | * @param {Object} elements the given elements 22 | * @return {Array} elements in a more usable format 23 | */ 24 | export default function (originalElements) { 25 | let elements = [] 26 | 27 | for (const [ tag, originalRules ] of Object.entries(originalElements)) { 28 | let defaults = [] 29 | let pseudos = {} 30 | let rules = {} 31 | 32 | for (const [ selector, decls ] of Object.entries(originalRules)) { 33 | /** gather all the default values */ 34 | if (findAtDecl(decls, 'default')) defaults.push(selector) 35 | 36 | /** gather all the pseudo selectors */ 37 | let pseudo = findAtDecl(decls, 'pseudo') 38 | if (pseudo) pseudos[pseudo] = selector 39 | 40 | /** remap the rules to always be { prop: RegExp, transform: Function } */ 41 | rules[selector] = compact(decls.map((decl) => { 42 | if (isPlainObject(decl) && Object.keys(decl).length === 0) return 43 | 44 | const prop = isPlainObject(decl) ? Object.keys(decl)[0] : decl 45 | const transform = isPlainObject(decl) ? Object.values(decl)[0] : () => {} 46 | 47 | if (isString(prop) && prop.startsWith('@')) return 48 | 49 | return { prop: toRegExp(prop), transform } 50 | })) 51 | } 52 | 53 | elements.push({ tag, defaults, pseudos, rules }) 54 | } 55 | 56 | return elements 57 | } 58 | 59 | /** 60 | * finds the given at declaration value 61 | * @param {Array[Object]} decls the decls from an element 62 | * @param {String} the prop 63 | * @return {Any} the found value 64 | */ 65 | function findAtDecl (decls, prop) { 66 | const foundDecls = decls.filter((decl) => { 67 | return (isPlainObject(decl) && 68 | Object.keys(decl).length > 0 && 69 | Object.keys(decl)[0] === `@${prop}`) || decl === `@${prop}` 70 | }) 71 | 72 | if (foundDecls.length === 0) { return } 73 | 74 | const decl = foundDecls[0] 75 | 76 | return isPlainObject(decl) ? Object.values(decl)[0] : true 77 | } 78 | 79 | /** 80 | * convert the given string to a regular expression 81 | * @param {String|RegExp} prop the string to convert 82 | * @return {RegExp} the regular expression 83 | */ 84 | function toRegExp (string) { 85 | if (isString(string) && string.startsWith('/') && string.lastIndexOf('/') !== 0) { 86 | const pattern = string.substr(1, string.lastIndexOf('/') - 1) 87 | const opts = string.substr(string.lastIndexOf('/') + 1).toLowerCase() 88 | 89 | return new RegExp(pattern, opts.includes('i') ? opts : `${opts}i`) 90 | } 91 | 92 | if (isString(string)) { 93 | return new RegExp(`^${escapeRegExp(string)}$`, 'i') 94 | } 95 | 96 | return string 97 | } 98 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-element-expander/expanders.js: -------------------------------------------------------------------------------- 1 | import selectorParser from 'postcss-selector-parser' 2 | 3 | /** 4 | * replace all custom element tag selectors 5 | * @param {Object} element the element definition 6 | * @param {String} selector the selector 7 | * @return {String} the replaced selector 8 | */ 9 | function replaceElementTagMentions (element, selector) { 10 | const processor = selectorParser((selectors) => { 11 | let nodesToReplace = [] 12 | 13 | /** 14 | * looping breaks if we replace dynamically 15 | * so instead collect an array of nodes to swap and do it at the end 16 | */ 17 | selectors.walk((node) => { 18 | if (node.value === element.tag && node.type === 'tag') { nodesToReplace.push(node) } 19 | }) 20 | 21 | nodesToReplace.forEach((node) => node.replaceWith(element.pseudos.root)) 22 | }) 23 | 24 | return processor.process(selector).result 25 | } 26 | 27 | /** 28 | * replace all custom element pseudo selectors 29 | * @param {Object} element the element definiton 30 | * @param {String} selector the selector 31 | * @return {String} the replaced selector 32 | */ 33 | function replaceElementPseudoMentions (element, selector) { 34 | const processor = selectorParser((selectors) => { 35 | let nodesToReplace = [] 36 | 37 | /** 38 | * looping breaks if we replace dynamically 39 | * so instead collect an array of nodes to swap and do it at the end 40 | */ 41 | selectors.each((selector) => { 42 | let onElementTag = false 43 | 44 | selector.each((node) => { 45 | if (node.type === 'tag' && node.value === element.tag) { 46 | onElementTag = true 47 | } else if (node.type === 'combinator') { 48 | onElementTag = false 49 | } else if (node.type === 'pseudo' && onElementTag) { 50 | const matchedPseudos = Object.entries(element.pseudos).filter(([ pseudo ]) => { 51 | return node.value.replace(/::?/, '') === pseudo 52 | }) 53 | 54 | if (matchedPseudos.length > 0) { 55 | const [, value] = matchedPseudos[0] 56 | nodesToReplace.push({ node, value }) 57 | } 58 | } 59 | }) 60 | }) 61 | 62 | nodesToReplace.forEach(({ node, value }) => node.replaceWith(` ${value}`)) 63 | }) 64 | 65 | return processor.process(selector).result 66 | } 67 | 68 | /** 69 | * expand the given rule to correctly the style the element 70 | * @param {Object} element element The element definition 71 | * @param {Array} selectors the matched selectors to for 72 | * @return {Array[Rule]} an array of the expanded rules 73 | */ 74 | function expandElementRule (element, selectors = [], originalRule) { 75 | /** early return if we don't have any selectors */ 76 | if (selectors.length === 0) return [] 77 | 78 | let usedProps = [] 79 | let expandedRules = [] 80 | let defaultRules = [] 81 | 82 | /** create the base rule */ 83 | const baseRule = originalRule.clone() 84 | baseRule.selectors = selectors 85 | baseRule.selector = replaceElementTagMentions(element, baseRule.selector) 86 | 87 | /** create postcss rules for each element rule */ 88 | for (const [ ruleSelector, ruleDecls ] of Object.entries(element.rules)) { 89 | const isRoot = element.pseudos.root === ruleSelector 90 | const isDefault = element.defaults.includes(ruleSelector) 91 | const expandedRule = baseRule.clone() 92 | 93 | /** gather all rules that get decls be default */ 94 | if (isDefault) { defaultRules.push(expandedRule) } 95 | 96 | /** map all the selectors to target this rule selector */ 97 | if (!isRoot) { expandedRule.selectors = expandedRule.selectors.map((selector) => `${selector} ${ruleSelector}`) } 98 | 99 | /** strip any non whitelisted props, run tranforms, gather used props */ 100 | expandedRule.walkDecls((decl) => { 101 | const matchedRuleDecls = ruleDecls.filter(({ prop }) => prop.test(decl.prop)) 102 | 103 | if (matchedRuleDecls.length === 0) { return decl.remove() } 104 | 105 | usedProps.push(decl.prop) 106 | matchedRuleDecls.forEach(({ transform }) => transform(decl, originalRule)) 107 | }) 108 | 109 | expandedRules.push(expandedRule) 110 | } 111 | 112 | baseRule.walkDecls((decl) => { 113 | if (!usedProps.includes(decl.prop)) { 114 | defaultRules.forEach((defaultRule) => defaultRule.prepend(decl.clone())) 115 | } 116 | }) 117 | 118 | return expandedRules 119 | } 120 | 121 | export { replaceElementTagMentions, replaceElementPseudoMentions, expandElementRule } 122 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-element-expander/findDirectElementSelectors.js: -------------------------------------------------------------------------------- 1 | import selectorParser from 'postcss-selector-parser' 2 | const simpleSelectorParser = selectorParser() 3 | 4 | /** 5 | * find all selectors that target the give element 6 | * @param {Object} element the element definition 7 | * @param {String} selector the selector 8 | * @return {Array} the matched selectors 9 | */ 10 | export default function (element, selector) { 11 | const selectors = simpleSelectorParser.process(selector).res 12 | 13 | return selectors.filter((selector) => { 14 | let selectorNodes = selector.nodes.concat([]).reverse() // clone the array 15 | 16 | for (const node of selectorNodes) { 17 | if (node.type === 'combinator') { return false } 18 | 19 | if (node.type === 'pseudo' && node.value.replace(/::?/, '') in element.pseudos) { return false } 20 | 21 | if (node.type === 'tag' && node.value === element.tag) { return true } 22 | } 23 | }).map((selector) => String(selector).trim()) 24 | } 25 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-element-expander/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import postcss from 'postcss' 4 | import coerceElements from './coerceElements' 5 | import tagAliasSelectors from './tagAliasSelectors' 6 | import findDirectElementSelectors from './findDirectElementSelectors' 7 | import { replaceElementTagMentions, replaceElementPseudoMentions, expandElementRule } from './expanders' 8 | 9 | /** 10 | * elements var looks like this before being coerced 11 | * 12 | * { 13 | * button: { 14 | * '.button': [ { '@pseudo': 'root' }, { '@default': true }, 'background-color' ], 15 | * '.text': [ { '@pseudo': 'text' }, { color: function() { tranform here } } ], 16 | * }, 17 | * ... 18 | * } 19 | */ 20 | 21 | /** 22 | * aliases var looks like this 23 | * 24 | * { 25 | * button: [ $node, $node, $node, ... ] 26 | * ... 27 | * } 28 | */ 29 | 30 | /** 31 | * Convert CSS to match custom elements 32 | * @param {Object} options.elements An object of elements that define how to 33 | * split up the css for each element 34 | * @param {[type]} options.$ A cheerio instance 35 | */ 36 | export default postcss.plugin('postcss-element-expander', ({ elements, aliases }) => { 37 | elements = coerceElements(elements) 38 | 39 | return (root, result) => { 40 | for (let element of elements) { 41 | /** 42 | * add the element tag to any css selectors that implicitly target an element 43 | * .i.e. #my-button that selects 44 | */ 45 | root.walkRules((rule) => { 46 | tagAliasSelectors(element, aliases[element.tag], rule) 47 | }) 48 | 49 | /** 50 | * There are 3 (non-mutually exclusive) possibilities when it contains the element tag 51 | * 52 | * 1. it directly targets the element - i.e. button { background: blue; } 53 | * in this case we need generate entirely new rules, prepend before the original rule, and strip the used selectors 54 | * 55 | * 2. it uses an element tag as an ancestor/sibling - .i.e. button span { color: black; } 56 | * 57 | * 3. it uses an element pseudo element - .i.e. button::text { color: blue } 58 | */ 59 | root.walkRules(new RegExp(element.tag, 'i'), (rule) => { 60 | /** CASE 1 */ 61 | /** grab all the selectors that target this element */ 62 | const elementSelectors = findDirectElementSelectors(element, rule.selector) 63 | 64 | /** Create new rules to properly target the elements */ 65 | const expandedRules = expandElementRule(element, elementSelectors, rule) 66 | expandedRules.forEach((expandedRule) => rule.before(expandedRule)) 67 | 68 | /** remove the directly targeting selectors from the original rule */ 69 | rule.selectors = rule.selectors.filter((selector) => !elementSelectors.includes(selector)) 70 | 71 | /** remove the rule if has no selectors */ 72 | if (rule.selector.trim() === '') return rule.remove() 73 | 74 | /** CASE 2 */ 75 | /** Replace all mentions of the element pseudo elements */ 76 | rule.selector = replaceElementPseudoMentions(element, rule.selector) 77 | 78 | /** CASE 3 */ 79 | /** Replace all mentions of the element tag */ 80 | rule.selector = replaceElementTagMentions(element, rule.selector) 81 | }) 82 | } 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-element-expander/tagAliasSelectors.js: -------------------------------------------------------------------------------- 1 | import selectorParser from 'postcss-selector-parser' 2 | 3 | const simpleSelectorParser = selectorParser() 4 | 5 | /** 6 | * Add the element tag to selectors from the rule that match the element alias 7 | * @param {Object} element element definition 8 | * @param {Array[$node]} aliases array of cheerio nodes 9 | * @param {Rule} rule postcss node 10 | */ 11 | export default function (element, aliases, rule) { 12 | if (!aliases) return 13 | 14 | let selectors = [] 15 | 16 | rule.selectors.forEach((selector) => { 17 | const matchedAliases = aliases.filter((alias) => alias.is(selector.replace(/::?\S*/g, ''))).length > 0 18 | 19 | /** the selector in an alias that doesn't target the tag already */ 20 | if (matchedAliases && !targetsTag(selector)) { 21 | selectors.push(appendElementSelector(element, selector)) 22 | } 23 | 24 | /** dont add the original selector back in if it targets a pseudo selector */ 25 | if (!targetsElementPseudo(element, selector)) { selectors.push(selector) } 26 | }) 27 | 28 | rule.selectors = selectors 29 | } 30 | 31 | /** 32 | * checks if selector targets a tag 33 | * @param {String} selector the selector 34 | * @return {Boolean} if the selector targets a tag 35 | */ 36 | function targetsTag (selector) { 37 | const selectors = simpleSelectorParser.process(selector).res 38 | 39 | return selectors.filter((selector) => { 40 | let selectorNodes = selector.nodes.concat([]).reverse() // clone the array 41 | 42 | for (const node of selectorNodes) { 43 | if (node.type === 'cominator') { break } 44 | 45 | if (node.type === 'tag') { return true } 46 | } 47 | 48 | return false 49 | }).length > 0 50 | } 51 | 52 | /** 53 | * find all selectors that target the give element 54 | * @param {Object} element the element definition 55 | * @param {String} selector the selector 56 | * @return {Array} the matched selectors 57 | */ 58 | function targetsElementPseudo (element, selector) { 59 | const selectors = simpleSelectorParser.process(selector).res 60 | 61 | return selectors.filter((selector) => { 62 | let selectorNodes = selector.nodes.concat([]).reverse() // clone the array 63 | 64 | for (const node of selectorNodes) { 65 | if (node.type === 'cominator') { break } 66 | 67 | if (node.type === 'pseudo' && node.value.replace(/::?/, '') in element.pseudos) { 68 | return true 69 | } 70 | 71 | if (node.type === 'tag' && node.value === element.tag) { break } 72 | } 73 | 74 | return false 75 | }).length > 0 76 | } 77 | 78 | /** 79 | * Add the element tag to the end of the selector 80 | * @param {Object} element element definition 81 | * @param {String} selector the selector 82 | * @return {String} the modified selector 83 | */ 84 | function appendElementSelector (element, selector) { 85 | const processor = selectorParser((selectors) => { 86 | let combinatorNode = null 87 | 88 | /** 89 | * looping breaks if we insert dynamically 90 | */ 91 | selectors.each((selector) => { 92 | const elementNode = selectorParser.tag({ value: element.tag }) 93 | selector.walk((node) => { 94 | if (node.type === 'combinator') { combinatorNode = node } 95 | }) 96 | 97 | if (combinatorNode) { 98 | selector.insertAfter(combinatorNode, elementNode) 99 | } else { 100 | selector.prepend(elementNode) 101 | } 102 | }) 103 | }) 104 | 105 | return processor.process(selector).result 106 | } 107 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-expand-shorthand/index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import shorthandExpand from 'css-shorthand-expand' 3 | 4 | export default postcss.plugin('postcss-expand-shorthand', () => (root) => { 5 | root.walkDecls((decl) => { 6 | if (shouldExpand(decl.prop) && !!decl.value) { 7 | const expandedDecls = shorthandExpand(decl.prop, decl.value) 8 | 9 | if (!expandedDecls) { return } 10 | 11 | for (const [ prop, value ] of Object.entries(expandedDecls)) { 12 | decl.before(postcss.decl({ prop, value })) 13 | } 14 | 15 | decl.remove() 16 | } 17 | }) 18 | }) 19 | 20 | function shouldExpand (prop) { 21 | return ['background', 'font', 'margin'].includes(prop) 22 | } 23 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-merge-adjacent-media/index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | 3 | export default postcss.plugin('postcss-merge-adjacent-media', () => (root) => { 4 | root.walkAtRules((rule) => { 5 | if (rule.name !== 'media') { return } 6 | 7 | const nextRule = getNextRule(rule) 8 | 9 | if (!nextRule || nextRule.type !== 'atrule') { return } 10 | 11 | if (nextRule.params === rule.params) { 12 | nextRule.prepend(rule.nodes) 13 | rule.remove() 14 | } 15 | }) 16 | }) 17 | 18 | function getNextRule (rule) { 19 | const nextNode = rule.next() 20 | if (!nextNode) { return } 21 | 22 | if (nextNode.type === 'atrule' || nextNode.type === 'rule') { return nextNode } 23 | 24 | return getNextRule(nextNode) 25 | } 26 | -------------------------------------------------------------------------------- /packages/heml-styles/src/plugins/postcss-zero-out-margin/index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | 3 | /** 4 | * convert margin-top/margin-bottom to 0 when they are margin auto 5 | */ 6 | export default postcss.plugin('postcss-zero-out-margin', () => (root) => { 7 | root.walkDecls(/margin-top|margin-bottom/i, (decl) => { 8 | decl.value = decl.value.toLowerCase() === 'auto' ? '0' : decl.value 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /packages/heml-utils/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "css-groups": { 6 | "version": "0.1.1", 7 | "resolved": "https://registry.npmjs.org/css-groups/-/css-groups-0.1.1.tgz", 8 | "integrity": "sha1-UyvG3LId3Mps1BCdAkmPyY/7BO8=" 9 | }, 10 | "lodash": { 11 | "version": "4.17.4", 12 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 13 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/heml-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/utils", 3 | "version": "1.1.2", 4 | "description": "A collection of utilities for HEML", 5 | "keywords": [ 6 | "heml" 7 | ], 8 | "homepage": "https://heml.io", 9 | "bugs": "https://github.com/SparkPost/heml/issues", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SparkPost/heml.git" 14 | }, 15 | "author": "SparkPost (https://sparkpost.com)", 16 | "files": [ 17 | "build/" 18 | ], 19 | "main": "build/index.js", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "@heml/render": "^1.1.2", 25 | "css-groups": "^0.1.1", 26 | "lodash": "^4.17.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/heml-utils/src/HEMLError.js: -------------------------------------------------------------------------------- 1 | import { min, max } from 'lodash' 2 | 3 | export default class HEMLError extends Error { 4 | constructor (message, $node) { 5 | super(message) 6 | this.name = 'HEMLError' 7 | 8 | if ($node) { 9 | this.$node = $node 10 | this.selector = buildExactSelector($node) 11 | } 12 | 13 | Error.captureStackTrace(this, HEMLError) 14 | } 15 | } 16 | 17 | function buildExactSelector ($node) { 18 | const nodeSelector = buildSelector($node[0]) 19 | const strSelector = $node.parents() 20 | .map((index, node) => buildSelector(node)) 21 | .toArray() 22 | .reverse() 23 | .concat([nodeSelector]) 24 | .join(' > ') 25 | 26 | const chopAfter = min(max(0, strSelector.lastIndexOf('#')), 27 | max(0, strSelector.lastIndexOf('html')), 28 | max(0, strSelector.lastIndexOf('heml'))) 29 | 30 | return strSelector.substr(chopAfter) 31 | } 32 | 33 | function buildSelector (node) { 34 | if (node.id) { 35 | return `#${node.id}` 36 | } 37 | 38 | const tag = node.tagName.toLowerCase() 39 | const siblingsBefore = findSiblingsBefore(node) 40 | const siblingsAfter = findSiblingsAfter(node) 41 | const siblings = siblingsBefore.concat(siblingsAfter) 42 | 43 | const sameTag = siblings.filter((s) => s.tagName.toLowerCase() === tag) 44 | 45 | if (siblings.length === 0 || sameTag.length === 0) { 46 | return tag 47 | } 48 | 49 | const sameTagAndClass = siblings.filter((s) => s.className === node.className && s.tagName.toLowerCase() === tag) 50 | 51 | if (node.className && sameTagAndClass.length === 0) { 52 | return `${tag}.${node.className.split(' ').join('.')}` 53 | } 54 | 55 | return `${tag}:nth-child(${siblingsBefore.length + 1})` 56 | } 57 | 58 | function findSiblingsBefore (node, siblings = []) { 59 | if (!node.previousSibling) { return siblings } 60 | 61 | if (node.previousSibling.tagName) { siblings = siblings.concat([node.previousSibling]) } 62 | 63 | return findSiblingsBefore(node.previousSibling, siblings) 64 | } 65 | 66 | function findSiblingsAfter (node, siblings = []) { 67 | if (!node.nextSibling) { return siblings } 68 | 69 | if (node.nextSibling.tagName) { siblings = siblings.concat([node.nextSibling]) } 70 | 71 | return findSiblingsAfter(node.nextSibling, siblings) 72 | } 73 | -------------------------------------------------------------------------------- /packages/heml-utils/src/condition.js: -------------------------------------------------------------------------------- 1 | const parts = { 2 | 'START_CONDITION': '' 5 | } 6 | 7 | function condition (condition, content) { 8 | return ` 9 | START_CONDITION${condition}END_CONDITION 10 | ${content.trim()} 11 | END_COMMENT_CONDITIONAL 12 | ` 13 | } 14 | 15 | condition.replace = function (html) { 16 | for (let [search, replace] of Object.entries(parts)) { 17 | html = html.replace(new RegExp(search, 'g'), replace) 18 | } 19 | 20 | return html 21 | } 22 | 23 | export default condition 24 | -------------------------------------------------------------------------------- /packages/heml-utils/src/createElement.js: -------------------------------------------------------------------------------- 1 | import { defaults, isFunction } from 'lodash' 2 | 3 | const textRegex = /^(text(-([^-\s]+))?(-([^-\s]+))?|word-(break|spacing|wrap)|line-break|hanging-punctuation|hyphens|letter-spacing|overflow-wrap|tab-size|white-space|font-family|font-weight|font-style|font-variant|color)$/i 4 | 5 | export default function (name, element) { 6 | if (!name || name.trim().length === 0) { 7 | throw new Error(`When creating an element, you must set the name. ${name.trim().length === 0 ? 'An empty string' : `"${name}"`} was given.`) 8 | } 9 | 10 | if (isFunction(element)) { 11 | element = { render: element } 12 | } 13 | 14 | if (element.containsText) { 15 | element.rules = element.rules || {} 16 | element.rules['.header'] = [ textRegex ] 17 | element.rules['.text'] = [ textRegex, 'font-size', 'line-height' ] 18 | } 19 | 20 | element = defaults({}, element || {}, { 21 | tagName: name.trim().toLowerCase(), 22 | attrs: [], 23 | children: true, 24 | defaultAttrs: {}, 25 | preRender () {}, 26 | render () { return false }, 27 | postRender () {} 28 | }) 29 | 30 | element.defaultAttrs.class = element.defaultAttrs.class || '' 31 | 32 | return element 33 | } 34 | -------------------------------------------------------------------------------- /packages/heml-utils/src/index.js: -------------------------------------------------------------------------------- 1 | import { renderElement } from '@heml/render' 2 | import cssGroups from 'css-groups' 3 | import createElement from './createElement' 4 | import HEMLError from './HEMLError' 5 | import transforms from './transforms' 6 | import condition from './condition' 7 | 8 | module.exports = { createElement, renderElement, HEMLError, cssGroups, transforms, condition } 9 | -------------------------------------------------------------------------------- /packages/heml-utils/src/transforms/convertProp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * convert a decleration to different properity 3 | * .i.e. max-width -> width 4 | * @param {String} prop 5 | * @return {Function} 6 | */ 7 | export default function convertProp (prop) { 8 | return (decl) => { decl.prop = prop } 9 | } 10 | -------------------------------------------------------------------------------- /packages/heml-utils/src/transforms/fallbackFor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds the property this tranform is attached to, if the desired property wasn't given 3 | * @param {String} prop 4 | * @return {Function} 5 | */ 6 | export default function fallbackFor (desiredProp) { 7 | return (prop, rule) => { 8 | let hasDesiredProp = false 9 | rule.walkDecls(desiredProp, () => { hasDesiredProp = true }) 10 | 11 | /** remove the fallback property if we already have the desired properity */ 12 | if (hasDesiredProp) { 13 | prop.remove() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/heml-utils/src/transforms/ieAlignFallback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * inline margin-left: auto; and margin-right: auto; otherwise, through it to 0 3 | */ 4 | export default function ieAlignFallback (decl) { 5 | if (decl.prop === 'margin-top' || decl.prop === 'margin-bottom') { 6 | return decl.remove() 7 | } 8 | 9 | if ((decl.prop === 'margin-left' || decl.prop === 'margin-right') && decl.value !== 'auto') { 10 | decl.value = '0' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/heml-utils/src/transforms/index.js: -------------------------------------------------------------------------------- 1 | import trueHide from './trueHide' 2 | import convertProp from './convertProp' 3 | import ieAlignFallback from './ieAlignFallback' 4 | import fallbackFor from './fallbackFor' 5 | 6 | export default { trueHide, convertProp, ieAlignFallback, fallbackFor } 7 | -------------------------------------------------------------------------------- /packages/heml-utils/src/transforms/trueHide.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { isUndefined } from 'lodash' 4 | 5 | export default (type, containsTables = false) => (decl, originalRule) => { 6 | if (decl.value.trim().toLowerCase() === 'none') { 7 | decl.after(decl.clone({ prop: 'mso-hide', value: 'all' })) 8 | decl.after(decl.clone({ prop: 'max-height', value: '0px' })) 9 | decl.after(decl.clone({ prop: 'overflow', value: 'hidden' })) 10 | 11 | if (type === 'block' || type === 'table' || containsTables) { 12 | const hideTableRule = decl.parent.clone() 13 | hideTableRule.selectors = hideTableRule.selectors.map((s) => `${s} table`) 14 | hideTableRule.removeAll() 15 | hideTableRule.append(decl.clone({ prop: 'mso-hide', value: 'all' })) 16 | originalRule.after(hideTableRule) 17 | } 18 | } else if (decl.value.trim().toLowerCase() === type || isUndefined(type)) { 19 | decl.after(decl.clone({ prop: 'mso-hide', value: 'none' })) 20 | decl.after(decl.clone({ prop: 'max-height', value: 'initial' })) 21 | decl.after(decl.clone({ prop: 'overflow', value: 'auto' })) 22 | 23 | if (type === 'block' || type === 'table' || containsTables) { 24 | const showTableRule = decl.parent.clone() 25 | showTableRule.selectors = showTableRule.selectors.map((s) => `${s} table`) 26 | showTableRule.removeAll() 27 | showTableRule.append(decl.clone({ prop: 'mso-hide', value: 'none' })) 28 | originalRule.after(showTableRule) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/heml-validate/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "lodash": { 6 | "version": "4.17.4", 7 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 8 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/heml-validate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heml/validate", 3 | "version": "1.1.2", 4 | "description": "Validator for HEML", 5 | "keywords": [ 6 | "heml" 7 | ], 8 | "homepage": "https://heml.io", 9 | "bugs": "https://github.com/SparkPost/heml/issues", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SparkPost/heml.git" 14 | }, 15 | "author": "SparkPost (https://sparkpost.com)", 16 | "files": [ 17 | "build/" 18 | ], 19 | "main": "build/index.js", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "@heml/utils": "^1.1.2", 25 | "lodash": "^4.17.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/heml-validate/src/index.js: -------------------------------------------------------------------------------- 1 | import * as validatorsObject from './validators' 2 | 3 | const validators = Object.values(validatorsObject) 4 | 5 | /** 6 | * Validate that a cheerio instance contains valid HEML 7 | * @param {Cheero} $ the heml cheerio 8 | * @param {Object} options 9 | * @return {Array[HEMLError]} an array of heml errors 10 | */ 11 | export default function validate ($, options = {}) { 12 | const { 13 | elements = [] 14 | } = options 15 | 16 | let errors = [] 17 | 18 | for (let element of elements) { 19 | const matchedValidators = validators.filter((validator) => validator.name in element) 20 | 21 | if (matchedValidators.length === 0) { return } 22 | 23 | const $nodes = $.findNodes(element.tagName) 24 | 25 | $nodes.forEach(($node) => matchedValidators.forEach((validator) => { 26 | try { 27 | validator($node, element, $) 28 | } catch (e) { 29 | errors.push(e) 30 | } 31 | })) 32 | } 33 | 34 | return errors 35 | } 36 | -------------------------------------------------------------------------------- /packages/heml-validate/src/validators/attrs.js: -------------------------------------------------------------------------------- 1 | import { HEMLError } from '@heml/utils' 2 | import { difference } from 'lodash' 3 | 4 | const nativeAttrs = [ 'id', 'class', 'dir', 'lang', 'accesskey', 'tabindex', 'title', 'translate' ] 5 | 6 | export default function attrs ($node, { tagName, attrs: allowedAttrs, defaultAttrs }) { 7 | /** allow any attributes through */ 8 | if (allowedAttrs === true) { return } 9 | 10 | allowedAttrs = allowedAttrs 11 | .concat(Object.keys(defaultAttrs)) 12 | .concat(nativeAttrs) 13 | 14 | const usedAttrs = Object.keys($node.get(0).attribs) 15 | 16 | const foundNotAllowedAttrs = difference(usedAttrs, allowedAttrs) 17 | 18 | if (foundNotAllowedAttrs.length > 0) { 19 | /** remove non-whitelisted attributes */ 20 | foundNotAllowedAttrs.forEach((attr) => $node.removeAttr(attr)) 21 | 22 | const plural = foundNotAllowedAttrs.length > 1 23 | throw new HEMLError(`Attribute${plural ? 's' : ''} ${foundNotAllowedAttrs.join(', ')} ${plural ? 'are' : 'is'} not allowed on ${tagName}.`, $node) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/heml-validate/src/validators/children.js: -------------------------------------------------------------------------------- 1 | import { HEMLError } from '@heml/utils' 2 | import { isArray, intersection, difference } from 'lodash' 3 | 4 | export default function children ($node, { tagName, children: requiredChildren }) { 5 | if (isArray(requiredChildren)) { 6 | const children = $node.children().toArray().map((c) => c.name) 7 | 8 | const foundRequiredChildren = intersection(requiredChildren, children) 9 | 10 | if (foundRequiredChildren.length < requiredChildren.length) { 11 | const missingRequiredChildren = difference(requiredChildren, foundRequiredChildren) 12 | 13 | throw new HEMLError(`${tagName} is missing required children: ${missingRequiredChildren}`, $node) 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /packages/heml-validate/src/validators/index.js: -------------------------------------------------------------------------------- 1 | import attrs from './attrs' 2 | import children from './children' 3 | import parent from './parent' 4 | import unique from './unique' 5 | 6 | export { attrs, children, parent, unique } 7 | -------------------------------------------------------------------------------- /packages/heml-validate/src/validators/parent.js: -------------------------------------------------------------------------------- 1 | import { HEMLError } from '@heml/utils' 2 | 3 | export default function parent ($node, { tagName, parent: allowedParents }) { 4 | const parentTag = $node.parent().get(0) 5 | 6 | if (!parentTag) { return } 7 | 8 | if (allowedParents.includes(parentTag.name)) { 9 | return 10 | } 11 | 12 | let message = `${tagName} is inside of ${parentTag.name}.` 13 | 14 | if (allowedParents.length === 0) { 15 | message = `${message} It may not have any parents.` 16 | } else { 17 | message = `${message} It should only be used in: ${allowedParents.join(', ')}` 18 | } 19 | 20 | throw new HEMLError(message, $node) 21 | } 22 | -------------------------------------------------------------------------------- /packages/heml-validate/src/validators/unique.js: -------------------------------------------------------------------------------- 1 | import { HEMLError } from '@heml/utils' 2 | 3 | export default function unique ($node, { tagName, unique: shouldBeUnique }, $) { 4 | const $nodes = $.findNodes(tagName) 5 | 6 | if ($nodes.length > 1 && shouldBeUnique) { 7 | /** remove all but the first $node */ 8 | $nodes.slice(1).forEach(($node) => $node.remove()) 9 | 10 | throw new HEMLError(`${tagName} should be unique. ${$nodes.length} were found.`, $node) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/heml/README.md: -------------------------------------------------------------------------------- 1 |

<heml>

2 | 3 | 4 |

5 | Guide • 6 | Documentation • 7 | Editor 8 |

9 | 10 | 11 | HEML is an open source markup language for building responsive email. 12 | 13 | - **Native Feel:** Do you know HTML and CSS? Check out our docs and you're off to the races! No special rules or styling paradigms to master. 14 | 15 | - **Forward Thinking:** HEML is designed to take advantage of all that email can do while still providing a solid experience for all clients. 16 | 17 | - **Extendable:** You can create your own powerful elements and style rules. Share them with the world, or keep em to yourself. Your choice. 18 | 19 | 20 | ## FAQ 21 | 22 | ### Why should I use HEML? 23 | 24 | It makes building emails easier. 25 | 26 | ### How do I use it? 27 | 28 | Check out our [usage guide](http://heml.io/docs/getting-started/usage). 29 | 30 | ### What do I do if I found a bug? 31 | 32 | Open up an [issue](https://github.com/SparkPost/heml/issues) on the repository. Thanks for catching it! 🙏 33 | 34 | ### Want to help? 35 | 36 | Awesome!! We welcome any and all help! Head over to the [issues](https://github.com/SparkPost/heml/issues) and see if anything catches your eye. 37 | 38 | ## License 39 | 40 | [MIT](https://github.com/SparkPost/heml/blob/master/LICENSE) 41 | -------------------------------------------------------------------------------- /packages/heml/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heml", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 11 | }, 12 | "accepts": { 13 | "version": "1.3.4", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 15 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", 16 | "requires": { 17 | "mime-types": "2.1.17", 18 | "negotiator": "0.6.1" 19 | } 20 | }, 21 | "ansi-align": { 22 | "version": "2.0.0", 23 | "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", 24 | "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", 25 | "requires": { 26 | "string-width": "2.1.1" 27 | } 28 | }, 29 | "ansi-escapes": { 30 | "version": "2.0.0", 31 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-2.0.0.tgz", 32 | "integrity": "sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs=" 33 | }, 34 | "ansi-regex": { 35 | "version": "3.0.0", 36 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 37 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" 38 | }, 39 | "ansi-styles": { 40 | "version": "3.2.0", 41 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", 42 | "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", 43 | "requires": { 44 | "color-convert": "1.9.0" 45 | } 46 | }, 47 | "array-flatten": { 48 | "version": "1.1.1", 49 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 50 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 51 | }, 52 | "babel-runtime": { 53 | "version": "6.26.0", 54 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 55 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 56 | "requires": { 57 | "core-js": "2.5.1", 58 | "regenerator-runtime": "0.11.0" 59 | } 60 | }, 61 | "balanced-match": { 62 | "version": "1.0.0", 63 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 64 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 65 | }, 66 | "bluebird": { 67 | "version": "3.5.1", 68 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 69 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 70 | }, 71 | "body-parser": { 72 | "version": "1.18.2", 73 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 74 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 75 | "requires": { 76 | "bytes": "3.0.0", 77 | "content-type": "1.0.4", 78 | "debug": "2.6.9", 79 | "depd": "1.1.1", 80 | "http-errors": "1.6.2", 81 | "iconv-lite": "0.4.19", 82 | "on-finished": "2.3.0", 83 | "qs": "6.5.1", 84 | "raw-body": "2.3.2", 85 | "type-is": "1.6.15" 86 | } 87 | }, 88 | "boxen": { 89 | "version": "1.2.2", 90 | "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.2.tgz", 91 | "integrity": "sha1-Px1AMsMP/qnUsCwyLq8up0HcvOU=", 92 | "requires": { 93 | "ansi-align": "2.0.0", 94 | "camelcase": "4.1.0", 95 | "chalk": "2.2.0", 96 | "cli-boxes": "1.0.0", 97 | "string-width": "2.1.1", 98 | "term-size": "1.2.0", 99 | "widest-line": "1.0.0" 100 | } 101 | }, 102 | "brace-expansion": { 103 | "version": "1.1.8", 104 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 105 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 106 | "requires": { 107 | "balanced-match": "1.0.0", 108 | "concat-map": "0.0.1" 109 | } 110 | }, 111 | "byte-length": { 112 | "version": "0.1.1", 113 | "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-0.1.1.tgz", 114 | "integrity": "sha1-6bR3TbznxZdkv1vofDAniaiHOMM=" 115 | }, 116 | "bytes": { 117 | "version": "3.0.0", 118 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 119 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 120 | }, 121 | "camelcase": { 122 | "version": "4.1.0", 123 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", 124 | "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" 125 | }, 126 | "chalk": { 127 | "version": "2.2.0", 128 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.0.tgz", 129 | "integrity": "sha512-0BMM/2hG3ZaoPfR6F+h/oWpZtsh3b/s62TjSM6MGCJWEbJDN1acqCXvyhhZsDSVFklpebUoQ5O1kKC7lOzrn9g==", 130 | "requires": { 131 | "ansi-styles": "3.2.0", 132 | "escape-string-regexp": "1.0.5", 133 | "supports-color": "4.5.0" 134 | } 135 | }, 136 | "cli-boxes": { 137 | "version": "1.0.0", 138 | "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", 139 | "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" 140 | }, 141 | "cli-color": { 142 | "version": "1.2.0", 143 | "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.2.0.tgz", 144 | "integrity": "sha1-OlrnT9drYmevZm5p4q+70B3vNNE=", 145 | "requires": { 146 | "ansi-regex": "2.1.1", 147 | "d": "1.0.0", 148 | "es5-ext": "0.10.35", 149 | "es6-iterator": "2.0.3", 150 | "memoizee": "0.4.11", 151 | "timers-ext": "0.1.2" 152 | }, 153 | "dependencies": { 154 | "ansi-regex": { 155 | "version": "2.1.1", 156 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 157 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 158 | } 159 | } 160 | }, 161 | "cli-cursor": { 162 | "version": "2.1.0", 163 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 164 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 165 | "requires": { 166 | "restore-cursor": "2.0.0" 167 | } 168 | }, 169 | "code-point-at": { 170 | "version": "1.1.0", 171 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 172 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 173 | }, 174 | "color-convert": { 175 | "version": "1.9.0", 176 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", 177 | "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", 178 | "requires": { 179 | "color-name": "1.1.3" 180 | } 181 | }, 182 | "color-name": { 183 | "version": "1.1.3", 184 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 185 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 186 | }, 187 | "commander": { 188 | "version": "2.11.0", 189 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 190 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" 191 | }, 192 | "concat-map": { 193 | "version": "0.0.1", 194 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 195 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 196 | }, 197 | "config-chain": { 198 | "version": "1.1.11", 199 | "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", 200 | "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", 201 | "requires": { 202 | "ini": "1.3.4", 203 | "proto-list": "1.2.4" 204 | } 205 | }, 206 | "content-disposition": { 207 | "version": "0.5.2", 208 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 209 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 210 | }, 211 | "content-type": { 212 | "version": "1.0.4", 213 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 214 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 215 | }, 216 | "cookie": { 217 | "version": "0.3.1", 218 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 219 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 220 | }, 221 | "cookie-signature": { 222 | "version": "1.0.6", 223 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 224 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 225 | }, 226 | "core-js": { 227 | "version": "2.5.1", 228 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", 229 | "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" 230 | }, 231 | "cross-spawn": { 232 | "version": "5.1.0", 233 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", 234 | "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", 235 | "requires": { 236 | "lru-cache": "4.1.1", 237 | "shebang-command": "1.2.0", 238 | "which": "1.3.0" 239 | } 240 | }, 241 | "d": { 242 | "version": "1.0.0", 243 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", 244 | "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", 245 | "requires": { 246 | "es5-ext": "0.10.35" 247 | } 248 | }, 249 | "debug": { 250 | "version": "2.6.9", 251 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 252 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 253 | "requires": { 254 | "ms": "2.0.0" 255 | } 256 | }, 257 | "depd": { 258 | "version": "1.1.1", 259 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 260 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 261 | }, 262 | "destroy": { 263 | "version": "1.0.4", 264 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 265 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 266 | }, 267 | "editorconfig": { 268 | "version": "0.13.3", 269 | "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.13.3.tgz", 270 | "integrity": "sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ==", 271 | "requires": { 272 | "bluebird": "3.5.1", 273 | "commander": "2.11.0", 274 | "lru-cache": "3.2.0", 275 | "semver": "5.4.1", 276 | "sigmund": "1.0.1" 277 | }, 278 | "dependencies": { 279 | "lru-cache": { 280 | "version": "3.2.0", 281 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", 282 | "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", 283 | "requires": { 284 | "pseudomap": "1.0.2" 285 | } 286 | } 287 | } 288 | }, 289 | "ee-first": { 290 | "version": "1.1.1", 291 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 292 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 293 | }, 294 | "encodeurl": { 295 | "version": "1.0.1", 296 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", 297 | "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" 298 | }, 299 | "es5-ext": { 300 | "version": "0.10.35", 301 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.35.tgz", 302 | "integrity": "sha1-GO6FjOajxFx9eekcFfzKnsVoSU8=", 303 | "requires": { 304 | "es6-iterator": "2.0.3", 305 | "es6-symbol": "3.1.1" 306 | } 307 | }, 308 | "es6-iterator": { 309 | "version": "2.0.3", 310 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 311 | "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", 312 | "requires": { 313 | "d": "1.0.0", 314 | "es5-ext": "0.10.35", 315 | "es6-symbol": "3.1.1" 316 | } 317 | }, 318 | "es6-symbol": { 319 | "version": "3.1.1", 320 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", 321 | "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", 322 | "requires": { 323 | "d": "1.0.0", 324 | "es5-ext": "0.10.35" 325 | } 326 | }, 327 | "es6-weak-map": { 328 | "version": "2.0.2", 329 | "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", 330 | "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", 331 | "requires": { 332 | "d": "1.0.0", 333 | "es5-ext": "0.10.35", 334 | "es6-iterator": "2.0.3", 335 | "es6-symbol": "3.1.1" 336 | } 337 | }, 338 | "escape-html": { 339 | "version": "1.0.3", 340 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 341 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 342 | }, 343 | "escape-string-regexp": { 344 | "version": "1.0.5", 345 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 346 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 347 | }, 348 | "etag": { 349 | "version": "1.8.1", 350 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 351 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 352 | }, 353 | "event-emitter": { 354 | "version": "0.3.5", 355 | "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", 356 | "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", 357 | "requires": { 358 | "d": "1.0.0", 359 | "es5-ext": "0.10.35" 360 | } 361 | }, 362 | "execa": { 363 | "version": "0.7.0", 364 | "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", 365 | "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", 366 | "requires": { 367 | "cross-spawn": "5.1.0", 368 | "get-stream": "3.0.0", 369 | "is-stream": "1.1.0", 370 | "npm-run-path": "2.0.2", 371 | "p-finally": "1.0.0", 372 | "signal-exit": "3.0.2", 373 | "strip-eof": "1.0.0" 374 | } 375 | }, 376 | "express": { 377 | "version": "4.16.2", 378 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", 379 | "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", 380 | "requires": { 381 | "accepts": "1.3.4", 382 | "array-flatten": "1.1.1", 383 | "body-parser": "1.18.2", 384 | "content-disposition": "0.5.2", 385 | "content-type": "1.0.4", 386 | "cookie": "0.3.1", 387 | "cookie-signature": "1.0.6", 388 | "debug": "2.6.9", 389 | "depd": "1.1.1", 390 | "encodeurl": "1.0.1", 391 | "escape-html": "1.0.3", 392 | "etag": "1.8.1", 393 | "finalhandler": "1.1.0", 394 | "fresh": "0.5.2", 395 | "merge-descriptors": "1.0.1", 396 | "methods": "1.1.2", 397 | "on-finished": "2.3.0", 398 | "parseurl": "1.3.2", 399 | "path-to-regexp": "0.1.7", 400 | "proxy-addr": "2.0.2", 401 | "qs": "6.5.1", 402 | "range-parser": "1.2.0", 403 | "safe-buffer": "5.1.1", 404 | "send": "0.16.1", 405 | "serve-static": "1.13.1", 406 | "setprototypeof": "1.1.0", 407 | "statuses": "1.3.1", 408 | "type-is": "1.6.15", 409 | "utils-merge": "1.0.1", 410 | "vary": "1.1.2" 411 | } 412 | }, 413 | "finalhandler": { 414 | "version": "1.1.0", 415 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", 416 | "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", 417 | "requires": { 418 | "debug": "2.6.9", 419 | "encodeurl": "1.0.1", 420 | "escape-html": "1.0.3", 421 | "on-finished": "2.3.0", 422 | "parseurl": "1.3.2", 423 | "statuses": "1.3.1", 424 | "unpipe": "1.0.0" 425 | } 426 | }, 427 | "forwarded": { 428 | "version": "0.1.2", 429 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 430 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 431 | }, 432 | "fresh": { 433 | "version": "0.5.2", 434 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 435 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 436 | }, 437 | "fs-extra": { 438 | "version": "4.0.2", 439 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz", 440 | "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=", 441 | "requires": { 442 | "graceful-fs": "4.1.11", 443 | "jsonfile": "4.0.0", 444 | "universalify": "0.1.1" 445 | } 446 | }, 447 | "fs.realpath": { 448 | "version": "1.0.0", 449 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 450 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 451 | }, 452 | "gaze": { 453 | "version": "1.1.2", 454 | "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", 455 | "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", 456 | "requires": { 457 | "globule": "1.2.0" 458 | } 459 | }, 460 | "get-stream": { 461 | "version": "3.0.0", 462 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", 463 | "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" 464 | }, 465 | "glob": { 466 | "version": "7.1.2", 467 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 468 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 469 | "requires": { 470 | "fs.realpath": "1.0.0", 471 | "inflight": "1.0.6", 472 | "inherits": "2.0.3", 473 | "minimatch": "3.0.4", 474 | "once": "1.4.0", 475 | "path-is-absolute": "1.0.1" 476 | } 477 | }, 478 | "globule": { 479 | "version": "1.2.0", 480 | "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", 481 | "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", 482 | "requires": { 483 | "glob": "7.1.2", 484 | "lodash": "4.17.4", 485 | "minimatch": "3.0.4" 486 | } 487 | }, 488 | "graceful-fs": { 489 | "version": "4.1.11", 490 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 491 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" 492 | }, 493 | "graceful-readlink": { 494 | "version": "1.0.1", 495 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 496 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" 497 | }, 498 | "has-flag": { 499 | "version": "2.0.0", 500 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", 501 | "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" 502 | }, 503 | "http-errors": { 504 | "version": "1.6.2", 505 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 506 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 507 | "requires": { 508 | "depd": "1.1.1", 509 | "inherits": "2.0.3", 510 | "setprototypeof": "1.0.3", 511 | "statuses": "1.3.1" 512 | }, 513 | "dependencies": { 514 | "setprototypeof": { 515 | "version": "1.0.3", 516 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 517 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 518 | } 519 | } 520 | }, 521 | "iconv-lite": { 522 | "version": "0.4.19", 523 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 524 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 525 | }, 526 | "inflight": { 527 | "version": "1.0.6", 528 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 529 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 530 | "requires": { 531 | "once": "1.4.0", 532 | "wrappy": "1.0.2" 533 | } 534 | }, 535 | "inherits": { 536 | "version": "2.0.3", 537 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 538 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 539 | }, 540 | "ini": { 541 | "version": "1.3.4", 542 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", 543 | "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" 544 | }, 545 | "ipaddr.js": { 546 | "version": "1.5.2", 547 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", 548 | "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" 549 | }, 550 | "is-fullwidth-code-point": { 551 | "version": "2.0.0", 552 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 553 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 554 | }, 555 | "is-promise": { 556 | "version": "2.1.0", 557 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 558 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 559 | }, 560 | "is-stream": { 561 | "version": "1.1.0", 562 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 563 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 564 | }, 565 | "isexe": { 566 | "version": "2.0.0", 567 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 568 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 569 | }, 570 | "js-beautify": { 571 | "version": "1.7.4", 572 | "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.7.4.tgz", 573 | "integrity": "sha512-6YX1g+lIl0/JDxjFFbgj7fz6i0bWFa2Hdc7PfGqFhynaEiYe1NJ3R1nda0VGaRiGU82OllR+EGDoWFpGr3k5Kg==", 574 | "requires": { 575 | "config-chain": "1.1.11", 576 | "editorconfig": "0.13.3", 577 | "mkdirp": "0.5.1", 578 | "nopt": "3.0.6" 579 | } 580 | }, 581 | "jsonfile": { 582 | "version": "4.0.0", 583 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 584 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 585 | "requires": { 586 | "graceful-fs": "4.1.11" 587 | } 588 | }, 589 | "lodash": { 590 | "version": "4.17.4", 591 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 592 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 593 | }, 594 | "log-update": { 595 | "version": "2.1.0", 596 | "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.1.0.tgz", 597 | "integrity": "sha1-6jcli1NU7bAuc7KRkAFsh9HIcUE=", 598 | "requires": { 599 | "ansi-escapes": "2.0.0", 600 | "cli-cursor": "2.1.0", 601 | "wrap-ansi": "3.0.1" 602 | } 603 | }, 604 | "lru-cache": { 605 | "version": "4.1.1", 606 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", 607 | "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", 608 | "requires": { 609 | "pseudomap": "1.0.2", 610 | "yallist": "2.1.2" 611 | } 612 | }, 613 | "lru-queue": { 614 | "version": "0.1.0", 615 | "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", 616 | "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", 617 | "requires": { 618 | "es5-ext": "0.10.35" 619 | } 620 | }, 621 | "media-typer": { 622 | "version": "0.3.0", 623 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 624 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 625 | }, 626 | "memoizee": { 627 | "version": "0.4.11", 628 | "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.11.tgz", 629 | "integrity": "sha1-vemBdmPJ5A/bKk6hw2cpYIeujI8=", 630 | "requires": { 631 | "d": "1.0.0", 632 | "es5-ext": "0.10.35", 633 | "es6-weak-map": "2.0.2", 634 | "event-emitter": "0.3.5", 635 | "is-promise": "2.1.0", 636 | "lru-queue": "0.1.0", 637 | "next-tick": "1.0.0", 638 | "timers-ext": "0.1.2" 639 | } 640 | }, 641 | "merge-descriptors": { 642 | "version": "1.0.1", 643 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 644 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 645 | }, 646 | "methods": { 647 | "version": "1.1.2", 648 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 649 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 650 | }, 651 | "mime": { 652 | "version": "1.4.1", 653 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 654 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 655 | }, 656 | "mime-db": { 657 | "version": "1.30.0", 658 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 659 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 660 | }, 661 | "mime-types": { 662 | "version": "2.1.17", 663 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 664 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 665 | "requires": { 666 | "mime-db": "1.30.0" 667 | } 668 | }, 669 | "mimic-fn": { 670 | "version": "1.1.0", 671 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", 672 | "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=" 673 | }, 674 | "minimatch": { 675 | "version": "3.0.4", 676 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 677 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 678 | "requires": { 679 | "brace-expansion": "1.1.8" 680 | } 681 | }, 682 | "minimist": { 683 | "version": "0.0.8", 684 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 685 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 686 | }, 687 | "mkdirp": { 688 | "version": "0.5.1", 689 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 690 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 691 | "requires": { 692 | "minimist": "0.0.8" 693 | } 694 | }, 695 | "ms": { 696 | "version": "2.0.0", 697 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 698 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 699 | }, 700 | "negotiator": { 701 | "version": "0.6.1", 702 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 703 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 704 | }, 705 | "next-tick": { 706 | "version": "1.0.0", 707 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", 708 | "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" 709 | }, 710 | "nopt": { 711 | "version": "3.0.6", 712 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", 713 | "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", 714 | "requires": { 715 | "abbrev": "1.1.1" 716 | } 717 | }, 718 | "npm-run-path": { 719 | "version": "2.0.2", 720 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", 721 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", 722 | "requires": { 723 | "path-key": "2.0.1" 724 | } 725 | }, 726 | "number-is-nan": { 727 | "version": "1.0.1", 728 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 729 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 730 | }, 731 | "on-finished": { 732 | "version": "2.3.0", 733 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 734 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 735 | "requires": { 736 | "ee-first": "1.1.1" 737 | } 738 | }, 739 | "once": { 740 | "version": "1.4.0", 741 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 742 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 743 | "requires": { 744 | "wrappy": "1.0.2" 745 | } 746 | }, 747 | "onetime": { 748 | "version": "2.0.1", 749 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 750 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 751 | "requires": { 752 | "mimic-fn": "1.1.0" 753 | } 754 | }, 755 | "open": { 756 | "version": "0.0.5", 757 | "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", 758 | "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" 759 | }, 760 | "p-finally": { 761 | "version": "1.0.0", 762 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 763 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" 764 | }, 765 | "parseurl": { 766 | "version": "1.3.2", 767 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 768 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 769 | }, 770 | "path-is-absolute": { 771 | "version": "1.0.1", 772 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 773 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 774 | }, 775 | "path-key": { 776 | "version": "2.0.1", 777 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 778 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" 779 | }, 780 | "path-to-regexp": { 781 | "version": "0.1.7", 782 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 783 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 784 | }, 785 | "proto-list": { 786 | "version": "1.2.4", 787 | "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", 788 | "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" 789 | }, 790 | "proxy-addr": { 791 | "version": "2.0.2", 792 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", 793 | "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", 794 | "requires": { 795 | "forwarded": "0.1.2", 796 | "ipaddr.js": "1.5.2" 797 | } 798 | }, 799 | "pseudomap": { 800 | "version": "1.0.2", 801 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 802 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 803 | }, 804 | "qs": { 805 | "version": "6.5.1", 806 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 807 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 808 | }, 809 | "querystringify": { 810 | "version": "1.0.0", 811 | "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", 812 | "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=" 813 | }, 814 | "range-parser": { 815 | "version": "1.2.0", 816 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 817 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 818 | }, 819 | "raw-body": { 820 | "version": "2.3.2", 821 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 822 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 823 | "requires": { 824 | "bytes": "3.0.0", 825 | "http-errors": "1.6.2", 826 | "iconv-lite": "0.4.19", 827 | "unpipe": "1.0.0" 828 | } 829 | }, 830 | "regenerator-runtime": { 831 | "version": "0.11.0", 832 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", 833 | "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" 834 | }, 835 | "reload": { 836 | "version": "2.2.2", 837 | "resolved": "https://registry.npmjs.org/reload/-/reload-2.2.2.tgz", 838 | "integrity": "sha512-su5O0Db0LSxAw4XHl7FeSlu79PYaFt+OFxEIdc2/kb+pechWOnlQxV2LB+kHVdjjghQ7/2J1G/AWH20qUAywQQ==", 839 | "requires": { 840 | "cli-color": "1.2.0", 841 | "commander": "2.9.0", 842 | "finalhandler": "1.0.6", 843 | "minimist": "1.2.0", 844 | "open": "0.0.5", 845 | "serve-static": "1.12.6", 846 | "supervisor": "0.12.0", 847 | "url-parse": "1.1.9", 848 | "ws": "3.0.0" 849 | }, 850 | "dependencies": { 851 | "commander": { 852 | "version": "2.9.0", 853 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", 854 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", 855 | "requires": { 856 | "graceful-readlink": "1.0.1" 857 | } 858 | }, 859 | "finalhandler": { 860 | "version": "1.0.6", 861 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.6.tgz", 862 | "integrity": "sha1-AHrqM9Gk0+QgF/YkhIrVjSEvgU8=", 863 | "requires": { 864 | "debug": "2.6.9", 865 | "encodeurl": "1.0.1", 866 | "escape-html": "1.0.3", 867 | "on-finished": "2.3.0", 868 | "parseurl": "1.3.2", 869 | "statuses": "1.3.1", 870 | "unpipe": "1.0.0" 871 | } 872 | }, 873 | "mime": { 874 | "version": "1.3.4", 875 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", 876 | "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" 877 | }, 878 | "minimist": { 879 | "version": "1.2.0", 880 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 881 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 882 | }, 883 | "send": { 884 | "version": "0.15.6", 885 | "resolved": "https://registry.npmjs.org/send/-/send-0.15.6.tgz", 886 | "integrity": "sha1-IPI6nJJbdiq4JwX+L52yUqzkfjQ=", 887 | "requires": { 888 | "debug": "2.6.9", 889 | "depd": "1.1.1", 890 | "destroy": "1.0.4", 891 | "encodeurl": "1.0.1", 892 | "escape-html": "1.0.3", 893 | "etag": "1.8.1", 894 | "fresh": "0.5.2", 895 | "http-errors": "1.6.2", 896 | "mime": "1.3.4", 897 | "ms": "2.0.0", 898 | "on-finished": "2.3.0", 899 | "range-parser": "1.2.0", 900 | "statuses": "1.3.1" 901 | } 902 | }, 903 | "serve-static": { 904 | "version": "1.12.6", 905 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.6.tgz", 906 | "integrity": "sha1-uXN3P2NEmTTaVOW+ul4x2fQhFXc=", 907 | "requires": { 908 | "encodeurl": "1.0.1", 909 | "escape-html": "1.0.3", 910 | "parseurl": "1.3.2", 911 | "send": "0.15.6" 912 | } 913 | } 914 | } 915 | }, 916 | "requires-port": { 917 | "version": "1.0.0", 918 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 919 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" 920 | }, 921 | "restore-cursor": { 922 | "version": "2.0.0", 923 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 924 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 925 | "requires": { 926 | "onetime": "2.0.1", 927 | "signal-exit": "3.0.2" 928 | } 929 | }, 930 | "safe-buffer": { 931 | "version": "5.1.1", 932 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 933 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 934 | }, 935 | "semver": { 936 | "version": "5.4.1", 937 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 938 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" 939 | }, 940 | "send": { 941 | "version": "0.16.1", 942 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", 943 | "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", 944 | "requires": { 945 | "debug": "2.6.9", 946 | "depd": "1.1.1", 947 | "destroy": "1.0.4", 948 | "encodeurl": "1.0.1", 949 | "escape-html": "1.0.3", 950 | "etag": "1.8.1", 951 | "fresh": "0.5.2", 952 | "http-errors": "1.6.2", 953 | "mime": "1.4.1", 954 | "ms": "2.0.0", 955 | "on-finished": "2.3.0", 956 | "range-parser": "1.2.0", 957 | "statuses": "1.3.1" 958 | } 959 | }, 960 | "serve-static": { 961 | "version": "1.13.1", 962 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 963 | "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", 964 | "requires": { 965 | "encodeurl": "1.0.1", 966 | "escape-html": "1.0.3", 967 | "parseurl": "1.3.2", 968 | "send": "0.16.1" 969 | } 970 | }, 971 | "setprototypeof": { 972 | "version": "1.1.0", 973 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 974 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 975 | }, 976 | "shebang-command": { 977 | "version": "1.2.0", 978 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 979 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 980 | "requires": { 981 | "shebang-regex": "1.0.0" 982 | } 983 | }, 984 | "shebang-regex": { 985 | "version": "1.0.0", 986 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 987 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" 988 | }, 989 | "sigmund": { 990 | "version": "1.0.1", 991 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 992 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" 993 | }, 994 | "signal-exit": { 995 | "version": "3.0.2", 996 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 997 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 998 | }, 999 | "statuses": { 1000 | "version": "1.3.1", 1001 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 1002 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 1003 | }, 1004 | "string-width": { 1005 | "version": "2.1.1", 1006 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1007 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1008 | "requires": { 1009 | "is-fullwidth-code-point": "2.0.0", 1010 | "strip-ansi": "4.0.0" 1011 | } 1012 | }, 1013 | "strip-ansi": { 1014 | "version": "4.0.0", 1015 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1016 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1017 | "requires": { 1018 | "ansi-regex": "3.0.0" 1019 | } 1020 | }, 1021 | "strip-eof": { 1022 | "version": "1.0.0", 1023 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", 1024 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" 1025 | }, 1026 | "supervisor": { 1027 | "version": "0.12.0", 1028 | "resolved": "https://registry.npmjs.org/supervisor/-/supervisor-0.12.0.tgz", 1029 | "integrity": "sha1-3n5jNwFbKRhRwQ81OMSn8EkX7ME=" 1030 | }, 1031 | "supports-color": { 1032 | "version": "4.5.0", 1033 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", 1034 | "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", 1035 | "requires": { 1036 | "has-flag": "2.0.0" 1037 | } 1038 | }, 1039 | "term-size": { 1040 | "version": "1.2.0", 1041 | "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", 1042 | "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", 1043 | "requires": { 1044 | "execa": "0.7.0" 1045 | } 1046 | }, 1047 | "timers-ext": { 1048 | "version": "0.1.2", 1049 | "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.2.tgz", 1050 | "integrity": "sha1-YcxHp2wavTGV8UUn+XjViulMUgQ=", 1051 | "requires": { 1052 | "es5-ext": "0.10.35", 1053 | "next-tick": "1.0.0" 1054 | } 1055 | }, 1056 | "type-is": { 1057 | "version": "1.6.15", 1058 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 1059 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", 1060 | "requires": { 1061 | "media-typer": "0.3.0", 1062 | "mime-types": "2.1.17" 1063 | } 1064 | }, 1065 | "ultron": { 1066 | "version": "1.1.0", 1067 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", 1068 | "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" 1069 | }, 1070 | "universalify": { 1071 | "version": "0.1.1", 1072 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", 1073 | "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" 1074 | }, 1075 | "unpipe": { 1076 | "version": "1.0.0", 1077 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1078 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1079 | }, 1080 | "url-parse": { 1081 | "version": "1.1.9", 1082 | "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.1.9.tgz", 1083 | "integrity": "sha1-xn8dd11R8KGJEd17P/rSe7nlvRk=", 1084 | "requires": { 1085 | "querystringify": "1.0.0", 1086 | "requires-port": "1.0.0" 1087 | } 1088 | }, 1089 | "utils-merge": { 1090 | "version": "1.0.1", 1091 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1092 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1093 | }, 1094 | "vary": { 1095 | "version": "1.1.2", 1096 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1097 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1098 | }, 1099 | "which": { 1100 | "version": "1.3.0", 1101 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", 1102 | "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", 1103 | "requires": { 1104 | "isexe": "2.0.0" 1105 | } 1106 | }, 1107 | "widest-line": { 1108 | "version": "1.0.0", 1109 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", 1110 | "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", 1111 | "requires": { 1112 | "string-width": "1.0.2" 1113 | }, 1114 | "dependencies": { 1115 | "ansi-regex": { 1116 | "version": "2.1.1", 1117 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 1118 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 1119 | }, 1120 | "is-fullwidth-code-point": { 1121 | "version": "1.0.0", 1122 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 1123 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 1124 | "requires": { 1125 | "number-is-nan": "1.0.1" 1126 | } 1127 | }, 1128 | "string-width": { 1129 | "version": "1.0.2", 1130 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 1131 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 1132 | "requires": { 1133 | "code-point-at": "1.1.0", 1134 | "is-fullwidth-code-point": "1.0.0", 1135 | "strip-ansi": "3.0.1" 1136 | } 1137 | }, 1138 | "strip-ansi": { 1139 | "version": "3.0.1", 1140 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1141 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1142 | "requires": { 1143 | "ansi-regex": "2.1.1" 1144 | } 1145 | } 1146 | } 1147 | }, 1148 | "wrap-ansi": { 1149 | "version": "3.0.1", 1150 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", 1151 | "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", 1152 | "requires": { 1153 | "string-width": "2.1.1", 1154 | "strip-ansi": "4.0.0" 1155 | } 1156 | }, 1157 | "wrappy": { 1158 | "version": "1.0.2", 1159 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1160 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1161 | }, 1162 | "ws": { 1163 | "version": "3.0.0", 1164 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.0.0.tgz", 1165 | "integrity": "sha1-mN2wAFbIOQy3Ued4h4hJf5kQO2w=", 1166 | "requires": { 1167 | "safe-buffer": "5.0.1", 1168 | "ultron": "1.1.0" 1169 | }, 1170 | "dependencies": { 1171 | "safe-buffer": { 1172 | "version": "5.0.1", 1173 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", 1174 | "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" 1175 | } 1176 | } 1177 | }, 1178 | "yallist": { 1179 | "version": "2.1.2", 1180 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 1181 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 1182 | } 1183 | } 1184 | } 1185 | -------------------------------------------------------------------------------- /packages/heml/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heml", 3 | "version": "1.1.3", 4 | "description": "HEML is an open source markup language for building responsive email", 5 | "keywords": [ 6 | "heml", 7 | "email" 8 | ], 9 | "homepage": "https://heml.io", 10 | "bugs": "https://github.com/SparkPost/heml/issues", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/SparkPost/heml.git" 15 | }, 16 | "author": "SparkPost (https://sparkpost.com)", 17 | "files": [ 18 | "build/" 19 | ], 20 | "main": "build/index.js", 21 | "bin": "build/bin/heml.js", 22 | "dependencies": { 23 | "@heml/elements": "^1.1.3", 24 | "@heml/inline": "^1.1.2", 25 | "@heml/parse": "^1.1.2", 26 | "@heml/render": "^1.1.2", 27 | "@heml/utils": "^1.1.2", 28 | "@heml/validate": "^1.1.2", 29 | "babel-runtime": "^6.26.0", 30 | "boxen": "^1.2.1", 31 | "byte-length": "^0.1.1", 32 | "chalk": "^2.1.0", 33 | "commander": "^2.11.0", 34 | "express": "^4.16.2", 35 | "fs-extra": "^4.0.2", 36 | "gaze": "^1.1.2", 37 | "get-port": "^3.2.0", 38 | "js-beautify": "^1.7.4", 39 | "lodash": "^4.17.4", 40 | "log-update": "^2.1.0", 41 | "open": "0.0.5", 42 | "reload": "^2.2.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/heml/src/bin/commands/build.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { writeFile } from 'fs-extra' 3 | import chalk, { red as error, yellow as code, blue, dim } from 'chalk' 4 | import isHemlFile from '../utils/isHemlFile' 5 | import renderHemlFile from '../utils/renderHemlFile' 6 | 7 | const errorBlock = chalk.bgRed.black 8 | const successBlock = chalk.bgGreen.black 9 | const { log } = console 10 | 11 | export default async function build (file, options) { 12 | const filepath = path.resolve(file) 13 | const outputpath = path.resolve(options.output || file.replace(/\.heml$/, '.html')) 14 | 15 | /** require .heml extention */ 16 | if (!isHemlFile(file)) { 17 | log(`${error('ERROR')} ${file} must have ${code('.heml')} extention`) 18 | process.exit(1) 19 | } 20 | 21 | try { 22 | log(`${chalk.bgBlue.black(' COMPILING ')}`) 23 | log(`${blue(' -')} Reading ${file}`) 24 | log(`${blue(' -')} Building HEML`) 25 | const { html, metadata, errors } = await renderHemlFile(filepath, options) 26 | 27 | log(`${blue(' -')} Writing ${metadata.size}`) 28 | await writeFile(outputpath, html) 29 | 30 | const relativePath = code(path.relative(process.cwd(), outputpath)) 31 | 32 | log(errors.length 33 | ? `\n${errorBlock(' DONE ')} Compiled with errors to ${code(relativePath)} in ${metadata.time}ms\n` 34 | : `\n${successBlock(' DONE ')} Compiled successfully to ${code(relativePath)} in ${metadata.time}ms\n`) 35 | 36 | if (errors.length) { 37 | log(error(`${errors.length} ${errors.length > 1 ? 'errors' : 'error'} `)) 38 | errors.forEach((err) => log(`> ${code(err.selector)}\n ${err.message}`)) 39 | } 40 | } catch (err) { 41 | log(`\n${errorBlock(' ERROR ')} ${err.message}\n${dim(err.stack)}`) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/heml/src/bin/commands/develop.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import express from 'express' 3 | import reloadServer from 'reload' 4 | import openUrl from 'open' 5 | import logUpdate from 'log-update' 6 | import boxen from 'boxen' 7 | import gaze from 'gaze' 8 | import getPort from 'get-port' 9 | import chalk, { red as error, yellow as code } from 'chalk' 10 | import isHemlFile from '../utils/isHemlFile' 11 | import renderHemlFile from '../utils/renderHemlFile' 12 | import buildErrorPage from '../utils/buildErrorPage' 13 | 14 | const errorBlock = chalk.bgRed.white 15 | const { log } = console 16 | 17 | export default async function develop (file, options) { 18 | const filepath = path.resolve(file) 19 | const { 20 | port = 3000, 21 | open = false 22 | } = options 23 | 24 | /** require .heml extention */ 25 | if (!isHemlFile(file)) { 26 | log(`${error('ERROR')} ${file} must have ${code('.heml')} extention`) 27 | process.exit(1) 28 | } 29 | 30 | try { 31 | const { update, url } = await startDevServer(path.dirname(filepath), port) 32 | const { html, errors, metadata } = await renderHemlFile(filepath) 33 | 34 | update({ html, errors, metadata }) 35 | 36 | if (open) openUrl(url) 37 | 38 | /** watch for file changes */ 39 | gaze(filepath, function (err) { 40 | if (err) throw err 41 | 42 | this.on('changed', async (changedFile) => { 43 | const { html, errors, metadata } = await renderHemlFile(filepath) 44 | update({ html, errors, metadata }) 45 | }) 46 | 47 | this.on('deleted', async (changedFile) => { 48 | log(`${errorBlock(' Error ')} ${code(file)} was deleted. Shutting down.`) 49 | process.exit() 50 | }) 51 | }) 52 | } catch (err) { 53 | if (err.code === 'ENOENT') { 54 | log(`${errorBlock(' Error ')} ${code(file)} doesn't exist`) 55 | } else { 56 | log(`${errorBlock(' Error ')} ${err.message}`) 57 | } 58 | process.exit() 59 | } 60 | } 61 | 62 | /** 63 | * update the cli UI 64 | * @param {String} params.url URL for preview server 65 | * @param {String} params.status the current status 66 | * @param {String} params.time time to compile the heml 67 | * @param {String} params.size size of the HTML in mb 68 | */ 69 | function renderCLI ({ url, status, time, size }) { 70 | return logUpdate(boxen( 71 | `${chalk.bgBlue.black(' HEML ')}\n\n` + 72 | `- ${chalk.bold('Preview:')} ${url}\n` + 73 | `- ${chalk.bold('Status:')} ${status}\n` + 74 | `- ${chalk.bold('Compile time:')} ${time}ms\n` + 75 | `- ${chalk.bold('Total size:')} ${size}`, 76 | { padding: 1, margin: 1 })) 77 | } 78 | 79 | /** 80 | * Launches a server that reloads when the update function is called 81 | * @param {String} defaultPreview the default content for when the sever loads 82 | * @return {Object} { server, port, update } 83 | */ 84 | function startDevServer (directory, port = 3000) { 85 | let url 86 | const app = express() 87 | const { reload } = reloadServer(app) 88 | let preview = '' 89 | 90 | app.get('/', (req, res) => res.send(preview)) 91 | app.use(express.static(directory)) 92 | 93 | function update ({ html, errors, metadata }) { 94 | let status = errors.length ? chalk.red('failed') : chalk.green('success') 95 | preview = errors.length 96 | ? buildErrorPage(errors) 97 | : html.replace('', '') 98 | 99 | renderCLI({ url, status, time: metadata.time, size: metadata.size }) 100 | reload() 101 | } 102 | 103 | return new Promise((resolve, reject) => { 104 | getPort({ port }).then((availablePort) => { 105 | url = `http://localhost:${availablePort}` 106 | 107 | app.listen(availablePort, () => resolve({ update, url, app })) 108 | }) 109 | 110 | process.on('uncaughtException', reject) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /packages/heml/src/bin/heml.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import cli from 'commander' 4 | import { first } from 'lodash' 5 | import develop from './commands/develop' 6 | import build from './commands/build' 7 | import { version } from '../../package' 8 | 9 | const commands = ['develop', 'build'] 10 | const args = process.argv.slice(2) 11 | 12 | cli 13 | .usage(' [options]') 14 | .version(version) 15 | 16 | cli 17 | .command('develop ') 18 | .description('Develop your email locally.') 19 | .option('--open', 'Open the email in your browser') 20 | .option('-p, --port ', 'Port for server', 3000) 21 | .action(develop) 22 | 23 | cli 24 | .command('build ') 25 | .description('Build an HEML email for sending in the wild.') 26 | .option('-o, --output ', 'The output HTML file') 27 | .option('-v, --validate [level]', 'Sets the validation level', /^(none|soft|strict)$/i, 'soft') 28 | .action(build) 29 | 30 | if (args.length === 0 || !commands.includes(first(args)) && !first(args).startsWith('-')) { 31 | cli.outputHelp() 32 | } 33 | 34 | cli.parse(process.argv) 35 | -------------------------------------------------------------------------------- /packages/heml/src/bin/utils/buildErrorPage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * builds the html that gets rendered when there are heml errors 3 | * @param {Array} errors an array of HEMLErrors 4 | * @return {String} some html 5 | */ 6 | export default function (errors = []) { 7 | const title = `${errors.length} validation ${errors.length > 1 ? 'errors' : 'error'}` 8 | return ` 9 | 10 | 11 | ${title} 12 | 43 | 44 | 45 |

${title}


46 | 47 |
48 | ${errors.map((error) => ` 49 |
50 |
> ${error.selector}
51 | ${error.toString()} 52 |
`).join('')} 53 |
54 | 55 | 56 | 57 | ` 58 | } 59 | -------------------------------------------------------------------------------- /packages/heml/src/bin/utils/isHemlFile.js: -------------------------------------------------------------------------------- 1 | export default function (path) { 2 | return /\.heml$/.test(path) 3 | } 4 | -------------------------------------------------------------------------------- /packages/heml/src/bin/utils/renderHemlFile.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs-extra' 2 | import heml from '../../' 3 | 4 | async function renderHemlFile (filepath, options) { 5 | const contents = await readFile(filepath, 'utf8') 6 | const startTime = process.hrtime() 7 | const results = await heml(contents, options) 8 | results.metadata.time = Math.round(process.hrtime(startTime)[1] / 1000000) 9 | 10 | return results 11 | } 12 | 13 | export default renderHemlFile 14 | -------------------------------------------------------------------------------- /packages/heml/src/index.js: -------------------------------------------------------------------------------- 1 | import parse from '@heml/parse' 2 | import render from '@heml/render' 3 | import inline from '@heml/inline' 4 | import validate from '@heml/validate' 5 | import { condition } from '@heml/utils' 6 | import byteLength from 'byte-length' 7 | import { html as beautify } from 'js-beautify' 8 | import { toArray, flattenDeep } from 'lodash' 9 | import * as coreElements from '@heml/elements' 10 | 11 | /** 12 | * renders the given HEML string with the config provided 13 | * @param {String} HEML the heml to render 14 | * @param {Object} options the options 15 | * @return {Object} { metadata, html, errors } 16 | */ 17 | async function heml (contents, options = {}) { 18 | const results = {} 19 | const { 20 | beautify: beautifyOptions = {}, 21 | validate: validateOption = 'soft' 22 | } = options 23 | 24 | options.elements = flattenDeep(toArray(coreElements).concat(options.elements || [])) 25 | 26 | /** parse it ✂️ */ 27 | const $heml = parse(contents, options) 28 | 29 | /** validate it 🕵 */ 30 | const errors = validate($heml, options) 31 | if (validateOption.toLowerCase() === 'strict' && errors.length > 0) { throw errors[0] } 32 | if (validateOption.toLowerCase() === 'soft') { results.errors = errors } 33 | 34 | /** render it 🤖 */ 35 | const { 36 | $: $html, 37 | metadata 38 | } = await render($heml, options) 39 | 40 | /** inline it ✍️ */ 41 | inline($html, options) 42 | 43 | /** beautify it 💅 */ 44 | results.html = condition.replace(beautify($html.html(), { 45 | indent_size: 2, 46 | indent_inner_html: true, 47 | preserve_newlines: false, 48 | extra_liners: [], 49 | ...beautifyOptions })) 50 | 51 | /** final touches 👌 */ 52 | metadata.size = `${(byteLength(results.html) / 1024).toFixed(2)}kb` 53 | results.metadata = metadata 54 | 55 | /** send it back 🎉 */ 56 | return results 57 | } 58 | 59 | /** module.exports for commonjs */ 60 | module.exports = heml 61 | -------------------------------------------------------------------------------- /test/__fixtures__/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('require-all')({ 2 | dirname: __dirname, 3 | filter: /.*\.fixture\.js$/ 4 | }) 5 | -------------------------------------------------------------------------------- /test/__fixtures__/simple.fixture.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | 3 | 4 | Welcome to HEML! 5 | 9 | 10 | 11 | 12 |

Explore the world of email! 💌

13 |
14 | 15 |
16 | ` 17 | -------------------------------------------------------------------------------- /test/__snapshots__/snapshot.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`simple 1`] = ` 4 | Object { 5 | "errors": Array [], 6 | "html": " 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 69 | 75 | Welcome to HEML! 76 | 77 | 83 | 84 | 85 | 86 | 87 | 104 | 105 |
88 |
92 | 93 | 94 | 99 | 100 |
95 | 96 |

Explore the world of email! 💌

97 |
98 |
103 |
106 |
                                                           
107 | 108 | ", 109 | "metadata": Object { 110 | "meta": Array [], 111 | "size": "3.86kb", 112 | "subject": "Welcome to HEML!", 113 | }, 114 | } 115 | `; 116 | -------------------------------------------------------------------------------- /test/snapshot.spec.js: -------------------------------------------------------------------------------- 1 | const heml = require('../packages/heml') 2 | const fixtures = require('./__fixtures__') 3 | 4 | for (const filename in fixtures) { 5 | const testName = filename.replace(/\.fixture\.js$/, '') 6 | const fixture = fixtures[filename] 7 | 8 | test(testName, () => heml(fixture).then((result) => expect(result).toMatchSnapshot())) 9 | } 10 | --------------------------------------------------------------------------------