diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py
index 90f7136..5b7eaee 100644
--- a/services/api-server/src/api_server/html5.py
+++ b/services/api-server/src/api_server/html5.py
@@ -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"""
-
- {_topbar(project_id, project_nav)}
-
- Проект не готов к HTML5 IDE
- {escape(error or "Snapshot не найден")}
- К списку проектов
-
-
- """
- 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"""
-
- {_topbar(project_id, project_nav)}
-
-
-
-
-
-
HTML5 editor
-
{escape(selected_module.qualified_name if selected_module else project_id)}
-
-
-
- {render_html5_source(selected_module)}
-
-
- Edges{len(snapshot.edges)}
- Diagnostics{len(snapshot.diagnostics)}
- Modules{len(modules)}
-
- {render_html5_object_context(project_id, None, None)}
- Типы
- {''.join(f'- {escape(kind)}{count}
' for kind, count in counts.most_common(10))}
- Результаты
-
- {render_html5_symbols(snapshot, q, project_id)}
-
- {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)}
-
-
-
-
- """
- 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'project: {escape(project_id)}'
- f'snapshot: {escape(snapshot.snapshot_id)}'
- f'nodes: {len(snapshot.nodes)}'
- f'edges: {len(snapshot.edges)}'
- f'diagnostics: {len(snapshot.diagnostics)}'
- 'server-rendered'
- 'client-js: htmx+sse only'
- )
-
-
-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 'Нет результатов
'
- 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"""
-
-
Символ
-
Выберите результат поиска для server-side definition/references по проекту {escape(project_id)}.
-
- """
- 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 'References не найдены
'
- )
- lineage_id = str(getattr(node, "lineage_id", ""))
- return f"""
-
-
Символ · {escape(str(kind))}
-
- {escape(str(name))}
- {escape(str(location))}
-
- {_symbol_summary(refs)}
-
{ref_items}
-
- """
-
-
-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"""
-
-
- {_source_summary(kind or "source", line_count, source_size, location)}
- {escape(source_text)}
-
- """
-
-
-def _source_summary(kind: str, line_count: int, source_size: int, location: str) -> str:
- return f"""
-
- {escape(kind)} · {escape(str(line_count))} lines · {escape(str(source_size))} chars · {escape(location)}
-
- """
-
-
def _page(title: str, body: str) -> str:
return f"""
@@ -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''
- f'{escape(str(name))}{escape(kind_value)}'
- )
-
-
-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"""
-
- {escape(str(name))}
- {escape(kind_value)}
- {escape(str(location))}
- """
-
-
-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"""
-
- {escape(str(len(refs)))} references · {escape(direction_text)} · {escape(kind_text)}
-
- """
-
-
-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"""
- Source
- """
- if source_lineage
- else ""
- )
- return f"""
-
- {escape(label or kind)}
- {escape(direction)} · {escape(kind)}
- {escape(place or "source unavailable")}
- {source_link}
-
- """
-
-
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}
diff --git a/services/api-server/src/api_server/html5_editor.py b/services/api-server/src/api_server/html5_editor.py
new file mode 100644
index 0000000..773f431
--- /dev/null
+++ b/services/api-server/src/api_server/html5_editor.py
@@ -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"""
+
+ {_topbar(project_id, project_nav)}
+
+ Проект не готов к HTML5 IDE
+ {escape(error or "Snapshot не найден")}
+ К списку проектов
+
+
+ """
+ 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"""
+
+ {_topbar(project_id, project_nav)}
+
+
+
+
+
+
HTML5 editor
+
{escape(selected_module.qualified_name if selected_module else project_id)}
+
+
+
+ {render_html5_source(selected_module)}
+
+
+
+
+
+ """
+ return _page(f"SFERA HTML5 - {project_id}", content)
+
+def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str:
+ return (
+ f'project: {escape(project_id)}'
+ f'snapshot: {escape(snapshot.snapshot_id)}'
+ f'nodes: {len(snapshot.nodes)}'
+ f'edges: {len(snapshot.edges)}'
+ f'diagnostics: {len(snapshot.diagnostics)}'
+ 'server-rendered'
+ 'client-js: htmx+sse only'
+ )
+
+
+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 'Нет результатов
'
+ 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"""
+
+
Символ
+
Выберите результат поиска для server-side definition/references по проекту {escape(project_id)}.
+
+ """
+ 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 'References не найдены
'
+ )
+ lineage_id = str(getattr(node, "lineage_id", ""))
+ return f"""
+
+
Символ · {escape(str(kind))}
+
+ {escape(str(name))}
+ {escape(str(location))}
+
+ {_symbol_summary(refs)}
+
{ref_items}
+
+ """
+
+
+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"""
+
+
+ {_source_summary(kind or "source", line_count, source_size, location)}
+ {escape(source_text)}
+
+ """
+
+
+def _source_summary(kind: str, line_count: int, source_size: int, location: str) -> str:
+ return f"""
+
+ {escape(kind)} · {escape(str(line_count))} lines · {escape(str(source_size))} chars · {escape(location)}
+
+ """
+
+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''
+ f'{escape(str(name))}{escape(kind_value)}'
+ )
+
+
+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"""
+
+ {escape(str(name))}
+ {escape(kind_value)}
+ {escape(str(location))}
+ """
+
+
+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"""
+
+ {escape(str(len(refs)))} references · {escape(direction_text)} · {escape(kind_text)}
+
+ """
+
+
+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"""
+ Source
+ """
+ if source_lineage
+ else ""
+ )
+ return f"""
+
+ {escape(label or kind)}
+ {escape(direction)} · {escape(kind)}
+ {escape(place or "source unavailable")}
+ {source_link}
+
+ """
+
+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// Сервер уже отрисовал контекст, дерево, поиск и метрики."
diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py
index 3638438..64bd8b3 100644
--- a/services/api-server/src/api_server/main.py
+++ b/services/api-server/src/api_server/main.py
@@ -38,7 +38,6 @@ from neo4j import AsyncGraphDatabase
from pydantic import BaseModel, Field
from api_server.html5 import (
- render_html5_editor,
render_html5_flowchart,
render_html5_index,
render_html5_object_context,
@@ -46,10 +45,6 @@ from api_server.html5 import (
render_html5_project_rows,
render_html5_project_report,
render_html5_review,
- render_html5_symbol_detail,
- render_html5_source,
- render_html5_status,
- render_html5_symbols,
)
from api_server.html5_authoring import (
render_html5_authoring_apply_result,
@@ -61,6 +56,13 @@ from api_server.html5_authoring import (
render_html5_metadata_apply_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 (
render_html5_operation_detail,
render_html5_operation_rows,