Split HTML5 editor renderer
This commit is contained in:
@@ -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// Сервер уже отрисовал контекст, дерево, поиск и метрики."
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user