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)} -
- -
- -
- """ - 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""" -
-
-
- {escape(str(name))} - {escape(kind or "source")} -
-
- {_metric("Lines", line_count)} - {_metric("Location", location)} -
-
- {_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""" +
+
+
+ {escape(str(name))} + {escape(kind or "source")} +
+
+ {_metric("Lines", line_count)} + {_metric("Location", location)} +
+
+ {_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,