1"""Tornado handlers for dynamic theme loading."""
2
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
5
6import os
7import re
8from glob import glob
9from os import path as osp
10from urllib.parse import urlparse
11
12from .server import FileFindHandler
13from .server import url_path_join as ujoin
14
15
16class ThemesHandler(FileFindHandler):
17    """A file handler that mangles local urls in CSS files."""
18
19    def initialize(self, path, default_filename=None,
20                   no_cache_paths=None, themes_url=None, labextensions_path=None, **kwargs):
21
22        # Get all of the available theme paths in order
23        labextensions_path = labextensions_path or []
24        ext_paths = []
25        for ext_dir in labextensions_path:
26            theme_pattern = ext_dir + '/**/themes'
27            ext_paths.extend([path for path in glob(theme_pattern, recursive=True)])
28
29        # Add the core theme path last
30        if not isinstance(path, list):
31            path = [path]
32        path = ext_paths + path
33
34        FileFindHandler.initialize(self, path,
35                                   default_filename=default_filename,
36                                   no_cache_paths=no_cache_paths)
37        self.themes_url = themes_url
38
39    def get_content(self, abspath, start=None, end=None):
40        """Retrieve the content of the requested resource which is located
41        at the given absolute path.
42
43        This method should either return a byte string or an iterator
44        of byte strings.
45        """
46        base, ext = osp.splitext(abspath)
47        if ext != '.css':
48            return FileFindHandler.get_content(abspath, start, end)
49
50        return self._get_css()
51
52    def get_content_size(self):
53        """Retrieve the total size of the resource at the given path."""
54        base, ext = osp.splitext(self.absolute_path)
55        if ext != '.css':
56            return FileFindHandler.get_content_size(self)
57        else:
58            return len(self._get_css())
59
60    def _get_css(self):
61        """Get the mangled css file contents."""
62        with open(self.absolute_path, 'rb') as fid:
63            data = fid.read().decode('utf-8')
64
65        basedir = osp.dirname(self.path).replace(os.sep, '/')
66        basepath = ujoin(self.themes_url, basedir)
67
68        # Replace local paths with mangled paths.
69        # We only match strings that are local urls,
70        # e.g. `url('../foo.css')`, `url('images/foo.png')`
71        pattern = (r"url\('(.*)'\)|"
72                   r'url\("(.*)"\)')
73
74        def replacer(m):
75            """Replace the matched relative url with the mangled url."""
76            group = m.group()
77            # Get the part that matched
78            part = [g for g in m.groups() if g][0]
79
80            # Ignore urls that start with `/` or have a protocol like `http`.
81            parsed = urlparse(part)
82            if part.startswith('/') or parsed.scheme:
83                return group
84
85            return group.replace(part, ujoin(basepath, part))
86
87        return re.sub(pattern, replacer, data).encode('utf-8')
88