Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 56 additions & 26 deletions django/contrib/staticfiles/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,21 @@
from django.utils.functional import LazyObject
from django.utils.regex_helper import _lazy_re_compile

comment_re = _lazy_re_compile(r"\/\*[^*]*\*+([^/*][^*]*\*+)*\/", re.DOTALL)
line_comment_re = _lazy_re_compile(
r"\/\*[^*]*\*+([^/*][^*]*\*+)*\/|\/\/[^\n]*", re.DOTALL
_css_ignored_re = _lazy_re_compile(
r"/\*.*?\*/" # block comment
r"|\\." # escape sequence
r"|'(?:[^'\\\n]|\\.)*'" # single-quoted string
r'|"(?:[^"\\\n]|\\.)*"', # double-quoted string
re.DOTALL,
)
_js_ignored_re = _lazy_re_compile(
r"/\*.*?\*/" # block comment
r"|//[^\n]*" # line comment
r"|\\." # escape sequence
r"|'(?:[^'\\\n]|\\.)*'" # single-quoted string
r'|"(?:[^"\\\n]|\\.)*"' # double-quoted string
r"|`(?:[^`\\]|\\.)*`", # template literal
re.DOTALL,
)


Expand Down Expand Up @@ -60,25 +72,29 @@ class HashedFilesMixin:
(
(
r"""(?P<matched>import"""
r"""(?s:(?P<import>[\s\{].*?|\*\s*as\s*\w+))"""
r"""\s*from\s*['"](?P<url>[./].*?)["']\s*;)"""
r"""(?P<import>[\s\{][^;]*?|\*\s*as\s*\w+)"""
r"""\s*from\s*['"](?P<url>[./].*?)["'])"""
),
"""import%(import)s from "%(url)s";""",
"""import%(import)s from "%(url)s\"""",
_js_ignored_re,
),
(
(
r"""(?P<matched>export(?s:(?P<exports>[\s\{].*?))"""
r"""\s*from\s*["'](?P<url>[./].*?)["']\s*;)"""
r"""(?P<matched>export(?P<exports>[\s\{][^;]*?)"""
r"""\s*from\s*["'](?P<url>[./].*?)["'])"""
),
"""export%(exports)s from "%(url)s";""",
"""export%(exports)s from "%(url)s\"""",
_js_ignored_re,
),
(
r"""(?P<matched>import\s*['"](?P<url>[./].*?)["']\s*;)""",
"""import"%(url)s";""",
r"""(?P<matched>import\s*['"](?P<url>[./].*?)["'])""",
"""import"%(url)s\"""",
_js_ignored_re,
),
(
r"""(?P<matched>import\(["'](?P<url>.*?)["']\))""",
r"""(?P<matched>import\(["'](?P<url>[./].*?)["']\))""",
"""import("%(url)s")""",
_js_ignored_re,
),
),
)
Expand Down Expand Up @@ -107,6 +123,7 @@ class HashedFilesMixin:
(
r"(?m)^(?P<matched>//# (?-i:sourceMappingURL)=(?P<url>.*))$",
"//# sourceMappingURL=%(url)s",
_js_ignored_re,
),
),
),
Expand All @@ -122,11 +139,18 @@ def __init__(self, *args, **kwargs):
for extension, patterns in self.patterns:
for pattern in patterns:
if isinstance(pattern, (tuple, list)):
pattern, template = pattern
if len(pattern) == 3:
pattern, template, ignored_re = pattern
else:
pattern, template = pattern
ignored_re = _css_ignored_re
else:
template = self.default_template
ignored_re = _css_ignored_re
compiled = re.compile(pattern, re.IGNORECASE)
self._patterns.setdefault(extension, []).append((compiled, template))
self._patterns.setdefault(extension, []).append(
(compiled, template, ignored_re)
)

def file_hash(self, name, content=None):
"""
Expand Down Expand Up @@ -210,22 +234,24 @@ def url(self, name, force=False):
"""
return self._url(self.stored_name, name, force)

def get_comment_blocks(self, content, include_line_comments=False):
def get_ignored_blocks(self, content, pattern):
"""
Return a list of (start, end) tuples for each comment block.
Return a sorted list of (start, end) tuples for content that should
be ignored during URL rewriting based on the specified pattern:
e.g. block comments and string literals for CSS, plus line comments
(// ...) and template literals (`...`) for JS.
"""
pattern = line_comment_re if include_line_comments else comment_re
return [(match.start(), match.end()) for match in re.finditer(pattern, content)]

def is_in_comment(self, pos, comments):
for start, end in comments:
if start < pos and pos < end:
def is_in_ignored_block(self, pos, ignored_blocks):
for start, end in ignored_blocks:
if start < pos < end:
return True
if pos < start:
return False
return False

def url_converter(self, name, hashed_files, template=None, comment_blocks=None):
def url_converter(self, name, hashed_files, template=None, ignored_blocks=None):
"""
Return the custom URL converter for the given file name.
"""
Expand Down Expand Up @@ -253,8 +279,10 @@ def converter(matchobj):
matched = matches["matched"]
url = matches["url"]

# Ignore URLs in comments.
if comment_blocks and self.is_in_comment(matchobj.start(), comment_blocks):
# Ignore URLs in comments and string literals.
if ignored_blocks and self.is_in_ignored_block(
matchobj.start(), ignored_blocks
):
return matched

# Ignore absolute/protocol-relative and data-uri URLs.
Expand Down Expand Up @@ -414,14 +442,16 @@ def path_level(name):
yield name, None, exc, False
for extension, patterns in self._patterns.items():
if matches_patterns(path, (extension,)):
for pattern, template in patterns:
if not any(p.search(content) for p, _, _ in patterns):
continue
for pattern, template, ignored_re in patterns:
converter = self.url_converter(
name,
hashed_files,
template,
self.get_comment_blocks(
self.get_ignored_blocks(
content,
include_line_comments=path.endswith(".js"),
ignored_re,
),
)
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,12 @@ body {
body {
background: #d3d6d8 /* url("does.not.exist.png") */ url(/static/cached/img/relative.png) /*url("does.not.exist.either.png")*/;
}

body {
content: "url(non_exist.png)";
content: 'url(non_exist.png)';
}

/* Tailwind-style selector */
.tw\:bg-\[url\(\'non_exist.png\'\)\] { background: url(../img/relative.png); }
body { font-family: 'sans-serif'; }
30 changes: 30 additions & 0 deletions tests/staticfiles_tests/project/documents/cached/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
} from "./module_test.js";
import relativeModule from "../nested/js/nested.js";

// automatic semicolon insertion
import * as m from "./module_test.js"
import { testConst as alias } from "./module_test.js"

// Dynamic imports.
const dynamicModule = import("./module_test.js");

Expand All @@ -35,3 +39,29 @@ const dynamicModule = import("./module_test_missing.js");
// ignore line comments
// import testConst from "./module_test_missing.js";
// const dynamicModule = import("./module_test_missing.js");

// imports inside string literals should be ignored
const msg = 'import { foo } from "./module_test_missing.js";';
const help = "import { bar } from './module_test_missing.js';";
const tmpl = `import { baz } from "./module_test_missing.js";`;
const dyn = 'const x = import("./module_test_missing.js");';
const multiLine = `
import { baz } from "./module_test_missing.js";
`;

// an export without a from clause must not consume a subsequent import's from
export { testConst };
import { firstConst } from "./module_test.js";
// imports inside JSDoc block comments should be ignored even when a
// real import precedes them (guarding against (?s:.*?) cross-boundary matches)
import '../nested/js/nested.js';
/**
* @example
* import { something } from "./module_test_missing.js";
*/
function jsdocExample() {}

// bare specifier imports should not be rewritten
import rootConst from "@vendor/package";
import rootConst from "#utils";
const buildModule = import("@vendor/package");
Loading
Loading