Skip to content

url() with quoted data: URL loses inner quotes, producing invalid CSS #217

@bartveneman

Description

@bartveneman

Summary

format-css strips the outer quotes from url('data:...') values even when the
inner content contains " or ' characters. The resulting unquoted url() is
invalid CSS because bare " and ' are not allowed inside an unquoted URL token
(CSS spec §4.3.6). Downstream parsers (PostCSS, stylelint) then report
"Unclosed string".

Reproduction

import { format } from '@projectwallace/format-css'

// Input: outer single quotes protect inner double quotes
const input = `.a { background: url('data:image/svg+xml,%3Csvg fill="red"%3E%3C/svg%3E'); }`

console.log(format(input))

Output (current — invalid CSS):

.a {
	background: url(data:image/svg+xml,%3Csvg fill="red"%3E%3C/svg%3E);
}

The outer single quotes are removed. The inner " characters are now bare inside
an unquoted url(), which is illegal. PostCSS/stylelint fails to parse this with:

Unclosed string (CssSyntaxError)

Expected output (valid CSS):

.a {
	background: url("data:image/svg+xml,%3Csvg fill=\"red\"%3E%3C/svg%3E");
}

The value should stay quoted (wrapped in double quotes) because it contains
characters that are not safe in an unquoted URL.

Real-world trigger

This surfaces with Gravity Forms stylesheets, which embed SVG icons as custom
properties using single-quoted url() values where SVG attributes use double
quotes:

#gform_wrapper_3[data-form-index="0"].gform-theme,
[data-parent-form="3_0"] {
  --gf-icon-ctrl-number: url('data:image/svg+xml,%3Csvg width=\'8\' height=\'14\' fill=\'none\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath fill=\'rgba(17, 35, 55, 0.65)\'/%3E%3C/svg%3E');
}

After formatting, the outer quotes are stripped and the inner single quotes
become bare, also triggering the same parse error.

Root cause

In dist/index.js, the url-printing branch unconditionally unquotes data: URLs:

if (/^['"]?data:/i.test(value)) parts.push(unquote(value));
else parts.push(print_string(value));

Suggested fix

Only unquote a data: URL value when it is safe to do so — i.e. the unquoted
value contains neither " nor '. When inner quotes are present, choose the
outer delimiter that does not conflict:

if (/^['"]?data:/i.test(value)) {
    const unquoted = unquote(value);
    if (!unquoted.includes('"') && !unquoted.includes("'")) {
        // Safe to leave unquoted
        parts.push(unquoted);
    } else if (!unquoted.includes('"')) {
        // Inner single quotes only — wrap in double quotes
        parts.push('"' + unquoted + '"');
    } else if (!unquoted.includes("'")) {
        // Inner double quotes only — wrap in single quotes
        parts.push("'" + unquoted + "'");
    } else {
        // Both quote types — URL-encode the double quotes and wrap in double quotes
        parts.push('"' + unquoted.replaceAll('"', '%22') + '"');
    }
}

Version

@projectwallace/format-css 3.1.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions