Repository: jsdom/cssstyle (js)

Issue:
update url parsing logic to match current browsers
it would be nice if url parsing worked like latest firefox/chrome.

for instance, running the following snippet in those browsers:
```
(() => {
  var div = document.createElement('div');
  div.style.backgroundImage = '   url(http://some/url/here1.png)   , URL(2)  ';
  console.log(`backgroundImage:${div.style.backgroundImage};`);
})();
```

prints:
```
backgroundImage:url("http://some/url/here1.png"), url("2");
```

i've searched the code a bit, and think we can achieve this by modifying the following function:
https://github.com/jsdom/cssstyle/blob/f2db1ebef45c03b51787f5b9d00a3eb843f07787/lib/parsers.js#L210-L247

we can also add some better unit tests here:
- https://github.com/jsdom/cssstyle/blob/f2db1ebef45c03b51787f5b9d00a3eb843f07787/lib/parsers.test.js#L63-L65
- https://github.com/jsdom/cssstyle/blob/f2db1ebef45c03b51787f5b9d00a3eb843f07787/lib/CSSStyleDeclaration.test.js#L377-L385

if i make the "formatted" string look like `url("foo.png")` instead of the current `url(foo.png)`, then I could see that breaking people's existing tests (but it would "match" modern browsers more closely).  If I just allow trailing/closing whitespace, and multiple urls, then that would be "more" backwards compatible I think.

I can work on this in the next few days and submit a PR.  I have quite a few ideas on how to get it to work.

Thanks!


Pull request description:
Update parseUrl function
Fix #103

* Handle escaped char.
* Output URL string with quotes.
  Although the unquoted output is valid, all major browsers appear to quote the output.

This is a BREAKING CHANGE


Patch:
diff --git a/lib/parsers.js b/lib/parsers.js
index 6869951..5e0c030 100644
--- a/lib/parsers.js
+++ b/lib/parsers.js
@@ -33,7 +33,7 @@ var lengthRegEx = new RegExp(
 );
 var percentRegEx = new RegExp(`^${NUMBER}%$`);
 var angleRegEx = new RegExp(`^${NUMBER}(?:deg|g?rad|turn)$`);
-var urlRegEx = /^url\(\s*([^)]*)\s*\)$/;
+var urlRegEx = /^url\(\s*((?:[^)]|\\\))*)\s*\)$/;
 var stringRegEx = /^("[^"]*"|'[^']*')$/;
 var varRegEx = /^var\(|(?<=[*/\s(])var\(/;
 var calcRegEx =
@@ -218,24 +218,45 @@ exports.parseUrl = function parseUrl(val) {
     str = str.substr(1, str.length - 2);
   }
 
+  var urlstr = '';
+  var escaped = false;
   var i;
   for (i = 0; i < str.length; i++) {
     switch (str[i]) {
+      case '\\':
+        if (escaped) {
+          urlstr += '\\\\';
+          escaped = false;
+        } else {
+          escaped = true;
+        }
+        break;
       case '(':
       case ')':
       case ' ':
       case '\t':
       case '\n':
       case "'":
+        if (!escaped) {
+          return undefined;
+        }
+        urlstr += str[i];
+        escaped = false;
+        break;
       case '"':
-        return undefined;
-      case '\\':
-        i++;
+        if (!escaped) {
+          return undefined;
+        }
+        urlstr += '\\"';
+        escaped = false;
         break;
+      default:
+        urlstr += str[i];
+        escaped = false;
     }
   }
 
-  return 'url(' + str + ')';
+  return 'url("' + urlstr + '")';
 };
 
 exports.parseString = function parseString(val) {


Test patch:
diff --git a/test/CSSStyleDeclaration.js b/test/CSSStyleDeclaration.js
index f50ebeb..8819693 100644
--- a/test/CSSStyleDeclaration.js
+++ b/test/CSSStyleDeclaration.js
@@ -97,8 +97,8 @@ describe('CSSStyleDeclaration', () => {
     var style = new CSSStyleDeclaration();
     style.background = 'blue url(http://www.example.com/some_img.jpg)';
     assert.strictEqual(style.backgroundColor, 'blue');
-    assert.strictEqual(style.backgroundImage, 'url(http://www.example.com/some_img.jpg)');
-    assert.strictEqual(style.background, 'blue url(http://www.example.com/some_img.jpg)');
+    assert.strictEqual(style.backgroundImage, 'url("http://www.example.com/some_img.jpg")');
+    assert.strictEqual(style.background, 'blue url("http://www.example.com/some_img.jpg")');
     style.border = '0 solid black';
     assert.strictEqual(style.borderWidth, '0px');
     assert.strictEqual(style.borderStyle, 'solid');
@@ -225,8 +225,8 @@ describe('CSSStyleDeclaration', () => {
     var style = new CSSStyleDeclaration();
     style.background = 'rgb(0, 0, 0) url(/something/somewhere.jpg)';
     assert.strictEqual(style.backgroundColor, 'rgb(0, 0, 0)');
-    assert.strictEqual(style.backgroundImage, 'url(/something/somewhere.jpg)');
-    assert.strictEqual(style.cssText, 'background: rgb(0, 0, 0) url(/something/somewhere.jpg);');
+    assert.strictEqual(style.backgroundImage, 'url("/something/somewhere.jpg")');
+    assert.strictEqual(style.cssText, 'background: rgb(0, 0, 0) url("/something/somewhere.jpg");');
     style = new CSSStyleDeclaration();
     style.border = '  1px  solid   black  ';
     assert.strictEqual(style.border, '1px solid black');
@@ -546,11 +546,11 @@ describe('CSSStyleDeclaration', () => {
   it('url parsing works with quotes', () => {
     var style = new CSSStyleDeclaration();
     style.backgroundImage = 'url(http://some/url/here1.png)';
-    assert.strictEqual(style.backgroundImage, 'url(http://some/url/here1.png)');
+    assert.strictEqual(style.backgroundImage, 'url("http://some/url/here1.png")');
     style.backgroundImage = "url('http://some/url/here2.png')";
-    assert.strictEqual(style.backgroundImage, 'url(http://some/url/here2.png)');
+    assert.strictEqual(style.backgroundImage, 'url("http://some/url/here2.png")');
     style.backgroundImage = 'url("http://some/url/here3.png")';
-    assert.strictEqual(style.backgroundImage, 'url(http://some/url/here3.png)');
+    assert.strictEqual(style.backgroundImage, 'url("http://some/url/here3.png")');
   });
 
   it('setting 0 to a padding or margin works', () => {
diff --git a/test/parsers.js b/test/parsers.js
index c318ff0..c8daf33 100644
--- a/test/parsers.js
+++ b/test/parsers.js
@@ -176,6 +176,133 @@ describe('parseMeasurement', () => {
   it.todo('test');
 });
 describe('parseUrl', () => {
+  it('should return undefined', () => {
+    let input = 'url(var(--foo))';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, undefined);
+  });
+
+  it('should return quoted url string', () => {
+    let input = 'url(sample.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample.png")');
+  });
+
+  it('should return quoted url string', () => {
+    let input = "url('sample.png')";
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample.png")');
+  });
+
+  it('should return quoted url string', () => {
+    let input = 'url("sample.png")';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample.png")');
+  });
+
+  it('should return quoted url string without escape', () => {
+    let input = 'url(sample\\-escaped.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample-escaped.png")');
+  });
+
+  it('should return quoted url string with escape', () => {
+    let input = 'url(sample\\\\-escaped.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample\\\\-escaped.png")');
+  });
+
+  it('should return undefined', () => {
+    let input = 'url(sample unescaped -space.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, undefined);
+  });
+
+  it('should return quoted url string without escape', () => {
+    let input = 'url(sample\\ escaped\\ -space.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample escaped -space.png")');
+  });
+
+  it('should return undefined', () => {
+    let input = 'url(sample\tunescaped\t-tab.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, undefined);
+  });
+
+  it('should return quoted url string without escape', () => {
+    let input = 'url(sample\\\tescaped\\\t-tab.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample\tescaped\t-tab.png")');
+  });
+
+  it('should return undefined', () => {
+    let input = 'url(sample\nunescaped\n-lf.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, undefined);
+  });
+
+  it('should return quoted url string without escape', () => {
+    let input = 'url(sample\\\nescaped\\\n-lf.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample\nescaped\n-lf.png")');
+  });
+
+  it('should return undefined', () => {
+    let input = "url(sample'unescaped'-quote.png)";
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, undefined);
+  });
+
+  it('should return quoted url string without escape', () => {
+    let input = "url(sample\\'escaped\\'-quote.png)";
+    let output = parsers.parseUrl(input);
+
+    // prettier-ignore
+    assert.strictEqual(output, "url(\"sample'escaped'-quote.png\")");
+  });
+
+  it('should return undefined', () => {
+    let input = 'url(sample"unescaped"-double-quote.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, undefined);
+  });
+
+  it('should return quoted url string with escape', () => {
+    let input = 'url(sample\\"escaped\\"-double-quote.png)';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("sample\\"escaped\\"-double-quote.png")');
+  });
+
+  it('should return quoted empty url string', () => {
+    let input = 'url()';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("")');
+  });
+
+  it('should return quoted empty url string', () => {
+    let input = 'url("")';
+    let output = parsers.parseUrl(input);
+
+    assert.strictEqual(output, 'url("")');
+  });
+
   it.todo('test');
 });
 describe('parseString', () => {
@@ -314,28 +441,28 @@ describe('parseImage', () => {
     let input = 'url(example.png)';
     let output = parsers.parseImage(input);
 
-    assert.strictEqual(output, 'url(example.png)');
+    assert.strictEqual(output, 'url("example.png")');
   });
 
   it('should return value', () => {
     let input = 'url(example.png), url("example2.png")';
     let output = parsers.parseImage(input);
 
-    assert.strictEqual(output, 'url(example.png), url(example2.png)');
+    assert.strictEqual(output, 'url("example.png"), url("example2.png")');
   });
 
   it('should return value', () => {
     let input = 'none, url(example.png)';
     let output = parsers.parseImage(input);
 
-    assert.strictEqual(output, 'none, url(example.png)');
+    assert.strictEqual(output, 'none, url("example.png")');
   });
 
   it('should return value', () => {
     let input = 'linear-gradient(green, blue), url(example.png)';
     let output = parsers.parseImage(input);
 
-    assert.strictEqual(output, 'linear-gradient(green, blue), url(example.png)');
+    assert.strictEqual(output, 'linear-gradient(green, blue), url("example.png")');
   });
 
   it('should return value as is if var() is included', () => {