Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions macro_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,13 @@
If this is confusing, ask Cal to explain.
"""

import importlib.util
import os
import json

module_list_path = os.getenv("MODULE_LIST_PATH", "docs/assets/module-list.json")
tag_index_path = os.getenv("TAG_INDEX_PATH", "docs/assets/tag-index.json")


def _load_mkdocs_hooks():
"""Load mkdocs_hooks.py by file path rather than `import mkdocs_hooks`.

mkdocs and mkdocs-macros load their own hook module through different
mechanisms (mkdocs briefly patches sys.path and restores it; mkdocs-macros
execs this file directly by path), so a plain cross-file import can't
reliably resolve `mkdocs_hooks` regardless of how *this* file was loaded.
Resolving relative to our own (always-correct) __file__ sidesteps that.
"""
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mkdocs_hooks.py")
spec = importlib.util.spec_from_file_location("mkdocs_hooks", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


configure_safe_includes = _load_mkdocs_hooks().configure_safe_includes


class CaseInsensitiveDict(dict):
"""Dict wrapper allowing `applications[app_name]` lookups regardless of case."""

Expand Down Expand Up @@ -79,10 +59,3 @@ def pages_with_tag(tag):
{"title": e["title"], "path": os.path.relpath(e["path"], current_dir)}
for e in entries
]


def on_pre_page_macros(env):
# env.env (the actual Jinja Environment) doesn't exist yet at define_env()
# time - on_config() creates it afterwards - so patch it here instead.
# configure_safe_includes() is idempotent, so re-running per page is cheap.
configure_safe_includes(env.env)
72 changes: 0 additions & 72 deletions mkdocs_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,80 +13,9 @@
import re

import yaml
from jinja2.compiler import CodeGenerator
from jinja2.exceptions import TemplateNotFound

module_list_path = os.getenv("MODULE_LIST_PATH", "docs/assets/module-list.json")


class SafeIncludeCodeGenerator(CodeGenerator):
"""Makes every `{% include %}` in this environment render as nothing
instead of raising, if the included template errors while rendering.

Partials driven by module-list.json data (versions, network licences,
...) can fail on one app's unexpected/missing data. Without this, that
error propagates and takes out the *whole* page (mkdocs-macros' error
block) or the *whole* build (the supported-apps index renders every app
inline via the theme's own Jinja env, outside mkdocs-macros' handling).

`{% extends %}` and `{% import %}` are untouched (separate visit_*
methods) - only `{% include %}` gets this treatment.

Set as `environment.code_generator_class` (an official Jinja extension
point - see Environment.code_generator_class), it replaces the include's
normal streaming codegen with a call to `environment.safe_include()`,
which renders the target as one buffered string (so a mid-render failure
can't leak partial output) and swallows/logs any exception.
"""

def visit_Include(self, node, frame):
self.writeline("yield environment.safe_include(", node)
self.visit(node.template, frame)
if node.with_context:
self.write(f", {{**context.get_all(), **{self.dump_local_context(frame)}}}")
else:
self.write(", {}")
self.write(f", {node.ignore_missing!r})")


def make_safe_include(jinja_env):
"""Build the `environment.safe_include()` called by the codegen above.

A closure (not a method) bound to one specific environment via
`env.safe_include = make_safe_include(env)`, since generated template
code looks it up as a plain attribute (`environment.safe_include(...)`)
rather than through the descriptor protocol.
"""
def safe_include(name, context_dict, ignore_missing=False):
try:
template = (jinja_env.select_template(name)
if isinstance(name, (list, tuple))
else jinja_env.get_template(name))
except TemplateNotFound:
if ignore_missing:
return ""
raise
try:
return template.render(context_dict)
except Exception as e:
print(f"::WARNING file={name},title=include_failed,col=0,endColumn=0,line=0::{e}")
return ""
return safe_include


def configure_safe_includes(jinja_env):
"""Make every `{% include %}` rendered by `jinja_env` fail soft.

Idempotent - safe to call more than once on the same environment (e.g.
macro_hooks.py's on_pre_page_macros(), which fires per page).
"""
if getattr(jinja_env, "_safe_includes_configured", False):
return
jinja_env.code_generator_class = SafeIncludeCodeGenerator
jinja_env.safe_include = make_safe_include(jinja_env)
jinja_env._safe_includes_configured = True


_FRONT_MATTER = re.compile(r"\A---\s*\n(.*?)\n---\s*\n", re.DOTALL)


Expand Down Expand Up @@ -161,7 +90,6 @@ def on_env(env, config, files, **kwargs):
for app in applications.values()
for domain in app.get("domains", [])
})
configure_safe_includes(env)


def lint(*args, **kwargs):
Expand Down
Loading