Files
export_1c_help/export1c_help/convert.py
T
mihailkudravcev aec0407b87 Initial release: export 1C Help to Gitea wiki.
Convert Ext/Help/ru.html to Markdown and push to a wiki git repository.
2026-07-23 14:54:04 +03:00

280 lines
8.5 KiB
Python

"""HTML (1C Help) → Markdown converter (stdlib only)."""
from __future__ import annotations
import re
from html.parser import HTMLParser
from typing import Callable
from urllib.parse import unquote
_BLOCK_TAGS = frozenset(
{"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li", "tr", "blockquote", "pre"}
)
_SKIP_TAGS = frozenset({"script", "style", "head", "meta", "link"})
_HEADER = {"h1": "#", "h2": "##", "h3": "###", "h4": "####", "h5": "#####", "h6": "######"}
LinkRewriter = Callable[[str, str], str]
class _HelpHTMLParser(HTMLParser):
def __init__(self, rewrite_link: LinkRewriter | None = None) -> None:
super().__init__(convert_charrefs=True)
self.rewrite_link = rewrite_link or (lambda href, text: text)
self.parts: list[str] = []
self._skip_depth = 0
self._bold = 0
self._italic = 0
self._code = 0
self._list_stack: list[str] = [] # "ul" | "ol"
self._li_index: list[int] = []
self._link_href: str | None = None
self._link_text: list[str] = []
self._in_anchor_name: str | None = None
self.title: str | None = None
self._pending_break = False
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
tag = tag.lower()
ad = {k.lower(): (v or "") for k, v in attrs}
if tag in _SKIP_TAGS:
self._skip_depth += 1
return
if self._skip_depth:
return
if tag == "br":
self._emit("\n")
return
if tag == "hr":
self._close_inline()
self._emit("\n\n---\n\n")
return
if tag in ("b", "strong"):
self._bold += 1
self._emit("**")
return
if tag in ("i", "em"):
self._italic += 1
self._emit("*")
return
if tag in ("code", "tt"):
self._code += 1
self._emit("`")
return
if tag == "a":
name = ad.get("name") or ad.get("id")
href = ad.get("href", "")
if name and (not href or href.startswith("#")):
# named anchor — keep as HTML comment / empty target for TOC
self._in_anchor_name = name
return
if href.startswith("v8help://"):
return
self._link_href = href
self._link_text = []
return
if tag in _HEADER:
self._close_inline()
self._ensure_blank()
self._emit(f"{_HEADER[tag]} ")
return
if tag == "p":
self._close_inline()
self._ensure_blank()
return
if tag == "ul":
self._close_inline()
self._ensure_blank()
self._list_stack.append("ul")
self._li_index.append(0)
return
if tag == "ol":
self._close_inline()
self._ensure_blank()
self._list_stack.append("ol")
self._li_index.append(0)
return
if tag == "li":
self._close_inline()
self._emit("\n")
depth = max(len(self._list_stack) - 1, 0)
indent = " " * depth
if self._list_stack and self._list_stack[-1] == "ol":
self._li_index[-1] += 1
self._emit(f"{indent}{self._li_index[-1]}. ")
else:
self._emit(f"{indent}- ")
return
if tag in ("table", "tbody", "thead"):
self._close_inline()
self._ensure_blank()
return
if tag == "tr":
self._emit("\n")
return
if tag in ("td", "th"):
self._emit(" | ")
return
def handle_endtag(self, tag: str) -> None:
tag = tag.lower()
if tag in _SKIP_TAGS:
if self._skip_depth:
self._skip_depth -= 1
return
if self._skip_depth:
return
if tag in ("b", "strong") and self._bold:
self._emit("**")
self._bold -= 1
return
if tag in ("i", "em") and self._italic:
self._emit("*")
self._italic -= 1
return
if tag in ("code", "tt") and self._code:
self._emit("`")
self._code -= 1
return
if tag == "a":
if self._in_anchor_name is not None:
self._in_anchor_name = None
return
if self._link_href is None:
return
text = "".join(self._link_text).strip()
href = self._link_href
self._link_href = None
self._link_text = []
if not text and not href:
return
self._emit(self.rewrite_link(href, text or href))
return
if tag in _HEADER:
# capture first h1 as title
# title extracted later from markdown
self._emit("\n\n")
return
if tag in ("p", "div"):
self._emit("\n\n")
return
if tag in ("ul", "ol"):
if self._list_stack:
self._list_stack.pop()
if self._li_index:
self._li_index.pop()
self._emit("\n\n")
return
if tag == "li":
return
def handle_data(self, data: str) -> None:
if self._skip_depth:
return
if self._link_href is not None:
self._link_text.append(data)
return
# collapse whitespace inside blocks but keep intentional spaces
if not data:
return
self._emit(data)
def _emit(self, s: str) -> None:
self.parts.append(s)
def _ensure_blank(self) -> None:
text = "".join(self.parts)
if not text.endswith("\n\n"):
if text.endswith("\n"):
self.parts.append("\n")
elif text:
self.parts.append("\n\n")
def _close_inline(self) -> None:
while self._bold:
self.parts.append("**")
self._bold -= 1
while self._italic:
self.parts.append("*")
self._italic -= 1
while self._code:
self.parts.append("`")
self._code -= 1
def _normalize_md(text: str) -> str:
text = text.replace("\r\n", "\n").replace("\r", "\n")
text = re.sub(r"[ \t]+\n", "\n", text)
text = re.sub(r"\n{3,}", "\n\n", text)
# fix bold/italic glued spaces
text = re.sub(r"\*\*\s+\*\*", "", text)
return text.strip() + "\n"
def extract_h1(html: str) -> str | None:
m = re.search(r"<h1[^>]*>(.*?)</h1>", html, flags=re.IGNORECASE | re.DOTALL)
if not m:
return None
raw = re.sub(r"<[^>]+>", "", m.group(1))
raw = re.sub(r"\s+", " ", raw).strip()
return raw or None
def html_to_markdown(
html: str,
*,
rewrite_link: LinkRewriter | None = None,
strip_first_h1: str | None = None,
) -> str:
"""Convert 1C help HTML to GitHub/Gitea-flavoured Markdown."""
parser = _HelpHTMLParser(rewrite_link=rewrite_link)
parser.feed(html)
parser.close()
md = _normalize_md("".join(parser.parts))
if strip_first_h1:
# remove leading "# Title\n" if it matches page title
esc = re.escape(strip_first_h1.strip())
md = re.sub(rf"^#\s+{esc}\s*\n+", "", md, count=1, flags=re.IGNORECASE)
# drop empty ITS-only stubs that are just TOC junk
md = re.sub(r"\n\|\s*\n", "\n", md)
return _normalize_md(md)
def default_link_rewrite(href: str, text: str, meta_to_title: dict[str, str]) -> str:
"""Rewrite href into markdown / wiki link."""
href = href.strip()
text = text.strip() or href
if href.startswith("#"):
# keep plain text for in-page TOC (anchors rarely useful in wiki MD)
return text
if href.startswith(("http://", "https://", "mailto:")):
return f"[{text}]({href})"
# Catalog.X/Help or Catalog.X.Form.Y/Help[#anchor]
m = re.match(
r"^([A-Za-z]+(?:\.[^/#\s]+)+)/Help(?:#(.*))?$",
unquote(href),
)
if m:
meta_key = m.group(1)
title = meta_to_title.get(meta_key)
if title:
if text == title or text == meta_key:
return f"[[{title}]]"
return f"[[{text}|{title}]]"
return f"{text} (`{meta_key}`)"
# relative image or unknown
if re.search(r"\.(png|jpe?g|gif|webp|bmp)$", href, re.I):
return f"![{text}]({href})"
return text