#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ convert_sdk_docs.py — Экспорт справки KOMPAS SDK (Help & Manual WebHelp) в единый KOMPAS_SDK_ru-RU.md. Обрабатывает все HTML-страницы из docs/KOMPAS_SDK_ru-RU/, извлекая: - Заголовок, хлебные крошки, тело топика - Информативные изображения (сохраняются как base64 data-URI) - Ссылки между страницами (преобразуются в якоря #page-N) Результат: docs/KOMPAS_SDK_ru-RU.md — один большой Markdown-файл. Изображения сохраняются ТОЛЬКО информативные (не UI/навигационные). """ import os import sys import re import json import base64 import html as hmod from html.parser import HTMLParser from pathlib import Path # ─── Настройки ─────────────────────────────────────────────────────────────── HERE = Path(__file__).resolve().parent.parent DOC_DIR = HERE / "docs" / "KOMPAS_SDK_ru-RU" INDEX_PATH = HERE / "docs" / "kompas_sdk_index.tsv" OUTPUT_MD = HERE / "docs" / "KOMPAS_SDK_ru-RU.md" # Изображения, которые НЕ являются информативными (UI-элементы навигации и т.п.) UI_IMAGE_PREFIXES = [ # Навигация / UI "leftright", "toc", "mob_", "TextPlus", "TextMinus", "TextNormal", "ZoomIn", "ZoomClose", "spacer.gif", "bg.png", "page-bg.png", "blackpaisley.jpeg", "banner_company", "kompas_logo_circle", "kompas-logo", "internet.svg", "icon-new-window", "icon-open-file", "warn", "note", "closehelp", "cicon_loadindex_ani", "kw-page", "social_mail", "_a", "_praezession", "kbd.png", "page-icon", "logo-image-desk", "logo-image-mobile", "close_data", "open_data", ] # ─── Поиск информативных изображений ──────────────────────────────────────── def is_informative_image(src: str) -> bool: """Возвращает True, если изображение информативное (не UI).""" basename = os.path.basename(src).lower() for prefix in UI_IMAGE_PREFIXES: if prefix.lower() in basename: return False # Также пропускаем tiny-изображения (< 500 байт) — обычно это spacer/иконки img_path = DOC_DIR / src if img_path.exists() and img_path.stat().st_size < 500: return False return True # ─── Парсер для извлечения тела топика ────────────────────────────────────── class TopicExtractor(HTMLParser): """Извлекает текст из
title = "" for m in re.finditer(r'
]*>(.*?)
', html_content, re.DOTALL): title = re.sub(r'<[^>]+>', '', m.group(1)).strip() title = hmod.unescape(title) break # Breadcrumbs из]*>(.*?)
', html_content, re.DOTALL, ) if bc_match: bc_html = bc_match.group(1) for a_m in re.finditer(r']+href="([^"]*)"[^>]*>(.*?)', bc_html, re.DOTALL): href = a_m.group(1) text = re.sub(r'<[^>]+>', '', a_m.group(2)).strip() text = hmod.unescape(text) if text: breadcrumbs.append((text, os.path.basename(href))) return title, breadcrumbs # ─── Преобразование ссылок в якоря ────────────────────────────────────────── def convert_links(md_lines: list[str], page_index: int) -> list[str]: """Преобразует [текст](file.html) → [текст](#page-{index}) в Markdown.""" result = [] for line in md_lines: # Преобразуем ссылки на другие HTML-страницы в якоря def replace_link(m): href = m.group(1) text = m.group(2) fname = os.path.basename(href).replace(".html", "") return f"[{text}](#page-{fname})" line = re.sub(r'\[([^\]]+)\]\(([^)]+\.html[^)]*)\)', replace_link, line) result.append(line) return result # ─── Преобразование HTML-сущностей в читаемый текст ────────────────────────── def unescape_html(s: str) -> str: """Полностью раскрывает HTML-сущности.""" # Сначала и неразрывные пробелы s = s.replace(" ", " ") s = s.replace(" ", " ") s = s.replace("", "") s = s.replace("—", "—") s = s.replace("–", "–") s = s.replace("«", "«") s = s.replace("»", "»") s = s.replace(""", '"') s = s.replace("'", "'") s = s.replace("'", "'") s = s.replace("<", "<") s = s = s.replace(">", ">") return hmod.unescape(s) # ─── Основной парсер одной страницы ────────────────────────────────────────── def parse_page(html_content: str, page_index: int) -> dict | None: """ Парсит одну HTML-страницу и возвращает dict с полями: title, breadcrumbs, body_lines (список строк Markdown), images (list of data-uri), href_base (filename без .html). Возвращает None, если страница не содержит полезного контента. """ # Извлекаем заголовок и хлебные крошки title, breadcrumbs = extract_title_and_breadcrumbs(html_content) # Извлекаем тело топика extractor = TopicExtractor() extractor.feed(html_content) raw_text = "".join(extractor.out) # Очищаем текст body_lines = clean_text(raw_text) # Если нет полезного контента — пропускаем if not body_lines or len(body_lines) <= 2: return None # Извлекаем изображения images = extract_images(html_content) # Преобразуем ссылки body_lines = convert_links(body_lines, page_index) # Определяем basename для якорей href_base_match = re.search(r' list[dict]: """Строит индекс из zoom_pageinfo.js.""" if not INDEX_PATH.exists(): print("Индекс отсутствует — строю...", file=sys.stderr) # Парсим zoom_pageinfo.js напрямую pageinfo_path = DOC_DIR / "zoom_pageinfo.js" with open(pageinfo_path, "r", encoding="utf-8") as f: text = f.read() marker = "pagedata = " pos = text.find(marker) if pos < 0: sys.exit(f"Не найден pagedata в {pageinfo_path}") payload = text[pos + len(marker):].strip().rstrip(";").strip() data = json.loads(payload) rows = [] for entry in data: if not entry or not entry[0] or entry[0] == 0: continue fname = entry[0].lstrip("./").strip() title = hmod.unescape((entry[1] or "").strip()) desc = hmod.unescape((entry[2] or "").strip())[:300] rows.append({"file": fname, "title": title, "desc": desc}) with open(INDEX_PATH, "w", encoding="utf-8", newline="\n") as f: for r in rows: f.write(f"{r['file']}\t{r['title']}\t{r['desc']}\n") # Читаем индекс pages = [] with open(INDEX_PATH, "r", encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) >= 1: pages.append({"file": parts[0], "title": parts[1] if len(parts) > 1 else "", "desc": parts[2] if len(parts) > 2 else ""}) return pages def main(): print(f"Каталог справки: {DOC_DIR}") print(f"Выходной файл: {OUTPUT_MD}") # Строим индекс если нужно pages = build_index() print(f"Страниц в индексе: {len(pages)}") # Заголовок документа md_parts = [] md_parts.append("# KOMPAS SDK — Справочная система КОМПАС-3D\n") md_parts.append(f"*Автоматически экспортировано из Help & Manual WebHelp ({len(pages)} страниц).*\n") # Оглавление (только страницы с заголовками) toc_entries = [] processed_count = 0 skipped_count = 0 image_count = 0 for i, page_info in enumerate(pages): fname = page_info["file"] fpath = DOC_DIR / fname if not fpath.exists(): skipped_count += 1 continue try: html_content = fpath.read_text(encoding="utf-8-sig") except Exception as e: print(f" ⚠ Не удалось прочитать {fname}: {e}", file=sys.stderr) skipped_count += 1 continue parsed = parse_page(html_content, i) if parsed is None: skipped_count += 1 continue processed_count += 1 href_base = parsed["href_base"] or f"page-{i}" # Собираем заголовок для TOC title = parsed["title"] or fname.replace(".html", "").replace("_", " ") toc_entries.append(f" - [{title}](#{href_base})") # Разделитель страницы md_parts.append(f"\n---\n\n") md_parts.append(f"## {title}\n") md_parts.append(f"\n") # Хлебные крошки if parsed["breadcrumbs"]: bc_text = " > ".join(t for t, _ in parsed["breadcrumbs"]) md_parts.append(f"*{bc_text}*\n") # Изображения (вставляются перед телом или в нужных местах) if parsed["images"]: for data_uri, alt in parsed["images"]: alt_clean = alt.replace('"', "'").replace("'", "\\'") md_parts.append(f"\n") image_count += 1 # Тело страницы for line in parsed["body_lines"]: line_unescaped = unescape_html(line) # Убираем лишние пробелы, но сохраняем структуру md_parts.append(f"{line_unescaped}\n") # Добавляем оглавление в начало (после заголовка) final_md = md_parts[:2] # Заголовок + подзаголовок final_md.append("## Оглавление\n") final_md.extend(toc_entries) final_md.extend(md_parts[2:]) # Записываем файл output_text = "\n".join(final_md) OUTPUT_MD.write_text(output_text, encoding="utf-8") print(f"\n✅ Готово!") print(f" Обработано страниц: {processed_count}") print(f" Пропущено (пустые/ошибки): {skipped_count}") print(f" Сохранено изображений: {image_count}") file_size = OUTPUT_MD.stat().st_size print(f" Размер файла: {file_size / 1024 / 1024:.1f} MB") if __name__ == "__main__": main()