Add versioned independent projection contracts
This commit is contained in:
parent
4c5773c865
commit
96e3965855
22 changed files with 3561 additions and 133 deletions
|
|
@ -1,31 +1,17 @@
|
|||
"""Deterministic built-in renderer contract and safe template primitives."""
|
||||
"""Compatibility shim over the versioned manual projection boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from markdown_it import MarkdownIt
|
||||
|
||||
from .errors import DocForgeError
|
||||
from .models import Edge, Node, ProjectSnapshot, RenderView
|
||||
|
||||
_TEMPLATE_TOKEN = re.compile(r"{{\s*([a-z_][a-z0-9_]*)\s*}}")
|
||||
_ALLOWED_TOKENS = frozenset(
|
||||
{
|
||||
"docforge_content",
|
||||
"docforge_project_id",
|
||||
"docforge_render_identity",
|
||||
"docforge_title",
|
||||
"docforge_view_id",
|
||||
}
|
||||
)
|
||||
from .manual_projection import build_manual_projection_package, build_manual_render_plan
|
||||
from .models import ProjectSnapshot, RenderView
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -55,13 +41,12 @@ class Renderer(Protocol):
|
|||
|
||||
|
||||
class GenericHtmlRenderer:
|
||||
"""Render validated nodes through escaped CommonMark and a strict token template."""
|
||||
"""Preserve the public v1 renderer API over the plan-only manual renderer."""
|
||||
|
||||
renderer_id = "generic_html"
|
||||
contract_version = "1"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.markdown = MarkdownIt("commonmark", {"html": False, "typographer": False})
|
||||
self.renderer_version = (
|
||||
f"{self.contract_version}+markdown-it-py-{version('markdown-it-py')}"
|
||||
)
|
||||
|
|
@ -74,22 +59,6 @@ class GenericHtmlRenderer:
|
|||
*,
|
||||
changeset_hash: str | None,
|
||||
) -> PreparedRender:
|
||||
try:
|
||||
template = template_bytes.decode("utf-8")
|
||||
except UnicodeDecodeError as error:
|
||||
raise DocForgeError("invalid_template", "Render template is not valid UTF-8") from error
|
||||
tokens = _TEMPLATE_TOKEN.findall(template)
|
||||
unknown = sorted(set(tokens) - _ALLOWED_TOKENS)
|
||||
remainder = _TEMPLATE_TOKEN.sub("", template)
|
||||
if unknown or "{{" in remainder or "}}" in remainder:
|
||||
raise DocForgeError(
|
||||
"invalid_template", "Render template contains unsupported tokens", tokens=unknown
|
||||
)
|
||||
if tokens.count("docforge_content") != 1:
|
||||
raise DocForgeError(
|
||||
"invalid_template", "Render template must contain docforge_content exactly once"
|
||||
)
|
||||
|
||||
selected = tuple(
|
||||
node for node in snapshot.nodes if not view.families or node.family in view.families
|
||||
)
|
||||
|
|
@ -128,16 +97,26 @@ class GenericHtmlRenderer:
|
|||
render_identity = hashlib.sha256(
|
||||
json.dumps(identity_payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
).hexdigest()
|
||||
content = self._content(selected, selected_edges)
|
||||
replacements = {
|
||||
"docforge_content": content,
|
||||
"docforge_project_id": html.escape(snapshot.descriptor.project_id, quote=True),
|
||||
"docforge_render_identity": render_identity,
|
||||
"docforge_title": html.escape(view.title, quote=True),
|
||||
"docforge_view_id": html.escape(view.view_id, quote=True),
|
||||
}
|
||||
rendered = _TEMPLATE_TOKEN.sub(lambda match: replacements[match.group(1)], template)
|
||||
output = rendered.rstrip().encode("utf-8") + b"\n"
|
||||
plan = build_manual_render_plan(snapshot, view, changeset_hash=changeset_hash)
|
||||
package = build_manual_projection_package(
|
||||
plan,
|
||||
template_bytes,
|
||||
renderer_id=self.renderer_id,
|
||||
renderer_version=self.renderer_version,
|
||||
max_output_bytes=snapshot.descriptor.limits.max_render_bytes,
|
||||
)
|
||||
from docforge_renderers.manual import ManualHtmlRenderer
|
||||
|
||||
result = ManualHtmlRenderer(self.renderer_version).render(
|
||||
package,
|
||||
render_identity=render_identity,
|
||||
)
|
||||
if len(result.artifacts) != 1:
|
||||
raise DocForgeError(
|
||||
"invalid_projection",
|
||||
"Manual renderer returned an unsupported artifact set",
|
||||
)
|
||||
output = result.artifacts[0].content
|
||||
return PreparedRender(
|
||||
render_identity=render_identity,
|
||||
output_hash=hashlib.sha256(output).hexdigest(),
|
||||
|
|
@ -147,44 +126,6 @@ class GenericHtmlRenderer:
|
|||
template_hash=template_hash,
|
||||
)
|
||||
|
||||
def _content(self, nodes: tuple[Node, ...], edges: tuple[Edge, ...]) -> str:
|
||||
navigation = ['<nav aria-label="Documentation"><ul>']
|
||||
for node in nodes:
|
||||
navigation.append(
|
||||
f'<li><a href="#node-{html.escape(node.node_id, quote=True)}">'
|
||||
f"{html.escape(node.title)}</a></li>"
|
||||
)
|
||||
navigation.append("</ul></nav>")
|
||||
sections = [*navigation]
|
||||
edge_map: dict[str, list[Edge]] = {}
|
||||
for edge in edges:
|
||||
edge_map.setdefault(edge.source_id, []).append(edge)
|
||||
for node in nodes:
|
||||
sections.extend(
|
||||
[
|
||||
f'<section id="node-{html.escape(node.node_id, quote=True)}">',
|
||||
f"<h2>{html.escape(node.title)}</h2>",
|
||||
'<dl class="docforge-node-meta">',
|
||||
f"<dt>ID</dt><dd>{html.escape(node.node_id)}</dd>",
|
||||
f"<dt>Family</dt><dd>{html.escape(node.family)}</dd>",
|
||||
f"<dt>Status</dt><dd>{html.escape(node.status)}</dd>",
|
||||
f"<dt>Authority</dt><dd>{html.escape(node.authority)}</dd>",
|
||||
"</dl>",
|
||||
f'<p class="docforge-summary">{html.escape(node.summary)}</p>',
|
||||
self.markdown.render(node.content).rstrip(),
|
||||
]
|
||||
)
|
||||
relationships = edge_map.get(node.node_id, [])
|
||||
if relationships:
|
||||
sections.append('<ul class="docforge-relationships">')
|
||||
for edge in relationships:
|
||||
sections.append(
|
||||
f"<li>{html.escape(edge.relation)}: {html.escape(edge.target_id)}</li>"
|
||||
)
|
||||
sections.append("</ul>")
|
||||
sections.append("</section>")
|
||||
return "\n".join(sections)
|
||||
|
||||
|
||||
_RENDERERS: dict[str, type[GenericHtmlRenderer]] = {
|
||||
GenericHtmlRenderer.renderer_id: GenericHtmlRenderer
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue