You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			101 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			101 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			Python
		
	
"""Tornado handlers for dynamic theme loading."""
 | 
						|
 | 
						|
# Copyright (c) Jupyter Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
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,  # noqa: ARG002
 | 
						|
    ) -> None:
 | 
						|
        """Initialize the handler."""
 | 
						|
        # Get all of the available theme paths in order
 | 
						|
        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))
 | 
						|
 | 
						|
        # Add the core theme path last
 | 
						|
        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(  # type:ignore[override]
 | 
						|
        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)
 | 
						|
 | 
						|
        # Replace local paths with mangled paths.
 | 
						|
        # We only match strings that are local urls,
 | 
						|
        # e.g. `url('../foo.css')`, `url('images/foo.png')`
 | 
						|
        pattern = r"url\('(.*)'\)|url\('(.*)'\)"
 | 
						|
 | 
						|
        def replacer(m: Any) -> Any:
 | 
						|
            """Replace the matched relative url with the mangled url."""
 | 
						|
            group = m.group()
 | 
						|
            # Get the part that matched
 | 
						|
            part = next(g for g in m.groups() if g)
 | 
						|
 | 
						|
            # Ignore urls that start with `/` or have a protocol like `http`.
 | 
						|
            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")
 |