"""Discover Ext/Help/ru.html pages in a 1C configuration dump."""
from __future__ import annotations
import re
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
from export1c_help.types_map import DIR_TO_LINK_TYPE
_SYN_RE = re.compile(
r"\s*\s*ru\s*(.*?)",
re.DOTALL,
)
@dataclass(frozen=True)
class HelpPage:
"""One Help/ru.html page."""
path: Path
src_root: Path
type_dir: str
object_name: str
form_name: str | None
meta_key: str
synonym: str
link_aliases: tuple[str, ...] = ()
@property
def is_form(self) -> bool:
return self.form_name is not None
@property
def rel_posix(self) -> str:
return self.path.relative_to(self.src_root).as_posix()
def _xml_unescape(s: str) -> str:
return (
s.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
.replace(""", '"')
.replace("'", "'")
)
def _read_ru_synonym(xml_path: Path) -> str | None:
if not xml_path.is_file():
return None
try:
text = xml_path.read_text(encoding="utf-8-sig", errors="ignore")
except OSError:
return None
m = _SYN_RE.search(text)
if m:
return _xml_unescape(m.group(1).strip())
try:
root = ET.parse(xml_path).getroot()
except ET.ParseError:
return None
for el in root.iter():
if el.tag.endswith("content") and el.text and el.text.strip():
return el.text.strip()
return None
def _subsystem_chain(parts: tuple[str, ...]) -> list[str] | None:
"""
From relative parts ending with Ext/Help/ru.html build subsystem name chain.
Subsystems/A/Ext/Help/ru.html → [A]
Subsystems/A/Subsystems/B/Ext/Help/ru.html → [A, B]
"""
if parts[0] != "Subsystems":
return None
body = parts[1:-3] # drop Ext/Help/ru.html
names: list[str] = []
i = 0
while i < len(body):
if i == 0:
names.append(body[i])
i += 1
continue
if body[i] == "Subsystems" and i + 1 < len(body):
names.append(body[i + 1])
i += 2
continue
return None
return names or None
def _subsystem_xml(src_root: Path, chain: list[str]) -> Path:
# Subsystems/A.xml or Subsystems/A/Subsystems/B.xml
path = src_root / "Subsystems"
for idx, name in enumerate(chain):
if idx == 0:
candidate = path / f"{name}.xml"
path = path / name
else:
candidate = path / "Subsystems" / f"{name}.xml"
path = path / "Subsystems" / name
return candidate
def _parse_help_path(src_root: Path, html_path: Path) -> HelpPage | None:
try:
rel = html_path.relative_to(src_root)
except ValueError:
return None
parts = rel.parts
if len(parts) < 4 or parts[-1] != "ru.html" or parts[-2] != "Help" or parts[-3] != "Ext":
return None
type_dir = parts[0]
link_type = DIR_TO_LINK_TYPE.get(type_dir)
if not link_type:
return None
form_name: str | None = None
aliases: list[str] = []
if type_dir == "Subsystems":
chain = _subsystem_chain(parts)
if not chain:
return None
object_name = chain[-1]
meta_key = "Subsystem." + ".Subsystem.".join(chain)
# 1C help links usually use the leaf name only
aliases.append(f"Subsystem.{object_name}")
syn = _read_ru_synonym(_subsystem_xml(src_root, chain)) or object_name
return HelpPage(
path=html_path,
src_root=src_root,
type_dir=type_dir,
object_name=object_name,
form_name=None,
meta_key=meta_key,
synonym=syn,
link_aliases=tuple(aliases),
)
if type_dir == "CommonForms":
object_name = parts[1]
meta_key = f"CommonForm.{object_name}"
syn = _read_ru_synonym(src_root / "CommonForms" / f"{object_name}.xml") or object_name
return HelpPage(
path=html_path,
src_root=src_root,
type_dir=type_dir,
object_name=object_name,
form_name=None,
meta_key=meta_key,
synonym=syn,
)
if "Forms" in parts:
try:
forms_idx = parts.index("Forms")
except ValueError:
return None
object_name = parts[1]
form_name = parts[forms_idx + 1]
meta_key = f"{link_type}.{object_name}.Form.{form_name}"
form_xml = src_root / type_dir / object_name / "Forms" / f"{form_name}.xml"
syn = _read_ru_synonym(form_xml) or form_name
obj_syn = _read_ru_synonym(src_root / type_dir / f"{object_name}.xml")
if obj_syn:
syn = f"{obj_syn} ({syn})"
return HelpPage(
path=html_path,
src_root=src_root,
type_dir=type_dir,
object_name=object_name,
form_name=form_name,
meta_key=meta_key,
synonym=syn,
)
object_name = parts[1]
meta_key = f"{link_type}.{object_name}"
syn = _read_ru_synonym(src_root / type_dir / f"{object_name}.xml") or object_name
return HelpPage(
path=html_path,
src_root=src_root,
type_dir=type_dir,
object_name=object_name,
form_name=None,
meta_key=meta_key,
synonym=syn,
)
def discover_help_pages(
src_root: Path,
*,
include_forms: bool = True,
) -> list[HelpPage]:
"""Find all Help/ru.html under src_root."""
src_root = src_root.resolve()
pages: list[HelpPage] = []
seen_meta: set[str] = set()
for html in sorted(src_root.rglob("Ext/Help/ru.html")):
page = _parse_help_path(src_root, html)
if page is None:
continue
if page.is_form and not include_forms:
continue
if page.meta_key in seen_meta:
# should not happen; keep first
continue
seen_meta.add(page.meta_key)
pages.append(page)
return pages
def build_link_title_map(pages: list[HelpPage], titles: dict[str, str]) -> dict[str, str]:
"""
meta_key / alias → wiki title for href rewrite.
Leaf aliases (Subsystem.X) are registered only when unique.
"""
result: dict[str, str] = {}
alias_owners: dict[str, list[str]] = {}
for p in pages:
result[p.meta_key] = titles[p.meta_key]
for alias in p.link_aliases:
alias_owners.setdefault(alias, []).append(p.meta_key)
for alias, owners in alias_owners.items():
if len(owners) == 1:
result[alias] = titles[owners[0]]
return result