|
"""Tornado handlers for dynamic theme loading.""" |
|
|
|
|
|
|
|
from __future__ import annotations |
|
|
|
import os |
|
import re |
|
from glob import glob |
|
from typing import Any, Generator |
|
from urllib.parse import urlparse |
|
|
|
from jupyter_server.base.handlers import FileFindHandler |
|
from jupyter_server.utils import url_path_join as ujoin |
|
|
|
|
|
class ThemesHandler(FileFindHandler): |
|
"""A file handler that mangles local urls in CSS files.""" |
|
|
|
def initialize( |
|
self, |
|
path: str | list[str], |
|
default_filename: str | None = None, |
|
no_cache_paths: list[str] | None = None, |
|
themes_url: str | None = None, |
|
labextensions_path: list[str] | None = None, |
|
**kwargs: Any, |
|
) -> None: |
|
"""Initialize the handler.""" |
|
|
|
labextensions_path = labextensions_path or [] |
|
ext_paths: list[str] = [] |
|
for ext_dir in labextensions_path: |
|
theme_pattern = ext_dir + "/**/themes" |
|
ext_paths.extend(path for path in glob(theme_pattern, recursive=True)) |
|
|
|
|
|
if not isinstance(path, list): |
|
path = [path] |
|
path = ext_paths + path |
|
|
|
FileFindHandler.initialize( |
|
self, path, default_filename=default_filename, no_cache_paths=no_cache_paths |
|
) |
|
self.themes_url = themes_url |
|
|
|
def get_content( |
|
self, abspath: str, start: int | None = None, end: int | None = None |
|
) -> bytes | Generator[bytes, None, None]: |
|
"""Retrieve the content of the requested resource which is located |
|
at the given absolute path. |
|
|
|
This method should either return a byte string or an iterator |
|
of byte strings. |
|
""" |
|
base, ext = os.path.splitext(abspath) |
|
if ext != ".css": |
|
return FileFindHandler.get_content(abspath, start, end) |
|
|
|
return self._get_css() |
|
|
|
def get_content_size(self) -> int: |
|
"""Retrieve the total size of the resource at the given path.""" |
|
assert self.absolute_path is not None |
|
base, ext = os.path.splitext(self.absolute_path) |
|
if ext != ".css": |
|
return FileFindHandler.get_content_size(self) |
|
return len(self._get_css()) |
|
|
|
def _get_css(self) -> bytes: |
|
"""Get the mangled css file contents.""" |
|
assert self.absolute_path is not None |
|
with open(self.absolute_path, "rb") as fid: |
|
data = fid.read().decode("utf-8") |
|
|
|
if not self.themes_url: |
|
return b"" |
|
|
|
basedir = os.path.dirname(self.path).replace(os.sep, "/") |
|
basepath = ujoin(self.themes_url, basedir) |
|
|
|
|
|
|
|
|
|
pattern = r"url\('(.*)'\)|url\('(.*)'\)" |
|
|
|
def replacer(m: Any) -> Any: |
|
"""Replace the matched relative url with the mangled url.""" |
|
group = m.group() |
|
|
|
part = next(g for g in m.groups() if g) |
|
|
|
|
|
parsed = urlparse(part) |
|
if part.startswith("/") or parsed.scheme: |
|
return group |
|
|
|
return group.replace(part, ujoin(basepath, part)) |
|
|
|
return re.sub(pattern, replacer, data).encode("utf-8") |
|
|