From 83b0167d5dca21c959f996d9ab40fffc9b53566c Mon Sep 17 00:00:00 2001 From: Mike Voets Date: Sun, 2 Aug 2020 22:41:31 +1200 Subject: [PATCH] Fix #256, add support for multitoken values in content attribute Adds support for in content attributes for pseudoelements Also adds the possibility of defining counter-reset and counter-increment in styles --- lib/inline.js | 121 +++++++++++++++++- lib/numbers.js | 40 ++++++ package.json | 12 +- test/cases/juice-content/pseudo-elements.html | 30 +++++ test/cases/juice-content/pseudo-elements.out | 6 +- 5 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 lib/numbers.js diff --git a/lib/inline.js b/lib/inline.js index 7916b23..866ee74 100644 --- a/lib/inline.js +++ b/lib/inline.js @@ -1,6 +1,7 @@ 'use strict'; var utils = require('./utils'); +var numbers = require('./numbers'); module.exports = function makeJuiceClient(juiceClient) { @@ -26,6 +27,7 @@ function inlineDocument($, css, options) { var rules = utils.parseCSS(css); var editedElements = []; var styleAttributeName = 'style'; + var counters = {}; if (options.styleAttributeName) { styleAttributeName = options.styleAttributeName; @@ -129,6 +131,7 @@ function inlineDocument($, css, options) { pseudoEl = el[pseudoElPropName] = $('').get(0); pseudoEl.pseudoElementType = pseudoElementType; pseudoEl.pseudoElementParent = el; + pseudoEl.counterProps = el.counterProps; el[pseudoElPropName] = pseudoEl; } el = pseudoEl; @@ -147,13 +150,59 @@ function inlineDocument($, css, options) { editedElements.push(el); } + if (!el.counterProps) { + el.counterProps = el.parent && el.parent.counterProps + ? Object.create(el.parent.counterProps) + : {}; + } + + function resetCounter(el, value) { + var tokens = value.split(/\s+/); + + for (var j = 0; j < tokens.length; j++) { + var counter = tokens[j]; + var resetval = parseInt(tokens[j+1], 10); + + isNaN(resetval) + ? el.counterProps[counter] = counters[counter] = 0 + : el.counterProps[counter] = counters[tokens[j++]] = resetval; + } + } + + function incrementCounter(el, value) { + var tokens = value.split(/\s+/); + + for (var j = 0; j < tokens.length; j++) { + var counter = tokens[j]; + + if (el.counterProps[counter] === undefined) { + continue; + } + + var incrval = parseInt(tokens[j+1], 10); + + isNaN(incrval) + ? el.counterProps[counter] = counters[counter] += 1 + : el.counterProps[counter] = counters[tokens[j++]] += incrval; + } + } + // go through the properties function addProps(style, selector) { for (var i = 0, l = style.length; i < l; i++) { if (style[i].type == 'property') { var name = style[i].name; var value = style[i].value; - var important = style[i].value.match(/!important$/) !== null; + + if (name === 'counter-reset') { + resetCounter(el, value); + } + + if (name === 'counter-increment') { + incrementCounter(el, value); + } + + var important = value.match(/!important$/) !== null; if (important && !options.preserveImportant) value = value.replace(/\s*!important$/, ''); // adds line number and column number for the properties as "additionalPriority" to the // properties because in CSS the position directly affect the priority. @@ -218,7 +267,7 @@ function inlineDocument($, css, options) { function inlinePseudoElements(el) { if (el.pseudoElementType && el.styleProps.content) { - var parsed = parseContent(el.styleProps.content.value); + var parsed = parseContent(el); if (parsed.img) { el.name = 'img'; $(el).attr('src', parsed.img); @@ -283,7 +332,37 @@ function inlineDocument($, css, options) { } } -function parseContent(content) { +function findVariableValue(el, variable) { + while (el) { + if (variable in el.styleProps) { + return el.styleProps[variable].value; + } + + var el = el.parent || el.pseudoElementParent; + } +} + +function applyCounterStyle(counter, style) { + switch (style) { + case 'lower-roman': + return numbers.romanize(counter).toLowerCase(); + case 'upper-roman': + return numbers.romanize(counter); + case 'lower-latin': + case 'lower-alpha': + return numbers.alphanumeric(counter).toLowerCase(); + case 'upper-latin': + case 'upper-alpha': + return numbers.alphanumeric(counter); + // TODO support more counter styles + default: + return counter.toString(); + } +} + +function parseContent(el) { + var content = el.styleProps.content.value; + if (content === 'none' || content === 'normal') { return ''; } @@ -294,8 +373,40 @@ function parseContent(content) { return { img: url }; } - // Naive parsing, assume well-formed value - content = content.slice(1, content.length - 1); + var parsed = []; + + var tokens = content.split(/['"]/); + for (var i = 0; i < tokens.length; i++) { + if (tokens[i] === '') continue; + + var varMatch = tokens[i].match(/var\s*\(\s*(.*?)\s*(,\s*(.*?)\s*)?\s*\)/i); + if (varMatch) { + var variable = findVariableValue(el, varMatch[1]) || varMatch[2]; + parsed.push(variable.replace(/^['"]|['"]$/g, '')); + continue; + } + + var counterMatch = tokens[i].match(/counter\s*\(\s*(.*?)\s*(,\s*(.*?)\s*)?\s*\)/i); + if (counterMatch && counterMatch[1] in el.counterProps) { + var counter = el.counterProps[counterMatch[1]]; + parsed.push(applyCounterStyle(counter, counterMatch[3])); + continue; + } + + var attrMatch = tokens[i].match(/attr\s*\(\s*(.*?)\s*\)/i); + if (attrMatch) { + var attr = attrMatch[1]; + parsed.push(el.pseudoElementParent + ? el.pseudoElementParent.attribs[attr] + : el.attribs[attr] + ); + continue; + } + + parsed.push(tokens[i]); + } + + content = parsed.join(''); // Naive unescape, assume no unicode char codes content = content.replace(/\\/g, ''); return content; diff --git a/lib/numbers.js b/lib/numbers.js new file mode 100644 index 0000000..4fdafde --- /dev/null +++ b/lib/numbers.js @@ -0,0 +1,40 @@ +'use strict'; + +/** + * Converts a decimal number to roman numeral. + * https://stackoverflow.com/questions/9083037/convert-a-number-into-a-roman-numeral-in-javascript + * + * @param {Number} number + * @api private. + */ +exports.romanize = function(num) { + if (isNaN(num)) + return NaN; + var digits = String(+num).split(""), + key = ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM", + "","X","XX","XXX","XL","L","LX","LXX","LXXX","XC", + "","I","II","III","IV","V","VI","VII","VIII","IX"], + roman = "", + i = 3; + while (i--) + roman = (key[+digits.pop() + (i * 10)] || "") + roman; + return Array(+digits.join("") + 1).join("M") + roman; +} + +/** + * Converts a decimal number to alphanumeric numeral. + * https://stackoverflow.com/questions/45787459/convert-number-to-alphabet-string-javascript + * + * @param {Number} number + * @api private. + */ +exports.alphanumeric = function(num) { + var s = '', t; + + while (num > 0) { + t = (num - 1) % 26; + s = String.fromCharCode(65 + t) + s; + num = (num - t)/26 | 0; + } + return s || undefined; +} \ No newline at end of file diff --git a/package.json b/package.json index fa8838f..616cebb 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,15 @@ }, "license": "MIT", "contributors": [ - { "name": "Guillermo Rauch" }, - { "name": "Andrew Kelley" }, - { "name": "Jarrett Widman" } + { + "name": "Guillermo Rauch" + }, + { + "name": "Andrew Kelley" + }, + { + "name": "Jarrett Widman" + } ], "engines": { "node": ">=10.0.0" diff --git a/test/cases/juice-content/pseudo-elements.html b/test/cases/juice-content/pseudo-elements.html index 645a879..d907e9c 100644 --- a/test/cases/juice-content/pseudo-elements.html +++ b/test/cases/juice-content/pseudo-elements.html @@ -1,5 +1,10 @@ Test @@ -54,4 +80,8 @@ test + +Test + + diff --git a/test/cases/juice-content/pseudo-elements.out b/test/cases/juice-content/pseudo-elements.out index 610ec61..ee324e0 100644 --- a/test/cases/juice-content/pseudo-elements.out +++ b/test/cases/juice-content/pseudo-elements.out @@ -1,4 +1,4 @@ - + ®+Testa b @@ -6,4 +6,8 @@ test +Element / 21. -2 +Element / 22. -4Test | Length: 100 [xxii:V] +-3 +-2