Files
export_1c_help/export1c_help/export.py
T
2026-07-23 15:30:43 +03:00

392 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Build markdown wiki pages from discovered Help HTML."""
from __future__ import annotations
import json
import re
import shutil
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from export1c_help.__version__ import __version__
from export1c_help.config_version import read_config_name, read_config_version
from export1c_help.convert import default_link_rewrite, extract_h1, html_to_markdown
from export1c_help.discover import HelpPage, build_link_title_map, discover_help_pages
from export1c_help.lifecycle import (
LifecycleResult,
PageRecord,
content_hash,
load_prev_manifest,
merge_lifecycle,
prev_records,
)
from export1c_help.naming import assign_titles
from export1c_help.types_map import DIR_TO_RU
@dataclass
class ExportStats:
pages_total: int = 0
pages_written: int = 0
pages_skipped_empty: int = 0
images_copied: int = 0
created_count: int = 0
edited_count: int = 0
unchanged_count: int = 0
deleted_count: int = 0
config_version: str = "unknown"
def _is_effectively_empty(md: str) -> bool:
plain = re_sub_md(md)
return len(plain) < 20
def re_sub_md(md: str) -> str:
t = re.sub(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]", r"\1", md)
t = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", t)
t = re.sub(r"[#*_`|-]", " ", t)
t = re.sub(r"\s+", " ", t).strip()
return t
def _page_footer(rec: PageRecord, *, config_version: str) -> str:
lines = [
"",
"---",
"",
f"*Конфигурация выгрузки: **{config_version}*** ",
f"*Страница создана в: **{rec.created_in}** · изменена в: **{rec.edited_in}*** ",
f"*Мета: `{rec.meta_key}` · файл: `{rec.source}`*",
"",
]
return "\n".join(lines)
def build_home(
pages: list[HelpPage],
titles: dict[str, str],
*,
source_label: str,
config_version: str,
config_name: str | None,
life: LifecycleResult,
) -> str:
by_type: dict[str, list[HelpPage]] = {}
for p in pages:
by_type.setdefault(p.type_dir, []).append(p)
name_line = f" (`{config_name}`)" if config_name else ""
lines = [
"# Справка конфигурации",
"",
"Автогенерация из встроенной справки 1С (`Ext/Help/ru.html`).",
"",
f"- **Версия конфигурации (эта выгрузка):** `{config_version}`{name_line}",
f"- Источник: `{source_label}`",
f"- Страниц: **{len(pages)}** "
f"(+{life.created_count} / ~{life.edited_count} / ={life.unchanged_count} / -{life.deleted_count})",
f"- Удалённых (накопительно): **{len(life.deleted)}** — см. [[History]]",
f"- Инструмент: `export_1c_help` v{__version__}",
f"- Дата: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
"",
"## Разделы",
"",
]
for type_dir in sorted(by_type, key=lambda d: DIR_TO_RU.get(d, d)):
label = DIR_TO_RU.get(type_dir, type_dir)
group = sorted(by_type[type_dir], key=lambda p: titles[p.meta_key].casefold())
lines.append(f"### {label} ({len(group)})")
lines.append("")
for p in group:
title = titles[p.meta_key]
lines.append(f"- [[{title}]]")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def build_sidebar(pages: list[HelpPage], titles: dict[str, str]) -> str:
by_type: dict[str, list[HelpPage]] = {}
for p in pages:
by_type.setdefault(p.type_dir, []).append(p)
lines = [
"# Справка",
"",
"[[Home|Оглавление]]",
"[[History|История выгрузок]]",
"",
]
for type_dir in sorted(by_type, key=lambda d: DIR_TO_RU.get(d, d)):
label = DIR_TO_RU.get(type_dir, type_dir)
lines.append(f"### {label}")
lines.append("")
objs = [p for p in by_type[type_dir] if not p.is_form]
objs.sort(key=lambda p: titles[p.meta_key].casefold())
for p in objs[:80]:
lines.append(f"- [[{titles[p.meta_key]}]]")
if len(objs) > 80:
lines.append(f"- … ещё {len(objs) - 80} (см. [[Home]])")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def build_history(
*,
config_version: str,
config_name: str | None,
life: LifecycleResult,
prev_exports: list[dict],
) -> str:
name = f" `{config_name}`" if config_name else ""
lines = [
"# История выгрузок справки",
"",
f"Текущая выгрузка: конфигурация **{config_version}**{name}.",
"",
"## Эта выгрузка",
"",
f"- создано страниц: **{life.created_count}**",
f"- изменено: **{life.edited_count}**",
f"- без изменений: **{life.unchanged_count}**",
f"- удалено в этой версии: **{life.deleted_count}**",
f"- всего удалённых (накопительно): **{len(life.deleted)}**",
"",
]
if life.deleted_count:
lines.append("### Удалено в этой версии")
lines.append("")
for rec in life.deleted:
if rec.deleted_in != config_version:
continue
lines.append(
f"- `{rec.meta_key}` — «{rec.title}» "
f"(создана в {rec.created_in}, последняя правка {rec.edited_in})"
)
lines.append("")
if life.deleted:
lines.append("### Все удалённые страницы")
lines.append("")
lines.append("| Мета | Заголовок | Создана | Изменена | Удалена |")
lines.append("|------|-----------|---------|----------|---------|")
for rec in life.deleted:
lines.append(
f"| `{rec.meta_key}` | {rec.title} | {rec.created_in} | "
f"{rec.edited_in} | {rec.deleted_in or ''} |"
)
lines.append("")
lines.append("## Предыдущие выгрузки")
lines.append("")
if not prev_exports:
lines.append("_Нет сохранённой истории (первая выгрузка с учётом версий)._")
lines.append("")
else:
lines.append("| Версия конфигурации | Дата (UTC) | Страниц | + | ~ | - |")
lines.append("|---------------------|------------|---------|---|---|---|")
for row in prev_exports:
lines.append(
f"| {row.get('config_version', '?')} | {row.get('exported_at', '?')} | "
f"{row.get('pages_written', '?')} | {row.get('created', '?')} | "
f"{row.get('edited', '?')} | {row.get('deleted', '?')} |"
)
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def export_help(
src_root: Path,
out_dir: Path,
*,
include_forms: bool = True,
skip_empty: bool = True,
clean: bool = False,
source_label: str | None = None,
prev_manifest_path: Path | None = None,
config_version: str | None = None,
) -> ExportStats:
"""Convert all help pages into a wiki working tree at out_dir."""
src_root = src_root.resolve()
out_dir = out_dir.resolve()
stats = ExportStats()
cfg_ver = (config_version or read_config_version(src_root)).strip() or "unknown"
cfg_name = read_config_name(src_root)
stats.config_version = cfg_ver
prev_manifest = load_prev_manifest(prev_manifest_path)
if prev_manifest is None and not clean:
prev_manifest = load_prev_manifest(out_dir / "manifest.json")
previous = prev_records(prev_manifest)
pages = discover_help_pages(src_root, include_forms=include_forms)
stats.pages_total = len(pages)
h1_by_meta: dict[str, str | None] = {}
html_cache: dict[str, str] = {}
for p in pages:
html = p.path.read_text(encoding="utf-8-sig", errors="ignore")
html_cache[p.meta_key] = html
h1_by_meta[p.meta_key] = extract_h1(html)
titles, files = assign_titles(pages, h1_by_meta)
link_map = build_link_title_map(pages, titles)
if clean and out_dir.exists():
for child in out_dir.iterdir():
if child.name == ".git":
continue
if child.is_dir():
shutil.rmtree(child)
else:
child.unlink()
out_dir.mkdir(parents=True, exist_ok=True)
def rewrite(href: str, text: str) -> str:
return default_link_rewrite(href, text, link_map)
draft_records: list[PageRecord] = []
body_by_meta: dict[str, str] = {}
for p in pages:
html = html_cache[p.meta_key]
title = titles[p.meta_key]
md = html_to_markdown(html, rewrite_link=rewrite, strip_first_h1=title)
if skip_empty and _is_effectively_empty(md):
stats.pages_skipped_empty += 1
continue
body = md.rstrip() + "\n"
body_by_meta[p.meta_key] = body
draft_records.append(
PageRecord(
meta_key=p.meta_key,
title=title,
filename=files[p.meta_key],
type_dir=p.type_dir,
object=p.object_name,
form=p.form_name,
source=p.rel_posix,
content_hash=content_hash(html),
created_in="",
edited_in="",
)
)
life = merge_lifecycle(
current=draft_records,
previous=previous,
config_version=cfg_ver,
)
stats.created_count = life.created_count
stats.edited_count = life.edited_count
stats.unchanged_count = life.unchanged_count
stats.deleted_count = life.deleted_count
for rec in life.active:
body = body_by_meta[rec.meta_key]
md = body.rstrip() + _page_footer(rec, config_version=cfg_ver)
(out_dir / rec.filename).write_text(md, encoding="utf-8")
stats.pages_written += 1
help_dir = src_root / Path(rec.source).parent
if help_dir.is_dir():
for img in help_dir.iterdir():
if img.suffix.lower() in {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}:
target = out_dir / img.name
if not target.exists():
shutil.copy2(img, target)
stats.images_copied += 1
label = source_label or str(src_root)
written_keys = {r.meta_key for r in life.active}
written_pages = [p for p in pages if p.meta_key in written_keys]
prev_exports: list[dict] = []
if prev_manifest:
prev_exports = list(prev_manifest.get("exports") or [])
# append previous export summary if present at top level
if prev_manifest.get("config_version"):
prev_exports.insert(
0,
{
"config_version": prev_manifest.get("config_version"),
"exported_at": prev_manifest.get("exported_at"),
"pages_written": prev_manifest.get("pages_written"),
"created": prev_manifest.get("created_count"),
"edited": prev_manifest.get("edited_count"),
"deleted": prev_manifest.get("deleted_count"),
},
)
# dedupe consecutive identical
deduped: list[dict] = []
for row in prev_exports:
if deduped and deduped[-1].get("exported_at") == row.get("exported_at"):
continue
deduped.append(row)
prev_exports = deduped[:50]
(out_dir / "Home.md").write_text(
build_home(
written_pages,
titles,
source_label=label,
config_version=cfg_ver,
config_name=cfg_name,
life=life,
),
encoding="utf-8",
)
(out_dir / "_Sidebar.md").write_text(
build_sidebar(written_pages, titles),
encoding="utf-8",
)
(out_dir / "History.md").write_text(
build_history(
config_version=cfg_ver,
config_name=cfg_name,
life=life,
prev_exports=prev_exports,
),
encoding="utf-8",
)
exported_at = datetime.now(timezone.utc).isoformat()
exports = [
{
"config_version": cfg_ver,
"exported_at": exported_at,
"pages_written": stats.pages_written,
"created": stats.created_count,
"edited": stats.edited_count,
"deleted": stats.deleted_count,
"tool_version": __version__,
},
*prev_exports,
][:50]
manifest = {
"tool": "export_1c_help",
"version": __version__,
"config_version": cfg_ver,
"config_name": cfg_name,
"source": label,
"exported_at": exported_at,
"pages_total": stats.pages_total,
"pages_written": stats.pages_written,
"pages_skipped_empty": stats.pages_skipped_empty,
"created_count": stats.created_count,
"edited_count": stats.edited_count,
"unchanged_count": stats.unchanged_count,
"deleted_count": stats.deleted_count,
"exports": exports,
"pages": [r.to_dict() for r in life.active],
"deleted_pages": [r.to_dict() for r in life.deleted],
}
(out_dir / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
return stats