diff --git a/README.md b/README.md index 7269491..81e500c 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ All juice methods take an options object that can contain any of these propertie * `removeStyleTags` - whether to remove the original `` tags after (possibly) inlining the css from them. Defaults to `true`. +* `resolveCSSVariables` - whether to resolve CSS variables. Defaults to `true`. + * `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 `{}`. * `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`. diff --git a/juice.d.ts b/juice.d.ts index 618ef21..4900106 100644 --- a/juice.d.ts +++ b/juice.d.ts @@ -49,6 +49,7 @@ declare namespace juice { inlinePseudoElements?: boolean; xmlMode?: boolean; preserveImportant?: boolean; + resolveCSSVariables?: boolean; } interface WebResourcesOptions { diff --git a/lib/cli.js b/lib/cli.js index d71fae7..89b9377 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -81,6 +81,10 @@ cli.options = { pMap: 'xmlMode', def: 'generate output with tags closed? input must be valid XML', coercion: JSON.parse }, + 'resolve-css-variables': { + pMap: 'resolveCSSVariables', + def: 'resolve CSS variables', + coercion: JSON.parse }, 'web-resources-inline-attribute': { pMap: 'webResourcesInlineAttribute', map: 'inlineAttribute', diff --git a/lib/inline.js b/lib/inline.js index 475a62e..a271591 100644 --- a/lib/inline.js +++ b/lib/inline.js @@ -2,6 +2,7 @@ var utils = require('./utils'); var numbers = require('./numbers'); +var variables = require('./variables'); module.exports = function makeJuiceClient(juiceClient) { @@ -250,13 +251,23 @@ function inlineDocument($, css, options) { props.sort(function(a, b) { return a.compareFunc(b); }); + var string = props .filter(function(prop) { + + // don't add css variables if we're resolving their values + if (options.resolveCSSVariables && (prop.prop.indexOf('--') === 0) ) { + return false; + } + // Content becomes the innerHTML of pseudo elements, not used as a // style property - return prop.prop !== 'content'; + return (prop.prop !== 'content'); }) .map(function(prop) { + if (options.resolveCSSVariables) { + prop.value = variables.replaceVariables(el,prop.value); + } return prop.prop + ': ' + prop.value.replace(/["]/g, '\'') + ';'; }) .join(' '); @@ -343,15 +354,7 @@ function removeImportant(value) { return value.replace(/\s*!important$/, '') } -function findVariableValue(el, variable) { - while (el) { - if (variable in el.styleProps) { - return el.styleProps[variable].value; - } - var el = el.pseudoElementParent || el.parent; - } -} function applyCounterStyle(counter, style) { switch (style) { @@ -392,7 +395,7 @@ function parseContent(el) { var varMatch = tokens[i].match(/var\s*\(\s*(.*?)\s*(,\s*(.*?)\s*)?\s*\)/i); if (varMatch) { - var variable = findVariableValue(el, varMatch[1]) || varMatch[2]; + var variable = variables.findVariableValue(el, varMatch[1]) || varMatch[2]; parsed.push(variable.replace(/^['"]|['"]$/g, '')); continue; } diff --git a/lib/utils.js b/lib/utils.js index 2f8c32c..f5cca30 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -44,7 +44,6 @@ exports.extract = function extract(selectorText) { if (sel.length) { sels.push(sel); } - return sels; }; @@ -158,6 +157,7 @@ exports.getDefaultOptions = function(options) { applyWidthAttributes: true, applyHeightAttributes: true, applyAttributesTableElements: true, + resolveCSSVariables: true, url: '' }, options); diff --git a/lib/variables.js b/lib/variables.js new file mode 100644 index 0000000..554c504 --- /dev/null +++ b/lib/variables.js @@ -0,0 +1,70 @@ +'use strict'; + +const uniqueString = (string) => { + let str = ''; + do{ + str = (Math.random() + 1).toString(36).substring(2); + + }while(string.indexOf(str) !== -1); + + return str; +} + +/** + * Replace css variables with their value + */ +const replaceVariables = (el,value) => { + + // find non-nested css function calls + // eg: rgb(...), drop-shadow(...) + let funcReg = /([a-z\-]+)\s*\(\s*([^\(\)]*?)\s*(?:,\s*([^\(\)]*?)\s*)?\s*\)/i; + let replacements = []; + let match; + let uniq = uniqueString(value); + + while( (match = funcReg.exec(value)) !== null ){ + let i = `${replacements.length}`; + + + // attempt to resolve variables + if( match[1].toLowerCase() == 'var' ){ + const varValue = findVariableValue(el, match[2]); + + // found variable value + if( varValue ){ + value = value.replace(match[0],varValue); + continue; + } + + // use default value + // var(--name , default-value) + if( match[3] ){ + value = value.replace(match[0],match[3]); + continue; + } + } + + let placeholder = `${uniq}${i.padStart(5,'-')}`; + value = value.replace(match[0],placeholder); + replacements.push({placeholder,replace:match[0]}); + } + + for( var i = replacements.length-1; i >=0; i--){ + const replacement = replacements[i]; + value = value.replace(replacement.placeholder,replacement.replace); + } + + return value; +} + +const findVariableValue = (el, variable) => { + while (el) { + if (el.styleProps && variable in el.styleProps) { + return el.styleProps[variable].value; + } + + var el = el.pseudoElementParent || el.parent; + } +} + +module.exports = { replaceVariables, findVariableValue }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0c6982a..a7adec0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "juice", - "version": "9.0.0", + "version": "9.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "juice", - "version": "9.0.0", + "version": "9.1.0", "license": "MIT", "dependencies": { "cheerio": "^1.0.0-rc.12", diff --git a/test/cases/css-variables.css b/test/cases/css-variables.css new file mode 100644 index 0000000..fdc273f --- /dev/null +++ b/test/cases/css-variables.css @@ -0,0 +1,15 @@ +:root{ + --border-width:1px; + --border-color:red; + --border-style-default:solid; +} +p { + --border-width:2px; + --border-style:var(--border-style-default); + border: VAR(--border-width) var(--border-style) var(--border-color); + background: var(--bg); +} + +td{ + background-color: var(--bg,var(--border-color,rgb(255,0,0))); +} diff --git a/test/cases/css-variables.html b/test/cases/css-variables.html new file mode 100644 index 0000000..b68348c --- /dev/null +++ b/test/cases/css-variables.html @@ -0,0 +1,6 @@ + + + +

woot

+woot + diff --git a/test/cases/css-variables.json b/test/cases/css-variables.json new file mode 100644 index 0000000..9e47e6f --- /dev/null +++ b/test/cases/css-variables.json @@ -0,0 +1,4 @@ +{ + "resolveCSSVariables": true, + "applyAttributesTableElements": true +} \ No newline at end of file diff --git a/test/cases/css-variables.out b/test/cases/css-variables.out new file mode 100644 index 0000000..3c2326c --- /dev/null +++ b/test/cases/css-variables.out @@ -0,0 +1,5 @@ + + +

woot

+woot + diff --git a/test/cases/juice-content/pseudo-elements.json b/test/cases/juice-content/pseudo-elements.json index 1120fc0..0678b26 100644 --- a/test/cases/juice-content/pseudo-elements.json +++ b/test/cases/juice-content/pseudo-elements.json @@ -1,5 +1,6 @@ { "url": "./", "removeStyleTags": true, - "inlinePseudoElements": true + "inlinePseudoElements": true, + "resolveCSSVariables": false } diff --git a/test/cli.js b/test/cli.js index 2b7bbfd..046a0f7 100644 --- a/test/cli.js +++ b/test/cli.js @@ -26,6 +26,7 @@ it('cli parses options', function(done) { assert.strictEqual(cli.argsToOptions({'applyHeightAttributes': 'true'}).applyHeightAttributes, true); assert.strictEqual(cli.argsToOptions({'applyAttributesTableElements': 'true'}).applyAttributesTableElements, true); assert.strictEqual(cli.argsToOptions({'xmlMode': 'true'}).xmlMode, true); + assert.strictEqual(cli.argsToOptions({'resolveCSSVariables': 'true'}).resolveCSSVariables, true); assert.strictEqual(cli.argsToOptions({'webResourcesInlineAttribute': 'true'}).webResources.inlineAttribute, true); assert.strictEqual(cli.argsToOptions({'webResourcesImages': '12'}).webResources.images, 12); assert.strictEqual(cli.argsToOptions({'webResourcesLinks': 'true'}).webResources.links, true); diff --git a/test/run.js b/test/run.js index 06d23ae..7d1063b 100644 --- a/test/run.js +++ b/test/run.js @@ -47,16 +47,18 @@ optionFiles.forEach(function(file) { }); function read(file) { - return fs.readFileSync(file, 'utf8'); + try{ + return fs.readFileSync(file, 'utf8'); + }catch(err){} } function test(testName, options) { var base = __dirname + '/cases/' + testName; var html = read(base + '.html'); var css = read(base + '.css'); - var config = options ? JSON.parse(read(base + '.json')) : null; + var config = read(base + '.json'); + config = config ? JSON.parse(config) : null; - options = {}; return function(done) { var onJuiced = function(err, actual) { @@ -68,8 +70,8 @@ function test(testName, options) { done(); }; - if (config === null) { - onJuiced(null, juice.inlineContent(html, css, options)); + if( !options ) { + onJuiced(null, juice.inlineContent(html, css, config)); } else { juice.juiceResources(html, config, onJuiced); }