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