├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .jscsrc ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── PROJECTS.md ├── README.md ├── bin └── juice ├── client.js ├── index.js ├── juice.d.ts ├── lib ├── cheerio.js ├── cli.js ├── inline.js ├── numbers.js ├── property.js ├── selector.js ├── utils.js └── variables.js ├── package-lock.json ├── package.json └── test ├── cases ├── alpha.css ├── alpha.html ├── alpha.out ├── cascading.css ├── cascading.html ├── cascading.out ├── cheerio.css ├── cheerio.html ├── cheerio.out ├── class+id.css ├── class+id.html ├── class+id.out ├── class.css ├── class.html ├── class.out ├── cli │ ├── code-block-cli.css │ ├── code-block-cli.html │ ├── code-block-cli.json │ └── code-block-cli.out ├── css-quotes.css ├── css-quotes.html ├── css-quotes.out ├── css-variables.css ├── css-variables.html ├── css-variables.json ├── css-variables.out ├── direct-descendents.css ├── direct-descendents.html ├── direct-descendents.out ├── ejs-unterminated.css ├── ejs-unterminated.html ├── ejs-unterminated.out ├── ejs.css ├── ejs.html ├── ejs.out ├── empty.css ├── empty.html ├── empty.out ├── entities.css ├── entities.html ├── entities.out ├── hbs-unterminated.css ├── hbs-unterminated.html ├── hbs-unterminated.out ├── hbs.css ├── hbs.html ├── hbs.out ├── id.css ├── id.html ├── id.out ├── identical-important.css ├── identical-important.html ├── identical-important.out ├── ignore-pseudos.css ├── ignore-pseudos.html ├── ignore-pseudos.out ├── important-specificity.css ├── important-specificity.html ├── important-specificity.out ├── important.css ├── important.html ├── important.out ├── improper-syntax-affect-cascade-issue44.css ├── improper-syntax-affect-cascade-issue44.html ├── improper-syntax-affect-cascade-issue44.out ├── inline-specificity.css ├── inline-specificity.html ├── inline-specificity.out ├── integration.css ├── integration.html ├── integration.out ├── juice-content │ ├── counter-reset.css │ ├── counter-reset.html │ ├── counter-reset.json │ ├── counter-reset.out │ ├── embed.css │ ├── embed.html │ ├── embed.json │ ├── embed.out │ ├── empty-style-tag.css │ ├── empty-style-tag.html │ ├── empty-style-tag.json │ ├── empty-style-tag.out │ ├── file-missing.css │ ├── file-missing.html │ ├── file-missing.json │ ├── file-missing.out │ ├── font-face-preserve.css │ ├── font-face-preserve.html │ ├── font-face-preserve.json │ ├── font-face-preserve.out │ ├── font-quotes.css │ ├── font-quotes.html │ ├── font-quotes.json │ ├── font-quotes.out │ ├── height-attr.css │ ├── height-attr.html │ ├── height-attr.json │ ├── height-attr.out │ ├── important.css │ ├── important.html │ ├── important.json │ ├── important.out │ ├── insert-preserve-fragment.css │ ├── insert-preserve-fragment.html │ ├── insert-preserve-fragment.json │ ├── insert-preserve-fragment.out │ ├── keyframes-preserve.css │ ├── keyframes-preserve.html │ ├── keyframes-preserve.json │ ├── keyframes-preserve.out │ ├── lt-in-comments-issue216.css │ ├── lt-in-comments-issue216.html │ ├── lt-in-comments-issue216.json │ ├── lt-in-comments-issue216.out │ ├── media-preserve.css │ ├── media-preserve.html │ ├── media-preserve.json │ ├── media-preserve.out │ ├── ms-filter-syntax-issue74.css │ ├── ms-filter-syntax-issue74.html │ ├── ms-filter-syntax-issue74.json │ ├── ms-filter-syntax-issue74.out │ ├── no-css.css │ ├── no-css.html │ ├── no-css.json │ ├── no-css.out │ ├── non-visual.css │ ├── non-visual.html │ ├── non-visual.json │ ├── non-visual.out │ ├── prevent-css-gradient-in-attribute-issue319.css │ ├── prevent-css-gradient-in-attribute-issue319.html │ ├── prevent-css-gradient-in-attribute-issue319.json │ ├── prevent-css-gradient-in-attribute-issue319.out │ ├── pseudo-elements-and-width-attr.css │ ├── pseudo-elements-and-width-attr.html │ ├── pseudo-elements-and-width-attr.json │ ├── pseudo-elements-and-width-attr.out │ ├── pseudo-elements.css │ ├── pseudo-elements.html │ ├── pseudo-elements.json │ ├── pseudo-elements.out │ ├── pseudo-selector-preserve.css │ ├── pseudo-selector-preserve.html │ ├── pseudo-selector-preserve.json │ ├── pseudo-selector-preserve.out │ ├── relative-url.css │ ├── relative-url.html │ ├── relative-url.json │ ├── relative-url.out │ ├── table-attr.css │ ├── table-attr.html │ ├── table-attr.json │ ├── table-attr.out │ ├── width-attr.css │ ├── width-attr.html │ ├── width-attr.json │ ├── width-attr.out │ ├── xml.css │ ├── xml.html │ ├── xml.json │ └── xml.out ├── linear-gradient-space-issue-226.css ├── linear-gradient-space-issue-226.html ├── linear-gradient-space-issue-226.out ├── media.css ├── media.html ├── media.out ├── multiple-properties-occourences.css ├── multiple-properties-occourences.html ├── multiple-properties-occourences.out ├── normalize.css ├── normalize.html ├── normalize.out ├── not-xhtml.css ├── not-xhtml.html ├── not-xhtml.out ├── php.css ├── php.html ├── php.out ├── preserve-events.css ├── preserve-events.html ├── preserve-events.out ├── propertyorder.css ├── propertyorder.html ├── propertyorder.out ├── pseudo-specificity.css ├── pseudo-specificity.html ├── pseudo-specificity.out ├── regression-media.css ├── regression-media.html ├── regression-media.out ├── regression-selector-newline.css ├── regression-selector-newline.html ├── regression-selector-newline.out ├── shorthand.css ├── shorthand.html ├── shorthand.out ├── specificity.css ├── specificity.html ├── specificity.out ├── specificitygte10.css ├── specificitygte10.html ├── specificitygte10.out ├── style-preservation.css ├── style-preservation.html ├── style-preservation.out ├── tag.css ├── tag.html ├── tag.out ├── templates.css ├── templates.html ├── templates.out ├── yui-reset.css ├── yui-reset.html └── yui-reset.out ├── cli.js ├── html ├── Test.css ├── doctype.in.html ├── doctype.out.html ├── no_css.in.html ├── no_css.out.html ├── remote_url.in.html ├── remote_url.out.html ├── spaces in path │ └── Test.css ├── spaces_in_path.in.html ├── spaces_in_path.out.html ├── two_styles.in.html └── two_styles.out.html ├── juice.test.js ├── run.js ├── test.js └── typescript └── juice-tests.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [18, 20, 22] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | - run: npm test 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *~ 4 | coverage 5 | npm-debug.log 6 | tmp 7 | test/typescript/*.js 8 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowKeywords": ["with"], 3 | "disallowKeywordsOnNewLine": ["else"], 4 | "disallowMixedSpacesAndTabs": true, 5 | "disallowMultipleVarDecl": "exceptUndefined", 6 | "disallowNewlineBeforeBlockStatements": true, 7 | "disallowSpaceAfterObjectKeys": true, 8 | "disallowSpaceAfterPrefixUnaryOperators": true, 9 | "disallowSpacesInFunction": { 10 | "beforeOpeningRoundBrace": true 11 | }, 12 | "disallowSpacesInsideParentheses": true, 13 | "disallowTrailingWhitespace": true, 14 | "maximumLineLength": 120, 15 | "requireCamelCaseOrUpperCaseIdentifiers": true, 16 | "requireCapitalizedConstructors": true, 17 | "requireCurlyBraces": true, 18 | "requireSpaceAfterKeywords": [ 19 | "if", 20 | "else", 21 | "for", 22 | "while", 23 | "do", 24 | "switch", 25 | "case", 26 | "return", 27 | "try", 28 | "catch", 29 | "typeof" 30 | ], 31 | "requireSpaceAfterLineComment": true, 32 | "requireSpaceAfterBinaryOperators": true, 33 | "requireSpaceBeforeBinaryOperators": true, 34 | "requireSpaceBeforeBlockStatements": true, 35 | "requireSpaceBeforeObjectValues": true, 36 | "requireSpacesInFunction": { 37 | "beforeOpeningCurlyBrace": true 38 | }, 39 | "requireEarlyReturn": true, 40 | "validateIndentation": 2, 41 | "validateLineBreaks": "LF", 42 | "validateQuoteMarks": "'" 43 | } 44 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | support/ 3 | coverage/ 4 | tmp/ 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 10.0.0 / 2023-12-03 2 | 3 | * Resolve css variables 4 | 5 | # 9.1.0 / 2023-07-03 6 | 7 | * Fix: An error is thrown in resetCounter when adding an counter-reset inline style to an element in combination with a different style (e.g., in head). 8 | 9 | # 8.0.0 / 2021-22-21 10 | 11 | * pseudo Update 12 | * !important table attribute update 13 | 14 | # 7.0.0 / 2020-07-10 15 | 16 | * Breaking: requires node 10+ 17 | * Please see changes in web-resource-inliner@5 for other details 18 | 19 | # 6.0.0 / 2019-11-27 20 | 21 | * Drop support for Node.js 4 22 | 23 | # 5.2.0 / 2019-03-18 24 | 25 | * Fix bug with `getPreservedText` (mixed up `preservePseudos` and `ignorePseudos`) 26 | 27 | # 5.1.0 / 2018-12-18 28 | 29 | * Add `codeBlocks` support in CLI `--options-file` 30 | 31 | # 5.0.0 / 2018-10-02 32 | 33 | * Fix gradient CSS being inlined into the `background` attribute 34 | 35 | # 5.0.0 / 2018-10-02 36 | 37 | * Adds `preservePseudos` option with default `true` 38 | * Adds `th` to list of elements that can receive a `width` attribute 39 | 40 | # 4.3.0 / 2018-06-01 41 | 42 | * Adds `preserveKeyFrames` option 43 | 44 | # 4.2.3 / 2018-03-08 45 | 46 | * fix help typo 47 | * fix memory leak in cheerio code block encode/decode 48 | * update TS definition to allow for optional fileContent option 49 | 50 | # 4.2.2 / 2017-10-22 51 | 52 | * Fix code blocks encode/decode so cheerio doesn't mulch them if they are tag attributes 53 | 54 | # 4.2.1 / 2017-10-20 55 | 56 | * Fix regex catastrophic backtracking issue with unclosed code blocks 57 | 58 | # 4.2.0 / 2017-10-05 59 | 60 | * Adds support for `preserveImportant` to CLI 61 | 62 | # 4.0.1 / 2016-11-24 63 | 64 | * Updated typescript definitions 65 | 66 | # 4.0.0 / 2016-11-17 67 | 68 | * deps: update web-resource-inliner and cross-spawn 69 | * engine: specify support as node 4.2+ 70 | * feat: add option `codeBlocks` to support general exclusion of fenced code, like EJS, HBS, etc. 71 | 72 | # 3.0.1 / 2016-10-06 73 | 74 | * deps: install mensch from npm instead of github 75 | 76 | # 3.0.0 / 2016-06-01 77 | 78 | * deps: upgrade web-resource-inliner, change cross-spawn-async to cross-spawn, move batch to devDeps 79 | 80 | # 2.0.0 / 2016-05-24 81 | 82 | * fix: specificity bugs 83 | * deps: upgrade cheerio, cssom, web-resource-inliner 84 | * major: most options default to `true` instead of `false` now 85 | * major: remove deprecated `toArray` 86 | 87 | # 1.11.0 / 2016-05-11 88 | 89 | * feat: exclude css properties from inliner with juiceClient.excludedProperties 90 | * fix: specificity of consecutive !important rules 91 | 92 | # 1.10.0 / 2016-02-25 93 | 94 | * cli: correctly handle absolute path for `optionsFile` 95 | * cli: convert all numbers and booleans from default string values 96 | * Replace deprecated dep `win-spawn` with `cross-spawn-async` 97 | 98 | # 1.9.0 / 2016-01-04 99 | 100 | * Add option `insertPreservedExtraCss` 101 | 102 | # 1.8.1 / 2015-12-01 103 | 104 | * Switch xtend to deep-extend to fix issue loading webResources settings when using --options-file 105 | 106 | # 1.8.0 / 2015-11-23 107 | 108 | * Make all options available through CLI 109 | * Add `--options-file` to CLI for loading JSON options 110 | * Make CLI tests work on Windows 111 | * Deprecate `utils.toArray()` 112 | * Fix handling of `:not()` specificity 113 | 114 | # 1.7.0 / 2015-11-03 115 | 116 | * Refactor to provide browser support at `juice/client` 117 | * Add option `applyHeightAttributes` 118 | * Bump dep `web-resource-inliner` 119 | 120 | # 1.6.0 / 2015-10-26 121 | 122 | * Add feature `data-embed` attribute 123 | * update deps: web-resource-inliner, batch, commander, slick 124 | 125 | # 1.5.0 / 2015-09-25 126 | 127 | * Exclude non-visual tags from inlining 128 | * Add option `preserveFontFaces` 129 | * update dep: web-resource-inliner 130 | 131 | # 1.4.0 / 2015-08-19 132 | 133 | * Add extra CSS option to CLI 134 | * CLI has test coverage now 135 | 136 | # 1.3.3 / 2015-07-14 137 | 138 | * Prevent mangling of EJS tags 139 | 140 | # 1.3.0 / 2015-07-02 141 | 142 | * Add option `preserveImportant` 143 | * Make lib `use_strict` compliant and lint files 144 | 145 | # 1.2.0 / 2015-05-21 146 | 147 | * Add `xmlMode` option 148 | 149 | # 1.1.2 / 2015-05-08 150 | 151 | * remove index.js and point `main` in package.json to `/lib/juice.js` 152 | 153 | # 1.1.1 154 | 155 | * publish with line endings fixed in /bin 156 | 157 | # 1.1.0 / 2015-05-04 158 | 159 | * Fix order of inlined style properties. Now sorted by selector specificity, resulting in the same computed styles that the original CSS would have had. 160 | * Add option to inline pseudo elements as elements 161 | 162 | # 1.0.2 / 2015-04-27 163 | 164 | * added option `applyAttributesTableElements` 165 | * bump version on web-resource-inliner to 1.1.1 166 | * fix bin/juice so it works as documented 167 | 168 | # 1.0.1 / 2015-02-22 169 | 170 | * legacy support for `url` option 171 | * bump version on web-resource-inliner to use `relativeTo` with a url and remote paths starting `//` 172 | * update skipped tests to mocha so they will run with `npm test` and on travis 173 | * bump web-resource-inliner version to expose `strict` option, which is now `false` by default 174 | 175 | # 1.0.0 / 2015-02-12 176 | 177 | * add support for node 0.12 178 | * drop support for node 0.8 179 | * use cheerio instead of jsdom 180 | * move remote resource fetching to external library web-resource-inliner 181 | * adjust public methods as needed to support other changes 182 | * rename `juiceContent` to `juiceResources` 183 | * maintain CSS single quotes 184 | * ability to inline css pixel widths to `width` attribute with `applyWidthAttributes` 185 | * alphabetize styles for improved specificity 186 | * ability to keep media query styles with option `preserveMediaQueries` 187 | * clean up testing setup, including removing old dependency on expresso 188 | * istanbul put in place to show test coverage 189 | * remove now unused options `applyLinkTags` and `removeLinkTags` 190 | 191 | # 0.5.0 / 2014-09-08 192 | 193 | * update dependencies [binarykitchen] 194 | * handle errors in loading external css 195 | * add repository field to readme 196 | 197 | # 0.4.0 / 2013-04-15 198 | 199 | * update jsdom dependency to 0.6.0 200 | 201 | # 0.3.3 / 2013-04-08 202 | 203 | * fix resolving file:// paths on windows (thanks Mirco Zeiss) 204 | * fix crash during cleanup. (thanks Ger Hobbelt) 205 | * update superagent to 0.14.0 206 | 207 | # 0.3.2 / 2013-03-26 208 | 209 | * fix regression: not ignoring pseudos 210 | 211 | # 0.3.1 / 2013-03-26 212 | 213 | * do not crash on ::selectors (covered by normalize.css test case) 214 | 215 | # 0.3.0 / 2013-03-26 216 | 217 | * update jsdom dependency to 0.5.4 218 | * support node v0.10 219 | * switch dependency to slick instead of mootools which was rudely unpublished 220 | 221 | # 0.2.0 / 2013-02-13 222 | 223 | * update jsdom dependency to 0.5.0 224 | 225 | # 0.1.3 / 2013-02-12 226 | 227 | * fix specificity test. all test cases passed now. 228 | * add a command line `juice` program 229 | 230 | # 0.1.2 / 2013-02-11 231 | 232 | * fix incorrectly lowercasing href 233 | 234 | # 0.1.1 / 2013-02-11 235 | 236 | * explicitly document which node versions are supported 237 | with `engines` and travis-ci. 238 | * expose `juice.inlineDocument` and `juice.inlineContent` 239 | 240 | # 0.1.0 / 2013-02-07 241 | 242 | * fix / test case for @media queries 243 | * merge [boost](https://github.com/superjoe30/boost) into juice 244 | * legacy `juice` function still works as is 245 | * add `juice(filePath, [options], callback)` 246 | * add `juice.juiceDocument(document, options, callback)` 247 | * add `juice.juiceContent(html, options, callback)` 248 | * remove `juice.juiceDom` 249 | 250 | # 0.0.9 / 2013-02-07 251 | 252 | * update jsdom dependency to 0.4.0 253 | * update cssom dependency to 0.2.5 254 | 255 | # 0.0.8 / 2013-02-06 256 | 257 | * expose a lower level export so you can operate on a jsdom document [superjoe30] 258 | * fix exports not working [superjoe30] 259 | * fix jshint problems [superjoe30] 260 | 261 | # 0.0.7 / 2013-02-06 262 | 263 | * fixed test case expected outputs to have starting and ending and tags as jsdom appends them in its html() function if they do not exist 264 | * regression test for previous fix for media queries. note i had to wrap my test .out content in tags in order to pass tests, it looks like they are appended at some point for partial html content which just guessing is new behavior from when these tests were written 265 | * make sure the css rule has selectorText to prevent parsing exception. hit this on @media rules which do not have selector text. afaik this means media queries will not be inlined. however everything else is. 266 | * bump jsdom 267 | * Added note regarding node-email-templates to README 268 | 269 | # 0.0.6 / 2011-12-20 270 | 271 | * Corrected juice unit tests for latest cssom. 272 | * Fixed presence of \n in selectors. 273 | * Fixed unneeded removal of inline event handlers in html. 274 | * Bumped jsdom. 275 | 276 | # 0.0.5 / 2011-10-10 277 | 278 | * Added whitelist of pseudos to ignore (fixes `:first-child` etc) 279 | * Fine-tuned jsdom for speed (disabled unneded features). 280 | * Added caching of parsed selectors. 281 | 282 | # 0.0.4 / 2011-10-10 283 | 284 | * Fixed `:hover`. 285 | 286 | # 0.0.3 / 2011-10-09 287 | 288 | * Fixed specificity :not recursion. 289 | 290 | # 0.0.2 / 2011-10-09 291 | 292 | * Fixed specificity calculation for `not()` and pseudos. [arian] 293 | 294 | # 0.0.1 / 2011-10-09 295 | 296 | * Initial release. 297 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contibutions 2 | 3 | Thanks for your interest. Before opening an issue or pull request please review these quick points: 4 | 5 | - If you'd like to report a bug please reproduce it in [Tonic](https://tonicdev.com/npm/juice) and include the link in the issue 6 | - Pull requests must have test coverage and pass on Travis 7 | - Feature changes should have accompanying documentation updates 8 | - API changes should be also reflected in `juice.d.ts` and `juice-tests.ts` files 9 | - JavaScript standards are defined in the .jscsrc file, please maintain those standards when making changes. The standards are a relaxed version of the Node.js standards. 10 | 11 | That's the main stuff, thanks! 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 Automattic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PROJECTS.md: -------------------------------------------------------------------------------- 1 | # Projects using juice 2 | 3 | * [node-email-templates][1] - Node.js module for rendering beautiful emails with [ejs][2] templates and email-friendly inline CSS using [juice][3]. 4 | * [swig-email-templates][4] - Uses [swig][5], which gives you [template inheritance][6], and can generate a [dummy context][7] from a template. 5 | * [nodejs-api-starter][8] - A project template for building web APIs with Node.js and GraphQL (see [`src/emails`][9]). 6 | * [notifme-template][10] - A Node.js library to easily handle all your notification (`emails` | `SMS` | `pushes` | `webpushes`) templates. (can be used in combination with [`notifme-sdk`][11]). 7 | * [mosaico][13] - The first opensource email template editor. Helps you build responsive and appealing email templates in few clicks. 8 | * [mjml][12] - A markup language designed for painless responsive email coding. The markup gets transpiled to HTML that works in any email client. 9 | * [maizzle][14] - HTML email development framework. 10 | * [grunt-email-workflow][15] - A Grunt workflow for designing and testing responsive HTML email templates with SCSS. 11 | 12 | [1]: https://github.com/niftylettuce/node-email-templates 13 | [2]: https://github.com/tj/ejs 14 | [3]: https://github.com/Automattic/juice 15 | [4]: https://github.com/andrewrk/swig-email-templates 16 | [5]: https://github.com/paularmstrong/swig 17 | [6]: https://docs.djangoproject.com/en/dev/topics/templates/#template-inheritance 18 | [7]: https://github.com/andrewrk/swig-dummy-context 19 | [8]: https://github.com/kriasoft/nodejs-api-starter 20 | [9]: https://github.com/kriasoft/nodejs-api-starter/tree/master/src/emails 21 | [10]: https://github.com/notifme/notifme-template 22 | [11]: https://github.com/notifme/notifme-sdk 23 | [12]: https://github.com/mjmlio/mjml 24 | [13]: https://github.com/voidlabs/mosaico 25 | [14]: https://github.com/maizzle/maizzle 26 | [15]: https://github.com/leemunroe/grunt-email-workflow 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Automattic/juice.svg?branch=master)](https://travis-ci.org/Automattic/juice) 2 | [![Dependency Status](https://david-dm.org/Automattic/juice.svg)](https://david-dm.org/Automattic/juice) 3 | 4 | # Juice ![](https://i.imgur.com/jN8Ht.gif) 5 | 6 | Given HTML, juice will inline your CSS properties into the `style` attribute. 7 | 8 | [Some projects using Juice](PROJECTS.md) 9 | 10 | ## How to use 11 | 12 | Juice has a number of functions based on whether you want to process a file, HTML string, or a cheerio document, and whether you want juice to automatically get remote stylesheets, scripts and image dataURIs to inline. 13 | 14 | To inline HTML without getting remote resources, using default options: 15 | 16 | ```js 17 | var juice = require('juice'); 18 | var result = juice("
"); 19 | ``` 20 | 21 | result will be: 22 | ```html 23 |
24 | ``` 25 | 26 | [Try out the web client version](https://automattic.github.io/juice/) 27 | 28 | ## What is this useful for ? 29 | 30 | - HTML emails. For a comprehensive list of supported selectors see [here](https://www.campaignmonitor.com/css/) 31 | - Embedding HTML in 3rd-party websites. 32 | 33 | ## Documentation 34 | 35 | Juice is exposed as a standard module, and from CLI with a smaller set of options. 36 | 37 | ### Options 38 | 39 | All juice methods take an options object that can contain any of these properties, though not every method uses all of these: 40 | 41 | * `applyAttributesTableElements` - whether to create attributes for styles in `juice.styleToAttribute` on elements set in `juice.tableElements`. Defaults to `true`. 42 | 43 | * `applyHeightAttributes` - whether to use any CSS pixel heights to create `height` attributes on elements set in `juice.heightElements`. Defaults to `true`. 44 | 45 | * `applyStyleTags` - whether to inline styles in `` Defaults to `true`. 46 | 47 | * `applyWidthAttributes` - whether to use any CSS pixel widths to create `width` attributes on elements set in `juice.widthElements`. Defaults to `true`. 48 | 49 | * `decodeStyleAttributes` - whether to decode the value of `style=` attributes. Defaults to `false`. 50 | 51 | * `extraCss` - extra css to apply to the file. Defaults to `""`. 52 | 53 | * `insertPreservedExtraCss` - whether to insert into the document any preserved `@media` or `@font-face` content from `extraCss` when using `preserveMediaQueries`, `preserveFontFaces` or `preserveKeyFrames`. When `true` order of preference to append the `` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `true`. 58 | 59 | * `preserveImportant` - preserves `!important` in values. Defaults to `false`. 60 | 61 | * `preserveMediaQueries` - preserves all media queries (and contained styles) within `` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `true`. 62 | 63 | * `preserveKeyFrames` - preserves all key frames within `` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `true`. 64 | 65 | * `preservePseudos` - preserves all rules containing pseudo selectors defined in `ignoredPseudos` within `` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `true`. 66 | 67 | * `removeStyleTags` - whether to remove the original `` tags after (possibly) inlining the css from them. Defaults to `true`. 68 | 69 | * `resolveCSSVariables` - whether to resolve CSS variables. Defaults to `true`. 70 | 71 | * `webResources` - An options object that will be passed to [web-resource-inliner](https://www.npmjs.com/package/web-resource-inliner) for juice functions that will get remote resources (`juiceResources` and `juiceFile`). Defaults to `{}`. 72 | 73 | * `xmlMode` - whether to output XML/XHTML with all tags closed. Note that the input *must* also be valid XML/XHTML or you will get undesirable results. Defaults to `false`. 74 | 75 | 76 | ### Methods 77 | 78 | #### juice(html [, options]) 79 | 80 | Returns string containing inlined HTML. Does not fetch remote resources. 81 | 82 | * `html` - html string, accepts complete documents as well as fragments 83 | * `options` - optional, see Options above 84 | 85 | #### juice.juiceResources(html, options, callback) 86 | 87 | Callback returns string containing inlined HTML. Fetches remote resources. 88 | 89 | * `html` - html string 90 | * `options` - see Options above 91 | * `callback(err, html)` 92 | - `err` - `Error` object or `null` 93 | - `html` - inlined HTML 94 | 95 | #### juice.juiceFile(filePath, options, callback) 96 | 97 | Callback returns string containing inlined HTML. Fetches remote resources. 98 | 99 | * `filePath` - path to the html file to be juiced 100 | * `options` - see Options above 101 | * `callback(err, html)` 102 | - `err` - `Error` object or `null` 103 | - `html` - inlined HTML 104 | 105 | #### juice.juiceDocument($ [, options]) 106 | 107 | This takes a cheerio instance and performs inlining in-place. Returns the 108 | same cheerio instance. Does not fetch remote resources. 109 | 110 | * `$` - a cheerio instance, be sure to use the same cheerio version that juice uses 111 | * `options` - optional, see Options above` 112 | 113 | #### juice.inlineContent(html, css [, options]) 114 | 115 | This takes html and css and returns new html with the provided css inlined. 116 | It does not look at `` tag by the web-resource-inliner juice will not inline the styles and will not remove the `` tags. 168 | 169 | This can be used to embed email client support hacks that rely on css selectors into your email templates. 170 | 171 | ### CLI Options 172 | 173 | To use Juice from CLI, run `juice [options] input.html output.html` 174 | 175 | For a listing of all available options, just type `juice -h`. 176 | 177 | > Note that if you want to just type `juice` from the command line, you should `npm install juice -g` so it is globally available. 178 | 179 | CLI Options: 180 | 181 | The CLI should have all the above [options](#options) with the names changed from camel case to hyphen-delimited, so for example `extraCss` becomes `extra-css` and `webResources.scripts` becomes `web-resources-scripts`. 182 | 183 | These are additional options not included in the standard `juice` options listed above: 184 | 185 | - `--css [filepath]` will load and inject CSS into `extraCss`. 186 | - `--options-file [filepath]` will load and inject options from a JSON file. Options from the CLI will be given priority over options in the file when there is a conflict. 187 | - `codeBlocks` is optionally supported in the options file if you include it. This will allow you to support different template languages in a build process. 188 | 189 | ### Running Juice in the Browser 190 | 191 | Attempting to Browserify `require('juice')` fails because portions of Juice and its dependencies interact with the file system using the standard `require('fs')`. However, you can `require('juice/client')` via Browserify which has support for `juiceDocument`, `inlineDocument`, and `inlineContent`, but not `juiceFile`, `juiceResources`, or `inlineExternal`. *Note that automated tests are not running in the browser yet.* 192 | 193 | ## License 194 | 195 | MIT Licensed, see License.md 196 | 197 | ### 3rd-party 198 | 199 | - Uses [cheerio](https://github.com/cheeriojs/cheerio) for the underlying DOM 200 | representation. 201 | - Uses [mensch](https://github.com/brettstimmerman/mensch) to parse out CSS and 202 | [Slick](https://github.com/subtleGradient/slick) to tokenize them. 203 | - Icon by [UnheardSounds](http://unheardsounds.deviantart.com/gallery/26536908#/d2ngozi) 204 | -------------------------------------------------------------------------------- /bin/juice: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var juice = require('..'); 4 | var cli = require('../lib/cli'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | var program = cli.getProgram(); 9 | 10 | if (program.args.length < 2) { 11 | program.help(); 12 | } 13 | 14 | var [inputFile, outputFile] = program.args; 15 | var options = cli.argsToOptions(program); 16 | var queue = []; 17 | 18 | if (options.optionsFile) { 19 | var optionsFromFile = require(path.resolve(process.cwd(),options.optionsFile)); 20 | options = Object.assign({}, optionsFromFile, options, { 21 | webResources: Object.assign( 22 | {}, 23 | optionsFromFile && optionsFromFile.webResources, 24 | options && options.webResources 25 | ) 26 | }); 27 | } 28 | 29 | if (options.cssFile) { 30 | queue.push(function() { 31 | fs.readFile(options.cssFile, function(err, css) { 32 | if (handleError(err)) { return; } 33 | options.extraCss = css.toString(); 34 | next(); 35 | }); 36 | }); 37 | } 38 | 39 | next(); 40 | 41 | function doJuice() { 42 | delete options.cssFile; 43 | delete options.optionsFile; 44 | 45 | juice.juiceFile(inputFile, options, function(err, html) { 46 | if (handleError(err)) { return; } 47 | fs.writeFile(outputFile, html, handleError); 48 | }); 49 | } 50 | 51 | function next() { 52 | if (queue.length) { 53 | return queue.pop()(); 54 | } 55 | doJuice(); 56 | } 57 | 58 | function handleError(err) { 59 | if (err) { 60 | console.error(err.stack); 61 | process.exit(1); 62 | } 63 | return !!err; 64 | } 65 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cheerio = require('./lib/cheerio'); 4 | var makeJuiceClient = require('./lib/inline'); 5 | 6 | /** 7 | * Note that makeJuiceClient will take a base object (in this case a function) and enhance it 8 | * with a lot of useful properties and functions. 9 | * 10 | * This client adopts cheerio as a DOM parser and adds an "inlineContent" function that let 11 | * users to specify the CSS to be inlined instead of extracting it from the html. 12 | * 13 | * The weird "makeJuiceClient" behaviour is there in order to keep backward API compatibility. 14 | */ 15 | var juiceClient = makeJuiceClient(function(html,options) { 16 | return cheerio(html, { xmlMode: options && options.xmlMode}, juiceDocument, [options]); 17 | }); 18 | 19 | var juiceDocument = function(html, options) { 20 | return juiceClient.juiceDocument(html, options); 21 | } 22 | 23 | juiceClient.inlineContent = function(html, css, options) { 24 | return cheerio(html, { xmlMode: options && options.xmlMode}, juiceClient.inlineDocument, [css, options]); 25 | }; 26 | 27 | juiceClient.codeBlocks = cheerio.codeBlocks; 28 | 29 | module.exports = juiceClient; 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var utils = require('./lib/utils'); 8 | var packageJson = require('./package.json'); 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var inline = require('web-resource-inliner'); 12 | var juiceClient = require('./client'); 13 | var cheerio = require('./lib/cheerio'); 14 | var juice = juiceClient; 15 | 16 | module.exports = juice; 17 | 18 | juice.version = packageJson.version; 19 | 20 | juice.Selector = utils.Selector; 21 | juice.Property = utils.Property; 22 | juice.utils = utils; 23 | 24 | juice.juiceFile = juiceFile; 25 | juice.juiceResources = juiceResources; 26 | juice.inlineExternal = inlineExternal; 27 | 28 | function juiceFile(filePath, options, callback) { 29 | // set default options 30 | fs.readFile(filePath, 'utf8', function(err, content) { 31 | if (err) { 32 | return callback(err); 33 | } 34 | options = utils.getDefaultOptions(options); // so we can mutate options without guilt 35 | // Optional support for codeBlocks within optionsFile 36 | if (options.codeBlocks) { 37 | Object.keys(options.codeBlocks).forEach(function(key) { 38 | juice.codeBlocks[key] = options.codeBlocks[key]; 39 | }); 40 | } 41 | if (!options.webResources.relativeTo) { 42 | var rel = path.dirname(path.relative(process.cwd(),filePath)); 43 | options.webResources.relativeTo = rel; 44 | } 45 | juiceResources(content, options, callback); 46 | }); 47 | } 48 | 49 | function inlineExternal(html, inlineOptions, callback) { 50 | var options = Object.assign({fileContent: html}, inlineOptions); 51 | inline.html(options, callback); 52 | } 53 | 54 | function juiceResources(html, options, callback) { 55 | options = utils.getDefaultOptions(options); 56 | 57 | var onInline = function(err, html) { 58 | if (err) { 59 | return callback(err); 60 | } 61 | 62 | return callback(null, 63 | cheerio(html, { xmlMode: options && options.xmlMode}, juiceClient.juiceDocument, [options]) 64 | ); 65 | }; 66 | 67 | options.webResources.relativeTo = options.webResources.relativeTo || options.url; // legacy support 68 | inlineExternal(html, options.webResources, onInline); 69 | } 70 | -------------------------------------------------------------------------------- /juice.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Juice 3.0.0 2 | // Project: https://github.com/Automattic/juice 3 | // Definitions by: Kamil Nikel 4 | 5 | /* =================== USAGE =================== 6 | import juice = require('juice'); 7 | =============================================== */ 8 | 9 | export = juice; 10 | 11 | declare function juice(html: string, options?: juice.Options): string; 12 | 13 | declare namespace juice { 14 | 15 | export function juiceResources(html: string, options: Options, callback: Callback): string 16 | 17 | export function juiceFile(filePath: string, options: Options, callback: Callback): string 18 | 19 | export function juiceDocument($: any, options?: Options): any 20 | 21 | export function inlineContent(html: string, css: string, options?: Options): string 22 | 23 | export function inlineDocument($: any, css: string, options?: Options): any 24 | 25 | export let codeBlocks: { [index: string]: { start: string, end: string } }; 26 | export let excludedProperties: string[]; 27 | export let heightElements: HTMLElement[]; 28 | export let ignoredPseudos: string[]; 29 | export let nonVisualElements: HTMLElement[]; 30 | export let styleToAttribute: { [index: string]: string }; 31 | export let tableElements: HTMLElement[]; 32 | export let widthElements: HTMLElement[]; 33 | 34 | export interface Callback { (err: Error, html: string): any; } 35 | 36 | interface Options { 37 | extraCss?: string; 38 | applyStyleTags?: boolean; 39 | removeStyleTags?: boolean; 40 | preserveMediaQueries?: boolean; 41 | preserveFontFaces?: boolean; 42 | preserveKeyFrames?: boolean; 43 | preservePseudos?: boolean; 44 | insertPreservedExtraCss?: boolean; 45 | applyWidthAttributes?: boolean; 46 | applyHeightAttributes?: boolean; 47 | applyAttributesTableElements?: boolean; 48 | webResources?: WebResourcesOptions; 49 | inlinePseudoElements?: boolean; 50 | xmlMode?: boolean; 51 | preserveImportant?: boolean; 52 | resolveCSSVariables?: boolean; 53 | decodeStyleAttributes?: boolean; 54 | } 55 | 56 | interface WebResourcesOptions { 57 | fileContent?: string; 58 | inlineAttribute?: string; 59 | images?: boolean | number; 60 | svgs?: boolean | number; 61 | scripts?: boolean | number; 62 | links?: boolean | number; 63 | relativeTo?: string; 64 | rebaseRelativeTo?: string; 65 | strict?: boolean; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/cheerio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var cheerio = require('cheerio'); 7 | var utils = require('./utils'); 8 | 9 | var cheerioLoad = function(html, options, encodeEntities) { 10 | const { xmlMode, ...rest } = options; 11 | options = Object.assign({ xml: { decodeEntities: false, xmlMode } }, rest); 12 | html = encodeEntities(html); 13 | return cheerio.load(html, options); 14 | }; 15 | 16 | var createEntityConverters = function () { 17 | var codeBlockLookup = []; 18 | 19 | var encodeCodeBlocks = function(html) { 20 | var blocks = module.exports.codeBlocks; 21 | Object.keys(blocks).forEach(function(key) { 22 | var re = new RegExp(blocks[key].start + '([\\S\\s]*?)' + blocks[key].end, 'g'); 23 | html = html.replace(re, function(match, subMatch) { 24 | codeBlockLookup.push(match); 25 | return 'JUICE_CODE_BLOCK_' + (codeBlockLookup.length - 1) + '_'; 26 | }); 27 | }); 28 | return html; 29 | }; 30 | 31 | var decodeCodeBlocks = function(html) { 32 | for(var index = 0; index < codeBlockLookup.length; index++) { 33 | var re = new RegExp('JUICE_CODE_BLOCK_' + index + '_(="")?', 'gi'); 34 | html = html.replace(re, function() { 35 | return codeBlockLookup[index]; 36 | }); 37 | } 38 | return html; 39 | }; 40 | 41 | return { 42 | encodeEntities: encodeCodeBlocks, 43 | decodeEntities: decodeCodeBlocks, 44 | }; 45 | }; 46 | 47 | /** 48 | * Parses the input, calls the callback on the parsed DOM, and generates the output 49 | * 50 | * @param {String} html input html to be processed 51 | * @param {Object} options for the parser 52 | * @param {Function} callback to be invoked on the DOM 53 | * @param {Array} callbackExtraArguments to be passed to the callback 54 | * @return {String} resulting html 55 | */ 56 | module.exports = function(html, options, callback, callbackExtraArguments) { 57 | var entityConverters = createEntityConverters(); 58 | 59 | var $ = cheerioLoad(html, options, entityConverters.encodeEntities); 60 | var args = [ $ ]; 61 | args.push.apply(args, callbackExtraArguments); 62 | var doc = callback.apply(undefined, args) || $; 63 | 64 | if (options && options.xmlMode) { 65 | return entityConverters.decodeEntities(doc.xml()); 66 | } 67 | return entityConverters.decodeEntities(doc.html()); 68 | }; 69 | 70 | module.exports.codeBlocks = { 71 | EJS: { start: '<%', end: '%>' }, 72 | HBS: { start: '{{', end: '}}' } 73 | }; 74 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { program } = require('commander'); 4 | var pkg = require('../package'); 5 | 6 | var cli = {}; 7 | 8 | module.exports = cli; 9 | 10 | cli.getProgram = function() { 11 | program 12 | .name(pkg.name) 13 | .description(pkg.description) 14 | .version(pkg.version) 15 | .usage('[options] input.html output.html'); 16 | 17 | Object.keys(cli.options).forEach(function(key) { 18 | program.option('--' + key + ' [value]', cli.options[key].def); 19 | }); 20 | 21 | program.parse(process.argv); 22 | 23 | return program; 24 | }; 25 | 26 | cli.options = { 27 | 'css': { 28 | pMap: 'css', 29 | map: 'cssFile', 30 | def: 'Add an extra CSS file by name' }, 31 | 'options-file': { 32 | pMap: 'optionsFile', 33 | def: 'Load options from a JSON file' }, 34 | 'extra-css': { 35 | pMap: 'extraCss', 36 | def: 'Add extra CSS' }, 37 | 'insert-preserved-extra-css': { 38 | pMap: 'insertPreservedExtraCss', 39 | def: 'insert preserved @font-face and @media into document?', 40 | coercion: JSON.parse }, 41 | 'apply-style-tags': { 42 | pMap: 'applyStyleTags', 43 | def: 'inline from style tags?', 44 | coercion: JSON.parse }, 45 | 'remove-style-tags': { 46 | pMap: 'removeStyleTags', 47 | def: 'remove style tags?', 48 | coercion: JSON.parse }, 49 | 'preserve-important': { 50 | pMap: 'preserveImportant', 51 | def: 'preserve important?', 52 | coercion: JSON.parse }, 53 | 'preserve-media-queries': { 54 | pMap: 'preserveMediaQueries', 55 | def: 'preserve media queries?', 56 | coercion: JSON.parse }, 57 | 'preserve-font-faces': { 58 | pMap: 'preserveFontFaces', 59 | def: 'preserve font faces?', 60 | coercion: JSON.parse }, 61 | 'preserve-key-frames': { 62 | pMap: 'preserveKeyFrames', 63 | def: 'preserve key frames?', 64 | coercion: JSON.parse }, 65 | 'preserve-pseudos': { 66 | pMap: 'preservePseudos', 67 | def: 'preserve pseudo selectors?', 68 | coercion: JSON.parse }, 69 | 'apply-width-attributes': { 70 | pMap: 'applyWidthAttributes', 71 | def: 'apply width attributes to relevent elements?', 72 | coercion: JSON.parse }, 73 | 'apply-height-attributes': { 74 | pMap: 'applyHeightAttributes', 75 | def: 'apply height attributes to relevent elements?', 76 | coercion: JSON.parse }, 77 | 'apply-attributes-table-elements': { 78 | pMap: 'applyAttributesTableElements', 79 | def: 'apply attributes with and equivalent CSS value to table elements?', 80 | coercion: JSON.parse }, 81 | 'xml-mode': { 82 | pMap: 'xmlMode', 83 | def: 'generate output with tags closed? input must be valid XML', 84 | coercion: JSON.parse }, 85 | 'resolve-css-variables': { 86 | pMap: 'resolveCSSVariables', 87 | def: 'resolve CSS variables', 88 | coercion: JSON.parse }, 89 | 'web-resources-inline-attribute': { 90 | pMap: 'webResourcesInlineAttribute', 91 | map: 'inlineAttribute', 92 | def: 'see docs for web-resource-inliner inlineAttribute', 93 | coercion: JSON.parse }, 94 | 'web-resources-images': { 95 | pMap: 'webResourcesImages', 96 | map: 'images', 97 | def: 'see docs for web-resource-inliner images', 98 | coercion: JSON.parse }, 99 | 'web-resources-links': { 100 | pMap: 'webResourcesLinks', 101 | map: 'links', 102 | def: 'see docs for web-resource-inliner links', 103 | coercion: JSON.parse }, 104 | 'web-resources-scripts': { 105 | pMap: 'webResourcesScripts', 106 | map: 'scripts', 107 | def: 'see docs for web-resource-inliner scripts', 108 | coercion: JSON.parse }, 109 | 'web-resources-relative-to': { 110 | pMap: 'webResourcesRelativeTo', 111 | map: 'relativeTo', 112 | def: 'see docs for web-resource-inliner relativeTo' }, 113 | 'web-resources-rebase-relative-to': { 114 | pMap: 'webResourcesRebaseRelativeTo', 115 | map: 'rebaseRelativeTo', 116 | def: 'see docs for web-resource-inliner rebaseRelativeTo' }, 117 | 'web-resources-strict': { 118 | pMap: 'webResourcesStrict', 119 | map: 'strict', 120 | def: 'see docs for web-resource-inliner strict', 121 | coercion: JSON.parse }, 122 | 'decode-style-attributes': { 123 | pMap: 'decodeStyleAttributes', 124 | def: 'decode the value of `style=` attributes?', 125 | coercion: JSON.parse } 126 | }; 127 | 128 | cli.argsToOptions = function(program) { 129 | var result = { webResources: {} }; 130 | Object.keys(cli.options).forEach(function(key) { 131 | var option = cli.options[key]; 132 | var value = program.getOptionValue(option.pMap); 133 | if (value !== undefined) { 134 | if (option.coercion) { 135 | value = option.coercion(value); 136 | } 137 | 138 | if (option.pMap.match(/webResources/)) { 139 | result.webResources[option.map] = value; 140 | } else { 141 | result[option.map || option.pMap] = value; 142 | } 143 | } 144 | }); 145 | 146 | return result; 147 | }; 148 | -------------------------------------------------------------------------------- /lib/inline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { decode } = require('entities'); 4 | 5 | var utils = require('./utils'); 6 | var numbers = require('./numbers'); 7 | var variables = require('./variables'); 8 | 9 | module.exports = function makeJuiceClient(juiceClient) { 10 | 11 | juiceClient.ignoredPseudos = ['hover', 'active', 'focus', 'visited', 'link']; 12 | juiceClient.widthElements = ['TABLE', 'TD', 'TH', 'IMG']; 13 | juiceClient.heightElements = ['TABLE', 'TD', 'TH', 'IMG']; 14 | juiceClient.tableElements = ['TABLE', 'TH', 'TR', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'THEAD', 'TBODY', 'TFOOT']; 15 | juiceClient.nonVisualElements = [ 'HEAD', 'TITLE', 'BASE', 'LINK', 'STYLE', 'META', 'SCRIPT', 'NOSCRIPT' ]; 16 | juiceClient.styleToAttribute = { 17 | 'background-color': 'bgcolor', 18 | 'background-image': 'background', 19 | 'text-align': 'align', 20 | 'vertical-align': 'valign' 21 | }; 22 | juiceClient.excludedProperties = []; 23 | 24 | juiceClient.juiceDocument = juiceDocument; 25 | juiceClient.inlineDocument = inlineDocument; 26 | 27 | function inlineDocument($, css, options) { 28 | 29 | options = options || {}; 30 | var rules = utils.parseCSS(css); 31 | var editedElements = []; 32 | var styleAttributeName = 'style'; 33 | var counters = {}; 34 | 35 | if (options.styleAttributeName) { 36 | styleAttributeName = options.styleAttributeName; 37 | } 38 | 39 | rules.forEach(handleRule); 40 | editedElements.forEach(setStyleAttrs); 41 | 42 | if (options.inlinePseudoElements) { 43 | editedElements.forEach(inlinePseudoElements); 44 | } 45 | 46 | if (options.applyWidthAttributes) { 47 | editedElements.forEach(function(el) { 48 | setDimensionAttrs(el, 'width'); 49 | }); 50 | } 51 | 52 | if (options.applyHeightAttributes) { 53 | editedElements.forEach(function(el) { 54 | setDimensionAttrs(el, 'height'); 55 | }); 56 | } 57 | 58 | if (options.applyAttributesTableElements) { 59 | editedElements.forEach(setAttributesOnTableElements); 60 | } 61 | 62 | if (options.insertPreservedExtraCss && options.extraCss) { 63 | var preservedText = utils.getPreservedText(options.extraCss, { 64 | mediaQueries: options.preserveMediaQueries, 65 | fontFaces: options.preserveFontFaces, 66 | keyFrames: options.preserveKeyFrames 67 | }); 68 | if (preservedText) { 69 | var $appendTo = null; 70 | if (options.insertPreservedExtraCss !== true) { 71 | $appendTo = $(options.insertPreservedExtraCss); 72 | } else { 73 | $appendTo = $('head'); 74 | if (!$appendTo.length) { $appendTo = $('body'); } 75 | if (!$appendTo.length) { $appendTo = $.root(); } 76 | } 77 | 78 | $appendTo.first().append(''); 79 | } 80 | } 81 | 82 | function handleRule(rule) { 83 | var sel = rule[0]; 84 | var style = rule[1]; 85 | var selector = new utils.Selector(sel); 86 | var parsedSelector = selector.parsed(); 87 | 88 | if (!parsedSelector) { 89 | return; 90 | } 91 | 92 | var pseudoElementType = getPseudoElementType(parsedSelector); 93 | 94 | // skip rule if the selector has any pseudos which are ignored 95 | for (var i = 0; i < parsedSelector.length; ++i) { 96 | var subSel = parsedSelector[i]; 97 | if (subSel.pseudos) { 98 | for (var j = 0; j < subSel.pseudos.length; ++j) { 99 | var subSelPseudo = subSel.pseudos[j]; 100 | if (juiceClient.ignoredPseudos.indexOf(subSelPseudo.name) >= 0) { 101 | return; 102 | } 103 | } 104 | } 105 | } 106 | 107 | if (pseudoElementType) { 108 | var last = parsedSelector[parsedSelector.length - 1]; 109 | var pseudos = last.pseudos; 110 | last.pseudos = filterElementPseudos(last.pseudos); 111 | sel = parsedSelector.toString(); 112 | last.pseudos = pseudos; 113 | } 114 | 115 | var els; 116 | try { 117 | els = $(sel); 118 | } catch (err) { 119 | // skip invalid selector 120 | return; 121 | } 122 | 123 | els.each(function() { 124 | var el = this; 125 | 126 | if (el.name && juiceClient.nonVisualElements.indexOf(el.name.toUpperCase()) >= 0) { 127 | return; 128 | } 129 | 130 | if (!el.counterProps) { 131 | el.counterProps = el.parent && el.parent.counterProps 132 | ? Object.create(el.parent.counterProps) 133 | : {}; 134 | } 135 | 136 | if (pseudoElementType) { 137 | var pseudoElPropName = 'pseudo' + pseudoElementType; 138 | var pseudoEl = el[pseudoElPropName]; 139 | if (!pseudoEl) { 140 | pseudoEl = el[pseudoElPropName] = $('').get(0); 141 | pseudoEl.pseudoElementType = pseudoElementType; 142 | pseudoEl.pseudoElementParent = el; 143 | pseudoEl.counterProps = el.counterProps; 144 | el[pseudoElPropName] = pseudoEl; 145 | } 146 | el = pseudoEl; 147 | } 148 | 149 | if (!el.styleProps) { 150 | el.styleProps = {}; 151 | 152 | // if the element has inline styles, fake selector with topmost specificity 153 | if ($(el).attr(styleAttributeName)) { 154 | var styleAttributeValue = $(el).attr(styleAttributeName); 155 | var cssStyleAttributeValue = options.decodeStyleAttributes 156 | ? decode(styleAttributeValue) 157 | : styleAttributeValue; 158 | var cssText = '* { ' + cssStyleAttributeValue + ' } '; 159 | addProps(utils.parseCSS(cssText)[0][1], new utils.Selector(' 8 | 9 | 10 |

Cool!

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/cases/juice-content/counter-reset.json: -------------------------------------------------------------------------------- 1 | { 2 | "removeStyleTags": true, 3 | "inlinePseudoElements": true 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/juice-content/counter-reset.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Cool!

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/cases/juice-content/embed.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: blue; 3 | } -------------------------------------------------------------------------------- /test/cases/juice-content/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hi

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/cases/juice-content/embed.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./test/cases/juice-content", 3 | "removeStyleTags": true 4 | } -------------------------------------------------------------------------------- /test/cases/juice-content/embed.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |

hi

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/cases/juice-content/empty-style-tag.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/empty-style-tag.css -------------------------------------------------------------------------------- /test/cases/juice-content/empty-style-tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/empty-style-tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "removeStyleTags": true 3 | } -------------------------------------------------------------------------------- /test/cases/juice-content/empty-style-tag.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/file-missing.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/file-missing.css -------------------------------------------------------------------------------- /test/cases/juice-content/file-missing.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/cases/juice-content/file-missing.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/cases/juice-content/file-missing.out: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-face-preserve.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/font-face-preserve.css -------------------------------------------------------------------------------- /test/cases/juice-content/font-face-preserve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |

hi

23 | 24 | 25 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-face-preserve.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "preserveFontFaces": true, 5 | "preserveMediaQueries": false 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-face-preserve.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 |

hi

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/font-quotes.css -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |

hi

11 |

there

12 | 13 | 14 | -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true 4 | } -------------------------------------------------------------------------------- /test/cases/juice-content/font-quotes.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hi

7 |

there

8 | 9 | 10 | -------------------------------------------------------------------------------- /test/cases/juice-content/height-attr.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/height-attr.css -------------------------------------------------------------------------------- /test/cases/juice-content/height-attr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 |

none

17 | 18 | 19 | 22 | 25 | 28 | 29 | 30 | 33 | 36 | 39 | 40 |
20 | header 1 21 | 23 | header 2 24 | 26 | header 3 27 |
31 | high 32 | 34 | high 35 | 37 | high 38 |
41 | high 42 | high 43 | high 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/cases/juice-content/height-attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "applyHeightAttributes": true 5 | } -------------------------------------------------------------------------------- /test/cases/juice-content/height-attr.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

none

7 | 8 | 9 | 12 | 15 | 18 | 19 | 20 | 23 | 26 | 29 | 30 |
10 | header 1 11 | 13 | header 2 14 | 16 | header 3 17 |
21 | high 22 | 24 | high 25 | 27 | high 28 |
31 | high 32 | high 33 | high 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/cases/juice-content/important.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/important.css -------------------------------------------------------------------------------- /test/cases/juice-content/important.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 |

hi

18 | there 19 |

again

20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /test/cases/juice-content/important.json: -------------------------------------------------------------------------------- 1 | { 2 | "preserveImportant": true 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/juice-content/important.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hi

7 | there 8 |

again

9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /test/cases/juice-content/insert-preserve-fragment.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/insert-preserve-fragment.css -------------------------------------------------------------------------------- /test/cases/juice-content/insert-preserve-fragment.html: -------------------------------------------------------------------------------- 1 |
here
2 |
not here
3 |

4 | -------------------------------------------------------------------------------- /test/cases/juice-content/insert-preserve-fragment.json: -------------------------------------------------------------------------------- 1 | { 2 | "preserveFontFaces": true, 3 | "preserveMediaQueries": true, 4 | "insertPreservedExtraCss": "div", 5 | "extraCss": "@font-face { font-family: 'Extra'; }" 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/insert-preserve-fragment.out: -------------------------------------------------------------------------------- 1 |
here
5 |
not here
6 |

7 | -------------------------------------------------------------------------------- /test/cases/juice-content/keyframes-preserve.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/keyframes-preserve.css -------------------------------------------------------------------------------- /test/cases/juice-content/keyframes-preserve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 |

hi

21 | 22 | 23 | -------------------------------------------------------------------------------- /test/cases/juice-content/keyframes-preserve.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "preserveKeyFrames": true 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/juice-content/keyframes-preserve.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 |

hi

18 | 19 | 20 | -------------------------------------------------------------------------------- /test/cases/juice-content/lt-in-comments-issue216.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/lt-in-comments-issue216.css -------------------------------------------------------------------------------- /test/cases/juice-content/lt-in-comments-issue216.html: -------------------------------------------------------------------------------- 1 | 8 |
Hello!
-------------------------------------------------------------------------------- /test/cases/juice-content/lt-in-comments-issue216.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/juice-content/lt-in-comments-issue216.out: -------------------------------------------------------------------------------- 1 |
Hello!
-------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/media-preserve.css -------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |

hi

23 | 24 | 25 | -------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "preserveMediaQueries": true, 5 | "insertPreservedExtraCss": true, 6 | "preserveFontFaces": false, 7 | "extraCss": "@media all { body { color: blue; } }" 8 | } 9 | -------------------------------------------------------------------------------- /test/cases/juice-content/media-preserve.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 22 | 23 |

hi

24 | 25 | 26 | -------------------------------------------------------------------------------- /test/cases/juice-content/ms-filter-syntax-issue74.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/ms-filter-syntax-issue74.css -------------------------------------------------------------------------------- /test/cases/juice-content/ms-filter-syntax-issue74.html: -------------------------------------------------------------------------------- 1 | 6 |
Hello!
-------------------------------------------------------------------------------- /test/cases/juice-content/ms-filter-syntax-issue74.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/juice-content/ms-filter-syntax-issue74.out: -------------------------------------------------------------------------------- 1 |
Hello!
-------------------------------------------------------------------------------- /test/cases/juice-content/no-css.css: -------------------------------------------------------------------------------- 1 | /* No CSS silly rabbit :) */ 2 | -------------------------------------------------------------------------------- /test/cases/juice-content/no-css.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/cases/juice-content/no-css.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/cases/juice-content/no-css.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/non-visual.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/non-visual.css -------------------------------------------------------------------------------- /test/cases/juice-content/non-visual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/cases/juice-content/non-visual.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/juice-content/non-visual.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/cases/juice-content/prevent-css-gradient-in-attribute-issue319.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/prevent-css-gradient-in-attribute-issue319.css -------------------------------------------------------------------------------- /test/cases/juice-content/prevent-css-gradient-in-attribute-issue319.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Lorem
IpsumDolor
28 | 29 | -------------------------------------------------------------------------------- /test/cases/juice-content/prevent-css-gradient-in-attribute-issue319.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/juice-content/prevent-css-gradient-in-attribute-issue319.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Lorem
IpsumDolor
15 | 16 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements-and-width-attr.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/pseudo-elements-and-width-attr.css -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements-and-width-attr.html: -------------------------------------------------------------------------------- 1 | 2 | 38 | Test 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements-and-width-attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "inlinePseudoElements": true, 5 | "applyWidthAttributes": true 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements-and-width-attr.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | ®+Testa 4 | b 5 | b 6 | 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/pseudo-elements.css -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements.html: -------------------------------------------------------------------------------- 1 | 2 | 77 | Test 78 | 79 | 80 | 81 | 82 | test 83 | 84 | Test 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "inlinePseudoElements": true, 5 | "resolveCSSVariables": false 6 | } 7 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-elements.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | ®+Testa 4 | b 5 | b 6 | 7 | 8 | test 9 | Element / 21. -2 10 | Element / 22. -4Test | Length: 100 [xxii:V] 11 | -3 12 | -2 13 | 14 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-selector-preserve.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/pseudo-selector-preserve.css -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-selector-preserve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | hover me 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-selector-preserve.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "preservePseudos": true 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/juice-content/pseudo-selector-preserve.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | hover me 23 | 24 | -------------------------------------------------------------------------------- /test/cases/juice-content/relative-url.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/relative-url.css -------------------------------------------------------------------------------- /test/cases/juice-content/relative-url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/juice-content/relative-url.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "http://yui.yahooapis.com/" 3 | } -------------------------------------------------------------------------------- /test/cases/juice-content/relative-url.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/juice-content/table-attr.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/table-attr.css -------------------------------------------------------------------------------- /test/cases/juice-content/table-attr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 26 | 27 | 28 |

none

29 | 30 | 31 | 34 | 37 | 40 | 41 |
32 | wide 33 | 35 | wide 36 | 38 | wide 39 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /test/cases/juice-content/table-attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "applyAttributesTableElements": true 5 | } 6 | -------------------------------------------------------------------------------- /test/cases/juice-content/table-attr.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

none

7 | 8 | 9 | 12 | 15 | 18 | 19 |
10 | wide 11 | 13 | wide 14 | 16 | wide 17 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/width-attr.css -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 |

none

17 | 18 | 19 | 22 | 25 | 28 | 29 | 30 | 33 | 36 | 39 | 40 |
20 | header 1 21 | 23 | header 2 24 | 26 | header 3 27 |
31 | wide 32 | 34 | wide 35 | 37 | wide 38 |
41 | wide 42 | wide 43 | wide 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "removeStyleTags": true, 4 | "applyWidthAttributes": true 5 | } -------------------------------------------------------------------------------- /test/cases/juice-content/width-attr.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

none

7 | 8 | 9 | 12 | 15 | 18 | 19 | 20 | 23 | 26 | 29 | 30 |
10 | header 1 11 | 13 | header 2 14 | 16 | header 3 17 |
21 | wide 22 | 24 | wide 25 | 27 | wide 28 |
31 | wide 32 | wide 33 | wide 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/cases/juice-content/xml.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/juice-content/xml.css -------------------------------------------------------------------------------- /test/cases/juice-content/xml.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hello
7 | 8 | test 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/cases/juice-content/xml.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "./", 3 | "xmlMode": true 4 | } -------------------------------------------------------------------------------- /test/cases/juice-content/xml.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hello
7 | 8 | test 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/cases/linear-gradient-space-issue-226.css: -------------------------------------------------------------------------------- 1 | div{background-image: linear-gradient(to right, #0099E5, #7C37FF);} -------------------------------------------------------------------------------- /test/cases/linear-gradient-space-issue-226.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/cases/linear-gradient-space-issue-226.out: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/cases/media.css: -------------------------------------------------------------------------------- 1 | @media only screen and (-webkit-min-device-pixel-ratio: 2) { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/media.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

hi

4 | 5 | 6 | -------------------------------------------------------------------------------- /test/cases/media.out: -------------------------------------------------------------------------------- 1 | 2 | 3 |

hi

4 | 5 | 6 | -------------------------------------------------------------------------------- /test/cases/multiple-properties-occourences.css: -------------------------------------------------------------------------------- 1 | .a { 2 | color: #800000; 3 | color: red; 4 | color: rgba(255,0,0,.5); 5 | color: -prefix-unknownvalue; 6 | } 7 | .b { 8 | color: blue; 9 | } 10 | .b { 11 | color: rgba(0,0,255,.5); 12 | } 13 | .c { 14 | color: red !important; 15 | color: rgba(255,0,0,.5) !important; 16 | color: blue; 17 | } 18 | .d { 19 | padding: 5px; 20 | padding-top: 20px; 21 | padding: 10px; 22 | padding-top: unexpected value; 23 | padding: unknown; 24 | padding-top: again something unexpected; 25 | } 26 | -------------------------------------------------------------------------------- /test/cases/multiple-properties-occourences.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /test/cases/multiple-properties-occourences.out: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /test/cases/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.0 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | } 35 | 36 | /** 37 | * Prevent modern browsers from displaying `audio` without controls. 38 | * Remove excess height in iOS 5 devices. 39 | */ 40 | 41 | audio:not([controls]) { 42 | display: none; 43 | height: 0; 44 | } 45 | 46 | /** 47 | * Address styling not present in IE 8/9. 48 | */ 49 | 50 | [hidden] { 51 | display: none; 52 | } 53 | 54 | /* ========================================================================== 55 | Base 56 | ========================================================================== */ 57 | 58 | /** 59 | * 1. Set default font family to sans-serif. 60 | * 2. Prevent iOS text size adjust after orientation change, without disabling 61 | * user zoom. 62 | */ 63 | 64 | html { 65 | font-family: sans-serif; /* 1 */ 66 | -webkit-text-size-adjust: 100%; /* 2 */ 67 | -ms-text-size-adjust: 100%; /* 2 */ 68 | } 69 | 70 | /** 71 | * Remove default margin. 72 | */ 73 | 74 | body { 75 | margin: 0; 76 | } 77 | 78 | /* ========================================================================== 79 | Links 80 | ========================================================================== */ 81 | 82 | /** 83 | * Address `outline` inconsistency between Chrome and other browsers. 84 | */ 85 | 86 | a:focus { 87 | outline: thin dotted; 88 | } 89 | 90 | /** 91 | * Improve readability when focused and also mouse hovered in all browsers. 92 | */ 93 | 94 | a:active, 95 | a:hover { 96 | outline: 0; 97 | } 98 | 99 | /* ========================================================================== 100 | Typography 101 | ========================================================================== */ 102 | 103 | /** 104 | * Address variable `h1` font-size and margin within `section` and `article` 105 | * contexts in Firefox 4+, Safari 5, and Chrome. 106 | */ 107 | 108 | h1 { 109 | font-size: 2em; 110 | margin: 0.67em 0; 111 | } 112 | 113 | /** 114 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 115 | */ 116 | 117 | abbr[title] { 118 | border-bottom: 1px dotted; 119 | } 120 | 121 | /** 122 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: bold; 128 | } 129 | 130 | /** 131 | * Address styling not present in Safari 5 and Chrome. 132 | */ 133 | 134 | dfn { 135 | font-style: italic; 136 | } 137 | 138 | /** 139 | * Address differences between Firefox and other browsers. 140 | */ 141 | 142 | hr { 143 | -moz-box-sizing: content-box; 144 | box-sizing: content-box; 145 | height: 0; 146 | } 147 | 148 | /** 149 | * Address styling not present in IE 8/9. 150 | */ 151 | 152 | mark { 153 | background: #ff0; 154 | color: #000; 155 | } 156 | 157 | /** 158 | * Correct font family set oddly in Safari 5 and Chrome. 159 | */ 160 | 161 | code, 162 | kbd, 163 | pre, 164 | samp { 165 | font-family: monospace, serif; 166 | font-size: 1em; 167 | } 168 | 169 | /** 170 | * Improve readability of pre-formatted text in all browsers. 171 | */ 172 | 173 | pre { 174 | white-space: pre-wrap; 175 | } 176 | 177 | /** 178 | * Set consistent quote types. 179 | */ 180 | 181 | q { 182 | quotes: "\201C" "\201D" "\2018" "\2019"; 183 | } 184 | 185 | /** 186 | * Address inconsistent and variable font size in all browsers. 187 | */ 188 | 189 | small { 190 | font-size: 80%; 191 | } 192 | 193 | /** 194 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 195 | */ 196 | 197 | sub, 198 | sup { 199 | font-size: 75%; 200 | line-height: 0; 201 | position: relative; 202 | vertical-align: baseline; 203 | } 204 | 205 | sup { 206 | top: -0.5em; 207 | } 208 | 209 | sub { 210 | bottom: -0.25em; 211 | } 212 | 213 | /* ========================================================================== 214 | Embedded content 215 | ========================================================================== */ 216 | 217 | /** 218 | * Remove border when inside `a` element in IE 8/9. 219 | */ 220 | 221 | img { 222 | border: 0; 223 | } 224 | 225 | /** 226 | * Correct overflow displayed oddly in IE 9. 227 | */ 228 | 229 | svg:not(:root) { 230 | overflow: hidden; 231 | } 232 | 233 | /* ========================================================================== 234 | Figures 235 | ========================================================================== */ 236 | 237 | /** 238 | * Address margin not present in IE 8/9 and Safari 5. 239 | */ 240 | 241 | figure { 242 | margin: 0; 243 | } 244 | 245 | /* ========================================================================== 246 | Forms 247 | ========================================================================== */ 248 | 249 | /** 250 | * Define consistent border, margin, and padding. 251 | */ 252 | 253 | fieldset { 254 | border: 1px solid #c0c0c0; 255 | margin: 0 2px; 256 | padding: 0.35em 0.625em 0.75em; 257 | } 258 | 259 | /** 260 | * 1. Correct `color` not being inherited in IE 8/9. 261 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 262 | */ 263 | 264 | legend { 265 | border: 0; /* 1 */ 266 | padding: 0; /* 2 */ 267 | } 268 | 269 | /** 270 | * 1. Correct font family not being inherited in all browsers. 271 | * 2. Correct font size not being inherited in all browsers. 272 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 273 | */ 274 | 275 | button, 276 | input, 277 | select, 278 | textarea { 279 | font-family: inherit; /* 1 */ 280 | font-size: 100%; /* 2 */ 281 | margin: 0; /* 3 */ 282 | } 283 | 284 | /** 285 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 286 | * the UA stylesheet. 287 | */ 288 | 289 | button, 290 | input { 291 | line-height: normal; 292 | } 293 | 294 | /** 295 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 296 | * All other form control elements do not inherit `text-transform` values. 297 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 298 | * Correct `select` style inheritance in Firefox 4+ and Opera. 299 | */ 300 | 301 | button, 302 | select { 303 | text-transform: none; 304 | } 305 | 306 | /** 307 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 308 | * and `video` controls. 309 | * 2. Correct inability to style clickable `input` types in iOS. 310 | * 3. Improve usability and consistency of cursor style between image-type 311 | * `input` and others. 312 | */ 313 | 314 | button, 315 | html input[type="button"], /* 1 */ 316 | input[type="reset"], 317 | input[type="submit"] { 318 | -webkit-appearance: button; /* 2 */ 319 | cursor: pointer; /* 3 */ 320 | } 321 | 322 | /** 323 | * Re-set default cursor for disabled elements. 324 | */ 325 | 326 | button[disabled], 327 | html input[disabled] { 328 | cursor: default; 329 | } 330 | 331 | /** 332 | * 1. Address box sizing set to `content-box` in IE 8/9. 333 | * 2. Remove excess padding in IE 8/9. 334 | */ 335 | 336 | input[type="checkbox"], 337 | input[type="radio"] { 338 | box-sizing: border-box; /* 1 */ 339 | padding: 0; /* 2 */ 340 | } 341 | 342 | /** 343 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 344 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 345 | * (include `-moz` to future-proof). 346 | */ 347 | 348 | input[type="search"] { 349 | -webkit-appearance: textfield; /* 1 */ 350 | -moz-box-sizing: content-box; 351 | -webkit-box-sizing: content-box; /* 2 */ 352 | box-sizing: content-box; 353 | } 354 | 355 | /** 356 | * Remove inner padding and search cancel button in Safari 5 and Chrome 357 | * on OS X. 358 | */ 359 | 360 | input[type="search"]::-webkit-search-cancel-button, 361 | input[type="search"]::-webkit-search-decoration { 362 | -webkit-appearance: none; 363 | } 364 | 365 | /** 366 | * Remove inner padding and border in Firefox 4+. 367 | */ 368 | 369 | button::-moz-focus-inner, 370 | input::-moz-focus-inner { 371 | border: 0; 372 | padding: 0; 373 | } 374 | 375 | /** 376 | * 1. Remove default vertical scrollbar in IE 8/9. 377 | * 2. Improve readability and alignment in all browsers. 378 | */ 379 | 380 | textarea { 381 | overflow: auto; /* 1 */ 382 | vertical-align: top; /* 2 */ 383 | } 384 | 385 | /* ========================================================================== 386 | Tables 387 | ========================================================================== */ 388 | 389 | /** 390 | * Remove most spacing between table cells. 391 | */ 392 | 393 | table { 394 | border-collapse: collapse; 395 | border-spacing: 0; 396 | } 397 | -------------------------------------------------------------------------------- /test/cases/normalize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

normalize.css

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/normalize.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

normalize.css

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/not-xhtml.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/cases/not-xhtml.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/cases/not-xhtml.out: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/cases/php.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /test/cases/php.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/cases/php.out: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/cases/preserve-events.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/cases/preserve-events.html: -------------------------------------------------------------------------------- 1 | 2 | Google 3 |

Google

4 | 5 | -------------------------------------------------------------------------------- /test/cases/preserve-events.out: -------------------------------------------------------------------------------- 1 | 2 | Google 3 |

Google

4 | 5 | -------------------------------------------------------------------------------- /test/cases/propertyorder.css: -------------------------------------------------------------------------------- 1 | .b { 2 | padding: 20px; 3 | padding-top: 10px; 4 | color: red; 5 | } 6 | .a { 7 | padding-top: 0; 8 | padding: 5px; 9 | padding: 10px; 10 | } 11 | -------------------------------------------------------------------------------- /test/cases/propertyorder.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/cases/propertyorder.out: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/cases/pseudo-specificity.css: -------------------------------------------------------------------------------- 1 | span.a { 2 | color: red; 3 | background-color: red; 4 | } 5 | .a:first-child { 6 | color: blue; 7 | background-color: blue; 8 | } 9 | .a:not(div) { 10 | background-color: green; 11 | padding: 0px; 12 | } 13 | .a:first-child { 14 | padding: 10px; 15 | } 16 | span.b { 17 | padding: 20px; 18 | } 19 | -------------------------------------------------------------------------------- /test/cases/pseudo-specificity.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /test/cases/pseudo-specificity.out: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /test/cases/regression-media.css: -------------------------------------------------------------------------------- 1 | a, 2 | p { 3 | color: green; 4 | } 5 | 6 | @media only screen and (max-width: 600px) { 7 | a { color: red; } 8 | } -------------------------------------------------------------------------------- /test/cases/regression-media.html: -------------------------------------------------------------------------------- 1 | Test 2 |

Test

3 | -------------------------------------------------------------------------------- /test/cases/regression-media.out: -------------------------------------------------------------------------------- 1 | Test 2 |

Test

-------------------------------------------------------------------------------- /test/cases/regression-selector-newline.css: -------------------------------------------------------------------------------- 1 | a, 2 | p { 3 | color: red; 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/regression-selector-newline.html: -------------------------------------------------------------------------------- 1 | 2 | Test 3 |

Test

4 | 5 | -------------------------------------------------------------------------------- /test/cases/regression-selector-newline.out: -------------------------------------------------------------------------------- 1 | 2 | Test 3 |

Test

4 | 5 | -------------------------------------------------------------------------------- /test/cases/shorthand.css: -------------------------------------------------------------------------------- 1 | p { 2 | padding-bottom: 10px; 3 | } 4 | 5 | #p { 6 | padding: 0px; 7 | } 8 | -------------------------------------------------------------------------------- /test/cases/shorthand.html: -------------------------------------------------------------------------------- 1 | 2 |

woot

3 | 4 | -------------------------------------------------------------------------------- /test/cases/shorthand.out: -------------------------------------------------------------------------------- 1 | 2 |

woot

3 | 4 | -------------------------------------------------------------------------------- /test/cases/specificity.css: -------------------------------------------------------------------------------- 1 | #wrap #a { /* [2, 0, 0] - winner */ 2 | color: red; 3 | } 4 | #wrap a { /* [1, 0, 1] */ 5 | color: #000; 6 | } 7 | #a { /* [1, 0, 0] */ 8 | color: #00f; 9 | } 10 | div[a=b] .wrap div { /* [0, 2, 2] */ 11 | display: block; 12 | } 13 | #wrap .wrap { /* [1, 1, 0] - winner */ 14 | display: inline-block; 15 | } 16 | #wrap div.wrap #a { /* [2, 1, 1] */ 17 | background: #000; 18 | } 19 | #wrap[a=b] div.wrap #a { /* [2, 2, 1] - winner */ 20 | background: green; 21 | } 22 | #wrap[a=c] div.wrap #a { /* [2, 2, 1] - no match */ 23 | background: #00f; 24 | } 25 | 26 | #wrap .wrap #a { /* [2, 1, 0] */ 27 | background: #f00; 28 | } 29 | #wrap .wrap #a { /* [2, 1, 0] - winner */ 30 | border-color: green; 31 | } 32 | div #wrap-2 #a { /* [2, 0, 1] */ 33 | border-color: #008000; 34 | } 35 | -------------------------------------------------------------------------------- /test/cases/specificity.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /test/cases/specificity.out: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /test/cases/specificitygte10.css: -------------------------------------------------------------------------------- 1 | div.a.b.c.d.e.f.g.h.i.j { 2 | padding: 10px; 3 | } 4 | div div.a { 5 | padding-top: 0; 6 | } -------------------------------------------------------------------------------- /test/cases/specificitygte10.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | -------------------------------------------------------------------------------- /test/cases/specificitygte10.out: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | -------------------------------------------------------------------------------- /test/cases/style-preservation.css: -------------------------------------------------------------------------------- 1 | p { color: blue; background: blue; } 2 | -------------------------------------------------------------------------------- /test/cases/style-preservation.html: -------------------------------------------------------------------------------- 1 | 2 |

The color should stay intact

3 | 4 | -------------------------------------------------------------------------------- /test/cases/style-preservation.out: -------------------------------------------------------------------------------- 1 | 2 |

The color should stay intact

3 | 4 | -------------------------------------------------------------------------------- /test/cases/tag.css: -------------------------------------------------------------------------------- 1 | p { 2 | border: 1px solid #f00; 3 | margin: 1px; 4 | } 5 | -------------------------------------------------------------------------------- /test/cases/tag.html: -------------------------------------------------------------------------------- 1 | 2 |

Test

3 | 4 | -------------------------------------------------------------------------------- /test/cases/tag.out: -------------------------------------------------------------------------------- 1 | 2 |

Test

3 | 4 | -------------------------------------------------------------------------------- /test/cases/templates.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/juice/00a4cb810745798fc1ea5507f0becd7128b05317/test/cases/templates.css -------------------------------------------------------------------------------- /test/cases/templates.html: -------------------------------------------------------------------------------- 1 |

Dear {{ $person->user->firstname }},

2 | 3 | [{include file="email/html/footer.tpl"}] 4 | -------------------------------------------------------------------------------- /test/cases/templates.out: -------------------------------------------------------------------------------- 1 |

Dear {{ $person->user->firstname }},

2 | 3 | [{include file="email/html/footer.tpl"}] 4 | -------------------------------------------------------------------------------- /test/cases/yui-reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010, Yahoo! Inc. All rights reserved. 3 | * Code licensed under the BSD License: 4 | * http://developer.yahoo.com/yui/license.html 5 | * version: 3.3.0 6 | * build: 3167 7 | * */ 8 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;} 9 | -------------------------------------------------------------------------------- /test/cases/yui-reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

hello

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cases/yui-reset.out: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

hello

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var spawn = require('cross-spawn'); 6 | var cli = require('../lib/cli'); 7 | 8 | before(function() { 9 | if (fs.existsSync('tmp')) { return; } 10 | fs.mkdirSync('tmp'); 11 | }); 12 | 13 | it('cli parses options', function(done) { 14 | const parseArgs = (args) => cli.argsToOptions({ getOptionValue: (arg) => args[arg] }); 15 | assert.strictEqual(parseArgs({'css': 'file.css'}).cssFile, 'file.css'); 16 | assert.strictEqual(parseArgs({'optionsFile': 'options.json'}).optionsFile, 'options.json'); 17 | assert.strictEqual(parseArgs({'extraCss': 'body{color:red;}'}).extraCss, 'body{color:red;}'); 18 | assert.strictEqual(parseArgs({'insertPreservedExtraCss': 'true'}).insertPreservedExtraCss, true); 19 | assert.strictEqual(parseArgs({'applyStyleTags': 'true'}).applyStyleTags, true); 20 | assert.strictEqual(parseArgs({'removeStyleTags': 'true'}).removeStyleTags, true); 21 | assert.strictEqual(parseArgs({'preserveImportant': 'true'}).preserveImportant, true); 22 | assert.strictEqual(parseArgs({'preserveMediaQueries': 'true'}).preserveMediaQueries, true); 23 | assert.strictEqual(parseArgs({'preserveFontFaces': 'true'}).preserveFontFaces, true); 24 | assert.strictEqual(parseArgs({'preserveKeyFrames': 'true'}).preserveKeyFrames, true); 25 | assert.strictEqual(parseArgs({'applyWidthAttributes': 'true'}).applyWidthAttributes, true); 26 | assert.strictEqual(parseArgs({'applyHeightAttributes': 'true'}).applyHeightAttributes, true); 27 | assert.strictEqual(parseArgs({'applyAttributesTableElements': 'true'}).applyAttributesTableElements, true); 28 | assert.strictEqual(parseArgs({'xmlMode': 'true'}).xmlMode, true); 29 | assert.strictEqual(parseArgs({'resolveCSSVariables': 'true'}).resolveCSSVariables, true); 30 | assert.strictEqual(parseArgs({'decodeStyleAttributes': 'true'}).decodeStyleAttributes, true); 31 | assert.strictEqual(parseArgs({'webResourcesInlineAttribute': 'true'}).webResources.inlineAttribute, true); 32 | assert.strictEqual(parseArgs({'webResourcesImages': '12'}).webResources.images, 12); 33 | assert.strictEqual(parseArgs({'webResourcesLinks': 'true'}).webResources.links, true); 34 | assert.strictEqual(parseArgs({'webResourcesScripts': '24'}).webResources.scripts, 24); 35 | assert.strictEqual(parseArgs({'webResourcesRelativeTo': 'web'}).webResources.relativeTo, 'web'); 36 | assert.strictEqual(parseArgs({'webResourcesRebaseRelativeTo': 'root'}).webResources.rebaseRelativeTo, 'root'); 37 | assert.strictEqual(parseArgs({'webResourcesStrict': 'true'}).webResources.strict, true); 38 | done(); 39 | }); 40 | 41 | it('cli no css', function(done) { 42 | var inputPath = 'test/cases/juice-content/no-css.html'; 43 | var expectedPath = 'test/cases/juice-content/no-css.out'; 44 | var outputPath = 'tmp/no-css.out'; 45 | 46 | var juiceProcess = spawn('bin/juice', [inputPath, outputPath]); 47 | 48 | juiceProcess.on('error', done); 49 | 50 | juiceProcess.on('exit', function(code) { 51 | assert(code === 0, 'Expected exit code to be 0'); 52 | var output = fs.readFileSync(outputPath, 'utf8'); 53 | var expectedOutput = fs.readFileSync(expectedPath, 'utf8'); 54 | assert.equal(output, expectedOutput); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('cli css included', function(done) { 60 | var htmlPath = 'test/cases/integration.html'; 61 | var cssPath = 'test/cases/integration.css'; 62 | var expectedPath = 'test/cases/integration.out'; 63 | var outputPath = 'tmp/integration.out'; 64 | 65 | var juiceProcess = spawn('bin/juice', [htmlPath, '--css', cssPath, '--apply-width-attributes', 'false', outputPath]); 66 | 67 | juiceProcess.on('error', done); 68 | 69 | juiceProcess.on('exit', function(code) { 70 | assert(code === 0, 'Expected exit code to be 0'); 71 | var output = fs.readFileSync(outputPath, 'utf8'); 72 | var expectedOutput = fs.readFileSync(expectedPath, 'utf8'); 73 | assert.equal(output, expectedOutput); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('cli options included', function(done) { 79 | var htmlPath = 'test/cases/juice-content/font-face-preserve.html'; 80 | var optionsFilePath = 'test/cases/juice-content/font-face-preserve.json'; 81 | var expectedPath = 'test/cases/juice-content/font-face-preserve.out'; 82 | var outputPath = 'tmp/font-face-preserve.out'; 83 | 84 | var juiceProcess = spawn('bin/juice', [htmlPath, '--options-file', optionsFilePath, outputPath]); 85 | 86 | juiceProcess.on('error', done); 87 | 88 | juiceProcess.on('exit', function(code) { 89 | assert(code === 0, 'Expected exit code to be 0'); 90 | var output = fs.readFileSync(outputPath, 'utf8').replace(/\r/g, ''); 91 | var expectedOutput = fs.readFileSync(expectedPath, 'utf8'); 92 | assert.equal(output, expectedOutput); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('cli supports codeBlock', function(done) { 98 | var htmlPath = 'test/cases/cli/code-block-cli.html'; 99 | var optionsFilePath = 'test/cases/cli/code-block-cli.json'; 100 | var expectedPath = 'test/cases/cli/code-block-cli.out'; 101 | var outputPath = 'tmp/code-block-cli.out'; 102 | 103 | var juiceProcess = spawn('bin/juice', [htmlPath, '--options-file', optionsFilePath, outputPath]); 104 | 105 | juiceProcess.on('error', done); 106 | 107 | juiceProcess.on('exit', function(code) { 108 | assert(code === 0, 'Expected exit code to be 0'); 109 | var output = fs.readFileSync(outputPath, 'utf8').replace(/\r/g, ''); 110 | var expectedOutput = fs.readFileSync(expectedPath, 'utf8'); 111 | assert.equal(output, expectedOutput); 112 | done(); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/html/Test.css: -------------------------------------------------------------------------------- 1 | strong { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/html/doctype.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | -------------------------------------------------------------------------------- /test/html/doctype.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | -------------------------------------------------------------------------------- /test/html/no_css.in.html: -------------------------------------------------------------------------------- 1 |

hi

2 | -------------------------------------------------------------------------------- /test/html/no_css.out.html: -------------------------------------------------------------------------------- 1 |

hi

2 | -------------------------------------------------------------------------------- /test/html/remote_url.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/html/remote_url.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/html/spaces in path/Test.css: -------------------------------------------------------------------------------- 1 | strong { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/html/spaces_in_path.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello, you are my favorite person

7 | 8 | 9 | -------------------------------------------------------------------------------- /test/html/spaces_in_path.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

hello, you are my favorite person

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/html/two_styles.in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 16 | 17 | 18 | 19 |

hello, you are my favorite person

20 | 21 | 22 | -------------------------------------------------------------------------------- /test/html/two_styles.out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

hello, you are my favorite person

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/juice.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*! 4 | * Juice unit tests. 5 | */ 6 | 7 | /** 8 | * Test dependencies. 9 | */ 10 | 11 | var juice = require('../'); 12 | var Selector = juice.Selector; 13 | var Property = juice.Property; 14 | var utils = juice.utils; 15 | var assert = require('assert'); 16 | 17 | /** 18 | * Tests. 19 | */ 20 | 21 | it('extracting selectors', function() { 22 | var extract = utils.extract; 23 | 24 | assert.deepEqual(extract('#a'),['#a']); 25 | assert.deepEqual(extract('#a, .b'),['#a', '.b']); 26 | assert.deepEqual(extract('#a, .b,'),['#a', '.b']); 27 | assert.deepEqual(extract('.test.a, #a.b'),['.test.a', '#a.b']); 28 | assert.deepEqual(extract('a[type=text, a=b], .a, .b, #c #d'),['a[type=text, a=b]', '.a', '.b', '#c #d']); 29 | assert.deepEqual(extract('a:not(.a,.b,.c)'),['a:not(.a,.b,.c)']); 30 | assert.deepEqual(extract('a:not(.a,.b,.c), .b'),['a:not(.a,.b,.c)', '.b']); 31 | assert.deepEqual(extract('a:not(.a,.b,[type=text]), .b'),['a:not(.a,.b,[type=text])', '.b']); 32 | assert.deepEqual(extract('a:not(.a,.b,[type=text, a=b]), .b'),['a:not(.a,.b,[type=text, a=b])', '.b']); 33 | }); 34 | 35 | it('selector specificity comparison', function() { 36 | var compare = utils.compare; 37 | 38 | assert.deepEqual(compare([0, 1, 2, 3], [0, 2, 0, 0]),[0, 2, 0, 0]); 39 | assert.deepEqual(compare([0, 2, 0, 0], [0, 1, 2, 3]),[0, 2, 0, 0]); 40 | 41 | // Check that the second reference is returned upon draws 42 | var b = [0, 1, 1, 4]; 43 | assert.deepEqual(compare([0, 1, 1, 4], b),b); 44 | 45 | assert.deepEqual(compare([0, 0, 0, 4], [0, 0, 0, 10]),[0, 0, 0, 10]); 46 | assert.deepEqual(compare([0, 0, 0, 10], [0, 0, 0, 4]),[0, 0, 0, 10]); 47 | 48 | assert.deepEqual(compare([0, 4, 0, 0], [0, 0, 100, 4]),[0, 4, 0, 0]); 49 | assert.deepEqual(compare([0, 0, 100, 4], [0, 4, 0, 0]),[0, 4, 0, 0]); 50 | 51 | assert.deepEqual(compare([0, 1, 1, 5], [0, 1, 1, 15]),[0, 1, 1, 15]); 52 | assert.deepEqual(compare([0, 1, 1, 15], [0, 1, 1, 5]),[0, 1, 1, 15]); 53 | }); 54 | 55 | it('selector specificity calculator', function() { 56 | function spec(selector) { 57 | return new Selector(selector).specificity(); 58 | } 59 | 60 | assert.deepEqual(spec('#test'),[0, 1, 0, 0]); 61 | assert.deepEqual(spec('#a #b #c'),[0, 3, 0, 0]); 62 | assert.deepEqual(spec('.a .b .c'),[0, 0, 3, 0]); 63 | assert.deepEqual(spec('div.a div.b div.c'),[0, 0, 3, 3]); 64 | assert.deepEqual(spec('div a span'),[0, 0, 0, 3]); 65 | assert.deepEqual(spec('#test input[type=text]'),[0, 1, 1, 1]); 66 | assert.deepEqual(spec('[type=text]'), [0, 0, 1, 0]); 67 | assert.deepEqual(spec('*'),[0, 0, 0, 0]); 68 | assert.deepEqual(spec('div *'),[0, 0, 0, 1]); 69 | assert.deepEqual(spec('div.a.b'),[0, 0, 2, 1]); 70 | assert.deepEqual(spec('div:not(.a):not(.b)'),[0, 0, 2, 1]); 71 | 72 | assert.deepEqual(spec('div.a'),[0, 0, 1, 1]); 73 | assert.deepEqual(spec('.a:first-child'),[0, 0, 1, 1]); 74 | assert.deepEqual(spec('div:not(.c)'),[0, 0, 1, 1]); 75 | 76 | }); 77 | 78 | it('property comparison based on selector specificity', function() { 79 | function prop(k, v, sel) { 80 | return new Property(k, v, new Selector(sel)); 81 | } 82 | 83 | var a = prop('color', 'white', '#woot'); 84 | var b = prop('color', 'red', '#a #woot'); 85 | 86 | assert.deepEqual(a.compare(b),b); 87 | 88 | a = prop('background-color', 'red', '#a'); 89 | b = prop('background-color', 'red', '.a.b.c'); 90 | 91 | assert.deepEqual(a.compare(b),a); 92 | 93 | a = prop('background-color', 'red', '#a .b.c'); 94 | b = prop('background-color', 'red', '.a.b.c #c'); 95 | 96 | assert.deepEqual(a.compare(b),b); 97 | }); 98 | 99 | it('property toString', function() { 100 | var a = new Property('color', 'white', new Selector('#woot')); 101 | 102 | assert.equal(a.toString(), 'color: white;'); 103 | }); 104 | 105 | it('parse simple css into a object structure', function() { 106 | var parse = utils.parseCSS; 107 | 108 | var actual = parse('a, b { c: e; }'); 109 | 110 | var a = actual[0]; 111 | var b = actual[1]; 112 | 113 | assert.equal(a[0],'a'); 114 | assert.deepEqual(a[1]['0'],{ type: 'property', name: 'c', value: 'e', position: { start: { line: 1, col: 8 }, end: { line: 1, col: 12 } }}); 115 | assert.equal(a[1].length,1); 116 | assert.deepEqual(a[1],b[1]); 117 | }); 118 | 119 | it('parse complex css into a object structure', function() { 120 | var parse = utils.parseCSS; 121 | 122 | var actual = parse(['a, b { c: e; }', 'b.e #d { d: e; }','c[a=b] { d: e; }'].join('\n')); 123 | var a = actual[0]; 124 | var b = actual[1]; 125 | var bed = actual[2]; 126 | var cab = actual[3]; 127 | 128 | /* 129 | delete bed[1].parentRule; 130 | delete cab[1].parentRule; 131 | delete bed[1].__starts; 132 | delete cab[1].__starts; 133 | */ 134 | 135 | assert.deepEqual(a[1],b[1]); 136 | assert.equal(bed[1].name,cab[1].name); 137 | assert.equal(bed[1].value,cab[1].value); 138 | }); 139 | 140 | it('test excludedProperties setting', function() { 141 | juice.excludedProperties = ['-webkit-font-smoothing']; 142 | assert.deepEqual( 143 | juice( 144 | '
woot
', 145 | {extraCss: 'div { color: blue; -webkit-font-smoothing: antialiased; }'} 146 | ), 147 | '
woot
' 148 | ); 149 | // Reset global setting 150 | juice.excludedProperties = []; 151 | }); 152 | 153 | it('test juice', function() { 154 | assert.deepEqual( 155 | juice('
woot
', {extraCss: 'div { color: red; }'}), 156 | '
woot
'); 157 | }); 158 | 159 | it('test consecutive important rules', function() { 160 | assert.deepEqual( 161 | juice.inlineContent('

woot

', 'p a {color: red !important;} a {color: black !important;}'), 162 | '

woot

'); 163 | }); 164 | 165 | it('test * specificity', function() { 166 | assert.deepEqual( 167 | juice.inlineContent('
', 168 | '* { margin: 0; padding: 0; } .b { margin: 0 !important; } .a { padding: 20px; }'), 169 | '
'); 170 | }); 171 | 172 | it('test style attributes priority', function() { 173 | assert.deepEqual( 174 | juice.inlineContent('
', 'div { color: black; }'), 175 | '
'); 176 | }); 177 | 178 | it('test style attributes and important priority', function() { 179 | assert.deepEqual( 180 | juice.inlineContent('
', 'div { color: black !important; }'), 181 | '
'); 182 | }); 183 | 184 | it('test style attributes and important priority', function() { 185 | assert.deepEqual( 186 | juice.inlineContent('
', 'div { color: black !important; }'), 187 | '
'); 188 | }); 189 | 190 | it('test that preserved text order is stable', function() { 191 | assert.deepEqual( 192 | utils.getPreservedText('div { color: red; } @media (min-width: 320px) { div { color: green; } } @media (max-width: 640px) { div { color: blue; } }', { mediaQueries: true }, juice.ignoredPseudos).replace(/\s+/g, ' '), 193 | ' @media (min-width: 320px) { div { color: green; } } @media (max-width: 640px) { div { color: blue; } } '); 194 | }); 195 | 196 | it('can handle style attributes with html entities', function () { 197 | // Throws without decodeStyleAttributes: true 198 | assert.throws(function () { 199 | juice( 200 | '
' 201 | ); 202 | }); 203 | 204 | // Expected results with decodeStyleAttributes: true 205 | assert.deepEqual( 206 | juice( 207 | '
', 208 | { decodeStyleAttributes: true } 209 | ), 210 | "
" 211 | ); 212 | }); 213 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var juice = require('../'); 4 | var utils = require('../lib/utils'); 5 | var basename = require('path').basename; 6 | var fs = require('fs'); 7 | var assert = require('assert'); 8 | var cheerio = require('cheerio'); 9 | var htmlparser2 = require('htmlparser2'); 10 | 11 | 12 | /** 13 | * Auto-load and run tests. 14 | */ 15 | 16 | var files = fs.readdirSync(__dirname + '/cases'); 17 | files.forEach(function(file) { 18 | if (/\.html$/.test(file)) { 19 | var name = basename(file, '.html'); 20 | it(name, test(name, false)); 21 | } 22 | }); 23 | 24 | it('juice(html)', function() { 25 | var expected = '
'; 26 | var actual = juice('
'); 27 | assert.equal(utils.normalizeLineEndings(actual.trim()), utils.normalizeLineEndings(expected.trim())); 28 | }); 29 | 30 | it('juice(document) with htmlparser2', function() { 31 | var dom = htmlparser2.parseDocument('
'); 32 | var $ = cheerio.load(dom, {xml:true}); 33 | 34 | var expected = '
'; 35 | juice.juiceDocument($); 36 | var actual = $.html(); 37 | assert.equal(utils.normalizeLineEndings(actual.trim()), utils.normalizeLineEndings(expected.trim())); 38 | }); 39 | 40 | var optionFiles = fs.readdirSync(__dirname + '/cases/juice-content'); 41 | 42 | optionFiles.forEach(function(file) { 43 | if (/\.html$/.test(file)) { 44 | var name = 'juice-content/' + basename(file, '.html'); 45 | it(name, test(name, true)); 46 | } 47 | }); 48 | 49 | function read(file) { 50 | try{ 51 | return fs.readFileSync(file, 'utf8'); 52 | }catch(err){} 53 | } 54 | 55 | function test(testName, options) { 56 | var base = __dirname + '/cases/' + testName; 57 | var html = read(base + '.html'); 58 | var css = read(base + '.css'); 59 | var config = read(base + '.json'); 60 | config = config ? JSON.parse(config) : null; 61 | 62 | 63 | return function(done) { 64 | var onJuiced = function(err, actual) { 65 | if (err) { 66 | return done(err); 67 | } 68 | var expected = read(base + '.out'); 69 | assert.equal(utils.normalizeLineEndings(actual.trim()), utils.normalizeLineEndings(expected.trim())); 70 | done(); 71 | }; 72 | 73 | if( !options ) { 74 | onJuiced(null, juice.inlineContent(html, css, config)); 75 | } else { 76 | juice.juiceResources(html, config, onJuiced); 77 | } 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /*globals describe:false it:false*/ 2 | 3 | 'use strict'; 4 | 5 | var juice = require('../'); 6 | var utils = require('../lib/utils'); 7 | var path = require('path'); 8 | var fs = require('fs'); 9 | var Batch = require('batch'); 10 | var assert = require('assert'); 11 | 12 | var tests = [ 13 | 'doctype', 14 | 'no_css', 15 | 'two_styles', 16 | 'remote_url', 17 | 'spaces_in_path', 18 | ]; 19 | 20 | tests.forEach(function(testName) { 21 | it(testName, createIt(testName)); 22 | }); 23 | 24 | it('inlineContent', function() { 25 | var html = '

Hello

'; 26 | var css = 'p{font-weight:bold;}'; 27 | 28 | assert.strictEqual(juice.inlineContent(html, css), '

Hello

'); 29 | }); 30 | 31 | function createIt(testName) { 32 | return function(cb) { 33 | var batch = new Batch(); 34 | batch.push(function(cb) { 35 | juice.juiceFile(path.join(__dirname, 'html', testName + '.in.html'), {}, cb); 36 | }); 37 | batch.push(function(cb) { 38 | fs.readFile(path.join(__dirname, 'html', testName + '.out.html'), 'utf8', cb); 39 | }); 40 | batch.end(function(err, results) { 41 | if (err) { 42 | return cb(err); 43 | } 44 | assert.strictEqual(utils.normalizeLineEndings(results[1].trim()), utils.normalizeLineEndings(results[0].trim())); 45 | cb(); 46 | }); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /test/typescript/juice-tests.ts: -------------------------------------------------------------------------------- 1 | import juice = require('../../'); 2 | import cheerio = require('cheerio'); 3 | import htmlparser2 = require('htmlparser2'); 4 | 5 | const sample = '
'; 6 | const someHtml = '

yo

'; 7 | const someCss = '.x{font-size: 20em;}'; 8 | const noOptions = {}; 9 | const mostOptions = { 10 | extraCss: someCss, 11 | applyStyleTags: true, 12 | removeStyleTags: true, 13 | preserveMediaQueries: true, 14 | preserveFontFaces: true, 15 | preserveKeyFrames: true, 16 | insertPreservedExtraCss: true, 17 | applyWidthAttributes: true, 18 | applyHeightAttributes: true, 19 | applyAttributesTableElements: true, 20 | inlinePseudoElements: true, 21 | xmlMode: true, 22 | preserveImportant: true, 23 | }; 24 | const minWebResourceOptions = { 25 | webResources: {}, 26 | }; 27 | const someWebResourceOptions = { 28 | webResources: { 29 | images: false, 30 | }, 31 | }; 32 | const allWebResourceOptions = { 33 | webResources: { 34 | fileContent: '', 35 | inlineAttribute: 'inlineme', 36 | images: true, 37 | svgs: true, 38 | scripts: true, 39 | links: true, 40 | relativeTo: 'https://github.com/', 41 | rebaseRelativeTo: 'assets', 42 | strict: true, 43 | }, 44 | }; 45 | 46 | const someMoreHtml = '

test

'; 47 | const cheerio$ = cheerio.load(someMoreHtml); 48 | const htmlparser2dom = htmlparser2.parseDOM(someMoreHtml); 49 | const cheerioHtmlparser2$ = cheerio.load(htmlparser2dom); 50 | 51 | const x: string = juice(someHtml, {}); 52 | 53 | juice.juiceResources( 54 | sample, 55 | noOptions, 56 | (err: Error, html: string): void => console.log(html) 57 | ); 58 | 59 | juice.juiceResources( 60 | sample, 61 | mostOptions, 62 | (err: Error, html: string): void => console.log(html) 63 | ); 64 | 65 | juice.juiceResources( 66 | sample, 67 | minWebResourceOptions, 68 | (err: Error, html: string): void => console.log(html) 69 | ); 70 | 71 | juice.juiceResources( 72 | sample, 73 | someWebResourceOptions, 74 | (err: Error, html: string): void => console.log(html) 75 | ); 76 | 77 | juice.juiceResources( 78 | sample, 79 | allWebResourceOptions, 80 | (err: Error, html: string): void => console.log(html) 81 | ); 82 | 83 | juice.juiceResources( 84 | sample, 85 | { extraCss: 'body{background: red;}' }, 86 | (err: Error, html: string): void => console.log(html) 87 | ); 88 | 89 | juice.juiceFile( 90 | 'somePath.html', 91 | noOptions, 92 | (err: Error, html: string): void => console.log(html) 93 | ); 94 | 95 | juice.juiceFile( 96 | 'somePath.html', 97 | mostOptions, 98 | (err: Error, html: string): void => console.log(html) 99 | ); 100 | 101 | juice.juiceFile( 102 | 'somePath.html', 103 | minWebResourceOptions, 104 | (err: Error, html: string): void => console.log(html) 105 | ); 106 | 107 | juice.juiceFile( 108 | 'somePath.html', 109 | someWebResourceOptions, 110 | (err: Error, html: string): void => console.log(html) 111 | ); 112 | 113 | juice.juiceFile( 114 | 'somePath.html', 115 | allWebResourceOptions, 116 | (err: Error, html: string): void => console.log(html) 117 | ); 118 | 119 | const c1 = juice.juiceDocument( 120 | cheerio$, 121 | noOptions 122 | ); 123 | 124 | const c2 = juice.juiceDocument( 125 | cheerio$, 126 | mostOptions 127 | ); 128 | 129 | const c3 = juice.juiceDocument( 130 | cheerio$, 131 | minWebResourceOptions 132 | ); 133 | 134 | const c4 = juice.juiceDocument( 135 | cheerio$, 136 | someWebResourceOptions 137 | ); 138 | 139 | const c5 = juice.juiceDocument( 140 | cheerio$, 141 | allWebResourceOptions 142 | ); 143 | 144 | const c6 = juice.juiceDocument( 145 | cheerio$, 146 | ); 147 | 148 | const c7 = juice.juiceDocument( 149 | cheerioHtmlparser2$, 150 | noOptions 151 | ); 152 | 153 | juice.inlineContent(someHtml, someCss, noOptions); 154 | 155 | juice.inlineContent(someHtml, someCss, mostOptions); 156 | 157 | juice.inlineContent(someHtml, someCss, minWebResourceOptions); 158 | 159 | juice.inlineContent(someHtml, someCss, someWebResourceOptions); 160 | 161 | juice.inlineContent(someHtml, someCss, allWebResourceOptions); 162 | 163 | juice.inlineDocument(cheerio$, someCss, noOptions); 164 | 165 | juice.inlineDocument(cheerio$, someCss, mostOptions); 166 | 167 | juice.inlineDocument(cheerio$, someCss, minWebResourceOptions); 168 | 169 | juice.inlineDocument(cheerio$, someCss, someWebResourceOptions); 170 | 171 | juice.inlineDocument(cheerio$, someCss, allWebResourceOptions); 172 | 173 | juice.inlineDocument(cheerioHtmlparser2$, someCss, noOptions); 174 | 175 | // Global settings 176 | 177 | juice.codeBlocks['HBS'] = { start: '{{', end: '}}' }; 178 | juice.ignoredPseudos = ['hover']; 179 | juice.widthElements = []; 180 | juice.heightElements = []; 181 | juice.styleToAttribute = { x: 'y' }; 182 | juice.tableElements = []; 183 | juice.nonVisualElements = []; 184 | juice.excludedProperties = []; 185 | --------------------------------------------------------------------------------