API Reference

mkdocs_rsoxs

mkdocs_rsoxs package.

__version__ = '0.1.3' module-attribute

extensions

codexec

CodexecBlock

Bases: Block

on_create(parent)

Called when a block is initially found and initialized. The on_create method should create the container for the block under the parent element. Other child elements can be created on the root of the container, but outer element of the created container should be returned.

Source code in src/mkdocs_rsoxs/extensions/codexec.py
def on_create(self, parent: etree.Element):
    """Called when a block is initially found and initialized.
    The on_create method should create the container for the
    block under the parent element. Other child elements can
    be created on the root of the container, but outer element
    of the created container should be returned.
    """
    div = etree.SubElement(parent, "div")
    div.set("class", "codexec")
    return div
on_end(block)

When a block is parsed to completion, the on_end event is executed. This allows an extension to perform any post processing on the elements. You could save the data as raw text and then parse it special at the end or you could walk the HTML elements and move content around, add attributes, or whatever else is needed.

Source code in src/mkdocs_rsoxs/extensions/codexec.py
def on_end(self, block: etree.Element) -> None:
    """When a block is parsed to completion, the on_end event is
    executed. This allows an extension to perform any post
    processing on the elements. You could save the data as raw
    text and then parse it special at the end or you could walk
    the HTML elements and move content around, add attributes, or
    whatever else is needed."""

    code = block.find(".//code")
    if code is None:
        log.warning(
            "CodexecBlock: No <code> element found in block. "
            "This block will not be rendered."
        )
        return

    regex = re.compile(r"^:::([a-zA-Z0-9_]+)\s*")
    content = (code.text or "").strip(" \n\t")
    m = regex.match(content)
    if m:
        # Extract the language from the first line
        self.lang = m.group(1)
        content = content[m.end() :].strip(" \n\t")

    content = re.sub(
        r"(?<!\\)\\(.)",
        r"\\\\\1",
        content,
    )  # Escape single backslashes

    # run button
    button = etree.SubElement(block, "button", {"class": "ghost icon"})
    callback = """({output, ok}) => {
        const result = this.parentElement.getElementsByClassName('codexec-result')[0];
        if (result) {
            if (ok === false) {
                result.classList.add('error');
            } else {
                result.classList.remove('error');
            }
            const pre = result.querySelector('pre');
            if (pre) {
                pre.innerHTML = output;
            }
        }
        this.dataset.status = '';
    }
    """
    button.set("data-status", "")
    button.set(
        "onclick",
        f"""this.dataset.status='loading';runCodexec(`{content}`,'{self.lang}').then({callback})""",
    )
    icon = etree.SubElement(button, "svg")
    icon.set("class", "play")
    icon.set("xmlns", "http://www.w3.org/2000/svg")
    icon.set("viewBox", "0 0 24 24")
    icon.set("fill", "currentColor")
    path = etree.SubElement(icon, "path")
    path.set("fill-rule", "evenodd")
    path.set(
        "d",
        "M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z",
    )

    # <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle-icon lucide-loader-circle"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
    icon = etree.SubElement(button, "svg")
    icon.set("class", "loading animate-spin")
    icon.set("xmlns", "http://www.w3.org/2000/svg")
    icon.set("viewBox", "0 0 24 24")
    icon.set("stroke", "currentColor")
    icon.set("stroke-width", "2")
    icon.set("stroke-linecap", "round")
    icon.set("stroke-linejoin", "round")
    icon.set("fill", "none")
    path = etree.SubElement(icon, "path")
    path.set(
        "d",
        "M21 12a9 9 0 1 1-6.219-8.56",
    )

    # add result container
    result = etree.SubElement(block, "div")
    result.set("class", "codexec-result")
    etree.SubElement(result, "pre")
makeExtension(*args, **kwargs)

Return extension.

Source code in src/mkdocs_rsoxs/extensions/codexec.py
def makeExtension(*args, **kwargs):
    """Return extension."""
    return CodexecBlockExtension(*args, **kwargs)

echarts

alpha
EchartsBlock

Bases: Block

on_create(parent)

Called when a block is initially found and initialized. The on_create method should create the container for the block under the parent element. Other child elements can be created on the root of the container, but outer element of the created container should be returned.

Source code in src/mkdocs_rsoxs/extensions/echarts/alpha.py
def on_create(self, parent: etree.Element):  # type: ignore
    """Called when a block is initially found and initialized.
    The on_create method should create the container for the
    block under the parent element. Other child elements can
    be created on the root of the container, but outer element
    of the created container should be returned.
    """

    # generate a random id if not provided
    log.debug("echarts block found")
    self.div_id = self.attrs.get("id", generate_random_string(6))
    div = etree.SubElement(parent, "div")
    return div
on_add(block)

When any calls occur to process new content, on_add is called. This gives the block a chance to return the element where the content is desired.

Source code in src/mkdocs_rsoxs/extensions/echarts/alpha.py
def on_add(self, block: etree.Element):
    """When any calls occur to process new content, on_add is called.
    This gives the block a chance to return the element where the
    content is desired."""

    # save the raw content in all cases
    self.raw_content = block.text or ""
    # ignore empty block
    if block.text is None:
        return block
    # check if it looks like a json object
    start = block.text.find("{")
    end = block.text.rfind("}")
    if start == -1 or end == -1:
        return block

    log.debug("echarts options can be extracted from the block")
    self.echarts_options = block.text[start : end + 1]
    # remove the text (otherwise the raw text will be displayed)
    block.text = ""
    return block
on_end(block)

When a block is parsed to completion, the on_end event is executed. This allows an extension to perform any post processing on the elements. You could save the data as raw text and then parse it special at the end or you could walk the HTML elements and move content around, add attributes, or whatever else is needed.

Source code in src/mkdocs_rsoxs/extensions/echarts/alpha.py
def on_end(self, block: etree.Element) -> None:
    """When a block is parsed to completion, the on_end event is
    executed. This allows an extension to perform any post
    processing on the elements. You could save the data as raw
    text and then parse it special at the end or you could walk
    the HTML elements and move content around, add attributes, or
    whatever else is needed."""
    log.debug(f"content of echarts block:\n{self.raw_content}")
    if self.echarts_options == "":
        log.warning(
            f"No echarts options found in the following block:"
            f"\n---\n{self.raw_content}\n---\n"
        )
        return

    script = etree.SubElement(block, "script")
    script.set("type", "text/javascript")
    script.set("defer", "true")

    script.text = f"""
    const target{self.div_id} = document.getElementById('{self.div_id}');
    const chart{self.div_id} = echarts.init(target{self.div_id}, 'shadcn', {{ renderer: '{self.renderer}' }});
    chart{self.div_id}.setOption({self.echarts_options});
    window.addEventListener('resize', function() {{ chart{self.div_id}.resize(); }});
    const observer{self.div_id} = new IntersectionObserver((entries) => {{
        entries.forEach(entry => {{
            if (entry.isIntersecting) {{
                chart{self.div_id}.resize();
                observer{self.div_id}.disconnect(); // Stop observing if you want a one-time trigger
            }}
        }});
    }});

    observer{self.div_id}.observe(target{self.div_id});
    """

    # set some attributes if not defined by the user
    block.set("id", self.div_id)
    block.set("class", block.get("class", "echarts"))
    block.set("style", block.get("style", "width:100%;height:500px;"))

    self.echarts_options = ""
    self.div_id = ""
on_markdown()

Check how element should be treated by the Markdown parser.

Source code in src/mkdocs_rsoxs/extensions/echarts/alpha.py
def on_markdown(self) -> str:  # type: ignore
    """Check how element should be treated by the Markdown parser."""
    return "raw"
makeExtension(*args, **kwargs)

Return extension.

Source code in src/mkdocs_rsoxs/extensions/echarts/alpha.py
def makeExtension(*args, **kwargs):
    """Return extension."""
    return EchartsBlockExtension(*args, **kwargs)

hover_card

HoverCardProcessor

Bases: InlineProcessor

Matches [trigger]^[hover card content] syntax.

filters

parse_author(site_author)

Returns the email address of the site author.

Source code in src/mkdocs_rsoxs/filters.py
def parse_author(site_author: str) -> Union[str, None]:
    """Returns the email address of the site author."""
    # parse thinks like "Alban Siffer <31479857+asiffer@users.noreply.github.com>"
    if "<" in site_author and ">" in site_author:
        chunks = site_author.split("<")
        email = chunks[-1].split(">")[0]
        name = chunks[0].strip()
    else:
        email = None
        name = site_author.strip()

    if email:
        return f'<a href="mailto:{email}">{name}</a>'
    return f"<span>{name}</span>"

active_section(nav)

Return the top-level active section

Source code in src/mkdocs_rsoxs/filters.py
def active_section(nav: Navigation) -> Union[Section, None]:
    """Return the top-level active section"""
    for item in nav:
        if isinstance(item, Section) and item.is_section and item.active:
            return item
    return None

first_page(section)

Return the first page in a section

Source code in src/mkdocs_rsoxs/filters.py
def first_page(section: Section) -> Union[Page, None]:
    """Return the first page in a section"""
    for item in section.children:
        if isinstance(item, Page) and item.is_page:
            return item

    for item in section.children:
        if isinstance(item, Section):
            fp = first_page(item)
            if fp:
                return fp

    return None

file_exists(path, config)

Return whether path exists under docs_dir or the active theme static dirs.

Source code in src/mkdocs_rsoxs/filters.py
def file_exists(path: str, config: MkDocsConfig) -> bool:
    """Return whether ``path`` exists under docs_dir or the active theme static dirs."""
    p = Path(config.docs_dir) / Path(path)
    if p.is_file():
        return True
    for d in getattr(config.theme, "dirs", []) or []:
        tp = Path(d) / Path(path)
        if tp.is_file():
            return True
    return False

is_http_url(path)

Check if a path is a valid URL (http, https and also data scheme)

Source code in src/mkdocs_rsoxs/filters.py
def is_http_url(path: str) -> bool:
    """Check if a path is a valid URL (http, https and also data scheme)"""
    try:
        parsed = urllib.parse.urlparse(path)
    except Exception:
        return False

    if parsed.scheme not in ("http", "https", "data"):
        return False
    return True

jinja_plugin

plugins

excalidraw

ExcalidrawPlugin()

Bases: RouterMixin, BasePlugin[ExcalidrawPluginConfig]

This plugin enabled the real time edition of excalidraw scenes in development mode

Source code in src/mkdocs_rsoxs/plugins/_router.py
def __init__(self):
    self.bottle = Bottle()
    """We use `bottle` to handle plugin routes since
    its `wsgi()` method is very convenient with regards to
    the `_serve_request()` method of the mkdocs dev server
    """
is_dev_server = False class-attribute instance-attribute

Internal flag to detect if we are in development mode.

bottle = Bottle() instance-attribute

We use bottle to handle plugin routes since its wsgi() method is very convenient with regards to the _serve_request() method of the mkdocs dev server

on_startup(*, command, dirty)

Detect if the server is running in development mode.

Parameters:

Name Type Description Default
command str

the command being run (e.g., "serve" or "build")

required
dirty bool

whether the site is dirty (i.e., needs to be rebuilt)

required
Source code in src/mkdocs_rsoxs/plugins/excalidraw.py
def on_startup(self, *, command: str, dirty: bool):
    """Detect if the server is running in development mode.

    Parameters:
        command: the command being run (e.g., "serve" or "build")
        dirty: whether the site is dirty (i.e., needs to be rebuilt)
    """
    self.is_dev_server = command == "serve"
on_config(config, **kwargs)

Three operations are performed:

  • detect and create the excalidraw directory
  • load the internal excalidraw markdown extension
  • inject the HTTP routes needed to handle excalidraw scenes and SVGs

Parameters:

Name Type Description Default
config MkDocsConfig

MkDocs configuration object.

required
Source code in src/mkdocs_rsoxs/plugins/excalidraw.py
def on_config(self, config: MkDocsConfig, **kwargs):
    """Three operations are performed:

    - detect and create the excalidraw directory
    - load the internal excalidraw markdown extension
    - inject the HTTP routes needed to handle excalidraw scenes and SVGs

    Parameters:
        config: MkDocs configuration object.
    """
    base = os.path.dirname(config["config_file_path"])
    excalidraw_path = os.path.join(base, self.config.directory)
    extension_config = {
        "base_dir": excalidraw_path,
        "svg_only": not self.is_dev_server,
    }
    log.debug(
        f"Loading markdown extension 'shadcn.extensions.excalidraw' "
        f"with configuration: {extension_config}"
    )
    config["markdown_extensions"].append("shadcn.extensions.excalidraw")
    config["mdx_configs"]["shadcn.extensions.excalidraw"] = extension_config
    # create directory
    log.debug(f"creating excalidraw directory: {excalidraw_path}")
    os.makedirs(excalidraw_path, exist_ok=True)

    # these routes are injected in on_serve
    log.debug("injecting HTTP routes for excalidraw plugin")
    self.add_route(
        "/excalidraw/scene",
        scene_handler_factory(excalidraw_path),
        method=["GET", "POST"],
    )
    self.add_route(
        "/excalidraw/svg",
        svg_handler_factory(excalidraw_path),
        method=["GET", "POST"],
    )
add_route(path, handler, method='GET')

Add a route to the router.

Source code in src/mkdocs_rsoxs/plugins/_router.py
def add_route(
    self,
    path: str,
    handler: Callable,
    method: str | list[str] = "GET",
):
    """Add a route to the router."""
    self.bottle.route(path, method=method)(handler)
extend_server(server)

Extend the mkdocs dev server to add custom behavior.

Source code in src/mkdocs_rsoxs/plugins/_router.py
def extend_server(self, server: LiveReloadServer):
    """Extend the mkdocs dev server to add custom behavior."""
    original = server._serve_request

    @wraps(original)
    def _serve_request(
        server_self, environ, start_response
    ) -> Iterable[bytes] | None:
        # put priority to base routes
        result = original(environ, start_response)
        if result is not None:
            return result
        # run our extra route handler
        return self.bottle.wsgi(environ, start_response)

    # monkey patch the _serve_request method of the server
    setattr(server, "_serve_request", MethodType(_serve_request, server))
on_serve(server, /, *, config, builder)

This method is called when the server is started. At the end of the mkdocs workflow. At this moment we have access to the live server to inject our new routes. See https://www.mkdocs.org/dev-guide/plugins/#events to see the mkdocs workflow.

Source code in src/mkdocs_rsoxs/plugins/_router.py
def on_serve(self, server: LiveReloadServer, /, *, config, builder):
    """This method is called when the server is started. At the end of the mkdocs
    workflow. At this moment we have access to the live server to inject our new
    routes.
    See https://www.mkdocs.org/dev-guide/plugins/#events to see the mkdocs workflow.
    """
    self.extend_server(server)
svg_handler_factory(directory)

Attributes:

Name Type Description
directory str

Directory where the SVG files are stored. This is relative to the docs_dir.

Source code in src/mkdocs_rsoxs/plugins/excalidraw.py
def svg_handler_factory(directory: str):
    """
    Attributes:
        directory (str): Directory where the SVG files are stored. This is relative to the docs_dir.
    """

    def handler():
        file = request.query.get("file")
        svg_file = os.path.join(directory, file.replace(".json", ".svg"))

        if request.method == "POST":
            log.info(f"POST {request.path}?{request.query_string}")
            log.debug(f"opening {svg_file} for writing request body")
            with open(svg_file, "wb") as f:
                f.write(request.body.read())

        elif request.method == "GET":
            log.info(f"GET {request.path}?{request.query_string}")
            if not os.path.exists(svg_file):
                log.warning(f"file {svg_file} not found, creating it")
                with open(svg_file, "w") as f:
                    # default height to 400px
                    f.write(
                        f"""<svg id="{file}" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 670 400" width="100%" height="400"></svg>"""
                    )
            response.content_type = "image/svg+xml"
            return open(svg_file, "r").read().strip()

    return handler

mixins

base
Mixin

A base mixin class for MkDocs plugins.

dev
DevServerMixin

Bases: Mixin

A mixin to add development server capabilities to MkDocs plugins.

git
GitTimestampsMixin

Bases: Mixin

on_config(config)

Called when the config is loaded.

Attributes:

Name Type Description
config dict

The MkDocs configuration dictionary.

Source code in src/mkdocs_rsoxs/plugins/mixins/git.py
def on_config(self, config: MkDocsConfig):
    """Called when the config is loaded.

    Attributes:
        config (dict): The MkDocs configuration dictionary.

    """
    repo = find_repo(config.config_file_path)
    config[REPO_CONFIG_KEY] = repo
    logger.info("Git mixin activated.")
    logger.debug(f"Git repository: {config[REPO_CONFIG_KEY]}")

    self.has_commits = repo is not None and repo.head.is_valid()
    if not self.has_commits:
        logger.warning("No git commit found.")
    return super().on_config(config)
find_repo(abs_src_file)

Find the git repository for the given source file. Returns None if no repository is found.

Source code in src/mkdocs_rsoxs/plugins/mixins/git.py
def find_repo(abs_src_file: str) -> Union[Repo, None]:
    """
    Find the git repository for the given source file.
    Returns None if no repository is found.
    """
    try:
        return Repo(abs_src_file, search_parent_directories=True)
    except Exception:
        print(f"Could not find git repository starting from {abs_src_file}")
        return None
katex
KatexMixin

Bases: Mixin

A mixin to add extra features to Katex plugin.

markdown
MarkdownMixin()

Bases: Mixin

A mixin to expose raw page markdown in templates, copy them to the build dir, and provide a URL.

Source code in src/mkdocs_rsoxs/plugins/mixins/markdown.py
def __init__(self):
    self.raw_markdown = {}
order
OrderMixin

Bases: Mixin

page_index = 0 class-attribute instance-attribute

Internal page index for orderning purpose

page_indices = set() class-attribute instance-attribute

Internal set of pages that have hard-coded order

nav_order = [] class-attribute instance-attribute

Internal list of pages in the order they appear in the navigation

on_files(files, config)

Remove order from file destination URI, to get nicer URLs.

Source code in src/mkdocs_rsoxs/plugins/mixins/order.py
def on_files(self, files: Files, config: MkDocsConfig) -> Files:
    """Remove order from file destination URI, to get nicer URLs."""
    rex = re.compile(r"((?:^|/))([0-9]+[ _])")
    for file in files:
        if rex.search(file.dest_uri):
            file.dest_uri = rex.sub(
                lambda m: m.group(1),
                file.dest_uri,
            )
    return super().on_files(files, config)
table
TableMixin

Bases: Mixin

A mixin to wrap

to better manage overflow

search

SearchPlugin()

Bases: GitTimestampsMixin, DevServerMixin, OrderMixin, MkdocstringsMixin, KatexMixin, TableMixin, MarkdownMixin, SearchPlugin

⚠️ HACK ⚠️ Custom plugin. As search is loaded by default, we subclass it so as to inject what we want (and without adding a list of additional plugins)

Source code in src/mkdocs_rsoxs/plugins/mixins/markdown.py
def __init__(self):
    self.raw_markdown = {}
page_index = 0 class-attribute instance-attribute

Internal page index for orderning purpose

page_indices = set() class-attribute instance-attribute

Internal set of pages that have hard-coded order

nav_order = [] class-attribute instance-attribute

Internal list of pages in the order they appear in the navigation

on_files(files, config)

Remove order from file destination URI, to get nicer URLs.

Source code in src/mkdocs_rsoxs/plugins/mixins/order.py
def on_files(self, files: Files, config: MkDocsConfig) -> Files:
    """Remove order from file destination URI, to get nicer URLs."""
    rex = re.compile(r"((?:^|/))([0-9]+[ _])")
    for file in files:
        if rex.search(file.dest_uri):
            file.dest_uri = rex.sub(
                lambda m: m.group(1),
                file.dest_uri,
            )
    return super().on_files(files, config)

utils

deep_merge(base, override)

Recursively merges override into base.

Parameters:

Name Type Description Default
base MutableMapping

the dictionary to merge into (modified in place)

required
override Mapping

the dictionary to merge from (not modified)

required
Source code in src/mkdocs_rsoxs/utils.py
def deep_merge(base: MutableMapping, override: Mapping):
    """Recursively merges override into base.

    Parameters:
        base: the dictionary to merge into (modified in place)
        override: the dictionary to merge from (not modified)
    """
    for key in override:
        if (
            key in base
            and isinstance(base[key], MutableMapping)
            and isinstance(override[key], Mapping)
        ):
            deep_merge(base[key], override[key])
        else:
            base[key] = override[key]
    return base