#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ convert_sdk_to_md.py — Экспорт справки KOMPAS SDK (Help & Manual WebHelp) в отдельные Markdown-файлы. Каждая HTML-страница из docs/KOMPAS_SDK_ru-RU/ конвертируется в отдельный .md файл в docs/KOMPAS_SDK_ru-RU/md/. Информативные изображения сохраняются как base64 data-URI прямо в Markdown (inline). Изображения, которые НЕ являются информативными (UI, навигация и т.п.), пропускаются. """ import os import sys import re import json import base64 import html as hmod from html.parser import HTMLParser from pathlib import Path # ─── Настройки ─────────────────────────────────────────────────────────────── for _s in (sys.stdout, sys.stderr): try: _s.reconfigure(encoding="utf-8") except Exception: pass HERE = Path(__file__).resolve().parent.parent DOC_DIR = HERE / "docs" / "KOMPAS_SDK_ru-RU" INDEX_PATH = HERE / "docs" / "kompas_sdk_index.tsv" OUTPUT_DIR = DOC_DIR / "md" # docs/KOMPAS_SDK_ru-RU/md/ # Изображения, которые НЕ являются информативными (UI-элементы навигации и т.п.) UI_IMAGE_PREFIXES = [ "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: basename = os.path.basename(src).lower() for prefix in UI_IMAGE_PREFIXES: if prefix.lower() in basename: return False img_path = DOC_DIR / src if img_path.exists() and img_path.stat().st_size < 500: return False return True # ─── Парсер тела топика ───────────────────────────────────────────────────── class TopicExtractor(HTMLParser): BLOCK_TAGS = {"p", "div", "br", "tr", "li", "h1", "h2", "h3", "h4", "table", "pre", "td", "th"} def __init__(self): super().__init__(convert_charrefs=True) self.depth = 0 self.capturing = False self.skip = 0 self.out = [] def handle_starttag(self, tag, attrs): attrd = dict(attrs) if not self.capturing and attrd.get("id") == "topicbody": self.capturing = True self.depth = 1 return if not self.capturing: return if tag in ("script", "style"): self.skip += 1 if tag in self.BLOCK_TAGS: self.out.append("\n") if tag != "br": self.depth += 1 def handle_endtag(self, tag): if not self.capturing: return if tag in ("script", "style") and self.skip: self.skip -= 1 if tag != "br": self.depth -= 1 if self.depth <= 0: self.capturing = False def handle_data(self, data): if self.capturing and not self.skip: self.out.append(data) # ─── Шум и подвал ──────────────────────────────────────────────────────────── NOISE_WORDS = { "please enable javascript to view this site.", "scroll", "предыдущий", "вверх", "следующий", "содержание", "содержане", "указатель", "поиск", "подразделы:", "(отсутствуют)", "поделиться", } FOOTER_RE = re.compile(r"©?\s*ООО\s*«?АСКОН") def clean_text(raw: str) -> list[str]: cleaned = [] for line in raw.splitlines(): s = re.sub(r"[ \t]+", " ", line).strip() if not s: continue if s.lower() in NOISE_WORDS: continue if FOOTER_RE.search(s): break cleaned.append(s) return cleaned # ─── Извлечение изображений как base64 data-URI ────────────────────────────── def extract_images(html_content: str) -> list[tuple[str, str]]: images = [] for m in re.finditer(r']+>', html_content): tag = m.group() src_m = re.search(r'''src=["']([^"']+)["']''', tag) alt_m = re.search(r'''alt=["']([^"']*)["']''', tag) if not src_m: continue src = src_m.group(1) alt = alt_m.group(1) if alt_m else "" if not is_informative_image(src): continue img_path = DOC_DIR / src if not img_path.exists(): continue ext = os.path.splitext(src)[1].lower().lstrip(".") mime_map = { "jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "gif": "image/gif", "svg": "image/svg+xml", "webp": "image/webp", "bmp": "image/bmp", } mime = mime_map.get(ext, f"image/{ext}") data = base64.b64encode(img_path.read_bytes()).decode("ascii") data_uri = f"data:{mime};base64,{data}" images.append((data_uri, alt)) return images # ─── Заголовок и хлебные крошки ────────────────────────────────────────────── def extract_title_and_breadcrumbs(html_content: str) -> tuple[str, list[tuple[str, str]]]: title = "" for m in re.finditer(r'(.*?)', html_content): title = hmod.unescape(m.group(1)).strip() break if not 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 = [] bc_match = re.search( r']*>(.*?)

', 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 # ─── Раскрытие HTML-сущностей ──────────────────────────────────────────────── def unescape_html(s: str) -> str: 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.replace(">", ">") return hmod.unescape(s) # ─── Извлечение ссылок из HTML-тела топика ────────────────────────────────── def extract_links_from_html(html_content: str) -> list[tuple[str, str]]: """ Находит все в topicbody и возвращает список [(anchor_text, href), ...] для последующей подстановки. anchor_text — текст ссылки без HTML-тегов. """ # Ищем всё содержимое после id="topicbody" до
body_match = re.search( r'id="topicbody">(.*?)(?:)', html_content, re.DOTALL) if not body_match: return [] body_html = body_match.group(1) links = [] for a_m in re.finditer(r']+href=["\']([^"\']+)["\'][^>]*>(.*?)', body_html, re.DOTALL): href = a_m.group(1) if not href.endswith('.html'): continue text = re.sub(r'<[^>]+>', '', a_m.group(2)).strip() text = hmod.unescape(text) if text: links.append((text, href)) return links # ─── Преобразование ссылок в тексте Markdown ──────────────────────────────── def convert_links_in_text(text: str, links: list[tuple[str, str]]) -> str: """ Заменяет [текст](file.html) на [текст](./file.md). Также заменяет plain-text ссылки (просто текст, совпадающий с anchor). """ def replace_link(m): link_text = m.group(1) href = m.group(2) fname = os.path.basename(href).replace(".html", ".md") return f"[{link_text}](./{fname})" text = re.sub(r'\[([^\]]+)\]\(([^)]+\.html[^)]*)\)', replace_link, text) # Заменяем "текст (file.html)" → "[текст](./file.md)" for link_text, href in links: fname = os.path.basename(href).replace(".html", ".md") old = f"{link_text} ({href})" new = f"[{link_text}](./{fname})" if old in text: text = text.replace(old, new) # Заменяем plain-text ссылки: текст, стоящий рядом со ссылкой, # но без скобок — просто "Текст" на строке, где есть иная ссылка. # Это покрывает случаи вида "...свойству IApplication::Visible значение TRUE." # где IApplication::Visible — ссылка. for link_text, href in links: fname = os.path.basename(href).replace(".html", ".md") try: # Ищем точное совпадение текста ссылки (не внутри [[...]] или [..](..)) # Пропускаем, если текст уже в Markdown-формате [text](url) pattern = r'(? dict | None: title, breadcrumbs = extract_title_and_breadcrumbs(html_content) # Извлекаем ссылки из HTML ДО очистки от тегов links = extract_links_from_html(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) # Определяем basename для имени выходного файла href_base_match = re.search(r' list[dict]: if INDEX_PATH.exists(): 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 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") return rows # ─── Генерация YAML-фронтматтера ───────────────────────────────────────────── def frontmatter(title: str, breadcrumbs: list[tuple[str, str]], source_file: str) -> str: lines = [f"---", f"title: \"{title}\"", f"source: {source_file}"] if breadcrumbs: lines.append(f"breadcrumbs:") for text, fname in breadcrumbs: lines.append(f' - text: "{text}"') lines.append(f" file: {fname}") lines.append("---") return "\n".join(lines) # ─── Основная функция ──────────────────────────────────────────────────────── def main(): print(f"Справка: {DOC_DIR}") print(f"Выходной каталог: {OUTPUT_DIR}") OUTPUT_DIR.mkdir(parents=True, exist_ok=True) pages = build_index() print(f"Страниц в индексе: {len(pages)}") processed = 0 skipped = 0 total_images = 0 errors = 0 pages_with_links = 0 for i, page_info in enumerate(pages): fname = page_info["file"] fpath = DOC_DIR / fname out_path = OUTPUT_DIR / fname.replace(".html", ".md") if not fpath.exists(): skipped += 1 continue try: html_content = fpath.read_text(encoding="utf-8-sig") except Exception as e: print(f" ⚠ Ошибка чтения {fname}: {e}", file=sys.stderr) errors += 1 skipped += 1 continue try: parsed = parse_page(html_content) except Exception as e: print(f" ⚠ Parse error {fname}: {e}", file=sys.stderr) errors += 1 skipped += 1 continue if parsed is None: skipped += 1 continue if parsed["links"]: pages_with_links += 1 # Генерируем MD-контент md_parts = [] md_parts.append(frontmatter( parsed["title"], parsed["breadcrumbs"], fname)) md_parts.append("") # Хлебные крошки (как текст) if parsed["breadcrumbs"]: bc_text = " > ".join(t for t, _ in parsed["breadcrumbs"]) md_parts.append(f"*{bc_text}*") md_parts.append("") # Изображения if parsed["images"]: for data_uri, alt in parsed["images"]: alt_clean = alt.replace('"', "'").replace("'", "\\'") md_parts.append(f"![{alt_clean}]({data_uri})") md_parts.append("") total_images += 1 # Тело страницы md_parts.extend(parsed["body_lines"]) md_parts.append("") # trailing newline output_text = "\n".join(md_parts) out_path.write_text(output_text, encoding="utf-8") processed += 1 print(f"\nDone!") print(f" Processed pages: {processed}") print(f" Skipped (empty/errors): {skipped}") print(f" Read errors: {errors}") print(f" Images saved: {total_images}") print(f" Pages with links: {pages_with_links}") if __name__ == "__main__": main()