added TipTap

This commit is contained in:
2026-09-09 13:29:39 +03:00
parent df72dc4b8e
commit 959813c11d
6 changed files with 491 additions and 25 deletions
+33 -4
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import re
from typing import Any
from app.services.espd.html_util import html_to_plain
@@ -34,14 +35,42 @@ def resolve_doc_html_map(
return out
def _section_plain(html: str, *title_parts: str) -> str:
"""Extract plain text of first h2 whose title contains any of title_parts."""
if not html:
return ""
parts = re.split(r"(?i)(<h2\b[^>]*>.*?</h2>)", html)
i = 1
while i < len(parts):
heading = re.sub(r"<[^>]+>", "", parts[i])
body = parts[i + 1] if i + 1 < len(parts) else ""
lower = heading.lower()
if any(p.lower() in lower for p in title_parts):
return html_to_plain(body)
i += 2
return ""
def content_extra_context(doc_type: DocumentType, html_or_plain: str) -> dict[str, str]:
plain = html_or_plain or ""
if "<" in plain:
plain = html_to_plain(plain)
extra = {"DOC_CONTENT": plain}
html = html_or_plain or ""
plain = html_to_plain(html) if "<" in html else html
extra: dict[str, str] = {"DOC_CONTENT": plain}
key = CONTENT_CONTEXT_KEYS.get(doc_type)
if key:
extra[key] = plain
# Sectional placeholders used by existing rp.docx / pmi.docx templates
if doc_type == DocumentType.RP:
purpose = _section_plain(html, "назначение")
conditions = _section_plain(html, "условия выполнения", "условия")
extra["PURPOSE"] = purpose or plain
extra["EXECUTION_CONDITIONS"] = conditions or ""
elif doc_type == DocumentType.PMI:
obj = _section_plain(html, "объект")
methods = _section_plain(html, "методы")
extra["TEST_OBJECT"] = obj or plain
extra["TEST_METHODS"] = methods or ""
return extra
+110
View File
@@ -0,0 +1,110 @@
"""Default HTML skeletons for ESPD rich-text editors (visible outlines)."""
from __future__ import annotations
from app.services.espd.models import DocumentType
# Placeholder paragraph shown under each heading so the outline is obvious in TipTap.
_HINT = "<p><em>Заполните раздел…</em></p>"
def _h2(title: str) -> str:
return f"<h2>{title}</h2>{_HINT}"
def _h3(title: str) -> str:
return f"<h3>{title}</h3>{_HINT}"
SKELETONS: dict[str, str] = {
DocumentType.RO.value: "\n".join(
[
_h2("1. Назначение программы"),
_h3("1.1. Функциональное назначение"),
_h3("1.2. Эксплуатационное назначение"),
_h2("2. Условия выполнения программы"),
_h3("2.1. Требования к техническим средствам"),
_h3("2.2. Требования к программным средствам"),
_h3("2.3. Требования к персоналу"),
_h2("3. Установка программы"),
_h3("3.1. Состав дистрибутива"),
_h3("3.2. Порядок установки"),
_h2("4. Работа с программой"),
_h3("4.1. Запуск программы"),
_h3("4.2. Основные операции"),
_h3("4.3. Завершение работы"),
_h2("5. Сообщения оператору"),
]
),
DocumentType.RP.value: "\n".join(
[
_h2("1. Назначение программы"),
_h2("2. Условия выполнения программы"),
_h2("3. Характеристики программы"),
_h2("4. Обращение к программе"),
_h2("5. Входные и выходные данные"),
_h2("6. Сообщения"),
]
),
DocumentType.RSP.value: "\n".join(
[
_h2("1. Общие сведения о программе"),
_h2("2. Структура программы"),
_h2("3. Настройка программы"),
_h2("4. Проверка программы"),
_h2("5. Дополнительные возможности"),
_h2("6. Сообщения системному программисту"),
]
),
DocumentType.PMI.value: "\n".join(
[
_h2("1. Объект испытаний"),
_h2("2. Цель испытаний"),
_h2("3. Требования к программе"),
_h2("4. Требования к программной документации"),
_h2("5. Средства и порядок испытаний"),
_h2("6. Методы испытаний"),
]
),
DocumentType.TEXT.value: "\n".join(
[
_h2("1. Общие сведения"),
"<p><em>Краткое описание состава текста программы.</em></p>",
_h2("2. Текст программы"),
"<pre><code>// Вставьте листинг или фрагменты исходного кода…\n</code></pre>",
_h2("3. Примечания"),
]
),
DocumentType.IMP.value: "\n".join(
[
_h2("1. Перечень исполняемых модулей"),
"<p><em>Таблица файлов формируется из загруженного ZIP при генерации DOCX. "
"Здесь можно указать пояснения к составу поставки.</em></p>",
_h2("2. Инструкция по установке / размещению"),
_h2("3. Проверка контрольных сумм"),
"<p><em>Контрольная сумма ZIP подставляется автоматически при генерации.</em></p>",
]
),
}
def get_skeleton(doc_type: str | DocumentType) -> str:
key = doc_type.value if isinstance(doc_type, DocumentType) else str(doc_type)
return SKELETONS.get(key, "")
def all_skeletons() -> dict[str, str]:
return dict(SKELETONS)
def is_blank_html(html: str | None) -> bool:
"""True if editor content is empty / only whitespace or empty tags."""
if not html or not str(html).strip():
return True
import re
from html import unescape
text = re.sub(r"<[^>]+>", " ", str(html))
text = unescape(text)
text = re.sub(r"\s+", " ", text).strip()
return text == ""