Split HTML5 editor renderer
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-17 11:19:22 +03:00
parent 53e983af4e
commit b8256927bf
3 changed files with 368 additions and 350 deletions
+1 -345
View File
@@ -5,12 +5,7 @@ from html import escape
from typing import Iterable from typing import Iterable
from urllib.parse import quote from urllib.parse import quote
from api_server.html5_authoring import ( from sir import NodeKind
render_html5_authoring_changes,
render_html5_authoring_preview,
render_html5_metadata_authoring,
)
from sir import NodeKind, SirSnapshot
_HTML5_OBJECT_CONTEXT_KINDS = { _HTML5_OBJECT_CONTEXT_KINDS = {
NodeKind.CATALOG.value, NodeKind.CATALOG.value,
@@ -100,113 +95,6 @@ def render_html5_project_rows(projects: Iterable[object]) -> str:
return project_rows return project_rows
def render_html5_editor(
*,
project_id: str,
projects: Iterable[object],
snapshot: SirSnapshot | None,
error: str | None = None,
q: str = "",
) -> str:
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
if error or snapshot is None:
content = f"""
<main class="workspace" data-html5-page="editor" data-project-id="{escape(project_id)}">
{_topbar(project_id, project_nav)}
<section class="empty-state" data-html5-error>
<h1>Проект не готов к HTML5 IDE</h1>
<p>{escape(error or "Snapshot не найден")}</p>
<a class="button" href="/html5">К списку проектов</a>
</section>
</main>
"""
return _page(f"SFERA HTML5 - {project_id}", content)
counts = Counter(str(node.kind.value if hasattr(node.kind, "value") else node.kind) for node in snapshot.nodes)
modules = [node for node in snapshot.nodes if node.kind == NodeKind.MODULE]
objects = [
node
for node in snapshot.nodes
if node.kind
in {
NodeKind.CATALOG,
NodeKind.DOCUMENT,
NodeKind.REGISTER,
NodeKind.COMMON_MODULE,
NodeKind.REPORT,
NodeKind.DATA_PROCESSOR,
}
]
tree_nodes = objects[:120] or modules[:120]
selected_module = modules[0] if modules else None
content = f"""
<main
class="workspace"
data-html5-page="editor"
data-project-id="{escape(project_id)}"
hx-ext="sse"
sse-connect="/html5/projects/{quote(project_id)}/events"
>
{_topbar(project_id, project_nav)}
<section class="layout">
<aside class="panel tree" data-html5-tree>
<div class="panel-title">Дерево объектов</div>
<nav>{''.join(_tree_item(project_id, node) for node in tree_nodes) or '<p class="muted">Объекты не найдены</p>'}</nav>
</aside>
<section class="editor" data-html5-editor>
<div class="editor-head">
<div>
<p class="eyebrow">HTML5 editor</p>
<h1>{escape(selected_module.qualified_name if selected_module else project_id)}</h1>
</div>
<form
class="search"
action="/html5/projects/{quote(project_id)}/editor"
method="get"
data-html5-search
hx-get="/html5/projects/{quote(project_id)}/symbols"
hx-target="[data-html5-symbol-results]"
hx-swap="innerHTML"
hx-push-url="false"
>
<input name="q" value="{escape(q)}" placeholder="Найти символ на сервере" />
<button type="submit">Найти</button>
</form>
</div>
{render_html5_source(selected_module)}
</section>
<aside class="panel inspector" data-html5-inspector>
<div class="panel-title">Серверный контекст</div>
<dl class="metrics">
<div><dt>Nodes</dt><dd>{len(snapshot.nodes)}</dd></div>
<div><dt>Edges</dt><dd>{len(snapshot.edges)}</dd></div>
<div><dt>Diagnostics</dt><dd>{len(snapshot.diagnostics)}</dd></div>
<div><dt>Modules</dt><dd>{len(modules)}</dd></div>
</dl>
{render_html5_object_context(project_id, None, None)}
<div class="panel-title">Типы</div>
<ul class="compact">{''.join(f'<li><span>{escape(kind)}</span><b>{count}</b></li>' for kind, count in counts.most_common(10))}</ul>
<div class="panel-title">Результаты</div>
<div data-html5-symbol-results>
{render_html5_symbols(snapshot, q, project_id)}
</div>
{render_html5_symbol_detail(project_id, None)}
{render_html5_flowchart(project_id, None)}
{render_html5_project_report(project_id, None)}
{render_html5_review(project_id, None)}
{render_html5_authoring_preview(project_id, None)}
{render_html5_metadata_authoring(project_id)}
{render_html5_authoring_changes(project_id, None)}
</aside>
</section>
<footer class="status" data-html5-status sse-swap="status">
{render_html5_status(project_id, snapshot)}
</footer>
</main>
"""
return _page(f"SFERA HTML5 - {project_id}", content)
def render_html5_flowchart( def render_html5_flowchart(
project_id: str, project_id: str,
flowchart: object | None, flowchart: object | None,
@@ -585,124 +473,6 @@ def render_html5_object_context(
""" """
def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str:
return (
f'<span>project: {escape(project_id)}</span>'
f'<span>snapshot: {escape(snapshot.snapshot_id)}</span>'
f'<span>nodes: {len(snapshot.nodes)}</span>'
f'<span>edges: {len(snapshot.edges)}</span>'
f'<span>diagnostics: {len(snapshot.diagnostics)}</span>'
'<span>server-rendered</span>'
'<span>client-js: htmx+sse only</span>'
)
def html5_symbol_results(snapshot: SirSnapshot, q: str) -> list[object]:
query = q.strip().lower()
if not query:
modules = [node for node in snapshot.nodes if node.kind == NodeKind.MODULE]
return (modules[:12] or snapshot.nodes[:12])
return [
node for node in snapshot.nodes
if query in (node.qualified_name or node.name).lower()
][:30]
def render_html5_symbols(snapshot: SirSnapshot, q: str, project_id: str | None = None) -> str:
results = html5_symbol_results(snapshot, q)
if not results:
return '<p class="muted">Нет результатов</p>'
return "".join(_symbol_result(node, project_id) for node in results)
def render_html5_symbol_detail(project_id: str, references: object | None, *, oob: bool = False) -> str:
oob_attr = ' hx-swap-oob="outerHTML"' if oob else ""
if references is None:
return f"""
<div class="symbol-detail" data-html5-symbol-detail{oob_attr}>
<div class="panel-title">Символ</div>
<p class="muted padded">Выберите результат поиска для server-side definition/references по проекту {escape(project_id)}.</p>
</div>
"""
symbol = getattr(references, "symbol", None)
node = getattr(symbol, "node", None)
source = getattr(symbol, "source", None)
name = getattr(node, "qualified_name", None) or getattr(node, "name", "symbol")
kind = getattr(node, "kind", "")
source_path = getattr(source, "source_path", None) or ""
line = getattr(source, "line_start", None)
location = f"{source_path}:{line}" if source_path and line else source_path or "source unavailable"
refs = getattr(references, "references", []) or []
ref_items = (
"".join(_symbol_reference_item(project_id, ref) for ref in refs[:10])
or '<p class="muted padded">References не найдены</p>'
)
lineage_id = str(getattr(node, "lineage_id", ""))
return f"""
<div
class="symbol-detail"
data-html5-symbol-detail
data-html5-lineage-id="{escape(lineage_id)}"
{oob_attr}
>
<div class="panel-title">Символ · {escape(str(kind))}</div>
<article class="symbol-focus">
<strong>{escape(str(name))}</strong>
<small>{escape(str(location))}</small>
</article>
{_symbol_summary(refs)}
<div class="review-list">{ref_items}</div>
</div>
"""
def render_html5_source(node: object | None, *, oob: bool = False) -> str:
name = "source" if node is None else getattr(node, "qualified_name", None) or getattr(node, "name", "source")
kind = "" if node is None else _enum_text(getattr(node, "kind", ""))
lineage_id = "" if node is None else str(getattr(node, "lineage_id", ""))
source_path = "" if node is None else str(
getattr(node, "source_path", None) or getattr(getattr(node, "source_ref", None), "source_path", None) or ""
)
line = "" if node is None else str(
getattr(node, "line_start", None) or getattr(getattr(node, "source_ref", None), "line_start", None) or ""
)
location = f"{source_path}:{line}" if source_path and line else source_path or "source unavailable"
source_text = _node_source_text(node)
line_count = len(source_text.splitlines()) or 1
source_size = len(source_text)
oob_attr = ' hx-swap-oob="outerHTML"' if oob else ""
return f"""
<article
class="source-panel"
data-html5-source
data-html5-source-name="{escape(str(name))}"
data-html5-lineage-id="{escape(lineage_id)}"
{oob_attr}
>
<header class="source-head">
<div>
<strong>{escape(str(name))}</strong>
<small>{escape(kind or "source")}</small>
</div>
<dl>
{_metric("Lines", line_count)}
{_metric("Location", location)}
</dl>
</header>
{_source_summary(kind or "source", line_count, source_size, location)}
<pre class="code">{escape(source_text)}</pre>
</article>
"""
def _source_summary(kind: str, line_count: int, source_size: int, location: str) -> str:
return f"""
<p class="source-summary" data-html5-source-summary>
{escape(kind)} · {escape(str(line_count))} lines · {escape(str(source_size))} chars · {escape(location)}
</p>
"""
def _page(title: str, body: str) -> str: def _page(title: str, body: str) -> str:
return f"""<!doctype html> return f"""<!doctype html>
<html lang="ru"> <html lang="ru">
@@ -817,109 +587,6 @@ def _review_item(project_id: str, finding: dict) -> str:
""" """
def _tree_item(project_id: str, node: object) -> str:
name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
kind = getattr(node, "kind", "")
kind_value = str(kind.value if hasattr(kind, "value") else kind)
lineage_id = str(getattr(node, "lineage_id", ""))
object_kinds = {
NodeKind.CATALOG.value,
NodeKind.DOCUMENT.value,
NodeKind.REGISTER.value,
NodeKind.COMMON_MODULE.value,
NodeKind.REPORT.value,
NodeKind.DATA_PROCESSOR.value,
}
if kind_value in object_kinds:
htmx_attrs = (
f'hx-get="/html5/projects/{quote(project_id)}/objects/context/{quote(str(name), safe="")}" '
'hx-target="[data-html5-object-context]" hx-swap="outerHTML"'
)
else:
htmx_attrs = (
f'hx-get="/html5/projects/{quote(project_id)}/source/{quote(lineage_id, safe="")}" '
'hx-target="[data-html5-source]" hx-swap="outerHTML"'
)
return (
f'<a class="tree-item" href="#{quote(str(name))}" '
f'data-html5-node-kind="{escape(kind_value)}" '
f'data-html5-lineage-id="{escape(lineage_id)}" '
f'{htmx_attrs}>'
f'<span>{escape(str(name))}</span><small>{escape(kind_value)}</small></a>'
)
def _symbol_result(node: object, project_id: str | None = None) -> str:
name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
kind = getattr(node, "kind", "")
kind_value = str(kind.value if hasattr(kind, "value") else kind)
lineage_id = str(getattr(node, "lineage_id", ""))
source_path = getattr(node, "source_path", None) or getattr(getattr(node, "source_ref", None), "source_path", None) or ""
line = getattr(node, "line_start", None) or getattr(getattr(node, "source_ref", None), "line_start", None)
location = f"{source_path}:{line}" if source_path and line else source_path or "source unavailable"
htmx_attrs = (
f'hx-get="/html5/projects/{quote(project_id)}/symbols/{quote(lineage_id, safe="")}/detail" '
'hx-target="[data-html5-symbol-detail]" hx-swap="outerHTML"'
if project_id and lineage_id
else ""
)
return f"""
<article class="symbol" data-html5-symbol data-html5-lineage-id="{escape(lineage_id)}" {htmx_attrs}>
<strong>{escape(str(name))}</strong>
<span>{escape(kind_value)}</span>
<small>{escape(str(location))}</small>
</article>"""
def _symbol_summary(references: Iterable[object]) -> str:
refs = list(references)
directions = Counter(str(getattr(ref, "direction", "") or "UNKNOWN") for ref in refs)
kinds = Counter(str(getattr(ref, "kind", "") or "REFERENCE") for ref in refs)
direction_text = ", ".join(f"{name}: {count}" for name, count in sorted(directions.items())) or "no directions"
kind_text = ", ".join(f"{name}: {count}" for name, count in sorted(kinds.items())) or "no kinds"
return f"""
<p class="symbol-summary" data-html5-symbol-summary>
{escape(str(len(refs)))} references · {escape(direction_text)} · {escape(kind_text)}
</p>
"""
def _symbol_reference_item(project_id: str, reference: object) -> str:
kind = str(getattr(reference, "kind", ""))
direction = str(getattr(reference, "direction", ""))
source = getattr(reference, "source", None)
target = getattr(reference, "target", None)
location = getattr(reference, "location", None)
source_name = getattr(source, "qualified_name", None) or getattr(source, "name", "")
target_name = getattr(target, "qualified_name", None) or getattr(target, "name", "")
source_path = getattr(location, "source_path", None) or ""
line = getattr(location, "line_start", None)
place = f"{source_path}:{line}" if source_path and line else source_path
label = f"{source_name} -> {target_name}".strip(" ->")
source_lineage = str(getattr(source, "lineage_id", "") or "")
source_link = (
f"""
<a
href="/html5/projects/{quote(project_id)}/source/{quote(source_lineage, safe='')}"
hx-get="/html5/projects/{quote(project_id)}/source/{quote(source_lineage, safe='')}"
hx-target="[data-html5-source]"
hx-swap="outerHTML"
data-html5-symbol-source="{escape(source_lineage)}"
>Source</a>
"""
if source_lineage
else ""
)
return f"""
<article class="symbol-reference" data-html5-symbol-reference>
<strong>{escape(label or kind)}</strong>
<span>{escape(direction)} · {escape(kind)}</span>
<small>{escape(place or "source unavailable")}</small>
<span class="inline-actions">{source_link}</span>
</article>
"""
def _named_node_item(label: str, node: object) -> str: def _named_node_item(label: str, node: object) -> str:
name = getattr(node, "qualified_name", None) or getattr(node, "name", "") name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
kind = getattr(node, "kind", label) kind = getattr(node, "kind", label)
@@ -1275,17 +942,6 @@ def _flowchart_node_item(project_id: str, node: object, depth: int) -> str:
""" """
def _node_source_text(node: object | None) -> str:
if node is None:
return "// Модуль не найден в snapshot.\n// HTML5 IDE показывает серверный fallback без клиентского JS."
attributes = getattr(node, "attributes", {}) or {}
source_text = attributes.get("source_text") or attributes.get("text")
if isinstance(source_text, str) and source_text.strip():
return source_text
name = getattr(node, "qualified_name", None) or getattr(node, "name", "Module")
return f"// {name}\n// Исходный текст не сохранен в snapshot.\n// Сервер уже отрисовал контекст, дерево, поиск и метрики."
def _css() -> str: def _css() -> str:
return """ return """
:root{color-scheme:light;--bg:#f7f8fb;--card:#fff;--text:#17202a;--muted:#687385;--line:#dce2ea;--brand:#2457d6;--ok:#168457;--warn:#a16207} :root{color-scheme:light;--bg:#f7f8fb;--card:#fff;--text:#17202a;--muted:#687385;--line:#dce2ea;--brand:#2457d6;--ok:#168457;--warn:#a16207}
@@ -0,0 +1,360 @@
from __future__ import annotations
from collections import Counter
from html import escape
from typing import Iterable
from urllib.parse import quote
from api_server.html5 import (
_enum_text,
_metric,
_page,
_project_link,
_topbar,
render_html5_flowchart,
render_html5_object_context,
render_html5_project_report,
render_html5_review,
)
from api_server.html5_authoring import (
render_html5_authoring_changes,
render_html5_authoring_preview,
render_html5_metadata_authoring,
)
from sir import NodeKind, SirSnapshot
def render_html5_editor(
*,
project_id: str,
projects: Iterable[object],
snapshot: SirSnapshot | None,
error: str | None = None,
q: str = "",
) -> str:
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
if error or snapshot is None:
content = f"""
<main class="workspace" data-html5-page="editor" data-project-id="{escape(project_id)}">
{_topbar(project_id, project_nav)}
<section class="empty-state" data-html5-error>
<h1>Проект не готов к HTML5 IDE</h1>
<p>{escape(error or "Snapshot не найден")}</p>
<a class="button" href="/html5">К списку проектов</a>
</section>
</main>
"""
return _page(f"SFERA HTML5 - {project_id}", content)
counts = Counter(str(node.kind.value if hasattr(node.kind, "value") else node.kind) for node in snapshot.nodes)
modules = [node for node in snapshot.nodes if node.kind == NodeKind.MODULE]
objects = [
node
for node in snapshot.nodes
if node.kind
in {
NodeKind.CATALOG,
NodeKind.DOCUMENT,
NodeKind.REGISTER,
NodeKind.COMMON_MODULE,
NodeKind.REPORT,
NodeKind.DATA_PROCESSOR,
}
]
tree_nodes = objects[:120] or modules[:120]
selected_module = modules[0] if modules else None
content = f"""
<main
class="workspace"
data-html5-page="editor"
data-project-id="{escape(project_id)}"
hx-ext="sse"
sse-connect="/html5/projects/{quote(project_id)}/events"
>
{_topbar(project_id, project_nav)}
<section class="layout">
<aside class="panel tree" data-html5-tree>
<div class="panel-title">Дерево объектов</div>
<nav>{''.join(_tree_item(project_id, node) for node in tree_nodes) or '<p class="muted">Объекты не найдены</p>'}</nav>
</aside>
<section class="editor" data-html5-editor>
<div class="editor-head">
<div>
<p class="eyebrow">HTML5 editor</p>
<h1>{escape(selected_module.qualified_name if selected_module else project_id)}</h1>
</div>
<form
class="search"
action="/html5/projects/{quote(project_id)}/editor"
method="get"
data-html5-search
hx-get="/html5/projects/{quote(project_id)}/symbols"
hx-target="[data-html5-symbol-results]"
hx-swap="innerHTML"
hx-push-url="false"
>
<input name="q" value="{escape(q)}" placeholder="Найти символ на сервере" />
<button type="submit">Найти</button>
</form>
</div>
{render_html5_source(selected_module)}
</section>
<aside class="panel inspector" data-html5-inspector>
<div class="panel-title">Серверный контекст</div>
<dl class="metrics">
<div><dt>Nodes</dt><dd>{len(snapshot.nodes)}</dd></div>
<div><dt>Edges</dt><dd>{len(snapshot.edges)}</dd></div>
<div><dt>Diagnostics</dt><dd>{len(snapshot.diagnostics)}</dd></div>
<div><dt>Modules</dt><dd>{len(modules)}</dd></div>
</dl>
{render_html5_object_context(project_id, None, None)}
<div class="panel-title">Типы</div>
<ul class="compact">{''.join(f'<li><span>{escape(kind)}</span><b>{count}</b></li>' for kind, count in counts.most_common(10))}</ul>
<div class="panel-title">Результаты</div>
<div data-html5-symbol-results>
{render_html5_symbols(snapshot, q, project_id)}
</div>
{render_html5_symbol_detail(project_id, None)}
{render_html5_flowchart(project_id, None)}
{render_html5_project_report(project_id, None)}
{render_html5_review(project_id, None)}
{render_html5_authoring_preview(project_id, None)}
{render_html5_metadata_authoring(project_id)}
{render_html5_authoring_changes(project_id, None)}
</aside>
</section>
<footer class="status" data-html5-status sse-swap="status">
{render_html5_status(project_id, snapshot)}
</footer>
</main>
"""
return _page(f"SFERA HTML5 - {project_id}", content)
def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str:
return (
f'<span>project: {escape(project_id)}</span>'
f'<span>snapshot: {escape(snapshot.snapshot_id)}</span>'
f'<span>nodes: {len(snapshot.nodes)}</span>'
f'<span>edges: {len(snapshot.edges)}</span>'
f'<span>diagnostics: {len(snapshot.diagnostics)}</span>'
'<span>server-rendered</span>'
'<span>client-js: htmx+sse only</span>'
)
def html5_symbol_results(snapshot: SirSnapshot, q: str) -> list[object]:
query = q.strip().lower()
if not query:
modules = [node for node in snapshot.nodes if node.kind == NodeKind.MODULE]
return (modules[:12] or snapshot.nodes[:12])
return [
node for node in snapshot.nodes
if query in (node.qualified_name or node.name).lower()
][:30]
def render_html5_symbols(snapshot: SirSnapshot, q: str, project_id: str | None = None) -> str:
results = html5_symbol_results(snapshot, q)
if not results:
return '<p class="muted">Нет результатов</p>'
return "".join(_symbol_result(node, project_id) for node in results)
def render_html5_symbol_detail(project_id: str, references: object | None, *, oob: bool = False) -> str:
oob_attr = ' hx-swap-oob="outerHTML"' if oob else ""
if references is None:
return f"""
<div class="symbol-detail" data-html5-symbol-detail{oob_attr}>
<div class="panel-title">Символ</div>
<p class="muted padded">Выберите результат поиска для server-side definition/references по проекту {escape(project_id)}.</p>
</div>
"""
symbol = getattr(references, "symbol", None)
node = getattr(symbol, "node", None)
source = getattr(symbol, "source", None)
name = getattr(node, "qualified_name", None) or getattr(node, "name", "symbol")
kind = getattr(node, "kind", "")
source_path = getattr(source, "source_path", None) or ""
line = getattr(source, "line_start", None)
location = f"{source_path}:{line}" if source_path and line else source_path or "source unavailable"
refs = getattr(references, "references", []) or []
ref_items = (
"".join(_symbol_reference_item(project_id, ref) for ref in refs[:10])
or '<p class="muted padded">References не найдены</p>'
)
lineage_id = str(getattr(node, "lineage_id", ""))
return f"""
<div
class="symbol-detail"
data-html5-symbol-detail
data-html5-lineage-id="{escape(lineage_id)}"
{oob_attr}
>
<div class="panel-title">Символ · {escape(str(kind))}</div>
<article class="symbol-focus">
<strong>{escape(str(name))}</strong>
<small>{escape(str(location))}</small>
</article>
{_symbol_summary(refs)}
<div class="review-list">{ref_items}</div>
</div>
"""
def render_html5_source(node: object | None, *, oob: bool = False) -> str:
name = "source" if node is None else getattr(node, "qualified_name", None) or getattr(node, "name", "source")
kind = "" if node is None else _enum_text(getattr(node, "kind", ""))
lineage_id = "" if node is None else str(getattr(node, "lineage_id", ""))
source_path = "" if node is None else str(
getattr(node, "source_path", None) or getattr(getattr(node, "source_ref", None), "source_path", None) or ""
)
line = "" if node is None else str(
getattr(node, "line_start", None) or getattr(getattr(node, "source_ref", None), "line_start", None) or ""
)
location = f"{source_path}:{line}" if source_path and line else source_path or "source unavailable"
source_text = _node_source_text(node)
line_count = len(source_text.splitlines()) or 1
source_size = len(source_text)
oob_attr = ' hx-swap-oob="outerHTML"' if oob else ""
return f"""
<article
class="source-panel"
data-html5-source
data-html5-source-name="{escape(str(name))}"
data-html5-lineage-id="{escape(lineage_id)}"
{oob_attr}
>
<header class="source-head">
<div>
<strong>{escape(str(name))}</strong>
<small>{escape(kind or "source")}</small>
</div>
<dl>
{_metric("Lines", line_count)}
{_metric("Location", location)}
</dl>
</header>
{_source_summary(kind or "source", line_count, source_size, location)}
<pre class="code">{escape(source_text)}</pre>
</article>
"""
def _source_summary(kind: str, line_count: int, source_size: int, location: str) -> str:
return f"""
<p class="source-summary" data-html5-source-summary>
{escape(kind)} · {escape(str(line_count))} lines · {escape(str(source_size))} chars · {escape(location)}
</p>
"""
def _tree_item(project_id: str, node: object) -> str:
name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
kind = getattr(node, "kind", "")
kind_value = str(kind.value if hasattr(kind, "value") else kind)
lineage_id = str(getattr(node, "lineage_id", ""))
object_kinds = {
NodeKind.CATALOG.value,
NodeKind.DOCUMENT.value,
NodeKind.REGISTER.value,
NodeKind.COMMON_MODULE.value,
NodeKind.REPORT.value,
NodeKind.DATA_PROCESSOR.value,
}
if kind_value in object_kinds:
htmx_attrs = (
f'hx-get="/html5/projects/{quote(project_id)}/objects/context/{quote(str(name), safe="")}" '
'hx-target="[data-html5-object-context]" hx-swap="outerHTML"'
)
else:
htmx_attrs = (
f'hx-get="/html5/projects/{quote(project_id)}/source/{quote(lineage_id, safe="")}" '
'hx-target="[data-html5-source]" hx-swap="outerHTML"'
)
return (
f'<a class="tree-item" href="#{quote(str(name))}" '
f'data-html5-node-kind="{escape(kind_value)}" '
f'data-html5-lineage-id="{escape(lineage_id)}" '
f'{htmx_attrs}>'
f'<span>{escape(str(name))}</span><small>{escape(kind_value)}</small></a>'
)
def _symbol_result(node: object, project_id: str | None = None) -> str:
name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
kind = getattr(node, "kind", "")
kind_value = str(kind.value if hasattr(kind, "value") else kind)
lineage_id = str(getattr(node, "lineage_id", ""))
source_path = getattr(node, "source_path", None) or getattr(getattr(node, "source_ref", None), "source_path", None) or ""
line = getattr(node, "line_start", None) or getattr(getattr(node, "source_ref", None), "line_start", None)
location = f"{source_path}:{line}" if source_path and line else source_path or "source unavailable"
htmx_attrs = (
f'hx-get="/html5/projects/{quote(project_id)}/symbols/{quote(lineage_id, safe="")}/detail" '
'hx-target="[data-html5-symbol-detail]" hx-swap="outerHTML"'
if project_id and lineage_id
else ""
)
return f"""
<article class="symbol" data-html5-symbol data-html5-lineage-id="{escape(lineage_id)}" {htmx_attrs}>
<strong>{escape(str(name))}</strong>
<span>{escape(kind_value)}</span>
<small>{escape(str(location))}</small>
</article>"""
def _symbol_summary(references: Iterable[object]) -> str:
refs = list(references)
directions = Counter(str(getattr(ref, "direction", "") or "UNKNOWN") for ref in refs)
kinds = Counter(str(getattr(ref, "kind", "") or "REFERENCE") for ref in refs)
direction_text = ", ".join(f"{name}: {count}" for name, count in sorted(directions.items())) or "no directions"
kind_text = ", ".join(f"{name}: {count}" for name, count in sorted(kinds.items())) or "no kinds"
return f"""
<p class="symbol-summary" data-html5-symbol-summary>
{escape(str(len(refs)))} references · {escape(direction_text)} · {escape(kind_text)}
</p>
"""
def _symbol_reference_item(project_id: str, reference: object) -> str:
kind = str(getattr(reference, "kind", ""))
direction = str(getattr(reference, "direction", ""))
source = getattr(reference, "source", None)
target = getattr(reference, "target", None)
location = getattr(reference, "location", None)
source_name = getattr(source, "qualified_name", None) or getattr(source, "name", "")
target_name = getattr(target, "qualified_name", None) or getattr(target, "name", "")
source_path = getattr(location, "source_path", None) or ""
line = getattr(location, "line_start", None)
place = f"{source_path}:{line}" if source_path and line else source_path
label = f"{source_name} -> {target_name}".strip(" ->")
source_lineage = str(getattr(source, "lineage_id", "") or "")
source_link = (
f"""
<a
href="/html5/projects/{quote(project_id)}/source/{quote(source_lineage, safe='')}"
hx-get="/html5/projects/{quote(project_id)}/source/{quote(source_lineage, safe='')}"
hx-target="[data-html5-source]"
hx-swap="outerHTML"
data-html5-symbol-source="{escape(source_lineage)}"
>Source</a>
"""
if source_lineage
else ""
)
return f"""
<article class="symbol-reference" data-html5-symbol-reference>
<strong>{escape(label or kind)}</strong>
<span>{escape(direction)} · {escape(kind)}</span>
<small>{escape(place or "source unavailable")}</small>
<span class="inline-actions">{source_link}</span>
</article>
"""
def _node_source_text(node: object | None) -> str:
if node is None:
return "// Модуль не найден в snapshot.\n// HTML5 IDE показывает серверный fallback без клиентского JS."
attributes = getattr(node, "attributes", {}) or {}
source_text = attributes.get("source_text") or attributes.get("text")
if isinstance(source_text, str) and source_text.strip():
return source_text
name = getattr(node, "qualified_name", None) or getattr(node, "name", "Module")
return f"// {name}\n// Исходный текст не сохранен в snapshot.\n// Сервер уже отрисовал контекст, дерево, поиск и метрики."
+7 -5
View File
@@ -38,7 +38,6 @@ from neo4j import AsyncGraphDatabase
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from api_server.html5 import ( from api_server.html5 import (
render_html5_editor,
render_html5_flowchart, render_html5_flowchart,
render_html5_index, render_html5_index,
render_html5_object_context, render_html5_object_context,
@@ -46,10 +45,6 @@ from api_server.html5 import (
render_html5_project_rows, render_html5_project_rows,
render_html5_project_report, render_html5_project_report,
render_html5_review, render_html5_review,
render_html5_symbol_detail,
render_html5_source,
render_html5_status,
render_html5_symbols,
) )
from api_server.html5_authoring import ( from api_server.html5_authoring import (
render_html5_authoring_apply_result, render_html5_authoring_apply_result,
@@ -61,6 +56,13 @@ from api_server.html5_authoring import (
render_html5_metadata_apply_result, render_html5_metadata_apply_result,
render_html5_metadata_preview_result, render_html5_metadata_preview_result,
) )
from api_server.html5_editor import (
render_html5_editor,
render_html5_source,
render_html5_status,
render_html5_symbol_detail,
render_html5_symbols,
)
from api_server.html5_operations import ( from api_server.html5_operations import (
render_html5_operation_detail, render_html5_operation_detail,
render_html5_operation_rows, render_html5_operation_rows,