1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages
DocForge2/src/docforge/render_config.py

136 lines
5.7 KiB
Python
Raw Normal View History

"""Strict parsing and confinement for optional declared render views."""
from __future__ import annotations
from pathlib import Path
from .config_validation import ID_PATTERN, confined_path, require_string, string_list
from .errors import DocForgeError
from .models import Limits, RenderConfig, RenderView
_RENDER_KEYS = frozenset({"template_root", "preview_root", "views"})
_VIEW_KEYS = frozenset({"id", "renderer", "template", "output", "title", "families"})
_RENDERERS = frozenset({"generic_html"})
def _paths_overlap(first: Path, second: Path) -> bool:
return first == second or first.is_relative_to(second) or second.is_relative_to(first)
def load_render_config(
root: Path,
document: object,
*,
descriptor_path: Path,
content_roots: tuple[Path, ...],
authority_files: tuple[Path, ...],
cache_root: Path,
index_path: Path,
changeset_root: Path,
limits: Limits,
) -> RenderConfig | None:
if document is None:
return None
if not isinstance(document, dict):
raise DocForgeError("invalid_config", "render must be a table")
unknown = sorted(set(document) - _RENDER_KEYS)
if unknown:
raise DocForgeError("invalid_config", "render has unknown fields", fields=unknown)
template_root = confined_path(
root,
document.get("template_root"),
field="render.template_root",
must_exist=True,
expected="directory",
)
preview_root = confined_path(
root,
document.get("preview_root"),
field="render.preview_root",
must_exist=False,
)
template_protected = (*content_roots, cache_root, changeset_root)
if any(_paths_overlap(template_root, path) for path in template_protected):
raise DocForgeError(
"invalid_config", "Template input must not overlap canonical or derived roots"
)
if any(path == template_root or path.is_relative_to(template_root) for path in authority_files):
raise DocForgeError("invalid_config", "Template input must not contain authority files")
protected_roots = (*content_roots, cache_root, changeset_root, template_root)
if any(_paths_overlap(preview_root, path) for path in protected_roots):
raise DocForgeError("invalid_config", "Preview output must not overlap other project roots")
if any(path == preview_root or path.is_relative_to(preview_root) for path in authority_files):
raise DocForgeError("invalid_config", "Preview output must not contain authority files")
view_documents = document.get("views")
if not isinstance(view_documents, list) or not view_documents:
raise DocForgeError("invalid_config", "render.views must contain at least one view")
if len(view_documents) > limits.max_render_views:
raise DocForgeError("invalid_config", "render.views exceeds the configured limit")
views: list[RenderView] = []
view_ids: set[str] = set()
output_paths: set[Path] = set()
for view_document in view_documents:
if not isinstance(view_document, dict):
raise DocForgeError("invalid_config", "Each render view must be a table")
unknown_view = sorted(set(view_document) - _VIEW_KEYS)
if unknown_view:
raise DocForgeError(
"invalid_config", "Render view has unknown fields", fields=unknown_view
)
view_id = require_string(view_document, "id", descriptor_path)
if ID_PATTERN.fullmatch(view_id) is None or view_id in view_ids:
raise DocForgeError(
"invalid_config", "Render view ID is invalid or duplicated", id=view_id
)
view_ids.add(view_id)
renderer = require_string(view_document, "renderer", descriptor_path)
if renderer not in _RENDERERS:
raise DocForgeError(
"unsupported_renderer",
"Render view names an unsupported built-in renderer",
renderer=renderer,
)
template = confined_path(
template_root,
view_document.get("template"),
field="render.view.template",
must_exist=True,
expected="file",
)
output = confined_path(
root,
view_document.get("output"),
field="render.view.output",
must_exist=False,
)
if output.suffix != ".html":
raise DocForgeError("invalid_config", "generic_html output must use an .html file")
if output in output_paths:
raise DocForgeError("invalid_config", "Render view outputs must be unique")
output_paths.add(output)
forbidden_outputs = (*content_roots, changeset_root, preview_root, template_root)
if any(output == path or output.is_relative_to(path) for path in forbidden_outputs):
raise DocForgeError("invalid_config", "Render output overlaps a protected project root")
if output in authority_files or output in {descriptor_path, index_path}:
raise DocForgeError("invalid_config", "Render output overlaps a protected project file")
views.append(
RenderView(
view_id=view_id,
renderer=renderer,
template_path=template,
output_path=output,
title=require_string(view_document, "title", descriptor_path),
families=string_list(
view_document.get("families", []),
key="render.view.families",
source=descriptor_path,
),
)
)
return RenderConfig(
template_root=template_root,
preview_root=preview_root,
views=tuple(sorted(views, key=lambda view: view.view_id)),
)