Repository: Automattic/juice (js)

Issue:
CSS counters is not supported
My Input is 

```
<style>
body {
    counter-reset: section;
}
h2::before {
    counter-increment: section;
    content:counter(section);
}
</style>
<h2> Title 1</h2>
<h2> Title 2</h2>

```

Juice output

![image](https://cloud.githubusercontent.com/assets/1135045/21602899/da41d4c2-d1d3-11e6-9206-f4108971e306.png)

I hope Juice output

```
<h2><span>1</span> Title 1</h2>
<h2><span>2</span> Title 2</h2>
```

Pull request description:
Add support for counters and multiple values in pseudo-elements
Adds support for `<var>` `<attr>` `<counter>` in content attributes for pseudo-elements. Also adds the possibility of defining counter-reset and counter-increment in styles.

Fixes #256.

Patch:
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] = $('<span />').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"


Test patch:
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 @@
 <html><body>
 <style>
+body {
+  --var: 'Element';
+  counter-reset: element 20 other;
+}
+
 a {
   text-decoration: underline;
 }
@@ -47,6 +52,27 @@ d:after {
   content: url('data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=');
   width: 20%;
 }
+
+e {
+  counter-increment: element other -2;
+}
+
+e:before {
+  content: var(--var) " / " counter(element) '. ' counter(other);
+}
+
+e[length]:after {
+  content: ' | Length: ' attr(length) ' [' counter(element, lower-roman) ':' counter(element, upper-latin) ']';
+}
+
+f {
+  counter-increment: other;
+}
+
+f:before {
+  content: counter(other);
+}
+
 </style>
 <a href="#">Test</a>
 <b></b>
@@ -54,4 +80,8 @@ d:after {
 <c></c>
 <c class="not"></c>
 <d><span>test</span></d>
+<e><span></span></e>
+<e length="100"><span>Test</span></e>
+<f><span></span></f>
+<f><span></span></f>
 </body></html>
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 @@
-<html><body>
+<html><body style="--var: 'Element'; counter-reset: element 20 other;">
 
 <a href="#" style="text-decoration: underline; color: blue;"><span>®+</span>Test<span style="font-weight: bold;">a</span></a>
 <b><span style="color: green;">b</span></b>
@@ -6,4 +6,8 @@
 <c style="color: red;"></c>
 <c class="not"></c>
 <d><img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="><span>test</span><img style="width: 20%;" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="></d>
+<e style="counter-increment: element other -2;"><span>Element / 21. -2</span><span></span></e>
+<e length="100" style="counter-increment: element other -2;"><span>Element / 22. -4</span><span>Test</span><span> | Length: 100 [xxii:V]</span></e>
+<f style="counter-increment: other;"><span>-3</span><span></span></f>
+<f style="counter-increment: other;"><span>-2</span><span></span></f>
 </body></html>