diff --git a/pipeline/compressors/__init__.py b/pipeline/compressors/__init__.py index fb1cd738..fe749500 100644 --- a/pipeline/compressors/__init__.py +++ b/pipeline/compressors/__init__.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import base64 import os import posixpath import re import subprocess +import warnings from itertools import takewhile +from typing import Iterator, Optional, Sequence from django.contrib.staticfiles.storage import staticfiles_storage from django.utils.encoding import force_str, smart_bytes @@ -12,8 +16,57 @@ from pipeline.exceptions import CompressorError from pipeline.utils import relpath, set_std_streams_blocking, to_class -URL_DETECTOR = r"""url\((['"]?)\s*(.*?)\1\)""" -URL_REPLACER = r"""url\(__EMBED__(.+?)(\?\d+)?\)""" +# Regex matching url(...), url('...'), and url("...") patterns. +# +# Replacements will preserve the quotes and any whitespace contained within +# the pattern, transforming only the filename. +# +# Verbose and documented, to ease future maintenance. +_CSS_URL_REWRITE_PATH_RE_STR = r""" + (?P + url\( # The opening `url(`. + (?P['"]?) # Optional quote (' or "). + \s* + ) + (?P.*?) # The path to capture. + (?P + (?P=url_quote) # The quote found earlier, if any. + \s* + \) # The end `)`, completing `url(...)`. + ) +""" + + +# Regex matching `//@ sourceMappingURL=...` and variants. +# +# This will capture sourceMappingURL and sourceURL keywords, both +# `//@` and `//#` variants, and both `//` and `/* ... */` comment types. +# +# Verbose and documented, to ease future maintenance. +_SOURCEMAP_REWRITE_PATH_RE_STR = r""" + (?P + /(?:/|(?P\*)) # Opening comment (`//#`, `//@`, + [#@]\s+ # `/*@`, `/*#`). + source(?:Mapping)?URL= # The sourcemap indicator. + \s* + ) + (?P.*?) # The path to capture. + (?P + \s* + (?(sourcemap_mlcomment)\*/\s*) # End comment (`*/`) + ) + $ # The line should now end. +""" + + +# Implementation of the above regexes, for CSS and JavaScript. +CSS_REWRITE_PATH_RE = re.compile( + f"{_CSS_URL_REWRITE_PATH_RE_STR}|{_SOURCEMAP_REWRITE_PATH_RE_STR}", re.X | re.M +) +JS_REWRITE_PATH_RE = re.compile(_SOURCEMAP_REWRITE_PATH_RE_STR, re.X | re.M) + + +URL_REPLACER = re.compile(r"""url\(__EMBED__(.+?)(\?\d+)?\)""") NON_REWRITABLE_URL = re.compile(r"^(#|http:|https:|data:|//)") DEFAULT_TEMPLATE_FUNC = "template" @@ -51,9 +104,27 @@ def js_compressor(self): def css_compressor(self): return to_class(settings.CSS_COMPRESSOR) - def compress_js(self, paths, templates=None, **kwargs): + def compress_js( + self, + paths: Sequence[str], + templates: Optional[Sequence[str]] = None, + *, + output_filename: Optional[str] = None, + **kwargs, + ) -> str: """Concatenate and compress JS files""" - js = self.concatenate(paths) + # Note how a semicolon is added between the two files to make sure that + # their behavior is not changed. '(expression1)\n(expression2)' calls + # `expression1` with `expression2` as an argument! Superfluous + # semicolons are valid in JavaScript and will be removed by the + # minifier. + js = self.concatenate( + paths, + file_sep=";", + output_filename=output_filename, + rewrite_path_re=JS_REWRITE_PATH_RE, + ) + if templates: js = js + self.compile_templates(templates) @@ -68,7 +139,13 @@ def compress_js(self, paths, templates=None, **kwargs): def compress_css(self, paths, output_filename, variant=None, **kwargs): """Concatenate and compress CSS files""" - css = self.concatenate_and_rewrite(paths, output_filename, variant) + css = self.concatenate( + paths, + file_sep="", + rewrite_path_re=CSS_REWRITE_PATH_RE, + output_filename=output_filename, + variant=variant, + ) compressor = self.css_compressor if compressor: css = getattr(compressor(verbose=self.verbose), "compress_css")(css) @@ -131,38 +208,116 @@ def template_name(self, path, base): def concatenate_and_rewrite(self, paths, output_filename, variant=None): """Concatenate together files and rewrite urls""" - stylesheets = [] - for path in paths: + warnings.warn( + "Compressor.concatenate_and_rewrite() is deprecated. Please " + "call concatenate() instead.", + DeprecationWarning, + stacklevel=2, + ) - def reconstruct(match): - quote = match.group(1) or "" - asset_path = match.group(2) - if NON_REWRITABLE_URL.match(asset_path): - return f"url({quote}{asset_path}{quote})" - asset_url = self.construct_asset_path( - asset_path, path, output_filename, variant + return self.concatenate( + paths=paths, + file_sep="", + rewrite_path_re=CSS_REWRITE_PATH_RE, + output_filename=output_filename, + variant=variant, + ) + + def concatenate( + self, + paths: Sequence[str], + *, + file_sep: Optional[str] = None, + output_filename: Optional[str] = None, + rewrite_path_re: Optional[re.Pattern] = None, + variant: Optional[str] = None, + ) -> str: + """Concatenate together a list of files. + + The caller can specify a delimiter between files and any regexes + used to normalize relative paths. Path normalization is important for + ensuring that local resources or sourcemaps can be updated in time + for Django's static media post-processing phase. + """ + + def _reconstruct( + m: re.Match, + source_path: str, + ) -> str: + groups = m.groupdict() + asset_path: Optional[str] = None + prefix = "" + suffix = "" + + for prefix in ("sourcemap", "url"): + asset_path = groups.get(f"{prefix}_path") + + if asset_path is not None: + asset_path = asset_path.strip() + prefix, suffix = m.group(f"{prefix}_prefix", f"{prefix}_suffix") + break + + if asset_path is None: + # This is empty. Return the whole match as-is. + return m.group() + + if asset_path and not NON_REWRITABLE_URL.match(asset_path): + asset_path = self.construct_asset_path( + asset_path=asset_path, + source_path=source_path, + output_filename=output_filename, + variant=variant, ) - return f"url({asset_url})" - content = self.read_text(path) - # content needs to be unicode to avoid explosions with non-ascii chars - content = re.sub(URL_DETECTOR, reconstruct, content) - stylesheets.append(content) - return "\n".join(stylesheets) + return f"{prefix}{asset_path}{suffix}" + + def _iter_files() -> Iterator[str]: + if not output_filename or not rewrite_path_re: + # This is legacy call, which does not support sourcemap-aware + # asset rewriting. Pipeline itself won't invoke this outside + # of tests, but it maybe important for third-parties who + # are specializing these classes. + warnings.warn( + "Compressor.concatenate() was called without passing " + "rewrite_path_re_= or output_filename=. If you are " + "specializing Compressor, please update your call " + "to remain compatible with future changes.", + DeprecationWarning, + stacklevel=3, + ) - def concatenate(self, paths): - """Concatenate together a list of files""" - # Note how a semicolon is added between the two files to make sure that - # their behavior is not changed. '(expression1)\n(expression2)' calls - # `expression1` with `expression2` as an argument! Superfluos semicolons - # are valid in JavaScript and will be removed by the minifier. - return "\n;".join([self.read_text(path) for path in paths]) + return (self.read_text(path) for path in paths) + + # Now that we can attempt the modern support for concatenating + # files, handling rewriting of relative assets in the process. + return ( + rewrite_path_re.sub( + lambda m: _reconstruct(m, path), self.read_text(path) + ) + for path in paths + ) + + if file_sep is None: + warnings.warn( + "Compressor.concatenate() was called without passing " + "file_sep=. If you are specializing Compressor, please " + "update your call to remain compatible with future changes. " + "Defaulting to JavaScript behavior for " + "backwards-compatibility.", + DeprecationWarning, + stacklevel=2, + ) + file_sep = ";" + + return f"\n{file_sep}".join(_iter_files()) - def construct_asset_path(self, asset_path, css_path, output_filename, variant=None): - """Return a rewritten asset URL for a stylesheet""" + def construct_asset_path( + self, asset_path, source_path, output_filename, variant=None + ): + """Return a rewritten asset URL for a stylesheet or JavaScript file.""" public_path = self.absolute_path( asset_path, - os.path.dirname(css_path).replace("\\", "/"), + os.path.dirname(source_path).replace("\\", "/"), ) if self.embeddable(public_path, variant): return "__EMBED__%s" % public_path @@ -196,7 +351,7 @@ def datauri(match): data = self.encoded_content(path) return f'url("data:{mime_type};charset=utf-8;base64,{data}")' - return re.sub(URL_REPLACER, datauri, css) + return URL_REPLACER.sub(datauri, css) def encoded_content(self, path): """Return the base64 encoded contents""" diff --git a/pipeline/packager.py b/pipeline/packager.py index f6c4a3ef..20f34197 100644 --- a/pipeline/packager.py +++ b/pipeline/packager.py @@ -152,6 +152,7 @@ def pack_javascripts(self, package, **kwargs): package, self.compressor.compress_js, js_compressed, + output_filename=package.output_filename, templates=package.templates, **kwargs, ) diff --git a/tests/assets/css/sourcemap.css b/tests/assets/css/sourcemap.css new file mode 100644 index 00000000..30e87e8b --- /dev/null +++ b/tests/assets/css/sourcemap.css @@ -0,0 +1,24 @@ +div { + display: inline; +} + +span { + display: block; +} + + +//# sourceMappingURL=sourcemap1.css.map + +//@ sourceMappingURL=sourcemap2.css.map + +/*# sourceMappingURL=sourcemap3.css.map */ + +/*@ sourceMappingURL=sourcemap4.css.map */ + +//# sourceURL=sourcemap5.css.map + +//@ sourceURL=sourcemap6.css.map + +/*# sourceURL=sourcemap7.css.map */ + +/*@ sourceURL=sourcemap8.css.map */ diff --git a/tests/assets/js/sourcemap.js b/tests/assets/js/sourcemap.js new file mode 100644 index 00000000..4d609994 --- /dev/null +++ b/tests/assets/js/sourcemap.js @@ -0,0 +1,18 @@ +const abc = 123; + + +//# sourceMappingURL=sourcemap1.js.map + +//@ sourceMappingURL=sourcemap2.js.map + +/*# sourceMappingURL=sourcemap3.js.map */ + +/*@ sourceMappingURL=sourcemap4.js.map */ + +//# sourceURL=sourcemap5.js.map + +//@ sourceURL=sourcemap6.js.map + +/*# sourceURL=sourcemap7.js.map */ + +/*@ sourceURL=sourcemap8.js.map */ diff --git a/tests/tests/test_compressor.py b/tests/tests/test_compressor.py index fc5ce538..0a7c7ccd 100644 --- a/tests/tests/test_compressor.py +++ b/tests/tests/test_compressor.py @@ -14,7 +14,13 @@ from django.test.client import RequestFactory from pipeline.collector import default_collector -from pipeline.compressors import TEMPLATE_FUNC, Compressor, SubProcessCompressor +from pipeline.compressors import ( + CSS_REWRITE_PATH_RE, + JS_REWRITE_PATH_RE, + TEMPLATE_FUNC, + Compressor, + SubProcessCompressor, +) from pipeline.compressors.yuglify import YuglifyCompressor from tests.utils import _, pipeline_settings @@ -160,22 +166,25 @@ def test_construct_asset_path(self): ) self.assertEqual(asset_path, "/images/sprite.png") - def test_url_rewrite(self): - output = self.compressor.concatenate_and_rewrite( + def test_concatenate_with_url_rewrite(self) -> None: + output = self.compressor.concatenate( [ _("pipeline/css/urls.css"), ], - "css/screen.css", + file_sep="", + output_filename="css/screen.css", + rewrite_path_re=CSS_REWRITE_PATH_RE, ) + self.assertEqual( """.embedded-url-svg { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E% 3C/svg%3E"); } @font-face { font-family: 'Pipeline'; - src: url(../pipeline/fonts/pipeline.eot); - src: url(../pipeline/fonts/pipeline.eot?#iefix) format('embedded-opentype'); - src: local('☺'), url(../pipeline/fonts/pipeline.woff) format('woff'), url(../pipeline/fonts/pipeline.ttf) format('truetype'), url(../pipeline/fonts/pipeline.svg#IyfZbseF) format('svg'); + src: url('../pipeline/fonts/pipeline.eot'); + src: url('../pipeline/fonts/pipeline.eot?#iefix') format('embedded-opentype'); + src: local('☺'), url('../pipeline/fonts/pipeline.woff') format('woff'), url('../pipeline/fonts/pipeline.ttf') format('truetype'), url('../pipeline/fonts/pipeline.svg#IyfZbseF') format('svg'); font-weight: normal; font-style: normal; } @@ -202,13 +211,272 @@ def test_url_rewrite(self): output, ) - def test_url_rewrite_data_uri(self): - output = self.compressor.concatenate_and_rewrite( + def test_concatenate_with_url_rewrite_data_uri(self): + output = self.compressor.concatenate( [ _("pipeline/css/nested/nested.css"), ], - "pipeline/screen.css", + file_sep="", + output_filename="pipeline/screen.css", + rewrite_path_re=CSS_REWRITE_PATH_RE, + ) + + self.assertEqual( + """.data-url { + background-image: url(data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22iso-8859-1%22%3F%3E%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%3Csvg%20version%3D%221.1%22%20id%3D%22Layer_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%20%20width%3D%2212px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2012%2014%22%20style%3D%22enable-background%3Anew%200%200%2012%2014%3B%22%20xml%3Aspace%3D%22preserve%22%3E%3Cpath%20d%3D%22M11%2C6V5c0-2.762-2.239-5-5-5S1%2C2.238%2C1%2C5v1H0v8h12V6H11z%20M6.5%2C9.847V12h-1V9.847C5.207%2C9.673%2C5%2C9.366%2C5%2C9%20c0-0.553%2C0.448-1%2C1-1s1%2C0.447%2C1%2C1C7%2C9.366%2C6.793%2C9.673%2C6.5%2C9.847z%20M9%2C6H3V5c0-1.657%2C1.343-3%2C3-3s3%2C1.343%2C3%2C3V6z%22%2F%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E); +} +.data-url-quoted { + background-image: url('data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22iso-8859-1%22%3F%3E%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%3Csvg%20version%3D%221.1%22%20id%3D%22Layer_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%20%20width%3D%2212px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2012%2014%22%20style%3D%22enable-background%3Anew%200%200%2012%2014%3B%22%20xml%3Aspace%3D%22preserve%22%3E%3Cpath%20d%3D%22M11%2C6V5c0-2.762-2.239-5-5-5S1%2C2.238%2C1%2C5v1H0v8h12V6H11z%20M6.5%2C9.847V12h-1V9.847C5.207%2C9.673%2C5%2C9.366%2C5%2C9%20c0-0.553%2C0.448-1%2C1-1s1%2C0.447%2C1%2C1C7%2C9.366%2C6.793%2C9.673%2C6.5%2C9.847z%20M9%2C6H3V5c0-1.657%2C1.343-3%2C3-3s3%2C1.343%2C3%2C3V6z%22%2F%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E'); +} +""", # noqa + output, + ) + + def test_concatenate_css_with_sourcemap(self) -> None: + output = self.compressor.concatenate( + [ + _("pipeline/css/sourcemap.css"), + ], + file_sep="", + output_filename="css/sourcemap-bundle.css", + rewrite_path_re=CSS_REWRITE_PATH_RE, + ) + + self.assertEqual( + output, + "div {\n" + " display: inline;\n" + "}\n" + "\n" + "span {\n" + " display: block;\n" + "}\n" + "\n" + "\n" + "//# sourceMappingURL=../pipeline/css/sourcemap1.css.map\n" + "\n" + "//@ sourceMappingURL=../pipeline/css/sourcemap2.css.map \n" + "\n" + "/*# sourceMappingURL=../pipeline/css/sourcemap3.css.map */\n" + "\n" + "/*@ sourceMappingURL=../pipeline/css/sourcemap4.css.map */\n" + "\n" + "//# sourceURL=../pipeline/css/sourcemap5.css.map\n" + "\n" + "//@ sourceURL=../pipeline/css/sourcemap6.css.map \n" + "\n" + "/*# sourceURL=../pipeline/css/sourcemap7.css.map */\n" + "\n" + "/*@ sourceURL=../pipeline/css/sourcemap8.css.map */\n", + ) + + def test_concatenate_js_with_sourcemap(self) -> None: + output = self.compressor.concatenate( + [ + _("pipeline/js/sourcemap.js"), + ], + file_sep=";", + output_filename="js/sourcemap-bundle.js", + rewrite_path_re=JS_REWRITE_PATH_RE, + ) + + self.assertEqual( + output, + "const abc = 123;\n" + "\n" + "\n" + "//# sourceMappingURL=../pipeline/js/sourcemap1.js.map\n" + "\n" + "//@ sourceMappingURL=../pipeline/js/sourcemap2.js.map \n" + "\n" + "/*# sourceMappingURL=../pipeline/js/sourcemap3.js.map */\n" + "\n" + "/*@ sourceMappingURL=../pipeline/js/sourcemap4.js.map */\n" + "\n" + "//# sourceURL=../pipeline/js/sourcemap5.js.map\n" + "\n" + "//@ sourceURL=../pipeline/js/sourcemap6.js.map \n" + "\n" + "/*# sourceURL=../pipeline/js/sourcemap7.js.map */\n" + "\n" + "/*@ sourceURL=../pipeline/js/sourcemap8.js.map */\n", + ) + + def test_concatenate_without_rewrite_path_re(self) -> None: + message = ( + "Compressor.concatenate() was called without passing " + "rewrite_path_re_= or output_filename=. If you are " + "specializing Compressor, please update your call " + "to remain compatible with future changes." + ) + + with self.assertWarnsMessage(DeprecationWarning, message): + output = self.compressor.concatenate( + [ + _("pipeline/js/sourcemap.js"), + ], + file_sep=";", + output_filename="js/sourcemap-bundle.js", + ) + + self.assertEqual( + output, + "const abc = 123;\n" + "\n" + "\n" + "//# sourceMappingURL=sourcemap1.js.map\n" + "\n" + "//@ sourceMappingURL=sourcemap2.js.map \n" + "\n" + "/*# sourceMappingURL=sourcemap3.js.map */\n" + "\n" + "/*@ sourceMappingURL=sourcemap4.js.map */\n" + "\n" + "//# sourceURL=sourcemap5.js.map\n" + "\n" + "//@ sourceURL=sourcemap6.js.map \n" + "\n" + "/*# sourceURL=sourcemap7.js.map */\n" + "\n" + "/*@ sourceURL=sourcemap8.js.map */\n", + ) + + def test_concatenate_without_output_filename(self) -> None: + message = ( + "Compressor.concatenate() was called without passing " + "rewrite_path_re_= or output_filename=. If you are " + "specializing Compressor, please update your call " + "to remain compatible with future changes." + ) + + with self.assertWarnsMessage(DeprecationWarning, message): + output = self.compressor.concatenate( + [ + _("pipeline/js/sourcemap.js"), + ], + file_sep=";", + rewrite_path_re=JS_REWRITE_PATH_RE, + ) + + self.assertEqual( + output, + "const abc = 123;\n" + "\n" + "\n" + "//# sourceMappingURL=sourcemap1.js.map\n" + "\n" + "//@ sourceMappingURL=sourcemap2.js.map \n" + "\n" + "/*# sourceMappingURL=sourcemap3.js.map */\n" + "\n" + "/*@ sourceMappingURL=sourcemap4.js.map */\n" + "\n" + "//# sourceURL=sourcemap5.js.map\n" + "\n" + "//@ sourceURL=sourcemap6.js.map \n" + "\n" + "/*# sourceURL=sourcemap7.js.map */\n" + "\n" + "/*@ sourceURL=sourcemap8.js.map */\n", + ) + + def test_concatenate_without_file_sep(self) -> None: + message = ( + "Compressor.concatenate() was called without passing " + "file_sep=. If you are specializing Compressor, please " + "update your call to remain compatible with future changes. " + "Defaulting to JavaScript behavior for " + "backwards-compatibility." + ) + + with self.assertWarnsMessage(DeprecationWarning, message): + output = self.compressor.concatenate( + [ + _("pipeline/js/first.js"), + _("pipeline/js/second.js"), + ], + output_filename="js/sourcemap-bundle.js", + rewrite_path_re=JS_REWRITE_PATH_RE, + ) + + self.assertEqual( + output, + "(function() {\n" + " window.concat = function() {\n" + " console.log(arguments);\n" + " }\n" + "}()) // No semicolon\n" + "\n" + ";(function() {\n" + " window.cat = function() {\n" + ' console.log("hello world");\n' + " }\n" + "}());\n", + ) + + def test_legacy_concatenate_and_rewrite(self) -> None: + message = ( + "Compressor.concatenate_and_rewrite() is deprecated. Please " + "call concatenate() instead." + ) + + with self.assertWarnsMessage(DeprecationWarning, message): + output = self.compressor.concatenate_and_rewrite( + [ + _("pipeline/css/urls.css"), + ], + "css/screen.css", + ) + + self.assertEqual( + """.embedded-url-svg { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E% 3C/svg%3E"); +} +@font-face { + font-family: 'Pipeline'; + src: url('../pipeline/fonts/pipeline.eot'); + src: url('../pipeline/fonts/pipeline.eot?#iefix') format('embedded-opentype'); + src: local('☺'), url('../pipeline/fonts/pipeline.woff') format('woff'), url('../pipeline/fonts/pipeline.ttf') format('truetype'), url('../pipeline/fonts/pipeline.svg#IyfZbseF') format('svg'); + font-weight: normal; + font-style: normal; +} +.relative-url { + background-image: url(../pipeline/images/sprite-buttons.png); +} +.relative-url-querystring { + background-image: url(../pipeline/images/sprite-buttons.png?v=1.0#foo=bar); +} +.absolute-url { + background-image: url(/images/sprite-buttons.png); +} +.absolute-full-url { + background-image: url(http://localhost/images/sprite-buttons.png); +} +.no-protocol-url { + background-image: url(//images/sprite-buttons.png); +} +.anchor-tag-url { + background-image: url(#image-gradient); +} +@font-face{src:url(../pipeline/fonts/pipeline.eot);src:url(../pipeline/fonts/pipeline.eot?#iefix) format('embedded-opentype'),url(../pipeline/fonts/pipeline.woff) format('woff'),url(../pipeline/fonts/pipeline.ttf) format('truetype');} +""", # noqa + output, ) + + def test_legacy_concatenate_and_rewrite_with_data_uri(self) -> None: + message = ( + "Compressor.concatenate_and_rewrite() is deprecated. Please " + "call concatenate() instead." + ) + + with self.assertWarnsMessage(DeprecationWarning, message): + output = self.compressor.concatenate_and_rewrite( + [ + _("pipeline/css/nested/nested.css"), + ], + "pipeline/screen.css", + ) + self.assertEqual( """.data-url { background-image: url(data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22iso-8859-1%22%3F%3E%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%3Csvg%20version%3D%221.1%22%20id%3D%22Layer_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%20%20width%3D%2212px%22%20height%3D%2214px%22%20viewBox%3D%220%200%2012%2014%22%20style%3D%22enable-background%3Anew%200%200%2012%2014%3B%22%20xml%3Aspace%3D%22preserve%22%3E%3Cpath%20d%3D%22M11%2C6V5c0-2.762-2.239-5-5-5S1%2C2.238%2C1%2C5v1H0v8h12V6H11z%20M6.5%2C9.847V12h-1V9.847C5.207%2C9.673%2C5%2C9.366%2C5%2C9%20c0-0.553%2C0.448-1%2C1-1s1%2C0.447%2C1%2C1C7%2C9.366%2C6.793%2C9.673%2C6.5%2C9.847z%20M9%2C6H3V5c0-1.657%2C1.343-3%2C3-3s3%2C1.343%2C3%2C3V6z%22%2F%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3Cg%3E%3C%2Fg%3E%3C%2Fsvg%3E);