Split HTML5 editor renderer
This commit is contained in:
@@ -5,12 +5,7 @@ from html import escape
|
||||
from typing import Iterable
|
||||
from urllib.parse import quote
|
||||
|
||||
from api_server.html5_authoring import (
|
||||
render_html5_authoring_changes,
|
||||
render_html5_authoring_preview,
|
||||
render_html5_metadata_authoring,
|
||||
)
|
||||
from sir import NodeKind, SirSnapshot
|
||||
from sir import NodeKind
|
||||
|
||||
_HTML5_OBJECT_CONTEXT_KINDS = {
|
||||
NodeKind.CATALOG.value,
|
||||
@@ -100,113 +95,6 @@ def render_html5_project_rows(projects: Iterable[object]) -> str:
|
||||
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(
|
||||
project_id: str,
|
||||
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:
|
||||
return f"""<!doctype html>
|
||||
<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:
|
||||
name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
|
||||
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:
|
||||
return """
|
||||
:root{color-scheme:light;--bg:#f7f8fb;--card:#fff;--text:#17202a;--muted:#687385;--line:#dce2ea;--brand:#2457d6;--ok:#168457;--warn:#a16207}
|
||||
|
||||
Reference in New Issue
Block a user