From 7ec0d822e3aec801fa9b4a85da52b922a0be58c1 Mon Sep 17 00:00:00 2001
From: Radomir Dopieralski <openstack@dopieralski.pl>
Date: Thu, 21 Nov 2024 16:29:42 +0100
Subject: [PATCH] Fix #22 Use importer callbacks to handle static path prefixes

Instead of import paths, use the importer callback so we can handle
static paths prefixes, and possibly also non-filesystem storage.

This is more complicated than anticipated, because we have to re-create
the fallback mechanisms for handling partials and extensions, and
because finders.find ignores prefixes.

There is probably room for improvement, especially around the source
maps, but this is a start.

Note that the test_raw_css_import test had to be changed, because
the libsass doesn't seem to use @import when callbacks are used.
---
 django_libsass.py        | 45 +++++++++++++++++++++++++++++++++++++++-
 tests/tests/test_sass.py |  2 +-
 2 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/django_libsass.py b/django_libsass.py
index 12c74b5..8ddf692 100644
--- a/django_libsass.py
+++ b/django_libsass.py
@@ -30,6 +30,49 @@ def static(path):
 INCLUDE_PATHS = None  # populate this on first call to 'get_include_paths'
 
 
+def importer(path, prev):
+    """
+    A callback for handling included files.
+    """
+    if path.startswith('/'):
+        # An absolute path was used, don't try relative paths.
+        candidates = [path[1:]]
+    elif prev == 'stdin':
+        # The parent is STDIN, so only try absolute paths.
+        candidates = [path]
+    else:
+        # Try both relative and absolute paths, prefer relative.
+        candidates = [
+            os.path.normpath(os.path.join(os.path.dirname(prev), path)),
+            path,
+        ]
+    # Try adding _ in front of the file for partials.
+    for candidate in candidates[:]:
+        if '/' in candidate:
+            candidates.insert(0, '/_'.join(candidate.rsplit('/', 1)))
+        else:
+            candidates.insert(0, '_' + candidate)
+    # Try adding extensions.
+    for candidate in candidates[:]:
+        for ext in ['.scss', '.sass', '.css']:
+            candidates.append(candidate + ext)
+    for finder in get_finders():
+        # We can't use finder.find() because we need the prefixes.
+        for storage_filename, storage in finder.list([]):
+            prefix = getattr(storage, "prefix", "")
+            filename = os.path.join(prefix, storage_filename)
+            if filename in candidates:
+                return [(filename, storage.open(storage_filename).read())]
+    # Additional includes are just regular directories.
+    for directory in ADDITIONAL_INCLUDE_PATHS:
+        for candidate in candidates:
+            filename = os.path.join(directory, candidate)
+            if os.path.exists(filename):
+                return [(candidate, open(filename).read())]
+    # Nothing was found.
+    return None
+
+
 def get_include_paths():
     """
     Generate a list of include paths that libsass should use to find files
@@ -110,11 +153,11 @@ def compile(**kwargs):
     kwargs = kwargs.copy()
     if PRECISION is not None:
         kwargs['precision'] = PRECISION
-    kwargs['include_paths'] = (kwargs.get('include_paths') or []) + get_include_paths()
 
     custom_functions = CUSTOM_FUNCTIONS.copy()
     custom_functions.update(kwargs.get('custom_functions', {}))
     kwargs['custom_functions'] = custom_functions
+    kwargs['importers'] = [(0, importer)]
 
     if SOURCEMAPS and kwargs.get('filename', None):
         # We need to pass source_map_file to libsass so it generates
diff --git a/tests/tests/test_sass.py b/tests/tests/test_sass.py
index 0a44589..3df4923 100644
--- a/tests/tests/test_sass.py
+++ b/tests/tests/test_sass.py
@@ -19,7 +19,7 @@ def test_extra_include_path(self):
 
     def test_raw_css_import(self):
         result = compile(filename=os.path.join(settings.BASE_DIR, 'tests', 'static', 'css', 'with_raw_css_import.scss'))
-        self.assertIn('@import url(raw1.css);', result)
+        self.assertIn('.raw-style-1', result)
         self.assertIn('.raw-style-2', result)
 
     def test_static_function(self):